diff --git a/HTTPAPI.md b/HTTPAPI.md new file mode 100644 index 00000000..c6fdf2f2 --- /dev/null +++ b/HTTPAPI.md @@ -0,0 +1,166 @@ +# Features + +* Check current status (on/off) + +- Turn on/off or toggle the client +- Get server list +- Get current server +- Activate server +- Add new server +- Modify server +- Delete server +- Get current mode +- Switch mode + +# Specification + +**PORT:** 9528 + +### HTTP Status Code + +- 200 - Succeed +- 400 - Fail +- 404 (For `PATCH /servers/{ID}` and `DELETE /servers/{ID}`) - `{ID}` Not found +- 404 (For `GET /current`) No server is activated + +### Methods + +Note: To run the sample shell commands, replace the `Id` (if any) below for your own ones. + +- #### 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 or toggle the client `PUT /status` + +###### Sample Shell command + +```shell +curl -X PUT http://localhost:9528/status -d 'Enable=false' +``` + +Omit the argument `Enable` to **toggle**. + +- #### Get server list `GET /servers` + +###### Sample Shell command + +```shell +$ curl -X GET http://localhost:9528/servers +``` + +###### 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 /current` + +###### Sample Shell command + +```shell +$ curl -X GET http://localhost:9528/current +``` + +###### 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 + } +``` + +- #### Activate server `PUT /current` + +###### Sample Shell command + +```shell +$ curl -X PUT http://localhost:9528/current -d 'Id=71552DCD-B298-4591-B59A-82DA4B07AEF8' +``` + +- #### Add Server `POST /servers ` + +###### Sample Shell command + +```shell +$ curl -X POST http://localhost:9528/servers -d 'ServerPort=6666&ServerHost=tw1-sta40.somehost.com&Remark=someRemark&PluginOptions=&Plugin=&Password=myPassword&Method=chacha20-ietf-poly1305' +``` + +- #### Modify Server `PATCH /servers/{ID} ` + +###### Sample Shell command + +```shell +$ curl -X PATCH http://localhost:9528/servers/71552DCD-B298-4591-B59A-82DA4B07AEF8 -d 'ServerPort=6666&Remark=someRemark' +``` + +- #### Delete Server `DELETE /server/{ID}` + +###### Sample Shell command + +```shell +$ curl -X DELETE http://localhost:9528/servers/71552DCD-B298-4591-B59A-82DA4B07AEF8 +``` + +- #### 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 `PUT /mode` + +###### Sample Shell command + +```shell +$ curl -X PUT http://localhost:9528/status -d 'Mode=global' +``` diff --git a/README.md b/README.md index 83af4b3c..0a2fc05a 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,28 @@ -# ShadowsocksX-NG +# ShadowsocksX-NG, API Supported -Current version is 1.8.2 +Current version is 1.8.2 develop. -[![Build Status](https://travis-ci.org/shadowsocks/ShadowsocksX-NG.svg?branch=develop)](https://travis-ci.org/shadowsocks/ShadowsocksX-NG) +This is a fork with minor feature added. All credits go to the **original authors**. -Next Generation of [ShadowsocksX](https://github.com/shadowsocks/shadowsocks-iOS) +## Introduction -## Why a new implementation? +[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). -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. +With the **HTTP API** you could also control the app with **curl** in **terminal**. -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. +## Feature -## Requirements +The **HTTP API** enables users to do the following: -### Running - -- macOS 10.11+ - -### Building - -- Xcode 10.0+ -- 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) +- Turn on/off or 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 diff --git a/ShadowsocksX-NG.xcodeproj/project.pbxproj b/ShadowsocksX-NG.xcodeproj/project.pbxproj index 137a5a84..8c5eb2b7 100755 --- a/ShadowsocksX-NG.xcodeproj/project.pbxproj +++ b/ShadowsocksX-NG.xcodeproj/project.pbxproj @@ -11,6 +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 */; }; + 8EE2EDD9214F7CEC00FB4562 /* HTTPUserProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */; }; 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 */; }; @@ -148,6 +149,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 = ""; }; + 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPUserProxy.swift; 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 = ""; }; @@ -366,6 +368,7 @@ isa = PBXGroup; children = ( 9BB706A51D1B982300551F0E /* SWBApplication.m */, + 8EE2EDD6214F7CEC00FB4562 /* HTTPUserProxy.swift */, 9BB706A61D1B982300551F0E /* SWBApplication.h */, 9B3FFF511D09DBA20019A709 /* ShadowsocksX-NG-Bridging-Header.h */, 9B3FFF151D072FDE0019A709 /* LaunchAtLoginController.h */, @@ -842,6 +845,7 @@ 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 */, diff --git a/ShadowsocksX-NG/AppDelegate.swift b/ShadowsocksX-NG/AppDelegate.swift index 1c88cb36..8d57fe3c 100755 --- a/ShadowsocksX-NG/AppDelegate.swift +++ b/ShadowsocksX-NG/AppDelegate.swift @@ -182,6 +182,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele // Register global hotkey ShortcutsController.bindShortcuts() + + // Start HTTP API Server + HTTPUserProxy.shard.start() } func applicationWillTerminate(_ aNotification: Notification) { diff --git a/ShadowsocksX-NG/HTTPUserProxy.swift b/ShadowsocksX-NG/HTTPUserProxy.swift new file mode 100644 index 00000000..482eaf7a --- /dev/null +++ b/ShadowsocksX-NG/HTTPUserProxy.swift @@ -0,0 +1,424 @@ +// +// HTTPUserProxy.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 app = AppFacade() + + let server = GCDWebServer() + let api_port:UInt = 9528 + + let UUID_REGEX:String = "[\\w\\d-]*" + + func start(){ + setRouter() + do{ + try server.start(options: [GCDWebServerOption_Port:api_port,"BindToLocalhost":true]) + }catch{ + NSLog("Error:HTTPUserProxy start fail") + } + } + + func setRouter(){ + // GET /status + addHandler_getStatus() + // PUT /status + addHandler_setStatus() + + // GET /servers + addHandler_getServerList() + // GET /current + addHandler_getCurrentServer() + // PUT /current + addHandler_setCurrentServer() + // POST /servers + addHandler_addServer() + // PATCH /servers/{uuid} + addHandler_modifyServer() + // DELETE /servers/{uuid} + addHandler_deleteServer() + + // GET /mode + addHandler_getMode() + // PUT /mode + addHandler_setMode() + } + + func addHandler_getStatus() { + server.addHandler(forMethod: "GET", path: "/status", request: GCDWebServerRequest.self, processBlock: {request in + return GCDWebServerDataResponse(jsonObject: ["Enable":self.app.getStatus()], contentType: "json") + }) + } + + func addHandler_setStatus() { + 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.app.setStatus(status: targetStatus) + return GCDWebServerResponse() + } + } + else { + self.app.toggleStatus() + return GCDWebServerResponse() + } + return GCDWebServerResponse(statusCode: 400) + }) + } + + func addHandler_getServerList() { + server.addHandler(forMethod: "GET", path: "/servers", request: GCDWebServerRequest.self, processBlock: {request in + 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.app.getCurrentServerId() { + return GCDWebServerDataResponse(jsonObject: self.app.getServer(uuid: activeId)!, contentType: "json") + } + else { + return GCDWebServerResponse(statusCode: 404); + } + }) + } + + func addHandler_setCurrentServer() { + server.addHandler(forMethod: "PUT", path: "/current", request: GCDWebServerURLEncodedFormRequest.self, processBlock: {request in + + if let targetId = (request as? GCDWebServerURLEncodedFormRequest)?.arguments["Id"] as? String{ + if self.app.getServer(uuid: targetId) != nil { + self.app.setCurrentServer(uuid: targetId); + return GCDWebServerResponse() + } + } + return GCDWebServerResponse(statusCode: 400) + }) + } + + 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] { + if (server["ServerPort"] != nil) { + server["ServerPort"] = UInt16(server["ServerPort"] as! String) + if (Validator.profile(server)) { // validate + self.app.addServer(server: server) + return GCDWebServerResponse(); + } + } + } + return GCDWebServerResponse(statusCode: 400) + }); + } + + func addHandler_modifyServer() { + 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] { + if (server["ServerPort"] != nil) { + server["ServerPort"] = UInt16(server["ServerPort"] as! String) + } + if (self.app.getServer(uuid: id) != nil) { + if (Validator.existAttributes(server)) { + if (self.app.getCurrentServerId() != id) { + self.app.modifyServer(uuid: id, server: server) + return GCDWebServerResponse() + } + else { + return GCDWebServerResponse(statusCode: 400); + } + } + } else { + return GCDWebServerResponse(statusCode: 404) + } + } + return GCDWebServerResponse(statusCode: 400) + }) + } + + func addHandler_deleteServer() { + 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.app.getServer(uuid: id)) != nil) { + if (self.app.getCurrentServerId() != id) { + self.app.deleteServer(uuid: id) + return GCDWebServerResponse() + } else { + return GCDWebServerResponse(statusCode: 400) + } + } + else { + return GCDWebServerResponse(statusCode: 404) + } + }) + } + + func addHandler_getMode() { + server.addHandler(forMethod: "GET", path: "/mode", request: GCDWebServerRequest.self, processBlock: {request in + 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 = ProxyType(rawValue: mode_str) { + self.app.setMode(mode: mode); + + return GCDWebServerResponse() + } + } + return GCDWebServerResponse(statusCode: 400) + }) + } +} + +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"); + } + + 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) + + 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; + + } + 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; + } + 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()->ProxyType { + let mode_str = self.defaults.string(forKey: "ShadowsocksRunningMode"); + switch mode_str { + case "auto": return .pac + case "global": return .global; + case "manual": return .manual + default:fatalError() + } + } + + func setMode(mode:ProxyType) { + let defaults = UserDefaults.standard + + switch mode{ + 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 { + // 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 existAttributes(data); + } + + 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 + 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 !str.isEmpty; + } + + 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 + } + } +} 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 {