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.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;
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule solarxr-protocol updated: eed73567f7...2fe6f9cf8d
Reference in New Issue
Block a user