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