1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 // Member variable names here are kept to ease transition to swift. Can rename at end. 4 // swiftlint:disable identifier_name 5 6 import Foundation 7 8 @objc(EXUpdatesCheckAutomaticallyConfig) 9 public enum CheckAutomaticallyConfig: Int { 10 case Always = 0 11 case WifiOnly = 1 12 case Never = 2 13 case ErrorRecoveryOnly = 3 14 var asString: String { 15 switch self { 16 case .Always: 17 return "ALWAYS" 18 case .WifiOnly: 19 return "WIFI_ONLY" 20 case .Never: 21 return "NEVER" 22 case .ErrorRecoveryOnly: 23 return "ERROR_RECOVERY_ONLY" 24 } 25 } 26 } 27 28 @objc(EXUpdatesConfigError) 29 public enum UpdatesConfigError: Int, Error { 30 case ExpoUpdatesConfigPlistError 31 } 32 33 /** 34 * Holds global, immutable configuration values for updates, as well as doing some rudimentary 35 * validation. 36 * 37 * In most apps, these configuration values are baked into the build, and this class functions as a 38 * utility for reading and memoizing the values. 39 * 40 * In development clients (including Expo Go) where this configuration is intended to be dynamic at 41 * runtime and updates from multiple scopes can potentially be opened, multiple instances of this 42 * class may be created over the lifetime of the app, but only one should be active at a time. 43 */ 44 @objc(EXUpdatesConfig) 45 @objcMembers 46 public final class UpdatesConfig: NSObject { 47 public static let PlistName = "Expo" 48 49 public static let EXUpdatesConfigEnableAutoSetupKey = "EXUpdatesAutoSetup" 50 public static let EXUpdatesConfigEnabledKey = "EXUpdatesEnabled" 51 public static let EXUpdatesConfigScopeKeyKey = "EXUpdatesScopeKey" 52 public static let EXUpdatesConfigUpdateUrlKey = "EXUpdatesURL" 53 public static let EXUpdatesConfigRequestHeadersKey = "EXUpdatesRequestHeaders" 54 public static let EXUpdatesConfigReleaseChannelKey = "EXUpdatesReleaseChannel" 55 public static let EXUpdatesConfigLaunchWaitMsKey = "EXUpdatesLaunchWaitMs" 56 public static let EXUpdatesConfigCheckOnLaunchKey = "EXUpdatesCheckOnLaunch" 57 public static let EXUpdatesConfigSDKVersionKey = "EXUpdatesSDKVersion" 58 public static let EXUpdatesConfigRuntimeVersionKey = "EXUpdatesRuntimeVersion" 59 public static let EXUpdatesConfigHasEmbeddedUpdateKey = "EXUpdatesHasEmbeddedUpdate" 60 public static let EXUpdatesConfigExpectsSignedManifestKey = "EXUpdatesExpectsSignedManifest" 61 public static let EXUpdatesConfigCodeSigningCertificateKey = "EXUpdatesCodeSigningCertificate" 62 public static let EXUpdatesConfigCodeSigningMetadataKey = "EXUpdatesCodeSigningMetadata" 63 public static let EXUpdatesConfigCodeSigningIncludeManifestResponseCertificateChainKey = "EXUpdatesCodeSigningIncludeManifestResponseCertificateChain" 64 public static let EXUpdatesConfigCodeSigningAllowUnsignedManifestsKey = "EXUpdatesConfigCodeSigningAllowUnsignedManifests" 65 public static let EXUpdatesConfigEnableExpoUpdatesProtocolV0CompatibilityModeKey = "EXUpdatesConfigEnableExpoUpdatesProtocolV0CompatibilityMode" 66 67 public static let EXUpdatesConfigCheckOnLaunchValueAlways = "ALWAYS" 68 public static let EXUpdatesConfigCheckOnLaunchValueWifiOnly = "WIFI_ONLY" 69 public static let EXUpdatesConfigCheckOnLaunchValueErrorRecoveryOnly = "ERROR_RECOVERY_ONLY" 70 public static let EXUpdatesConfigCheckOnLaunchValueNever = "NEVER" 71 72 private static let ReleaseChannelDefaultValue = "default" 73 74 public let isEnabled: Bool 75 public let expectsSignedManifest: Bool 76 public let scopeKey: String? 77 public let updateUrl: URL? 78 public let requestHeaders: [String: String] 79 public let releaseChannel: String 80 public let launchWaitMs: Int 81 public let checkOnLaunch: CheckAutomaticallyConfig 82 public let codeSigningConfiguration: CodeSigningConfiguration? 83 84 // used only in Expo Go to prevent loading rollbacks and other directives, which don't make much sense in the context of Expo Go 85 public let enableExpoUpdatesProtocolV0CompatibilityMode: Bool 86 87 public let sdkVersion: String? 88 public let runtimeVersion: String? 89 90 public let hasEmbeddedUpdate: Bool 91 92 internal required init( 93 isEnabled: Bool, 94 expectsSignedManifest: Bool, 95 scopeKey: String?, 96 updateUrl: URL?, 97 requestHeaders: [String: String], 98 releaseChannel: String, 99 launchWaitMs: Int, 100 checkOnLaunch: CheckAutomaticallyConfig, 101 codeSigningConfiguration: CodeSigningConfiguration?, 102 sdkVersion: String?, 103 runtimeVersion: String?, 104 hasEmbeddedUpdate: Bool, 105 enableExpoUpdatesProtocolV0CompatibilityMode: Bool 106 ) { 107 self.isEnabled = isEnabled 108 self.expectsSignedManifest = expectsSignedManifest 109 self.scopeKey = scopeKey 110 self.updateUrl = updateUrl 111 self.requestHeaders = requestHeaders 112 self.releaseChannel = releaseChannel 113 self.launchWaitMs = launchWaitMs 114 self.checkOnLaunch = checkOnLaunch 115 self.codeSigningConfiguration = codeSigningConfiguration 116 self.sdkVersion = sdkVersion 117 self.runtimeVersion = runtimeVersion 118 self.hasEmbeddedUpdate = hasEmbeddedUpdate 119 self.enableExpoUpdatesProtocolV0CompatibilityMode = enableExpoUpdatesProtocolV0CompatibilityMode 120 } 121 isMissingRuntimeVersionnull122 public func isMissingRuntimeVersion() -> Bool { 123 return (runtimeVersion?.isEmpty ?? true) && (sdkVersion?.isEmpty ?? true) 124 } 125 configWithExpoPlistnull126 public static func configWithExpoPlist(mergingOtherDictionary: [String: Any]?) throws -> UpdatesConfig { 127 guard let configPath = Bundle.main.path(forResource: PlistName, ofType: "plist") else { 128 throw UpdatesConfigError.ExpoUpdatesConfigPlistError 129 } 130 return try configWithExpoPlist(configPlistPath: configPath, mergingOtherDictionary: mergingOtherDictionary) 131 } 132 configWithExpoPlistnull133 public static func configWithExpoPlist(configPlistPath: String, mergingOtherDictionary: [String: Any]?) throws -> UpdatesConfig { 134 // swiftlint:disable:next legacy_objc_type 135 guard let configNSDictionary = NSDictionary(contentsOfFile: configPlistPath) as? [String: Any] else { 136 throw UpdatesConfigError.ExpoUpdatesConfigPlistError 137 } 138 139 var dictionary: [String: Any] = configNSDictionary 140 if let mergingOtherDictionary = mergingOtherDictionary { 141 dictionary = dictionary.merging(mergingOtherDictionary, uniquingKeysWith: { _, new in new }) 142 } 143 144 return UpdatesConfig.config(fromDictionary: dictionary) 145 } 146 confignull147 public static func config(fromDictionary config: [String: Any]) -> UpdatesConfig { 148 let isEnabled = config.optionalValue(forKey: EXUpdatesConfigEnabledKey) ?? true 149 let expectsSignedManifest = config.optionalValue(forKey: EXUpdatesConfigExpectsSignedManifestKey) ?? false 150 let updateUrl: URL? = config.optionalValue(forKey: EXUpdatesConfigUpdateUrlKey).let { it in 151 URL(string: it) 152 } 153 154 var scopeKey: String? = config.optionalValue(forKey: EXUpdatesConfigScopeKeyKey) 155 if scopeKey == nil, 156 let updateUrl = updateUrl { 157 scopeKey = UpdatesConfig.normalizedURLOrigin(url: updateUrl) 158 } 159 160 let requestHeaders: [String: String] = config.optionalValue(forKey: EXUpdatesConfigRequestHeadersKey) ?? [:] 161 let releaseChannel = config.optionalValue(forKey: EXUpdatesConfigReleaseChannelKey) ?? ReleaseChannelDefaultValue 162 let launchWaitMs = config.optionalValue(forKey: EXUpdatesConfigLaunchWaitMsKey).let { (it: Any) in 163 // The only way I can figure out how to detect numbers is to do a is NSNumber (is any Numeric didn't work). 164 // This might be able to change when we switch out the plist decoder above 165 // swiftlint:disable:next legacy_objc_type 166 if let it = it as? NSNumber { 167 return it.intValue 168 } else if let it = it as? String { 169 let formatter = NumberFormatter() 170 formatter.numberStyle = .none 171 return formatter.number(from: it)?.intValue 172 } 173 return nil 174 } ?? 0 175 176 let checkOnLaunch = config.optionalValue(forKey: EXUpdatesConfigCheckOnLaunchKey).let { (it: String) in 177 switch it { 178 case EXUpdatesConfigCheckOnLaunchValueNever: 179 return CheckAutomaticallyConfig.Never 180 case EXUpdatesConfigCheckOnLaunchValueErrorRecoveryOnly: 181 return CheckAutomaticallyConfig.ErrorRecoveryOnly 182 case EXUpdatesConfigCheckOnLaunchValueWifiOnly: 183 return CheckAutomaticallyConfig.WifiOnly 184 case EXUpdatesConfigCheckOnLaunchValueAlways: 185 return CheckAutomaticallyConfig.Always 186 default: 187 return CheckAutomaticallyConfig.Always 188 } 189 } ?? CheckAutomaticallyConfig.Always 190 191 let sdkVersion: String? = config.optionalValue(forKey: EXUpdatesConfigSDKVersionKey) 192 let runtimeVersion: String? = config.optionalValue(forKey: EXUpdatesConfigRuntimeVersionKey) 193 let hasEmbeddedUpdate = config.optionalValue(forKey: EXUpdatesConfigHasEmbeddedUpdateKey) ?? true 194 195 let codeSigningConfiguration = config.optionalValue(forKey: EXUpdatesConfigCodeSigningCertificateKey).let { (certificateString: String) in 196 let codeSigningMetadata: [String: String] = config.requiredValue(forKey: EXUpdatesConfigCodeSigningMetadataKey) 197 let codeSigningIncludeManifestResponseCertificateChain: Bool = config.optionalValue( 198 forKey: EXUpdatesConfigCodeSigningIncludeManifestResponseCertificateChainKey 199 ) ?? false 200 let codeSigningAllowUnsignedManifests: Bool = config.optionalValue(forKey: EXUpdatesConfigCodeSigningAllowUnsignedManifestsKey) ?? false 201 202 return (try? UpdatesConfig.codeSigningConfigurationForCodeSigningCertificate( 203 certificateString, 204 codeSigningMetadata: codeSigningMetadata, 205 codeSigningIncludeManifestResponseCertificateChain: codeSigningIncludeManifestResponseCertificateChain, 206 codeSigningAllowUnsignedManifests: codeSigningAllowUnsignedManifests 207 )).require("Invalid code signing configuration") 208 } 209 210 let enableExpoUpdatesProtocolV0CompatibilityMode = config.optionalValue(forKey: EXUpdatesConfigEnableExpoUpdatesProtocolV0CompatibilityModeKey) ?? false 211 212 return UpdatesConfig( 213 isEnabled: isEnabled, 214 expectsSignedManifest: expectsSignedManifest, 215 scopeKey: scopeKey, 216 updateUrl: updateUrl, 217 requestHeaders: requestHeaders, 218 releaseChannel: releaseChannel, 219 launchWaitMs: launchWaitMs, 220 checkOnLaunch: checkOnLaunch, 221 codeSigningConfiguration: codeSigningConfiguration, 222 sdkVersion: sdkVersion, 223 runtimeVersion: runtimeVersion, 224 hasEmbeddedUpdate: hasEmbeddedUpdate, 225 enableExpoUpdatesProtocolV0CompatibilityMode: enableExpoUpdatesProtocolV0CompatibilityMode 226 ) 227 } 228 229 private static func codeSigningConfigurationForCodeSigningCertificate( 230 _ codeSigningCertificate: String, 231 codeSigningMetadata: [String: String], 232 codeSigningIncludeManifestResponseCertificateChain: Bool, 233 codeSigningAllowUnsignedManifests: Bool 234 ) throws -> CodeSigningConfiguration? { 235 return try CodeSigningConfiguration( 236 embeddedCertificateString: codeSigningCertificate, 237 metadata: codeSigningMetadata, 238 includeManifestResponseCertificateChain: codeSigningIncludeManifestResponseCertificateChain, 239 allowUnsignedManifests: codeSigningAllowUnsignedManifests 240 ) 241 } 242 normalizedURLOriginnull243 public static func normalizedURLOrigin(url: URL) -> String { 244 let scheme = url.scheme.require("updateUrl must have a valid scheme") 245 let host = url.host.require("updateUrl must have a valid host") 246 var portOuter: Int? = url.port 247 if let port = portOuter, 248 Int(port) > -1, 249 port == UpdatesConfig.defaultPortForScheme(scheme: scheme) { 250 portOuter = nil 251 } 252 253 guard let port = portOuter, 254 Int(port) > -1 else { 255 return "\(scheme)://\(host)" 256 } 257 258 return "\(scheme)://\(host):\(Int(port))" 259 } 260 defaultPortForSchemenull261 private static func defaultPortForScheme(scheme: String?) -> Int? { 262 switch scheme { 263 case "http": 264 return 80 265 case "ws": 266 return 80 267 case "https": 268 return 443 269 case "wss": 270 return 443 271 case "ftp": 272 return 21 273 default: 274 return nil 275 } 276 } 277 } 278