diff --git a/HTTPAPI.md b/HTTPAPI.md new file mode 100644 index 00000000..29e7298d --- /dev/null +++ b/HTTPAPI.md @@ -0,0 +1,76 @@ +# Copyright +Most code in HTTPUserProxy.swift comes from [yichengchen's fork](https://github.com/yichengchen/ShadowsocksX-R/blob/42b409beb85aee19a4852e09e7c3e4c2f73f49d3/ShadowsocksX-NG/ApiServer.swift), with slight modification to solve some incompatibility due to obsolete methods. + +# API Feature +1. Check current status (on/off) +2. Toggle the client +3. Get server list +4. Switch server +5. Get current mode +6. Switch mode + +# HTTP API +**Port:** 9528 + +**Default response** + +| Name | Type | Description | +| :----- | :--- | :------------------------ | +| status | int | 1 for success, 0 for fail | + +--- + +### Check current status (on/off) + +`GET /status` + +**Response** + +| Name | Type | Description | +| :----- | :------ | :---------- | +| enable | boolean | | + +### Toggle the client +`POST /toggle` + +**NO** Parameter + +### Get server list +`GET /servers` + +**Response** + +An **Array** of the following object: + +| Name | Type | Description | +| :----- | :----- | :------------------------------------------------------- | +| id | string | internal UUID of the server | +| remark | string | refer to Remarks in Servers Perferences Panel of the app | +| active | int | 1 for active, 0 for inactive | + +### Switch server +`POST /servers` + +**Parameter** + +| Name | Type | Description | +| :--- | :----- | :-------------------------- | +| id | string | internal UUID of the server | + +### Get current mode +`GET /mode` + +**Response** + +| Name | Type | Description | +| :--- | :----- | :----------------- | +| mode | string | auto/manual/global | + +### Set current mode +`POST /mode` + +**Parameter** + +| Name | Type | Description | +| :--- | :----- | :----------------- | +| mode | string | auto/manual/global | \ No newline at end of file diff --git a/ShadowsocksX-NG.xcodeproj/project.pbxproj b/ShadowsocksX-NG.xcodeproj/project.pbxproj index d3f372cf..811fc2f6 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 */; }; + 8E1DA77F2138231800659D99 /* HTTPUserProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1DA77D2138231800659D99 /* HTTPUserProxy.swift */; }; + 8EC6307D2141429B002D56A2 /* AppleScriptUserProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EC6307C2141429B002D56A2 /* AppleScriptUserProxy.swift */; }; + 8EC6308421414792002D56A2 /* AppleScriptDefinition.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8EC6308221414751002D56A2 /* 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 = ""; }; + 8E1DA77D2138231800659D99 /* HTTPUserProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPUserProxy.swift; sourceTree = ""; }; + 8EC6307C2141429B002D56A2 /* AppleScriptUserProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScriptUserProxy.swift; sourceTree = ""; }; + 8EC6308221414751002D56A2 /* 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 = ""; }; @@ -358,6 +364,8 @@ 9B0BFFE71D0460A70040E62B /* ShadowsocksX-NG */ = { isa = PBXGroup; children = ( + 8EC6308221414751002D56A2 /* AppleScriptDefinition.sdef */, + 8EC6307C2141429B002D56A2 /* AppleScriptUserProxy.swift */, 9BB706A51D1B982300551F0E /* SWBApplication.m */, 9BB706A61D1B982300551F0E /* SWBApplication.h */, 9B3FFF511D09DBA20019A709 /* ShadowsocksX-NG-Bridging-Header.h */, @@ -378,6 +386,7 @@ 9B3FFF1D1D0732660019A709 /* Utils.m */, 9B3FFF1F1D0734060019A709 /* Utils.h */, 9B3FFF4D1D09D9D50019A709 /* ProxyConfHelper.h */, + 8E1DA77D2138231800659D99 /* HTTPUserProxy.swift */, 9B3FFF4E1D09D9D50019A709 /* ProxyConfHelper.m */, 9B3FFF501D09DAEA0019A709 /* proxy_conf_helper_version.h */, 9BA04B211D23D5A5005AAD7F /* ProxyConfTool.h */, @@ -608,6 +617,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8EC6308421414792002D56A2 /* AppleScriptDefinition.sdef in Resources */, 9B7297E7214D69C300FD24AA /* libmbedcrypto.2.12.0.dylib in Resources */, 9BCB1D6C20A15B0E005BABE7 /* libsodium.23.dylib in Resources */, 9BCB1D6D20A15B0E005BABE7 /* libcares.2.dylib in Resources */, @@ -819,8 +829,10 @@ 9B3FFF171D072FDE0019A709 /* LaunchAtLoginController.m in Sources */, 9B86459D1E7C2CAD00A84029 /* ProxyInterfacesViewCtrl.swift in Sources */, 9B3FFF4F1D09D9D50019A709 /* ProxyConfHelper.m in Sources */, + 8EC6307D2141429B002D56A2 /* AppleScriptUserProxy.swift in Sources */, 9B5831F61E7302F8009D5B7D /* ShortcutsController.m in Sources */, 9BB706A71D1B982300551F0E /* SWBApplication.m in Sources */, + 8E1DA77F2138231800659D99 /* HTTPUserProxy.swift in Sources */, 9B3FFF1E1D0732660019A709 /* Utils.m in Sources */, 9B7297EA214D7C6B00FD24AA /* ShareServerProfilesWindowController.swift in Sources */, 9B3FFF321D08CEE40019A709 /* SWBQRCodeWindowController.m in Sources */, diff --git a/ShadowsocksX-NG/AppDelegate.swift b/ShadowsocksX-NG/AppDelegate.swift index 57096620..761fe172 100755 --- a/ShadowsocksX-NG/AppDelegate.swift +++ b/ShadowsocksX-NG/AppDelegate.swift @@ -183,6 +183,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele // Register global hotkey ShortcutsController.bindShortcuts() + + // Start API Server + HTTPUserProxy.shard.start() } func applicationWillTerminate(_ aNotification: Notification) { @@ -211,6 +214,20 @@ 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) { @@ -326,24 +343,15 @@ 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 +377,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 +443,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 diff --git a/ShadowsocksX-NG/AppleScriptDefinition.sdef b/ShadowsocksX-NG/AppleScriptDefinition.sdef new file mode 100644 index 00000000..80288ad1 --- /dev/null +++ b/ShadowsocksX-NG/AppleScriptDefinition.sdef @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + 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..c51acd97 --- /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:String]]() + + 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..de7e03e9 100644 --- a/ShadowsocksX-NG/Info.plist +++ b/ShadowsocksX-NG/Info.plist @@ -43,11 +43,15 @@ $(MACOSX_DEPLOYMENT_TARGET) LSUIElement + NSAppleScriptEnabled + NSHumanReadableCopyright Copyright © 2016-2018 qiuyuzhou. All rights reserved. License GPLv3. NSMainNibFile MainMenu NSPrincipalClass SWBApplication + OSAScriptingDefinition + AppleScriptDefinition.sdef