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.source !== partialBoard.source,
name: source.version,
isBranch: !!source.branch,
isBranch: source.branch == source.version,
});
return curr;

View File

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

View File

@@ -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;
}[];

View File

@@ -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;
});

View File

@@ -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,
});

View File

@@ -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
}