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
+4 -2
View File
@@ -120,11 +120,13 @@
"destination": "Save to folder", "destination": "Save to folder",
"downloads": "(Downloads)", "downloads": "(Downloads)",
"saveToGallery": "Save media to gallery", "saveToGallery": "Save media to gallery",
"saveToHistory": "Save to history" "saveToHistory": "Save to history",
"verifyChecksums": "Verify checksums when receiving files"
}, },
"send": { "send": {
"title": "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": { "network": {
"title": "Network", "title": "Network",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 55 /// Locales: 55
/// Strings: 18451 (335 per locale) /// Strings: 18453 (335 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+6
View File
@@ -1088,6 +1088,9 @@ class Translations$settingsTab$receive$en {
/// en: 'Save to history' /// en: 'Save to history'
String get saveToHistory => '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 // Path: settingsTab.send
@@ -1103,6 +1106,9 @@ class Translations$settingsTab$send$en {
/// en: 'Automatically accept requests in "Share via link" mode' /// en: 'Automatically accept requests in "Share via link" mode'
String get shareViaLinkAutoAccept => '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 // Path: settingsTab.network
+4
View File
@@ -34,6 +34,8 @@ class SettingsState with SettingsStateMappable {
final String? deviceModel; final String? deviceModel;
final bool shareViaLinkAutoAccept; final bool shareViaLinkAutoAccept;
final bool receiveViaLinkAutoAccept; final bool receiveViaLinkAutoAccept;
final bool createChecksums; // create checksums when sending files
final bool verifyChecksums; // verify checksums when receiving files
final int discoveryTimeout; final int discoveryTimeout;
final bool advancedSettings; final bool advancedSettings;
@@ -63,6 +65,8 @@ class SettingsState with SettingsStateMappable {
required this.deviceModel, required this.deviceModel,
required this.shareViaLinkAutoAccept, required this.shareViaLinkAutoAccept,
required this.receiveViaLinkAutoAccept, required this.receiveViaLinkAutoAccept,
required this.createChecksums,
required this.verifyChecksums,
required this.discoveryTimeout, required this.discoveryTimeout,
required this.advancedSettings, required this.advancedSettings,
}); });
@@ -144,6 +144,16 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
'receiveViaLinkAutoAccept', 'receiveViaLinkAutoAccept',
_$receiveViaLinkAutoAccept, _$receiveViaLinkAutoAccept,
); );
static bool _$createChecksums(SettingsState v) => v.createChecksums;
static const Field<SettingsState, bool> _f$createChecksums = Field(
'createChecksums',
_$createChecksums,
);
static bool _$verifyChecksums(SettingsState v) => v.verifyChecksums;
static const Field<SettingsState, bool> _f$verifyChecksums = Field(
'verifyChecksums',
_$verifyChecksums,
);
static int _$discoveryTimeout(SettingsState v) => v.discoveryTimeout; static int _$discoveryTimeout(SettingsState v) => v.discoveryTimeout;
static const Field<SettingsState, int> _f$discoveryTimeout = Field( static const Field<SettingsState, int> _f$discoveryTimeout = Field(
'discoveryTimeout', 'discoveryTimeout',
@@ -182,6 +192,8 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
#deviceModel: _f$deviceModel, #deviceModel: _f$deviceModel,
#shareViaLinkAutoAccept: _f$shareViaLinkAutoAccept, #shareViaLinkAutoAccept: _f$shareViaLinkAutoAccept,
#receiveViaLinkAutoAccept: _f$receiveViaLinkAutoAccept, #receiveViaLinkAutoAccept: _f$receiveViaLinkAutoAccept,
#createChecksums: _f$createChecksums,
#verifyChecksums: _f$verifyChecksums,
#discoveryTimeout: _f$discoveryTimeout, #discoveryTimeout: _f$discoveryTimeout,
#advancedSettings: _f$advancedSettings, #advancedSettings: _f$advancedSettings,
}; };
@@ -213,6 +225,8 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
deviceModel: data.dec(_f$deviceModel), deviceModel: data.dec(_f$deviceModel),
shareViaLinkAutoAccept: data.dec(_f$shareViaLinkAutoAccept), shareViaLinkAutoAccept: data.dec(_f$shareViaLinkAutoAccept),
receiveViaLinkAutoAccept: data.dec(_f$receiveViaLinkAutoAccept), receiveViaLinkAutoAccept: data.dec(_f$receiveViaLinkAutoAccept),
createChecksums: data.dec(_f$createChecksums),
verifyChecksums: data.dec(_f$verifyChecksums),
discoveryTimeout: data.dec(_f$discoveryTimeout), discoveryTimeout: data.dec(_f$discoveryTimeout),
advancedSettings: data.dec(_f$advancedSettings), advancedSettings: data.dec(_f$advancedSettings),
); );
@@ -310,6 +324,8 @@ abstract class SettingsStateCopyWith<$R, $In extends SettingsState, $Out>
String? deviceModel, String? deviceModel,
bool? shareViaLinkAutoAccept, bool? shareViaLinkAutoAccept,
bool? receiveViaLinkAutoAccept, bool? receiveViaLinkAutoAccept,
bool? createChecksums,
bool? verifyChecksums,
int? discoveryTimeout, int? discoveryTimeout,
bool? advancedSettings, bool? advancedSettings,
}); });
@@ -369,6 +385,8 @@ class _SettingsStateCopyWithImpl<$R, $Out>
Object? deviceModel = $none, Object? deviceModel = $none,
bool? shareViaLinkAutoAccept, bool? shareViaLinkAutoAccept,
bool? receiveViaLinkAutoAccept, bool? receiveViaLinkAutoAccept,
bool? createChecksums,
bool? verifyChecksums,
int? discoveryTimeout, int? discoveryTimeout,
bool? advancedSettings, bool? advancedSettings,
}) => $apply( }) => $apply(
@@ -402,6 +420,8 @@ class _SettingsStateCopyWithImpl<$R, $Out>
#shareViaLinkAutoAccept: shareViaLinkAutoAccept, #shareViaLinkAutoAccept: shareViaLinkAutoAccept,
if (receiveViaLinkAutoAccept != null) if (receiveViaLinkAutoAccept != null)
#receiveViaLinkAutoAccept: receiveViaLinkAutoAccept, #receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
if (createChecksums != null) #createChecksums: createChecksums,
if (verifyChecksums != null) #verifyChecksums: verifyChecksums,
if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout, if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout,
if (advancedSettings != null) #advancedSettings: advancedSettings, if (advancedSettings != null) #advancedSettings: advancedSettings,
}), }),
@@ -445,6 +465,8 @@ class _SettingsStateCopyWithImpl<$R, $Out>
#receiveViaLinkAutoAccept, #receiveViaLinkAutoAccept,
or: $value.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), discoveryTimeout: data.get(#discoveryTimeout, or: $value.discoveryTimeout),
advancedSettings: data.get(#advancedSettings, or: $value.advancedSettings), advancedSettings: data.get(#advancedSettings, or: $value.advancedSettings),
); );
+20
View File
@@ -259,6 +259,19 @@ class SettingsTab extends StatelessWidget {
await ref.notifier(settingsProvider).setSaveToHistory(b); 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) if (vm.advanced)
@@ -272,6 +285,13 @@ class SettingsTab extends StatelessWidget {
await ref.notifier(settingsProvider).setShareViaLinkAutoAccept(b); 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( _SettingsSection(
+49 -44
View File
@@ -74,6 +74,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
// if someone else answers on that address. // if someone else answers on that address.
final client = ref.read(httpProvider).pinnedTo(target.fingerprint); final client = ref.read(httpProvider).pinnedTo(target.fingerprint);
final sessionId = _uuid.v4(); final sessionId = _uuid.v4();
final createChecksums = ref.read(settingsProvider).createChecksums;
// The ids are assigned upfront, so the checksums calculated below // The ids are assigned upfront, so the checksums calculated below
// can be mapped back to the corresponding file. // can be mapped back to the corresponding file.
@@ -116,7 +117,9 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
errorMessage: null, 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, startTime: null,
endTime: null, endTime: null,
sendingTasks: [], sendingTasks: [],
@@ -134,54 +137,56 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
// Calculate the checksums which are part of the request. // Calculate the checksums which are part of the request.
// The files are read and hashed in Rust, one file after another. // The files are read and hashed in Rust, one file after another.
final hashCancelToken = rust_cancel.createCancellationToken();
_hashCancelTokens[sessionId] = hashCancelToken;
final hashes = <String, String>{}; final hashes = <String, String>{};
try { if (createChecksums) {
for (final (:id, :file) in selectedFiles) { final hashCancelToken = rust_cancel.createCancellationToken();
try { _hashCancelTokens[sessionId] = hashCancelToken;
hashes[id] = await calculateFileHash( try {
path: file.path, for (final (:id, :file) in selectedFiles) {
bytes: file.bytes, try {
cancelToken: hashCancelToken, hashes[id] = await calculateFileHash(
onProgress: (bytes) { path: file.path,
if (state[sessionId] == null) { bytes: file.bytes,
// session has been canceled while calculating the checksums cancelToken: hashCancelToken,
return; onProgress: (bytes) {
} if (state[sessionId] == null) {
ref // session has been canceled while calculating the checksums
.notifier(progressProvider) return;
.setProgress( }
sessionId: sessionId, ref
fileId: id, .notifier(progressProvider)
progress: file.size == 0 ? 1 : (bytes / file.size).clamp(0, 1), .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. } catch (e) {
// Errors caused by the cancellation are not logged. if (state[sessionId] != null) {
_logger.warning('Could not calculate the checksum of ${file.name}', e); // 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) { if (state[sessionId] == null) {
// session has been canceled while calculating the checksums // session has been canceled while calculating the checksums
return; return;
} }
// Also set for files whose hashing failed, so the progress bar stays // Also set for files whose hashing failed, so the progress bar stays
// consistent with the files that are left. // consistent with the files that are left.
ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1); ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1);
state = state.updateSession( state = state.updateSession(
sessionId: sessionId, sessionId: sessionId,
state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1), state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1),
); );
}
} finally {
_hashCancelTokens.remove(sessionId);
} }
} finally {
_hashCancelTokens.remove(sessionId);
} }
final hashedState = state[sessionId]; final hashedState = state[sessionId];
@@ -126,6 +126,7 @@ class ServerService extends Notifier<ServerState?> {
.dispatchTakeResult( .dispatchTakeResult(
IsolateHttpServerStartAction( IsolateHttpServerStartAction(
pin: settings.receivePin, pin: settings.receivePin,
verifyChecksums: settings.verifyChecksums,
web: webSendState != null || webUpload web: webSendState != null || webUpload
? WebParams( ? WebParams(
send: webSendState != null send: webSendState != null
@@ -90,6 +90,8 @@ const _deviceType = 'ls_device_type';
const _deviceModel = 'ls_device_model'; const _deviceModel = 'ls_device_model';
const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept'; const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept';
const _receiveViaLinkAutoAccept = 'ls_receive_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 _advancedSettingsKey = 'ls_advanced_settings';
const _whatsNewKey = 'ls_whats_new'; const _whatsNewKey = 'ls_whats_new';
@@ -377,6 +379,22 @@ class PersistenceService {
await _prefs.setBool(_receiveViaLinkAutoAccept, receiveViaLinkAutoAccept); await _prefs.setBool(_receiveViaLinkAutoAccept, receiveViaLinkAutoAccept);
} }
bool getCreateChecksums() {
return _prefs.getBool(_createChecksums) ?? true;
}
Future<void> setCreateChecksums(bool createChecksums) async {
await _prefs.setBool(_createChecksums, createChecksums);
}
bool getVerifyChecksums() {
return _prefs.getBool(_verifyChecksums) ?? true;
}
Future<void> setVerifyChecksums(bool verifyChecksums) async {
await _prefs.setBool(_verifyChecksums, verifyChecksums);
}
String getMulticastGroup() { String getMulticastGroup() {
return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup; return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup;
} }
+16
View File
@@ -70,6 +70,8 @@ class SettingsService extends PureNotifier<SettingsState> {
deviceModel: _persistence.getDeviceModel(), deviceModel: _persistence.getDeviceModel(),
shareViaLinkAutoAccept: _persistence.getShareViaLinkAutoAccept(), shareViaLinkAutoAccept: _persistence.getShareViaLinkAutoAccept(),
receiveViaLinkAutoAccept: _persistence.getReceiveViaLinkAutoAccept(), receiveViaLinkAutoAccept: _persistence.getReceiveViaLinkAutoAccept(),
createChecksums: _persistence.getCreateChecksums(),
verifyChecksums: _persistence.getVerifyChecksums(),
discoveryTimeout: _persistence.getDiscoveryTimeout(), discoveryTimeout: _persistence.getDiscoveryTimeout(),
advancedSettings: _persistence.getAdvancedSettingsEnabled(), advancedSettings: _persistence.getAdvancedSettingsEnabled(),
); );
@@ -273,4 +275,18 @@ class SettingsService extends PureNotifier<SettingsState> {
receiveViaLinkAutoAccept: receiveViaLinkAutoAccept, receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
); );
} }
Future<void> setCreateChecksums(bool createChecksums) async {
await _persistence.setCreateChecksums(createChecksums);
state = state.copyWith(
createChecksums: createChecksums,
);
}
Future<void> setVerifyChecksums(bool verifyChecksums) async {
await _persistence.setVerifyChecksums(verifyChecksums);
state = state.copyWith(
verifyChecksums: verifyChecksums,
);
}
} }
+36
View File
@@ -318,6 +318,42 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService
) )
as _i4.Future<void>); as _i4.Future<void>);
@override
bool getCreateChecksums() =>
(super.noSuchMethod(
Invocation.method(#getCreateChecksums, []),
returnValue: false,
returnValueForMissingStub: false,
)
as bool);
@override
_i4.Future<void> setCreateChecksums(bool? createChecksums) =>
(super.noSuchMethod(
Invocation.method(#setCreateChecksums, [createChecksums]),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
)
as _i4.Future<void>);
@override
bool getVerifyChecksums() =>
(super.noSuchMethod(
Invocation.method(#getVerifyChecksums, []),
returnValue: false,
returnValueForMissingStub: false,
)
as bool);
@override
_i4.Future<void> setVerifyChecksums(bool? verifyChecksums) =>
(super.noSuchMethod(
Invocation.method(#setVerifyChecksums, [verifyChecksums]),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
)
as _i4.Future<void>);
@override @override
String getMulticastGroup() => String getMulticastGroup() =>
(super.noSuchMethod( (super.noSuchMethod(
+1
View File
@@ -95,6 +95,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx: server_tx, event_tx: server_tx,
}), }),
None, None,
+9
View File
@@ -39,6 +39,11 @@ pub struct ServerConfigV2 {
/// Optional PIN that senders must provide via the `pin` query parameter. /// Optional PIN that senders must provide via the `pin` query parameter.
pub pin: Option<String>, 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. /// Channel on which the server emits events that must be handled by the application.
pub event_tx: mpsc::Sender<ServerEventV2>, pub event_tx: mpsc::Sender<ServerEventV2>,
} }
@@ -48,6 +53,9 @@ pub(crate) struct V2State {
/// Optional PIN required for prepare-upload requests. /// Optional PIN required for prepare-upload requests.
pub(crate) pin: Option<String>, 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. /// Channel on which server events are emitted to the application.
pub(crate) event_tx: mpsc::Sender<ServerEventV2>, pub(crate) event_tx: mpsc::Sender<ServerEventV2>,
@@ -95,6 +103,7 @@ impl AppState {
let v2 = v2_config.map(|config| { let v2 = v2_config.map(|config| {
Arc::new(V2State { Arc::new(V2State {
pin: config.pin, pin: config.pin,
verify_checksums: config.verify_checksums,
event_tx: config.event_tx, event_tx: config.event_tx,
session: Mutex::new(None), session: Mutex::new(None),
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())), 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 mut upload_guard = UploadGuard::new(v2.clone(), session_id.clone(), file_id.clone());
let file_size = file_dto.size; 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 { let timestamps = match &file_dto.metadata {
Some(metadata) => FileTimestamps { Some(metadata) => FileTimestamps {
modified: metadata.modified_time(), modified: metadata.modified_time(),
+1
View File
@@ -76,6 +76,7 @@ async fn start_register_server(
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx, event_tx,
}), }),
None, None,
+57 -1
View File
@@ -37,6 +37,16 @@ async fn start_test_server(
pin: Option<String>, pin: Option<String>,
accept: bool, accept: bool,
save_dir: Option<PathBuf>, 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 { ) -> TestServer {
let _ = tracing_subscriber::fmt().with_test_writer().try_init(); let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let port = free_port(); let port = free_port();
@@ -125,7 +135,11 @@ async fn start_test_server(
token: "server-fingerprint".to_string(), token: "server-fingerprint".to_string(),
}, },
None, None,
Some(ServerConfigV2 { pin, event_tx }), Some(ServerConfigV2 {
pin,
verify_checksums,
event_tx,
}),
None, None,
stop_rx, stop_rx,
) )
@@ -499,6 +513,45 @@ async fn test_upload_with_mismatched_sha256() {
assert_status(result, 422); 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] #[tokio::test]
async fn test_upload_retry_after_mismatched_sha256() { async fn test_upload_retry_after_mismatched_sha256() {
let server = start_test_server(None, true, None).await; let server = start_test_server(None, true, None).await;
@@ -927,6 +980,7 @@ async fn test_prepare_upload_aborted_by_sender_disconnect() {
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx, event_tx,
}), }),
None, None,
@@ -1029,6 +1083,7 @@ async fn test_prepare_upload_cancelled_by_session_less_cancel() {
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx, event_tx,
}), }),
None, None,
@@ -1164,6 +1219,7 @@ async fn test_prepare_upload_aborted_by_sender_disconnect_tls() {
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx, event_tx,
}), }),
None, None,
+1
View File
@@ -123,6 +123,7 @@ async fn start_tls_server_with_web(identity: &Identity, web: Option<WebConfig>)
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx, event_tx,
}), }),
web, web,
+2
View File
@@ -140,6 +140,7 @@ async fn start_test_server(
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx: v2_event_tx, event_tx: v2_event_tx,
}), }),
web_config, web_config,
@@ -340,6 +341,7 @@ async fn test_upload_page() {
None, None,
Some(ServerConfigV2 { Some(ServerConfigV2 {
pin: None, pin: None,
verify_checksums: true,
event_tx: v2_event_tx, event_tx: v2_event_tx,
}), }),
Some(WebConfig { Some(WebConfig {
@@ -34,6 +34,7 @@ Future<RsHttpServer> startServer({
DeviceType? deviceType, DeviceType? deviceType,
required String fingerprint, required String fingerprint,
String? pin, String? pin,
required bool verifyChecksums,
WebParams? web, WebParams? web,
String? showToken, String? showToken,
}) => RustLib.instance.api.crateApiServerStartServer( }) => RustLib.instance.api.crateApiServerStartServer(
@@ -45,6 +46,7 @@ Future<RsHttpServer> startServer({
deviceType: deviceType, deviceType: deviceType,
fingerprint: fingerprint, fingerprint: fingerprint,
pin: pin, pin: pin,
verifyChecksums: verifyChecksums,
web: web, web: web,
showToken: showToken, showToken: showToken,
); );
@@ -302,6 +302,7 @@ abstract class RustLibApi extends BaseApi {
DeviceType? deviceType, DeviceType? deviceType,
required String fingerprint, required String fingerprint,
String? pin, String? pin,
required bool verifyChecksums,
WebParams? web, WebParams? web,
String? showToken, String? showToken,
}); });
@@ -2043,6 +2044,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
DeviceType? deviceType, DeviceType? deviceType,
required String fingerprint, required String fingerprint,
String? pin, String? pin,
required bool verifyChecksums,
WebParams? web, WebParams? web,
String? showToken, String? showToken,
}) { }) {
@@ -2058,6 +2060,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_opt_box_autoadd_device_type(deviceType, serializer); sse_encode_opt_box_autoadd_device_type(deviceType, serializer);
sse_encode_String(fingerprint, serializer); sse_encode_String(fingerprint, serializer);
sse_encode_opt_String(pin, serializer); sse_encode_opt_String(pin, serializer);
sse_encode_bool(verifyChecksums, serializer);
sse_encode_opt_box_autoadd_web_params(web, serializer); sse_encode_opt_box_autoadd_web_params(web, serializer);
sse_encode_opt_String(showToken, serializer); sse_encode_opt_String(showToken, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_); pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_);
@@ -2067,7 +2070,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException, decodeErrorData: sse_decode_AnyhowException,
), ),
constMeta: kCrateApiServerStartServerConstMeta, 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, apiImpl: this,
), ),
); );
@@ -2075,7 +2078,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta( TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta(
debugName: 'start_server', 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 @override
@@ -30,6 +30,10 @@ class HttpServerStartTask implements BaseHttpServerTask {
/// Optional PIN that senders must provide to start an upload session. /// Optional PIN that senders must provide to start an upload session.
final String? pin; 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. /// Serves the web pages: the download page (web send) and/or the upload page.
/// `null` disables the web pages. /// `null` disables the web pages.
final WebParams? web; final WebParams? web;
@@ -40,6 +44,7 @@ class HttpServerStartTask implements BaseHttpServerTask {
HttpServerStartTask({ HttpServerStartTask({
required this.pin, required this.pin,
required this.verifyChecksums,
required this.web, required this.web,
required this.showToken, required this.showToken,
}); });
@@ -398,6 +403,7 @@ Future<void> setupHttpServerIsolate(
deviceType: syncState.deviceInfo.deviceType.toRust(), deviceType: syncState.deviceInfo.deviceType.toRust(),
fingerprint: syncState.securityContext.certificateHash, fingerprint: syncState.securityContext.certificateHash,
pin: startTask.pin, pin: startTask.pin,
verifyChecksums: startTask.verifyChecksums,
web: startTask.web, web: startTask.web,
showToken: startTask.showToken, showToken: startTask.showToken,
); );
@@ -256,6 +256,10 @@ class IsolateHttpUploadCancelAction extends ReduxAction<IsolateController, Paren
class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> { class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> {
final String? pin; 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. /// Serves the web pages: the download page (web send) and/or the upload page.
/// `null` disables the web pages. /// `null` disables the web pages.
final WebParams? web; final WebParams? web;
@@ -266,6 +270,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
IsolateHttpServerStartAction({ IsolateHttpServerStartAction({
required this.pin, required this.pin,
required this.verifyChecksums,
required this.web, required this.web,
required this.showToken, required this.showToken,
}); });
@@ -282,6 +287,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
connection.sendWrappedTaskAndListenStream( connection.sendWrappedTaskAndListenStream(
task: HttpServerStartTask( task: HttpServerStartTask(
pin: pin, pin: pin,
verifyChecksums: verifyChecksums,
web: web, web: web,
showToken: showToken, showToken: showToken,
), ),
@@ -22,6 +22,7 @@ class HttpServerService {
required DeviceType? deviceType, required DeviceType? deviceType,
required String fingerprint, required String fingerprint,
required String? pin, required String? pin,
required bool verifyChecksums,
required WebParams? web, required WebParams? web,
required String? showToken, required String? showToken,
}) async { }) async {
@@ -38,6 +39,7 @@ class HttpServerService {
deviceType: deviceType, deviceType: deviceType,
fingerprint: fingerprint, fingerprint: fingerprint,
pin: pin, pin: pin,
verifyChecksums: verifyChecksums,
web: web, web: web,
showToken: showToken, showToken: showToken,
); );
@@ -183,6 +183,7 @@ pub async fn start_server(
device_type: Option<DeviceType>, device_type: Option<DeviceType>,
fingerprint: String, fingerprint: String,
pin: Option<String>, pin: Option<String>,
verify_checksums: bool,
web: Option<WebParams>, web: Option<WebParams>,
show_token: Option<String>, show_token: Option<String>,
) -> anyhow::Result<RsHttpServer> { ) -> anyhow::Result<RsHttpServer> {
@@ -243,7 +244,11 @@ pub async fn start_server(
token: fingerprint, token: fingerprint,
}, },
internal_config, internal_config,
Some(ServerConfigV2 { pin, event_tx }), Some(ServerConfigV2 {
pin,
verify_checksums,
event_tx,
}),
web_config, web_config,
stop_rx, stop_rx,
) )
@@ -3201,6 +3201,7 @@ fn wire__crate__api__server__start_server_impl(
<Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer); <Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer);
let api_fingerprint = <String>::sse_decode(&mut deserializer); let api_fingerprint = <String>::sse_decode(&mut deserializer);
let api_pin = <Option<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_web = <Option<crate::api::server::WebParams>>::sse_decode(&mut deserializer);
let api_show_token = <Option<String>>::sse_decode(&mut deserializer); let api_show_token = <Option<String>>::sse_decode(&mut deserializer);
deserializer.end(); deserializer.end();
@@ -3216,6 +3217,7 @@ fn wire__crate__api__server__start_server_impl(
api_device_type, api_device_type,
api_fingerprint, api_fingerprint,
api_pin, api_pin,
api_verify_checksums,
api_web, api_web,
api_show_token, api_show_token,
) )