@onekeyfe/react-native-range-downloader 3.0.81-alpha.1 → 3.0.81-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReactNativeRangeDownloader.podspec +1 -2
- package/android/build.gradle +0 -1
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +12 -5
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweep.kt +111 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +194 -214
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +21 -21
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +39 -0
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +26 -0
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweepTest.kt +107 -0
- package/ios/FirmwareArtifactStore.swift +667 -435
- package/ios/RangeDownloadLogic.swift +317 -0
- package/ios/ReactNativeRangeDownloader.swift +60 -484
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +4 -7
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +7 -6
- package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +7 -7
- package/nitrogen/generated/android/c++/JFirmwareArtifactReceipt.hpp +7 -3
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -19
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReceipt.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -4
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +15 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -3
- package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -11
- package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +32 -13
- package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +39 -8
- package/nitrogen/generated/ios/swift/FirmwareArtifactReceipt.swift +13 -2
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -1
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -19
- package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +6 -5
- package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +9 -9
- package/nitrogen/generated/shared/c++/FirmwareArtifactReceipt.hpp +6 -2
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -1
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -4
- package/package.json +1 -2
- package/src/ReactNativeRangeDownloader.nitro.ts +7 -11
- package/ios/FirmwareBackgroundSessionEventRouter.swift +0 -17
- package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReconcileParams.hpp +0 -76
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReconcileParams.kt +0 -38
- package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReconcileParams.swift +0 -48
- package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReconcileParams.hpp +0 -76
package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt
CHANGED
|
@@ -2,11 +2,9 @@ package com.margelo.nitro.reactnativerangedownloader
|
|
|
2
2
|
|
|
3
3
|
import android.system.Os
|
|
4
4
|
import com.margelo.nitro.NitroModules
|
|
5
|
-
import com.sniconnect.SniPinnedTransport
|
|
6
5
|
import java.io.BufferedInputStream
|
|
7
6
|
import java.io.File
|
|
8
7
|
import java.io.FileInputStream
|
|
9
|
-
import java.io.FileOutputStream
|
|
10
8
|
import java.io.IOException
|
|
11
9
|
import java.io.RandomAccessFile
|
|
12
10
|
import java.security.MessageDigest
|
|
@@ -14,14 +12,29 @@ import java.util.UUID
|
|
|
14
12
|
import java.util.concurrent.ConcurrentHashMap
|
|
15
13
|
import java.util.concurrent.TimeUnit
|
|
16
14
|
import java.util.zip.ZipInputStream
|
|
15
|
+
import javax.net.ssl.SSLException
|
|
16
|
+
import kotlin.math.ceil
|
|
17
17
|
import okhttp3.Call
|
|
18
18
|
import okhttp3.OkHttpClient
|
|
19
19
|
import okhttp3.Protocol
|
|
20
20
|
import okhttp3.Request
|
|
21
21
|
import okhttp3.Response
|
|
22
22
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
|
|
24
|
+
private const val DEFAULT_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS = 180.0
|
|
25
|
+
private const val MAX_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS = 24.0 * 60 * 60
|
|
26
|
+
|
|
27
|
+
internal fun validateFirmwareDownloadDeadlineSeconds(value: Double?): Double {
|
|
28
|
+
val deadline = value ?: DEFAULT_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS
|
|
29
|
+
require(
|
|
30
|
+
deadline.isFinite() &&
|
|
31
|
+
deadline > 0 &&
|
|
32
|
+
deadline <= MAX_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS
|
|
33
|
+
) {
|
|
34
|
+
"Invalid firmware download deadline"
|
|
35
|
+
}
|
|
36
|
+
return deadline
|
|
37
|
+
}
|
|
25
38
|
|
|
26
39
|
internal data class StoredFirmwareArtifact(
|
|
27
40
|
val artifactRef: String,
|
|
@@ -44,22 +57,25 @@ private data class StagedFirmwareArchiveEntry(
|
|
|
44
57
|
|
|
45
58
|
private data class FirmwareDownloadKey(
|
|
46
59
|
val transactionId: String,
|
|
47
|
-
val
|
|
60
|
+
val taskId: String,
|
|
61
|
+
val downloadToken: String,
|
|
48
62
|
)
|
|
49
63
|
|
|
64
|
+
private class FirmwareDownloadLock {
|
|
65
|
+
val monitor = Any()
|
|
66
|
+
var references = 0
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
internal object FirmwareArtifactStore {
|
|
51
70
|
const val MAX_READ_BYTES = 256 * 1024
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
private const val MAX_LEASE_METADATA_BYTES = 1024L * 1024
|
|
55
|
-
private const val MAX_TOTAL_LEASE_REFS = 8192
|
|
56
|
-
private const val FINAL_ARTIFACT_GRACE_MS = 24L * 60 * 60 * 1000
|
|
57
|
-
private const val PARTIAL_ARTIFACT_GRACE_MS = 7L * 24 * 60 * 60 * 1000
|
|
72
|
+
const val MAX_ARTIFACT_BYTES = 512L * 1024 * 1024
|
|
58
73
|
private val sha256Pattern = Regex("^[a-fA-F0-9]{64}$")
|
|
59
74
|
private val artifactRefPattern = Regex("^fw:[a-f0-9]{64}$")
|
|
60
75
|
private val leaseRefPattern = Regex("^fwlease:[a-f0-9-]{36}$")
|
|
61
76
|
private val identifierPattern = Regex("^[A-Za-z0-9._:-]{1,160}$")
|
|
62
|
-
private val downloadLocks =
|
|
77
|
+
private val downloadLocks =
|
|
78
|
+
ConcurrentHashMap<FirmwareDownloadKey, FirmwareDownloadLock>()
|
|
63
79
|
private val activeCalls =
|
|
64
80
|
ConcurrentHashMap<String, MutableSet<Call>>()
|
|
65
81
|
private val cancelledTransactions =
|
|
@@ -69,6 +85,7 @@ internal object FirmwareArtifactStore {
|
|
|
69
85
|
private val leaseLock = Any()
|
|
70
86
|
private val readerLock = Any()
|
|
71
87
|
private val readers = mutableMapOf<String, OpenReader>()
|
|
88
|
+
private val leases = mutableMapOf<String, LeaseState>()
|
|
72
89
|
|
|
73
90
|
private data class LeaseState(
|
|
74
91
|
val transactionId: String,
|
|
@@ -97,26 +114,47 @@ internal object FirmwareArtifactStore {
|
|
|
97
114
|
check(!cancelledTransactions.contains(params.transactionId)) {
|
|
98
115
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
99
116
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
validated.expectedSha256?.let {
|
|
118
|
+
retainExpectedArtifact(
|
|
119
|
+
leaseRef = params.leaseRef,
|
|
120
|
+
transactionId = params.transactionId,
|
|
121
|
+
artifactRef = "fw:$it",
|
|
122
|
+
)
|
|
123
|
+
} ?: requireLeaseTransaction(params.leaseRef, params.transactionId)
|
|
105
124
|
val lockKey = FirmwareDownloadKey(
|
|
106
125
|
params.transactionId,
|
|
107
|
-
|
|
126
|
+
params.taskId,
|
|
127
|
+
validated.downloadToken,
|
|
108
128
|
)
|
|
109
|
-
val
|
|
110
|
-
|
|
129
|
+
val downloadLock = downloadLocks.compute(lockKey) { _, current ->
|
|
130
|
+
(current ?: FirmwareDownloadLock()).also {
|
|
131
|
+
it.references += 1
|
|
132
|
+
}
|
|
133
|
+
} ?: error("Firmware artifact lock is unavailable")
|
|
134
|
+
markDownloadActive(validated.downloadToken, 1)
|
|
111
135
|
try {
|
|
112
|
-
|
|
136
|
+
val artifact = synchronized(downloadLock.monitor) {
|
|
113
137
|
check(!cancelledTransactions.contains(params.transactionId)) {
|
|
114
138
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
115
139
|
}
|
|
116
140
|
downloadLocked(params, validated)
|
|
117
141
|
}
|
|
142
|
+
retainExpectedArtifact(
|
|
143
|
+
leaseRef = params.leaseRef,
|
|
144
|
+
transactionId = params.transactionId,
|
|
145
|
+
artifactRef = artifact.artifactRef,
|
|
146
|
+
)
|
|
147
|
+
return artifact
|
|
118
148
|
} finally {
|
|
119
|
-
markDownloadActive(validated.
|
|
149
|
+
markDownloadActive(validated.downloadToken, -1)
|
|
150
|
+
downloadLocks.compute(lockKey) { _, current ->
|
|
151
|
+
if (current !== downloadLock) {
|
|
152
|
+
current
|
|
153
|
+
} else {
|
|
154
|
+
current.references -= 1
|
|
155
|
+
current.takeIf { it.references > 0 }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
120
158
|
}
|
|
121
159
|
}
|
|
122
160
|
|
|
@@ -133,7 +171,7 @@ internal object FirmwareArtifactStore {
|
|
|
133
171
|
fun discard(artifactRef: String) {
|
|
134
172
|
val file = resolveArtifactFile(artifactRef)
|
|
135
173
|
synchronized(leaseLock) {
|
|
136
|
-
require(
|
|
174
|
+
require(leases.values.none { artifactRef in it.artifactRefs }) {
|
|
137
175
|
"ARTIFACT_LEASED: firmware artifact is retained"
|
|
138
176
|
}
|
|
139
177
|
}
|
|
@@ -186,16 +224,19 @@ internal object FirmwareArtifactStore {
|
|
|
186
224
|
fun materializeArchive(
|
|
187
225
|
leaseRef: String,
|
|
188
226
|
artifactRef: String,
|
|
189
|
-
expectedEntries: Array<FirmwareArchiveExpectedEntry
|
|
227
|
+
expectedEntries: Array<FirmwareArchiveExpectedEntry>?,
|
|
190
228
|
): List<StoredFirmwareArchiveEntry> {
|
|
191
229
|
requireLease(leaseRef)
|
|
192
230
|
val archiveFile = resolveArtifactFile(artifactRef)
|
|
193
|
-
val requirements =
|
|
231
|
+
val requirements = expectedEntries?.let {
|
|
232
|
+
FirmwareArchiveRules.validateRequirements(it)
|
|
233
|
+
}
|
|
194
234
|
val centralEntries = FirmwareArchiveRules.validateCentralDirectory(
|
|
195
235
|
archiveFile,
|
|
196
236
|
requirements,
|
|
197
237
|
)
|
|
198
|
-
val requirementsByName = requirements
|
|
238
|
+
val requirementsByName = requirements?.associateBy { it.entryName }.orEmpty()
|
|
239
|
+
val centralEntriesByName = centralEntries.associateBy { it.name }
|
|
199
240
|
val centralNames = centralEntries.mapTo(mutableSetOf()) { it.name }
|
|
200
241
|
val scratchDir = File(root, "archive-${UUID.randomUUID()}")
|
|
201
242
|
check(scratchDir.mkdirs()) { "Firmware archive scratch directory cannot be created" }
|
|
@@ -208,16 +249,18 @@ internal object FirmwareArtifactStore {
|
|
|
208
249
|
require(!zipEntry.isDirectory) {
|
|
209
250
|
"Firmware archive contains an unexpected directory"
|
|
210
251
|
}
|
|
211
|
-
val
|
|
252
|
+
val centralEntry = centralEntriesByName[zipEntry.name]
|
|
212
253
|
?: error("Firmware archive contains an unexpected entry")
|
|
254
|
+
val requirement = requirementsByName[zipEntry.name]
|
|
213
255
|
require(
|
|
214
256
|
centralNames.contains(zipEntry.name) &&
|
|
215
257
|
entryNames.add(zipEntry.name)
|
|
216
258
|
) {
|
|
217
259
|
"Firmware archive contains a duplicate or mismatched entry"
|
|
218
260
|
}
|
|
219
|
-
val expectedSize = requirement
|
|
220
|
-
|
|
261
|
+
val expectedSize = requirement?.expectedSize?.toLong()
|
|
262
|
+
?: centralEntry.uncompressedSize
|
|
263
|
+
val expectedSha256 = requirement?.expectedSha256?.lowercase()
|
|
221
264
|
val scratchFile = File(scratchDir, "${staged.size}.entry")
|
|
222
265
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
223
266
|
var entrySize = 0L
|
|
@@ -237,11 +280,14 @@ internal object FirmwareArtifactStore {
|
|
|
237
280
|
output.fd.sync()
|
|
238
281
|
}
|
|
239
282
|
val sha256 = digest.digest().toHex()
|
|
240
|
-
require(
|
|
283
|
+
require(
|
|
284
|
+
entrySize == expectedSize &&
|
|
285
|
+
(expectedSha256 == null || sha256 == expectedSha256)
|
|
286
|
+
) {
|
|
241
287
|
"Firmware archive entry integrity mismatch"
|
|
242
288
|
}
|
|
243
289
|
staged += StagedFirmwareArchiveEntry(
|
|
244
|
-
entryName =
|
|
290
|
+
entryName = zipEntry.name,
|
|
245
291
|
size = entrySize,
|
|
246
292
|
sha256 = sha256,
|
|
247
293
|
file = scratchFile,
|
|
@@ -280,10 +326,11 @@ internal object FirmwareArtifactStore {
|
|
|
280
326
|
}
|
|
281
327
|
|
|
282
328
|
private data class ValidatedDownload(
|
|
283
|
-
val expectedSize: Long
|
|
329
|
+
val expectedSize: Long?,
|
|
284
330
|
val maxBytes: Long,
|
|
285
|
-
val expectedSha256: String
|
|
286
|
-
val
|
|
331
|
+
val expectedSha256: String?,
|
|
332
|
+
val downloadToken: String,
|
|
333
|
+
val overallDeadlineSeconds: Double,
|
|
287
334
|
)
|
|
288
335
|
|
|
289
336
|
private fun validateDownloadParams(
|
|
@@ -316,31 +363,32 @@ internal object FirmwareArtifactStore {
|
|
|
316
363
|
) {
|
|
317
364
|
"Firmware URL must use HTTPS port 443"
|
|
318
365
|
}
|
|
319
|
-
val expectedSize = params.expectedSize
|
|
366
|
+
val expectedSize = params.expectedSize?.toExactPositiveLong("expectedSize")
|
|
320
367
|
val maxBytes = params.maxBytes.toExactPositiveLong("maxBytes")
|
|
321
|
-
require(
|
|
368
|
+
require(
|
|
369
|
+
maxBytes <= MAX_ARTIFACT_BYTES &&
|
|
370
|
+
(expectedSize == null || expectedSize <= maxBytes)
|
|
371
|
+
) {
|
|
322
372
|
"Invalid firmware maxBytes"
|
|
323
373
|
}
|
|
324
|
-
|
|
374
|
+
val expectedSha256 = params.expectedSha256?.lowercase()
|
|
375
|
+
require(expectedSha256 == null || sha256Pattern.matches(expectedSha256)) {
|
|
325
376
|
"Invalid firmware artifact SHA-256"
|
|
326
377
|
}
|
|
327
|
-
|
|
378
|
+
val overallDeadlineSeconds =
|
|
379
|
+
validateFirmwareDownloadDeadlineSeconds(params.overallDeadlineSeconds)
|
|
380
|
+
require(params.routeType == "domain") {
|
|
328
381
|
"Invalid firmware route type"
|
|
329
382
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"Pinned route requires resolvedIp"
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
require(params.resolvedIp == null) {
|
|
336
|
-
"Domain route must not include resolvedIp"
|
|
337
|
-
}
|
|
383
|
+
require(params.resolvedIp == null) {
|
|
384
|
+
"Domain route must not include resolvedIp"
|
|
338
385
|
}
|
|
339
386
|
return ValidatedDownload(
|
|
340
387
|
expectedSize = expectedSize,
|
|
341
388
|
maxBytes = maxBytes,
|
|
342
|
-
expectedSha256 =
|
|
343
|
-
|
|
389
|
+
expectedSha256 = expectedSha256,
|
|
390
|
+
downloadToken = expectedSha256 ?: sha256(params.url),
|
|
391
|
+
overallDeadlineSeconds = overallDeadlineSeconds,
|
|
344
392
|
)
|
|
345
393
|
}
|
|
346
394
|
|
|
@@ -348,26 +396,37 @@ internal object FirmwareArtifactStore {
|
|
|
348
396
|
params: FirmwareArtifactDownloadParams,
|
|
349
397
|
validated: ValidatedDownload,
|
|
350
398
|
): StoredFirmwareArtifact {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
399
|
+
validated.expectedSha256?.let { expectedSha256 ->
|
|
400
|
+
val finalFile = artifactFile(expectedSha256)
|
|
401
|
+
validateDownloadedArtifactOrNull(
|
|
402
|
+
finalFile,
|
|
403
|
+
validated.expectedSize,
|
|
404
|
+
expectedSha256,
|
|
405
|
+
validated.maxBytes,
|
|
406
|
+
)?.let { return it }
|
|
407
|
+
}
|
|
357
408
|
|
|
409
|
+
val transactionToken = sha256(params.transactionId).take(16)
|
|
358
410
|
val partialFile = File(
|
|
359
411
|
root,
|
|
360
|
-
"${validated.
|
|
412
|
+
"${validated.downloadToken}.${params.taskId}.$transactionToken.partial",
|
|
361
413
|
)
|
|
362
|
-
if (partialFile.length() >
|
|
414
|
+
if (validated.expectedSha256 == null && partialFile.length() > 0) {
|
|
415
|
+
check(partialFile.delete()) { "Unverified firmware partial cannot be removed" }
|
|
416
|
+
} else if (partialFile.length() > validated.maxBytes) {
|
|
363
417
|
check(partialFile.delete()) { "Invalid firmware partial cannot be removed" }
|
|
364
418
|
}
|
|
365
|
-
if (
|
|
366
|
-
|
|
419
|
+
if (
|
|
420
|
+
validated.expectedSize != null &&
|
|
421
|
+
partialFile.length() == validated.expectedSize
|
|
422
|
+
) {
|
|
423
|
+
validateDownloadedArtifactOrNull(
|
|
367
424
|
partialFile,
|
|
368
425
|
validated.expectedSize,
|
|
369
426
|
validated.expectedSha256,
|
|
427
|
+
validated.maxBytes,
|
|
370
428
|
)?.let {
|
|
429
|
+
val finalFile = artifactFile(it.sha256)
|
|
371
430
|
promoteAtomically(partialFile, finalFile)
|
|
372
431
|
return StoredFirmwareArtifact(
|
|
373
432
|
it.artifactRef,
|
|
@@ -387,25 +446,16 @@ internal object FirmwareArtifactStore {
|
|
|
387
446
|
requestBuilder.header("Range", "bytes=$resumeOffset-")
|
|
388
447
|
}
|
|
389
448
|
|
|
390
|
-
val client =
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
)
|
|
395
|
-
} else {
|
|
396
|
-
OkHttpClient.Builder()
|
|
397
|
-
.protocols(listOf(Protocol.HTTP_1_1))
|
|
398
|
-
.followRedirects(false)
|
|
399
|
-
.followSslRedirects(false)
|
|
400
|
-
.build()
|
|
401
|
-
}
|
|
449
|
+
val client = OkHttpClient.Builder()
|
|
450
|
+
.protocols(listOf(Protocol.HTTP_1_1))
|
|
451
|
+
.followRedirects(false)
|
|
452
|
+
.followSslRedirects(false)
|
|
453
|
+
.build()
|
|
402
454
|
val call = client.newCall(requestBuilder.build())
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
call.timeout().timeout(deadline.toLong().coerceAtLeast(1), TimeUnit.SECONDS)
|
|
408
|
-
}
|
|
455
|
+
call.timeout().timeout(
|
|
456
|
+
ceil(validated.overallDeadlineSeconds * 1000).toLong(),
|
|
457
|
+
TimeUnit.MILLISECONDS,
|
|
458
|
+
)
|
|
409
459
|
|
|
410
460
|
registerCall(params.transactionId, call)
|
|
411
461
|
try {
|
|
@@ -428,6 +478,12 @@ internal object FirmwareArtifactStore {
|
|
|
428
478
|
error,
|
|
429
479
|
)
|
|
430
480
|
}
|
|
481
|
+
if (generateSequence<Throwable>(error) { it.cause }.any { it is SSLException }) {
|
|
482
|
+
throw IllegalStateException(
|
|
483
|
+
"ARTIFACT_TLS_FAILED: firmware TLS validation failed",
|
|
484
|
+
error,
|
|
485
|
+
)
|
|
486
|
+
}
|
|
431
487
|
throw IllegalStateException(
|
|
432
488
|
"ARTIFACT_NETWORK_FAILED: firmware request failed",
|
|
433
489
|
error,
|
|
@@ -440,15 +496,17 @@ internal object FirmwareArtifactStore {
|
|
|
440
496
|
}
|
|
441
497
|
|
|
442
498
|
val artifact = try {
|
|
443
|
-
|
|
499
|
+
validateDownloadedArtifact(
|
|
444
500
|
partialFile,
|
|
445
501
|
validated.expectedSize,
|
|
446
502
|
validated.expectedSha256,
|
|
503
|
+
validated.maxBytes,
|
|
447
504
|
)
|
|
448
505
|
} catch (error: Throwable) {
|
|
449
506
|
partialFile.delete()
|
|
450
507
|
throw error
|
|
451
508
|
}
|
|
509
|
+
val finalFile = artifactFile(artifact.sha256)
|
|
452
510
|
promoteAtomically(partialFile, finalFile)
|
|
453
511
|
return StoredFirmwareArtifact(
|
|
454
512
|
artifact.artifactRef,
|
|
@@ -462,7 +520,7 @@ internal object FirmwareArtifactStore {
|
|
|
462
520
|
response: Response,
|
|
463
521
|
partialFile: File,
|
|
464
522
|
resumeOffset: Long,
|
|
465
|
-
expectedSize: Long
|
|
523
|
+
expectedSize: Long?,
|
|
466
524
|
maxBytes: Long,
|
|
467
525
|
) {
|
|
468
526
|
require(response.code == 200 || response.code == 206) {
|
|
@@ -475,6 +533,7 @@ internal object FirmwareArtifactStore {
|
|
|
475
533
|
response.header("Content-Range"),
|
|
476
534
|
if (append) resumeOffset else 0,
|
|
477
535
|
expectedSize,
|
|
536
|
+
maxBytes,
|
|
478
537
|
)
|
|
479
538
|
) {
|
|
480
539
|
"ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
|
|
@@ -507,7 +566,8 @@ internal object FirmwareArtifactStore {
|
|
|
507
566
|
private fun validateContentRange(
|
|
508
567
|
value: String?,
|
|
509
568
|
expectedStart: Long,
|
|
510
|
-
expectedTotal: Long
|
|
569
|
+
expectedTotal: Long?,
|
|
570
|
+
maxBytes: Long,
|
|
511
571
|
): Boolean {
|
|
512
572
|
val match = value
|
|
513
573
|
?.lowercase()
|
|
@@ -519,7 +579,39 @@ internal object FirmwareArtifactStore {
|
|
|
519
579
|
return start == expectedStart &&
|
|
520
580
|
end >= start &&
|
|
521
581
|
end < total &&
|
|
522
|
-
total ==
|
|
582
|
+
(expectedTotal?.let { total == it } ?: (total in 1..maxBytes))
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private fun validateDownloadedArtifactOrNull(
|
|
586
|
+
file: File,
|
|
587
|
+
expectedSize: Long?,
|
|
588
|
+
expectedSha256: String?,
|
|
589
|
+
maxBytes: Long,
|
|
590
|
+
): StoredFirmwareArtifact? = try {
|
|
591
|
+
validateDownloadedArtifact(file, expectedSize, expectedSha256, maxBytes)
|
|
592
|
+
} catch (_: Throwable) {
|
|
593
|
+
null
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private fun validateDownloadedArtifact(
|
|
597
|
+
file: File,
|
|
598
|
+
expectedSize: Long?,
|
|
599
|
+
expectedSha256: String?,
|
|
600
|
+
maxBytes: Long,
|
|
601
|
+
): StoredFirmwareArtifact {
|
|
602
|
+
val size = file.length()
|
|
603
|
+
require(
|
|
604
|
+
file.isFile &&
|
|
605
|
+
size in 1..maxBytes &&
|
|
606
|
+
(expectedSize == null || size == expectedSize)
|
|
607
|
+
) {
|
|
608
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
|
|
609
|
+
}
|
|
610
|
+
val sha256 = hashFile(file)
|
|
611
|
+
require(expectedSha256 == null || sha256 == expectedSha256) {
|
|
612
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
|
|
613
|
+
}
|
|
614
|
+
return StoredFirmwareArtifact("fw:$sha256", size, sha256, file)
|
|
523
615
|
}
|
|
524
616
|
|
|
525
617
|
private fun validateStoredArtifactOrNull(
|
|
@@ -563,13 +655,11 @@ internal object FirmwareArtifactStore {
|
|
|
563
655
|
"Invalid firmware transactionId"
|
|
564
656
|
}
|
|
565
657
|
return synchronized(leaseLock) {
|
|
566
|
-
val leases = loadLeasesLocked()
|
|
567
658
|
require(leases.size < 32) {
|
|
568
659
|
"Too many firmware artifact leases"
|
|
569
660
|
}
|
|
570
661
|
val leaseRef = "fwlease:${UUID.randomUUID()}"
|
|
571
662
|
leases[leaseRef] = LeaseState(transactionId, mutableSetOf())
|
|
572
|
-
saveLeasesLocked(leases)
|
|
573
663
|
leaseRef
|
|
574
664
|
}
|
|
575
665
|
}
|
|
@@ -592,41 +682,18 @@ internal object FirmwareArtifactStore {
|
|
|
592
682
|
"Invalid firmware lease disposition"
|
|
593
683
|
}
|
|
594
684
|
val transactionId = synchronized(leaseLock) {
|
|
595
|
-
val leases = loadLeasesLocked()
|
|
596
685
|
val removed = leases.remove(validateLeaseRef(leaseRef))
|
|
597
686
|
require(removed != null) {
|
|
598
687
|
"Firmware artifact lease is unavailable"
|
|
599
688
|
}
|
|
600
|
-
saveLeasesLocked(leases)
|
|
601
689
|
removed.transactionId
|
|
602
690
|
}
|
|
603
691
|
cancelledTransactions.remove(transactionId)
|
|
604
|
-
downloadLocks.keys.removeIf { it.transactionId == transactionId }
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
fun reconcileLeases(activeLeaseRefs: Array<String>) {
|
|
608
|
-
require(activeLeaseRefs.size <= 32) {
|
|
609
|
-
"Too many active firmware artifact leases"
|
|
610
|
-
}
|
|
611
|
-
val active = activeLeaseRefs.mapTo(mutableSetOf()) {
|
|
612
|
-
validateLeaseRef(it)
|
|
613
|
-
}
|
|
614
|
-
require(active.size == activeLeaseRefs.size) {
|
|
615
|
-
"Duplicate active firmware artifact lease"
|
|
616
|
-
}
|
|
617
|
-
synchronized(leaseLock) {
|
|
618
|
-
val leases = loadLeasesLocked()
|
|
619
|
-
require(active.all { leases.containsKey(it) }) {
|
|
620
|
-
"Firmware artifact lease reconciliation is incomplete"
|
|
621
|
-
}
|
|
622
|
-
leases.keys.retainAll(active)
|
|
623
|
-
saveLeasesLocked(leases)
|
|
624
|
-
}
|
|
625
692
|
}
|
|
626
693
|
|
|
627
694
|
fun sweepOrphans(): Pair<Int, Long> {
|
|
628
695
|
val retainedSha256 = synchronized(leaseLock) {
|
|
629
|
-
|
|
696
|
+
leases.values
|
|
630
697
|
.flatMap { it.artifactRefs }
|
|
631
698
|
.mapTo(mutableSetOf()) { it.removePrefix("fw:") }
|
|
632
699
|
}
|
|
@@ -636,45 +703,31 @@ internal object FirmwareArtifactStore {
|
|
|
636
703
|
val openFiles = synchronized(readerLock) {
|
|
637
704
|
readers.values.mapTo(mutableSetOf()) { it.file.absolutePath }
|
|
638
705
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
!sha256Pattern.matches(sha256) ||
|
|
647
|
-
sha256 in retainedSha256 ||
|
|
648
|
-
sha256 in activeSha256 ||
|
|
649
|
-
file.absolutePath in openFiles
|
|
650
|
-
) {
|
|
651
|
-
return@forEach
|
|
652
|
-
}
|
|
653
|
-
val grace = if (file.name.endsWith(".bin")) {
|
|
654
|
-
FINAL_ARTIFACT_GRACE_MS
|
|
655
|
-
} else if (file.name.endsWith(".partial")) {
|
|
656
|
-
PARTIAL_ARTIFACT_GRACE_MS
|
|
657
|
-
} else {
|
|
658
|
-
return@forEach
|
|
659
|
-
}
|
|
660
|
-
if (now - file.lastModified() < grace) return@forEach
|
|
661
|
-
val size = file.length()
|
|
662
|
-
if (file.delete()) {
|
|
663
|
-
deletedFiles += 1
|
|
664
|
-
deletedBytes += size
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return deletedFiles to deletedBytes
|
|
706
|
+
return sweepFirmwareArtifactOrphansAtRoot(
|
|
707
|
+
root = root,
|
|
708
|
+
retainedSha256 = retainedSha256,
|
|
709
|
+
activeSha256 = activeSha256,
|
|
710
|
+
openPaths = openFiles,
|
|
711
|
+
)
|
|
668
712
|
}
|
|
669
713
|
|
|
670
714
|
private fun requireLease(leaseRef: String) {
|
|
671
715
|
synchronized(leaseLock) {
|
|
672
|
-
require(
|
|
716
|
+
require(leases.containsKey(validateLeaseRef(leaseRef))) {
|
|
673
717
|
"Firmware artifact lease is unavailable"
|
|
674
718
|
}
|
|
675
719
|
}
|
|
676
720
|
}
|
|
677
721
|
|
|
722
|
+
private fun requireLeaseTransaction(leaseRef: String, transactionId: String) {
|
|
723
|
+
synchronized(leaseLock) {
|
|
724
|
+
val lease = leases[validateLeaseRef(leaseRef)]
|
|
725
|
+
require(lease?.transactionId == transactionId) {
|
|
726
|
+
"Firmware artifact lease transaction mismatch"
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
678
731
|
private fun retainExpectedArtifact(
|
|
679
732
|
leaseRef: String,
|
|
680
733
|
transactionId: String?,
|
|
@@ -684,7 +737,6 @@ internal object FirmwareArtifactStore {
|
|
|
684
737
|
"Invalid firmware artifactRef"
|
|
685
738
|
}
|
|
686
739
|
synchronized(leaseLock) {
|
|
687
|
-
val leases = loadLeasesLocked()
|
|
688
740
|
val lease = leases[validateLeaseRef(leaseRef)]
|
|
689
741
|
?: error("Firmware artifact lease is unavailable")
|
|
690
742
|
if (transactionId != null) {
|
|
@@ -692,9 +744,7 @@ internal object FirmwareArtifactStore {
|
|
|
692
744
|
"Firmware artifact lease transaction mismatch"
|
|
693
745
|
}
|
|
694
746
|
}
|
|
695
|
-
|
|
696
|
-
saveLeasesLocked(leases)
|
|
697
|
-
}
|
|
747
|
+
lease.artifactRefs.add(artifactRef)
|
|
698
748
|
}
|
|
699
749
|
}
|
|
700
750
|
|
|
@@ -705,81 +755,6 @@ internal object FirmwareArtifactStore {
|
|
|
705
755
|
return leaseRef
|
|
706
756
|
}
|
|
707
757
|
|
|
708
|
-
private fun loadLeasesLocked(): MutableMap<String, LeaseState> {
|
|
709
|
-
val file = File(root, "leases.json")
|
|
710
|
-
if (!file.exists()) return mutableMapOf()
|
|
711
|
-
require(file.length() in 1..MAX_LEASE_METADATA_BYTES) {
|
|
712
|
-
"Firmware lease metadata is too large"
|
|
713
|
-
}
|
|
714
|
-
val envelope = JSONObject(file.readText(Charsets.UTF_8))
|
|
715
|
-
require(envelope.optInt("schemaVersion") == 1) {
|
|
716
|
-
"Unsupported firmware lease schema"
|
|
717
|
-
}
|
|
718
|
-
val result = mutableMapOf<String, LeaseState>()
|
|
719
|
-
val jsonLeases = envelope.getJSONObject("leases")
|
|
720
|
-
val keys = jsonLeases.keys()
|
|
721
|
-
while (keys.hasNext()) {
|
|
722
|
-
val leaseRef = validateLeaseRef(keys.next())
|
|
723
|
-
val jsonLease = jsonLeases.getJSONObject(leaseRef)
|
|
724
|
-
val transactionId = jsonLease.getString("transactionId")
|
|
725
|
-
require(identifierPattern.matches(transactionId)) {
|
|
726
|
-
"Invalid persisted firmware transactionId"
|
|
727
|
-
}
|
|
728
|
-
val jsonRefs = jsonLease.getJSONArray("artifactRefs")
|
|
729
|
-
require(jsonRefs.length() <= 4096) {
|
|
730
|
-
"Too many persisted firmware artifact refs"
|
|
731
|
-
}
|
|
732
|
-
val refs = mutableSetOf<String>()
|
|
733
|
-
for (index in 0 until jsonRefs.length()) {
|
|
734
|
-
val artifactRef = jsonRefs.getString(index)
|
|
735
|
-
require(artifactRefPattern.matches(artifactRef) && refs.add(artifactRef)) {
|
|
736
|
-
"Invalid persisted firmware artifact ref"
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
result[leaseRef] = LeaseState(transactionId, refs)
|
|
740
|
-
}
|
|
741
|
-
require(result.size <= 32) {
|
|
742
|
-
"Too many persisted firmware artifact leases"
|
|
743
|
-
}
|
|
744
|
-
require(result.values.sumOf { it.artifactRefs.size } <= MAX_TOTAL_LEASE_REFS) {
|
|
745
|
-
"Too many persisted firmware artifact refs"
|
|
746
|
-
}
|
|
747
|
-
return result
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
private fun saveLeasesLocked(leases: Map<String, LeaseState>) {
|
|
751
|
-
require(
|
|
752
|
-
leases.size <= 32 &&
|
|
753
|
-
leases.values.sumOf { it.artifactRefs.size } <= MAX_TOTAL_LEASE_REFS
|
|
754
|
-
) {
|
|
755
|
-
"Firmware lease metadata is too large"
|
|
756
|
-
}
|
|
757
|
-
val jsonLeases = JSONObject()
|
|
758
|
-
leases.toSortedMap().forEach { (leaseRef, lease) ->
|
|
759
|
-
jsonLeases.put(
|
|
760
|
-
leaseRef,
|
|
761
|
-
JSONObject()
|
|
762
|
-
.put("transactionId", lease.transactionId)
|
|
763
|
-
.put("artifactRefs", JSONArray(lease.artifactRefs.sorted())),
|
|
764
|
-
)
|
|
765
|
-
}
|
|
766
|
-
val bytes = JSONObject()
|
|
767
|
-
.put("schemaVersion", 1)
|
|
768
|
-
.put("leases", jsonLeases)
|
|
769
|
-
.toString()
|
|
770
|
-
.toByteArray(Charsets.UTF_8)
|
|
771
|
-
require(bytes.size.toLong() <= MAX_LEASE_METADATA_BYTES) {
|
|
772
|
-
"Firmware lease metadata is too large"
|
|
773
|
-
}
|
|
774
|
-
val destination = File(root, "leases.json")
|
|
775
|
-
val temporary = File(root, ".leases-${UUID.randomUUID()}.tmp")
|
|
776
|
-
FileOutputStream(temporary).use { output ->
|
|
777
|
-
output.write(bytes)
|
|
778
|
-
output.fd.sync()
|
|
779
|
-
}
|
|
780
|
-
Os.rename(temporary.absolutePath, destination.absolutePath)
|
|
781
|
-
}
|
|
782
|
-
|
|
783
758
|
private fun markDownloadActive(sha256: String, delta: Int) {
|
|
784
759
|
synchronized(activeDownloadLock) {
|
|
785
760
|
val count = (activeDownloadCounts[sha256] ?: 0) + delta
|
|
@@ -804,6 +779,11 @@ internal object FirmwareArtifactStore {
|
|
|
804
779
|
return digest.digest().toHex()
|
|
805
780
|
}
|
|
806
781
|
|
|
782
|
+
private fun sha256(value: String): String =
|
|
783
|
+
MessageDigest.getInstance("SHA-256")
|
|
784
|
+
.digest(value.toByteArray(Charsets.UTF_8))
|
|
785
|
+
.toHex()
|
|
786
|
+
|
|
807
787
|
private fun promoteAtomically(source: File, destination: File) {
|
|
808
788
|
destination.parentFile?.mkdirs()
|
|
809
789
|
Os.rename(source.absolutePath, destination.absolutePath)
|