From 739b04065e3f235e2e14603d1583cadabc62c2e0 Mon Sep 17 00:00:00 2001 From: yangziy Date: Mon, 17 Sep 2018 13:15:48 +0800 Subject: [PATCH 01/11] Add HTTP API & AppleScript support. Refactor in class AppDelegate. --- ShadowsocksX-NG.xcodeproj/project.pbxproj | 12 +++ ShadowsocksX-NG/AppDelegate.swift | 85 ++++++++++++--------- ShadowsocksX-NG/AppleScriptDefinition.sdef | 51 +++++++++++++ ShadowsocksX-NG/AppleScriptUserProxy.swift | 72 ++++++++++++++++++ ShadowsocksX-NG/HTTPUserProxy.swift | 88 ++++++++++++++++++++++ ShadowsocksX-NG/Info.plist | 4 + 6 files changed, 278 insertions(+), 34 deletions(-) create mode 100644 ShadowsocksX-NG/AppleScriptDefinition.sdef create mode 100644 ShadowsocksX-NG/AppleScriptUserProxy.swift create mode 100644 ShadowsocksX-NG/HTTPUserProxy.swift diff --git a/ShadowsocksX-NG.xcodeproj/project.pbxproj b/ShadowsocksX-NG.xcodeproj/project.pbxproj index d3f372cf..46884f19 100755 --- a/ShadowsocksX-NG.xcodeproj/project.pbxproj +++ b/ShadowsocksX-NG.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 1C82DBA81FA96C7500B32551 /* obfs-local in Resources */ = {isa = PBXBuildFile; fileRef = 1C82DBA51FA96C7400B32551 /* obfs-local */; }; 1C82DBAA1FA96FB600B32551 /* install_simple_obfs.sh in Resources */ = {isa = PBXBuildFile; fileRef = 1C82DBA91FA96F0300B32551 /* install_simple_obfs.sh */; }; 258E511BA910B0521B24DAB8 /* Pods_ShadowsocksX_NG.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 283ED1A8E9B711AC65670031 /* Pods_ShadowsocksX_NG.framework */; }; + 8EE2EDD8214F7CEC00FB4562 /* AppleScriptUserProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE2EDD4214F7CEC00FB4562 /* AppleScriptUserProxy.swift */; }; + 8EE2EDD9214F7CEC00FB4562 /* HTTPUserProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */; }; + 8EE2EDDA214F7CEC00FB4562 /* AppleScriptDefinition.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8EE2EDD7214F7CEC00FB4562 /* AppleScriptDefinition.sdef */; }; 9B07EFA71D048BBB0052D9DF /* ss-local in Resources */ = {isa = PBXBuildFile; fileRef = 9B07EFA61D048BBB0052D9DF /* ss-local */; }; 9B07EFAC1D048E880052D9DF /* menu_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B07EFA81D048E880052D9DF /* menu_icon@2x.png */; }; 9B07EFAD1D048E880052D9DF /* menu_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B07EFA91D048E880052D9DF /* menu_icon.png */; }; @@ -145,6 +148,9 @@ 50D54926AA21B0D4D8DD9C4F /* Pods-ShadowsocksX-NGUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NGUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NGUITests/Pods-ShadowsocksX-NGUITests.release.xcconfig"; sourceTree = ""; }; 58907E7F50405104B42CB189 /* Pods-ShadowsocksX-NGUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NGUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NGUITests/Pods-ShadowsocksX-NGUITests.debug.xcconfig"; sourceTree = ""; }; 5B6203C1228FCD3D365814AC /* Pods-ShadowsocksX-NGTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NGTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NGTests/Pods-ShadowsocksX-NGTests.debug.xcconfig"; sourceTree = ""; }; + 8EE2EDD4214F7CEC00FB4562 /* AppleScriptUserProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleScriptUserProxy.swift; sourceTree = ""; }; + 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPUserProxy.swift; sourceTree = ""; }; + 8EE2EDD7214F7CEC00FB4562 /* AppleScriptDefinition.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = AppleScriptDefinition.sdef; sourceTree = ""; }; 9B07EFA61D048BBB0052D9DF /* ss-local */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = "ss-local"; sourceTree = ""; }; 9B07EFA81D048E880052D9DF /* menu_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icon@2x.png"; sourceTree = ""; }; 9B07EFA91D048E880052D9DF /* menu_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icon.png; sourceTree = ""; }; @@ -359,6 +365,9 @@ isa = PBXGroup; children = ( 9BB706A51D1B982300551F0E /* SWBApplication.m */, + 8EE2EDD7214F7CEC00FB4562 /* AppleScriptDefinition.sdef */, + 8EE2EDD4214F7CEC00FB4562 /* AppleScriptUserProxy.swift */, + 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */, 9BB706A61D1B982300551F0E /* SWBApplication.h */, 9B3FFF511D09DBA20019A709 /* ShadowsocksX-NG-Bridging-Header.h */, 9B3FFF151D072FDE0019A709 /* LaunchAtLoginController.h */, @@ -635,6 +644,7 @@ 9B3FFF341D08CEF70019A709 /* SWBQRCodeWindowController.xib in Resources */, 9B3FFF231D088E8D0019A709 /* abp.js in Resources */, 9B07EFAD1D048E880052D9DF /* menu_icon.png in Resources */, + 8EE2EDDA214F7CEC00FB4562 /* AppleScriptDefinition.sdef in Resources */, 9BAFE2E21E83ED7F00F71CCE /* PreferencesWinController.xib in Resources */, 9B0BFFEB1D0460A70040E62B /* Assets.xcassets in Resources */, 08FCA0FF1E24BE1A0070984F /* example-gui-config.json in Resources */, @@ -823,10 +833,12 @@ 9BB706A71D1B982300551F0E /* SWBApplication.m in Sources */, 9B3FFF1E1D0732660019A709 /* Utils.m in Sources */, 9B7297EA214D7C6B00FD24AA /* ShareServerProfilesWindowController.swift in Sources */, + 8EE2EDD9214F7CEC00FB4562 /* HTTPUserProxy.swift in Sources */, 9B3FFF321D08CEE40019A709 /* SWBQRCodeWindowController.m in Sources */, 9B3FFF211D08826E0019A709 /* PACUtils.swift in Sources */, 9B3FFF141D0705810019A709 /* Notifications.swift in Sources */, 9BEEF0701D04DDB100FC52B3 /* ServerProfileManager.swift in Sources */, + 8EE2EDD8214F7CEC00FB4562 /* AppleScriptUserProxy.swift in Sources */, 9BEEF06E1D04DCE400FC52B3 /* ServerProfile.swift in Sources */, 9B3FFF0D1D05FEB30019A709 /* Utils.swift in Sources */, 9BEEF0751D04EF3E00FC52B3 /* PreferencesWindowController.swift in Sources */, diff --git a/ShadowsocksX-NG/AppDelegate.swift b/ShadowsocksX-NG/AppDelegate.swift index 57096620..3d89131c 100755 --- a/ShadowsocksX-NG/AppDelegate.swift +++ b/ShadowsocksX-NG/AppDelegate.swift @@ -40,12 +40,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele @IBOutlet weak var lanchAtLoginMenuItem: NSMenuItem! + @IBOutlet weak var hudWindow: NSPanel! @IBOutlet weak var panelView: NSView! @IBOutlet weak var isNameTextField: NSTextField! - + let kProfileMenuItemIndexBase = 100 - + var statusItem: NSStatusItem! static let StatusItemIconWidth: CGFloat = NSStatusItem.variableLength @@ -133,7 +134,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele self.updateServersMenu() self.updateRunningModeMenu() SyncSSLocal() - } + } ) _ = notifyCenter.rx.notification(NOTIFY_TOGGLE_RUNNING_SHORTCUT) .subscribe(onNext: { noti in @@ -180,9 +181,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele ProxyConfHelper.install() ProxyConfHelper.startMonitorPAC() applyConfig() - + // Register global hotkey ShortcutsController.bindShortcuts() + + // Start API Server + HTTPUserProxy.shard.start() } func applicationWillTerminate(_ aNotification: Notification) { @@ -191,7 +195,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele StopPrivoxy() ProxyConfHelper.disableProxy() } - + func applyConfig() { SyncSSLocal() @@ -211,7 +215,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele ProxyConfHelper.disableProxy() } } - + + func changeMode(mode:String!) { + let defaults = UserDefaults.standard + + switch mode{ + case "auto":defaults.setValue("auto", forKey: "ShadowsocksRunningMode") + case "global":defaults.setValue("global", forKey: "ShadowsocksRunningMode") + case "manual":defaults.setValue("manual", forKey: "ShadowsocksRunningMode") + default: fatalError() + } + + updateRunningModeMenu() + applyConfig() + } + // MARK: - UI Methods @IBAction func toggleRunning(_ sender: NSMenuItem) { self.doToggleRunning(showToast: false) @@ -324,26 +342,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele ]) } } - + @IBAction func selectPACMode(_ sender: NSMenuItem) { - let defaults = UserDefaults.standard - defaults.setValue("auto", forKey: "ShadowsocksRunningMode") - updateRunningModeMenu() - applyConfig() + changeMode(mode: "auto") } @IBAction func selectGlobalMode(_ sender: NSMenuItem) { - let defaults = UserDefaults.standard - defaults.setValue("global", forKey: "ShadowsocksRunningMode") - updateRunningModeMenu() - applyConfig() + changeMode(mode: "global") } @IBAction func selectManualMode(_ sender: NSMenuItem) { - let defaults = UserDefaults.standard - defaults.setValue("manual", forKey: "ShadowsocksRunningMode") - updateRunningModeMenu() - applyConfig() + changeMode(mode: "manual") } @IBAction func editServerPreferences(_ sender: NSMenuItem) { @@ -369,19 +378,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele allInOnePreferencesWinCtrl.window?.makeKeyAndOrderFront(self) } - @IBAction func selectServer(_ sender: NSMenuItem) { - let index = sender.tag - kProfileMenuItemIndexBase + func changeServer(@objc uuid: String) { let spMgr = ServerProfileManager.instance - let newProfile = spMgr.profiles[index] - if newProfile.uuid != spMgr.activeProfileId { - spMgr.setActiveProfiledId(newProfile.uuid) + + if uuid != spMgr.activeProfileId { + spMgr.setActiveProfiledId(uuid) updateServersMenu() SyncSSLocal() applyConfig() } + updateRunningModeMenu() } + @IBAction func selectServer(_ sender: NSMenuItem) { + let index = sender.tag - kProfileMenuItemIndexBase + let spMgr = ServerProfileManager.instance + let newProfileId = spMgr.profiles[index].uuid + + changeServer(uuid:newProfileId) + } + @IBAction func copyExportCommand(_ sender: NSMenuItem) { // Get the Http proxy config. let defaults = UserDefaults.standard @@ -427,7 +444,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele func updateRunningModeMenu() { let defaults = UserDefaults.standard - let mode = defaults.string(forKey: "ShadowsocksRunningMode") + let mode = defaults.string(forKey: "ShadowsocksRunningMosde") var serverMenuText = "Servers".localized @@ -468,12 +485,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele if isOn { if let m = mode { switch m { - case "auto": - statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_p_icon")) - case "global": - statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_g_icon")) - case "manual": - statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_m_icon")) + case "auto": + statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_p_icon")) + case "global": + statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_g_icon")) + case "manual": + statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_m_icon")) default: break } statusItem.image?.isTemplate = true @@ -512,9 +529,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele func updateServersMenu() { guard let menu = serversMenuItem.submenu else { return } + let mgr = ServerProfileManager.instance let profiles = mgr.profiles - // Remove all profile menu items let beginIndex = menu.index(of: serverProfilesBeginSeparatorMenuItem) + 1 let endIndex = menu.index(of: serverProfilesEndSeparatorMenuItem) @@ -522,7 +539,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele for index in (beginIndex.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + diff --git a/ShadowsocksX-NG/AppleScriptUserProxy.swift b/ShadowsocksX-NG/AppleScriptUserProxy.swift new file mode 100644 index 00000000..0890a1ac --- /dev/null +++ b/ShadowsocksX-NG/AppleScriptUserProxy.swift @@ -0,0 +1,72 @@ +// +// AppleScriptCommand.swift +// ShadowsocksX-NG +// +// Created by melonEater on 2018/9/6. +// Copyright © 2018 qiuyuzhou. All rights reserved. +// + +import Cocoa + + +class AppleScriptUserProxy: NSScriptCommand { + let appdeleget = NSApplication.shared.delegate as! AppDelegate + let SerMgr = ServerProfileManager.instance + + override func performDefaultImplementation() -> Any? { + switch(self.commandDescription.commandName) { + case "isRunning": + return isRunning() + case "toggle": + toggle() + case "mode": + return getMode() + case "change mode": + changeMode(mode: self.directParameter as! String) + case "servers": + return getServerList(); + case "change server": + setServer(remark: self.directParameter as! String) + default: + return nil; + } + return nil + } + + func toggle() { + self.appdeleget.doToggleRunning(showToast: false) + } + + func isRunning() -> Bool { + let isOn = UserDefaults.standard.bool(forKey: "ShadowsocksOn") + return isOn + } + + func getMode() -> String { + return UserDefaults.standard.string(forKey: "ShadowsocksRunningMode") as! String + } + + func changeMode(mode:String) { + appdeleget.changeMode(mode: mode) + } + + func getServerList() -> [String] { + var data = [String]() + + for each in self.SerMgr.profiles{ + data.append(each.remark) + } + + return data + } + + func setServer(remark: String) { + for each in self.SerMgr.profiles{ + if (each.remark == remark) { + self.appdeleget.changeServer(uuid: each.uuid) + return + } + } + } +} + diff --git a/ShadowsocksX-NG/HTTPUserProxy.swift b/ShadowsocksX-NG/HTTPUserProxy.swift new file mode 100644 index 00000000..5945a995 --- /dev/null +++ b/ShadowsocksX-NG/HTTPUserProxy.swift @@ -0,0 +1,88 @@ +// +// ApiServer.swift +// ShadowsocksX-R +// +// Created by CYC on 2016/10/9. +// Copyright © 2016年 qiuyuzhou. All rights reserved. +// + +import Foundation +import GCDWebServer + + + +class HTTPUserProxy{ + static let shard = HTTPUserProxy() + + let apiserver = GCDWebServer() + let SerMgr = ServerProfileManager.instance + let defaults = UserDefaults.standard + let appdeleget = NSApplication.shared.delegate as! AppDelegate + let api_port:UInt = 9528 + + func start(){ + setRouter() + do{ + try apiserver.start(options: [GCDWebServerOption_Port:api_port,"BindToLocalhost":true]) + }catch{ + NSLog("Error:ApiServ start fail") + } + } + + func setRouter(){ + apiserver.addHandler(forMethod: "GET", path: "/status", request: GCDWebServerRequest.self, processBlock: {request in + let isOn = self.defaults.bool(forKey: "ShadowsocksOn") + return GCDWebServerDataResponse(jsonObject: ["enable":isOn], contentType: "json") + }) + + apiserver.addHandler(forMethod: "POST", path: "/toggle", request: GCDWebServerRequest.self, processBlock: {request in + self.appdeleget.doToggleRunning(showToast: false) + return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") + }) + + apiserver.addHandler(forMethod: "GET", path: "/servers", request: GCDWebServerRequest.self, processBlock: {request in + + var data = [[String:Any]]() + + for each in self.SerMgr.profiles{ + data.append(["id":each.uuid,"remark":each.remark, + "active":self.SerMgr.activeProfileId == each.uuid ? 1 : 0]) + } + + return GCDWebServerDataResponse(jsonObject: data, contentType: "json") + }) + + apiserver.addHandler(forMethod: "POST", path: "/servers", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in + + let uuid = ((request as! GCDWebServerURLEncodedFormRequest).arguments["id"])as? String + for each in self.SerMgr.profiles{ + if (each.uuid == uuid) { + self.appdeleget.changeServer(uuid: uuid!) + return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") + + } + } + return GCDWebServerDataResponse(jsonObject: ["status":0], contentType: "json") + }) + + + apiserver.addHandler(forMethod: "GET", path: "/mode", request: GCDWebServerRequest.self, processBlock: {request in + if let current = self.defaults.string(forKey: "ShadowsocksRunningMode"){ + return GCDWebServerDataResponse(jsonObject: ["mode":current], contentType: "json") + } + return GCDWebServerDataResponse(jsonObject: ["mode":"unknow"], contentType: "json") + }) + + apiserver.addHandler(forMethod: "POST", path: "/mode", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in + let arg = ((request as! GCDWebServerURLEncodedFormRequest).arguments["mode"])as? String + + if (arg != "auto" && arg != "global" && arg != "manual") { + return GCDWebServerDataResponse(jsonObject: ["status":0], contentType: "json") + } + + self.appdeleget.changeMode(mode: arg!) + + return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") + }) + } +} diff --git a/ShadowsocksX-NG/Info.plist b/ShadowsocksX-NG/Info.plist index c48f1b80..05adbfc0 100644 --- a/ShadowsocksX-NG/Info.plist +++ b/ShadowsocksX-NG/Info.plist @@ -2,6 +2,10 @@ + OSAScriptingDefinition + AppleScriptDefinition.sdef + NSAppleScriptEnabled + CFBundleDevelopmentRegion en CFBundleExecutable From 11e05fcc3384850e7230083b96bb4dc108e7c232 Mon Sep 17 00:00:00 2001 From: yangziy Date: Mon, 17 Sep 2018 20:21:42 +0800 Subject: [PATCH 02/11] Add HTTP API document. --- HTTPAPI.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 HTTPAPI.md diff --git a/HTTPAPI.md b/HTTPAPI.md new file mode 100644 index 00000000..0830580a --- /dev/null +++ b/HTTPAPI.md @@ -0,0 +1,107 @@ +# Features + +* Check current status (on/off) + +- Toggle the client + +- Get server list + +- Switch server + +- Get current mode + +- Switch mode + +# Specification + +URL: http://localhost:9528/ + +- #### Check current status (on/off) `GET /status` + +###### Sample Return + +``` +{ + "enable": true +} +``` + +- #### Toggle the client `POST /toggle` + +###### Sample Return + +``` +{ + "status": 1 + // 1 for toggle succeed, 0 for fail +} +``` + +- #### Get server list `GET /servers` + +###### Sample Return + +``` +[ + { + "active": 1, + "id": "93C547E0-49C9-1234-9CAD-EE8D5C4A1A8F", + "remark": "us1", + // remark: as in Server Preferences Panel of the app. + }, + { + "active" : 0, + "id" : "71552DCD-B298-495E-904E-82DA4B07AEF8", + "remark" : "hk2" + }, + { + "active" : 0, + "id" : "E8879F3D-95AE-4714-BC04-9B271C2BC52D", + "remark" : "jp1" + },... +] +``` + +- #### Switch server `POST /servers` + +###### Argument + +| Name | Description | Sample | +| ---- | ----------------------------- | -------------------------------------- | +| id | As returned in `GET /servers` | "E8879F3D-95AE-4714-BC04-9B271C2BC52D" | + +###### Sample Return + +``` +{ + "status": 1 + // 1 for succeed, 0 for fail +} +``` + +If the `id` is invalid or fail to match any `id` in config, "status" = 0. + +- #### Get current mode `GET /mode` + +###### Sample Return + +``` +{ + "mode": "auto" +} +``` + + `mode`∈ {"auto", "global", "manual"}. + +- #### Switch mode `POST /mode` + +###### Sample Return + +``` +{ + "status": 1 + // 1 for succeed, 0 for fail +} +``` + +If the `mode`∉ {"auto", "global", "manual"}, "status" = 0. \ No newline at end of file From 6ca21307f1811c70780194d8d3b3ca78114ff796 Mon Sep 17 00:00:00 2001 From: yangziy Date: Mon, 17 Sep 2018 22:57:23 +0800 Subject: [PATCH 03/11] Rewrite server-relevant API. Update document. --- HTTPAPI.md | 134 +++++++++++++++++++++------- ShadowsocksX-NG/HTTPUserProxy.swift | 72 +++++++++++++-- ShadowsocksX-NG/ServerProfile.swift | 25 +++--- 3 files changed, 181 insertions(+), 50 deletions(-) diff --git a/HTTPAPI.md b/HTTPAPI.md index 0830580a..9758b975 100644 --- a/HTTPAPI.md +++ b/HTTPAPI.md @@ -3,13 +3,12 @@ * Check current status (on/off) - Toggle the client - - Get server list - -- Switch server - +- Get current server +- Select server +- Add new / modify existing server +- Delete server - Get current mode - - Switch mode # Specification @@ -26,60 +25,124 @@ URL: http://localhost:9528/ } ``` -- #### Toggle the client `POST /toggle` +- #### Toggle the client `POST /status` ###### Sample Return ``` { "status": 1 - // 1 for toggle succeed, 0 for fail } ``` -- #### Get server list `GET /servers` +`1` for success, `0` for failure. + +- #### Get server list `GET /server/list` ###### Sample Return ``` [ - { - "active": 1, - "id": "93C547E0-49C9-1234-9CAD-EE8D5C4A1A8F", - "remark": "us1", - // remark: as in Server Preferences Panel of the app. - }, - { - "active" : 0, - "id" : "71552DCD-B298-495E-904E-82DA4B07AEF8", - "remark" : "hk2" - }, - { - "active" : 0, - "id" : "E8879F3D-95AE-4714-BC04-9B271C2BC52D", - "remark" : "jp1" - },... + { + "Id" : "93C127E0-49C9-4332-9CAD-EE6B9A3D1A8F", + "Method" : "chacha20-ietf-poly1305", + "Password" : "password", + "Plugin" : "", + "PluginOptions" : "", + "Remark" : "jp1", + "ServerHost" : "jp1-sta40.somehost.com", + "ServerPort" : 49234 + }, + { + "Id" : "71552DCD-B298-4591-B59A-82DA4B07AEF8", + "Method" : "chacha20-ietf-poly1305", + "Password" : "password", + "Plugin" : "", + "PluginOptions" : "", + "Remark" : "us1", + "ServerHost" : "us1-sta40.somehost.com", + "ServerPort" : 49234 + },... ] ``` -- #### Switch server `POST /servers` +- #### Get current server `GET /server/current` + +###### Sample Return + +``` +{ + "Id" : "93C127E0-49C9-4332-9CAD-EE6B9A3D1A8F" +} +``` + +- #### Select server `POST /server/current` ###### Argument -| Name | Description | Sample | -| ---- | ----------------------------- | -------------------------------------- | -| id | As returned in `GET /servers` | "E8879F3D-95AE-4714-BC04-9B271C2BC52D" | +| Name | Description | Sample | +| ---- | --------------------------------- | -------------------------------------- | +| Id | As returned in `GET /server/list` | "71552DCD-B298-4591-B59A-82DA4B07AEF8" | ###### Sample Return ``` { "status": 1 - // 1 for succeed, 0 for fail } ``` -If the `id` is invalid or fail to match any `id` in config, "status" = 0. +If the `Id` is invalid or fail to match any id in config, `"status": 0`. + +- #### Add Server / Modify Existing Server `POST /server ` + +###### Argument + +| Name | Sample | +| ------------- | ---------------------- | +| ServerPort | 49234 | +| ServerHost | jp1-sta40.somehost.com | +| Remark | jp1 | +| PluginOptions | | +| Plugin | | +| Password | Password | +| Method | chacha20-ietf-poly1305 | + +To indicate modification, pass `Id` in addition. + +| Name | Description | Sample | +| ---- | --------------------------------- | -------------------------------------- | +| Id | As returned in `GET /server/list` | "71552DCD-B298-4591-B59A-82DA4B07AEF8" | + +For meaning of the arguments, refer to `GET /server/list` and the Server Perferences Panel of the app. + +###### Sample Return + +``` +{ + "status": 1 +} +``` + +- #### Delete Server `DELETE /server` + +###### Argument + +| Name | Description | Sample | +| ---- | --------------------------------- | -------------------------------------- | +| Id | As returned in `GET /server/list` | "71552DCD-B298-4591-B59A-82DA4B07AEF8" | + +###### Sample Return + +``` +{ + "status": 1 +} +``` + +If `Id` == id of current server, operation will no effect, `"status":0`. + +If `Id` not match, `"status":0`. - #### Get current mode `GET /mode` @@ -95,6 +158,12 @@ If the `id` is invalid or fail to match any `id` in config, "status" = 0. - #### Switch mode `POST /mode` +###### Argument + +| Name | Description | Sample | +| ---- | -------------------------- | -------- | +| mode | As returned in `GET /mode` | "global" | + ###### Sample Return ``` @@ -104,4 +173,7 @@ If the `id` is invalid or fail to match any `id` in config, "status" = 0. } ``` -If the `mode`∉ {"auto", "global", "manual"}, "status" = 0. \ No newline at end of file +--- + +All json names are case sensitive. Be careful. + diff --git a/ShadowsocksX-NG/HTTPUserProxy.swift b/ShadowsocksX-NG/HTTPUserProxy.swift index 5945a995..d477ae6b 100644 --- a/ShadowsocksX-NG/HTTPUserProxy.swift +++ b/ShadowsocksX-NG/HTTPUserProxy.swift @@ -35,36 +35,92 @@ class HTTPUserProxy{ return GCDWebServerDataResponse(jsonObject: ["enable":isOn], contentType: "json") }) - apiserver.addHandler(forMethod: "POST", path: "/toggle", request: GCDWebServerRequest.self, processBlock: {request in + apiserver.addHandler(forMethod: "POST", path: "/status", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in self.appdeleget.doToggleRunning(showToast: false) return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") }) - apiserver.addHandler(forMethod: "GET", path: "/servers", request: GCDWebServerRequest.self, processBlock: {request in + apiserver.addHandler(forMethod: "GET", path: "/server/list", request: GCDWebServerRequest.self, processBlock: {request in var data = [[String:Any]]() for each in self.SerMgr.profiles{ - data.append(["id":each.uuid,"remark":each.remark, - "active":self.SerMgr.activeProfileId == each.uuid ? 1 : 0]) + data.append(each.toDictionary()) } return GCDWebServerDataResponse(jsonObject: data, contentType: "json") }) - apiserver.addHandler(forMethod: "POST", path: "/servers", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in + apiserver.addHandler(forMethod: "GET", path: "/server/current", request: GCDWebServerRequest.self, processBlock: {request in - let uuid = ((request as! GCDWebServerURLEncodedFormRequest).arguments["id"])as? String + return GCDWebServerDataResponse(jsonObject: ["Id":self.SerMgr.activeProfileId], contentType: "json") + }) + + apiserver.addHandler(forMethod: "POST", path: "/server/current", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in + + let uuid = ((request as! GCDWebServerURLEncodedFormRequest).arguments["Id"])as? String for each in self.SerMgr.profiles{ if (each.uuid == uuid) { self.appdeleget.changeServer(uuid: uuid!) return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") - + + } + } + return GCDWebServerDataResponse(jsonObject: ["status":0], contentType: "json") + }) + + apiserver.addHandler(forMethod: "POST", path: "/server", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in + + var data = ((request as! GCDWebServerURLEncodedFormRequest).arguments) as! [String: Any] + data["ServerPort"] = Double(data["ServerPort"] as! String) + let id = data["Id"] as? String + if (id != nil) { + for each in self.SerMgr.profiles{ + if (each.uuid == id) { + ServerProfile.copy(fromDict: data, toProfile: each) + if (each.isValid()) { + self.SerMgr.save() + self.appdeleget.updateServersMenu() + return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") + } + } } } + else { + let profile = ServerProfile.fromDictionary(data) + if (profile.isValid()) { + self.SerMgr.profiles.append(profile) + self.SerMgr.save() + self.appdeleget.updateServersMenu() + return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") + } + } + return GCDWebServerDataResponse(jsonObject: ["status":0], contentType: "json") }) + apiserver.addHandler(forMethod: "DELETE", path: "/server", request: GCDWebServerRequest.self + , processBlock: {request in + + let uuid = (request.query?["Id"])as! String + + if (uuid == self.SerMgr.activeProfileId) { + return GCDWebServerDataResponse(jsonObject: ["status":0], contentType: "json") + } + + for i in 0.. ServerProfile { + static func copy(fromDict:[String:Any?], toProfile:ServerProfile) { let cp = { (profile: ServerProfile) in - profile.serverHost = data["ServerHost"] as! String - profile.serverPort = (data["ServerPort"] as! NSNumber).uint16Value - profile.method = data["Method"] as! String - profile.password = data["Password"] as! String - if let remark = data["Remark"] { + profile.serverHost = fromDict["ServerHost"] as! String + profile.serverPort = (fromDict["ServerPort"] as! NSNumber).uint16Value + profile.method = fromDict["Method"] as! String + profile.password = fromDict["Password"] as! String + if let remark = fromDict["Remark"] { profile.remark = remark as! String } - if let plugin = data["Plugin"] as? String { + if let plugin = fromDict["Plugin"] as? String { profile.plugin = plugin } - if let pluginOptions = data["PluginOptions"] as? String { + if let pluginOptions = fromDict["PluginOptions"] as? String { profile.pluginOptions = pluginOptions } } - + cp(toProfile) + } + + static func fromDictionary(_ data:[String:Any?]) -> ServerProfile { if let id = data["Id"] as? String { let profile = ServerProfile(uuid: id) - cp(profile) + copy(fromDict: data, toProfile: profile) return profile } else { let profile = ServerProfile() - cp(profile) + copy(fromDict: data, toProfile: profile) return profile } } From ea3e2186234ba8795d978c94d2762a48a2d92184 Mon Sep 17 00:00:00 2001 From: yangziy Date: Mon, 24 Sep 2018 18:45:33 +0800 Subject: [PATCH 04/11] Replace README.md & Move HTTPAPI.md to master. --- HTTPAPI.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 71 +++++--------------- 2 files changed, 208 insertions(+), 54 deletions(-) create mode 100644 HTTPAPI.md diff --git a/HTTPAPI.md b/HTTPAPI.md new file mode 100644 index 00000000..0eec8a5c --- /dev/null +++ b/HTTPAPI.md @@ -0,0 +1,191 @@ +# Features + +* Check current status (on/off) + +- Toggle the client +- Get server list +- Get current server +- Select server +- Add new / modify existing server +- Delete server +- Get current mode +- Switch mode + +# Specification + +baseURL: http://localhost:9528/ + +- #### Check current status (on/off) `GET /status` + +###### Sample Shell command + +```shell +$ curl -X GET http://localhost:9528/status +``` + +###### Sample Return + +```json +{"enable": true} +``` + +- #### Turn on/off the client `POST /status` + +###### Sample Shell command + +```shell +$ curl -X POST http://localhost:9528/status +``` + +The command above will toggle the client. + +Or you may want to specify the argument + +```shell +$ curl -X POST -d 'enable=false' http://localhost:9528/status +``` + +###### Sample Return + +```json +{"status": 1} +``` + +**Note**: `1` for command succeed, `0` for fail. + +- #### Get server list `GET /server/list` + +###### Sample Shell command + +```shell +$ curl -X GET http://localhost:9528/server/list +``` + +###### Sample Return + +```json +[ + { + "Id" : "93C127E0-49C9-4332-9CAD-EE6B9A3D1A8F", + "Method" : "chacha20-ietf-poly1305", + "Password" : "password", + "Plugin" : "", + "PluginOptions" : "", + "Remark" : "jp1", + "ServerHost" : "jp1-sta40.somehost.com", + "ServerPort" : 49234 + }, + { + "Id" : "71552DCD-B298-4591-B59A-82DA4B07AEF8", + "Method" : "chacha20-ietf-poly1305", + "Password" : "password", + "Plugin" : "", + "PluginOptions" : "", + "Remark" : "us1", + "ServerHost" : "us1-sta40.somehost.com", + "ServerPort" : 49234 + },... +] +``` + +- #### Get current server `GET /server/current` + +###### Sample Shell command + +```shell +$ curl -X GET http://localhost:9528/server/current +``` + +###### Sample Return + +```json +{"Id" : "93C127E0-49C9-4332-9CAD-EE6B9A3D1A8F"} +``` + +- #### Select server `POST /server/current` + +###### Sample Shell command + +```shell +$ curl -X POST -d 'Id=71552DCD-B298-4591-B59A-82DA4B07AEF8' http://localhost:9528/server/current +``` + +###### Sample Return + +```json +{"status": 1} +``` + +If the `Id` is invalid or fail to match any id in config, `"status": 0`. + +- #### Add Server / Modify Existing Server `POST /server ` + +Sample Shell command + +```shell +$ curl -X POST -d \ +'ServerPort=49234&ServerHost=tw1-sta40.somehost.com&Remark=someRemark&PluginOptions=&Plugin=&Password=myPassword&Method=chacha20-ietf-poly1305' http://localhost:9528/server +``` + +To indicate modification, pass `Id` in addition. + +```shell +$ curl -X POST -d \ +'Id=71552DCD-B298-4591-B59A-82DA4B07AEF8&ServerPort=49234&ServerHost=tw1-sta40.somehost.com&Remark=someRemark&PluginOptions=&Plugin=&Password=myPassword&Method=chacha20-ietf-poly1305' http://localhost:9528/server +``` + +For meaning of the arguments, refer to `GET /server/list` and the Server Perferences Panel of the app. + +###### Sample Return + +```json +{"status": 1} +``` + +- #### Delete Server `DELETE /server` + +Sample Shell command + +```shell +$ curl -X POST -d 'Id=71552DCD-B298-4591-B59A-82DA4B07AEF8' http://localhost:9528/server +``` + +Sample Return + +```json +{"status": 1} +``` + +If `Id` == id of current server, operation will no effect, `"status":0`. + +If `Id` not match, `"status":0`. + +- #### Get current mode `GET /mode` + +###### Sample Shell command + +```shell +$ curl -X GET http://localhost:9528/mode +``` + +###### Sample Return + +```json +{"mode": "auto"} +``` + + `mode`∈ {"auto", "global", "manual"}. + +- #### Switch mode `POST /mode` + +###### Sample Shell command + +```shell +$ curl -X POST -d 'mode=global' http://localhost:9528/status +``` + +###### Sample Return + +```json +{"status": 1} +``` diff --git a/README.md b/README.md index 926f376d..d8b300db 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,26 @@ -# ShadowsocksX-NG +# ShadowsocksX-NG, API Supported Current version is 1.8.1 -[![Build Status](https://travis-ci.org/shadowsocks/ShadowsocksX-NG.svg?branch=develop)](https://travis-ci.org/shadowsocks/ShadowsocksX-NG) +## Introduction -Next Generation of [ShadowsocksX](https://github.com/shadowsocks/shadowsocks-iOS) +[shadowsocks/**ShadowsocksX-NG**](https://github.com/shadowsocks/ShadowsocksX-NG) doesn't support Alfred control. So I steal code from [yichengchen/**ShadowsocksX-R**](https://github.com/yichengchen/ShadowsocksX-R/blob/42b409beb85aee19a4852e09e7c3e4c2f73f49d3/ShadowsocksX-NG/ApiServer.swift) , replace obsolete methods to get it work. Now the app is equipped with **HTTP API**. You may want to download the **Alfred workflow** from [yangziy/Alfred_ShadowsocksController](https://github.com/yangziy/Alfred_ShadowsocksController). -## Why a new implementation? +All credits go to the original authors. Any distribution & reproduction must follow the original licenses. [GPLv3](https://www.gnu.org/licenses/quick-guide-gplv3.en.html) -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. +## Feature -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. +The **HTTP API** enables users to do the following: -## Requirements - -### Running - -- macOS 10.11+ - -### Building - -- Xcode 9.4.1+ -- CocoaPods 1.5.3+ - -## Download - -From [here](https://github.com/shadowsocks/ShadowsocksX-NG/releases/) - -## Features - -- `ss-local` from shadowsocks-libev 3.2.0 -- Support SIP003 plugins. Embed `kcptun` and `simple-obfs`. -- Could update PAC by download GFW List from GitHub. -- Share your server profiles by qrcode or url. -- Import server profile urls from pasteboard. -- Import server profile by scan QRCode on screen. -- Custom rules for PAC. -- Support for [AEAD Ciphers](https://shadowsocks.org/en/spec/AEAD-Ciphers.html) -- HTTP Proxy by [privoxy](http://www.privoxy.org/) - -## Difference from original ShadowsocksX - -`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, -so that you could configure your apps to use the SOCKS5 proxy manually. - -## Contributing - -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/) - -## License - -The project is released under the terms of the GPLv3. +- Check current status (on/off) +- Toggle the client +- Get server list +- Add new server +- Modify server +- Delete server +- Get current server +- Select server +- Get current mode +- Switch mode +For usage, consult [HTTP API Specification](https://github.com/yangziy/ShadowsocksX-NG_WithAPI/blob/master/HTTPAPI.md) . \ No newline at end of file From b11ac3eaebd52e3b4ecdea617db64f72a996e1d0 Mon Sep 17 00:00:00 2001 From: yangziy Date: Mon, 24 Sep 2018 20:51:54 +0800 Subject: [PATCH 05/11] Modify README.md & API document. --- HTTPAPI.md | 4 ++-- README.md | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/HTTPAPI.md b/HTTPAPI.md index 0eec8a5c..08d03b61 100644 --- a/HTTPAPI.md +++ b/HTTPAPI.md @@ -2,7 +2,7 @@ * Check current status (on/off) -- Toggle the client +- Turn on/off or toggle the client - Get server list - Get current server - Select server @@ -29,7 +29,7 @@ $ curl -X GET http://localhost:9528/status {"enable": true} ``` -- #### Turn on/off the client `POST /status` +- #### Turn on/off or toggle the client `POST /status` ###### Sample Shell command diff --git a/README.md b/README.md index d8b300db..d7009090 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,21 @@ # ShadowsocksX-NG, API Supported -Current version is 1.8.1 +Current version: 1.8.1 + +This is a fork with minor feature added. All credits go to the **original authors**. ## Introduction -[shadowsocks/**ShadowsocksX-NG**](https://github.com/shadowsocks/ShadowsocksX-NG) doesn't support Alfred control. So I steal code from [yichengchen/**ShadowsocksX-R**](https://github.com/yichengchen/ShadowsocksX-R/blob/42b409beb85aee19a4852e09e7c3e4c2f73f49d3/ShadowsocksX-NG/ApiServer.swift) , replace obsolete methods to get it work. Now the app is equipped with **HTTP API**. You may want to download the **Alfred workflow** from [yangziy/Alfred_ShadowsocksController](https://github.com/yangziy/Alfred_ShadowsocksController). +[shadowsocks/**ShadowsocksX-NG**](https://github.com/shadowsocks/ShadowsocksX-NG) doesn't support Alfred control. So I copy code from its obsolete fork [yichengchen/**ShadowsocksX-R**](https://github.com/yichengchen/ShadowsocksX-R/blob/42b409beb85aee19a4852e09e7c3e4c2f73f49d3/ShadowsocksX-NG/ApiServer.swift) to euip the app with **HTTP API**, enabling Alfred Control. You may want to download the **Alfred workflow** from [yangziy/Alfred_ShadowsocksController](https://github.com/yangziy/Alfred_ShadowsocksController). -All credits go to the original authors. Any distribution & reproduction must follow the original licenses. [GPLv3](https://www.gnu.org/licenses/quick-guide-gplv3.en.html) +With the **HTTP API** you could also control the app with **curl** in **terminal**. ## Feature The **HTTP API** enables users to do the following: - Check current status (on/off) -- Toggle the client +- Turn on/off or toggle the client - Get server list - Add new server - Modify server From 8b0631665ed85885b7f951bf4fdcf1790c421162 Mon Sep 17 00:00:00 2001 From: yangziy Date: Tue, 25 Sep 2018 17:08:06 +0800 Subject: [PATCH 06/11] Remove AppleScript Support --- ShadowsocksX-NG.xcodeproj/project.pbxproj | 8 --- ShadowsocksX-NG/AppleScriptDefinition.sdef | 51 --------------- ShadowsocksX-NG/AppleScriptUserProxy.swift | 72 ---------------------- ShadowsocksX-NG/Info.plist | 4 -- 4 files changed, 135 deletions(-) delete mode 100644 ShadowsocksX-NG/AppleScriptDefinition.sdef delete mode 100644 ShadowsocksX-NG/AppleScriptUserProxy.swift diff --git a/ShadowsocksX-NG.xcodeproj/project.pbxproj b/ShadowsocksX-NG.xcodeproj/project.pbxproj index 54ed005e..94068355 100755 --- a/ShadowsocksX-NG.xcodeproj/project.pbxproj +++ b/ShadowsocksX-NG.xcodeproj/project.pbxproj @@ -11,9 +11,7 @@ 1C82DBA81FA96C7500B32551 /* obfs-local in Resources */ = {isa = PBXBuildFile; fileRef = 1C82DBA51FA96C7400B32551 /* obfs-local */; }; 1C82DBAA1FA96FB600B32551 /* install_simple_obfs.sh in Resources */ = {isa = PBXBuildFile; fileRef = 1C82DBA91FA96F0300B32551 /* install_simple_obfs.sh */; }; 258E511BA910B0521B24DAB8 /* Pods_ShadowsocksX_NG.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 283ED1A8E9B711AC65670031 /* Pods_ShadowsocksX_NG.framework */; }; - 8EE2EDD8214F7CEC00FB4562 /* AppleScriptUserProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE2EDD4214F7CEC00FB4562 /* AppleScriptUserProxy.swift */; }; 8EE2EDD9214F7CEC00FB4562 /* HTTPUserProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */; }; - 8EE2EDDA214F7CEC00FB4562 /* AppleScriptDefinition.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8EE2EDD7214F7CEC00FB4562 /* AppleScriptDefinition.sdef */; }; 9B07EFA71D048BBB0052D9DF /* ss-local in Resources */ = {isa = PBXBuildFile; fileRef = 9B07EFA61D048BBB0052D9DF /* ss-local */; }; 9B07EFAC1D048E880052D9DF /* menu_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B07EFA81D048E880052D9DF /* menu_icon@2x.png */; }; 9B07EFAD1D048E880052D9DF /* menu_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B07EFA91D048E880052D9DF /* menu_icon.png */; }; @@ -150,9 +148,7 @@ 50D54926AA21B0D4D8DD9C4F /* Pods-ShadowsocksX-NGUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NGUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NGUITests/Pods-ShadowsocksX-NGUITests.release.xcconfig"; sourceTree = ""; }; 58907E7F50405104B42CB189 /* Pods-ShadowsocksX-NGUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NGUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NGUITests/Pods-ShadowsocksX-NGUITests.debug.xcconfig"; sourceTree = ""; }; 5B6203C1228FCD3D365814AC /* Pods-ShadowsocksX-NGTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NGTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NGTests/Pods-ShadowsocksX-NGTests.debug.xcconfig"; sourceTree = ""; }; - 8EE2EDD4214F7CEC00FB4562 /* AppleScriptUserProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleScriptUserProxy.swift; sourceTree = ""; }; 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPUserProxy.swift; sourceTree = ""; }; - 8EE2EDD7214F7CEC00FB4562 /* AppleScriptDefinition.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = AppleScriptDefinition.sdef; sourceTree = ""; }; 9B07EFA61D048BBB0052D9DF /* ss-local */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = "ss-local"; sourceTree = ""; }; 9B07EFA81D048E880052D9DF /* menu_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icon@2x.png"; sourceTree = ""; }; 9B07EFA91D048E880052D9DF /* menu_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icon.png; sourceTree = ""; }; @@ -370,8 +366,6 @@ isa = PBXGroup; children = ( 9BB706A51D1B982300551F0E /* SWBApplication.m */, - 8EE2EDD7214F7CEC00FB4562 /* AppleScriptDefinition.sdef */, - 8EE2EDD4214F7CEC00FB4562 /* AppleScriptUserProxy.swift */, 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */, 9BB706A61D1B982300551F0E /* SWBApplication.h */, 9B3FFF511D09DBA20019A709 /* ShadowsocksX-NG-Bridging-Header.h */, @@ -659,7 +653,6 @@ 9B3FFF341D08CEF70019A709 /* SWBQRCodeWindowController.xib in Resources */, 9B3FFF231D088E8D0019A709 /* abp.js in Resources */, 9B07EFAD1D048E880052D9DF /* menu_icon.png in Resources */, - 8EE2EDDA214F7CEC00FB4562 /* AppleScriptDefinition.sdef in Resources */, 9BAFE2E21E83ED7F00F71CCE /* PreferencesWinController.xib in Resources */, 9B0BFFEB1D0460A70040E62B /* Assets.xcassets in Resources */, 08FCA0FF1E24BE1A0070984F /* example-gui-config.json in Resources */, @@ -854,7 +847,6 @@ 9B3FFF211D08826E0019A709 /* PACUtils.swift in Sources */, 9B3FFF141D0705810019A709 /* Notifications.swift in Sources */, 9BEEF0701D04DDB100FC52B3 /* ServerProfileManager.swift in Sources */, - 8EE2EDD8214F7CEC00FB4562 /* AppleScriptUserProxy.swift in Sources */, 9BEEF06E1D04DCE400FC52B3 /* ServerProfile.swift in Sources */, 9B3FFF0D1D05FEB30019A709 /* Utils.swift in Sources */, 9BEEF0751D04EF3E00FC52B3 /* PreferencesWindowController.swift in Sources */, diff --git a/ShadowsocksX-NG/AppleScriptDefinition.sdef b/ShadowsocksX-NG/AppleScriptDefinition.sdef deleted file mode 100644 index 80288ad1..00000000 --- a/ShadowsocksX-NG/AppleScriptDefinition.sdef +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - diff --git a/ShadowsocksX-NG/AppleScriptUserProxy.swift b/ShadowsocksX-NG/AppleScriptUserProxy.swift deleted file mode 100644 index 0890a1ac..00000000 --- a/ShadowsocksX-NG/AppleScriptUserProxy.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// AppleScriptCommand.swift -// ShadowsocksX-NG -// -// Created by melonEater on 2018/9/6. -// Copyright © 2018 qiuyuzhou. All rights reserved. -// - -import Cocoa - - -class AppleScriptUserProxy: NSScriptCommand { - let appdeleget = NSApplication.shared.delegate as! AppDelegate - let SerMgr = ServerProfileManager.instance - - override func performDefaultImplementation() -> Any? { - switch(self.commandDescription.commandName) { - case "isRunning": - return isRunning() - case "toggle": - toggle() - case "mode": - return getMode() - case "change mode": - changeMode(mode: self.directParameter as! String) - case "servers": - return getServerList(); - case "change server": - setServer(remark: self.directParameter as! String) - default: - return nil; - } - return nil - } - - func toggle() { - self.appdeleget.doToggleRunning(showToast: false) - } - - func isRunning() -> Bool { - let isOn = UserDefaults.standard.bool(forKey: "ShadowsocksOn") - return isOn - } - - func getMode() -> String { - return UserDefaults.standard.string(forKey: "ShadowsocksRunningMode") as! String - } - - func changeMode(mode:String) { - appdeleget.changeMode(mode: mode) - } - - func getServerList() -> [String] { - var data = [String]() - - for each in self.SerMgr.profiles{ - data.append(each.remark) - } - - return data - } - - func setServer(remark: String) { - for each in self.SerMgr.profiles{ - if (each.remark == remark) { - self.appdeleget.changeServer(uuid: each.uuid) - return - } - } - } -} - diff --git a/ShadowsocksX-NG/Info.plist b/ShadowsocksX-NG/Info.plist index 413ac6ac..9588eaef 100644 --- a/ShadowsocksX-NG/Info.plist +++ b/ShadowsocksX-NG/Info.plist @@ -2,10 +2,6 @@ - OSAScriptingDefinition - AppleScriptDefinition.sdef - NSAppleScriptEnabled - CFBundleDevelopmentRegion en CFBundleExecutable From 9d6d1f3bbe88b5e97629cfbab2b91f472399b590 Mon Sep 17 00:00:00 2001 From: yangziy Date: Wed, 26 Sep 2018 14:55:43 +0800 Subject: [PATCH 07/11] Revert previously refactored code in AppDelegate.swift, ServerProfileManager.swift; add class APIAdapter. API Modification. --- ShadowsocksX-NG/AppDelegate.swift | 84 +++---- ShadowsocksX-NG/HTTPUserProxy.swift | 360 ++++++++++++++++++++-------- ShadowsocksX-NG/ServerProfile.swift | 25 +- 3 files changed, 301 insertions(+), 168 deletions(-) diff --git a/ShadowsocksX-NG/AppDelegate.swift b/ShadowsocksX-NG/AppDelegate.swift index 2a013319..2bb77e1a 100755 --- a/ShadowsocksX-NG/AppDelegate.swift +++ b/ShadowsocksX-NG/AppDelegate.swift @@ -40,13 +40,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele @IBOutlet weak var lanchAtLoginMenuItem: NSMenuItem! - @IBOutlet weak var hudWindow: NSPanel! @IBOutlet weak var panelView: NSView! @IBOutlet weak var isNameTextField: NSTextField! - + let kProfileMenuItemIndexBase = 100 - + var statusItem: NSStatusItem! static let StatusItemIconWidth: CGFloat = NSStatusItem.variableLength @@ -136,7 +135,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele self.updateServersMenu() self.updateRunningModeMenu() SyncSSLocal() - } + } ) _ = notifyCenter.rx.notification(NOTIFY_TOGGLE_RUNNING_SHORTCUT) .subscribe(onNext: { noti in @@ -183,11 +182,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele ProxyConfHelper.install() ProxyConfHelper.startMonitorPAC() applyConfig() - + // Register global hotkey ShortcutsController.bindShortcuts() - // Start API Server + // Start HTTP API Server HTTPUserProxy.shard.start() } @@ -197,7 +196,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele StopPrivoxy() ProxyConfHelper.disableProxy() } - + func applyConfig() { SyncSSLocal() @@ -217,21 +216,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele ProxyConfHelper.disableProxy() } } - - func changeMode(mode:String!) { - let defaults = UserDefaults.standard - - switch mode{ - case "auto":defaults.setValue("auto", forKey: "ShadowsocksRunningMode") - case "global":defaults.setValue("global", forKey: "ShadowsocksRunningMode") - case "manual":defaults.setValue("manual", forKey: "ShadowsocksRunningMode") - default: fatalError() - } - - updateRunningModeMenu() - applyConfig() - } - + // MARK: - UI Methods @IBAction func toggleRunning(_ sender: NSMenuItem) { self.doToggleRunning(showToast: false) @@ -316,17 +301,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele ]) } } - + @IBAction func selectPACMode(_ sender: NSMenuItem) { - changeMode(mode: "auto") + let defaults = UserDefaults.standard + defaults.setValue("auto", forKey: "ShadowsocksRunningMode") + updateRunningModeMenu() + applyConfig() } @IBAction func selectGlobalMode(_ sender: NSMenuItem) { - changeMode(mode: "global") + let defaults = UserDefaults.standard + defaults.setValue("global", forKey: "ShadowsocksRunningMode") + updateRunningModeMenu() + applyConfig() } @IBAction func selectManualMode(_ sender: NSMenuItem) { - changeMode(mode: "manual") + let defaults = UserDefaults.standard + defaults.setValue("manual", forKey: "ShadowsocksRunningMode") + updateRunningModeMenu() + applyConfig() } @IBAction func editServerPreferences(_ sender: NSMenuItem) { @@ -351,27 +345,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele allInOnePreferencesWinCtrl.window?.makeKeyAndOrderFront(self) } - func changeServer(@objc uuid: String) { + @IBAction func selectServer(_ sender: NSMenuItem) { + let index = sender.tag - kProfileMenuItemIndexBase let spMgr = ServerProfileManager.instance - - if uuid != spMgr.activeProfileId { - spMgr.setActiveProfiledId(uuid) + let newProfile = spMgr.profiles[index] + if newProfile.uuid != spMgr.activeProfileId { + spMgr.setActiveProfiledId(newProfile.uuid) updateServersMenu() SyncSSLocal() applyConfig() } - updateRunningModeMenu() } - @IBAction func selectServer(_ sender: NSMenuItem) { - let index = sender.tag - kProfileMenuItemIndexBase - let spMgr = ServerProfileManager.instance - let newProfileId = spMgr.profiles[index].uuid - - changeServer(uuid:newProfileId) - } - @IBAction func copyExportCommand(_ sender: NSMenuItem) { // Get the Http proxy config. let defaults = UserDefaults.standard @@ -417,7 +403,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele func updateRunningModeMenu() { let defaults = UserDefaults.standard - let mode = defaults.string(forKey: "ShadowsocksRunningMosde") + let mode = defaults.string(forKey: "ShadowsocksRunningMode") var serverMenuText = "Servers".localized @@ -458,12 +444,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele if isOn { if let m = mode { switch m { - case "auto": - statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_p_icon")) - case "global": - statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_g_icon")) - case "manual": - statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_m_icon")) + case "auto": + statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_p_icon")) + case "global": + statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_g_icon")) + case "manual": + statusItem.image = NSImage(named: NSImage.Name(rawValue: "menu_m_icon")) default: break } statusItem.image?.isTemplate = true @@ -502,9 +488,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele func updateServersMenu() { guard let menu = serversMenuItem.submenu else { return } - let mgr = ServerProfileManager.instance let profiles = mgr.profiles + // Remove all profile menu items let beginIndex = menu.index(of: serverProfilesBeginSeparatorMenuItem) + 1 let endIndex = menu.index(of: serverProfilesEndSeparatorMenuItem) @@ -512,7 +498,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele for index in (beginIndex..Bool { + return self.defaults.bool(forKey: "ShadowsocksOn"); + } + + func setStatus(status:Bool) { + if (status == self.defaults.bool(forKey: "ShadowsocksOn")) { + return; + } + else { + appdeleget.doToggleRunning(showToast: false) + } + } + + func toggleStatus() { + appdeleget.doToggleRunning(showToast: false) + } + + func getServerList()->[Dictionary] { + return self.SerMgr.profiles.map {$0.toDictionary()} + } + + func getCurrentServerId()->String? { + return self.SerMgr.activeProfileId; + } + + func setCurrentServer(uuid:String) { + self.SerMgr.setActiveProfiledId(uuid) + self.appdeleget.updateServersMenu() + SyncSSLocal() + self.appdeleget.applyConfig() + self.appdeleget.updateRunningModeMenu() + } + + func getServer(uuid:String)->Dictionary? { + if let i = self.SerMgr.profiles.index(where: {$0.uuid == uuid}) { + return self.SerMgr.profiles[i].toDictionary() + } + else { + return nil; + } + } + + func addServer(server:Dictionary) { + let profile = ServerProfile.fromDictionary(server) - apiserver.addHandler(forMethod: "POST", path: "/mode", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in - let arg = ((request as! GCDWebServerURLEncodedFormRequest).arguments["mode"])as? String - - if (arg != "auto" && arg != "global" && arg != "manual") { - return GCDWebServerDataResponse(jsonObject: ["status":0], contentType: "json") - } + self.SerMgr.profiles.append(profile) + self.SerMgr.save() + self.appdeleget.updateServersMenu() + } + + func modifyServer(uuid:String, server:Dictionary) { + let index = self.SerMgr.profiles.index(where: {$0.uuid == uuid})! + let profile = self.SerMgr.profiles[index] + + if (server["ServerHost"] != nil) { + profile.serverHost = server["ServerHost"] as! String; + } + if (server["ServerPort"] != nil) { + profile.serverPort = server["ServerPort"] as! uint16; + } + if (server["Method"] != nil) { + profile.method = server["Method"] as! String; - self.appdeleget.changeMode(mode: arg!) + } + if (server["Password"] != nil) { + profile.password = server["Password"] as! String; + } + if (server["Remark"] != nil) { + profile.remark = server["Remark"] as! String; + } + if (server["Plugin"] != nil) { + profile.plugin = server["Plugin"] as! String; - return GCDWebServerDataResponse(jsonObject: ["status":1], contentType: "json") - }) + } + if (server["PluginOptions"] != nil) { + profile.pluginOptions = server["PluginOptions"] as! String; + } + + self.SerMgr.save() + self.appdeleget.updateServersMenu() + } + + func deleteServer(uuid:String) { + let index = self.SerMgr.profiles.index(where: {$0.uuid == uuid})! + + self.SerMgr.profiles.remove(at: index) + + self.SerMgr.save() + self.appdeleget.updateServersMenu() + } + + func getMode()->Mode { + let mode_str = self.defaults.string(forKey: "ShadowsocksRunningMode"); + switch mode_str { + case "auto": return .auto + case "global": return .global; + case "manual": return .manual + default:fatalError() + } + } + + func setMode(mode:Mode) { + let defaults = UserDefaults.standard + + switch mode{ + case .auto:defaults.setValue("auto", forKey: "ShadowsocksRunningMode") + case .global:defaults.setValue("global", forKey: "ShadowsocksRunningMode") + case .manual:defaults.setValue("manual", forKey: "ShadowsocksRunningMode") + } + + self.appdeleget.updateRunningModeMenu() + self.appdeleget.applyConfig() + } + + func validate(server:Dictionary) -> Bool { + let serverHost = server["ServerHost"] as? String + let serverPort = server["ServerPort"] as? uint16 + let method = server["Method"] as? String + let password = server["Password"] as? String + + if (serverHost != nil && serverPort != nil && method != nil && password != nil) { + return ServerProfile.fromDictionary(server).isValid() + } + + return false; } } diff --git a/ShadowsocksX-NG/ServerProfile.swift b/ShadowsocksX-NG/ServerProfile.swift index 1e9fe88f..b66da167 100644 --- a/ShadowsocksX-NG/ServerProfile.swift +++ b/ShadowsocksX-NG/ServerProfile.swift @@ -132,34 +132,31 @@ class ServerProfile: NSObject, NSCopying { return copy; } - static func copy(fromDict:[String:Any?], toProfile:ServerProfile) { + static func fromDictionary(_ data:[String:Any?]) -> ServerProfile { let cp = { (profile: ServerProfile) in - profile.serverHost = fromDict["ServerHost"] as! String - profile.serverPort = (fromDict["ServerPort"] as! NSNumber).uint16Value - profile.method = fromDict["Method"] as! String - profile.password = fromDict["Password"] as! String - if let remark = fromDict["Remark"] { + profile.serverHost = data["ServerHost"] as! String + profile.serverPort = (data["ServerPort"] as! NSNumber).uint16Value + profile.method = data["Method"] as! String + profile.password = data["Password"] as! String + if let remark = data["Remark"] { profile.remark = remark as! String } - if let plugin = fromDict["Plugin"] as? String { + if let plugin = data["Plugin"] as? String { profile.plugin = plugin } - if let pluginOptions = fromDict["PluginOptions"] as? String { + if let pluginOptions = data["PluginOptions"] as? String { profile.pluginOptions = pluginOptions } } - cp(toProfile) - } - - static func fromDictionary(_ data:[String:Any?]) -> ServerProfile { + if let id = data["Id"] as? String { let profile = ServerProfile(uuid: id) - copy(fromDict: data, toProfile: profile) + cp(profile) return profile } else { let profile = ServerProfile() - copy(fromDict: data, toProfile: profile) + cp(profile) return profile } } From bba646495a5c65d2f2fa3d38bf9fd9f20613688e Mon Sep 17 00:00:00 2001 From: yangziy Date: Wed, 26 Sep 2018 15:23:55 +0800 Subject: [PATCH 08/11] Enhanced stability of HTTP API --- ShadowsocksX-NG/HTTPUserProxy.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ShadowsocksX-NG/HTTPUserProxy.swift b/ShadowsocksX-NG/HTTPUserProxy.swift index 35be6282..d6c162e1 100644 --- a/ShadowsocksX-NG/HTTPUserProxy.swift +++ b/ShadowsocksX-NG/HTTPUserProxy.swift @@ -108,10 +108,12 @@ class HTTPUserProxy{ func addHandler_addServer() { server.addHandler(forMethod: "POST", path: "/servers", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in if var server = ((request as? GCDWebServerURLEncodedFormRequest)?.arguments) as? [String: Any] { - server["ServerPort"] = UInt16(server["ServerPort"] as! String) - if (self.adapter.validate(server: server)) { - self.adapter.addServer(server: server) - return GCDWebServerResponse(); + if (server["ServerPort"] != nil) { + server["ServerPort"] = UInt16(server["ServerPort"] as! String) + if (self.adapter.validate(server: server)) { + self.adapter.addServer(server: server) + return GCDWebServerResponse(); + } } } return GCDWebServerResponse(statusCode: 400) @@ -122,7 +124,9 @@ class HTTPUserProxy{ server.addHandler(forMethod: "PATCH", pathRegex: "/servers/"+self.UUID_REGEX, request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in let id = String(request.path.dropFirst("/servers/".count)) if var server = ((request as? GCDWebServerURLEncodedFormRequest)?.arguments) as? [String: Any] { - server["ServerPort"] = UInt16(server["ServerPort"] as! String) + if (server["ServerPort"] != nil) { + server["ServerPort"] = UInt16(server["ServerPort"] as! String) + } if (self.adapter.getServer(uuid: id) != nil) { if (self.adapter.validate(server: server)) { self.adapter.modifyServer(uuid: id, server: server) From 2da51dd5082dd6ced03bf73bb315281bece00f90 Mon Sep 17 00:00:00 2001 From: yangziy Date: Wed, 26 Sep 2018 15:58:02 +0800 Subject: [PATCH 09/11] DELETE / MODIFY currently activated server would not perform. --- ShadowsocksX-NG/HTTPUserProxy.swift | 38 +++++++++++++---------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/ShadowsocksX-NG/HTTPUserProxy.swift b/ShadowsocksX-NG/HTTPUserProxy.swift index d6c162e1..51a0242d 100644 --- a/ShadowsocksX-NG/HTTPUserProxy.swift +++ b/ShadowsocksX-NG/HTTPUserProxy.swift @@ -33,7 +33,7 @@ class HTTPUserProxy{ addHandler_getStatus() // PUT /status addHandler_setStatus() - + // GET /servers addHandler_getServerList() // GET /current @@ -110,7 +110,7 @@ class HTTPUserProxy{ if var server = ((request as? GCDWebServerURLEncodedFormRequest)?.arguments) as? [String: Any] { if (server["ServerPort"] != nil) { server["ServerPort"] = UInt16(server["ServerPort"] as! String) - if (self.adapter.validate(server: server)) { + if (true) { // validate self.adapter.addServer(server: server) return GCDWebServerResponse(); } @@ -128,9 +128,14 @@ class HTTPUserProxy{ server["ServerPort"] = UInt16(server["ServerPort"] as! String) } if (self.adapter.getServer(uuid: id) != nil) { - if (self.adapter.validate(server: server)) { - self.adapter.modifyServer(uuid: id, server: server) - return GCDWebServerResponse(); + if (true) { // validate + if (self.adapter.getCurrentServerId() != id) { + self.adapter.modifyServer(uuid: id, server: server) + return GCDWebServerResponse() + } + else { + return GCDWebServerResponse(statusCode: 400); + } } } else { return GCDWebServerResponse(statusCode: 404) @@ -145,8 +150,12 @@ class HTTPUserProxy{ , processBlock: {request in let id = String(request.path.dropFirst("/servers/".count)) if((self.adapter.getServer(uuid: id)) != nil) { - self.adapter.deleteServer(uuid: id) - return GCDWebServerResponse(statusCode: 200) + if (self.adapter.getCurrentServerId() != id) { + self.adapter.deleteServer(uuid: id) + return GCDWebServerResponse() + } else { + return GCDWebServerResponse(statusCode: 400) + } } else { return GCDWebServerResponse(statusCode: 404) @@ -166,7 +175,7 @@ class HTTPUserProxy{ if let mode = APIAdapter.Mode(rawValue: mode_str) { self.adapter.setMode(mode: mode); - return GCDWebServerResponse(statusCode: 200) + return GCDWebServerResponse() } } return GCDWebServerResponse(statusCode: 400) @@ -294,17 +303,4 @@ class APIAdapter { self.appdeleget.updateRunningModeMenu() self.appdeleget.applyConfig() } - - func validate(server:Dictionary) -> Bool { - let serverHost = server["ServerHost"] as? String - let serverPort = server["ServerPort"] as? uint16 - let method = server["Method"] as? String - let password = server["Password"] as? String - - if (serverHost != nil && serverPort != nil && method != nil && password != nil) { - return ServerProfile.fromDictionary(server).isValid() - } - - return false; - } } From b617e8309f94e0637ee979cb241e2b98e9bce950 Mon Sep 17 00:00:00 2001 From: yangziy Date: Thu, 27 Sep 2018 18:14:03 +0800 Subject: [PATCH 10/11] Validate server profile sent from user. --- ShadowsocksX-NG/HTTPUserProxy.swift | 127 ++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 5 deletions(-) diff --git a/ShadowsocksX-NG/HTTPUserProxy.swift b/ShadowsocksX-NG/HTTPUserProxy.swift index 51a0242d..f9b33cf0 100644 --- a/ShadowsocksX-NG/HTTPUserProxy.swift +++ b/ShadowsocksX-NG/HTTPUserProxy.swift @@ -13,6 +13,7 @@ class HTTPUserProxy{ static let shard = HTTPUserProxy() let adapter = APIAdapter() + let v = Validator() let server = GCDWebServer() let api_port:UInt = 9528 @@ -84,7 +85,7 @@ class HTTPUserProxy{ func addHandler_getCurrentServer() { server.addHandler(forMethod: "GET", path: "/current", request: GCDWebServerRequest.self, processBlock: {request in if let activeId = self.adapter.getCurrentServerId() { - return GCDWebServerDataResponse(jsonObject: self.adapter.getServer(uuid: activeId), contentType: "json") + return GCDWebServerDataResponse(jsonObject: self.adapter.getServer(uuid: activeId)!, contentType: "json") } else { return GCDWebServerResponse(statusCode: 404); @@ -110,7 +111,7 @@ class HTTPUserProxy{ if var server = ((request as? GCDWebServerURLEncodedFormRequest)?.arguments) as? [String: Any] { if (server["ServerPort"] != nil) { server["ServerPort"] = UInt16(server["ServerPort"] as! String) - if (true) { // validate + if (Validator.integrity(server) && Validator.existAttributes(server)) { // validate self.adapter.addServer(server: server) return GCDWebServerResponse(); } @@ -128,7 +129,7 @@ class HTTPUserProxy{ server["ServerPort"] = UInt16(server["ServerPort"] as! String) } if (self.adapter.getServer(uuid: id) != nil) { - if (true) { // validate + if (Validator.existAttributes(server)) { if (self.adapter.getCurrentServerId() != id) { self.adapter.modifyServer(uuid: id, server: server) return GCDWebServerResponse() @@ -185,7 +186,7 @@ class HTTPUserProxy{ class APIAdapter { enum Mode:String {case auto="auto", global="global", manual="manual"}; - + let SerMgr = ServerProfileManager.instance let defaults = UserDefaults.standard let appdeleget = NSApplication.shared.delegate as! AppDelegate @@ -262,7 +263,6 @@ class APIAdapter { } if (server["Plugin"] != nil) { profile.plugin = server["Plugin"] as! String; - } if (server["PluginOptions"] != nil) { profile.pluginOptions = server["PluginOptions"] as! String; @@ -304,3 +304,120 @@ class APIAdapter { self.appdeleget.applyConfig() } } + +class Validator { + static func integrity(_ data: Dictionary) -> Bool { + if (data["ServerHost"] == nil || data["ServerPort"] as? NSNumber == nil + || data["Method"] == nil || data["Password"] == nil) { + return false; + } + return true; + } + + static func existAttributes(_ server:Dictionary) -> Bool { + var result = true; + + if (server["ServerHost"] != nil) { + result = result && serverHost(server["ServerHost"] as! String); + } + if (server["ServerPort"] != nil) { + result = result && serverPort(server["ServerPort"] as! uint16); + } + if (server["Method"] != nil) { + result = result && method(server["Method"] as! String); + } + if (server["Password"] != nil) { + result = result && password(server["Password"] as! String); + } + if (server["Remark"] != nil) { + result = result && remark(server["Remark"] as! String); + } + if (server["Plugin"] != nil) { + result = result && plugin(server["Plugin"] as! String); + } + if (server["PluginOptions"] != nil) { + result = result && pluginOptions(server["PluginOptions"] as! String); + } + + return result; + } + + static func serverHost(_ str:String) -> Bool { + return validateIpAddress(str) || validateDomainName(str); + } + + static func serverPort(_ str:uint16) -> Bool { + return true; + } + + static func method(_ str:String) -> Bool { + // Copy from PreferencesWindowController.swift + // Better to make valid methods enumeration type. + return [ + "aes-128-gcm", + "aes-192-gcm", + "aes-256-gcm", + "aes-128-cfb", + "aes-192-cfb", + "aes-256-cfb", + "aes-128-ctr", + "aes-192-ctr", + "aes-256-ctr", + "camellia-128-cfb", + "camellia-192-cfb", + "camellia-256-cfb", + "bf-cfb", + "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305", + "salsa20", + "chacha20", + "chacha20-ietf", + "rc4-md5", + ].contains(str); + } + + static func password(_ str:String) -> Bool { + return true; + } + + static func remark(_ str:String) -> Bool { + return true; + } + + static func plugin(_ str:String) -> Bool { + return true; + } + + static func pluginOptions(_ str:String) -> Bool { + return true; + } + + // Copy from ServerProfile.swift + private static func validateIpAddress(_ ipToValidate: String) -> Bool { + + var sin = sockaddr_in() + var sin6 = sockaddr_in6() + + if ipToValidate.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 { + // IPv6 peer. + return true + } + else if ipToValidate.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 { + // IPv4 peer. + return true + } + + return false; + } + + // Copy from ServerProfile.swift + private static func validateDomainName(_ value: String) -> Bool { + let validHostnameRegex = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" + + if (value.range(of: validHostnameRegex, options: .regularExpression) != nil) { + return true + } else { + return false + } + } +} From 93510a6397b561ac30fed4270ab6f88c77f3b090 Mon Sep 17 00:00:00 2001 From: yangziy Date: Thu, 27 Sep 2018 20:24:03 +0800 Subject: [PATCH 11/11] Now text data sent to HTTP API are validated. --- ShadowsocksX-NG/HTTPUserProxy.swift | 71 ++++++++++++++--------------- ShadowsocksX-NG/Utils.swift | 7 +-- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/ShadowsocksX-NG/HTTPUserProxy.swift b/ShadowsocksX-NG/HTTPUserProxy.swift index f9b33cf0..c9d96b3e 100644 --- a/ShadowsocksX-NG/HTTPUserProxy.swift +++ b/ShadowsocksX-NG/HTTPUserProxy.swift @@ -12,8 +12,7 @@ import GCDWebServer class HTTPUserProxy{ static let shard = HTTPUserProxy() - let adapter = APIAdapter() - let v = Validator() + let app = AppFacade() let server = GCDWebServer() let api_port:UInt = 9528 @@ -56,7 +55,7 @@ class HTTPUserProxy{ func addHandler_getStatus() { server.addHandler(forMethod: "GET", path: "/status", request: GCDWebServerRequest.self, processBlock: {request in - return GCDWebServerDataResponse(jsonObject: ["Enable":self.adapter.getStatus()], contentType: "json") + return GCDWebServerDataResponse(jsonObject: ["Enable":self.app.getStatus()], contentType: "json") }) } @@ -64,12 +63,12 @@ class HTTPUserProxy{ server.addHandler(forMethod: "PUT", path: "/status", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in if let targetStatus_str = (request as? GCDWebServerURLEncodedFormRequest)?.arguments["Enable"] as? String{ if let targetStatus = Bool(targetStatus_str) { - self.adapter.setStatus(status: targetStatus) + self.app.setStatus(status: targetStatus) return GCDWebServerResponse() } } else { - self.adapter.toggleStatus() + self.app.toggleStatus() return GCDWebServerResponse() } return GCDWebServerResponse(statusCode: 400) @@ -78,14 +77,14 @@ class HTTPUserProxy{ func addHandler_getServerList() { server.addHandler(forMethod: "GET", path: "/servers", request: GCDWebServerRequest.self, processBlock: {request in - return GCDWebServerDataResponse(jsonObject: self.adapter.getServerList(), contentType: "json") + return GCDWebServerDataResponse(jsonObject: self.app.getServerList(), contentType: "json") }) } func addHandler_getCurrentServer() { server.addHandler(forMethod: "GET", path: "/current", request: GCDWebServerRequest.self, processBlock: {request in - if let activeId = self.adapter.getCurrentServerId() { - return GCDWebServerDataResponse(jsonObject: self.adapter.getServer(uuid: activeId)!, contentType: "json") + if let activeId = self.app.getCurrentServerId() { + return GCDWebServerDataResponse(jsonObject: self.app.getServer(uuid: activeId)!, contentType: "json") } else { return GCDWebServerResponse(statusCode: 404); @@ -97,8 +96,8 @@ class HTTPUserProxy{ server.addHandler(forMethod: "PUT", path: "/current", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in if let targetId = (request as? GCDWebServerURLEncodedFormRequest)?.arguments["Id"] as? String{ - if self.adapter.getServer(uuid: targetId) != nil { - self.adapter.setCurrentServer(uuid: targetId); + if self.app.getServer(uuid: targetId) != nil { + self.app.setCurrentServer(uuid: targetId); return GCDWebServerResponse() } } @@ -111,8 +110,8 @@ class HTTPUserProxy{ if var server = ((request as? GCDWebServerURLEncodedFormRequest)?.arguments) as? [String: Any] { if (server["ServerPort"] != nil) { server["ServerPort"] = UInt16(server["ServerPort"] as! String) - if (Validator.integrity(server) && Validator.existAttributes(server)) { // validate - self.adapter.addServer(server: server) + if (Validator.profile(server)) { // validate + self.app.addServer(server: server) return GCDWebServerResponse(); } } @@ -128,10 +127,10 @@ class HTTPUserProxy{ if (server["ServerPort"] != nil) { server["ServerPort"] = UInt16(server["ServerPort"] as! String) } - if (self.adapter.getServer(uuid: id) != nil) { + if (self.app.getServer(uuid: id) != nil) { if (Validator.existAttributes(server)) { - if (self.adapter.getCurrentServerId() != id) { - self.adapter.modifyServer(uuid: id, server: server) + if (self.app.getCurrentServerId() != id) { + self.app.modifyServer(uuid: id, server: server) return GCDWebServerResponse() } else { @@ -150,9 +149,9 @@ class HTTPUserProxy{ server.addHandler(forMethod: "DELETE", pathRegex: "/servers/"+self.UUID_REGEX, request: GCDWebServerRequest.self , processBlock: {request in let id = String(request.path.dropFirst("/servers/".count)) - if((self.adapter.getServer(uuid: id)) != nil) { - if (self.adapter.getCurrentServerId() != id) { - self.adapter.deleteServer(uuid: id) + if((self.app.getServer(uuid: id)) != nil) { + if (self.app.getCurrentServerId() != id) { + self.app.deleteServer(uuid: id) return GCDWebServerResponse() } else { return GCDWebServerResponse(statusCode: 400) @@ -166,15 +165,15 @@ class HTTPUserProxy{ func addHandler_getMode() { server.addHandler(forMethod: "GET", path: "/mode", request: GCDWebServerRequest.self, processBlock: {request in - return GCDWebServerDataResponse(jsonObject: ["Mode":self.adapter.getMode().rawValue], contentType: "json") + return GCDWebServerDataResponse(jsonObject: ["Mode":self.app.getMode().rawValue], contentType: "json") }) } func addHandler_setMode() { server.addHandler(forMethod: "PUT", path: "/mode", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in if let mode_str = (request as? GCDWebServerURLEncodedFormRequest)?.arguments["Mode"] as? String{ - if let mode = APIAdapter.Mode(rawValue: mode_str) { - self.adapter.setMode(mode: mode); + if let mode = ProxyType(rawValue: mode_str) { + self.app.setMode(mode: mode); return GCDWebServerResponse() } @@ -184,12 +183,10 @@ class HTTPUserProxy{ } } -class APIAdapter { - enum Mode:String {case auto="auto", global="global", manual="manual"}; - - let SerMgr = ServerProfileManager.instance - let defaults = UserDefaults.standard - let appdeleget = NSApplication.shared.delegate as! AppDelegate +class AppFacade { + private let SerMgr = ServerProfileManager.instance + private let defaults = UserDefaults.standard + private let appdeleget = NSApplication.shared.delegate as! AppDelegate func getStatus()->Bool { return self.defaults.bool(forKey: "ShadowsocksOn"); @@ -281,37 +278,40 @@ class APIAdapter { self.appdeleget.updateServersMenu() } - func getMode()->Mode { + func getMode()->ProxyType { let mode_str = self.defaults.string(forKey: "ShadowsocksRunningMode"); switch mode_str { - case "auto": return .auto + case "auto": return .pac case "global": return .global; case "manual": return .manual default:fatalError() } } - func setMode(mode:Mode) { + func setMode(mode:ProxyType) { let defaults = UserDefaults.standard switch mode{ - case .auto:defaults.setValue("auto", forKey: "ShadowsocksRunningMode") + case .pac:defaults.setValue("auto", forKey: "ShadowsocksRunningMode") case .global:defaults.setValue("global", forKey: "ShadowsocksRunningMode") case .manual:defaults.setValue("manual", forKey: "ShadowsocksRunningMode") } + Globals.proxyType = mode + self.appdeleget.updateRunningModeMenu() self.appdeleget.applyConfig() } } class Validator { - static func integrity(_ data: Dictionary) -> Bool { + // Check if a ServerProfile can be constructed from input dictionary + static func profile(_ data: Dictionary) -> Bool { if (data["ServerHost"] == nil || data["ServerPort"] as? NSNumber == nil || data["Method"] == nil || data["Password"] == nil) { return false; } - return true; + return existAttributes(data); } static func existAttributes(_ server:Dictionary) -> Bool { @@ -352,7 +352,6 @@ class Validator { static func method(_ str:String) -> Bool { // Copy from PreferencesWindowController.swift - // Better to make valid methods enumeration type. return [ "aes-128-gcm", "aes-192-gcm", @@ -373,11 +372,11 @@ class Validator { "chacha20", "chacha20-ietf", "rc4-md5", - ].contains(str); + ].contains(str) } static func password(_ str:String) -> Bool { - return true; + return !str.isEmpty; } static func remark(_ str:String) -> Bool { diff --git a/ShadowsocksX-NG/Utils.swift b/ShadowsocksX-NG/Utils.swift index 280504cc..ecbe2ee8 100644 --- a/ShadowsocksX-NG/Utils.swift +++ b/ShadowsocksX-NG/Utils.swift @@ -24,9 +24,10 @@ extension Data { } } -enum ProxyType { - case pac - case global +enum ProxyType:String { + case pac = "auto" + case global = "global" + case manual = "manual" } struct Globals {