This commit is contained in:
Vicent Tsai
2017-01-10 21:40:55 +08:00
21 changed files with 528 additions and 108 deletions
+10
View File
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
08FCA0FF1E24BE1A0070984F /* example-gui-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 08FCA0FE1E24BE1A0070984F /* example-gui-config.json */; };
258E511BA910B0521B24DAB8 /* Pods_ShadowsocksX_NG.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 283ED1A8E9B711AC65670031 /* Pods_ShadowsocksX_NG.framework */; };
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 */; };
@@ -59,6 +60,7 @@
C6E28E951DA79705004F8330 /* HTTPPreferencesWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6E28E971DA79705004F8330 /* HTTPPreferencesWindowController.xib */; };
C8E42A6C1D4F270A0074C7EA /* UserRulesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E42A6A1D4F270A0074C7EA /* UserRulesController.swift */; };
C8E42A6E1D4F2CAF0074C7EA /* UserRulesController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C8E42A701D4F2CAF0074C7EA /* UserRulesController.xib */; };
D8E3630B1E2072980027449B /* ServerProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E3630A1E2072980027449B /* ServerProfileTests.swift */; };
E0E57CCA7EB34B90F9D340F2 /* Pods_ShadowsocksX_NGTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 388120F062D7EB7DD0D8DDCA /* Pods_ShadowsocksX_NGTests.framework */; };
F0809FF1595BE2966343D3C7 /* libPods-proxy_conf_helper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E7783AEDB4A3BDDC9FF16AC /* libPods-proxy_conf_helper.a */; };
/* End PBXBuildFile section */
@@ -93,6 +95,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
08FCA0FE1E24BE1A0070984F /* example-gui-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "example-gui-config.json"; sourceTree = "<group>"; };
19083CFCED87354F006967FF /* Pods_ShadowsocksX_NGUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShadowsocksX_NGUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1E7783AEDB4A3BDDC9FF16AC /* libPods-proxy_conf_helper.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-proxy_conf_helper.a"; sourceTree = BUILT_PRODUCTS_DIR; };
283ED1A8E9B711AC65670031 /* Pods_ShadowsocksX_NG.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShadowsocksX_NG.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -176,6 +179,7 @@
C8E42A6A1D4F270A0074C7EA /* UserRulesController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRulesController.swift; sourceTree = "<group>"; };
C8E42A6F1D4F2CAF0074C7EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UserRulesController.xib; sourceTree = "<group>"; };
C8E42A721D4F2CB10074C7EA /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/UserRulesController.strings"; sourceTree = "<group>"; };
D8E3630A1E2072980027449B /* ServerProfileTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerProfileTests.swift; sourceTree = "<group>"; };
E9E9FB3855DA55D0710EE7BD /* Pods-ShadowsocksX-NG.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NG.release.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NG/Pods-ShadowsocksX-NG.release.xcconfig"; sourceTree = "<group>"; };
FE3237E9FB24D9B924A0E630 /* Pods-ShadowsocksX-NG.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NG.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NG/Pods-ShadowsocksX-NG.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -227,6 +231,7 @@
9B07EFB01D048E900052D9DF /* Support Files */ = {
isa = PBXGroup;
children = (
08FCA0FE1E24BE1A0070984F /* example-gui-config.json */,
C6D4298E1DA75988002A5711 /* install_privoxy.sh */,
C6D4298F1DA75988002A5711 /* privoxy */,
C6D429901DA75988002A5711 /* reload_conf_privoxy.sh */,
@@ -309,6 +314,7 @@
9B0BFFF71D0460A70040E62B /* ShadowsocksX-NGTests */ = {
isa = PBXGroup;
children = (
D8E3630A1E2072980027449B /* ServerProfileTests.swift */,
9B0BFFF81D0460A70040E62B /* ShadowsocksX_NGTests.swift */,
9B0BFFFA1D0460A70040E62B /* Info.plist */,
);
@@ -490,6 +496,7 @@
C6D429951DA75988002A5711 /* reload_conf_privoxy.sh in Resources */,
9B0BFFEB1D0460A70040E62B /* Assets.xcassets in Resources */,
9B2491B71D0ACC3E003BBECC /* AdvPreferencesWindowController.xib in Resources */,
08FCA0FF1E24BE1A0070984F /* example-gui-config.json in Resources */,
9B3FFF271D0898EB0019A709 /* gfwlist.txt in Resources */,
C6D429931DA75988002A5711 /* install_privoxy.sh in Resources */,
9BC70EDC1D2E3E3100EDA4CA /* Localizable.strings in Resources */,
@@ -666,6 +673,7 @@
buildActionMask = 2147483647;
files = (
9B0BFFF91D0460A70040E62B /* ShadowsocksX_NGTests.swift in Sources */,
D8E3630B1E2072980027449B /* ServerProfileTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -914,6 +922,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.qiuyuzhou.ShadowsocksX-NGTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "ShadowsocksX-NG/ShadowsocksX-NG-Bridging-Header.h";
SWIFT_VERSION = 3.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ShadowsocksX-NG.app/Contents/MacOS/ShadowsocksX-NG";
};
@@ -929,6 +938,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.qiuyuzhou.ShadowsocksX-NGTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "ShadowsocksX-NG/ShadowsocksX-NG-Bridging-Header.h";
SWIFT_VERSION = 3.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ShadowsocksX-NG.app/Contents/MacOS/ShadowsocksX-NG";
};
+25 -4
View File
@@ -39,6 +39,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
@IBOutlet weak var serversMenuItem: NSMenuItem!
@IBOutlet var showQRCodeMenuItem: NSMenuItem!
@IBOutlet var scanQRCodeMenuItem: NSMenuItem!
@IBOutlet var showBunchJsonExampleFileItem: NSMenuItem!
@IBOutlet var importBunchJsonFileItem: NSMenuItem!
@IBOutlet var exportAllServerProfileItem: NSMenuItem!
@IBOutlet var serversPreferencesMenuItem: NSMenuItem!
@IBOutlet weak var lanchAtLoginMenuItem: NSMenuItem!
@@ -115,6 +118,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}
}
self.updateServersMenu()
self.updateRunningModeMenu()
SyncSSLocal()
}
)
@@ -141,9 +145,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
var isChanged = false
for url in urls {
let profielDict = ParseSSURL(url)
if let profielDict = profielDict {
let profile = ServerProfile.fromDictionary(profielDict as [String : AnyObject])
if let profile = ServerProfile(url: url) {
mgr.profiles.append(profile)
isChanged = true
@@ -337,7 +339,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
ScanQRCodeOnScreen()
}
@IBAction func toggleLaunghAtLogin(_ sender: NSMenuItem) {
@IBAction func showBunchJsonExampleFile(sender: NSMenuItem) {
ServerProfileManager.showExampleConfigFile()
}
@IBAction func importBunchJsonFile(sender: NSMenuItem) {
ServerProfileManager.instance.importConfigFile()
//updateServersMenu()//not working
}
@IBAction func exportAllServerProfile(sender: NSMenuItem) {
ServerProfileManager.instance.exportConfigFile()
}
@IBAction func toggleLaunghAtLogin(sender: NSMenuItem) {
launchAtLoginController.launchAtLogin = !launchAtLoginController.launchAtLogin;
updateLaunchAtLoginMenu()
}
@@ -539,6 +554,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
let showQRItem = showQRCodeMenuItem
let scanQRItem = scanQRCodeMenuItem
let preferencesItem = serversPreferencesMenuItem
let showBunch = showBunchJsonExampleFileItem
let importBuntch = importBunchJsonFileItem
let exportAllServer = exportAllServerProfileItem
var i = 0
for p in mgr.profiles {
@@ -565,6 +583,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}
serversMenuItem.submenu?.addItem(showQRItem!)
serversMenuItem.submenu?.addItem(scanQRItem!)
serversMenuItem.submenu?.addItem(showBunch!)
serversMenuItem.submenu?.addItem(importBuntch!)
serversMenuItem.submenu?.addItem(exportAllServer!)
serversMenuItem.submenu?.addItem(NSMenuItem.separator())
serversMenuItem.submenu?.addItem(preferencesItem!)
}
@@ -44,7 +44,7 @@
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wCp-nB-dda">
<rect key="frame" x="18" y="308" width="439" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Don't change these preferences if you have no idea what are they." id="8wL-qv-5nD">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Don't change these preferences if you have no idea what they are." id="8wL-qv-5nD">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@@ -26,7 +26,7 @@
"By Handle SS URL" = "By Handle SS URL";
" Encription Method: \(profile.method)" = " Encription Method: \(profile.method)";
" Encryption Method: \(profile.method)" = " Encryption Method: \(profile.method)";
"Current server profile is not valid." = "Current server profile is not valid.";
+23 -2
View File
@@ -16,10 +16,12 @@
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="ShadowsocksX_NG" customModuleProvider="target">
<connections>
<outlet property="autoModeMenuItem" destination="r07-Gu-aEz" id="9aH-pQ-Rgi"/>
<outlet property="exportAllServerProfileItem" destination="6k0-gn-DQv" id="W2x-96-ISj"/>
<outlet property="globalModeMenuItem" destination="Mw3-Jm-eXA" id="ar5-Yx-3ze"/>
<outlet property="importBunchJsonFileItem" destination="T9g-gy-gvv" id="vua-jg-YWe"/>
<outlet property="lanchAtLoginMenuItem" destination="eUq-p7-ICK" id="q3D-7x-0db"/>
<outlet property="hudWindow" destination="QWV-F6-ac1" id="K6D-a4-oqE"/>
<outlet property="isNameTextField" destination="rUN-Nq-HDb" id="ayK-aM-rSa"/>
<outlet property="lanchAtLoginMenuItem" destination="eUq-p7-ICK" id="w4p-0c-DZn"/>
<outlet property="manualModeMenuItem" destination="8PR-gs-c5N" id="9qz-mU-5kt"/>
<outlet property="panelView" destination="rai-SH-9tZ" id="5aU-ld-rWb"/>
<outlet property="proxyMenuItem" destination="diI-fB-Rss" id="Qjk-9U-3Qy"/>
@@ -27,6 +29,7 @@
<outlet property="scanQRCodeMenuItem" destination="Qe6-bF-paT" id="XHa-pa-nCa"/>
<outlet property="serversMenuItem" destination="u5M-hQ-VSc" id="8gp-SY-Y4U"/>
<outlet property="serversPreferencesMenuItem" destination="M5r-E7-44f" id="voe-SX-k6a"/>
<outlet property="showBunchJsonExampleFileItem" destination="pdy-JE-50Q" id="xcZ-ep-mON"/>
<outlet property="showQRCodeMenuItem" destination="R6A-96-Zcb" id="XHz-pz-nCz"/>
<outlet property="showRunningModeMenuItem" destination="CCV-hX-fVA" id="AU2-hH-w8F"/>
<outlet property="statusMenu" destination="Hob-KD-bx9" id="clA-ZW-0pT"/>
@@ -113,6 +116,24 @@
<action selector="scanQRCodeFromScreen:" target="Voe-Tx-rLC" id="zQZ-IT-H4a"/>
</connections>
</menuItem>
<menuItem title="Show Bunch Json Example File..." id="pdy-JE-50Q">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="showBunchJsonExampleFileWithSender:" target="Voe-Tx-rLC" id="lMy-Lo-OyB"/>
</connections>
</menuItem>
<menuItem title="Import Bunch Json File..." id="T9g-gy-gvv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="importBunchJsonFileWithSender:" target="Voe-Tx-rLC" id="bRk-cg-9yn"/>
</connections>
</menuItem>
<menuItem title="Export All Server To Json..." id="6k0-gn-DQv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="exportAllServerProfileWithSender:" target="Voe-Tx-rLC" id="0ip-dJ-juc"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="6sL-A4-S7N"/>
<menuItem title="Server Preferences..." id="M5r-E7-44f">
<modifierMask key="keyEquivalentModifierMask"/>
@@ -138,7 +159,7 @@
<menuItem title="Launch At Login" id="eUq-p7-ICK">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleLaunghAtLogin:" target="Voe-Tx-rLC" id="erk-ce-mO3"/>
<action selector="toggleLaunghAtLoginWithSender:" target="Voe-Tx-rLC" id="eFB-1Q-AOT"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="DTJ-NF-Wxr"/>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11201" systemVersion="16A323" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11201"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
<capability name="box content view" minToolsVersion="7.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="PreferencesWindowController" customModule="ShadowsocksX_NG" customModuleProvider="target">
@@ -26,7 +27,7 @@
<window title="Server Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" oneShot="NO" animationBehavior="default" id="F0z-JX-Cv5">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<rect key="contentRect" x="423" y="365" width="590" height="330"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1418"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="590" height="330"/>
<autoresizingMask key="autoresizingMask"/>
@@ -70,6 +71,7 @@
<connections>
<outlet property="dataSource" destination="-2" id="fz3-X6-PbB"/>
<outlet property="delegate" destination="-2" id="6zR-v1-wZj"/>
<outlet property="menu" destination="QmU-th-vjP" id="J0d-c9-pYs"/>
</connections>
</tableView>
</subviews>
@@ -339,6 +341,17 @@ DQ
<real key="minimum" value="1"/>
<real key="maximum" value="65535"/>
</numberFormatter>
<menu showsStateColumn="NO" id="QmU-th-vjP">
<items>
<menuItem title="Duplicate" id="bl9-lq-u9V">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="duplicate:" target="-2" id="dEb-Dt-yBW"/>
</connections>
</menuItem>
</items>
<point key="canvasLocation" x="-25.5" y="171.5"/>
</menu>
</objects>
<resources>
<image name="NSAddTemplate" width="11" height="11"/>
+3 -2
View File
@@ -8,7 +8,7 @@
import Foundation
let SS_LOCAL_VERSION = "2.4.6"
let SS_LOCAL_VERSION = "2.5.6"
let PRIVOXY_VERSION = "3.0.26.static"
let APP_SUPPORT_DIR = "/Library/Application Support/ShadowsocksX-NG/"
let LAUNCH_AGENT_DIR = "/Library/LaunchAgents/"
@@ -114,7 +114,8 @@ func InstallSSLocal() {
let homeDir = NSHomeDirectory()
let appSupportDir = homeDir+APP_SUPPORT_DIR
if !fileMgr.fileExists(atPath: appSupportDir + "ss-local-\(SS_LOCAL_VERSION)/ss-local")
|| !fileMgr.fileExists(atPath: appSupportDir + "libcrypto.1.0.0.dylib") {
|| !fileMgr.fileExists(atPath: appSupportDir + "libcrypto.1.0.0.dylib")
|| !fileMgr.fileExists(atPath: appSupportDir + "libpcre.1.dylib") {
let bundle = Bundle.main
let installerPath = bundle.path(forResource: "install_ss_local.sh", ofType: nil)
let task = Process.launchedProcess(launchPath: installerPath!, arguments: [""])
@@ -48,9 +48,16 @@ class PreferencesWindowController: NSWindowController
"aes-256-cfb",
"des-cfb",
"bf-cfb",
"camellia-128-cfb",
"camellia-192-cfb",
"camellia-256-cfb",
"idea-cfb",
"cast5-cfb",
"rc2-cfb",
"rc4-md5",
"seed-cfb",
"chacha20",
"chacha20-ietf",
"salsa20",
"rc4",
"table",
@@ -114,6 +121,20 @@ class PreferencesWindowController: NSWindowController
window?.performClose(self)
}
@IBAction func duplicate(_ sender: Any) {
let profile = profileMgr.profiles[profilesTableView.clickedRow]
let duplicateProfile = profile.copy() as! ServerProfile
duplicateProfile.uuid = UUID().uuidString
profileMgr.profiles.insert(duplicateProfile, at: profilesTableView.clickedRow+1)
profilesTableView.beginUpdates()
let index = IndexSet(integer: profileMgr.profiles.count-1)
profilesTableView.insertRows(at: index, withAnimation: .effectFade)
self.profilesTableView.scrollRowToVisible(profilesTableView.clickedRow+1)
self.profilesTableView.selectRowIndexes(index, byExtendingSelection: false)
profilesTableView.endUpdates()
updateProfileBoxVisible()
}
@IBAction func copyCurrentProfileURL2Pasteboard(_ sender: NSButton) {
let index = profilesTableView.selectedRow
if index >= 0 {
+98 -24
View File
@@ -9,26 +9,87 @@
import Cocoa
class ServerProfile: NSObject {
var uuid: String
class ServerProfile: NSObject, NSCopying {
var uuid: String
var serverHost: String = ""
var serverPort: uint16 = 8379
var method:String = "aes-128-cfb"
var password:String = ""
var remark:String = ""
var ota: Bool = false // onetime authentication
override init() {
uuid = UUID().uuidString
}
init(uuid: String) {
self.uuid = uuid
}
convenience init?(url: URL?) {
self.init()
func padBase64(string: String) -> String {
var length = string.characters.count
if length % 4 == 0 {
return string
} else {
length = 4 - length % 4 + length
return string.padding(toLength: length, withPad: "=", startingAt: 0)
}
}
func decodeUrl(url: URL?) -> String? {
guard let encodedStr = url?.host else {
return nil
}
guard let data = Data(base64Encoded: padBase64(string: encodedStr)) else {
return url?.absoluteString
}
guard let decoded = String(data: data, encoding: String.Encoding.utf8) else {
return nil
}
return "ss://\(decoded)"
}
guard let decodedUrl = decodeUrl(url: url) else {
return nil
}
guard var parsedUrl = URLComponents(string: decodedUrl) else {
return nil
}
guard let host = parsedUrl.host, let port = parsedUrl.port,
let method = parsedUrl.user, let password = parsedUrl.password else {
return nil
}
self.serverHost = host
self.serverPort = UInt16(port)
self.method = method
self.password = password
remark = parsedUrl.queryItems?
.filter({ $0.name == "Remark" }).first?.value ?? ""
if let otaStr = parsedUrl.queryItems?
.filter({ $0.name == "OTA" }).first?.value {
ota = NSString(string: otaStr).boolValue
}
}
static func fromDictionary(_ data:[String:AnyObject]) -> ServerProfile {
public func copy(with zone: NSZone? = nil) -> Any {
let copy = ServerProfile()
copy.serverHost = self.serverHost
copy.serverPort = self.serverPort
copy.method = self.method
copy.password = self.password
copy.remark = self.remark
copy.ota = self.ota
return copy;
}
static func fromDictionary(_ data:[String:Any?]) -> ServerProfile {
let cp = {
(profile: ServerProfile) in
profile.serverHost = data["ServerHost"] as! String
@@ -42,7 +103,7 @@ class ServerProfile: NSObject {
profile.ota = ota as! Bool
}
}
if let id = data["Id"] as? String {
let profile = ServerProfile(uuid: id)
cp(profile)
@@ -53,7 +114,7 @@ class ServerProfile: NSObject {
return profile
}
}
func toDictionary() -> [String:AnyObject] {
var d = [String:AnyObject]()
d["Id"] = uuid as AnyObject?
@@ -65,28 +126,28 @@ class ServerProfile: NSObject {
d["OTA"] = ota as AnyObject?
return d
}
func toJsonConfig() -> [String: AnyObject] {
var conf: [String: AnyObject] = ["server": serverHost as AnyObject,
"server_port": NSNumber(value: serverPort as UInt16),
"password": password as AnyObject,
"method": method as AnyObject,]
let defaults = UserDefaults.standard
conf["local_port"] = NSNumber(value: UInt16(defaults.integer(forKey: "LocalSocks5.ListenPort")) as UInt16)
conf["local_address"] = defaults.string(forKey: "LocalSocks5.ListenAddress") as AnyObject?
conf["timeout"] = NSNumber(value: UInt32(defaults.integer(forKey: "LocalSocks5.Timeout")) as UInt32)
conf["auth"] = NSNumber(value: ota as Bool)
return conf
}
func isValid() -> Bool {
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
@@ -95,35 +156,48 @@ class ServerProfile: NSObject {
// IPv4 peer.
return true
}
return false;
}
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
}
}
if !(validateIpAddress(serverHost) || validateDomainName(serverHost)){
return false
}
if password.isEmpty {
return false
}
return true
}
func URL() -> Foundation.URL? {
let parts = "\(method):\(password)@\(serverHost):\(serverPort)"
let base64String = parts.data(using: String.Encoding.utf8)?
.base64EncodedString(options: NSData.Base64EncodingOptions())
var url = URLComponents()
url.host = serverHost
url.user = method
url.password = password
url.port = Int(serverPort)
url.queryItems = [URLQueryItem(name: "Remark", value: remark),
URLQueryItem(name: "OTA", value: ota.description)]
let parts = url.string?.replacingOccurrences(
of: "//", with: "",
options: String.CompareOptions.anchored, range: nil)
let base64String = parts?.data(using: String.Encoding.utf8)?
.base64EncodedString(options: Data.Base64EncodingOptions())
if var s = base64String {
s = s.trimmingCharacters(in: CharacterSet(charactersIn: "="))
return Foundation.URL(string: "ss://\(s)")
+112 -2
View File
@@ -2,7 +2,7 @@
// ServerProfileManager.swift
// ShadowsocksX-NG
//
// Created by 邱宇舟 on 16/6/6.
// Created by 邱宇舟 on 16/6/6. Modified by 秦宇航 16/9/12
// Copyright © 2016年 qiuyuzhou. All rights reserved.
//
@@ -21,7 +21,7 @@ class ServerProfileManager: NSObject {
let defaults = UserDefaults.standard
if let _profiles = defaults.array(forKey: "ServerProfiles") {
for _profile in _profiles {
let profile = ServerProfile.fromDictionary(_profile as! [String : AnyObject])
let profile = ServerProfile.fromDictionary(_profile as! [String: Any])
profiles.append(profile)
}
}
@@ -70,4 +70,114 @@ class ServerProfileManager: NSObject {
return nil
}
}
func importConfigFile() {
let openPanel = NSOpenPanel()
openPanel.title = "Choose Config Json File".localized
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = true
openPanel.becomeKey()
openPanel.begin { (result) -> Void in
if (result == NSFileHandlingPanelOKButton && (openPanel.url) != nil) {
let fileManager = FileManager.default
let filePath:String = (openPanel.url?.path)!
if (fileManager.fileExists(atPath: filePath) && filePath.hasSuffix("json")) {
let data = fileManager.contents(atPath: filePath)
let readString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!
let readStringData = readString.data(using: String.Encoding.utf8.rawValue)
let jsonArr1 = try! JSONSerialization.jsonObject(with: readStringData!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
for item in jsonArr1.object(forKey: "configs") as! [[String: AnyObject]]{
let profile = ServerProfile()
profile.serverHost = item["server"] as! String
profile.serverPort = UInt16((item["server_port"]?.integerValue)!)
profile.method = item["method"] as! String
profile.password = item["password"] as! String
profile.remark = item["remarks"] as! String
self.profiles.append(profile)
self.save()
}
NotificationCenter.default.post(name: NSNotification.Name(rawValue: NOTIFY_SERVER_PROFILES_CHANGED), object: nil)
let configsCount = (jsonArr1.object(forKey: "configs") as! [[String: AnyObject]]).count
let notification = NSUserNotification()
notification.title = "Import Server Profile succeed!".localized
notification.informativeText = "Successful import \(configsCount) items".localized
NSUserNotificationCenter.default
.deliver(notification)
}else{
let notification = NSUserNotification()
notification.title = "Import Server Profile failed!".localized
notification.informativeText = "Invalid config file!".localized
NSUserNotificationCenter.default
.deliver(notification)
return
}
}
}
}
func exportConfigFile() {
//读取example文件,删掉configs里面的配置,再用NSDictionary填充到configs里面
let fileManager = FileManager.default
let filePath:String = Bundle.main.path(forResource: "example-gui-config", ofType: "json")!
let data = fileManager.contents(atPath: filePath)
let readString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!
let readStringData = readString.data(using: String.Encoding.utf8.rawValue)
let jsonArr1 = try! JSONSerialization.jsonObject(with: readStringData!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
let configsArray:NSMutableArray = [] //not using var?
for profile in profiles{
let configProfile:NSMutableDictionary = [:] //not using var?
//standard ss profile
configProfile.setValue(true, forKey: "enable")
configProfile.setValue(profile.serverHost, forKey: "server")
configProfile.setValue(NSNumber(value:profile.serverPort), forKey: "server_port")//not work
configProfile.setValue(profile.password, forKey: "password")
configProfile.setValue(profile.method, forKey: "method")
configProfile.setValue(profile.remark, forKey: "remarks")
configProfile.setValue(profile.remark.data(using: String.Encoding.utf8)?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)), forKey: "remarks_base64")
configsArray.add(configProfile)
}
jsonArr1.setValue(configsArray, forKey: "configs")
let jsonData = try! JSONSerialization.data(withJSONObject: jsonArr1, options: JSONSerialization.WritingOptions.prettyPrinted)
let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String
let savePanel = NSSavePanel()
savePanel.title = "Export Config Json File".localized
savePanel.canCreateDirectories = true
savePanel.allowedFileTypes = ["json"]
savePanel.nameFieldStringValue = "export.json"
savePanel.becomeKey()
savePanel.begin { (result) -> Void in
if (result == NSFileHandlingPanelOKButton && (savePanel.url) != nil) {
//write jsonArr1 back to file
try! jsonString.write(toFile: (savePanel.url?.path)!, atomically: true, encoding: String.Encoding.utf8)
NSWorkspace.shared().selectFile((savePanel.url?.path)!, inFileViewerRootedAtPath: (savePanel.directoryURL?.path)!)
let notification = NSUserNotification()
notification.title = "Export Server Profile succeed!".localized
notification.informativeText = "Successful Export \(self.profiles.count) items".localized
NSUserNotificationCenter.default
.deliver(notification)
}
}
}
class func showExampleConfigFile() {
//copy file to ~/Downloads folder
let filePath:String = Bundle.main.path(forResource: "example-gui-config", ofType: "json")!
let fileMgr = FileManager.default
let dataPath = NSHomeDirectory() + "/Downloads"
let destPath = dataPath + "/example-gui-config.json"
//检测文件是否已经存在,如果存在直接用sharedWorkspace显示
if fileMgr.fileExists(atPath: destPath) {
NSWorkspace.shared().selectFile(destPath, inFileViewerRootedAtPath: dataPath)
}else{
try! fileMgr.copyItem(atPath: filePath, toPath: destPath)
NSWorkspace.shared().selectFile(destPath, inFileViewerRootedAtPath: dataPath)
}
}
}
-2
View File
@@ -11,6 +11,4 @@
void ScanQRCodeOnScreen();
NSDictionary<NSString *, id>* ParseSSURL(NSURL* url);
#endif /* QRCodeUtils_h */
-60
View File
@@ -71,63 +71,3 @@ void ScanQRCodeOnScreen() {
}
];
}
// 解析SS URL,如果成功则返回一个与ServerProfile类兼容的dict
NSDictionary<NSString *, id>* ParseSSURL(NSURL* url) {
if (!url.host) {
return nil;
}
NSString *urlString = [url absoluteString];
int i = 0;
NSString *errorReason = nil;
while(i < 2) {
if (i == 1) {
NSString* host = url.host;
if ([host length]%4!=0) {
int n = 4 - [host length]%4;
if (1==n) {
host = [host stringByAppendingString:@"="];
} else if (2==n) {
host = [host stringByAppendingString:@"=="];
}
}
NSData *data = [[NSData alloc] initWithBase64EncodedString:host options:0];
NSString *decodedString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
urlString = decodedString;
}
i++;
urlString = [urlString stringByReplacingOccurrencesOfString:@"ss://" withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, urlString.length)];
NSRange firstColonRange = [urlString rangeOfString:@":"];
NSRange lastColonRange = [urlString rangeOfString:@":" options:NSBackwardsSearch];
NSRange lastAtRange = [urlString rangeOfString:@"@" options:NSBackwardsSearch];
if (firstColonRange.length == 0) {
errorReason = @"colon not found";
continue;
}
if (firstColonRange.location == lastColonRange.location) {
errorReason = @"only one colon";
continue;
}
if (lastAtRange.length == 0) {
errorReason = @"at not found";
continue;
}
if (!((firstColonRange.location < lastAtRange.location) && (lastAtRange.location < lastColonRange.location))) {
errorReason = @"wrong position";
continue;
}
NSString *method = [urlString substringWithRange:NSMakeRange(0, firstColonRange.location)];
NSString *password = [urlString substringWithRange:NSMakeRange(firstColonRange.location + 1, lastAtRange.location - firstColonRange.location - 1)];
NSString *IP = [urlString substringWithRange:NSMakeRange(lastAtRange.location + 1, lastColonRange.location - lastAtRange.location - 1)];
NSString *port = [urlString substringWithRange:NSMakeRange(lastColonRange.location + 1, urlString.length - lastColonRange.location - 1)];
return @{@"ServerHost": IP,
@"ServerPort": @([port integerValue]),
@"Method": method,
@"Password": password,
};
}
return nil;
}
@@ -17,8 +17,8 @@
/* Class = "NSButtonCell"; title = "Enable OTA"; ObjectID = "hRZ-Rp-I5e"; */
"hRZ-Rp-I5e.title" = "Enable OTA";
/* Class = "NSButtonCell"; title = "Cacnel"; ObjectID = "j03-9E-uHW"; */
"j03-9E-uHW.title" = "Cacnel";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "j03-9E-uHW"; */
"j03-9E-uHW.title" = "Cancel";
/* Class = "NSTextFieldCell"; title = "Password:"; ObjectID = "kil-xC-HPD"; */
"kil-xC-HPD.title" = "Password:";
+44
View File
@@ -0,0 +1,44 @@
{
"index": 0,
"random": false,
"global": false,
"enabled": true,
"shareOverLan": false,
"isDefault": false,
"localPort": 1080,
"pacUrl": null,
"useOnlinePac": false,
"reconnectTimes": 3,
"randomAlgorithm": 0,
"TTL": 0,
"proxyEnable": false,
"proxyType": 0,
"proxyHost": null,
"proxyPort": 0,
"proxyAuthUser": null,
"proxyAuthPass": null,
"authUser": null,
"authPass": null,
"autoban": false,
"configs": [{
"remarks": "example",
"server": "abc.xyz",
"server_port": 1234,
"method": "rc4-md5",
"remarks_base64": "ZXhhbXBsZQ==",
"password": "passwd",
"tcp_over_udp": false,
"udp_over_tcp": false,
"enable": true
},{
"remarks": "example2",
"server": "xyz.xyz",
"server_port": 1234,
"method": "rc4-md5",
"remarks_base64": "ZXhhbXBsZTI=",
"password": "passwd",
"tcp_over_udp": false,
"udp_over_tcp": false,
"enable": true
}]
}
+7 -4
View File
@@ -8,11 +8,14 @@
cd `dirname "${BASH_SOURCE[0]}"`
mkdir -p "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local-2.4.6"
cp -f ss-local "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local-2.4.6/"
mkdir -p "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local-2.5.6"
cp -f ss-local "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local-2.5.6/"
rm -f "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local"
ln -s "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local-2.4.6/ss-local" "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local"
ln -s "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local-2.5.6/ss-local" "$HOME/Library/Application Support/ShadowsocksX-NG/ss-local"
cp -f libcrypto.1.0.0.dylib "$HOME/Library/Application Support/ShadowsocksX-NG/"
cp -f libpcre.1.dylib "$HOME/Library/Application Support/ShadowsocksX-NG/"
rm -f "$HOME/Library/Application Support/ShadowsocksX-NG/libpcre.dylib"
ln -s "$HOME/Library/Application Support/ShadowsocksX-NG/libpcre.1.dylib" "$HOME/Library/Application Support/ShadowsocksX-NG/libpcre.dylib"
echo done
echo done
Binary file not shown.
Binary file not shown.
@@ -80,3 +80,11 @@
/* Class = "NSMenuItem"; title = "HTTP Proxy Preference ..."; ObjectID = "uEp-Gz-cu0"; */
"uEp-Gz-cu0.title" = "HTTP代理设置...";
/* Class = "NSMenuItem"; title = "Show Bunch Json Example File..."; ObjectID = "pdy-JE-50Q"; */
"pdy-JE-50Q.title" = "显示示例服务器配置文件...";
/* Class = "NSMenuItem"; title = "Import Bunch Json File..."; ObjectID = "T9g-gy-gvv"; */
"T9g-gy-gvv.title" = "导入服务器配置文件...";
/* Class = "NSMenuItem"; title = "Export All Server To Json..."; ObjectID = "6k0-gn-DQv"; */
"6k0-gn-DQv.title" = "导出全部服务器配置...";
@@ -34,3 +34,6 @@
/* Class = "NSButtonCell"; title = "OK"; ObjectID = "zPE-oD-PwK"; */
"zPE-oD-PwK.title" = "确定";
/* Class = "NSMenuItem"; title = "Duplicate"; ObjectID = "bl9-lq-u9V"; */
"bl9-lq-u9V.title" = "复制";
@@ -0,0 +1,153 @@
//
// ServerProfileTests.swift
// ShadowsocksX-NG
//
// Created by Rainux Luo on 07/01/2017.
// Copyright © 2017 qiuyuzhou. All rights reserved.
//
import XCTest
@testable import ShadowsocksX_NG
class ServerProfileTests: XCTestCase {
var profile: ServerProfile!
override func setUp() {
super.setUp()
profile = ServerProfile.fromDictionary(["ServerHost": "example.com",
"ServerPort": 8388,
"Method": "aes-256-cfb",
"Password": "password",
"Remark": "Protoss Prism",
"OTA": true])
XCTAssertNotNil(profile)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testInitWithSelfGeneratedURL() {
let newProfile = ServerProfile.init(url: profile.URL())
XCTAssertEqual(newProfile?.serverHost, profile.serverHost)
XCTAssertEqual(newProfile?.serverPort, profile.serverPort)
XCTAssertEqual(newProfile?.method, profile.method)
XCTAssertEqual(newProfile?.password, profile.password)
XCTAssertEqual(newProfile?.remark, profile.remark)
XCTAssertEqual(newProfile?.ota, profile.ota)
}
func testInitWithPlainURL() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388")
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.serverHost, "example.com")
XCTAssertEqual(profile?.serverPort, 8388)
XCTAssertEqual(profile?.method, "aes-256-cfb")
XCTAssertEqual(profile?.password, "password")
XCTAssertEqual(profile?.remark, "")
XCTAssertEqual(profile?.ota, false)
}
func testInitWithPlainURLandQuery() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true")
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.serverHost, "example.com")
XCTAssertEqual(profile?.serverPort, 8388)
XCTAssertEqual(profile?.method, "aes-256-cfb")
XCTAssertEqual(profile?.password, "password")
XCTAssertEqual(profile?.remark, "Prism")
XCTAssertEqual(profile?.ota, true)
}
func testInitWithPlainURLandAnotherQuery() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=0")
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.serverHost, "example.com")
XCTAssertEqual(profile?.serverPort, 8388)
XCTAssertEqual(profile?.method, "aes-256-cfb")
XCTAssertEqual(profile?.password, "password")
XCTAssertEqual(profile?.remark, "Prism")
XCTAssertEqual(profile?.ota, false)
}
func testInitWithBase64EncodedURL() {
// "ss://aes-256-cfb:password@example.com:8388"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OA")
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.serverHost, "example.com")
XCTAssertEqual(profile?.serverPort, 8388)
XCTAssertEqual(profile?.method, "aes-256-cfb")
XCTAssertEqual(profile?.password, "password")
XCTAssertEqual(profile?.remark, "")
XCTAssertEqual(profile?.ota, false)
}
func testInitWithBase64EncodedURLandQuery() {
// "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OD9SZW1hcms9UHJpc20mT1RBPXRydWU")
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.serverHost, "example.com")
XCTAssertEqual(profile?.serverPort, 8388)
XCTAssertEqual(profile?.method, "aes-256-cfb")
XCTAssertEqual(profile?.password, "password")
XCTAssertEqual(profile?.remark, "Prism")
XCTAssertEqual(profile?.ota, true)
}
func testInitWithEmptyURL() {
let url = URL(string: "ss://")
let profile = ServerProfile(url: url)
XCTAssertNil(profile)
}
func testInitWithInvalidURL() {
let url = URL(string: "ss://invalid url")
let profile = ServerProfile(url: url)
XCTAssertNil(profile)
}
func testInitWithBase64EncodedInvalidURL() {
// "ss://invalid url"
let url = URL(string: "ss://aW52YWxpZCB1cmw")
let profile = ServerProfile(url: url)
XCTAssertNil(profile)
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
@@ -32,5 +32,5 @@ class ShadowsocksX_NGTests: XCTestCase {
// Put the code you want to measure the time of here.
}
}
}