<lambda>null1 package expo.modules.updates.loader
2 
3 import android.content.Context
4 import expo.modules.jsonutils.require
5 import expo.modules.updates.UpdatesConfiguration
6 import expo.modules.updates.UpdatesUtils
7 import expo.modules.updates.db.entity.AssetEntity
8 import expo.modules.structuredheaders.Dictionary
9 import expo.modules.updates.launcher.NoDatabaseLauncher
10 import expo.modules.updates.selectionpolicy.SelectionPolicies
11 import okhttp3.*
12 import okhttp3.brotli.BrotliInterceptor
13 import org.json.JSONArray
14 import org.json.JSONException
15 import org.json.JSONObject
16 import java.io.File
17 import java.io.IOException
18 import java.util.*
19 import kotlin.math.min
20 import org.apache.commons.fileupload.MultipartStream
21 import org.apache.commons.fileupload.ParameterParser
22 import java.io.ByteArrayOutputStream
23 import expo.modules.easclient.EASClientID
24 import okhttp3.Headers.Companion.toHeaders
25 import expo.modules.jsonutils.getNullable
26 import expo.modules.structuredheaders.StringItem
27 import expo.modules.updates.codesigning.ValidationResult
28 import expo.modules.updates.db.UpdatesDatabase
29 import expo.modules.updates.db.entity.UpdateEntity
30 import expo.modules.updates.logging.UpdatesErrorCode
31 import expo.modules.updates.logging.UpdatesLogger
32 import expo.modules.updates.manifest.*
33 import java.security.cert.CertificateException
34 
35 /**
36  * Utility class that holds all the logic for downloading data and files, such as update manifests
37  * and assets, using an instance of [OkHttpClient].
38  */
39 open class FileDownloader(context: Context, private val client: OkHttpClient) {
40   constructor(context: Context) : this(
41     context,
42     OkHttpClient.Builder()
43       .cache(getCache(context))
44       .addInterceptor(BrotliInterceptor)
45       .build()
46   )
47   private val logger = UpdatesLogger(context)
48 
49   interface FileDownloadCallback {
50     fun onFailure(e: Exception)
51     fun onSuccess(file: File, hash: ByteArray)
52   }
53 
54   interface RemoteUpdateDownloadCallback {
55     fun onFailure(message: String, e: Exception)
56     fun onSuccess(updateResponse: UpdateResponse)
57   }
58 
59   interface AssetDownloadCallback {
60     fun onFailure(e: Exception, assetEntity: AssetEntity)
61     fun onSuccess(assetEntity: AssetEntity, isNew: Boolean)
62   }
63 
64   private fun downloadFileAndVerifyHashAndWriteToPath(
65     request: Request,
66     expectedBase64URLEncodedSHA256Hash: String?,
67     destination: File,
68     callback: FileDownloadCallback
69   ) {
70     downloadData(
71       request,
72       object : Callback {
73         override fun onFailure(call: Call, e: IOException) {
74           callback.onFailure(e)
75         }
76 
77         @Throws(IOException::class)
78         override fun onResponse(call: Call, response: Response) {
79           if (!response.isSuccessful) {
80             callback.onFailure(
81               Exception(
82                 "Network request failed: " + response.body!!
83                   .string()
84               )
85             )
86             return
87           }
88           try {
89             response.body!!.byteStream().use { inputStream ->
90               val hash = UpdatesUtils.verifySHA256AndWriteToFile(inputStream, destination, expectedBase64URLEncodedSHA256Hash)
91               callback.onSuccess(destination, hash)
92             }
93           } catch (e: Exception) {
94             logger.error("Failed to download file to destination $destination: ${e.localizedMessage}", UpdatesErrorCode.AssetsFailedToLoad, e)
95             callback.onFailure(e)
96           }
97         }
98       }
99     )
100   }
101 
102   internal fun parseRemoteUpdateResponse(response: Response, configuration: UpdatesConfiguration, callback: RemoteUpdateDownloadCallback) {
103     val responseHeaders = response.headers
104     val responseHeaderData = ResponseHeaderData(
105       protocolVersionRaw = responseHeaders["expo-protocol-version"],
106       manifestFiltersRaw = responseHeaders["expo-manifest-filters"],
107       serverDefinedHeadersRaw = responseHeaders["expo-server-defined-headers"],
108       manifestSignature = responseHeaders["expo-manifest-signature"],
109     )
110     val responseBody = response.body
111 
112     if (response.code == 204 || responseBody == null) {
113       // If the protocol version greater than 0, we support returning a 204 and no body to mean no-op.
114       // A 204 has no content-type.
115       if (responseHeaderData.protocolVersion != null && responseHeaderData.protocolVersion > 0) {
116         callback.onSuccess(
117           UpdateResponse(
118             responseHeaderData = responseHeaderData,
119             manifestUpdateResponsePart = null,
120             directiveUpdateResponsePart = null
121           )
122         )
123         return
124       }
125 
126       val message = "Missing body in remote update"
127       logger.error(message, UpdatesErrorCode.UpdateFailedToLoad)
128       callback.onFailure(
129         message,
130         IOException(message)
131       )
132       return
133     }
134 
135     val contentType = response.header("content-type") ?: ""
136     val isMultipart = contentType.startsWith("multipart/", ignoreCase = true)
137     if (isMultipart) {
138       val boundaryParameter = ParameterParser().parse(contentType, ';')["boundary"]
139       if (boundaryParameter == null) {
140         val message = "Missing boundary in multipart remote update content-type"
141         logger.error(message, UpdatesErrorCode.UpdateFailedToLoad)
142         callback.onFailure(
143           message,
144           IOException(message)
145         )
146         return
147       }
148 
149       parseMultipartRemoteUpdateResponse(responseBody, responseHeaderData, boundaryParameter, configuration, callback)
150     } else {
151       val manifestResponseInfo = ResponsePartInfo(
152         responseHeaderData = responseHeaderData,
153         responsePartHeaderData = ResponsePartHeaderData(
154           signature = responseHeaders["expo-signature"]
155         ),
156         body = response.body!!.string()
157       )
158 
159       parseManifest(
160         manifestResponseInfo,
161         null,
162         null,
163         configuration,
164         object : ParseManifestCallback {
165           override fun onFailure(message: String, e: Exception) {
166             callback.onFailure(message, e)
167           }
168 
169           override fun onSuccess(manifestUpdateResponsePart: UpdateResponsePart.ManifestUpdateResponsePart) {
170             callback.onSuccess(
171               UpdateResponse(
172                 responseHeaderData = responseHeaderData,
173                 manifestUpdateResponsePart = manifestUpdateResponsePart,
174                 directiveUpdateResponsePart = null
175               )
176             )
177           }
178         }
179       )
180     }
181   }
182 
183   private fun parseHeaders(text: String): Headers {
184     val headers = mutableMapOf<String, String>()
185     val lines = text.split(CRLF)
186     for (line in lines) {
187       val indexOfSeparator = line.indexOf(":")
188       if (indexOfSeparator == -1) {
189         continue
190       }
191       val key = line.substring(0, indexOfSeparator).trim()
192       val value = line.substring(indexOfSeparator + 1).trim()
193       headers[key] = value
194     }
195     return headers.toHeaders()
196   }
197 
198   private fun parseMultipartRemoteUpdateResponse(responseBody: ResponseBody, responseHeaderData: ResponseHeaderData, boundary: String, configuration: UpdatesConfiguration, callback: RemoteUpdateDownloadCallback) {
199     var manifestPartBodyAndHeaders: Pair<String, Headers>? = null
200     var extensionsBody: String? = null
201     var certificateChainString: String? = null
202     var directivePartBodyAndHeaders: Pair<String, Headers>? = null
203 
204     val multipartStream = MultipartStream(responseBody.byteStream(), boundary.toByteArray())
205 
206     try {
207       var nextPart = multipartStream.skipPreamble()
208       while (nextPart) {
209         val headers = parseHeaders(multipartStream.readHeaders())
210 
211         // always read the body to progress the reader
212         val output = ByteArrayOutputStream()
213         multipartStream.readBodyData(output)
214 
215         val contentDispositionValue = headers["content-disposition"]
216         if (contentDispositionValue != null) {
217           val contentDispositionParameterMap = ParameterParser().parse(contentDispositionValue, ';')
218           val contentDispositionName = contentDispositionParameterMap["name"]
219           if (contentDispositionName != null) {
220             when (contentDispositionName) {
221               "manifest" -> manifestPartBodyAndHeaders = Pair(output.toString(), headers)
222               "extensions" -> extensionsBody = output.toString()
223               "certificate_chain" -> certificateChainString = output.toString()
224               "directive" -> directivePartBodyAndHeaders = Pair(output.toString(), headers)
225             }
226           }
227         }
228         nextPart = multipartStream.readBoundary()
229       }
230     } catch (e: Exception) {
231       val message = "Error while reading multipart remote update response"
232       logger.error(message, UpdatesErrorCode.UpdateFailedToLoad, e)
233       callback.onFailure(
234         message,
235         e
236       )
237       return
238     }
239 
240     val extensions = try {
241       extensionsBody?.let { JSONObject(it) }
242     } catch (e: Exception) {
243       val message = "Failed to parse multipart remote update extensions"
244       logger.error(message, UpdatesErrorCode.UpdateFailedToLoad)
245       callback.onFailure(
246         message,
247         e
248       )
249       return
250     }
251 
252     // in v0 compatibility mode require a manifest
253     if (configuration.enableExpoUpdatesProtocolV0CompatibilityMode && manifestPartBodyAndHeaders == null) {
254       val 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."
255       logger.error(message, UpdatesErrorCode.UpdateFailedToLoad)
256       callback.onFailure(message, IOException(message))
257       return
258     }
259 
260     val manifestResponseInfo = manifestPartBodyAndHeaders?.let {
261       ResponsePartInfo(
262         responseHeaderData = responseHeaderData,
263         responsePartHeaderData = ResponsePartHeaderData(
264           signature = manifestPartBodyAndHeaders.second["expo-signature"]
265         ),
266         body = manifestPartBodyAndHeaders.first
267       )
268     }
269 
270     // in v0 compatibility mode ignore directives
271     val directiveResponseInfo = if (configuration.enableExpoUpdatesProtocolV0CompatibilityMode) {
272       null
273     } else {
274       directivePartBodyAndHeaders?.let {
275         ResponsePartInfo(
276           responseHeaderData = responseHeaderData,
277           responsePartHeaderData = ResponsePartHeaderData(
278             signature = directivePartBodyAndHeaders.second["expo-signature"]
279           ),
280           body = directivePartBodyAndHeaders.first
281         )
282       }
283     }
284 
285     var parseManifestResponse: UpdateResponsePart.ManifestUpdateResponsePart? = null
286     var parseDirectiveResponse: UpdateResponsePart.DirectiveUpdateResponsePart? = null
287     var didError = false
288 
289     // need to parse the directive and manifest in parallel, to do so use this common callback.
290     // would be a great place to have better coroutine stuff
291     val maybeFinish = {
292       if (!didError) {
293         val isManifestDone = manifestResponseInfo == null || parseManifestResponse != null
294         val isDirectiveDone = directiveResponseInfo == null || parseDirectiveResponse != null
295 
296         if (isManifestDone && isDirectiveDone) {
297           callback.onSuccess(
298             UpdateResponse(
299               responseHeaderData = responseHeaderData,
300               manifestUpdateResponsePart = parseManifestResponse,
301               directiveUpdateResponsePart = parseDirectiveResponse
302             )
303           )
304         }
305       }
306     }
307 
308     if (directiveResponseInfo != null) {
309       parseDirective(
310         directiveResponseInfo,
311         certificateChainString,
312         configuration,
313         object : ParseDirectiveCallback {
314           override fun onFailure(message: String, e: Exception) {
315             if (!didError) {
316               didError = true
317               callback.onFailure(message, e)
318             }
319           }
320 
321           override fun onSuccess(directiveUpdateResponsePart: UpdateResponsePart.DirectiveUpdateResponsePart) {
322             parseDirectiveResponse = directiveUpdateResponsePart
323             maybeFinish()
324           }
325         }
326       )
327     }
328 
329     if (manifestResponseInfo != null) {
330       parseManifest(
331         manifestResponseInfo,
332         extensions,
333         certificateChainString,
334         configuration,
335         object : ParseManifestCallback {
336           override fun onFailure(message: String, e: Exception) {
337             if (!didError) {
338               didError = true
339               callback.onFailure(message, e)
340             }
341           }
342           override fun onSuccess(manifestUpdateResponsePart: UpdateResponsePart.ManifestUpdateResponsePart) {
343             parseManifestResponse = manifestUpdateResponsePart
344             maybeFinish()
345           }
346         }
347       )
348     }
349 
350     // if both parts are empty, we still want to finish
351     if (manifestResponseInfo == null && directiveResponseInfo == null) {
352       maybeFinish()
353     }
354   }
355 
356   interface ParseDirectiveCallback {
357     fun onFailure(message: String, e: Exception)
358     fun onSuccess(directiveUpdateResponsePart: UpdateResponsePart.DirectiveUpdateResponsePart)
359   }
360 
361   private fun parseDirective(
362     directiveResponsePartInfo: ResponsePartInfo,
363     certificateChainFromManifestResponse: String?,
364     configuration: UpdatesConfiguration,
365     callback: ParseDirectiveCallback
366   ) {
367     try {
368       val body = directiveResponsePartInfo.body
369 
370       // check code signing if code signing is configured
371       // 1. verify the code signing signature (throw if invalid)
372       // 2. then, if the code signing certificate is only valid for a particular project, verify that the directive
373       //    has the correct info for code signing. If the code signing certificate doesn't specify a particular
374       //    project, it is assumed to be valid for all projects
375       // 3. consider the directive verified if both of these pass
376       try {
377         configuration.codeSigningConfiguration?.let { codeSigningConfiguration ->
378           val signatureValidationResult = codeSigningConfiguration.validateSignature(
379             directiveResponsePartInfo.responsePartHeaderData.signature,
380             body.toByteArray(),
381             certificateChainFromManifestResponse,
382           )
383           if (signatureValidationResult.validationResult == ValidationResult.INVALID) {
384             throw IOException("Directive download was successful, but signature was incorrect")
385           }
386 
387           if (signatureValidationResult.validationResult != ValidationResult.SKIPPED) {
388             val directiveForProjectInformation = UpdateDirective.fromJSONString(body)
389             signatureValidationResult.expoProjectInformation?.let { expoProjectInformation ->
390               if (expoProjectInformation.projectId != directiveForProjectInformation.signingInfo?.easProjectId ||
391                 expoProjectInformation.scopeKey != directiveForProjectInformation.signingInfo.scopeKey
392               ) {
393                 throw CertificateException("Invalid certificate for directive project ID or scope key")
394               }
395             }
396           }
397         }
398       } catch (e: Exception) {
399         callback.onFailure(e.message!!, e)
400         return
401       }
402 
403       callback.onSuccess(UpdateResponsePart.DirectiveUpdateResponsePart(UpdateDirective.fromJSONString(body)))
404     } catch (e: Exception) {
405       val message = "Failed to parse directive data: ${e.localizedMessage}"
406       logger.error(message, UpdatesErrorCode.UpdateFailedToLoad, e)
407       callback.onFailure(
408         message,
409         e
410       )
411     }
412   }
413 
414   interface ParseManifestCallback {
415     fun onFailure(message: String, e: Exception)
416     fun onSuccess(manifestUpdateResponsePart: UpdateResponsePart.ManifestUpdateResponsePart)
417   }
418 
419   private fun parseManifest(
420     manifestResponseInfo: ResponsePartInfo,
421     extensions: JSONObject?,
422     certificateChainFromManifestResponse: String?,
423     configuration: UpdatesConfiguration,
424     callback: ParseManifestCallback
425   ) {
426     try {
427       val updateResponseJson = extractUpdateResponseJson(manifestResponseInfo.body, configuration)
428       val isSignatureInBody =
429         updateResponseJson.has("manifestString") && updateResponseJson.has("signature")
430       val signature = if (isSignatureInBody) {
431         updateResponseJson.getNullable("signature")
432       } else {
433         manifestResponseInfo.responseHeaderData.manifestSignature
434       }
435 
436       /**
437        * The updateResponseJson is just the manifest when it is unsigned, or the signature is sent as a header.
438        * If the signature is in the body, the updateResponseJson looks like:
439        * {
440        *   manifestString: string;
441        *   signature: string;
442        * }
443        */
444       val manifestString = if (isSignatureInBody) {
445         updateResponseJson.getString("manifestString")
446       } else {
447         manifestResponseInfo.body
448       }
449       val preManifest = JSONObject(manifestString)
450 
451       // XDL serves unsigned manifests with the `signature` key set to "UNSIGNED".
452       // We should treat these manifests as unsigned rather than signed with an invalid signature.
453       val isUnsignedFromXDL = "UNSIGNED" == signature
454       if (signature != null && !isUnsignedFromXDL) {
455         verifyExpoPublicRSASignature(
456           this@FileDownloader,
457           manifestString,
458           signature,
459           object : RSASignatureListener {
460             override fun onError(exception: Exception, isNetworkError: Boolean) {
461               callback.onFailure("Could not validate signed manifest", exception)
462             }
463 
464             override fun onCompleted(isValid: Boolean) {
465               if (isValid) {
466                 try {
467                   checkCodeSigningAndCreateManifest(
468                     bodyString = manifestResponseInfo.body,
469                     preManifest = preManifest,
470                     responseHeaderData = manifestResponseInfo.responseHeaderData,
471                     responsePartHeaderData = manifestResponseInfo.responsePartHeaderData,
472                     extensions = extensions,
473                     certificateChainFromManifestResponse = certificateChainFromManifestResponse,
474                     isVerified = true,
475                     configuration = configuration,
476                     logger = logger,
477                     callback = callback
478                   )
479                 } catch (e: Exception) {
480                   callback.onFailure("Failed to parse manifest data", e)
481                 }
482               } else {
483                 val message = "Manifest signature is invalid; aborting"
484                 logger.error(message, UpdatesErrorCode.UpdateHasInvalidSignature)
485                 callback.onFailure(
486                   message,
487                   Exception("Manifest signature is invalid")
488                 )
489               }
490             }
491           }
492         )
493       } else {
494         checkCodeSigningAndCreateManifest(
495           bodyString = manifestResponseInfo.body,
496           preManifest = preManifest,
497           responseHeaderData = manifestResponseInfo.responseHeaderData,
498           responsePartHeaderData = manifestResponseInfo.responsePartHeaderData,
499           extensions = extensions,
500           certificateChainFromManifestResponse = certificateChainFromManifestResponse,
501           isVerified = false,
502           configuration = configuration,
503           logger = logger,
504           callback = callback
505         )
506       }
507     } catch (e: Exception) {
508       val message = "Failed to parse manifest data: ${e.localizedMessage}"
509       logger.error(message, UpdatesErrorCode.UpdateFailedToLoad, e)
510       callback.onFailure(
511         message,
512         e
513       )
514     }
515   }
516 
517   fun downloadRemoteUpdate(
518     configuration: UpdatesConfiguration,
519     extraHeaders: JSONObject?,
520     context: Context,
521     callback: RemoteUpdateDownloadCallback
522   ) {
523     try {
524       downloadData(
525         createRequestForRemoteUpdate(configuration, extraHeaders, context),
526         object : Callback {
527           override fun onFailure(call: Call, e: IOException) {
528             val message = "Failed to download remote update from URL: ${configuration.updateUrl}: ${e.localizedMessage}"
529             logger.error(message, UpdatesErrorCode.UpdateFailedToLoad, e)
530             callback.onFailure(
531               message,
532               e
533             )
534           }
535 
536           @Throws(IOException::class)
537           override fun onResponse(call: Call, response: Response) {
538             if (!response.isSuccessful) {
539               val message = "Failed to download remote update from URL: ${configuration.updateUrl}"
540               logger.error(message, UpdatesErrorCode.UpdateFailedToLoad)
541               callback.onFailure(
542                 message,
543                 Exception(response.body!!.string())
544               )
545               return
546             }
547 
548             parseRemoteUpdateResponse(response, configuration, callback)
549           }
550         }
551       )
552     } catch (e: Exception) {
553       val message = "Failed to download remote update from URL: ${configuration.updateUrl}: ${e.localizedMessage}"
554       logger.error(message, UpdatesErrorCode.UpdateFailedToLoad, e)
555       callback.onFailure(
556         message,
557         e
558       )
559     }
560   }
561 
562   fun downloadAsset(
563     asset: AssetEntity,
564     destinationDirectory: File?,
565     configuration: UpdatesConfiguration,
566     context: Context,
567     callback: AssetDownloadCallback
568   ) {
569     if (asset.url == null) {
570       val message = "Could not download asset " + asset.key + " with no URL"
571       logger.error(message, UpdatesErrorCode.AssetsFailedToLoad)
572       callback.onFailure(Exception(message), asset)
573       return
574     }
575     val filename = UpdatesUtils.createFilenameForAsset(asset)
576     val path = File(destinationDirectory, filename)
577     if (path.exists()) {
578       asset.relativePath = filename
579       callback.onSuccess(asset, false)
580     } else {
581       try {
582         downloadFileAndVerifyHashAndWriteToPath(
583           createRequestForAsset(asset, configuration, context),
584           asset.expectedHash,
585           path,
586           object : FileDownloadCallback {
587             override fun onFailure(e: Exception) {
588               callback.onFailure(e, asset)
589             }
590 
591             override fun onSuccess(file: File, hash: ByteArray) {
592               asset.downloadTime = Date()
593               asset.relativePath = filename
594               asset.hash = hash
595               callback.onSuccess(asset, true)
596             }
597           }
598         )
599       } catch (e: Exception) {
600         logger.error("Failed to download asset ${asset.key}: ${e.localizedMessage}", UpdatesErrorCode.AssetsFailedToLoad, e)
601         callback.onFailure(e, asset)
602       }
603     }
604   }
605 
606   fun downloadData(request: Request, callback: Callback) {
607     downloadData(request, callback, false)
608   }
609 
610   private fun downloadData(request: Request, callback: Callback, isRetry: Boolean) {
611     client.newCall(request).enqueue(object : Callback {
612       override fun onFailure(call: Call, e: IOException) {
613         if (isRetry) {
614           callback.onFailure(call, e)
615         } else {
616           downloadData(request, callback, true)
617         }
618       }
619 
620       @Throws(IOException::class)
621       override fun onResponse(call: Call, response: Response) {
622         callback.onResponse(call, response)
623       }
624     })
625   }
626 
627   companion object {
628     private val TAG = FileDownloader::class.java.simpleName
629 
630     // Standard line separator for HTTP.
631     private const val CRLF = "\r\n"
632 
633     @Throws(Exception::class)
634     private fun checkCodeSigningAndCreateManifest(
635       bodyString: String,
636       preManifest: JSONObject,
637       responseHeaderData: ResponseHeaderData,
638       responsePartHeaderData: ResponsePartHeaderData,
639       extensions: JSONObject?,
640       certificateChainFromManifestResponse: String?,
641       isVerified: Boolean,
642       configuration: UpdatesConfiguration,
643       logger: UpdatesLogger,
644       callback: ParseManifestCallback
645     ) {
646       if (configuration.expectsSignedManifest) {
647         preManifest.put("isVerified", isVerified)
648       }
649 
650       // check code signing if code signing is configured
651       // 1. verify the code signing signature (throw if invalid)
652       // 2. then, if the code signing certificate is only valid for a particular project, verify that the manifest
653       //    has the correct info for code signing. If the code signing certificate doesn't specify a particular
654       //    project, it is assumed to be valid for all projects
655       // 3. mark the manifest as verified if both of these pass
656       try {
657         configuration.codeSigningConfiguration?.let { codeSigningConfiguration ->
658           val signatureValidationResult = codeSigningConfiguration.validateSignature(
659             responsePartHeaderData.signature,
660             bodyString.toByteArray(),
661             certificateChainFromManifestResponse,
662           )
663           if (signatureValidationResult.validationResult == ValidationResult.INVALID) {
664             throw IOException("Manifest download was successful, but signature was incorrect")
665           }
666 
667           if (signatureValidationResult.validationResult != ValidationResult.SKIPPED) {
668             val manifestForProjectInformation = ManifestFactory.getManifest(
669               preManifest,
670               responseHeaderData,
671               extensions,
672               configuration
673             ).manifest
674             signatureValidationResult.expoProjectInformation?.let { expoProjectInformation ->
675               if (expoProjectInformation.projectId != manifestForProjectInformation.getEASProjectID() ||
676                 expoProjectInformation.scopeKey != manifestForProjectInformation.getScopeKey()
677               ) {
678                 throw CertificateException("Invalid certificate for manifest project ID or scope key")
679               }
680             }
681 
682             logger.info("Update code signature verified successfully")
683             preManifest.put("isVerified", true)
684           }
685         }
686       } catch (e: Exception) {
687         logger.error(e.message!!, UpdatesErrorCode.UpdateCodeSigningError)
688         callback.onFailure(e.message!!, e)
689         return
690       }
691 
692       val updateManifest = ManifestFactory.getManifest(preManifest, responseHeaderData, extensions, configuration)
693       if (!SelectionPolicies.matchesFilters(updateManifest.updateEntity!!, responseHeaderData.manifestFilters)) {
694         val message =
695           "Downloaded manifest is invalid; provides filters that do not match its content"
696         callback.onFailure(message, Exception(message))
697       } else {
698         callback.onSuccess(UpdateResponsePart.ManifestUpdateResponsePart(updateManifest))
699       }
700     }
701 
702     @Throws(IOException::class)
703     private fun extractUpdateResponseJson(
704       manifestString: String,
705       configuration: UpdatesConfiguration
706     ): JSONObject {
707       try {
708         return JSONObject(manifestString)
709       } catch (e: JSONException) {
710         // Ignore this error, try to parse manifest as array
711       }
712 
713       // TODO: either add support for runtimeVersion or deprecate multi-manifests
714       try {
715         // the manifestString could be an array of manifest objects
716         // in this case, we choose the first compatible manifest in the array
717         val manifestArray = JSONArray(manifestString)
718         for (i in 0 until manifestArray.length()) {
719           val manifestCandidate = manifestArray.getJSONObject(i)
720           val sdkVersion = manifestCandidate.getString("sdkVersion")
721           if (configuration.sdkVersion != null && configuration.sdkVersion.split(",").contains(sdkVersion)
722           ) {
723             return manifestCandidate
724           }
725         }
726       } catch (e: JSONException) {
727         throw IOException(
728           "Manifest string is not a valid JSONObject or JSONArray: $manifestString",
729           e
730         )
731       }
732       throw IOException("No compatible manifest found. SDK Versions supported: " + configuration.sdkVersion + " Provided manifestString: " + manifestString)
733     }
734 
735     private fun Request.Builder.addHeadersFromJSONObject(headers: JSONObject?): Request.Builder {
736       if (headers == null) {
737         return this
738       }
739 
740       headers.keys().asSequence().forEach { key ->
741         header(key, headers.require<Any>(key).toString())
742       }
743       return this
744     }
745 
746     internal fun createRequestForAsset(
747       assetEntity: AssetEntity,
748       configuration: UpdatesConfiguration,
749       context: Context,
750     ): Request {
751       return Request.Builder()
752         .url(assetEntity.url!!.toString())
753         .addHeadersFromJSONObject(assetEntity.extraRequestHeaders)
754         .header("Expo-Platform", "android")
755         .header("Expo-Protocol-Version", "1")
756         .header("Expo-API-Version", "1")
757         .header("Expo-Updates-Environment", "BARE")
758         .header("EAS-Client-ID", EASClientID(context).uuid.toString())
759         .apply {
760           for ((key, value) in configuration.requestHeaders) {
761             header(key, value)
762           }
763         }
764         .build()
765     }
766 
767     internal fun createRequestForRemoteUpdate(
768       configuration: UpdatesConfiguration,
769       extraHeaders: JSONObject?,
770       context: Context
771     ): Request {
772       return Request.Builder()
773         .url(configuration.updateUrl.toString())
774         .addHeadersFromJSONObject(extraHeaders)
775         .header("Accept", "multipart/mixed,application/expo+json,application/json")
776         .header("Expo-Platform", "android")
777         .header("Expo-Protocol-Version", "1")
778         .header("Expo-API-Version", "1")
779         .header("Expo-Updates-Environment", "BARE")
780         .header("Expo-JSON-Error", "true")
781         .header("Expo-Accept-Signature", configuration.expectsSignedManifest.toString())
782         .header("EAS-Client-ID", EASClientID(context).uuid.toString())
783         .apply {
784           val runtimeVersion = configuration.runtimeVersion
785           val sdkVersion = configuration.sdkVersion
786           if (runtimeVersion != null && runtimeVersion.isNotEmpty()) {
787             header("Expo-Runtime-Version", runtimeVersion)
788           } else if (sdkVersion != null && sdkVersion.isNotEmpty()) {
789             header("Expo-SDK-Version", sdkVersion)
790           }
791         }
792         .header("Expo-Release-Channel", configuration.releaseChannel)
793         .apply {
794           val previousFatalError = NoDatabaseLauncher.consumeErrorLog(context)
795           if (previousFatalError != null) {
796             // some servers can have max length restrictions for headers,
797             // so we restrict the length of the string to 1024 characters --
798             // this should satisfy the requirements of most servers
799             header(
800               "Expo-Fatal-Error",
801               previousFatalError.substring(0, min(1024, previousFatalError.length))
802             )
803           }
804         }
805         .apply {
806           for ((key, value) in configuration.requestHeaders) {
807             header(key, value)
808           }
809         }
810         .apply {
811           configuration.codeSigningConfiguration?.let {
812             header("expo-expect-signature", it.getAcceptSignatureHeader())
813           }
814         }
815         .build()
816     }
817 
818     private fun getCache(context: Context): Cache {
819       val cacheSize = 50 * 1024 * 1024 // 50 MiB
820       return Cache(getCacheDirectory(context), cacheSize.toLong())
821     }
822 
823     private fun getCacheDirectory(context: Context): File {
824       return File(context.cacheDir, "okhttp")
825     }
826 
827     fun getExtraHeadersForRemoteUpdateRequest(
828       database: UpdatesDatabase,
829       configuration: UpdatesConfiguration,
830       launchedUpdate: UpdateEntity?,
831       embeddedUpdate: UpdateEntity?
832     ): JSONObject {
833       val extraHeaders =
834         ManifestMetadata.getServerDefinedHeaders(database, configuration) ?: JSONObject()
835 
836       ManifestMetadata.getExtraParams(database, configuration)?.let {
837         extraHeaders.put("Expo-Extra-Params", Dictionary.valueOf(it.mapValues { elem -> StringItem.valueOf(elem.value) }).serialize())
838       }
839 
840       launchedUpdate?.let {
841         extraHeaders.put("Expo-Current-Update-ID", it.id.toString().lowercase())
842       }
843       embeddedUpdate?.let {
844         extraHeaders.put("Expo-Embedded-Update-ID", it.id.toString().lowercase())
845       }
846 
847       return extraHeaders
848     }
849   }
850 }
851