Add digest to fw update files (#1616)

Co-authored-by: gorbit99 <gorbitgames@gmail.com>
This commit is contained in:
lucas lelievre
2025-11-06 01:07:26 +01:00
committed by GitHub
parent e0f1ad8d4f
commit 56c3290e1c
7 changed files with 68 additions and 37 deletions

View File

@@ -143,7 +143,7 @@ export function SelectSourceSetep({
!source.availableBoards.includes(partialBoard.board) || !source.availableBoards.includes(partialBoard.board) ||
source.source !== partialBoard.source, source.source !== partialBoard.source,
name: source.version, name: source.version,
isBranch: !!source.branch, isBranch: source.branch == source.version,
}); });
return curr; return curr;

View File

@@ -222,7 +222,8 @@ export function FirmwareUpdate() {
{ {
isFirmware: true, isFirmware: true,
firmwareId: '', firmwareId: '',
filePath: firmwareFile, filePath: firmwareFile.url,
digest: firmwareFile.digest,
offset: 0, offset: 0,
}, },
], ],

View File

@@ -26,7 +26,6 @@ export type DefaultsFile = {
export type BoardDefaults = { export type BoardDefaults = {
values: void; values: void;
editable: string[];
flashingRules: { flashingRules: {
applicationOffset: number; applicationOffset: number;
needBootPress: boolean; needBootPress: boolean;
@@ -59,6 +58,7 @@ export type BuildStatusDone = {
files: { files: {
filePath: string; filePath: string;
offset: number; offset: number;
digest: string;
isFirmware: boolean; isFirmware: boolean;
firmwareId: string; firmwareId: string;
}[]; }[];
@@ -94,6 +94,7 @@ export type FirmwareWithFiles = {
files: { files: {
filePath: string; filePath: string;
offset: number; offset: number;
digest: string;
isFirmware: boolean; isFirmware: boolean;
firmwareId: string; firmwareId: string;
}[]; }[];

View File

@@ -122,6 +122,7 @@ export const getFlashingRequests = (
const part = new FirmwarePartT(); const part = new FirmwarePartT();
part.offset = 0; part.offset = 0;
part.url = firmware.filePath; part.url = firmware.filePath;
part.digest = firmware.digest;
const method = new OTAFirmwareUpdateT(); const method = new OTAFirmwareUpdateT();
method.deviceId = dId; method.deviceId = dId;
@@ -147,10 +148,11 @@ export const getFlashingRequests = (
method.needManualReboot = method.needManualReboot =
defaultConfig?.flashingRules.needManualReboot ?? false; defaultConfig?.flashingRules.needManualReboot ?? false;
method.firmwarePart = firmwareFiles.map(({ offset, filePath }) => { method.firmwarePart = firmwareFiles.map(({ offset, filePath, digest }) => {
const part = new FirmwarePartT(); const part = new FirmwarePartT();
part.offset = offset; part.offset = offset;
part.url = filePath; part.url = filePath;
part.digest = digest;
return part; return part;
}); });

View File

@@ -8,7 +8,7 @@ export interface FirmwareRelease {
name: string; name: string;
version: string; version: string;
changelog: string; changelog: string;
firmwareFiles: Partial<Record<BoardType, string>>; firmwareFiles: Partial<Record<BoardType, { url: string; digest: string }>>;
userCanUpdate: boolean; userCanUpdate: boolean;
} }
@@ -108,8 +108,14 @@ export async function fetchCurrentFirmwareRelease(): Promise<FirmwareRelease | n
version, version,
changelog: release.body, changelog: release.body,
firmwareFiles: { firmwareFiles: {
[BoardType.SLIMEVR]: fwAsset.browser_download_url, [BoardType.SLIMEVR]: {
[BoardType.SLIMEVR_V1_2]: fw12Asset.browser_download_url, url: fwAsset.browser_download_url,
digest: fwAsset.digest,
},
[BoardType.SLIMEVR_V1_2]: {
url: fw12Asset.browser_download_url,
digest: fw12Asset.digest,
},
}, },
userCanUpdate, userCanUpdate,
}); });

View File

@@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.net.URL import java.net.URL
import java.security.MessageDigest
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@@ -60,11 +61,6 @@ class FirmwareUpdateHandler(private val server: VRServer) :
private val updatingDevicesStatus: MutableMap<UpdateDeviceId<*>, UpdateStatusEvent<*>> = private val updatingDevicesStatus: MutableMap<UpdateDeviceId<*>, UpdateStatusEvent<*>> =
ConcurrentHashMap() ConcurrentHashMap()
private val listeners: MutableList<FirmwareUpdateListener> = CopyOnWriteArrayList() 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 val mainScope: CoroutineScope = CoroutineScope(SupervisorJob())
private var clearJob: Deferred<Unit>? = null private var clearJob: Deferred<Unit>? = null
@@ -297,21 +293,27 @@ class FirmwareUpdateHandler(private val server: VRServer) :
) )
try { try {
// We add the firmware to an LRU cache
val toDownloadParts = getFirmwareParts(request) val toDownloadParts = getFirmwareParts(request)
val firmwareParts = val firmwareParts = try {
firmwareCache.getOrPut(toDownloadParts.joinToString("|") { "${it.url}#${it.offset}" }) { withTimeoutOrNull(30_000) {
withTimeoutOrNull(30_000) { toDownloadParts.map {
toDownloadParts.map { val firmware = downloadFirmware(it.url, it.digest)
val firmware = downloadFirmware(it.url) DownloadedFirmwarePart(
?: error("unable to download firmware part") firmware,
DownloadedFirmwarePart( it.offset,
firmware, )
it.offset, }.toTypedArray()
)
}.toTypedArray()
}
} }
} catch (e: Exception) {
onStatusChange(
UpdateStatusEvent(
deviceId,
FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED,
),
)
LogManager.severe("[FirmwareUpdateHandler] Unable to download firmware", e)
return@coroutineScope
}
val job = launch { val job = launch {
withTimeout(2 * 60 * 1000) { 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() val outputStream = ByteArrayOutputStream()
try { val chunk = ByteArray(4096)
val chunk = ByteArray(4096) var bytesRead: Int
var bytesRead: Int val stream: InputStream = URL(url).openStream()
val stream: InputStream = URL(url).openStream() while (stream.read(chunk).also { bytesRead = it } > 0) {
while (stream.read(chunk).also { bytesRead = it } > 0) { outputStream.write(chunk, 0, bytesRead)
outputStream.write(chunk, 0, bytesRead)
}
} catch (e: IOException) {
error("Cant download firmware $url")
} }
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
} }