From 61b73b36419357352a395341430aebff831be828 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Mon, 3 Aug 2026 17:31:29 +0200 Subject: [PATCH] feat: add option to disable checksum verification --- app/assets/i18n/en.json | 6 +- app/lib/gen/strings.g.dart | 2 +- app/lib/gen/strings_en.g.dart | 6 ++ app/lib/model/state/settings_state.dart | 4 + .../model/state/settings_state.mapper.dart | 22 +++++ app/lib/pages/tabs/settings_tab.dart | 20 ++++ app/lib/provider/network/send_provider.dart | 93 ++++++++++--------- .../network/server/server_provider.dart | 1 + app/lib/provider/persistence_provider.dart | 18 ++++ app/lib/provider/settings_provider.dart | 16 ++++ app/test/mocks.mocks.dart | 36 +++++++ cli/src/app/mod.rs | 1 + packages/core/src/http/server/mod.rs | 9 ++ packages/core/src/http/server/v2.rs | 5 +- packages/core/tests/discovery.rs | 1 + packages/core/tests/v2_server.rs | 58 +++++++++++- packages/core/tests/v2_tls_pinning.rs | 1 + packages/core/tests/v2_web_send.rs | 2 + .../lib/rust/api/server.dart | 2 + .../lib/rust/frb_generated.dart | 7 +- .../lib/src/isolate/child/server_isolate.dart | 6 ++ .../lib/src/isolate/parent/actions.dart | 6 ++ .../lib/src/task/server/http_server.dart | 2 + .../localsend_isolates/rust/src/api/server.rs | 7 +- .../rust/src/frb_generated.rs | 2 + 25 files changed, 281 insertions(+), 52 deletions(-) diff --git a/app/assets/i18n/en.json b/app/assets/i18n/en.json index 40551d1d..938d3737 100644 --- a/app/assets/i18n/en.json +++ b/app/assets/i18n/en.json @@ -120,11 +120,13 @@ "destination": "Save to folder", "downloads": "(Downloads)", "saveToGallery": "Save media to gallery", - "saveToHistory": "Save to history" + "saveToHistory": "Save to history", + "verifyChecksums": "Verify checksums when receiving files" }, "send": { "title": "Send", - "shareViaLinkAutoAccept": "Automatically accept requests in \"Share via link\" mode" + "shareViaLinkAutoAccept": "Automatically accept requests in \"Share via link\" mode", + "createChecksums": "Create checksums when sending files" }, "network": { "title": "Network", diff --git a/app/lib/gen/strings.g.dart b/app/lib/gen/strings.g.dart index b6eaca2b..16fb89e1 100644 --- a/app/lib/gen/strings.g.dart +++ b/app/lib/gen/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 55 -/// Strings: 18451 (335 per locale) +/// Strings: 18453 (335 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/app/lib/gen/strings_en.g.dart b/app/lib/gen/strings_en.g.dart index c60e3926..6eeb6aa6 100644 --- a/app/lib/gen/strings_en.g.dart +++ b/app/lib/gen/strings_en.g.dart @@ -1088,6 +1088,9 @@ class Translations$settingsTab$receive$en { /// en: 'Save to history' String get saveToHistory => 'Save to history'; + + /// en: 'Verify checksums when receiving files' + String get verifyChecksums => 'Verify checksums when receiving files'; } // Path: settingsTab.send @@ -1103,6 +1106,9 @@ class Translations$settingsTab$send$en { /// en: 'Automatically accept requests in "Share via link" mode' String get shareViaLinkAutoAccept => 'Automatically accept requests in "Share via link" mode'; + + /// en: 'Create checksums when sending files' + String get createChecksums => 'Create checksums when sending files'; } // Path: settingsTab.network diff --git a/app/lib/model/state/settings_state.dart b/app/lib/model/state/settings_state.dart index 5155ba27..e81e1775 100644 --- a/app/lib/model/state/settings_state.dart +++ b/app/lib/model/state/settings_state.dart @@ -34,6 +34,8 @@ class SettingsState with SettingsStateMappable { final String? deviceModel; final bool shareViaLinkAutoAccept; final bool receiveViaLinkAutoAccept; + final bool createChecksums; // create checksums when sending files + final bool verifyChecksums; // verify checksums when receiving files final int discoveryTimeout; final bool advancedSettings; @@ -63,6 +65,8 @@ class SettingsState with SettingsStateMappable { required this.deviceModel, required this.shareViaLinkAutoAccept, required this.receiveViaLinkAutoAccept, + required this.createChecksums, + required this.verifyChecksums, required this.discoveryTimeout, required this.advancedSettings, }); diff --git a/app/lib/model/state/settings_state.mapper.dart b/app/lib/model/state/settings_state.mapper.dart index 4c107081..9f38b0d7 100644 --- a/app/lib/model/state/settings_state.mapper.dart +++ b/app/lib/model/state/settings_state.mapper.dart @@ -144,6 +144,16 @@ class SettingsStateMapper extends ClassMapperBase { 'receiveViaLinkAutoAccept', _$receiveViaLinkAutoAccept, ); + static bool _$createChecksums(SettingsState v) => v.createChecksums; + static const Field _f$createChecksums = Field( + 'createChecksums', + _$createChecksums, + ); + static bool _$verifyChecksums(SettingsState v) => v.verifyChecksums; + static const Field _f$verifyChecksums = Field( + 'verifyChecksums', + _$verifyChecksums, + ); static int _$discoveryTimeout(SettingsState v) => v.discoveryTimeout; static const Field _f$discoveryTimeout = Field( 'discoveryTimeout', @@ -182,6 +192,8 @@ class SettingsStateMapper extends ClassMapperBase { #deviceModel: _f$deviceModel, #shareViaLinkAutoAccept: _f$shareViaLinkAutoAccept, #receiveViaLinkAutoAccept: _f$receiveViaLinkAutoAccept, + #createChecksums: _f$createChecksums, + #verifyChecksums: _f$verifyChecksums, #discoveryTimeout: _f$discoveryTimeout, #advancedSettings: _f$advancedSettings, }; @@ -213,6 +225,8 @@ class SettingsStateMapper extends ClassMapperBase { deviceModel: data.dec(_f$deviceModel), shareViaLinkAutoAccept: data.dec(_f$shareViaLinkAutoAccept), receiveViaLinkAutoAccept: data.dec(_f$receiveViaLinkAutoAccept), + createChecksums: data.dec(_f$createChecksums), + verifyChecksums: data.dec(_f$verifyChecksums), discoveryTimeout: data.dec(_f$discoveryTimeout), advancedSettings: data.dec(_f$advancedSettings), ); @@ -310,6 +324,8 @@ abstract class SettingsStateCopyWith<$R, $In extends SettingsState, $Out> String? deviceModel, bool? shareViaLinkAutoAccept, bool? receiveViaLinkAutoAccept, + bool? createChecksums, + bool? verifyChecksums, int? discoveryTimeout, bool? advancedSettings, }); @@ -369,6 +385,8 @@ class _SettingsStateCopyWithImpl<$R, $Out> Object? deviceModel = $none, bool? shareViaLinkAutoAccept, bool? receiveViaLinkAutoAccept, + bool? createChecksums, + bool? verifyChecksums, int? discoveryTimeout, bool? advancedSettings, }) => $apply( @@ -402,6 +420,8 @@ class _SettingsStateCopyWithImpl<$R, $Out> #shareViaLinkAutoAccept: shareViaLinkAutoAccept, if (receiveViaLinkAutoAccept != null) #receiveViaLinkAutoAccept: receiveViaLinkAutoAccept, + if (createChecksums != null) #createChecksums: createChecksums, + if (verifyChecksums != null) #verifyChecksums: verifyChecksums, if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout, if (advancedSettings != null) #advancedSettings: advancedSettings, }), @@ -445,6 +465,8 @@ class _SettingsStateCopyWithImpl<$R, $Out> #receiveViaLinkAutoAccept, or: $value.receiveViaLinkAutoAccept, ), + createChecksums: data.get(#createChecksums, or: $value.createChecksums), + verifyChecksums: data.get(#verifyChecksums, or: $value.verifyChecksums), discoveryTimeout: data.get(#discoveryTimeout, or: $value.discoveryTimeout), advancedSettings: data.get(#advancedSettings, or: $value.advancedSettings), ); diff --git a/app/lib/pages/tabs/settings_tab.dart b/app/lib/pages/tabs/settings_tab.dart index bc39721e..6813ba44 100644 --- a/app/lib/pages/tabs/settings_tab.dart +++ b/app/lib/pages/tabs/settings_tab.dart @@ -259,6 +259,19 @@ class SettingsTab extends StatelessWidget { await ref.notifier(settingsProvider).setSaveToHistory(b); }, ), + if (vm.advanced) + _BooleanEntry( + label: t.settingsTab.receive.verifyChecksums, + value: vm.settings.verifyChecksums, + onChanged: (b) async { + await ref.notifier(settingsProvider).setVerifyChecksums(b); + + // The checksums are verified by the Rust server, so it needs a restart. + if (ref.read(serverProvider) != null) { + await ref.notifier(serverProvider).restartServerFromSettings(); + } + }, + ), ], ), if (vm.advanced) @@ -272,6 +285,13 @@ class SettingsTab extends StatelessWidget { await ref.notifier(settingsProvider).setShareViaLinkAutoAccept(b); }, ), + _BooleanEntry( + label: t.settingsTab.send.createChecksums, + value: vm.settings.createChecksums, + onChanged: (b) async { + await ref.notifier(settingsProvider).setCreateChecksums(b); + }, + ), ], ), _SettingsSection( diff --git a/app/lib/provider/network/send_provider.dart b/app/lib/provider/network/send_provider.dart index 230ac68b..6aa38b5c 100644 --- a/app/lib/provider/network/send_provider.dart +++ b/app/lib/provider/network/send_provider.dart @@ -74,6 +74,7 @@ class SendNotifier extends Notifier> { // if someone else answers on that address. final client = ref.read(httpProvider).pinnedTo(target.fingerprint); final sessionId = _uuid.v4(); + final createChecksums = ref.read(settingsProvider).createChecksums; // The ids are assigned upfront, so the checksums calculated below // can be mapped back to the corresponding file. @@ -116,7 +117,9 @@ class SendNotifier extends Notifier> { errorMessage: null, ), }, - hashedFileCount: 0, + // Skipping the checksums marks all files as hashed, so the UI does not + // show the checksum progress. + hashedFileCount: createChecksums ? 0 : selectedFiles.length, startTime: null, endTime: null, sendingTasks: [], @@ -134,54 +137,56 @@ class SendNotifier extends Notifier> { // Calculate the checksums which are part of the request. // The files are read and hashed in Rust, one file after another. - final hashCancelToken = rust_cancel.createCancellationToken(); - _hashCancelTokens[sessionId] = hashCancelToken; final hashes = {}; - try { - for (final (:id, :file) in selectedFiles) { - try { - hashes[id] = await calculateFileHash( - path: file.path, - bytes: file.bytes, - cancelToken: hashCancelToken, - onProgress: (bytes) { - if (state[sessionId] == null) { - // session has been canceled while calculating the checksums - return; - } - ref - .notifier(progressProvider) - .setProgress( - sessionId: sessionId, - fileId: id, - progress: file.size == 0 ? 1 : (bytes / file.size).clamp(0, 1), - ); - }, - ); - } catch (e) { - if (state[sessionId] != null) { - // Sending the checksum is optional, so a file that cannot be read - // here still gets a chance to be sent. - // Errors caused by the cancellation are not logged. - _logger.warning('Could not calculate the checksum of ${file.name}', e); + if (createChecksums) { + final hashCancelToken = rust_cancel.createCancellationToken(); + _hashCancelTokens[sessionId] = hashCancelToken; + try { + for (final (:id, :file) in selectedFiles) { + try { + hashes[id] = await calculateFileHash( + path: file.path, + bytes: file.bytes, + cancelToken: hashCancelToken, + onProgress: (bytes) { + if (state[sessionId] == null) { + // session has been canceled while calculating the checksums + return; + } + ref + .notifier(progressProvider) + .setProgress( + sessionId: sessionId, + fileId: id, + progress: file.size == 0 ? 1 : (bytes / file.size).clamp(0, 1), + ); + }, + ); + } catch (e) { + if (state[sessionId] != null) { + // Sending the checksum is optional, so a file that cannot be read + // here still gets a chance to be sent. + // Errors caused by the cancellation are not logged. + _logger.warning('Could not calculate the checksum of ${file.name}', e); + } } - } - if (state[sessionId] == null) { - // session has been canceled while calculating the checksums - return; - } + if (state[sessionId] == null) { + // session has been canceled while calculating the checksums + return; + } - // Also set for files whose hashing failed, so the progress bar stays - // consistent with the files that are left. - ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1); - state = state.updateSession( - sessionId: sessionId, - state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1), - ); + // Also set for files whose hashing failed, so the progress bar stays + // consistent with the files that are left. + ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1); + state = state.updateSession( + sessionId: sessionId, + state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1), + ); + } + } finally { + _hashCancelTokens.remove(sessionId); } - } finally { - _hashCancelTokens.remove(sessionId); } final hashedState = state[sessionId]; diff --git a/app/lib/provider/network/server/server_provider.dart b/app/lib/provider/network/server/server_provider.dart index 809932cb..bbc0a908 100644 --- a/app/lib/provider/network/server/server_provider.dart +++ b/app/lib/provider/network/server/server_provider.dart @@ -126,6 +126,7 @@ class ServerService extends Notifier { .dispatchTakeResult( IsolateHttpServerStartAction( pin: settings.receivePin, + verifyChecksums: settings.verifyChecksums, web: webSendState != null || webUpload ? WebParams( send: webSendState != null diff --git a/app/lib/provider/persistence_provider.dart b/app/lib/provider/persistence_provider.dart index d7b07357..8c4f1425 100644 --- a/app/lib/provider/persistence_provider.dart +++ b/app/lib/provider/persistence_provider.dart @@ -90,6 +90,8 @@ const _deviceType = 'ls_device_type'; const _deviceModel = 'ls_device_model'; const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept'; const _receiveViaLinkAutoAccept = 'ls_receive_via_link_auto_accept'; +const _createChecksums = 'ls_create_checksums'; +const _verifyChecksums = 'ls_verify_checksums'; const _advancedSettingsKey = 'ls_advanced_settings'; const _whatsNewKey = 'ls_whats_new'; @@ -377,6 +379,22 @@ class PersistenceService { await _prefs.setBool(_receiveViaLinkAutoAccept, receiveViaLinkAutoAccept); } + bool getCreateChecksums() { + return _prefs.getBool(_createChecksums) ?? true; + } + + Future setCreateChecksums(bool createChecksums) async { + await _prefs.setBool(_createChecksums, createChecksums); + } + + bool getVerifyChecksums() { + return _prefs.getBool(_verifyChecksums) ?? true; + } + + Future setVerifyChecksums(bool verifyChecksums) async { + await _prefs.setBool(_verifyChecksums, verifyChecksums); + } + String getMulticastGroup() { return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup; } diff --git a/app/lib/provider/settings_provider.dart b/app/lib/provider/settings_provider.dart index 3127abb5..eb1a9d7f 100644 --- a/app/lib/provider/settings_provider.dart +++ b/app/lib/provider/settings_provider.dart @@ -70,6 +70,8 @@ class SettingsService extends PureNotifier { deviceModel: _persistence.getDeviceModel(), shareViaLinkAutoAccept: _persistence.getShareViaLinkAutoAccept(), receiveViaLinkAutoAccept: _persistence.getReceiveViaLinkAutoAccept(), + createChecksums: _persistence.getCreateChecksums(), + verifyChecksums: _persistence.getVerifyChecksums(), discoveryTimeout: _persistence.getDiscoveryTimeout(), advancedSettings: _persistence.getAdvancedSettingsEnabled(), ); @@ -273,4 +275,18 @@ class SettingsService extends PureNotifier { receiveViaLinkAutoAccept: receiveViaLinkAutoAccept, ); } + + Future setCreateChecksums(bool createChecksums) async { + await _persistence.setCreateChecksums(createChecksums); + state = state.copyWith( + createChecksums: createChecksums, + ); + } + + Future setVerifyChecksums(bool verifyChecksums) async { + await _persistence.setVerifyChecksums(verifyChecksums); + state = state.copyWith( + verifyChecksums: verifyChecksums, + ); + } } diff --git a/app/test/mocks.mocks.dart b/app/test/mocks.mocks.dart index 27c7e037..a12d0dda 100644 --- a/app/test/mocks.mocks.dart +++ b/app/test/mocks.mocks.dart @@ -318,6 +318,42 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService ) as _i4.Future); + @override + bool getCreateChecksums() => + (super.noSuchMethod( + Invocation.method(#getCreateChecksums, []), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i4.Future setCreateChecksums(bool? createChecksums) => + (super.noSuchMethod( + Invocation.method(#setCreateChecksums, [createChecksums]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + bool getVerifyChecksums() => + (super.noSuchMethod( + Invocation.method(#getVerifyChecksums, []), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i4.Future setVerifyChecksums(bool? verifyChecksums) => + (super.noSuchMethod( + Invocation.method(#setVerifyChecksums, [verifyChecksums]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + @override String getMulticastGroup() => (super.noSuchMethod( diff --git a/cli/src/app/mod.rs b/cli/src/app/mod.rs index 588f15bf..8b23dcd8 100644 --- a/cli/src/app/mod.rs +++ b/cli/src/app/mod.rs @@ -95,6 +95,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> { None, Some(ServerConfigV2 { pin: None, + verify_checksums: true, event_tx: server_tx, }), None, diff --git a/packages/core/src/http/server/mod.rs b/packages/core/src/http/server/mod.rs index b371a24b..a8b39be9 100644 --- a/packages/core/src/http/server/mod.rs +++ b/packages/core/src/http/server/mod.rs @@ -39,6 +39,11 @@ pub struct ServerConfigV2 { /// Optional PIN that senders must provide via the `pin` query parameter. pub pin: Option, + /// 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, } @@ -48,6 +53,9 @@ pub(crate) struct V2State { /// Optional PIN required for prepare-upload requests. pub(crate) pin: Option, + /// 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, @@ -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())), diff --git a/packages/core/src/http/server/v2.rs b/packages/core/src/http/server/v2.rs index c83c8ce1..23eaf3d6 100644 --- a/packages/core/src/http/server/v2.rs +++ b/packages/core/src/http/server/v2.rs @@ -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(), diff --git a/packages/core/tests/discovery.rs b/packages/core/tests/discovery.rs index f2631a82..f3da4bc1 100644 --- a/packages/core/tests/discovery.rs +++ b/packages/core/tests/discovery.rs @@ -76,6 +76,7 @@ async fn start_register_server( None, Some(ServerConfigV2 { pin: None, + verify_checksums: true, event_tx, }), None, diff --git a/packages/core/tests/v2_server.rs b/packages/core/tests/v2_server.rs index 0e90d20c..c96be5e3 100644 --- a/packages/core/tests/v2_server.rs +++ b/packages/core/tests/v2_server.rs @@ -37,6 +37,16 @@ async fn start_test_server( pin: Option, accept: bool, save_dir: Option, +) -> 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, + accept: bool, + save_dir: Option, + 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, diff --git a/packages/core/tests/v2_tls_pinning.rs b/packages/core/tests/v2_tls_pinning.rs index f53b16f5..2ee10129 100644 --- a/packages/core/tests/v2_tls_pinning.rs +++ b/packages/core/tests/v2_tls_pinning.rs @@ -123,6 +123,7 @@ async fn start_tls_server_with_web(identity: &Identity, web: Option) None, Some(ServerConfigV2 { pin: None, + verify_checksums: true, event_tx, }), web, diff --git a/packages/core/tests/v2_web_send.rs b/packages/core/tests/v2_web_send.rs index 14508e73..3829f3e9 100644 --- a/packages/core/tests/v2_web_send.rs +++ b/packages/core/tests/v2_web_send.rs @@ -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 { diff --git a/packages/localsend_isolates/lib/rust/api/server.dart b/packages/localsend_isolates/lib/rust/api/server.dart index 63c6294a..61a8d935 100644 --- a/packages/localsend_isolates/lib/rust/api/server.dart +++ b/packages/localsend_isolates/lib/rust/api/server.dart @@ -34,6 +34,7 @@ Future startServer({ DeviceType? deviceType, required String fingerprint, String? pin, + required bool verifyChecksums, WebParams? web, String? showToken, }) => RustLib.instance.api.crateApiServerStartServer( @@ -45,6 +46,7 @@ Future startServer({ deviceType: deviceType, fingerprint: fingerprint, pin: pin, + verifyChecksums: verifyChecksums, web: web, showToken: showToken, ); diff --git a/packages/localsend_isolates/lib/rust/frb_generated.dart b/packages/localsend_isolates/lib/rust/frb_generated.dart index fb083c46..ee877322 100644 --- a/packages/localsend_isolates/lib/rust/frb_generated.dart +++ b/packages/localsend_isolates/lib/rust/frb_generated.dart @@ -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 diff --git a/packages/localsend_isolates/lib/src/isolate/child/server_isolate.dart b/packages/localsend_isolates/lib/src/isolate/child/server_isolate.dart index 0c4e5dbc..452578e5 100644 --- a/packages/localsend_isolates/lib/src/isolate/child/server_isolate.dart +++ b/packages/localsend_isolates/lib/src/isolate/child/server_isolate.dart @@ -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 setupHttpServerIsolate( deviceType: syncState.deviceInfo.deviceType.toRust(), fingerprint: syncState.securityContext.certificateHash, pin: startTask.pin, + verifyChecksums: startTask.verifyChecksums, web: startTask.web, showToken: startTask.showToken, ); diff --git a/packages/localsend_isolates/lib/src/isolate/parent/actions.dart b/packages/localsend_isolates/lib/src/isolate/parent/actions.dart index d9e015d0..09e332b6 100644 --- a/packages/localsend_isolates/lib/src/isolate/parent/actions.dart +++ b/packages/localsend_isolates/lib/src/isolate/parent/actions.dart @@ -256,6 +256,10 @@ class IsolateHttpUploadCancelAction extends ReduxAction> { 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, fingerprint: String, pin: Option, + verify_checksums: bool, web: Option, show_token: Option, ) -> anyhow::Result { @@ -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, ) diff --git a/packages/localsend_isolates/rust/src/frb_generated.rs b/packages/localsend_isolates/rust/src/frb_generated.rs index 2cff37d6..d3310369 100644 --- a/packages/localsend_isolates/rust/src/frb_generated.rs +++ b/packages/localsend_isolates/rust/src/frb_generated.rs @@ -3201,6 +3201,7 @@ fn wire__crate__api__server__start_server_impl( >::sse_decode(&mut deserializer); let api_fingerprint = ::sse_decode(&mut deserializer); let api_pin = >::sse_decode(&mut deserializer); + let api_verify_checksums = ::sse_decode(&mut deserializer); let api_web = >::sse_decode(&mut deserializer); let api_show_token = >::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, )