fix: infer fingerprint based on cert
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-07-21 03:03:30 +02:00
parent 9e8a51d003
commit ed0478466b
11 changed files with 160 additions and 47 deletions
@@ -83,6 +83,11 @@ class ReceiveController {
for (final entry in event.files.entries) entry.key: entry.value.toDart(),
};
// The fingerprint of the sender's mTLS certificate cannot be spoofed, unlike the
// self-reported fingerprint in the JSON payload which is only used as fallback
// when encryption is disabled.
final senderFingerprint = event.certFingerprint ?? event.info.fingerprint;
_logger.info('Session Id: $sessionId');
_logger.info('Destination Directory: $destinationDir');
@@ -91,8 +96,8 @@ class ReceiveController {
session: ReceiveSessionState(
sessionId: sessionId,
status: SessionStatus.waiting,
sender: event.info.toDevice(event.ip, null),
senderAlias: server.ref.read(favoritesProvider).firstWhereOrNull((e) => e.fingerprint == event.info.fingerprint)?.alias ?? event.info.alias,
sender: event.info.toDevice(event.ip, null).copyWith(fingerprint: senderFingerprint),
senderAlias: server.ref.read(favoritesProvider).firstWhereOrNull((e) => e.fingerprint == senderFingerprint)?.alias ?? event.info.alias,
files: {
for (final file in files.values)
file.id: ReceivingFile(
@@ -118,7 +123,7 @@ class ReceiveController {
bool quickSave = settings.quickSave && server.getState().session?.message == null;
final quickSaveFromFavorites = settings.quickSaveFromFavorites && server.getState().session?.message == null;
if (quickSaveFromFavorites) {
final bool isFavorite = server.ref.read(favoritesProvider).any((e) => e.fingerprint == event.info.fingerprint);
final bool isFavorite = server.ref.read(favoritesProvider).any((e) => e.fingerprint == senderFingerprint);
if (isFavorite) {
quickSave = true;
}
@@ -357,9 +362,7 @@ class ReceiveController {
// (e.g. preparing the save target failed), the Rust server is still
// waiting for it and the sender's request would hang forever.
// Rejecting fails the request; a no-op if the target was already sent.
server.ref
.redux(parentIsolateProvider)
.dispatch(IsolateHttpServerRejectFileUploadAction(sessionId: event.sessionId, fileId: fileId));
server.ref.redux(parentIsolateProvider).dispatch(IsolateHttpServerRejectFileUploadAction(sessionId: event.sessionId, fileId: fileId));
}
server.ref
@@ -138,9 +138,7 @@ class SendController {
} catch (e, st) {
_logger.severe('Failed to resolve source for web send file ${event.fileId}', e, st);
// Unblock the web client's request waiting for the content source.
server.ref
.redux(parentIsolateProvider)
.dispatch(IsolateHttpServerRejectFileDownloadAction(sessionId: event.sessionId, fileId: event.fileId));
server.ref.redux(parentIsolateProvider).dispatch(IsolateHttpServerRejectFileDownloadAction(sessionId: event.sessionId, fileId: event.fileId));
return;
}
+25 -6
View File
@@ -45,6 +45,15 @@ fn verify_cert_from_cert(cert: X509Certificate, public_key: Option<&str>) -> any
Ok(())
}
/// Computes the SHA-256 fingerprint of a certificate in DER format.
/// Encoded as uppercase hex, the format used for LocalSend fingerprints.
pub fn fingerprint_from_cert_der(cert: &[u8]) -> String {
crate::crypto::hash::sha256(cert)
.iter()
.map(|byte| format!("{byte:02X}"))
.collect()
}
/// Extracts the public key from the certificate which is in DER format.
/// Encodes the public key in PEM format.
pub fn public_key_from_cert_der(cert: &[u8]) -> anyhow::Result<String> {
@@ -74,9 +83,7 @@ iK5jecOBG0RVVWLuw+TkuX8TUgrpIktH2+qEM1KdLyAMnL71hx2wMvE+lDKFKK9p
bQIDAQAB
-----END PUBLIC KEY-----";
#[test]
fn test_verify_cert() {
let cert = "-----BEGIN CERTIFICATE-----
static CERT: &str = "-----BEGIN CERTIFICATE-----
MIIDGTCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQsFADBQMRcwFQYDVQQDEw5Mb2Nh
bFNlbmQgVXNlcjEJMAcGA1UEChMAMQkwBwYDVQQLEwAxCTAHBgNVBAcTADEJMAcG
A1UECBMAMQkwBwYDVQQGEwAwHhcNMjUwMjA5MDAwMzE0WhcNMzUwMjA3MDAwMzE0
@@ -94,10 +101,12 @@ bfSiFW4hXWp75grVO8xfML7ZcWMlhKrOsOMUGiy1qs3qsyJ3w7B2Tz78HhXGO5dd
jyPmZarhixKO92UpEvKGxjO0E/3UUNUzxKTAAgFfhKpuwHUgIijM/EppZtA8OcSh
fEztiV0xKfcPVx4d6dqRt/NMElK1Ivw2vUuxTymphZkkFOzht9m73/kyKaeFp8Ij
VRus1zGVD8IVpIdPMyz01WJyS7M0fWaHXKWo+Bo=
-----END CERTIFICATE-----"
.to_string();
-----END CERTIFICATE-----";
#[test]
fn test_verify_cert() {
assert_eq!(
verify_cert_from_pem(cert, Some(PUBLIC_KEY)).map_err(|e| e.to_string()),
verify_cert_from_pem(CERT.to_string(), Some(PUBLIC_KEY)).map_err(|e| e.to_string()),
Ok(())
);
@@ -178,4 +187,14 @@ nidU/qXQvBJ7NPUkXXgbcgqxK735iijOqQHmKts=
Err("Time validity error".to_string())
);
}
#[test]
fn test_fingerprint_from_cert_der() {
let (cert_pem, _) = Pem::read(Cursor::new(CERT.as_bytes().to_vec())).unwrap();
assert_eq!(
fingerprint_from_cert_der(&cert_pem.contents),
// Must match the Dart side: uppercase hex SHA-256 of the DER bytes.
"4BADDE53A7F7CDEEED93189FD898E02BF6B4806CA4C05DE0ACE08319B86552FA"
);
}
}
+8 -1
View File
@@ -4,7 +4,7 @@ pub mod v2;
pub mod v3;
pub mod web;
use crate::crypto::cert::public_key_from_cert_der;
use crate::crypto::cert::{fingerprint_from_cert_der, public_key_from_cert_der};
use crate::http::server::internal::{InternalConfig, InternalState};
use crate::http::server::v2::ServerEventV2;
use crate::http::server::web::WebSendConfig;
@@ -367,6 +367,13 @@ pub struct RequestClientInfo {
}
impl RequestClientInfo {
/// The SHA-256 fingerprint (uppercase hex) of the client certificate
/// verified during the mTLS handshake.
/// `None` when the server runs without TLS.
fn cert_fingerprint(&self) -> Option<String> {
self.cert.as_deref().map(fingerprint_from_cert_der)
}
fn extract_public_key(&self) -> Option<String> {
match &self.cert {
Some(cert) => match public_key_from_cert_der(cert) {
+33 -9
View File
@@ -25,6 +25,10 @@ use uuid::Uuid;
#[derive(Debug)]
pub enum ServerEventV2 {
/// A device registered itself via `POST /api/localsend/v2/register`.
///
/// On TLS, this event is only emitted when `info.fingerprint` matches the
/// SHA-256 fingerprint of the client certificate verified during the mTLS
/// handshake, so the fingerprint cannot be spoofed.
Register {
/// The IP address of the remote device.
ip: IpAddr,
@@ -49,6 +53,12 @@ pub enum ServerEventV2 {
/// The device information of the sender.
info: RegisterDtoV2,
/// The SHA-256 fingerprint (uppercase hex) of the sender's client
/// certificate verified during the mTLS handshake. Unlike
/// `info.fingerprint`, this value cannot be spoofed.
/// `None` when the server runs without TLS.
cert_fingerprint: Option<String>,
/// The offered files, mapped by file ID.
files: HashMap<String, FileDto>,
@@ -137,14 +147,28 @@ pub(crate) async fn register(
) -> Result<JsonResponse<RegisterResponseDtoV2>, AppError> {
let payload = body.collect_to_json::<RegisterDtoV2>().await?;
// On TLS, only trust registrations whose claimed fingerprint is proven
// by the client certificate of the mTLS handshake.
let fingerprint_valid = match client_info.cert_fingerprint() {
Some(cert_fingerprint) => payload.fingerprint.to_ascii_uppercase() == cert_fingerprint,
None => true,
};
if let Some(v2) = &state.v2 {
let _ = v2
.event_tx
.send(ServerEventV2::Register {
ip: client_info.ip,
info: payload,
})
.await;
if fingerprint_valid {
let _ = v2
.event_tx
.send(ServerEventV2::Register {
ip: client_info.ip,
info: payload,
})
.await;
} else {
tracing::warn!(
"Ignoring register from {}: claimed fingerprint does not match the client certificate",
client_info.ip
);
}
}
let info = state.info.lock().await.clone();
@@ -221,6 +245,7 @@ pub(crate) async fn prepare_upload(
session_id: session_id.clone(),
ip: client_info.ip,
info: payload.info,
cert_fingerprint: client_info.cert_fingerprint(),
files: payload.files.clone(),
decision_tx,
};
@@ -375,8 +400,7 @@ pub(crate) async fn cancel(
let mut slot = v2.session.lock().await;
match slot.as_ref() {
Some(SessionStateV2::Active(session))
if session.session_id == *session_id
&& session.sender_ip == client_info.ip =>
if session.session_id == *session_id && session.sender_ip == client_info.ip =>
{
*slot = None;
true
@@ -166,6 +166,10 @@ sealed class RsServerEvent with _$RsServerEvent {
const RsServerEvent._();
/// A device registered itself via `POST /api/localsend/v2/register`.
///
/// On TLS, this event is only emitted when `info.fingerprint` matches the
/// fingerprint of the client certificate verified during the mTLS
/// handshake, so the fingerprint cannot be spoofed.
const factory RsServerEvent.register({
required String ip,
required RegisterDtoV2 info,
@@ -177,6 +181,12 @@ sealed class RsServerEvent with _$RsServerEvent {
required String sessionId,
required String ip,
required RegisterDtoV2 info,
/// The SHA-256 fingerprint (uppercase hex) of the sender's client
/// certificate verified during the mTLS handshake. Unlike
/// `info.fingerprint`, this value cannot be spoofed.
/// `None` when the server runs without TLS.
String? certFingerprint,
required Map<String, FileDto> files,
}) = RsServerEvent_PrepareUpload;
@@ -140,11 +140,11 @@ return show_(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String ip, RegisterDtoV2 info)? register,TResult Function( String sessionId, String ip, RegisterDtoV2 info, Map<String, FileDto> files)? prepareUpload,TResult Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,TResult Function( String sessionId)? prepareUploadAborted,TResult Function( String ip, String sessionId)? cancelReceived,TResult Function( String ip, String sessionId, String? userAgent)? webPrepareDownload,TResult Function( String sessionId, String fileId, FileDto file)? webFileDownload,TResult Function( List<String> args)? show_,required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String ip, RegisterDtoV2 info)? register,TResult Function( String sessionId, String ip, RegisterDtoV2 info, String? certFingerprint, Map<String, FileDto> files)? prepareUpload,TResult Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,TResult Function( String sessionId)? prepareUploadAborted,TResult Function( String ip, String sessionId)? cancelReceived,TResult Function( String ip, String sessionId, String? userAgent)? webPrepareDownload,TResult Function( String sessionId, String fileId, FileDto file)? webFileDownload,TResult Function( List<String> args)? show_,required TResult orElse(),}) {final _that = this;
switch (_that) {
case RsServerEvent_Register() when register != null:
return register(_that.ip,_that.info);case RsServerEvent_PrepareUpload() when prepareUpload != null:
return prepareUpload(_that.sessionId,_that.ip,_that.info,_that.files);case RsServerEvent_FileUpload() when fileUpload != null:
return prepareUpload(_that.sessionId,_that.ip,_that.info,_that.certFingerprint,_that.files);case RsServerEvent_FileUpload() when fileUpload != null:
return fileUpload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_SessionEnd() when sessionEnd != null:
return sessionEnd(_that.sessionId,_that.reason);case RsServerEvent_PrepareUploadAborted() when prepareUploadAborted != null:
return prepareUploadAborted(_that.sessionId);case RsServerEvent_CancelReceived() when cancelReceived != null:
@@ -169,11 +169,11 @@ return show_(_that.args);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String ip, RegisterDtoV2 info) register,required TResult Function( String sessionId, String ip, RegisterDtoV2 info, Map<String, FileDto> files) prepareUpload,required TResult Function( String sessionId, String fileId, FileDto file) fileUpload,required TResult Function( String sessionId, SessionEndReasonV2 reason) sessionEnd,required TResult Function( String sessionId) prepareUploadAborted,required TResult Function( String ip, String sessionId) cancelReceived,required TResult Function( String ip, String sessionId, String? userAgent) webPrepareDownload,required TResult Function( String sessionId, String fileId, FileDto file) webFileDownload,required TResult Function( List<String> args) show_,}) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String ip, RegisterDtoV2 info) register,required TResult Function( String sessionId, String ip, RegisterDtoV2 info, String? certFingerprint, Map<String, FileDto> files) prepareUpload,required TResult Function( String sessionId, String fileId, FileDto file) fileUpload,required TResult Function( String sessionId, SessionEndReasonV2 reason) sessionEnd,required TResult Function( String sessionId) prepareUploadAborted,required TResult Function( String ip, String sessionId) cancelReceived,required TResult Function( String ip, String sessionId, String? userAgent) webPrepareDownload,required TResult Function( String sessionId, String fileId, FileDto file) webFileDownload,required TResult Function( List<String> args) show_,}) {final _that = this;
switch (_that) {
case RsServerEvent_Register():
return register(_that.ip,_that.info);case RsServerEvent_PrepareUpload():
return prepareUpload(_that.sessionId,_that.ip,_that.info,_that.files);case RsServerEvent_FileUpload():
return prepareUpload(_that.sessionId,_that.ip,_that.info,_that.certFingerprint,_that.files);case RsServerEvent_FileUpload():
return fileUpload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_SessionEnd():
return sessionEnd(_that.sessionId,_that.reason);case RsServerEvent_PrepareUploadAborted():
return prepareUploadAborted(_that.sessionId);case RsServerEvent_CancelReceived():
@@ -194,11 +194,11 @@ return show_(_that.args);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String ip, RegisterDtoV2 info)? register,TResult? Function( String sessionId, String ip, RegisterDtoV2 info, Map<String, FileDto> files)? prepareUpload,TResult? Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult? Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,TResult? Function( String sessionId)? prepareUploadAborted,TResult? Function( String ip, String sessionId)? cancelReceived,TResult? Function( String ip, String sessionId, String? userAgent)? webPrepareDownload,TResult? Function( String sessionId, String fileId, FileDto file)? webFileDownload,TResult? Function( List<String> args)? show_,}) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String ip, RegisterDtoV2 info)? register,TResult? Function( String sessionId, String ip, RegisterDtoV2 info, String? certFingerprint, Map<String, FileDto> files)? prepareUpload,TResult? Function( String sessionId, String fileId, FileDto file)? fileUpload,TResult? Function( String sessionId, SessionEndReasonV2 reason)? sessionEnd,TResult? Function( String sessionId)? prepareUploadAborted,TResult? Function( String ip, String sessionId)? cancelReceived,TResult? Function( String ip, String sessionId, String? userAgent)? webPrepareDownload,TResult? Function( String sessionId, String fileId, FileDto file)? webFileDownload,TResult? Function( List<String> args)? show_,}) {final _that = this;
switch (_that) {
case RsServerEvent_Register() when register != null:
return register(_that.ip,_that.info);case RsServerEvent_PrepareUpload() when prepareUpload != null:
return prepareUpload(_that.sessionId,_that.ip,_that.info,_that.files);case RsServerEvent_FileUpload() when fileUpload != null:
return prepareUpload(_that.sessionId,_that.ip,_that.info,_that.certFingerprint,_that.files);case RsServerEvent_FileUpload() when fileUpload != null:
return fileUpload(_that.sessionId,_that.fileId,_that.file);case RsServerEvent_SessionEnd() when sessionEnd != null:
return sessionEnd(_that.sessionId,_that.reason);case RsServerEvent_PrepareUploadAborted() when prepareUploadAborted != null:
return prepareUploadAborted(_that.sessionId);case RsServerEvent_CancelReceived() when cancelReceived != null:
@@ -285,13 +285,18 @@ as RegisterDtoV2,
class RsServerEvent_PrepareUpload extends RsServerEvent {
const RsServerEvent_PrepareUpload({required this.sessionId, required this.ip, required this.info, required final Map<String, FileDto> files}): _files = files,super._();
const RsServerEvent_PrepareUpload({required this.sessionId, required this.ip, required this.info, this.certFingerprint, required final Map<String, FileDto> files}): _files = files,super._();
/// The session ID the upload session will have when the request is accepted.
final String sessionId;
final String ip;
final RegisterDtoV2 info;
/// The SHA-256 fingerprint (uppercase hex) of the sender's client
/// certificate verified during the mTLS handshake. Unlike
/// `info.fingerprint`, this value cannot be spoofed.
/// `None` when the server runs without TLS.
final String? certFingerprint;
final Map<String, FileDto> _files;
Map<String, FileDto> get files {
if (_files is EqualUnmodifiableMapView) return _files;
@@ -310,16 +315,16 @@ $RsServerEvent_PrepareUploadCopyWith<RsServerEvent_PrepareUpload> get copyWith =
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_PrepareUpload&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.info, info) || other.info == info)&&const DeepCollectionEquality().equals(other._files, _files));
return identical(this, other) || (other.runtimeType == runtimeType&&other is RsServerEvent_PrepareUpload&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.info, info) || other.info == info)&&(identical(other.certFingerprint, certFingerprint) || other.certFingerprint == certFingerprint)&&const DeepCollectionEquality().equals(other._files, _files));
}
@override
int get hashCode => Object.hash(runtimeType,sessionId,ip,info,const DeepCollectionEquality().hash(_files));
int get hashCode => Object.hash(runtimeType,sessionId,ip,info,certFingerprint,const DeepCollectionEquality().hash(_files));
@override
String toString() {
return 'RsServerEvent.prepareUpload(sessionId: $sessionId, ip: $ip, info: $info, files: $files)';
return 'RsServerEvent.prepareUpload(sessionId: $sessionId, ip: $ip, info: $info, certFingerprint: $certFingerprint, files: $files)';
}
@@ -330,7 +335,7 @@ abstract mixin class $RsServerEvent_PrepareUploadCopyWith<$Res> implements $RsSe
factory $RsServerEvent_PrepareUploadCopyWith(RsServerEvent_PrepareUpload value, $Res Function(RsServerEvent_PrepareUpload) _then) = _$RsServerEvent_PrepareUploadCopyWithImpl;
@useResult
$Res call({
String sessionId, String ip, RegisterDtoV2 info, Map<String, FileDto> files
String sessionId, String ip, RegisterDtoV2 info, String? certFingerprint, Map<String, FileDto> files
});
@@ -347,12 +352,13 @@ class _$RsServerEvent_PrepareUploadCopyWithImpl<$Res>
/// Create a copy of RsServerEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? sessionId = null,Object? ip = null,Object? info = null,Object? files = null,}) {
@pragma('vm:prefer-inline') $Res call({Object? sessionId = null,Object? ip = null,Object? info = null,Object? certFingerprint = freezed,Object? files = null,}) {
return _then(RsServerEvent_PrepareUpload(
sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,info: null == info ? _self.info : info // ignore: cast_nullable_to_non_nullable
as RegisterDtoV2,files: null == files ? _self._files : files // ignore: cast_nullable_to_non_nullable
as RegisterDtoV2,certFingerprint: freezed == certFingerprint ? _self.certFingerprint : certFingerprint // ignore: cast_nullable_to_non_nullable
as String?,files: null == files ? _self._files : files // ignore: cast_nullable_to_non_nullable
as Map<String, FileDto>,
));
}
@@ -2542,7 +2542,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sessionId: dco_decode_String(raw[1]),
ip: dco_decode_String(raw[2]),
info: dco_decode_box_autoadd_register_dto_v_2(raw[3]),
files: dco_decode_Map_String_file_dto_None(raw[4]),
certFingerprint: dco_decode_opt_String(raw[4]),
files: dco_decode_Map_String_file_dto_None(raw[5]),
);
case 2:
return RsServerEvent_FileUpload(
@@ -3694,8 +3695,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_sessionId = sse_decode_String(deserializer);
var var_ip = sse_decode_String(deserializer);
var var_info = sse_decode_box_autoadd_register_dto_v_2(deserializer);
var var_certFingerprint = sse_decode_opt_String(deserializer);
var var_files = sse_decode_Map_String_file_dto_None(deserializer);
return RsServerEvent_PrepareUpload(sessionId: var_sessionId, ip: var_ip, info: var_info, files: var_files);
return RsServerEvent_PrepareUpload(
sessionId: var_sessionId,
ip: var_ip,
info: var_info,
certFingerprint: var_certFingerprint,
files: var_files,
);
case 2:
var var_sessionId = sse_decode_String(deserializer);
var var_fileId = sse_decode_String(deserializer);
@@ -4838,11 +4846,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(0, serializer);
sse_encode_String(ip, serializer);
sse_encode_box_autoadd_register_dto_v_2(info, serializer);
case RsServerEvent_PrepareUpload(sessionId: final sessionId, ip: final ip, info: final info, files: final files):
case RsServerEvent_PrepareUpload(
sessionId: final sessionId,
ip: final ip,
info: final info,
certFingerprint: final certFingerprint,
files: final files,
):
sse_encode_i_32(1, serializer);
sse_encode_String(sessionId, serializer);
sse_encode_String(ip, serializer);
sse_encode_box_autoadd_register_dto_v_2(info, serializer);
sse_encode_opt_String(certFingerprint, serializer);
sse_encode_Map_String_file_dto_None(files, serializer);
case RsServerEvent_FileUpload(sessionId: final sessionId, fileId: final fileId, file: final file):
sse_encode_i_32(2, serializer);
@@ -156,6 +156,10 @@ sealed class HttpServerEvent {}
class HttpServerStartedEvent extends HttpServerEvent {}
/// A device registered itself on this server.
///
/// On TLS, this event is only emitted when [RegisterDtoV2.fingerprint] matches
/// the fingerprint of the client certificate verified during the mTLS
/// handshake, so the fingerprint cannot be spoofed.
class HttpServerRegisterEvent extends HttpServerEvent {
final String ip;
final RegisterDtoV2 info;
@@ -173,12 +177,20 @@ class HttpServerPrepareUploadEvent extends HttpServerEvent {
final String sessionId;
final String ip;
final RegisterDtoV2 info;
/// The SHA-256 fingerprint (uppercase hex) of the sender's client
/// certificate verified during the mTLS handshake. Unlike
/// [RegisterDtoV2.fingerprint], this value cannot be spoofed.
/// `null` when the server runs without TLS.
final String? certFingerprint;
final Map<String, FileDto> files;
HttpServerPrepareUploadEvent({
required this.sessionId,
required this.ip,
required this.info,
required this.certFingerprint,
required this.files,
});
}
@@ -359,12 +371,14 @@ Future<void> setupHttpServerIsolate(
id: task.id,
data: switch (event) {
RsServerEvent_Register(:final ip, :final info) => HttpServerRegisterEvent(ip: ip, info: info),
RsServerEvent_PrepareUpload(:final sessionId, :final ip, :final info, :final files) => HttpServerPrepareUploadEvent(
sessionId: sessionId,
ip: ip,
info: info,
files: files,
),
RsServerEvent_PrepareUpload(:final sessionId, :final ip, :final info, :final certFingerprint, :final files) =>
HttpServerPrepareUploadEvent(
sessionId: sessionId,
ip: ip,
info: info,
certFingerprint: certFingerprint,
files: files,
),
RsServerEvent_FileUpload(:final sessionId, :final fileId, :final file) => HttpServerFileUploadEvent(
sessionId: sessionId,
fileId: fileId,
@@ -21,6 +21,10 @@ use tokio::sync::{Mutex, mpsc, oneshot};
/// and [RsServerEvent::FileUpload] with [RsHttpServer::respond_file_upload].
pub enum RsServerEvent {
/// A device registered itself via `POST /api/localsend/v2/register`.
///
/// On TLS, this event is only emitted when `info.fingerprint` matches the
/// fingerprint of the client certificate verified during the mTLS
/// handshake, so the fingerprint cannot be spoofed.
Register { ip: String, info: RegisterDtoV2 },
/// A sender requests to upload files via `POST /api/localsend/v2/prepare-upload`.
@@ -29,6 +33,11 @@ pub enum RsServerEvent {
session_id: String,
ip: String,
info: RegisterDtoV2,
/// The SHA-256 fingerprint (uppercase hex) of the sender's client
/// certificate verified during the mTLS handshake. Unlike
/// `info.fingerprint`, this value cannot be spoofed.
/// `None` when the server runs without TLS.
cert_fingerprint: Option<String>,
files: HashMap<String, FileDto>,
},
@@ -250,6 +259,7 @@ impl RsHttpServer {
session_id,
ip,
info,
cert_fingerprint,
files,
decision_tx,
} => {
@@ -258,6 +268,7 @@ impl RsHttpServer {
session_id,
ip: ip.to_string(),
info,
cert_fingerprint,
files,
});
}
@@ -3704,6 +3704,7 @@ impl SseDecode for crate::api::server::RsServerEvent {
let mut var_sessionId = <String>::sse_decode(deserializer);
let mut var_ip = <String>::sse_decode(deserializer);
let mut var_info = <crate::api::server::RegisterDtoV2>::sse_decode(deserializer);
let mut var_certFingerprint = <Option<String>>::sse_decode(deserializer);
let mut var_files =
<std::collections::HashMap<String, crate::api::model::FileDto>>::sse_decode(
deserializer,
@@ -3712,6 +3713,7 @@ impl SseDecode for crate::api::server::RsServerEvent {
session_id: var_sessionId,
ip: var_ip,
info: var_info,
cert_fingerprint: var_certFingerprint,
files: var_files,
};
}
@@ -4867,12 +4869,14 @@ impl flutter_rust_bridge::IntoDart for crate::api::server::RsServerEvent {
session_id,
ip,
info,
cert_fingerprint,
files,
} => [
1.into_dart(),
session_id.into_into_dart().into_dart(),
ip.into_into_dart().into_dart(),
info.into_into_dart().into_dart(),
cert_fingerprint.into_into_dart().into_dart(),
files.into_into_dart().into_dart(),
]
.into_dart(),
@@ -5990,12 +5994,14 @@ impl SseEncode for crate::api::server::RsServerEvent {
session_id,
ip,
info,
cert_fingerprint,
files,
} => {
<i32>::sse_encode(1, serializer);
<String>::sse_encode(session_id, serializer);
<String>::sse_encode(ip, serializer);
<crate::api::server::RegisterDtoV2>::sse_encode(info, serializer);
<Option<String>>::sse_encode(cert_fingerprint, serializer);
<std::collections::HashMap<String, crate::api::model::FileDto>>::sse_encode(
files, serializer,
);