feat: add option to disable checksum verification
CI / format (push) Has been cancelled
CI / test (push) Has been cancelled
CI / packaging (push) Has been cancelled

This commit is contained in:
Tien Do Nam
2026-08-03 17:31:29 +02:00
parent c789295a47
commit 61b73b3641
25 changed files with 281 additions and 52 deletions
+9
View File
@@ -39,6 +39,11 @@ pub struct ServerConfigV2 {
/// Optional PIN that senders must provide via the `pin` query parameter.
pub pin: Option<String>,
/// Whether the SHA-256 checksums that senders provide for their files are
/// verified after receiving. When disabled, received files are not hashed
/// and a mismatch is not detected.
pub verify_checksums: bool,
/// Channel on which the server emits events that must be handled by the application.
pub event_tx: mpsc::Sender<ServerEventV2>,
}
@@ -48,6 +53,9 @@ pub(crate) struct V2State {
/// Optional PIN required for prepare-upload requests.
pub(crate) pin: Option<String>,
/// Whether sender-provided SHA-256 checksums are verified after receiving.
pub(crate) verify_checksums: bool,
/// Channel on which server events are emitted to the application.
pub(crate) event_tx: mpsc::Sender<ServerEventV2>,
@@ -95,6 +103,7 @@ impl AppState {
let v2 = v2_config.map(|config| {
Arc::new(V2State {
pin: config.pin,
verify_checksums: config.verify_checksums,
event_tx: config.event_tx,
session: Mutex::new(None),
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
+4 -1
View File
@@ -385,7 +385,10 @@ pub(crate) async fn upload(
let mut upload_guard = UploadGuard::new(v2.clone(), session_id.clone(), file_id.clone());
let file_size = file_dto.size;
let expected_sha256 = file_dto.sha256.clone();
let expected_sha256 = match v2.verify_checksums {
true => file_dto.sha256.clone(),
false => None,
};
let timestamps = match &file_dto.metadata {
Some(metadata) => FileTimestamps {
modified: metadata.modified_time(),
+1
View File
@@ -76,6 +76,7 @@ async fn start_register_server(
None,
Some(ServerConfigV2 {
pin: None,
verify_checksums: true,
event_tx,
}),
None,
+57 -1
View File
@@ -37,6 +37,16 @@ async fn start_test_server(
pin: Option<String>,
accept: bool,
save_dir: Option<PathBuf>,
) -> TestServer {
start_test_server_with_verification(pin, accept, save_dir, true).await
}
/// Like [start_test_server], but allows disabling the checksum verification.
async fn start_test_server_with_verification(
pin: Option<String>,
accept: bool,
save_dir: Option<PathBuf>,
verify_checksums: bool,
) -> TestServer {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let port = free_port();
@@ -125,7 +135,11 @@ async fn start_test_server(
token: "server-fingerprint".to_string(),
},
None,
Some(ServerConfigV2 { pin, event_tx }),
Some(ServerConfigV2 {
pin,
verify_checksums,
event_tx,
}),
None,
stop_rx,
)
@@ -499,6 +513,45 @@ async fn test_upload_with_mismatched_sha256() {
assert_status(result, 422);
}
#[tokio::test]
async fn test_upload_mismatched_sha256_with_verification_disabled() {
let server = start_test_server_with_verification(None, true, None, false).await;
let client = LsHttpClientV2::try_new_without_cert().unwrap();
let bytes = b"hello".to_vec();
let mut file = file_dto("file-a", "a.bin", bytes.len() as u64);
file.sha256 = Some(sha256_hex(b"something else"));
let response = client
.prepare_upload(
ProtocolType::Http,
"127.0.0.1",
server.port,
None,
prepare_upload_request(&[file]),
None,
CancellationToken::new(),
)
.await
.unwrap()
.response
.unwrap();
// The mismatch goes unnoticed because the received bytes are not hashed.
upload_bytes(
&client,
server.port,
&response.session_id,
"file-a",
&response.files["file-a"],
&bytes,
)
.await
.unwrap();
assert_eq!(server.received.lock().await["file-a"], bytes);
}
#[tokio::test]
async fn test_upload_retry_after_mismatched_sha256() {
let server = start_test_server(None, true, None).await;
@@ -927,6 +980,7 @@ async fn test_prepare_upload_aborted_by_sender_disconnect() {
None,
Some(ServerConfigV2 {
pin: None,
verify_checksums: true,
event_tx,
}),
None,
@@ -1029,6 +1083,7 @@ async fn test_prepare_upload_cancelled_by_session_less_cancel() {
None,
Some(ServerConfigV2 {
pin: None,
verify_checksums: true,
event_tx,
}),
None,
@@ -1164,6 +1219,7 @@ async fn test_prepare_upload_aborted_by_sender_disconnect_tls() {
None,
Some(ServerConfigV2 {
pin: None,
verify_checksums: true,
event_tx,
}),
None,
+1
View File
@@ -123,6 +123,7 @@ async fn start_tls_server_with_web(identity: &Identity, web: Option<WebConfig>)
None,
Some(ServerConfigV2 {
pin: None,
verify_checksums: true,
event_tx,
}),
web,
+2
View File
@@ -140,6 +140,7 @@ async fn start_test_server(
None,
Some(ServerConfigV2 {
pin: None,
verify_checksums: true,
event_tx: v2_event_tx,
}),
web_config,
@@ -340,6 +341,7 @@ async fn test_upload_page() {
None,
Some(ServerConfigV2 {
pin: None,
verify_checksums: true,
event_tx: v2_event_tx,
}),
Some(WebConfig {
@@ -34,6 +34,7 @@ Future<RsHttpServer> startServer({
DeviceType? deviceType,
required String fingerprint,
String? pin,
required bool verifyChecksums,
WebParams? web,
String? showToken,
}) => RustLib.instance.api.crateApiServerStartServer(
@@ -45,6 +46,7 @@ Future<RsHttpServer> startServer({
deviceType: deviceType,
fingerprint: fingerprint,
pin: pin,
verifyChecksums: verifyChecksums,
web: web,
showToken: showToken,
);
@@ -302,6 +302,7 @@ abstract class RustLibApi extends BaseApi {
DeviceType? deviceType,
required String fingerprint,
String? pin,
required bool verifyChecksums,
WebParams? web,
String? showToken,
});
@@ -2043,6 +2044,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
DeviceType? deviceType,
required String fingerprint,
String? pin,
required bool verifyChecksums,
WebParams? web,
String? showToken,
}) {
@@ -2058,6 +2060,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_opt_box_autoadd_device_type(deviceType, serializer);
sse_encode_String(fingerprint, serializer);
sse_encode_opt_String(pin, serializer);
sse_encode_bool(verifyChecksums, serializer);
sse_encode_opt_box_autoadd_web_params(web, serializer);
sse_encode_opt_String(showToken, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_);
@@ -2067,7 +2070,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateApiServerStartServerConstMeta,
argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin, web, showToken],
argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin, verifyChecksums, web, showToken],
apiImpl: this,
),
);
@@ -2075,7 +2078,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta(
debugName: 'start_server',
argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin', 'web', 'showToken'],
argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin', 'verifyChecksums', 'web', 'showToken'],
);
@override
@@ -30,6 +30,10 @@ class HttpServerStartTask implements BaseHttpServerTask {
/// Optional PIN that senders must provide to start an upload session.
final String? pin;
/// Whether the SHA-256 checksums that senders provide for their files are
/// verified after receiving.
final bool verifyChecksums;
/// Serves the web pages: the download page (web send) and/or the upload page.
/// `null` disables the web pages.
final WebParams? web;
@@ -40,6 +44,7 @@ class HttpServerStartTask implements BaseHttpServerTask {
HttpServerStartTask({
required this.pin,
required this.verifyChecksums,
required this.web,
required this.showToken,
});
@@ -398,6 +403,7 @@ Future<void> setupHttpServerIsolate(
deviceType: syncState.deviceInfo.deviceType.toRust(),
fingerprint: syncState.securityContext.certificateHash,
pin: startTask.pin,
verifyChecksums: startTask.verifyChecksums,
web: startTask.web,
showToken: startTask.showToken,
);
@@ -256,6 +256,10 @@ class IsolateHttpUploadCancelAction extends ReduxAction<IsolateController, Paren
class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> {
final String? pin;
/// Whether the SHA-256 checksums that senders provide for their files are
/// verified after receiving.
final bool verifyChecksums;
/// Serves the web pages: the download page (web send) and/or the upload page.
/// `null` disables the web pages.
final WebParams? web;
@@ -266,6 +270,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
IsolateHttpServerStartAction({
required this.pin,
required this.verifyChecksums,
required this.web,
required this.showToken,
});
@@ -282,6 +287,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
connection.sendWrappedTaskAndListenStream(
task: HttpServerStartTask(
pin: pin,
verifyChecksums: verifyChecksums,
web: web,
showToken: showToken,
),
@@ -22,6 +22,7 @@ class HttpServerService {
required DeviceType? deviceType,
required String fingerprint,
required String? pin,
required bool verifyChecksums,
required WebParams? web,
required String? showToken,
}) async {
@@ -38,6 +39,7 @@ class HttpServerService {
deviceType: deviceType,
fingerprint: fingerprint,
pin: pin,
verifyChecksums: verifyChecksums,
web: web,
showToken: showToken,
);
@@ -183,6 +183,7 @@ pub async fn start_server(
device_type: Option<DeviceType>,
fingerprint: String,
pin: Option<String>,
verify_checksums: bool,
web: Option<WebParams>,
show_token: Option<String>,
) -> anyhow::Result<RsHttpServer> {
@@ -243,7 +244,11 @@ pub async fn start_server(
token: fingerprint,
},
internal_config,
Some(ServerConfigV2 { pin, event_tx }),
Some(ServerConfigV2 {
pin,
verify_checksums,
event_tx,
}),
web_config,
stop_rx,
)
@@ -3201,6 +3201,7 @@ fn wire__crate__api__server__start_server_impl(
<Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer);
let api_fingerprint = <String>::sse_decode(&mut deserializer);
let api_pin = <Option<String>>::sse_decode(&mut deserializer);
let api_verify_checksums = <bool>::sse_decode(&mut deserializer);
let api_web = <Option<crate::api::server::WebParams>>::sse_decode(&mut deserializer);
let api_show_token = <Option<String>>::sse_decode(&mut deserializer);
deserializer.end();
@@ -3216,6 +3217,7 @@ fn wire__crate__api__server__start_server_impl(
api_device_type,
api_fingerprint,
api_pin,
api_verify_checksums,
api_web,
api_show_token,
)