diff --git a/README.md b/README.md index 7ed86d45..20c3b7a3 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/ShadowsocksX-NG.xcodeproj/project.pbxproj b/ShadowsocksX-NG.xcodeproj/project.pbxproj index 4d1e9188..8b4850a9 100755 --- a/ShadowsocksX-NG.xcodeproj/project.pbxproj +++ b/ShadowsocksX-NG.xcodeproj/project.pbxproj @@ -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 = ""; }; 9BB706A51D1B982300551F0E /* SWBApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SWBApplication.m; sourceTree = ""; }; 9BB706A61D1B982300551F0E /* SWBApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SWBApplication.h; sourceTree = ""; }; + 9BBE7B711F50790500E8FFE5 /* fix_dir_owner.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = fix_dir_owner.sh; sourceTree = ""; }; 9BE8FBC11D0B71CF00CAFD01 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/PreferencesWindowController.strings"; sourceTree = ""; }; 9BEEF0651D04CB8500FC52B3 /* install_ss_local.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = install_ss_local.sh; sourceTree = ""; }; 9BEEF0661D04CE8D00FC52B3 /* start_ss_local.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = start_ss_local.sh; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/ShadowsocksX-NG/AppDelegate.swift b/ShadowsocksX-NG/AppDelegate.swift index 841c2353..a90f763b 100755 --- a/ShadowsocksX-NG/AppDelegate.swift +++ b/ShadowsocksX-NG/AppDelegate.swift @@ -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.. - + - + @@ -16,15 +16,13 @@ - - + + - - @@ -73,6 +71,7 @@ + diff --git a/ShadowsocksX-NG/Info.plist b/ShadowsocksX-NG/Info.plist index 034917c8..96dbd739 100644 --- a/ShadowsocksX-NG/Info.plist +++ b/ShadowsocksX-NG/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.5.1 + 1.6.0 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ LSUIElement NSHumanReadableCopyright - Copyright © 2016年 qiuyuzhou. All rights reserved. License GPLv3. + Copyright © 2016-2017 qiuyuzhou. All rights reserved. License GPLv3. NSMainNibFile MainMenu NSPrincipalClass diff --git a/ShadowsocksX-NG/LaunchAgentUtils.swift b/ShadowsocksX-NG/LaunchAgentUtils.swift index 9e7dfba6..d74116d1 100644 --- a/ShadowsocksX-NG/LaunchAgentUtils.swift +++ b/ShadowsocksX-NG/LaunchAgentUtils.swift @@ -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, diff --git a/ShadowsocksX-NG/PreferencesWindowController.swift b/ShadowsocksX-NG/PreferencesWindowController.swift index 9cb0ddb2..883af377 100644 --- a/ShadowsocksX-NG/PreferencesWindowController.swift +++ b/ShadowsocksX-NG/PreferencesWindowController.swift @@ -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() } diff --git a/ShadowsocksX-NG/ProxyInterfacesViewCtrl.swift b/ShadowsocksX-NG/ProxyInterfacesViewCtrl.swift index 06d21d0a..61737942 100644 --- a/ShadowsocksX-NG/ProxyInterfacesViewCtrl.swift +++ b/ShadowsocksX-NG/ProxyInterfacesViewCtrl.swift @@ -68,5 +68,8 @@ class ProxyInterfacesViewCtrl: NSViewController, NSTableViewDataSource, NSTableV } else { selectedNetworkServices.remove(key) } + + UserDefaults.standard.set(selectedNetworkServices.allObjects, + forKey: "Proxy4NetworkServices") } } diff --git a/ShadowsocksX-NG/SWBQRCodeWindowController.h b/ShadowsocksX-NG/SWBQRCodeWindowController.h index 85a3e9a4..ff9b194c 100644 --- a/ShadowsocksX-NG/SWBQRCodeWindowController.h +++ b/ShadowsocksX-NG/SWBQRCodeWindowController.h @@ -11,6 +11,7 @@ @interface SWBQRCodeWindowController : NSWindowController +@property (nonatomic, copy) NSString *legacyQRCode; @property (nonatomic, copy) NSString *qrCode; @property (nonatomic, copy) NSString *title; diff --git a/ShadowsocksX-NG/SWBQRCodeWindowController.m b/ShadowsocksX-NG/SWBQRCodeWindowController.m index a7e97e9e..a11cf5b7 100644 --- a/ShadowsocksX-NG/SWBQRCodeWindowController.m +++ b/ShadowsocksX-NG/SWBQRCodeWindowController.m @@ -19,13 +19,32 @@ - (void)windowDidLoad { [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 @@ - (CGImageRef)createQRImageForString:(NSString *)string size:(CGSize)size { 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 @@ - (IBAction) copyQRCode: (id) sender{ [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 diff --git a/ShadowsocksX-NG/SWBQRCodeWindowController.xib b/ShadowsocksX-NG/SWBQRCodeWindowController.xib index ea35673c..96a94e9c 100644 --- a/ShadowsocksX-NG/SWBQRCodeWindowController.xib +++ b/ShadowsocksX-NG/SWBQRCodeWindowController.xib @@ -1,8 +1,8 @@ - + - + @@ -17,14 +17,14 @@ - - + + - + - + @@ -32,7 +32,7 @@ - + @@ -44,7 +44,7 @@ + + + + + + + + + - + diff --git a/ShadowsocksX-NG/ServerProfile.swift b/ShadowsocksX-NG/ServerProfile.swift index dfb1fa09..aff8b4e9 100644 --- a/ShadowsocksX-NG/ServerProfile.swift +++ b/ShadowsocksX-NG/ServerProfile.swift @@ -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 { diff --git a/ShadowsocksX-NG/ToastWindowController.swift b/ShadowsocksX-NG/ToastWindowController.swift index b6887210..161a1051 100644 --- a/ShadowsocksX-NG/ToastWindowController.swift +++ b/ShadowsocksX-NG/ToastWindowController.swift @@ -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 diff --git a/ShadowsocksX-NG/Utils.m b/ShadowsocksX-NG/Utils.m index c77a07a2..92912c9d 100644 --- a/ShadowsocksX-NG/Utils.m +++ b/ShadowsocksX-NG/Utils.m @@ -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); diff --git a/ShadowsocksX-NG/fix_dir_owner.sh b/ShadowsocksX-NG/fix_dir_owner.sh new file mode 100755 index 00000000..60f0fa57 --- /dev/null +++ b/ShadowsocksX-NG/fix_dir_owner.sh @@ -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" + diff --git a/ShadowsocksX-NG/proxy_conf_helper_version.h b/ShadowsocksX-NG/proxy_conf_helper_version.h index 4525a5cf..9a39b4bc 100644 --- a/ShadowsocksX-NG/proxy_conf_helper_version.h +++ b/ShadowsocksX-NG/proxy_conf_helper_version.h @@ -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 */ diff --git a/ShadowsocksX-NG/start_kcptun.sh b/ShadowsocksX-NG/start_kcptun.sh index 580d40ec..929c64d0 100755 --- a/ShadowsocksX-NG/start_kcptun.sh +++ b/ShadowsocksX-NG/start_kcptun.sh @@ -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 diff --git a/ShadowsocksX-NG/start_privoxy.sh b/ShadowsocksX-NG/start_privoxy.sh index bedef6a0..3018dd6a 100755 --- a/ShadowsocksX-NG/start_privoxy.sh +++ b/ShadowsocksX-NG/start_privoxy.sh @@ -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 diff --git a/ShadowsocksX-NG/start_ss_local.sh b/ShadowsocksX-NG/start_ss_local.sh index b4047d32..5a161453 100755 --- a/ShadowsocksX-NG/start_ss_local.sh +++ b/ShadowsocksX-NG/start_ss_local.sh @@ -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" \ No newline at end of file +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 diff --git a/ShadowsocksX-NG/stop_kcptun.sh b/ShadowsocksX-NG/stop_kcptun.sh index b192aeae..68536fc9 100755 --- a/ShadowsocksX-NG/stop_kcptun.sh +++ b/ShadowsocksX-NG/stop_kcptun.sh @@ -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" diff --git a/ShadowsocksX-NG/stop_privoxy.sh b/ShadowsocksX-NG/stop_privoxy.sh index 8cb6a2b3..e3ebb942 100755 --- a/ShadowsocksX-NG/stop_privoxy.sh +++ b/ShadowsocksX-NG/stop_privoxy.sh @@ -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" diff --git a/ShadowsocksX-NG/stop_ss_local.sh b/ShadowsocksX-NG/stop_ss_local.sh index 5a6cf577..15d59b36 100755 --- a/ShadowsocksX-NG/stop_ss_local.sh +++ b/ShadowsocksX-NG/stop_ss_local.sh @@ -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" \ No newline at end of file +launchctl stop com.qiuyuzhou.shadowsocksX-NG.local +launchctl unload "$HOME/Library/LaunchAgents/com.qiuyuzhou.shadowsocksX-NG.local.plist" diff --git a/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings b/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings index 1839eb1a..4785d170 100755 --- a/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings +++ b/ShadowsocksX-NG/zh-Hans.lproj/Localizable.strings @@ -26,7 +26,7 @@ * ./AppDelegate.swift */ -"Add Shadowsocks Server Profile" = "已添加新Shaodwsocks服务器配置"; +"Add Shadowsocks Server Profile" = "已添加新Shadowsocks服务器配置"; "By scan QR Code" = "通过扫描二维码"; diff --git a/ShadowsocksX-NGTests/ServerProfileTests.swift b/ShadowsocksX-NGTests/ServerProfileTests.swift index d9428951..c3b4d428 100644 --- a/ShadowsocksX-NGTests/ServerProfileTests.swift +++ b/ShadowsocksX-NGTests/ServerProfileTests.swift @@ -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,28 +119,54 @@ class ServerProfileTests: XCTestCase { } func testInitWithEmptyURL() { - let url = URL(string: "ss://") + let url = URL(string: "ss://")! let profile = ServerProfile(url: url) XCTAssertNil(profile) } - func testInitWithInvalidURL() { - let url = URL(string: "ss://invalid url") + func testInitWithBase64EncodedInvalidURL() { + // "ss://invalid url" + let url = URL(string: "ss://aW52YWxpZCB1cmw")! let profile = ServerProfile(url: url) XCTAssertNil(profile) } - func testInitWithBase64EncodedInvalidURL() { - // "ss://invalid url" - let url = URL(string: "ss://aW52YWxpZCB1cmw") + 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) - XCTAssertNil(profile) + 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() { diff --git a/proxy_conf_helper/main.m b/proxy_conf_helper/main.m index e1f39db5..f45a6c50 100644 --- a/proxy_conf_helper/main.m +++ b/proxy_conf_helper/main.m @@ -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 *)