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 { 51 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) { 64 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 258 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 282 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 295 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 331 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 341 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 headerDictionary = httpResponse.allHeaderFields as! [String: Any] 361 let responseHeaderData = ResponseHeaderData( 362 protocolVersionRaw: headerDictionary.optionalValue(forKey: "expo-protocol-version"), 363 serverDefinedHeadersRaw: headerDictionary.optionalValue(forKey: "expo-server-defined-headers"), 364 manifestFiltersRaw: headerDictionary.optionalValue(forKey: "expo-manifest-filters"), 365 manifestSignature: headerDictionary.optionalValue(forKey: "expo-manifest-signature") 366 ) 367 368 if httpResponse.statusCode == 204 || data == nil { 369 if let protocolVersion = responseHeaderData.protocolVersion, 370 protocolVersion > 0 { 371 successBlock(UpdateResponse( 372 responseHeaderData: responseHeaderData, 373 manifestUpdateResponsePart: nil, 374 directiveUpdateResponsePart: nil 375 )) 376 return 377 } 378 } 379 380 guard let data = data else { 381 let errorMessage = "Missing body in remote update" 382 logger.error(message: errorMessage, code: UpdatesErrorCode.unknown) 383 errorBlock(NSError( 384 domain: ErrorDomain, 385 code: FileDownloaderErrorCode.InvalidResponseError.rawValue, 386 userInfo: [NSLocalizedDescriptionKey: errorMessage] 387 )) 388 return 389 } 390 391 let contentType = headerDictionary.stringValueForCaseInsensitiveKey("content-type") ?? "" 392 393 if contentType.lowercased().hasPrefix("multipart/") { 394 guard let contentTypeParameters = ABI49_0_0EXUpdatesParameterParser().parseParameterString( 395 contentType, 396 withDelimiter: FileDownloader.ParameterParserSemicolonDelimiter 397 ) as? [String: Any], 398 let boundaryParameterValue: String = contentTypeParameters.optionalValue(forKey: "boundary") else { 399 let errorMessage = "Missing boundary in multipart manifest content-type" 400 logger.error(message: errorMessage, code: UpdatesErrorCode.unknown) 401 errorBlock(NSError( 402 domain: ErrorDomain, 403 code: FileDownloaderErrorCode.MissingMultipartBoundaryError.rawValue, 404 userInfo: [NSLocalizedDescriptionKey: errorMessage] 405 )) 406 return 407 } 408 409 parseMultipartManifestResponse( 410 httpResponse, 411 withData: data, 412 database: database, 413 boundary: boundaryParameterValue, 414 successBlock: successBlock, 415 errorBlock: errorBlock 416 ) 417 return 418 } else { 419 let responseHeaderData = ResponseHeaderData( 420 protocolVersionRaw: headerDictionary.optionalValue(forKey: "expo-protocol-version"), 421 serverDefinedHeadersRaw: headerDictionary.optionalValue(forKey: "expo-server-defined-headers"), 422 manifestFiltersRaw: headerDictionary.optionalValue(forKey: "expo-manifest-filters"), 423 manifestSignature: headerDictionary.optionalValue(forKey: "expo-manifest-signature") 424 ) 425 426 let manifestResponseInfo = ResponsePartInfo( 427 responseHeaderData: responseHeaderData, 428 responsePartHeaderData: ResponsePartHeaderData(signature: headerDictionary.optionalValue(forKey: "expo-signature")), 429 body: data 430 ) 431 432 parseManifestResponsePartInfo( 433 manifestResponseInfo, 434 extensions: [:], 435 certificateChainFromManifestResponse: nil, 436 database: database 437 ) { manifestUpdateResponsePart in 438 successBlock(UpdateResponse( 439 responseHeaderData: responseHeaderData, 440 manifestUpdateResponsePart: manifestUpdateResponsePart, 441 directiveUpdateResponsePart: nil 442 )) 443 } errorBlock: { error in 444 errorBlock(error) 445 } 446 447 return 448 } 449 } 450 451 private func parseMultipartManifestResponse( 452 _ httpResponse: HTTPURLResponse, 453 withData data: Data, 454 database: UpdatesDatabase, 455 boundary: String, 456 successBlock: @escaping RemoteUpdateDownloadSuccessBlock, 457 errorBlock: @escaping RemoteUpdateDownloadErrorBlock 458 ) { 459 let reader = ABI49_0_0EXUpdatesMultipartStreamReader(inputStream: InputStream(data: data), boundary: boundary) 460 461 var manifestPartHeadersAndData: ([String: Any], Data)? 462 var extensionsData: Data? 463 var certificateChainStringData: Data? 464 var directivePartHeadersAndData: ([String: Any], Data)? 465 466 let completed = data.isEmpty || reader.readAllParts { headers, content, _ in 467 if let contentDisposition = (headers as! [String: Any]).stringValueForCaseInsensitiveKey("content-disposition") { 468 if let contentDispositionParameters = ABI49_0_0EXUpdatesParameterParser().parseParameterString( 469 contentDisposition, 470 withDelimiter: FileDownloader.ParameterParserSemicolonDelimiter 471 ) as? [String: Any], 472 let contentDispositionNameFieldValue: String = contentDispositionParameters.optionalValue(forKey: "name") { 473 switch contentDispositionNameFieldValue { 474 case FileDownloader.MultipartManifestPartName: 475 if let headers = headers as? [String: Any], let content = content { 476 manifestPartHeadersAndData = (headers, content) 477 } 478 case FileDownloader.MultipartDirectivePartName: 479 if let headers = headers as? [String: Any], let content = content { 480 directivePartHeadersAndData = (headers, content) 481 } 482 case FileDownloader.MultipartExtensionsPartName: 483 extensionsData = content 484 case FileDownloader.MultipartCertificateChainPartName: 485 certificateChainStringData = content 486 default: 487 break 488 } 489 } 490 } 491 } 492 493 if !completed { 494 let message = "Could not read multipart remote update response" 495 logger.error(message: message, code: .unknown) 496 errorBlock(NSError( 497 domain: ErrorDomain, 498 code: FileDownloaderErrorCode.MultipartParsingError.rawValue, 499 userInfo: [NSLocalizedDescriptionKey: message] 500 )) 501 return 502 } 503 504 var extensions: [String: Any] = [:] 505 if let extensionsData = extensionsData { 506 let parsedExtensions: Any 507 do { 508 parsedExtensions = try JSONSerialization.jsonObject(with: extensionsData) 509 } catch { 510 errorBlock(error) 511 return 512 } 513 514 guard let parsedExtensions = parsedExtensions as? [String: Any] else { 515 let message = "Failed to parse multipart remote update extensions" 516 logger.error(message: message, code: .unknown) 517 errorBlock(NSError( 518 domain: ErrorDomain, 519 code: FileDownloaderErrorCode.MultipartParsingError.rawValue, 520 userInfo: [NSLocalizedDescriptionKey: message] 521 )) 522 return 523 } 524 525 extensions = parsedExtensions 526 } 527 528 if config.enableExpoUpdatesProtocolV0CompatibilityMode && manifestPartHeadersAndData == nil { 529 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." 530 logger.error(message: message, code: .unknown) 531 errorBlock(NSError( 532 domain: ErrorDomain, 533 code: FileDownloaderErrorCode.MultipartMissingManifestError.rawValue, 534 userInfo: [NSLocalizedDescriptionKey: message] 535 )) 536 return 537 } 538 539 let certificateChain = certificateChainStringData.let { it -> String? in 540 String(data: it, encoding: .utf8) 541 } 542 543 let responseHeaders = httpResponse.allHeaderFields as! [String: Any] 544 let responseHeaderData = ResponseHeaderData( 545 protocolVersionRaw: responseHeaders.optionalValue(forKey: "expo-protocol-version"), 546 serverDefinedHeadersRaw: responseHeaders.optionalValue(forKey: "expo-server-defined-headers"), 547 manifestFiltersRaw: responseHeaders.optionalValue(forKey: "expo-manifest-filters"), 548 manifestSignature: responseHeaders.optionalValue(forKey: "expo-manifest-signature") 549 ) 550 551 let manifestResponseInfo = manifestPartHeadersAndData.let { it in 552 ResponsePartInfo( 553 responseHeaderData: responseHeaderData, 554 responsePartHeaderData: ResponsePartHeaderData(signature: it.0.optionalValue(forKey: "expo-signature")), 555 body: it.1 556 ) 557 } 558 559 // in v0 compatibility mode ignore directives 560 let directiveResponseInfo = config.enableExpoUpdatesProtocolV0CompatibilityMode ? 561 nil : 562 directivePartHeadersAndData.let { it in 563 ResponsePartInfo( 564 responseHeaderData: responseHeaderData, 565 responsePartHeaderData: ResponsePartHeaderData(signature: it.0.optionalValue(forKey: "expo-signature")), 566 body: it.1 567 ) 568 } 569 570 var parseManifestResponse: ManifestUpdateResponsePart? 571 var parseDirectiveResponse: DirectiveUpdateResponsePart? 572 var didError = false 573 574 let maybeFinish = { 575 if !didError { 576 let isManifestDone = manifestResponseInfo == nil || parseManifestResponse != nil 577 let isDirectiveDone = directiveResponseInfo == nil || parseDirectiveResponse != nil 578 579 if isManifestDone && isDirectiveDone { 580 successBlock(UpdateResponse( 581 responseHeaderData: responseHeaderData, 582 manifestUpdateResponsePart: parseManifestResponse, 583 directiveUpdateResponsePart: parseDirectiveResponse 584 )) 585 } 586 } 587 } 588 589 if let directiveResponseInfo = directiveResponseInfo { 590 parseDirectiveResponsePartInfo( 591 directiveResponseInfo, 592 certificateChainFromManifestResponse: certificateChain 593 ) { directiveUpdateResponsePart in 594 parseDirectiveResponse = directiveUpdateResponsePart 595 maybeFinish() 596 } errorBlock: { error in 597 if !didError { 598 didError = true 599 errorBlock(error) 600 } 601 } 602 } 603 604 if let manifestResponseInfo = manifestResponseInfo { 605 parseManifestResponsePartInfo( 606 manifestResponseInfo, 607 extensions: extensions, 608 certificateChainFromManifestResponse: certificateChain, 609 database: database 610 ) { manifestUpdateResponsePart in 611 parseManifestResponse = manifestUpdateResponsePart 612 maybeFinish() 613 } errorBlock: { error in 614 if !didError { 615 didError = true 616 errorBlock(error) 617 } 618 } 619 } 620 621 // if both parts are empty, we still want to finish 622 if manifestResponseInfo == nil && directiveResponseInfo == nil { 623 maybeFinish() 624 } 625 } 626 627 private func parseDirectiveResponsePartInfo( 628 _ responsePartInfo: ResponsePartInfo, 629 certificateChainFromManifestResponse: String?, 630 successBlock: @escaping ParseDirectiveSuccessBlock, 631 errorBlock: @escaping ParseDirectiveErrorBlock 632 ) { 633 // check code signing if code signing is configured 634 // 1. verify the code signing signature (throw if invalid) 635 // 2. then, if the code signing certificate is only valid for a particular project, verify that the manifest 636 // has the correct info for code signing. If the code signing certificate doesn't specify a particular 637 // project, it is assumed to be valid for all projects 638 // 3. consider the directive valid if both of these pass 639 if let codeSigningConfiguration = config.codeSigningConfiguration { 640 let signatureValidationResult: SignatureValidationResult 641 do { 642 signatureValidationResult = try codeSigningConfiguration.validateSignature( 643 signature: responsePartInfo.responsePartHeaderData.signature, 644 signedData: responsePartInfo.body, 645 manifestResponseCertificateChain: certificateChainFromManifestResponse 646 ) 647 } catch { 648 let codeSigningError = error as? CodeSigningError 649 let message = codeSigningError?.message() ?? error.localizedDescription 650 self.logger.error(message: message, code: .unknown) 651 errorBlock(NSError( 652 domain: ErrorDomain, 653 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 654 userInfo: [NSLocalizedDescriptionKey: message] 655 )) 656 return 657 } 658 659 if signatureValidationResult.validationResult == .invalid { 660 let message = "Directive download was successful, but signature was incorrect" 661 self.logger.error(message: message, code: .unknown) 662 errorBlock(NSError( 663 domain: ErrorDomain, 664 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 665 userInfo: [NSLocalizedDescriptionKey: message] 666 )) 667 return 668 } 669 670 if signatureValidationResult.validationResult != .skipped { 671 if let expoProjectInformation = signatureValidationResult.expoProjectInformation { 672 let directive: UpdateDirective 673 do { 674 directive = try UpdateDirective.fromJSONData(responsePartInfo.body) 675 } catch { 676 let message = "Failed to parse directive: \(error.localizedDescription)" 677 self.logger.error(message: message, code: .unknown) 678 errorBlock(NSError( 679 domain: ErrorDomain, 680 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 681 userInfo: [NSLocalizedDescriptionKey: message] 682 )) 683 return 684 } 685 686 if expoProjectInformation.projectId != directive.signingInfo?.easProjectId || 687 expoProjectInformation.scopeKey != directive.signingInfo?.scopeKey { 688 let message = "Invalid certificate for directive project ID or scope key" 689 self.logger.error(message: message, code: .unknown) 690 errorBlock(NSError( 691 domain: ErrorDomain, 692 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 693 userInfo: [NSLocalizedDescriptionKey: message] 694 )) 695 return 696 } 697 } 698 logger.info(message: "Update directive code signature verified successfully") 699 } 700 } 701 702 let directive: UpdateDirective 703 do { 704 directive = try UpdateDirective.fromJSONData(responsePartInfo.body) 705 } catch { 706 let message = "Failed to parse directive: \(error.localizedDescription)" 707 self.logger.error(message: message, code: .unknown) 708 errorBlock(NSError( 709 domain: ErrorDomain, 710 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 711 userInfo: [NSLocalizedDescriptionKey: message] 712 )) 713 return 714 } 715 716 successBlock(DirectiveUpdateResponsePart(updateDirective: directive)) 717 } 718 719 private func parseManifestResponsePartInfo( 720 _ responsePartInfo: ResponsePartInfo, 721 extensions: [String: Any], 722 certificateChainFromManifestResponse: String?, 723 database: UpdatesDatabase, 724 successBlock: @escaping ParseManifestSuccessBlock, 725 errorBlock: @escaping ParseManifestErrorBlock 726 ) { 727 let headerSignature = responsePartInfo.responseHeaderData.manifestSignature 728 729 let updateResponseDictionary: [String: Any] 730 do { 731 let manifestBodyJson = try JSONSerialization.jsonObject(with: responsePartInfo.body) 732 updateResponseDictionary = try extractUpdateResponseDictionary(parsedJson: manifestBodyJson) 733 } catch { 734 errorBlock(error) 735 return 736 } 737 738 let bodyManifestString = updateResponseDictionary["manifestString"] 739 let bodySignature = updateResponseDictionary["signature"] 740 let isSignatureInBody = bodyManifestString != nil && bodySignature != nil 741 742 let signature = isSignatureInBody ? bodySignature : headerSignature 743 let manifestString = isSignatureInBody ? bodyManifestString : String(data: responsePartInfo.body, encoding: .utf8) 744 745 // XDL serves unsigned manifests with the `signature` key set to "UNSIGNED". 746 // We should treat these manifests as unsigned rather than signed with an invalid signature. 747 let isUnsignedFromXDL = signature as? String == "UNSIGNED" 748 749 guard let manifestString = manifestString as? String else { 750 let message = "manifestString should be a string" 751 logger.error(message: message, code: .unknown) 752 errorBlock(NSError( 753 domain: ErrorDomain, 754 code: FileDownloaderErrorCode.ManifestStringError.rawValue, 755 userInfo: [NSLocalizedDescriptionKey: message] 756 )) 757 return 758 } 759 760 guard let manifestStringData = manifestString.data(using: .utf8) else { 761 let message = "manifest should be a valid JSON object" 762 logger.error(message: message, code: .unknown) 763 errorBlock(NSError( 764 domain: ErrorDomain, 765 code: FileDownloaderErrorCode.ManifestJSONError.rawValue, 766 userInfo: [NSLocalizedDescriptionKey: message] 767 )) 768 return 769 } 770 771 var manifest: [String: Any]? 772 var manifestParseError: (any Error)? 773 do { 774 manifest = try JSONSerialization.jsonObject(with: manifestStringData) as? [String: Any] 775 } catch { 776 manifestParseError = error 777 } 778 779 guard let manifest = manifest, manifestParseError == nil else { 780 let message = "manifest should be a valid JSON object" 781 logger.error(message: message, code: .unknown) 782 errorBlock(NSError( 783 domain: ErrorDomain, 784 code: FileDownloaderErrorCode.ManifestJSONError.rawValue, 785 userInfo: [NSLocalizedDescriptionKey: message] 786 )) 787 return 788 } 789 790 if let signature = signature, !isUnsignedFromXDL { 791 guard let signature = signature as? String else { 792 let message = "signature should be a string" 793 logger.error(message: message, code: .unknown) 794 errorBlock(NSError( 795 domain: ErrorDomain, 796 code: FileDownloaderErrorCode.ManifestSignatureError.rawValue, 797 userInfo: [NSLocalizedDescriptionKey: message] 798 )) 799 return 800 } 801 802 Crypto.verifySignature( 803 withData: manifestString, 804 signature: signature, 805 config: config 806 ) { isValid in 807 guard isValid else { 808 let message = "Manifest verification failed" 809 self.logger.error(message: message, code: .unknown) 810 errorBlock(NSError( 811 domain: ErrorDomain, 812 code: FileDownloaderErrorCode.ManifestVerificationError.rawValue, 813 userInfo: [NSLocalizedDescriptionKey: message] 814 )) 815 return 816 } 817 818 self.createUpdate( 819 manifest: manifest, 820 responsePartInfo: responsePartInfo, 821 extensions: extensions, 822 certificateChainFromManifestResponse: certificateChainFromManifestResponse, 823 database: database, 824 isVerified: true, 825 successBlock: successBlock, 826 errorBlock: errorBlock 827 ) 828 } errorBlock: { error in 829 errorBlock(error) 830 } 831 } else { 832 createUpdate( 833 manifest: manifest, 834 responsePartInfo: responsePartInfo, 835 extensions: extensions, 836 certificateChainFromManifestResponse: certificateChainFromManifestResponse, 837 database: database, 838 isVerified: false, 839 successBlock: successBlock, 840 errorBlock: errorBlock 841 ) 842 } 843 } 844 845 private func extractUpdateResponseDictionary(parsedJson: Any) throws -> [String: Any] { 846 if let parsedJson = parsedJson as? [String: Any] { 847 return parsedJson 848 } else if let parsedJson = parsedJson as? [Any] { 849 // TODO: either add support for runtimeVersion or deprecate multi-manifests 850 for providedManifest in parsedJson { 851 if let providedManifest = providedManifest as? [String: Any], 852 let sdkVersion: String = providedManifest.optionalValue(forKey: "sdkVersion"), 853 let supportedSdkVersions = config.sdkVersion?.components(separatedBy: ","), 854 supportedSdkVersions.contains(sdkVersion) { 855 return providedManifest 856 } 857 } 858 } 859 860 throw NSError( 861 domain: ErrorDomain, 862 code: FileDownloaderErrorCode.NoCompatibleUpdateError.rawValue, 863 userInfo: [ 864 NSLocalizedDescriptionKey: String( 865 format: "No compatible update found at %@. Only %@ are supported.", 866 config.updateUrl?.absoluteString ?? "(missing config updateUrl)", 867 config.sdkVersion ?? "(missing sdkVersion field)" 868 ) 869 ] 870 ) 871 } 872 873 private func createUpdate( 874 manifest: [String: Any], 875 responsePartInfo: ResponsePartInfo, 876 extensions: [String: Any], 877 certificateChainFromManifestResponse: String?, 878 database: UpdatesDatabase, 879 isVerified: Bool, 880 successBlock: ParseManifestSuccessBlock, 881 errorBlock: ParseManifestErrorBlock 882 ) { 883 var mutableManifest = manifest 884 if config.expectsSignedManifest { 885 // 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. 886 mutableManifest["isVerified"] = isVerified 887 } 888 889 // check code signing if code signing is configured 890 // 1. verify the code signing signature (throw if invalid) 891 // 2. then, if the code signing certificate is only valid for a particular project, verify that the manifest 892 // has the correct info for code signing. If the code signing certificate doesn't specify a particular 893 // project, it is assumed to be valid for all projects 894 // 3. mark the manifest as verified if both of these pass 895 if let codeSigningConfiguration = config.codeSigningConfiguration { 896 let signatureValidationResult: SignatureValidationResult 897 do { 898 signatureValidationResult = try codeSigningConfiguration.validateSignature( 899 signature: responsePartInfo.responsePartHeaderData.signature, 900 signedData: responsePartInfo.body, 901 manifestResponseCertificateChain: certificateChainFromManifestResponse 902 ) 903 } catch { 904 let codeSigningError = error as? CodeSigningError 905 let message = codeSigningError?.message() ?? error.localizedDescription 906 self.logger.error(message: message, code: .unknown) 907 errorBlock(NSError( 908 domain: ErrorDomain, 909 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 910 userInfo: [NSLocalizedDescriptionKey: message] 911 )) 912 return 913 } 914 915 if signatureValidationResult.validationResult == .invalid { 916 let message = "Manifest download was successful, but signature was incorrect" 917 self.logger.error(message: message, code: .unknown) 918 errorBlock(NSError( 919 domain: ErrorDomain, 920 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 921 userInfo: [NSLocalizedDescriptionKey: message] 922 )) 923 return 924 } 925 926 if signatureValidationResult.validationResult != .skipped { 927 if let expoProjectInformation = signatureValidationResult.expoProjectInformation { 928 let update: Update 929 do { 930 update = try Update.update( 931 withManifest: mutableManifest, 932 responseHeaderData: responsePartInfo.responseHeaderData, 933 extensions: extensions, 934 config: config, 935 database: database 936 ) 937 } catch { 938 // Catch any assertions related to parsing the manifest JSON, 939 // this will ensure invalid manifests can be easily debugged. 940 // For example, this will catch nullish sdkVersion assertions. 941 let message = "Failed to parse manifest: \(error.localizedDescription)" 942 self.logger.error(message: message, code: .unknown) 943 errorBlock(NSError( 944 domain: ErrorDomain, 945 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 946 userInfo: [NSLocalizedDescriptionKey: message] 947 )) 948 return 949 } 950 951 let manifestForProjectInformation = update.manifest 952 if expoProjectInformation.projectId != manifestForProjectInformation.easProjectId() || 953 expoProjectInformation.scopeKey != manifestForProjectInformation.scopeKey() { 954 let message = "Invalid certificate for manifest project ID or scope key" 955 self.logger.error(message: message, code: .unknown) 956 errorBlock(NSError( 957 domain: ErrorDomain, 958 code: FileDownloaderErrorCode.CodeSigningSignatureError.rawValue, 959 userInfo: [NSLocalizedDescriptionKey: message] 960 )) 961 return 962 } 963 } 964 logger.info(message: "Update code signature verified successfully") 965 mutableManifest["isVerified"] = true 966 } 967 } 968 969 let update: Update 970 do { 971 update = try Update.update( 972 withManifest: mutableManifest, 973 responseHeaderData: responsePartInfo.responseHeaderData, 974 extensions: extensions, 975 config: config, 976 database: database 977 ) 978 } catch { 979 // Catch any assertions related to parsing the manifest JSON, 980 // this will ensure invalid manifests can be easily debugged. 981 // For example, this will catch nullish sdkVersion assertions. 982 let message = "Failed to parse manifest: \(error.localizedDescription)" 983 self.logger.error(message: message, code: .unknown) 984 errorBlock(NSError( 985 domain: ErrorDomain, 986 code: FileDownloaderErrorCode.ManifestParseError.rawValue, 987 userInfo: [NSLocalizedDescriptionKey: message] 988 )) 989 return 990 } 991 992 if !SelectionPolicies.doesUpdate(update, matchFilters: responsePartInfo.responseHeaderData.manifestFilters) { 993 let message = "Downloaded manifest is invalid; provides filters that do not match its content" 994 self.logger.error(message: message, code: .unknown) 995 errorBlock(NSError( 996 domain: ErrorDomain, 997 code: FileDownloaderErrorCode.MismatchedManifestFiltersError.rawValue, 998 userInfo: [NSLocalizedDescriptionKey: message] 999 )) 1000 return 1001 } 1002 1003 successBlock(ManifestUpdateResponsePart(updateManifest: update)) 1004 } 1005 1006 private func downloadData( 1007 withRequest request: URLRequest, 1008 successBlock: @escaping SuccessBlock, 1009 errorBlock: @escaping ErrorBlock 1010 ) { 1011 let task = session.dataTask(with: request) { data, response, error in 1012 guard let response = response else { 1013 // error is non-nil when data and response are both nil 1014 // swiftlint:disable:next force_unwrapping 1015 let error = error! 1016 self.logger.error(message: error.localizedDescription, code: .unknown) 1017 errorBlock(error) 1018 return 1019 } 1020 1021 if let httpResponse = response as? HTTPURLResponse, 1022 httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 { 1023 let encoding = FileDownloader.encoding(fromResponse: httpResponse) 1024 let body = data.let { it in 1025 String(data: it, encoding: encoding) 1026 } ?? "Unknown body response" 1027 let error = FileDownloader.error(fromResponse: httpResponse, body: body) 1028 self.logger.error(message: error.localizedDescription, code: .unknown) 1029 errorBlock(error) 1030 return 1031 } 1032 1033 successBlock(data, response) 1034 } 1035 task.resume() 1036 } 1037 1038 private static func encoding(fromResponse response: URLResponse) -> String.Encoding { 1039 if let textEncodingName = response.textEncodingName { 1040 let cfEncoding = CFStringConvertIANACharSetNameToEncoding(textEncodingName as CFString) 1041 return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(cfEncoding)) 1042 } 1043 // Default to UTF-8 1044 return .utf8 1045 } 1046 1047 private static func error(fromResponse response: HTTPURLResponse, body: String) -> NSError { 1048 return NSError( 1049 domain: ErrorDomain, 1050 code: response.statusCode, 1051 userInfo: [NSLocalizedDescriptionKey: body] 1052 ) 1053 } 1054 1055 // MARK: - NSURLSessionTaskDelegate 1056 1057 func urlSession( 1058 _ session: URLSession, 1059 task: URLSessionTask, 1060 willPerformHTTPRedirection response: HTTPURLResponse, 1061 newRequest request: URLRequest, 1062 completionHandler: @escaping (URLRequest?) -> Void 1063 ) { 1064 completionHandler(request) 1065 } 1066 1067 // MARK: - URLSessionDataDelegate 1068 1069 func urlSession( 1070 _ session: URLSession, 1071 dataTask: URLSessionDataTask, 1072 willCacheResponse proposedResponse: CachedURLResponse, 1073 completionHandler: @escaping (CachedURLResponse?) -> Void 1074 ) { 1075 completionHandler(proposedResponse) 1076 } 1077 } 1078