diff --git a/LaunchHelper/LaunchHelper.xcodeproj/project.pbxproj b/LaunchHelper/LaunchHelper.xcodeproj/project.pbxproj index 6a8cbf29..13ce3eda 100644 --- a/LaunchHelper/LaunchHelper.xcodeproj/project.pbxproj +++ b/LaunchHelper/LaunchHelper.xcodeproj/project.pbxproj @@ -267,12 +267,16 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = LaunchHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = "com.qiuyuzhou.ShadowsocksX-NG.LaunchHelper"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; }; name = Debug; @@ -281,12 +285,16 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = LaunchHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = "com.qiuyuzhou.ShadowsocksX-NG.LaunchHelper"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; }; name = Release; diff --git a/ShadowsocksX-NG.xcodeproj/project.pbxproj b/ShadowsocksX-NG.xcodeproj/project.pbxproj index bbfec57f..aa8311ac 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 */; }; + 2CC36EB3222E14FA00D8A8F0 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CC36EB2222E14FA00D8A8F0 /* CloudKit.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 */; }; 9B07EFAD1D048E880052D9DF /* menu_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B07EFA91D048E880052D9DF /* menu_icon.png */; }; @@ -145,6 +146,8 @@ 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; }; 297AF069022A197FD8E9D226 /* Pods-proxy_conf_helper.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-proxy_conf_helper.release.xcconfig"; path = "Pods/Target Support Files/Pods-proxy_conf_helper/Pods-proxy_conf_helper.release.xcconfig"; sourceTree = ""; }; + 2CC36EB1222E14D000D8A8F0 /* ShadowsocksX-NG.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ShadowsocksX-NG.entitlements"; sourceTree = ""; }; + 2CC36EB2222E14FA00D8A8F0 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 388120F062D7EB7DD0D8DDCA /* Pods_ShadowsocksX_NGTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShadowsocksX_NGTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3AC7CD9886196A997D6FC78D /* Pods-ShadowsocksX-NGTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShadowsocksX-NGTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ShadowsocksX-NGTests/Pods-ShadowsocksX-NGTests.release.xcconfig"; sourceTree = ""; }; 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 = ""; }; @@ -259,6 +262,7 @@ buildActionMask = 2147483647; files = ( 9B6BF9541E27B2570061B9A7 /* ServiceManagement.framework in Frameworks */, + 2CC36EB3222E14FA00D8A8F0 /* CloudKit.framework in Frameworks */, 9B3FFF3E1D08D9910019A709 /* SystemConfiguration.framework in Frameworks */, 258E511BA910B0521B24DAB8 /* Pods_ShadowsocksX_NG.framework in Frameworks */, ); @@ -370,6 +374,7 @@ 9B0BFFE71D0460A70040E62B /* ShadowsocksX-NG */ = { isa = PBXGroup; children = ( + 2CC36EB1222E14D000D8A8F0 /* ShadowsocksX-NG.entitlements */, 9BB706A51D1B982300551F0E /* SWBApplication.m */, 9BB706A61D1B982300551F0E /* SWBApplication.h */, 9B3FFF511D09DBA20019A709 /* ShadowsocksX-NG-Bridging-Header.h */, @@ -497,6 +502,7 @@ D3CE66CC039F651F28057DDB /* Frameworks */ = { isa = PBXGroup; children = ( + 2CC36EB2222E14FA00D8A8F0 /* CloudKit.framework */, 9B6BF9531E27B2570061B9A7 /* ServiceManagement.framework */, 9B3FFF3D1D08D9910019A709 /* SystemConfiguration.framework */, 9B3FFF3B1D08D93B0019A709 /* WebKit.framework */, @@ -586,6 +592,15 @@ 9B0BFFE41D0460A70040E62B = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 0900; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Push = { + enabled = 1; + }; + com.apple.iCloud = { + enabled = 1; + }; + }; }; 9B0BFFF31D0460A70040E62B = { CreatedOnToolsVersion = 7.3.1; @@ -594,6 +609,7 @@ }; 9B3FFF431D09CD3B0019A709 = { CreatedOnToolsVersion = 7.3.1; + ProvisioningStyle = Manual; }; }; }; @@ -1077,7 +1093,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = "ShadowsocksX-NG/ShadowsocksX-NG.entitlements"; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/libcork/include/", "$(SRCROOT)/libipset/include/", @@ -1093,6 +1113,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = "com.qiuyuzhou.ShadowsocksX-NG"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "ShadowsocksX-NG/ShadowsocksX-NG-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; @@ -1107,7 +1128,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = "ShadowsocksX-NG/ShadowsocksX-NG.entitlements"; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/libcork/include/", "$(SRCROOT)/libipset/include/", @@ -1123,6 +1148,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = "com.qiuyuzhou.ShadowsocksX-NG"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "ShadowsocksX-NG/ShadowsocksX-NG-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.0; diff --git a/ShadowsocksX-NG/ServerProfileManager.swift b/ShadowsocksX-NG/ServerProfileManager.swift index 12636eaf..301dae9b 100644 --- a/ShadowsocksX-NG/ServerProfileManager.swift +++ b/ShadowsocksX-NG/ServerProfileManager.swift @@ -7,16 +7,24 @@ // import Cocoa +import CloudKit + +typealias DictionaryArray = [[String: Any]] + +let recordId = CKRecordID(recordName: "UserDefaults") +var record = CKRecord(recordType: "UserDefaults", recordID: recordId) + +let database = CKContainer.default().privateCloudDatabase class ServerProfileManager: NSObject { static let instance:ServerProfileManager = ServerProfileManager() - var profiles:[ServerProfile] + var profiles:[ServerProfile] = [] var activeProfileId: String? fileprivate override init() { - profiles = [ServerProfile]() + super.init() let defaults = UserDefaults.standard if let _profiles = defaults.array(forKey: "ServerProfiles") { @@ -26,6 +34,8 @@ class ServerProfileManager: NSObject { } } activeProfileId = defaults.string(forKey: "ActiveServerProfileId") + + fetchCloudKitData() } func setActiveProfiledId(_ id: String) { @@ -35,15 +45,8 @@ class ServerProfileManager: NSObject { } func save() { - let defaults = UserDefaults.standard - var _profiles = [AnyObject]() - for profile in profiles { - if profile.isValid() { - let _profile = profile.toDictionary() - _profiles.append(_profile as AnyObject) - } - } - defaults.set(_profiles, forKey: "ServerProfiles") + profiles.saveToLocal() + profiles.saveToCloud() if getActiveProfile() == nil { activeProfileId = nil @@ -62,4 +65,73 @@ class ServerProfileManager: NSObject { return nil } } + + // MARK: - Helpers + func profilesDictionaryArray() -> DictionaryArray { + return profiles.filter({ $0.isValid() }).map({ $0.toDictionary() }) + } + + func fetchCloudKitData() { + database.fetch(withRecordID: recordId) { (record, error) in + if let error = error { + print(error) + + // Sync to Cloud + if !self.profiles.isEmpty { + self.profiles.saveToCloud() + } + + return + } + + guard let record = record, + let profilesString = record["ServerProfiles"] as? String, + let dictionaryArray = profilesString.jsonDictionaryArray(), + !dictionaryArray.isEmpty else { return } + + let _profiles = dictionaryArray.map({ ServerProfile.fromDictionary($0) }) + + if self.profiles.isEmpty || UserDefaults.standard.value(forKey: "Date") == nil { + self.profiles = _profiles + + // Sync to local + self.profiles.saveToLocal(date: record.modificationDate) + + return + } + + if let remoteDate = record.modificationDate, + let localDate = UserDefaults.standard.value(forKey: "Date") as? Date { + + // Sync latest data + if remoteDate > localDate { + self.profiles = _profiles + self.profiles.saveToLocal(date: remoteDate) + } else if remoteDate < localDate { + self.profiles.saveToCloud() + } + } + } + } +} + +extension Array where Element: ServerProfile { + func dictionaryArray() -> DictionaryArray { + return filter({ $0.isValid() }).map({ $0.toDictionary() }) + } + + func saveToCloud() { + record["ServerProfiles"] = dictionaryArray().toJSONString() + database.save(record) { (record, error) in + if let error = error { + print(error) + } + } + } + + func saveToLocal(date: Date? = Date()) { + let _profiles: DictionaryArray = dictionaryArray() + UserDefaults.standard.set(_profiles, forKey: "ServerProfiles") + UserDefaults.standard.set(date, forKey: "Date") + } } diff --git a/ShadowsocksX-NG/ShadowsocksX-NG.entitlements b/ShadowsocksX-NG/ShadowsocksX-NG.entitlements new file mode 100644 index 00000000..0e35f830 --- /dev/null +++ b/ShadowsocksX-NG/ShadowsocksX-NG.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.developer.aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.$(CFBundleIdentifier) + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) + + diff --git a/ShadowsocksX-NG/Utils.swift b/ShadowsocksX-NG/Utils.swift index 280504cc..94cd6c4b 100644 --- a/ShadowsocksX-NG/Utils.swift +++ b/ShadowsocksX-NG/Utils.swift @@ -12,6 +12,18 @@ extension String { var localized: String { return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") } + + func jsonDictionaryArray() -> [[String: Any]]? { + if let data = data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [[String:Any]] + } catch let error as NSError { + print(error) + } + } + + return nil + } } extension Data { @@ -24,6 +36,19 @@ extension Data { } } +extension Collection where Iterator.Element == [String: Any] { + func toJSONString(options: JSONSerialization.WritingOptions = .prettyPrinted) -> String { + if let array = self as? [[String: Any]], + let data = try? JSONSerialization.data(withJSONObject: array, options: options), + let string = String(data: data, encoding: String.Encoding.utf8) { + + return string + } + + return "[]" + } +} + enum ProxyType { case pac case global