1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 // swiftlint:disable closure_body_length 4 // swiftlint:disable file_length 5 // swiftlint:disable force_cast 6 // swiftlint:disable function_body_length 7 // swiftlint:disable function_parameter_count 8 // swiftlint:disable implicitly_unwrapped_optional 9 // swiftlint:disable identifier_name 10 // swiftlint:disable type_body_length 11 // swiftlint:disable legacy_objc_type 12 13 import Foundation 14 import ABI49_0_0EASClient 15 16 internal typealias SuccessBlock = (_ data: Data?, _ urlResponse: URLResponse) -> Void 17 internal typealias ErrorBlock = (_ error: Error) -> Void 18 internal typealias HashSuccessBlock = (_ data: Data, _ urlResponse: URLResponse, _ base64URLEncodedSHA256Hash: String) -> Void 19 20 internal typealias RemoteUpdateDownloadSuccessBlock = (_ updateResponse: UpdateResponse) -> Void 21 internal typealias RemoteUpdateDownloadErrorBlock = (_ error: Error) -> Void 22 23 private typealias ParseManifestSuccessBlock = (_ manifestUpdateResponsePart: ManifestUpdateResponsePart) -> Void 24 private typealias ParseManifestErrorBlock = (_ error: Error) -> Void 25 private typealias ParseDirectiveSuccessBlock = (_ directiveUpdateResponsePart: DirectiveUpdateResponsePart) -> Void 26 private typealias ParseDirectiveErrorBlock = (_ error: Error) -> Void 27 28 private let ErrorDomain = "ABI49_0_0EXUpdatesFileDownloader" 29 private enum FileDownloaderErrorCode: Int { 30 case FileWriteError = 1002 31 case ManifestVerificationError = 1003 32 case FileHashMismatchError = 1004 33 case NoCompatibleUpdateError = 1009 34 case MismatchedManifestFiltersError = 1021 35 case ManifestParseError = 1022 36 case InvalidResponseError = 1040 37 case ManifestStringError = 1041 38 case ManifestJSONError = 1042 39 case ManifestSignatureError = 1043 40 case MultipartParsingError = 1044 41 case MultipartMissingManifestError = 1045 42 case MissingMultipartBoundaryError = 1047 43 case CodeSigningSignatureError = 1048 44 } 45 46 enum FileDownloaderInternalError: Error { 47 case extractUpdateResponseDictionaryNil 48 } 49 50 private extension String { truncatenull51 func truncate(toMaxLength: Int) -> String { 52 if toMaxLength <= 0 { 53 return "" 54 } else if toMaxLength < self.count { 55 let endIndex = self.index(self.startIndex, offsetBy: toMaxLength) 56 return String(self[...endIndex]) 57 } else { 58 return self 59 } 60 } 61 } 62 63 private extension Dictionary where Iterator.Element == (key: String, value: Any) { stringValueForCaseInsensitiveKeynull64 func stringValueForCaseInsensitiveKey(_ searchKey: Key) -> String? { 65 let valueRaw = self.first { (key: Key, _: Value) in 66 return key.caseInsensitiveCompare(searchKey) == .orderedSame 67 }?.value 68 69 guard let valueRaw = valueRaw as? String else { 70 return nil 71 } 72 return valueRaw 73 } 74 } 75 76 /** 77 * Utility class that holds all the logic for downloading data and files, such as update manifests 78 * and assets, using NSURLSession. 79 */ 80 internal final class FileDownloader: NSObject, URLSessionDataDelegate { 81 private static let DefaultTimeoutInterval: TimeInterval = 60 82 private static let MultipartManifestPartName = "manifest" 83 private static let MultipartDirectivePartName = "directive" 84 private static let MultipartExtensionsPartName = "extensions" 85 private static let MultipartCertificateChainPartName = "certificate_chain" 86 87 // swiftlint:disable:next force_unwrapping 88 private static let ParameterParserSemicolonDelimiter = ";".utf16.first! 89 90 // these can be made non-forced lets when NSObject protocol is removed 91 private var session: URLSession! 92 private var sessionConfiguration: URLSessionConfiguration! 93 private var config: UpdatesConfig! 94 private var logger: UpdatesLogger! 95 96 convenience init(config: UpdatesConfig) { 97 self.init(config: config, urlSessionConfiguration: URLSessionConfiguration.default) 98 } 99 100 required init(config: UpdatesConfig, urlSessionConfiguration: URLSessionConfiguration) { 101 super.init() 102 self.sessionConfiguration = urlSessionConfiguration 103 self.config = config 104 self.logger = UpdatesLogger() 105 self.session = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil) 106 } 107 108 deinit { 109 self.session.finishTasksAndInvalidate() 110 } 111 112 static let assetFilesQueue: DispatchQueue = DispatchQueue(label: "expo.controller.AssetFilesQueue") 113 114 func downloadFile( 115 fromURL url: URL, 116 verifyingHash expectedBase64URLEncodedSHA256Hash: String?, 117 toPath destinationPath: String, 118 extraHeaders: [String: Any], 119 successBlock: @escaping HashSuccessBlock, 120 errorBlock: @escaping ErrorBlock 121 ) { 122 downloadData( 123 fromURL: url, 124 extraHeaders: extraHeaders 125 ) { data, response in 126 guard let data = data else { 127 let errorMessage = String( 128 format: "File download response was empty for URL: %@", 129 url.absoluteString 130 ) 131 self.logger.error(message: errorMessage, code: UpdatesErrorCode.assetsFailedToLoad) 132 errorBlock(NSError( 133 domain: ErrorDomain, 134 code: FileDownloaderErrorCode.InvalidResponseError.rawValue, 135 userInfo: [NSLocalizedDescriptionKey: errorMessage] 136 )) 137 return 138 } 139 140 let hashBase64String = UpdatesUtils.base64UrlEncodedSHA256WithData(data) 141 if let expectedBase64URLEncodedSHA256Hash = expectedBase64URLEncodedSHA256Hash, 142 expectedBase64URLEncodedSHA256Hash != hashBase64String { 143 let errorMessage = String( 144 format: "File download was successful but base64url-encoded SHA-256 did not match expected; expected: %@; actual: %@", 145 expectedBase64URLEncodedSHA256Hash, 146 hashBase64String 147 ) 148 self.logger.error(message: errorMessage, code: UpdatesErrorCode.assetsFailedToLoad) 149 errorBlock(NSError( 150 domain: ErrorDomain, 151 code: FileDownloaderErrorCode.FileHashMismatchError.rawValue, 152 userInfo: [NSLocalizedDescriptionKey: errorMessage] 153 )) 154 return 155 } 156 157 do { 158 try data.write(to: URL(fileURLWithPath: destinationPath), options: .atomic) 159 successBlock(data, response, hashBase64String) 160 return 161 } catch { 162 let errorMessage = String( 163 format: "Could not write to path %@: %@", 164 destinationPath, 165 error.localizedDescription 166 ) 167 self.logger.error(message: errorMessage, code: UpdatesErrorCode.unknown) 168 errorBlock(NSError( 169 domain: ErrorDomain, 170 code: FileDownloaderErrorCode.FileWriteError.rawValue, 171 userInfo: [ 172 NSLocalizedDescriptionKey: errorMessage, 173 NSUnderlyingErrorKey: error 174 ] 175 )) 176 return 177 } 178 } errorBlock: { error in 179 errorBlock(error) 180 } 181 } 182 183 func downloadData( 184 fromURL url: URL, 185 extraHeaders: [String: Any], 186 successBlock: @escaping SuccessBlock, 187 errorBlock: @escaping ErrorBlock 188 ) { 189 let request = createGenericRequest(withURL: url, extraHeaders: extraHeaders) 190 downloadData(withRequest: request, successBlock: successBlock, errorBlock: errorBlock) 191 } 192 193 func downloadRemoteUpdate( 194 fromURL url: URL, 195 withDatabase database: UpdatesDatabase, 196 extraHeaders: [String: Any]?, 197 successBlock: @escaping RemoteUpdateDownloadSuccessBlock, 198 errorBlock: @escaping RemoteUpdateDownloadErrorBlock 199 ) { 200 let request = createManifestRequest(withURL: url, extraHeaders: extraHeaders) 201 downloadData( 202 withRequest: request 203 ) { data, response in 204 guard let response = response as? HTTPURLResponse else { 205 let errorMessage = "response must be a HTTPURLResponse" 206 self.logger.error(message: errorMessage, code: UpdatesErrorCode.unknown) 207 errorBlock(NSError( 208 domain: ErrorDomain, 209 code: FileDownloaderErrorCode.InvalidResponseError.rawValue, 210 userInfo: [NSLocalizedDescriptionKey: errorMessage ] 211 )) 212 return 213 } 214 self.parseManifestResponse(response, withData: data, database: database, successBlock: successBlock, errorBlock: errorBlock) 215 } errorBlock: { error in 216 errorBlock(error) 217 } 218 } 219 220 /** 221 * Get extra (stateful) headers to pass into `downloadManifestFromURL:` 222 * Must be called on the database queue 223 */ 224 static func extraHeadersForRemoteUpdateRequest( 225 withDatabase database: UpdatesDatabase, 226 config: UpdatesConfig, 227 launchedUpdate: Update?, 228 embeddedUpdate: Update? 229 ) -> [String: Any] { 230 let scopeKey = config.scopeKey.require("Must have scopeKey in config") 231 232 var extraHeaders: [String: Any] = [:] 233 do { 234 extraHeaders = try database.serverDefinedHeaders(withScopeKey: scopeKey) ?? [:] 235 } catch { 236 NSLog("Error selecting serverDefinedHeaders from database: %@", [error.localizedDescription]) 237 } 238 239 do { 240 if let extraClientParams = try database.extraParams(withScopeKey: scopeKey) { 241 extraHeaders["Expo-Extra-Params"] = try StringStringDictionarySerializer.serialize(dictionary: extraClientParams) 242 } 243 } catch { 244 NSLog("Error adding extra params to headers: %@", [error.localizedDescription]) 245 } 246 247 if let launchedUpdate = launchedUpdate { 248 extraHeaders["Expo-Current-Update-ID"] = launchedUpdate.updateId.uuidString.lowercased() 249 } 250 251 if let embeddedUpdate = embeddedUpdate { 252 extraHeaders["Expo-Embedded-Update-ID"] = embeddedUpdate.updateId.uuidString.lowercased() 253 } 254 255 return extraHeaders 256 } 257 setHTTPHeaderFieldsnull258 private static func setHTTPHeaderFields(_ headers: [String: Any?]?, onRequest request: inout URLRequest) { 259 guard let headers = headers else { 260 return 261 } 262 263 for (key, value) in headers { 264 switch value { 265 case let value as String: 266 request.setValue(value, forHTTPHeaderField: key) 267 case let value as Bool: 268 request.setValue(value ? "true" : "false", forHTTPHeaderField: key) 269 case let value as NSNumber: 270 request.setValue(value.stringValue, forHTTPHeaderField: key) 271 case is NSNull: 272 // can probably remove this case after everything is swift 273 request.setValue("null", forHTTPHeaderField: key) 274 case nil: 275 request.setValue("null", forHTTPHeaderField: key) 276 default: 277 request.setValue((value as! NSObject).description, forHTTPHeaderField: key) 278 } 279 } 280 } 281 setHTTPHeaderFieldsnull282 private func setHTTPHeaderFields(request: inout URLRequest, extraHeaders: [String: Any?]) { 283 FileDownloader.setHTTPHeaderFields(extraHeaders, onRequest: &request) 284 request.setValue("ios", forHTTPHeaderField: "Expo-Platform") 285 request.setValue("1", forHTTPHeaderField: "Expo-Protocol-Version") 286 request.setValue("1", forHTTPHeaderField: "Expo-API-Version") 287 request.setValue("BARE", forHTTPHeaderField: "Expo-Updates-Environment") 288 request.setValue(EASClientID.uuid().uuidString, forHTTPHeaderField: "EAS-Client-ID") 289 290 for (key, value) in config.requestHeaders { 291 request.setValue(value, forHTTPHeaderField: key) 292 } 293 } 294 setManifestHTTPHeaderFieldsnull295 private func setManifestHTTPHeaderFields(request: inout URLRequest, extraHeaders: [String: Any?]?) { 296 // apply extra headers before anything else, so they don't override preset headers 297 FileDownloader.setHTTPHeaderFields(extraHeaders, onRequest: &request) 298 299 request.setValue("multipart/mixed,application/expo+json,application/json", forHTTPHeaderField: "Accept") 300 request.setValue("ios", forHTTPHeaderField: "Expo-Platform") 301 request.setValue("1", forHTTPHeaderField: "Expo-Protocol-Version") 302 request.setValue("1", forHTTPHeaderField: "Expo-API-Version") 303 request.setValue("BARE", forHTTPHeaderField: "Expo-Updates-Environment") 304 request.setValue(EASClientID.uuid().uuidString, forHTTPHeaderField: "EAS-Client-ID") 305 request.setValue("true", forHTTPHeaderField: "Expo-JSON-Error") 306 request.setValue(config.expectsSignedManifest ? "true" : "false", forHTTPHeaderField: "Expo-Accept-Signature") 307 request.setValue(config.releaseChannel, forHTTPHeaderField: "Expo-Release-Channel") 308 309 if let runtimeVersion = config.runtimeVersion { 310 request.setValue(runtimeVersion, forHTTPHeaderField: "Expo-Runtime-Version") 311 } else { 312 request.setValue(config.sdkVersion, forHTTPHeaderField: "Expo-SDK-Version") 313 } 314 315 if let previousFatalError = ErrorRecovery.consumeErrorLog() { 316 // some servers can have max length restrictions for headers, 317 // so we restrict the length of the string to 1024 characters -- 318 // this should satisfy the requirements of most servers 319 request.setValue(previousFatalError.truncate(toMaxLength: 1024), forHTTPHeaderField: "Expo-Fatal-Error") 320 } 321 322 for (key, value) in config.requestHeaders { 323 request.setValue(value, forHTTPHeaderField: key) 324 } 325 326 if let codeSigningConfiguration = config.codeSigningConfiguration { 327 request.setValue(codeSigningConfiguration.createAcceptSignatureHeader(), forHTTPHeaderField: "expo-expect-signature") 328 } 329 } 330 createManifestRequestnull331 func createManifestRequest(withURL url: URL, extraHeaders: [String: Any?]?) -> URLRequest { 332 var request = URLRequest( 333 url: url, 334 cachePolicy: self.sessionConfiguration.requestCachePolicy, 335 timeoutInterval: FileDownloader.DefaultTimeoutInterval 336 ) 337 setManifestHTTPHeaderFields(request: &request, extraHeaders: extraHeaders) 338 return request 339 } 340 createGenericRequestnull341 func createGenericRequest(withURL url: URL, extraHeaders: [String: Any?]) -> URLRequest { 342 var request = URLRequest( 343 url: url, 344 cachePolicy: self.sessionConfiguration.requestCachePolicy, 345 timeoutInterval: FileDownloader.DefaultTimeoutInterval 346 ) 347 setHTTPHeaderFields(request: &request, extraHeaders: extraHeaders) 348 return request 349 } 350 351 // MARK: - manifest parsing 352 353 func parseManifestResponse( 354 _ httpResponse: HTTPURLResponse, 355 withData data: Data?, 356 database: UpdatesDatabase, 357 successBlock: @escaping RemoteUpdateDownloadSuccessBlock, 358 errorBlock: @escaping RemoteUpdateDownloadErrorBlock 359 ) { 360 let responseHeaderData = ResponseHeaderData( 361 protocolVersionRaw: httpResponse.value(forHTTPHeaderField: "expo-protocol-version"), 362 serverDefinedHeadersRaw: httpResponse.value(forHTTPHeaderField: "expo-server-defined-headers"), 363 manifestFiltersRaw: httpResponse.value(forHTTPHeaderField: "expo-manifest-filters"), 364 manifestSignature: httpResponse.value(forHTTPHeaderField: "expo-manifest-signature") 365 ) 366 367 if httpResponse.statusCode == 204 || data == nil { 368 if let protocolVersion = responseHeaderData.protocolVersion, 369 protocolVersion > 0 { 370 successBlock(UpdateResponse( 371 responseHeaderData: responseHeaderData, 372 manifestUpdateResponsePart: nil, 373 directiveUpdateResponsePart: nil 374 )) 375 return 376 } 377 } 378 379 guard let data = data else { 380 let errorMessage = "Missing body in remote update" 381 logger.error(message: errorMessage, code: UpdatesErrorCode.unknown) 382 errorBlock(NSError( 383 domain: ErrorDomain, 384 code: FileDownloaderErrorCode.InvalidResponseError.rawValue, 385 userInfo: [NSLocalizedDescriptionKey: errorMessage] 386 )) 387 return 388 } 389 390 let contentType = httpResponse.value(forHTTPHeaderField: "content-type") ?? "" 391 392 if contentType.lowercased().hasPrefix("multipart/") { 393 guard let contentTypeParameters = ABI49_0_0EXUpdatesParameterParser().parseParameterString( 394 contentType, 395 withDelimiter: FileDownloader.ParameterParserSemicolonDelimiter 396 ) as? [String: Any], 397 let boundaryParameterValue: String = contentTypeParameters.optionalValue(forKey: "boundary") else { 398 let errorMessage = "Missing boundary in multipart manifest content-type" 399 logger.error(message: errorMessage, code: UpdatesErrorCode.unknown) 400 errorBlock(NSError( 401 domain: ErrorDomain, 402 code: FileDownloaderErrorCode.MissingMultipartBoundaryError.rawValue, 403 userInfo: [NSLocalizedDescriptionKey: errorMessage] 404 )) 405 return 406 } 407 408 parseMultipartManifestResponse( 409 httpResponse, 410 withData: data, 411 database: database, 412 boundary: boundaryParameterValue, 413 successBlock: successBlock, 414 errorBlock: errorBlock 415 ) 416 return 417 } else { 418 let responseHeaderData = ResponseHeaderData( 419 protocolVersionRaw: httpResponse.value(forHTTPHeaderField: "expo-protocol-version"), 420 serverDefinedHeadersRaw: httpResponse.value(forHTTPHeaderField: "expo-server-defined-headers"), 421 manifestFiltersRaw: httpResponse.value(forHTTPHeaderField: "expo-manifest-filters"), 422 manifestSignature: httpResponse.value(forHTTPHeaderField: "expo-manifest-signature") 423 ) 424 425 let manifestResponseInfo = ResponsePartInfo( 426 responseHeaderData: responseHeaderData, 427 responsePartHeaderData: ResponsePartHeaderData(signature: httpResponse.value(forHTTPHeaderField: "expo-signature")), 428 body: data 429 ) 430 431 parseManifestResponsePartInfo( 432 manifestResponseInfo, 433 extensions: [:], 434 certificateChainFromManifestResponse: nil, 435 database: database 436 ) { manifestUpdateResponsePart in 437 successBlock(UpdateResponse( 438 responseHeaderData: responseHeaderData, 439 manifestUpdateResponsePart: manifestUpdateResponsePart, 440 directiveUpdateResponsePart: nil 441 )) 442 } errorBlock: { error in 443 errorBlock(error) 444 } 445 446 return 447 } 448 } 449 450 private func parseMultipartManifestResponse( 451 _ httpResponse: HTTPURLResponse, 452 withData data: Data, 453 database: UpdatesDatabase, 454 boundary: String, 455 successBlock: @escaping RemoteUpdateDownloadSuccessBlock, 456 errorBlock: @escaping RemoteUpdateDownloadErrorBlock 457 ) { 458 let reader = ABI49_0_0EXUpdatesMultipartStreamReader(inputStream: InputStream(data: data), boundary: boundary) 459 460 var manifestPartHeadersAndData: ([String: Any], Data)? 461 var extensionsData: Data? 462 var certificateChainStringData: Data? 463 var directivePartHeadersAndData: ([String: Any], Data)? 464 465 let completed = data.isEmpty || reader.readAllParts { headers, content, _ in 466 if let contentDisposition = (headers as! [String: Any]).stringValueForCaseInsensitiveKey("content-disposition") { 467 if let contentDispositionParameters = ABI49_0_0EXUpdatesParameterParser().parseParameterString( 468 contentDisposition, 469 withDelimiter: FileDownloader.ParameterParserSemicolonDelimiter 470 ) as? [String: Any], 471 let contentDispositionNameFieldValue: String = contentDispositionParameters.optionalValue(forKey: "name") { 472 switch contentDispositionNameFieldValue { 473 case FileDownloader.MultipartManifestPartName: 474 if let headers = headers as? [String: Any], let content = content { 475 manifestPartHeadersAndData = (headers, content) 476 } 477 case FileDownloader.MultipartDirectivePartName: 478 if let headers = headers as? [String: Any], let content = content { 479 directivePartHeadersAndData = (headers, content) 480 } 481 case FileDownloader.MultipartExtensionsPartName: 482 extensionsData = content 483 case FileDownloader.MultipartCertificateChainPartName: 484 certificateChainStringData = content 485 default: 486 break 487 } 488 } 489 } 490 } 491 492 if !completed { 493 let message = "Could not read multipart remote update response" 494 logger.error(message: message, code: .unknown) 495 errorBlock(NSError( 496 domain: ErrorDomain, 497 code: FileDownloaderErrorCode.MultipartParsingError.rawValue, 498 userInfo: [NSLocalizedDescriptionKey: message] 499 )) 500 return 501 } 502 503 var extensions: [String: Any] = [:] 504 if let extensionsData = extensionsData { 505 let parsedExtensions: Any 506 do { 507 parsedExtensions = try JSONSerialization.jsonObject(with: extensionsData) 508 } catch { 509 errorBlock(error) 510 return 511 } 512 513 guard let parsedExtensions = parsedExtensions as? [String: Any] else { 514 let message = "Failed to parse multipart remote update extensions" 515 logger.error(message: message, code: .unknown) 516 errorBlock(NSError( 517 domain: ErrorDomain, 518 code: FileDownloaderErrorCode.MultipartParsingError.rawValue, 519 userInfo: [NSLocalizedDescriptionKey: message] 520 )) 521 return 522 } 523 524 extensions = parsedExtensions 525 } 526 527 if config.enableExpoUpdatesProtocolV0CompatibilityMode && manifestPartHeadersAndData == nil { 528 let message = "Multipart response missing manifest part. Manifest is required in version 0 of the expo-updates protocol. This may be due to the update being a rollback or other directive." 529 logger.error(message: message, code: .unknown) 530 errorBlock(NSError( 531 domain: ErrorDomain, 532 code: FileDownloaderErrorCode.MultipartMissingManifestError.rawValue, 533 userInfo: [NSLocalizedDescriptionKey: message] 534 )) 535 return 536 } 537 538 let certificateChain = certificateChainStringData.let { it -> String? in 539 String(data: it, encoding: .utf8) 540 } 541 542 let responseHeaderData = ResponseHeaderData( 543 protocolVersionRaw: httpResponse.value(forHTTPHeaderField: "expo-protocol-version"), 544 serverDefinedHeadersRaw: httpResponse.value(forHTTPHeaderField: "expo-server-defined-headers"), 545 manifestFiltersRaw: httpResponse.value(forHTTPHeaderField: "expo-manifest-filters"), 546 manifestSignature: httpResponse.value(forHTTPHeaderField: "expo-manifest-signature") 547 ) 548 549 let manifestResponseInfo = manifestPartHeadersAndData.let { it in 550 ResponsePartInfo( 551 responseHeaderData: responseHeaderData, 552 responsePartHeaderData: ResponsePartHeaderData(signature: it.0.optionalValue(forKey: "expo-signature")), 553 body: it.1 554 ) 555 } 556 557 // in v0 compatibility mode ignore directives 558 let directiveResponseInfo = config.enableExpoUpdatesProtocolV0CompatibilityMode ? 559 nil : 560 directivePartHeadersAndData.let { it in 561 ResponsePartInfo( 562 responseHeaderData: responseHeaderData, 563 responsePartHeaderData: ResponsePartHeaderData(signature: it.0.optionalValue(forKey: "expo-signature")), 564 body: it.1 565 ) 566 } 567 568 var parseManifestResponse: ManifestUpdateResponsePart? 569 var parseDirectiveResponse: DirectiveUpdateResponsePart? 570 var didError = false 571 572 let maybeFinish = { 573 if !didError { 574 let isManifestDone = manifestResponseInfo == nil || parseManifestResponse != nil 575 let isDirectiveDone = directiveResponseInfo == nil || parseDirectiveResponse != nil 576 577 if isManifestDone && isDirectiveDone { 578 successBlock(UpdateResponse( 579 responseHeaderData: responseHeaderData, 580 manifestUpdateResponsePart: parseManifestResponse, 581 directiveUpdateResponsePart: parseDirectiveResponse 582 )) 583 } 584 } 585 } 586 587 if let directiveResponseInfo = directiveResponseInfo { 588 parseDirectiveResponsePartInfo( 589 directiveResponseInfo, 590 certificateChainFromManifestResponse: certificateChain 591 ) { directiveUpdateResponsePart in 592 parseDirectiveResponse = directiveUpdateResponsePart 593 maybeFinish() 594 } errorBlock: { error in 595 if !didError { 596 didError = true 597 errorBlock(error) 598 } 599 } 600 } 601 602 if let manifestResponseInfo = manifestResponseInfo { 603 parseManifestResponsePartInfo( 604 manifestResponseInfo, 605 extensions: extensions, 606 certificateChainFromManifestResponse: certificateChain, 607 database: database 608 ) { manifestUpdateResponsePart in 609 parseManifestResponse = manifestUpdateResponsePart 610 maybeFinish() 611 } errorBlock: { error in 612 if !didError { 613 didError = true 614 errorBlock(error) 615 } 616 } 617 } 618 619 // if both parts are empty, we still want to finish 620 if manifestResponseInfo == nil && directiveResponseInfo == nil { 621 maybeFinish() 622 } 623 } 624 625 private func parseDirectiveResponsePartInfo( 626 _ responsePartInfo: ResponsePartInfo, 627 certificateChainFromManifestResponse: String?, 628 successBlock: @escaping ParseDirectiveSuccessBlock, 629 errorBlock: @escaping ParseDirectiveErrorBlock 630 ) { 631 // check code signing if code signing is configured 632 // 1. verify the code signing signature (throw if invalid) 633 // 2. then, if the code signing certificate is only valid for a particular project, verify that the manifest 634 // has the correct info for code signing. If the code signing certificate doesn't specify a particular 635 // project, it is assumed to be valid for all projects 636 // 3. consider the directive valid if both of these pass 637 if let codeSigningConfiguration = config.codeSigningConfiguration { 638 let signatureValidationResult: SignatureValidationResult 639 do { 640 signatureValidationResult = try codeSigningConfiguration.validateSignature( 641 signature: responsePartInfo.responsePartHeaderData.signature, 642 signedData: responsePartInfo.body, 643 manifestResponseCertificateChain: certificateChainFromManifestResponse 644 ) 645 } catch { 646 let codeSigningError = error as? CodeSigningError 647 let message = codeSigningError?.message() ?? error.localizedDescription 648 self.logger.error(message: message, code: .unknown) 649 errorBlock(NSError( 650 domain: ErrorDomain, 651 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 652 userInfo: [NSLocalizedDescriptionKey: message] 653 )) 654 return 655 } 656 657 if signatureValidationResult.validationResult == .invalid { 658 let message = "Directive download was successful, but signature was incorrect" 659 self.logger.error(message: message, code: .unknown) 660 errorBlock(NSError( 661 domain: ErrorDomain, 662 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 663 userInfo: [NSLocalizedDescriptionKey: message] 664 )) 665 return 666 } 667 668 if signatureValidationResult.validationResult != .skipped { 669 if let expoProjectInformation = signatureValidationResult.expoProjectInformation { 670 let directive: UpdateDirective 671 do { 672 directive = try UpdateDirective.fromJSONData(responsePartInfo.body) 673 } catch { 674 let message = "Failed to parse directive: \(error.localizedDescription)" 675 self.logger.error(message: message, code: .unknown) 676 errorBlock(NSError( 677 domain: ErrorDomain, 678 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 679 userInfo: [NSLocalizedDescriptionKey: message] 680 )) 681 return 682 } 683 684 if expoProjectInformation.projectId != directive.signingInfo?.easProjectId || 685 expoProjectInformation.scopeKey != directive.signingInfo?.scopeKey { 686 let message = "Invalid certificate for directive project ID or scope key" 687 self.logger.error(message: message, code: .unknown) 688 errorBlock(NSError( 689 domain: ErrorDomain, 690 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 691 userInfo: [NSLocalizedDescriptionKey: message] 692 )) 693 return 694 } 695 } 696 logger.info(message: "Update directive code signature verified successfully") 697 } 698 } 699 700 let directive: UpdateDirective 701 do { 702 directive = try UpdateDirective.fromJSONData(responsePartInfo.body) 703 } catch { 704 let message = "Failed to parse directive: \(error.localizedDescription)" 705 self.logger.error(message: message, code: .unknown) 706 errorBlock(NSError( 707 domain: ErrorDomain, 708 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 709 userInfo: [NSLocalizedDescriptionKey: message] 710 )) 711 return 712 } 713 714 successBlock(DirectiveUpdateResponsePart(updateDirective: directive)) 715 } 716 717 private func parseManifestResponsePartInfo( 718 _ responsePartInfo: ResponsePartInfo, 719 extensions: [String: Any], 720 certificateChainFromManifestResponse: String?, 721 database: UpdatesDatabase, 722 successBlock: @escaping ParseManifestSuccessBlock, 723 errorBlock: @escaping ParseManifestErrorBlock 724 ) { 725 let headerSignature = responsePartInfo.responseHeaderData.manifestSignature 726 727 let updateResponseDictionary: [String: Any] 728 do { 729 let manifestBodyJson = try JSONSerialization.jsonObject(with: responsePartInfo.body) 730 updateResponseDictionary = try extractUpdateResponseDictionary(parsedJson: manifestBodyJson) 731 } catch { 732 errorBlock(error) 733 return 734 } 735 736 let bodyManifestString = updateResponseDictionary["manifestString"] 737 let bodySignature = updateResponseDictionary["signature"] 738 let isSignatureInBody = bodyManifestString != nil && bodySignature != nil 739 740 let signature = isSignatureInBody ? bodySignature : headerSignature 741 let manifestString = isSignatureInBody ? bodyManifestString : String(data: responsePartInfo.body, encoding: .utf8) 742 743 // XDL serves unsigned manifests with the `signature` key set to "UNSIGNED". 744 // We should treat these manifests as unsigned rather than signed with an invalid signature. 745 let isUnsignedFromXDL = signature as? String == "UNSIGNED" 746 747 guard let manifestString = manifestString as? String else { 748 let message = "manifestString should be a string" 749 logger.error(message: message, code: .unknown) 750 errorBlock(NSError( 751 domain: ErrorDomain, 752 code: FileDownloaderErrorCode.ManifestStringError.rawValue, 753 userInfo: [NSLocalizedDescriptionKey: message] 754 )) 755 return 756 } 757 758 guard let manifestStringData = manifestString.data(using: .utf8) else { 759 let message = "manifest should be a valid JSON object" 760 logger.error(message: message, code: .unknown) 761 errorBlock(NSError( 762 domain: ErrorDomain, 763 code: FileDownloaderErrorCode.ManifestJSONError.rawValue, 764 userInfo: [NSLocalizedDescriptionKey: message] 765 )) 766 return 767 } 768 769 var manifest: [String: Any]? 770 var manifestParseError: (any Error)? 771 do { 772 manifest = try JSONSerialization.jsonObject(with: manifestStringData) as? [String: Any] 773 } catch { 774 manifestParseError = error 775 } 776 777 guard let manifest = manifest, manifestParseError == nil else { 778 let message = "manifest should be a valid JSON object" 779 logger.error(message: message, code: .unknown) 780 errorBlock(NSError( 781 domain: ErrorDomain, 782 code: FileDownloaderErrorCode.ManifestJSONError.rawValue, 783 userInfo: [NSLocalizedDescriptionKey: message] 784 )) 785 return 786 } 787 788 if let signature = signature, !isUnsignedFromXDL { 789 guard let signature = signature as? String else { 790 let message = "signature should be a string" 791 logger.error(message: message, code: .unknown) 792 errorBlock(NSError( 793 domain: ErrorDomain, 794 code: FileDownloaderErrorCode.ManifestSignatureError.rawValue, 795 userInfo: [NSLocalizedDescriptionKey: message] 796 )) 797 return 798 } 799 800 Crypto.verifySignature( 801 withData: manifestString, 802 signature: signature, 803 config: config 804 ) { isValid in 805 guard isValid else { 806 let message = "Manifest verification failed" 807 self.logger.error(message: message, code: .unknown) 808 errorBlock(NSError( 809 domain: ErrorDomain, 810 code: FileDownloaderErrorCode.ManifestVerificationError.rawValue, 811 userInfo: [NSLocalizedDescriptionKey: message] 812 )) 813 return 814 } 815 816 self.createUpdate( 817 manifest: manifest, 818 responsePartInfo: responsePartInfo, 819 extensions: extensions, 820 certificateChainFromManifestResponse: certificateChainFromManifestResponse, 821 database: database, 822 isVerified: true, 823 successBlock: successBlock, 824 errorBlock: errorBlock 825 ) 826 } errorBlock: { error in 827 errorBlock(error) 828 } 829 } else { 830 createUpdate( 831 manifest: manifest, 832 responsePartInfo: responsePartInfo, 833 extensions: extensions, 834 certificateChainFromManifestResponse: certificateChainFromManifestResponse, 835 database: database, 836 isVerified: false, 837 successBlock: successBlock, 838 errorBlock: errorBlock 839 ) 840 } 841 } 842 extractUpdateResponseDictionarynull843 private func extractUpdateResponseDictionary(parsedJson: Any) throws -> [String: Any] { 844 if let parsedJson = parsedJson as? [String: Any] { 845 return parsedJson 846 } else if let parsedJson = parsedJson as? [Any] { 847 // TODO: either add support for runtimeVersion or deprecate multi-manifests 848 for providedManifest in parsedJson { 849 if let providedManifest = providedManifest as? [String: Any], 850 let sdkVersion: String = providedManifest.optionalValue(forKey: "sdkVersion"), 851 let supportedSdkVersions = config.sdkVersion?.components(separatedBy: ","), 852 supportedSdkVersions.contains(sdkVersion) { 853 return providedManifest 854 } 855 } 856 } 857 858 throw NSError( 859 domain: ErrorDomain, 860 code: FileDownloaderErrorCode.NoCompatibleUpdateError.rawValue, 861 userInfo: [ 862 NSLocalizedDescriptionKey: String( 863 format: "No compatible update found at %@. Only %@ are supported.", 864 config.updateUrl?.absoluteString ?? "(missing config updateUrl)", 865 config.sdkVersion ?? "(missing sdkVersion field)" 866 ) 867 ] 868 ) 869 } 870 871 private func createUpdate( 872 manifest: [String: Any], 873 responsePartInfo: ResponsePartInfo, 874 extensions: [String: Any], 875 certificateChainFromManifestResponse: String?, 876 database: UpdatesDatabase, 877 isVerified: Bool, 878 successBlock: ParseManifestSuccessBlock, 879 errorBlock: ParseManifestErrorBlock 880 ) { 881 var mutableManifest = manifest 882 if config.expectsSignedManifest { 883 // There are a few cases in Expo Go where we still want to use the unsigned manifest anyway, so don't mark it as unverified. 884 mutableManifest["isVerified"] = isVerified 885 } 886 887 // check code signing if code signing is configured 888 // 1. verify the code signing signature (throw if invalid) 889 // 2. then, if the code signing certificate is only valid for a particular project, verify that the manifest 890 // has the correct info for code signing. If the code signing certificate doesn't specify a particular 891 // project, it is assumed to be valid for all projects 892 // 3. mark the manifest as verified if both of these pass 893 if let codeSigningConfiguration = config.codeSigningConfiguration { 894 let signatureValidationResult: SignatureValidationResult 895 do { 896 signatureValidationResult = try codeSigningConfiguration.validateSignature( 897 signature: responsePartInfo.responsePartHeaderData.signature, 898 signedData: responsePartInfo.body, 899 manifestResponseCertificateChain: certificateChainFromManifestResponse 900 ) 901 } catch { 902 let codeSigningError = error as? CodeSigningError 903 let message = codeSigningError?.message() ?? error.localizedDescription 904 self.logger.error(message: message, code: .unknown) 905 errorBlock(NSError( 906 domain: ErrorDomain, 907 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 908 userInfo: [NSLocalizedDescriptionKey: message] 909 )) 910 return 911 } 912 913 if signatureValidationResult.validationResult == .invalid { 914 let message = "Manifest download was successful, but signature was incorrect" 915 self.logger.error(message: message, code: .unknown) 916 errorBlock(NSError( 917 domain: ErrorDomain, 918 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 919 userInfo: [NSLocalizedDescriptionKey: message] 920 )) 921 return 922 } 923 924 if signatureValidationResult.validationResult != .skipped { 925 if let expoProjectInformation = signatureValidationResult.expoProjectInformation { 926 let update: Update 927 do { 928 update = try Update.update( 929 withManifest: mutableManifest, 930 responseHeaderData: responsePartInfo.responseHeaderData, 931 extensions: extensions, 932 config: config, 933 database: database 934 ) 935 } catch { 936 // Catch any assertions related to parsing the manifest JSON, 937 // this will ensure invalid manifests can be easily debugged. 938 // For example, this will catch nullish sdkVersion assertions. 939 let message = "Failed to parse manifest: \(error.localizedDescription)" 940 self.logger.error(message: message, code: .unknown) 941 errorBlock(NSError( 942 domain: ErrorDomain, 943 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 944 userInfo: [NSLocalizedDescriptionKey: message] 945 )) 946 return 947 } 948 949 let manifestForProjectInformation = update.manifest 950 if expoProjectInformation.projectId != manifestForProjectInformation.easProjectId() || 951 expoProjectInformation.scopeKey != manifestForProjectInformation.scopeKey() { 952 let message = "Invalid certificate for manifest project ID or scope key" 953 self.logger.error(message: message, code: .unknown) 954 errorBlock(NSError( 955 domain: ErrorDomain, 956 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 957 userInfo: [NSLocalizedDescriptionKey: message] 958 )) 959 return 960 } 961 } 962 logger.info(message: "Update code signature verified successfully") 963 mutableManifest["isVerified"] = true 964 } 965 } 966 967 let update: Update 968 do { 969 update = try Update.update( 970 withManifest: mutableManifest, 971 responseHeaderData: responsePartInfo.responseHeaderData, 972 extensions: extensions, 973 config: config, 974 database: database 975 ) 976 } catch { 977 // Catch any assertions related to parsing the manifest JSON, 978 // this will ensure invalid manifests can be easily debugged. 979 // For example, this will catch nullish sdkVersion assertions. 980 let message = "Failed to parse manifest: \(error.localizedDescription)" 981 self.logger.error(message: message, code: .unknown) 982 errorBlock(NSError( 983 domain: ErrorDomain, 984 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 985 userInfo: [NSLocalizedDescriptionKey: message] 986 )) 987 return 988 } 989 990 if !SelectionPolicies.doesUpdate(update, matchFilters: responsePartInfo.responseHeaderData.manifestFilters) { 991 let message = "Downloaded manifest is invalid; provides filters that do not match its content" 992 self.logger.error(message: message, code: .unknown) 993 errorBlock(NSError( 994 domain: ErrorDomain, 995 code: FileDownloaderErrorCode.MismatchedManifestFiltersError.rawValue, 996 userInfo: [NSLocalizedDescriptionKey: message] 997 )) 998 return 999 } 1000 1001 successBlock(ManifestUpdateResponsePart(updateManifest: update)) 1002 } 1003 1004 private func downloadData( 1005 withRequest request: URLRequest, 1006 successBlock: @escaping SuccessBlock, 1007 errorBlock: @escaping ErrorBlock 1008 ) { 1009 let task = session.dataTask(with: request) { data, response, error in 1010 guard let response = response else { 1011 // error is non-nil when data and response are both nil 1012 // swiftlint:disable:next force_unwrapping 1013 let error = error! 1014 self.logger.error(message: error.localizedDescription, code: .unknown) 1015 errorBlock(error) 1016 return 1017 } 1018 1019 if let httpResponse = response as? HTTPURLResponse, 1020 httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 { 1021 let encoding = FileDownloader.encoding(fromResponse: httpResponse) 1022 let body = data.let { it in 1023 String(data: it, encoding: encoding) 1024 } ?? "Unknown body response" 1025 let error = FileDownloader.error(fromResponse: httpResponse, body: body) 1026 self.logger.error(message: error.localizedDescription, code: .unknown) 1027 errorBlock(error) 1028 return 1029 } 1030 1031 successBlock(data, response) 1032 } 1033 task.resume() 1034 } 1035 encodingnull1036 private static func encoding(fromResponse response: URLResponse) -> String.Encoding { 1037 if let textEncodingName = response.textEncodingName { 1038 let cfEncoding = CFStringConvertIANACharSetNameToEncoding(textEncodingName as CFString) 1039 return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(cfEncoding)) 1040 } 1041 // Default to UTF-8 1042 return .utf8 1043 } 1044 errornull1045 private static func error(fromResponse response: HTTPURLResponse, body: String) -> NSError { 1046 return NSError( 1047 domain: ErrorDomain, 1048 code: response.statusCode, 1049 userInfo: [NSLocalizedDescriptionKey: body] 1050 ) 1051 } 1052 1053 // MARK: - NSURLSessionTaskDelegate 1054 1055 func urlSession( 1056 _ session: URLSession, 1057 task: URLSessionTask, 1058 willPerformHTTPRedirection response: HTTPURLResponse, 1059 newRequest request: URLRequest, 1060 completionHandler: @escaping (URLRequest?) -> Void 1061 ) { 1062 completionHandler(request) 1063 } 1064 1065 // MARK: - URLSessionDataDelegate 1066 1067 func urlSession( 1068 _ session: URLSession, 1069 dataTask: URLSessionDataTask, 1070 willCacheResponse proposedResponse: CachedURLResponse, 1071 completionHandler: @escaping (CachedURLResponse?) -> Void 1072 ) { 1073 completionHandler(proposedResponse) 1074 } 1075 } 1076