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