Merge branch 'release/v1.6.0'

This commit is contained in:
Qiu Yuzhou
2017-09-08 15:19:56 +08:00
25 changed files with 312 additions and 129 deletions

View File

@ -1,6 +1,6 @@
# ShadowsocksX-NG
Current version is 1.5
Current version is 1.5.2
[![Build Status](https://travis-ci.org/shadowsocks/ShadowsocksX-NG.svg?branch=develop)](https://travis-ci.org/shadowsocks/ShadowsocksX-NG)
@ -9,23 +9,23 @@ Next Generation of [ShadowsocksX](https://github.com/shadowsocks/shadowsocks-iOS
## Why?
It's hard to maintain the original implementation as there is too much unused code in it.
It also embeds the ss-local source. It's crazy to maintain dependencies of ss-local.
So it's hard to update the ss-local version.
It also embeds the `ss-local` source. It's crazy to maintain dependencies of `ss-local`.
So it's hard to update the `ss-local` version.
Now I just copied the ss-local from homebrew. Run ss-local executable as a Launch Agent in the background.
Serve PAC js file as a file URL. So there is only some source code related to GUI left.
Now I just copied the `ss-local` from Homebrew. Run `ss-local` executable as a Launch Agent in the background.
Serve PAC JS file as a file URL. So there is only some source code related to GUI left.
Then I will rewrite the GUI code in Swift.
## Requirements
### Running
- Mac OS X 10.11 +
- macOS 10.11+
### Building
- XCode 8.3+
- cocoapod 1.2+
- Xcode 8.3+
- CocoaPods 1.2+
## Download
@ -33,39 +33,39 @@ From [here](https://github.com/shadowsocks/ShadowsocksX-NG/releases/)
## Features
- Use ss-local from shadowsocks-libev 3.0.5
- Could Update PAC by download GFW List from GitHub.
- Show QRCode for current server profile.
- Scan QRCode from screen.
- Uses `ss-local` from shadowsocks-libev 3.0.5
- Could update PAC by download GFW List from GitHub.
- Shows QRCode for current server profile.
- Scans QRCode from screen.
- Auto launch at login.
- User rules for PAC.
- Support OTA
- Support for [AEAD Ciphers](https://shadowsocks.org/en/spec/AEAD-Ciphers.html)
- HTTP Proxy by [privoxy](http://www.privoxy.org/)
- Over [kcptun](https://github.com/xtaci/kcptun). Version 20170322
- Export/Import configure file.
- An advanced preferences panel to configure:
- Local socks5 listen address.
- Local socks5 listen port.
- Local socks5 timeout.
- An advanced preferences panel for configuring:
- Local SOCKS5 listen address.
- Local SOCKS5 listen port.
- Local SOCKS5 timeout.
- If enable UDP relay.
- GFW List URL.
- Manual specify network service profiles which would be configure the proxy.
- Could reorder shadowsocks profiles by drag & drop in servers preferences panel.
- Manually specify network service profiles which would be used to configure the proxy.
- Could reorder shadowsocks profiles by drag-&-dropping in servers preferences panel.
- Configurable global shortcuts for toggle running and switch proxy mode.
## Different from orignal ShadowsocksX
## Difference from original ShadowsocksX
Run ss-local as a background service through launchd, not as an in-app process.
So after you quit the app, the ss-local maybe be still running.
`ss-local` is run as a background service through launchd, not as an in-app process.
So after you quit the app, the `ss-local` might be still running.
Added a manual mode which won't configure the system proxy settings.
Then you could configure your apps to use socks5 proxy manual.
Added a manual mode which won't configure the system proxy settings,
so that you could configure your apps to use the SOCKS5 proxy manually.
## Contributing
## Contributing
[![gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ShadowsocksX-NG/Lobby)
Contributions must be available on a separately named branch based on the latest version of the main branch develop.
Contributions must be available on a separately named branch based on the latest version of the main branch `develop`.
ref: [GitFlow](http://nvie.com/posts/a-successful-git-branching-model/)

View File

@ -64,6 +64,7 @@
9BA04B231D23D5A5005AAD7F /* ProxyConfTool.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BA04B221D23D5A5005AAD7F /* ProxyConfTool.m */; };
9BAFE2E21E83ED7F00F71CCE /* PreferencesWinController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9BAFE2E41E83ED7F00F71CCE /* PreferencesWinController.xib */; };
9BB706A71D1B982300551F0E /* SWBApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BB706A51D1B982300551F0E /* SWBApplication.m */; };
9BBE7B751F508A0E00E8FFE5 /* fix_dir_owner.sh in Resources */ = {isa = PBXBuildFile; fileRef = 9BBE7B711F50790500E8FFE5 /* fix_dir_owner.sh */; };
9BC70EDC1D2E3E3100EDA4CA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9B172A6C1D0ADDDD00B87B9A /* Localizable.strings */; };
9BEEF0691D04D4D500FC52B3 /* install_ss_local.sh in Resources */ = {isa = PBXBuildFile; fileRef = 9BEEF0651D04CB8500FC52B3 /* install_ss_local.sh */; };
9BEEF06A1D04D4D500FC52B3 /* start_ss_local.sh in Resources */ = {isa = PBXBuildFile; fileRef = 9BEEF0661D04CE8D00FC52B3 /* start_ss_local.sh */; };
@ -216,6 +217,7 @@
9BAFE2EB1E83F91D00F71CCE /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = "<group>"; };
9BB706A51D1B982300551F0E /* SWBApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SWBApplication.m; sourceTree = "<group>"; };
9BB706A61D1B982300551F0E /* SWBApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SWBApplication.h; sourceTree = "<group>"; };
9BBE7B711F50790500E8FFE5 /* fix_dir_owner.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = fix_dir_owner.sh; sourceTree = "<group>"; };
9BE8FBC11D0B71CF00CAFD01 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/PreferencesWindowController.strings"; sourceTree = "<group>"; };
9BEEF0651D04CB8500FC52B3 /* install_ss_local.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = install_ss_local.sh; sourceTree = "<group>"; };
9BEEF0661D04CE8D00FC52B3 /* start_ss_local.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = start_ss_local.sh; sourceTree = "<group>"; };
@ -324,6 +326,7 @@
C6D429981DA76FBC002A5711 /* privoxy.config.example */,
9B9CBCB01E2644DC00FC61AA /* start_kcptun.sh */,
9B9CBCB11E26450D00FC61AA /* stop_kcptun.sh */,
9BBE7B711F50790500E8FFE5 /* fix_dir_owner.sh */,
);
name = "Support Files";
sourceTree = "<group>";
@ -577,6 +580,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9BBE7B751F508A0E00E8FFE5 /* fix_dir_owner.sh in Resources */,
9B3F7BFF1E82BF5B00C68B75 /* libev.4.dylib in Resources */,
9B3F7C001E82BF5B00C68B75 /* libmbedcrypto.2.4.2.dylib in Resources */,
9B3F7C011E82BF5B00C68B75 /* libsodium.18.dylib in Resources */,

View File

@ -32,10 +32,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
@IBOutlet weak var serversMenuItem: NSMenuItem!
@IBOutlet var showQRCodeMenuItem: NSMenuItem!
@IBOutlet var scanQRCodeMenuItem: NSMenuItem!
@IBOutlet var showBunchJsonExampleFileItem: NSMenuItem!
@IBOutlet var importBunchJsonFileItem: NSMenuItem!
@IBOutlet var exportAllServerProfileItem: NSMenuItem!
@IBOutlet var serversPreferencesMenuItem: NSMenuItem!
@IBOutlet var serverProfilesBeginSeparatorMenuItem: NSMenuItem!
@IBOutlet var serverProfilesEndSeparatorMenuItem: NSMenuItem!
@IBOutlet weak var copyHttpProxyExportCmdLineMenuItem: NSMenuItem!
@ -48,7 +46,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
let kProfileMenuItemIndexBase = 100
var statusItem: NSStatusItem!
static let StatusItemIconWidth:CGFloat = 20
static let StatusItemIconWidth: CGFloat = NSVariableStatusItemLength
func ensureLaunchAgentsDirOwner () {
let dirPath = NSHomeDirectory() + "/Library/LaunchAgents"
let fileMgr = FileManager.default
if fileMgr.fileExists(atPath: dirPath) {
do {
let attrs = try fileMgr.attributesOfItem(atPath: dirPath)
if attrs[FileAttributeKey.ownerAccountName] as! String != NSUserName() {
//try fileMgr.setAttributes([FileAttributeKey.ownerAccountName: NSUserName()], ofItemAtPath: dirPath)
let bashFilePath = Bundle.main.path(forResource: "fix_dir_owner.sh", ofType: nil)!
let script = "do shell script \"bash \(bashFilePath) \(NSUserName()) \" with administrator privileges"
if let appleScript = NSAppleScript(source: script) {
var err: NSDictionary? = nil
appleScript.executeAndReturnError(&err)
}
}
}
catch {
NSLog("Error when ensure the owner of $HOME/Library/LaunchAgents, \(error.localizedDescription)")
}
}
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
@ -56,6 +76,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
NSUserNotificationCenter.default.delegate = self
self.ensureLaunchAgentsDirOwner()
// Prepare ss-local
InstallSSLocal()
InstallKcptunClient()
@ -241,6 +263,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}
qrcodeWinCtrl = SWBQRCodeWindowController(windowNibName: "SWBQRCodeWindowController")
qrcodeWinCtrl.qrCode = profile.URL()!.absoluteString
qrcodeWinCtrl.legacyQRCode = profile.URL(legacy: true)!.absoluteString
qrcodeWinCtrl.title = profile.title()
qrcodeWinCtrl.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
@ -453,39 +476,33 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}
func updateServersMenu() {
guard let menu = serversMenuItem.submenu else { return }
let mgr = ServerProfileManager.instance
serversMenuItem.submenu?.removeAllItems()
let preferencesItem = serversPreferencesMenuItem
let showBunch = showBunchJsonExampleFileItem
let importBuntch = importBunchJsonFileItem
let exportAllServer = exportAllServerProfileItem
serversMenuItem.submenu?.addItem(preferencesItem!)
serversMenuItem.submenu?.addItem(NSMenuItem.separator())
var i = 0
for p in mgr.profiles {
let profiles = mgr.profiles
// Remove all profile menu items
let beginIndex = menu.index(of: serverProfilesBeginSeparatorMenuItem) + 1
let endIndex = menu.index(of: serverProfilesEndSeparatorMenuItem)
// Remove from end to begin, so the index won't change :)
for index in (beginIndex..<endIndex).reversed() {
menu.removeItem(at: index)
}
// Insert all profile menu items
for (i, profile) in profiles.enumerated().reversed() {
let item = NSMenuItem()
item.tag = i + kProfileMenuItemIndexBase
item.title = p.title()
if mgr.activeProfileId == p.uuid {
item.state = 1
}
if !p.isValid() {
item.isEnabled = false
}
item.title = profile.title()
item.state = (mgr.activeProfileId == profile.uuid) ? 1 : 0
item.isEnabled = profile.isValid()
item.action = #selector(AppDelegate.selectServer)
serversMenuItem.submenu?.addItem(item)
i += 1
menu.insertItem(item, at: beginIndex)
}
if !mgr.profiles.isEmpty {
serversMenuItem.submenu?.addItem(NSMenuItem.separator())
}
serversMenuItem.submenu?.addItem(showBunch!)
serversMenuItem.submenu?.addItem(importBuntch!)
serversMenuItem.submenu?.addItem(exportAllServer!)
// End separator is redundant if profile section is empty
serverProfilesEndSeparatorMenuItem.isHidden = profiles.isEmpty
}
func handleURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="12121"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -16,15 +16,13 @@
<connections>
<outlet property="autoModeMenuItem" destination="r07-Gu-aEz" id="9aH-pQ-Rgi"/>
<outlet property="copyHttpProxyExportCmdLineMenuItem" destination="lg6-To-GZA" id="VTb-he-dg4"/>
<outlet property="exportAllServerProfileItem" destination="6k0-gn-DQv" id="W2x-96-ISj"/>
<outlet property="globalModeMenuItem" destination="Mw3-Jm-eXA" id="ar5-Yx-3ze"/>
<outlet property="importBunchJsonFileItem" destination="T9g-gy-gvv" id="vua-jg-YWe"/>
<outlet property="manualModeMenuItem" destination="8PR-gs-c5N" id="9qz-mU-5kt"/>
<outlet property="runningStatusMenuItem" destination="fzk-mE-CEV" id="Vwm-Rg-Ykn"/>
<outlet property="scanQRCodeMenuItem" destination="Qe6-bF-paT" id="XHa-pa-nCa"/>
<outlet property="serverProfilesBeginSeparatorMenuItem" destination="4iN-w2-but" id="Jyu-48-AzD"/>
<outlet property="serverProfilesEndSeparatorMenuItem" destination="3cf-dF-7dx" id="eyc-6W-nWV"/>
<outlet property="serversMenuItem" destination="u5M-hQ-VSc" id="8gp-SY-Y4U"/>
<outlet property="serversPreferencesMenuItem" destination="M5r-E7-44f" id="voe-SX-k6a"/>
<outlet property="showBunchJsonExampleFileItem" destination="pdy-JE-50Q" id="xcZ-ep-mON"/>
<outlet property="showQRCodeMenuItem" destination="R6A-96-Zcb" id="XHz-pz-nCz"/>
<outlet property="statusMenu" destination="Hob-KD-bx9" id="clA-ZW-0pT"/>
<outlet property="toggleRunningMenuItem" destination="GSu-Tf-StS" id="XHw-pU-nCa"/>
@ -73,6 +71,7 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="4iN-w2-but"/>
<menuItem isSeparatorItem="YES" id="3cf-dF-7dx"/>
<menuItem title="Show Bunch Json Example File..." id="pdy-JE-50Q">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.5.1</string>
<string>1.6.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -44,7 +44,7 @@
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016 qiuyuzhou. All rights reserved. License GPLv3.</string>
<string>Copyright © 2016-2017 qiuyuzhou. All rights reserved. License GPLv3.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@ -60,7 +60,6 @@ func generateSSLocalLauchAgentPlist() -> Bool {
let dict: NSMutableDictionary = [
"Label": "com.qiuyuzhou.shadowsocksX-NG.local",
"WorkingDirectory": NSHomeDirectory() + APP_SUPPORT_DIR,
"KeepAlive": true,
"StandardOutPath": logFilePath,
"StandardErrorPath": logFilePath,
"ProgramArguments": arguments,
@ -203,7 +202,6 @@ func generatePrivoxyLauchAgentPlist() -> Bool {
let dict: NSMutableDictionary = [
"Label": "com.qiuyuzhou.shadowsocksX-NG.http",
"WorkingDirectory": NSHomeDirectory() + APP_SUPPORT_DIR,
"KeepAlive": true,
"StandardOutPath": logFilePath,
"StandardErrorPath": logFilePath,
"ProgramArguments": arguments
@ -358,7 +356,6 @@ func generateKcptunLauchAgentPlist() -> Bool {
let dict: NSMutableDictionary = [
"Label": "com.qiuyuzhou.shadowsocksX-NG.kcptun",
"WorkingDirectory": NSHomeDirectory() + APP_SUPPORT_DIR,
"KeepAlive": true,
"StandardOutPath": logFilePath,
"StandardErrorPath": logFilePath,
"ProgramArguments": arguments,

View File

@ -107,6 +107,7 @@ class PreferencesWindowController: NSWindowController
override func awakeFromNib() {
profilesTableView.register(forDraggedTypes: [tableViewDragType])
profilesTableView.allowsMultipleSelection = true
}
@IBAction func addProfile(_ sender: NSButton) {
@ -129,13 +130,20 @@ class PreferencesWindowController: NSWindowController
}
@IBAction func removeProfile(_ sender: NSButton) {
let index = profilesTableView.selectedRow
let index = Int(profilesTableView.selectedRowIndexes.first!)
var deleteCount = 0
if index >= 0 {
profilesTableView.beginUpdates()
profileMgr.profiles.remove(at: index)
profilesTableView.removeRows(at: IndexSet(integer: index), withAnimation: .effectFade)
for (_, toDeleteIndex) in profilesTableView.selectedRowIndexes.enumerated() {
print(profileMgr.profiles.count)
profileMgr.profiles.remove(at: toDeleteIndex - deleteCount)
profilesTableView.removeRows(at: IndexSet(integer: toDeleteIndex - deleteCount), withAnimation: .effectFade)
deleteCount += 1
}
profilesTableView.endUpdates()
}
self.profilesTableView.scrollRowToVisible(index-1)
self.profilesTableView.selectRowIndexes(IndexSet(integer: index-1), byExtendingSelection: false)
updateProfileBoxVisible()
}
@ -160,16 +168,23 @@ class PreferencesWindowController: NSWindowController
}
@IBAction func duplicate(_ sender: Any) {
let profile = profileMgr.profiles[profilesTableView.clickedRow]
let duplicateProfile = profile.copy() as! ServerProfile
duplicateProfile.uuid = UUID().uuidString
profileMgr.profiles.insert(duplicateProfile, at: profilesTableView.clickedRow+1)
profilesTableView.beginUpdates()
let index = IndexSet(integer: profileMgr.profiles.count-1)
profilesTableView.insertRows(at: index, withAnimation: .effectFade)
self.profilesTableView.scrollRowToVisible(profilesTableView.clickedRow+1)
self.profilesTableView.selectRowIndexes(index, byExtendingSelection: false)
profilesTableView.endUpdates()
var copyCount = 0
for (_, toDuplicateIndex) in profilesTableView.selectedRowIndexes.enumerated() {
print(profileMgr.profiles.count)
let profile = profileMgr.profiles[toDuplicateIndex + copyCount]
let duplicateProfile = profile.copy() as! ServerProfile
duplicateProfile.uuid = UUID().uuidString
profileMgr.profiles.insert(duplicateProfile, at:toDuplicateIndex + copyCount)
profilesTableView.beginUpdates()
let index = IndexSet(integer: toDuplicateIndex + copyCount)
profilesTableView.insertRows(at: index, withAnimation: .effectFade)
self.profilesTableView.scrollRowToVisible(toDuplicateIndex + copyCount)
self.profilesTableView.selectRowIndexes(index, byExtendingSelection: false)
profilesTableView.endUpdates()
copyCount += 1
}
updateProfileBoxVisible()
}

View File

@ -68,5 +68,8 @@ class ProxyInterfacesViewCtrl: NSViewController, NSTableViewDataSource, NSTableV
} else {
selectedNetworkServices.remove(key)
}
UserDefaults.standard.set(selectedNetworkServices.allObjects,
forKey: "Proxy4NetworkServices")
}
}

View File

@ -11,6 +11,7 @@
@interface SWBQRCodeWindowController : NSWindowController
@property (nonatomic, copy) NSString *legacyQRCode;
@property (nonatomic, copy) NSString *qrCode;
@property (nonatomic, copy) NSString *title;

View File

@ -19,13 +19,32 @@
[super windowDidLoad];
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
[self setQRCode:self.qrCode];
[self setQRCode:self.qrCode withOverlayText:@"Shadowsocks-NG SIP002"];
}
- (void)setQRCode:(NSString*) qrCode {
- (void)setQRCode:(NSString*) qrCode withOverlayText: (NSString*) text {
CGImageRef cgImgRef = [self createQRImageForString:qrCode size:CGSizeMake(250, 250)];
NSImage *image = [[NSImage alloc]initWithCGImage:cgImgRef size:CGSizeMake(250, 250)];
if (text) {
// Draw overlay text
NSDictionary* attrs = @{
NSForegroundColorAttributeName: [NSColor colorWithRed:28/255.0 green:155/255.0 blue:71/255.0 alpha:1],
NSBackgroundColorAttributeName: [NSColor whiteColor],
NSFontAttributeName: [NSFont fontWithName:@"Helvetica" size:(CGFloat)16],
};
NSMutableAttributedString* attrsText = [[NSMutableAttributedString alloc] initWithString: text
attributes: attrs];
[attrsText setAttributes:@{
NSForegroundColorAttributeName: [NSColor darkGrayColor],
NSBackgroundColorAttributeName: [NSColor whiteColor],
NSFontAttributeName: [NSFont fontWithName:@"Helvetica" size:(CGFloat)16],
} range: NSMakeRange(0, 14)];
[image lockFocus];
[attrsText drawAtPoint: NSMakePoint(45, 8)];
[image unlockFocus];
}
self.imageView.image = image;
}
@ -36,6 +55,14 @@
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
[filter setValue:data forKey:@"inputMessage"];
/*
L: 7%
M: 15%
Q: 25%
H: 30%
*/
[filter setValue:@"Q" forKey:@"inputCorrectionLevel"];
CIImage *image = [filter valueForKey:@"outputImage"];
// Calculate the size of the generated image and the scale for the desired image size
@ -78,4 +105,13 @@
[pasteboard writeObjects:copiedObjects];
}
- (void)flagsChanged:(NSEvent *)event {
NSUInteger modifiers = event.modifierFlags & NSDeviceIndependentModifierFlagsMask;
if (modifiers & NSAlternateKeyMask) {
[self setQRCode:self.legacyQRCode withOverlayText:@"Shadowsocks-NG Legacy"];
} else {
[self setQRCode:self.qrCode withOverlayText:@"Shadowsocks-NG SIP002"];
}
}
@end

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="12121"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<capability name="system font weights other than Regular or Bold" minToolsVersion="7.0"/>
</dependencies>
@ -17,14 +17,14 @@
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="QR Code" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" oneShot="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<rect key="contentRect" x="566" y="456" width="290" height="359"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<rect key="contentRect" x="566" y="456" width="290" height="379"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<view key="contentView" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="290" height="359"/>
<rect key="frame" x="0.0" y="0.0" width="290" height="379"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yRy-QH-TEe">
<rect key="frame" x="20" y="61" width="250" height="250"/>
<rect key="frame" x="20" y="81" width="250" height="250"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="iAJ-tw-LNU"/>
<connections>
@ -32,7 +32,7 @@
</connections>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9f5-sB-jlD">
<rect key="frame" x="18" y="322" width="254" height="17"/>
<rect key="frame" x="18" y="342" width="254" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Title For Server" id="fHU-xF-zyc">
<font key="font" metaFont="systemSemibold" size="13"/>
@ -44,7 +44,7 @@
</connections>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fAd-u3-u8m">
<rect key="frame" x="14" y="13" width="262" height="32"/>
<rect key="frame" x="14" y="20" width="262" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Copy To Pasteboard" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3q0-EO-Vug">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -54,9 +54,18 @@
<action selector="copyQRCode:" target="-2" id="4fF-ID-p1E"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dqP-hN-9CY">
<rect key="frame" x="34" y="56" width="222" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Press option/alt key to show legacy " id="x9U-Ti-73h">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<point key="canvasLocation" x="152" y="-70.5"/>
<point key="canvasLocation" x="152" y="-60.5"/>
</window>
<menu id="5h4-gO-Rh7">
<items>

View File

@ -31,7 +31,7 @@ class ServerProfile: NSObject, NSCopying {
self.uuid = uuid
}
convenience init?(url: URL?) {
convenience init?(url: URL) {
self.init()
func padBase64(string: String) -> String {
@ -44,14 +44,12 @@ class ServerProfile: NSObject, NSCopying {
}
}
func decodeUrl(url: URL?) -> String? {
guard let urlStr = url?.absoluteString else {
return nil
}
func decodeUrl(url: URL) -> String? {
let urlStr = url.absoluteString
let index = urlStr.index(urlStr.startIndex, offsetBy: 5)
let encodedStr = urlStr.substring(from: index)
guard let data = Data(base64Encoded: padBase64(string: encodedStr)) else {
return url?.absoluteString
return url.absoluteString
}
guard let decoded = String(data: data, encoding: String.Encoding.utf8) else {
return nil
@ -67,17 +65,40 @@ class ServerProfile: NSObject, NSCopying {
return nil
}
guard let host = parsedUrl.host, let port = parsedUrl.port,
let method = parsedUrl.user, let password = parsedUrl.password else {
let user = parsedUrl.user else {
return nil
}
self.serverHost = host
self.serverPort = UInt16(port)
self.method = method.lowercased()
self.password = password
// This can be overriden by the fragment part of SIP002 URL
remark = parsedUrl.queryItems?
.filter({ $0.name == "Remark" }).first?.value ?? ""
if let password = parsedUrl.password {
self.method = user.lowercased()
self.password = password
} else {
// SIP002 URL have no password section
guard let data = Data(base64Encoded: padBase64(string: user)),
let userInfo = String(data: data, encoding: .utf8) else {
return nil
}
let parts = userInfo.characters.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
if parts.count != 2 {
return nil
}
self.method = String(parts[0]).lowercased()
self.password = String(parts[1])
// SIP002 defines where to put the profile name
if let profileName = parsedUrl.fragment {
self.remark = profileName
}
}
if let otaStr = parsedUrl.queryItems?
.filter({ $0.name == "OTA" }).first?.value {
ota = NSString(string: otaStr).boolValue
@ -225,7 +246,7 @@ class ServerProfile: NSObject, NSCopying {
return true
}
func URL() -> Foundation.URL? {
private func makeLegacyURL() -> URL? {
var url = URLComponents()
url.host = serverHost
@ -254,6 +275,39 @@ class ServerProfile: NSObject, NSCopying {
}
return nil
}
func URL(legacy: Bool = false) -> URL? {
// If you want the URL from <= 1.5.1
if (legacy) {
return self.makeLegacyURL()
}
guard let rawUserInfo = "\(method):\(password)".data(using: .utf8) else {
return nil
}
let paddings = CharacterSet(charactersIn: "=")
let userInfo = rawUserInfo.base64EncodedString().trimmingCharacters(in: paddings)
var items = [URLQueryItem(name: "OTA", value: ota.description)]
if enabledKcptun {
items.append(URLQueryItem(name: "Kcptun", value: enabledKcptun.description))
items.append(contentsOf: kcptunProfile.urlQueryItems())
}
var comps = URLComponents()
comps.scheme = "ss"
comps.host = serverHost
comps.port = Int(serverPort)
comps.user = userInfo
comps.path = "/" // This is required by SIP0002 for URLs with fragment or query
comps.fragment = remark
comps.queryItems = items
let url = try? comps.asURL()
return url
}
func title() -> String {
if remark.isEmpty {

View File

@ -30,6 +30,8 @@ class ToastWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
self.shouldCascadeWindows = false
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
if let win = self.window {
win.isOpaque = false

View File

@ -56,7 +56,10 @@ void ScanQRCodeOnScreen() {
NSLog(@"%@", feature.messageString);
if ( [feature.messageString hasPrefix:@"ss://"] )
{
[foundSSUrls addObject:[NSURL URLWithString:feature.messageString]];
NSURL *url = [NSURL URLWithString:feature.messageString];
if (url) {
[foundSSUrls addObject:url];
}
}
}
CGImageRelease(image);

View File

@ -0,0 +1,12 @@
#!/bin/sh
# fix_dir_owner.sh
# ShadowsocksX-NG
#
# Created by 邱宇舟 on 2017/8/25.
# Copyright © 2017年 qiuyuzhou. All rights reserved.
LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
sudo chown $@ "$HOME/Library/LaunchAgents"

View File

@ -9,6 +9,6 @@
#ifndef proxy_conf_helper_version_h
#define proxy_conf_helper_version_h
#define kProxyConfHelperVersion @"1.5.0"
#define kProxyConfHelperVersion @"1.6.0"
#endif /* proxy_conf_helper_version_h */

View File

@ -6,4 +6,6 @@
# Created by 邱宇舟 on 2017/1/11.
# Copyright © 2017年 qiuyuzhou. All rights reserved.
chmod 644 "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.kcptun.plist"
launchctl load "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.kcptun.plist"
launchctl start com.qiuyuzhou.shadowsocksX-NG.kcptun

View File

@ -6,4 +6,6 @@
# Created by 王晨 on 16/10/7.
# Copyright © 2016年 zhfish. All rights reserved.
chmod 644 "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.http.plist"
launchctl load "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.http.plist"
launchctl start com.qiuyuzhou.shadowsocksX-NG.http

View File

@ -6,4 +6,6 @@
# Created by 邱宇舟 on 16/6/6.
# Copyright © 2016年 qiuyuzhou. All rights reserved.
launchctl load "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.local.plist"
chmod 644 "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.local.plist"
launchctl load "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.local.plist"
launchctl start com.qiuyuzhou.shadowsocksX-NG.local

View File

@ -6,4 +6,5 @@
# Created by 邱宇舟 on 2017/1/11.
# Copyright © 2017年 qiuyuzhou. All rights reserved.
launchctl stop com.qiuyuzhou.shadowsocksX-NG.kcptun
launchctl unload "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.kcptun.plist"

View File

@ -6,6 +6,5 @@
# Created by 王晨 on 16/10/7.
# Copyright © 2016年 zhfish. All rights reserved.
launchctl stop com.qiuyuzhou.shadowsocksX-NG.http
launchctl unload "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.http.plist"

View File

@ -6,6 +6,5 @@
# Created by 邱宇舟 on 16/6/6.
# Copyright © 2016年 qiuyuzhou. All rights reserved.
launchctl unload "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.local.plist"
launchctl stop com.qiuyuzhou.shadowsocksX-NG.local
launchctl unload "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.local.plist"

View File

@ -26,7 +26,7 @@
* ./AppDelegate.swift
*/
"Add Shadowsocks Server Profile" = "已添加新Shaodwsocks服务器配置";
"Add Shadowsocks Server Profile" = "已添加新Shadowsocks服务器配置";
"By scan QR Code" = "通过扫描二维码";

View File

@ -31,7 +31,7 @@ class ServerProfileTests: XCTestCase {
}
func testInitWithSelfGeneratedURL() {
let newProfile = ServerProfile.init(url: profile.URL())
let newProfile = ServerProfile.init(url: profile.URL()!)
XCTAssertEqual(newProfile?.serverHost, profile.serverHost)
XCTAssertEqual(newProfile?.serverPort, profile.serverPort)
@ -42,7 +42,7 @@ class ServerProfileTests: XCTestCase {
}
func testInitWithPlainURL() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388")
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388")!
let profile = ServerProfile(url: url)
@ -57,7 +57,7 @@ class ServerProfileTests: XCTestCase {
}
func testInitWithPlainURLandQuery() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true")
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true")!
let profile = ServerProfile(url: url)
@ -72,7 +72,7 @@ class ServerProfileTests: XCTestCase {
}
func testInitWithPlainURLandAnotherQuery() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=0")
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=0")!
let profile = ServerProfile(url: url)
@ -88,7 +88,7 @@ class ServerProfileTests: XCTestCase {
func testInitWithBase64EncodedURL() {
// "ss://aes-256-cfb:password@example.com:8388"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OA")
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OA")!
let profile = ServerProfile(url: url)
@ -104,7 +104,7 @@ class ServerProfileTests: XCTestCase {
func testInitWithBase64EncodedURLandQuery() {
// "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OD9SZW1hcms9UHJpc20mT1RBPXRydWU")
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OD9SZW1hcms9UHJpc20mT1RBPXRydWU")!
let profile = ServerProfile(url: url)
@ -119,15 +119,7 @@ class ServerProfileTests: XCTestCase {
}
func testInitWithEmptyURL() {
let url = URL(string: "ss://")
let profile = ServerProfile(url: url)
XCTAssertNil(profile)
}
func testInitWithInvalidURL() {
let url = URL(string: "ss://invalid url")
let url = URL(string: "ss://")!
let profile = ServerProfile(url: url)
@ -136,13 +128,47 @@ class ServerProfileTests: XCTestCase {
func testInitWithBase64EncodedInvalidURL() {
// "ss://invalid url"
let url = URL(string: "ss://aW52YWxpZCB1cmw")
let url = URL(string: "ss://aW52YWxpZCB1cmw")!
let profile = ServerProfile(url: url)
XCTAssertNil(profile)
}
func testInitWithSIP002URL() {
// "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmQ=@example.com:8388/?Remark=Prism&OTA=true")!
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.serverHost, "example.com")
XCTAssertEqual(profile?.serverPort, 8388)
XCTAssertEqual(profile?.method, "aes-256-cfb")
XCTAssertEqual(profile?.password, "password")
XCTAssertEqual(profile?.remark, "Prism")
XCTAssertEqual(profile?.ota, true)
}
func testInitWithSIP002URLProfileName() {
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmQ=@example.com:8388/#Name")!
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.remark, "Name")
}
func testInitWithSIP002URLProfileNameOverride() {
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmQ=@example.com:8388/?Remark=Name#Overriden")!
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.remark, "Overriden")
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {

View File

@ -158,7 +158,7 @@ int main(int argc, const char * argv[])
kCFNetworkProxiesSOCKSPort];
[proxies setObject:[NSNumber numberWithInt:1] forKey:(NSString*)
kCFNetworkProxiesSOCKSEnable];
[proxies setObject:@[@"127.0.0.1", @"localhost"] forKey:(NSString *)kCFNetworkProxiesExceptionsList];
[proxies setObject:@[@"127.0.0.1", @"localhost", @"192.168.0.0/16", @"10.0.0.0/8"] forKey:(NSString *)kCFNetworkProxiesExceptionsList];
if (privoxyPort != 0) {
[proxies setObject:@"127.0.0.1" forKey:(NSString *)