mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Add digest to fw update files (#1616)
Co-authored-by: gorbit99 <gorbitgames@gmail.com>
This commit is contained in:
@@ -143,7 +143,7 @@ export function SelectSourceSetep({
|
||||
!source.availableBoards.includes(partialBoard.board) ||
|
||||
source.source !== partialBoard.source,
|
||||
name: source.version,
|
||||
isBranch: !!source.branch,
|
||||
isBranch: source.branch == source.version,
|
||||
});
|
||||
|
||||
return curr;
|
||||
|
||||
@@ -222,7 +222,8 @@ export function FirmwareUpdate() {
|
||||
{
|
||||
isFirmware: true,
|
||||
firmwareId: '',
|
||||
filePath: firmwareFile,
|
||||
filePath: firmwareFile.url,
|
||||
digest: firmwareFile.digest,
|
||||
offset: 0,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -26,7 +26,6 @@ export type DefaultsFile = {
|
||||
|
||||
export type BoardDefaults = {
|
||||
values: void;
|
||||
editable: string[];
|
||||
flashingRules: {
|
||||
applicationOffset: number;
|
||||
needBootPress: boolean;
|
||||
@@ -59,6 +58,7 @@ export type BuildStatusDone = {
|
||||
files: {
|
||||
filePath: string;
|
||||
offset: number;
|
||||
digest: string;
|
||||
isFirmware: boolean;
|
||||
firmwareId: string;
|
||||
}[];
|
||||
@@ -94,6 +94,7 @@ export type FirmwareWithFiles = {
|
||||
files: {
|
||||
filePath: string;
|
||||
offset: number;
|
||||
digest: string;
|
||||
isFirmware: boolean;
|
||||
firmwareId: string;
|
||||
}[];
|
||||
|
||||
@@ -122,6 +122,7 @@ export const getFlashingRequests = (
|
||||
const part = new FirmwarePartT();
|
||||
part.offset = 0;
|
||||
part.url = firmware.filePath;
|
||||
part.digest = firmware.digest;
|
||||
|
||||
const method = new OTAFirmwareUpdateT();
|
||||
method.deviceId = dId;
|
||||
@@ -147,10 +148,11 @@ export const getFlashingRequests = (
|
||||
method.needManualReboot =
|
||||
defaultConfig?.flashingRules.needManualReboot ?? false;
|
||||
|
||||
method.firmwarePart = firmwareFiles.map(({ offset, filePath }) => {
|
||||
method.firmwarePart = firmwareFiles.map(({ offset, filePath, digest }) => {
|
||||
const part = new FirmwarePartT();
|
||||
part.offset = offset;
|
||||
part.url = filePath;
|
||||
part.digest = digest;
|
||||
return part;
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface FirmwareRelease {
|
||||
name: string;
|
||||
version: string;
|
||||
changelog: string;
|
||||
firmwareFiles: Partial<Record<BoardType, string>>;
|
||||
firmwareFiles: Partial<Record<BoardType, { url: string; digest: string }>>;
|
||||
userCanUpdate: boolean;
|
||||
}
|
||||
|
||||
@@ -108,8 +108,14 @@ export async function fetchCurrentFirmwareRelease(): Promise<FirmwareRelease | n
|
||||
version,
|
||||
changelog: release.body,
|
||||
firmwareFiles: {
|
||||
[BoardType.SLIMEVR]: fwAsset.browser_download_url,
|
||||
[BoardType.SLIMEVR_V1_2]: fw12Asset.browser_download_url,
|
||||
[BoardType.SLIMEVR]: {
|
||||
url: fwAsset.browser_download_url,
|
||||
digest: fwAsset.digest,
|
||||
},
|
||||
[BoardType.SLIMEVR_V1_2]: {
|
||||
url: fw12Asset.browser_download_url,
|
||||
digest: fw12Asset.digest,
|
||||
},
|
||||
},
|
||||
userCanUpdate,
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
@@ -60,11 +61,6 @@ class FirmwareUpdateHandler(private val server: VRServer) :
|
||||
private val updatingDevicesStatus: MutableMap<UpdateDeviceId<*>, UpdateStatusEvent<*>> =
|
||||
ConcurrentHashMap()
|
||||
private val listeners: MutableList<FirmwareUpdateListener> = CopyOnWriteArrayList()
|
||||
private val firmwareCache =
|
||||
InMemoryKache<String, Array<DownloadedFirmwarePart>>(maxSize = 5 * 1024 * 1024) {
|
||||
strategy = KacheStrategy.LRU
|
||||
sizeCalculator = { _, parts -> parts.sumOf { it.firmware.size }.toLong() }
|
||||
}
|
||||
private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob())
|
||||
private var clearJob: Deferred<Unit>? = null
|
||||
|
||||
@@ -297,21 +293,27 @@ class FirmwareUpdateHandler(private val server: VRServer) :
|
||||
)
|
||||
|
||||
try {
|
||||
// We add the firmware to an LRU cache
|
||||
val toDownloadParts = getFirmwareParts(request)
|
||||
val firmwareParts =
|
||||
firmwareCache.getOrPut(toDownloadParts.joinToString("|") { "${it.url}#${it.offset}" }) {
|
||||
withTimeoutOrNull(30_000) {
|
||||
toDownloadParts.map {
|
||||
val firmware = downloadFirmware(it.url)
|
||||
?: error("unable to download firmware part")
|
||||
DownloadedFirmwarePart(
|
||||
firmware,
|
||||
it.offset,
|
||||
)
|
||||
}.toTypedArray()
|
||||
}
|
||||
val firmwareParts = try {
|
||||
withTimeoutOrNull(30_000) {
|
||||
toDownloadParts.map {
|
||||
val firmware = downloadFirmware(it.url, it.digest)
|
||||
DownloadedFirmwarePart(
|
||||
firmware,
|
||||
it.offset,
|
||||
)
|
||||
}.toTypedArray()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED,
|
||||
),
|
||||
)
|
||||
LogManager.severe("[FirmwareUpdateHandler] Unable to download firmware", e)
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
val job = launch {
|
||||
withTimeout(2 * 60 * 1000) {
|
||||
@@ -485,19 +487,38 @@ class FirmwareUpdateHandler(private val server: VRServer) :
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadFirmware(url: String): ByteArray? {
|
||||
fun downloadFirmware(url: String, expectedDigest: String): ByteArray {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
try {
|
||||
val chunk = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
val stream: InputStream = URL(url).openStream()
|
||||
while (stream.read(chunk).also { bytesRead = it } > 0) {
|
||||
outputStream.write(chunk, 0, bytesRead)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
error("Cant download firmware $url")
|
||||
val chunk = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
val stream: InputStream = URL(url).openStream()
|
||||
while (stream.read(chunk).also { bytesRead = it } > 0) {
|
||||
outputStream.write(chunk, 0, bytesRead)
|
||||
}
|
||||
|
||||
return outputStream.toByteArray()
|
||||
val downloadedData = outputStream.toByteArray()
|
||||
|
||||
if (!verifyChecksum(downloadedData, expectedDigest)) {
|
||||
error("Checksum verification failed for $url")
|
||||
}
|
||||
|
||||
return downloadedData
|
||||
}
|
||||
|
||||
fun verifyChecksum(data: ByteArray, expectedDigest: String): Boolean {
|
||||
val parts = expectedDigest.split(":", limit = 2)
|
||||
if (parts.size != 2) {
|
||||
error("Invalid digest format. Expected 'algorithm:hash' got $expectedDigest")
|
||||
}
|
||||
|
||||
val algorithm = parts[0].uppercase().replace("-", "")
|
||||
val expectedHash = parts[1].lowercase()
|
||||
|
||||
val messageDigest = MessageDigest.getInstance(algorithm)
|
||||
val actualHash = messageDigest.digest(data).joinToString("") {
|
||||
"%02x".format(it)
|
||||
}
|
||||
|
||||
return actualHash == expectedHash
|
||||
}
|
||||
|
||||
Submodule solarxr-protocol updated: eed73567f7...2fe6f9cf8d
Reference in New Issue
Block a user