<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