1pub mod dufs;
4pub mod handlers;
5pub mod router;
6pub mod session;
7
8use std::net::SocketAddr;
9use std::sync::Arc;
10use hyper_util::rt::TokioIo;
11use rustls::pki_types::pem::PemObject;
12use rustls::pki_types::{CertificateDer, PrivateKeyDer};
13use tokio::io::{AsyncRead, AsyncWrite};
14use tokio::net::TcpListener;
15use tokio::sync::mpsc;
16use tokio_util::sync::CancellationToken;
17use tokio_util::task::TaskTracker;
18
19pub use dufs::{DufsConfig, DufsManager, DufsServerHandle, ServerResponseBody};
20pub use handlers::{
21 extract_password_from_url, percent_decode, ServerState, ServerTransferEvent,
22};
23pub use router::{route_request, ClientConnectionInfo};
24pub use session::{
25 DownloadFile, DownloadManager, DownloadSession, FileTransferStatus, PrepareUploadDecision,
26 SessionEndReason, SessionFile, SessionManager,
27};
28
29use crate::config::AppConfig;
30use crate::crypto::cert::TlsIdentity;
31use crate::crypto::verifier::PinnedClientCertVerifier;
32use crate::discovery::registry::PeerRegistry;
33
34pub struct ServerHandle {
36 bound_port: u16,
38 cancel_token: CancellationToken,
40 join_handle: tokio::task::JoinHandle<()>,
42 download_manager: Arc<DownloadManager>,
44 dufs_manager: Arc<DufsManager>,
46}
47
48impl ServerHandle {
49 pub fn bound_port(&self) -> u16 {
51 self.bound_port
52 }
53
54 pub fn download_manager(&self) -> &Arc<DownloadManager> {
56 &self.download_manager
57 }
58
59 pub fn dufs_manager(&self) -> &Arc<DufsManager> {
61 &self.dufs_manager
62 }
63
64 pub fn stop(&self) {
66 self.cancel_token.cancel();
67 }
68
69 pub async fn wait(self) {
71 let _ = self.join_handle.await;
72 }
73}
74
75#[derive(Clone)]
77pub struct ServerConfigHolder(pub Arc<tokio::sync::RwLock<AppConfig>>);
78
79impl From<AppConfig> for ServerConfigHolder {
80 fn from(c: AppConfig) -> Self {
81 ServerConfigHolder(Arc::new(tokio::sync::RwLock::new(c)))
82 }
83}
84
85impl From<Arc<tokio::sync::RwLock<AppConfig>>> for ServerConfigHolder {
86 fn from(a: Arc<tokio::sync::RwLock<AppConfig>>) -> Self {
87 ServerConfigHolder(a)
88 }
89}
90
91pub async fn start_server(
97 config: impl Into<ServerConfigHolder>,
98 tls_identity: Option<TlsIdentity>,
99 registry: Arc<PeerRegistry>,
100 event_tx: Option<mpsc::Sender<ServerTransferEvent>>,
101) -> anyhow::Result<ServerHandle> {
102 start_server_ext(config, tls_identity, registry, event_tx, None, None).await
103}
104
105pub async fn start_server_ext(
107 config: impl Into<ServerConfigHolder>,
108 tls_identity: Option<TlsIdentity>,
109 registry: Arc<PeerRegistry>,
110 event_tx: Option<mpsc::Sender<ServerTransferEvent>>,
111 download_manager: Option<Arc<DownloadManager>>,
112 dufs_manager: Option<Arc<DufsManager>>,
113) -> anyhow::Result<ServerHandle> {
114 let cancel_token = CancellationToken::new();
115 let config_holder = config.into();
116 let port = config_holder.0.read().await.port;
117 let addr = SocketAddr::from(([0, 0, 0, 0], port));
118 let listener = TcpListener::bind(addr).await?;
119 let bound_port = listener.local_addr()?.port();
120
121 let cert_fingerprint = match tls_identity.as_ref() {
122 Some(ident) => ident.fingerprint.clone(),
123 None => String::new(),
124 };
125
126 let tls_acceptor = match tls_identity.as_ref() {
127 Some(ident) => {
128 let cert_der = CertificateDer::from_pem_slice(ident.cert_pem.as_bytes())?;
129 let key_der = PrivateKeyDer::from_pem_slice(ident.key_pem.as_bytes())?;
130
131 let client_verifier = PinnedClientCertVerifier::new(&ident.cert_pem, false, None)?;
133
134 let server_config = rustls::ServerConfig::builder()
135 .with_client_cert_verifier(Arc::new(client_verifier))
136 .with_single_cert(vec![cert_der], key_der)?;
137
138 Some(tokio_rustls::TlsAcceptor::from(Arc::new(server_config)))
139 }
140 None => None,
141 };
142
143 let download_manager = download_manager.unwrap_or_else(|| Arc::new(DownloadManager::new()));
144 let dufs_manager = dufs_manager.unwrap_or_else(|| Arc::new(DufsManager::new()));
145
146 let state = ServerState {
147 config: config_holder.0,
148 cert_fingerprint,
149 session_manager: Arc::new(SessionManager::new()),
150 download_manager: download_manager.clone(),
151 registry,
152 event_tx,
153 dufs_manager: dufs_manager.clone(),
154 tls_identity: tls_identity.clone(),
155 };
156
157 let cancel = cancel_token.clone();
158 let join_handle = tokio::spawn(async move {
159 let tracker = TaskTracker::new();
160
161 loop {
162 tokio::select! {
163 _ = cancel.cancelled() => {
164 tracing::info!("Server shutdown signaled, closing listener");
165 break;
166 }
167 accept_res = listener.accept() => {
168 let (tcp_stream, remote_addr) = match accept_res {
169 Ok(res) => res,
170 Err(err) => {
171 tracing::warn!("Failed to accept incoming TCP connection: {err}");
172 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
173 continue;
174 }
175 };
176
177 let _ = tcp_stream.set_nodelay(true);
178 let tls_acceptor = tls_acceptor.clone();
179 let state = state.clone();
180 let conn_cancel = cancel.clone();
181
182 tracker.spawn(async move {
183 tokio::select! {
184 _ = conn_cancel.cancelled() => {}
185 _ = serve_connection(tcp_stream, remote_addr, tls_acceptor, state) => {}
186 }
187 });
188 }
189 }
190 }
191
192 tracker.close();
193 tracker.wait().await;
194 tracing::info!("Server terminated all active connections");
195 });
196
197 Ok(ServerHandle {
198 bound_port,
199 cancel_token,
200 join_handle,
201 download_manager,
202 dufs_manager,
203 })
204}
205
206async fn serve_connection(
207 tcp_stream: tokio::net::TcpStream,
208 remote_addr: SocketAddr,
209 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
210 state: ServerState,
211) {
212 if let Some(acceptor) = tls_acceptor {
213 let mut peek_buf = [0u8; 1];
216 match tcp_stream.peek(&mut peek_buf).await {
217 Ok(0) => return, Ok(_) => {
219 if peek_buf[0] != 0x16 {
220 serve_plaintext_http_redirect(tcp_stream, remote_addr, state).await;
221 return;
222 }
223 }
224 Err(err) => {
225 tracing::debug!("Failed to peek incoming connection from {remote_addr}: {err}");
226 return;
227 }
228 }
229
230 match acceptor.accept(tcp_stream).await {
231 Ok(tls_stream) => {
232 let (_, conn) = tls_stream.get_ref();
233 let client_cert_fingerprint = conn
234 .peer_certificates()
235 .and_then(|certs| certs.first())
236 .map(|c| crate::crypto::cert::fingerprint_from_cert_der(c.as_ref()));
237
238 let client_info = ClientConnectionInfo {
239 remote_ip: remote_addr.ip(),
240 cert_fingerprint: client_cert_fingerprint,
241 };
242
243 serve_http_stream(tls_stream, client_info, state).await;
244 }
245 Err(err) => {
246 tracing::debug!("TLS handshake failed with {remote_addr}: {err}");
247 }
248 }
249 } else {
250 let client_info = ClientConnectionInfo {
251 remote_ip: remote_addr.ip(),
252 cert_fingerprint: None,
253 };
254 serve_http_stream(tcp_stream, client_info, state).await;
255 }
256}
257
258async fn serve_plaintext_http_redirect(
259 stream: tokio::net::TcpStream,
260 remote_addr: SocketAddr,
261 state: ServerState,
262) {
263 let io = TokioIo::new(stream);
264 let state_for_svc = state.clone();
265 let service = hyper::service::service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
266 let state = state_for_svc.clone();
267 async move {
268 let host_header = req
269 .headers()
270 .get(hyper::header::HOST)
271 .and_then(|h| h.to_str().ok())
272 .unwrap_or("");
273
274 let host_part = if !host_header.is_empty() {
275 host_header.to_string()
276 } else {
277 let port = state.config.read().await.port;
278 format!("localhost:{}", port)
279 };
280
281 let uri = req.uri();
282 let path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/");
283 let target_url = format!("https://{}{}", host_part, path_and_query);
284
285 let status = match *req.method() {
286 hyper::Method::GET | hyper::Method::HEAD => hyper::StatusCode::MOVED_PERMANENTLY,
287 _ => hyper::StatusCode::TEMPORARY_REDIRECT,
288 };
289
290 let res = hyper::Response::builder()
291 .status(status)
292 .header(hyper::header::LOCATION, target_url)
293 .header(hyper::header::CONNECTION, "close")
294 .header(hyper::header::CONTENT_LENGTH, "0")
295 .body(dufs::full_box_body(bytes::Bytes::new()));
296
297 Ok::<_, std::convert::Infallible>(res.unwrap())
298 }
299 });
300
301 if let Err(err) = hyper::server::conn::http1::Builder::new()
302 .max_buf_size(16 * 1024)
303 .serve_connection(io, service)
304 .await
305 {
306 tracing::debug!("Plaintext HTTP redirect failed with {remote_addr}: {err}");
307 }
308}
309
310async fn serve_http_stream<IO>(stream: IO, client_info: ClientConnectionInfo, state: ServerState)
311where
312 IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
313{
314 let io = TokioIo::new(stream);
315 let service = hyper::service::service_fn(move |req| {
316 let client_info = client_info.clone();
317 let state = state.clone();
318 async move { route_request(req, client_info, &state).await }
319 });
320
321 if let Err(err) = hyper::server::conn::http1::Builder::new()
322 .max_buf_size(128 * 1024)
323 .serve_connection(io, service)
324 .await
325 {
326 tracing::debug!("HTTP/1.1 connection error: {err}");
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use std::collections::HashMap;
334 use sha2::{Digest, Sha256};
335 use lanflare_proto::discovery::{InfoResponseDtoV2, ProtocolType, RegisterDtoV2, RegisterResponseDtoV2};
336 use lanflare_proto::transfer::{FileDto, PrepareUploadRequestDtoV2, PrepareUploadResponseDtoV2};
337
338 #[tokio::test]
339 async fn test_server_plain_http_lifecycle_and_endpoints() {
340 let temp_dir = std::env::temp_dir().join(format!("lanflare_test_{}", uuid::Uuid::new_v4()));
341 tokio::fs::create_dir_all(&temp_dir).await.unwrap();
342
343 let config = AppConfig {
344 alias: "Test Node 1".to_string(),
345 port: 0, download_dir: temp_dir.clone(),
347 auto_accept: true,
348 pin: None,
349 ..Default::default()
350 };
351
352 let registry = PeerRegistry::new();
353 let (event_tx, mut event_rx) = mpsc::channel(16);
354
355 let server_handle = start_server(config, None, registry, Some(event_tx))
356 .await
357 .expect("Server should bind");
358
359 let port = server_handle.bound_port();
360 assert!(port > 0);
361
362 let client = reqwest::Client::new();
363 let base_url = format!("http://127.0.0.1:{port}");
364
365 let info_resp = client
367 .get(format!("{base_url}/api/localsend/v2/info"))
368 .send()
369 .await
370 .expect("Info request should succeed");
371 assert_eq!(info_resp.status(), reqwest::StatusCode::OK);
372 let info_dto: InfoResponseDtoV2 = info_resp.json().await.unwrap();
373 assert_eq!(info_dto.alias, "Test Node 1");
374
375 let reg_payload = RegisterDtoV2 {
377 alias: "Remote Peer".to_string(),
378 version: "2.0".to_string(),
379 device_model: Some("Linux".to_string()),
380 device_type: Some(lanflare_proto::discovery::DeviceType::Desktop),
381 fingerprint: "PEER_FINGERPRINT_123".to_string(),
382 port: 53317,
383 protocol: ProtocolType::Http,
384 download: false,
385 };
386
387 let reg_resp = client
388 .post(format!("{base_url}/api/localsend/v2/register"))
389 .json(®_payload)
390 .send()
391 .await
392 .expect("Register request should succeed");
393 assert_eq!(reg_resp.status(), reqwest::StatusCode::OK);
394 let reg_res_dto: RegisterResponseDtoV2 = reg_resp.json().await.unwrap();
395 assert_eq!(reg_res_dto.alias, "Test Node 1");
396
397 match event_rx.recv().await {
399 Some(ServerTransferEvent::DeviceRegistered { ip, info }) => {
400 assert_eq!(ip.to_string(), "127.0.0.1");
401 assert_eq!(info.alias, "Remote Peer");
402 }
403 other => panic!("Expected DeviceRegistered event, got: {other:?}"),
404 }
405
406 let file_content = b"Hello, LanFlare LocalSend Transfer Engine!";
408 let mut hasher = Sha256::new();
409 hasher.update(file_content);
410 let expected_sha256 = format!("{:x}", hasher.finalize());
411
412 let mut files = HashMap::new();
413 files.insert(
414 "file_1".to_string(),
415 FileDto {
416 id: "file_1".to_string(),
417 file_name: "greeting.txt".to_string(),
418 size: file_content.len() as u64,
419 file_type: "text/plain".to_string(),
420 sha256: Some(expected_sha256.clone()),
421 preview: None,
422 metadata: None,
423 },
424 );
425
426 let prep_req = PrepareUploadRequestDtoV2 {
427 info: reg_payload.clone(),
428 files,
429 };
430
431 let prep_resp = client
432 .post(format!("{base_url}/api/localsend/v2/prepare-upload"))
433 .json(&prep_req)
434 .send()
435 .await
436 .expect("Prepare upload should succeed");
437 assert_eq!(prep_resp.status(), reqwest::StatusCode::OK);
438
439 let prep_res_dto: PrepareUploadResponseDtoV2 = prep_resp.json().await.unwrap();
440 assert!(!prep_res_dto.session_id.is_empty());
441 let file_token = prep_res_dto.files.get("file_1").expect("Token for file_1");
442
443 let upload_url = format!(
445 "{base_url}/api/localsend/v2/upload?sessionId={}&fileId=file_1&token={}",
446 prep_res_dto.session_id, file_token
447 );
448
449 let upload_resp = client
450 .post(upload_url)
451 .body(file_content.to_vec())
452 .send()
453 .await
454 .expect("Upload request should succeed");
455 assert_eq!(upload_resp.status(), reqwest::StatusCode::OK);
456
457 let saved_file = temp_dir.join("greeting.txt");
459 assert!(saved_file.exists(), "File must exist on disk");
460 let saved_bytes = tokio::fs::read(&saved_file).await.unwrap();
461 assert_eq!(saved_bytes, file_content);
462
463 let started = event_rx.recv().await.unwrap();
465 assert!(matches!(started, ServerTransferEvent::FileStarted { .. }));
466
467 let completed = event_rx.recv().await.unwrap();
468 assert!(matches!(completed, ServerTransferEvent::FileCompleted { .. }));
469
470 let ended = event_rx.recv().await.unwrap();
471 assert!(matches!(ended, ServerTransferEvent::SessionEnded { .. }));
472
473 server_handle.stop();
475 server_handle.wait().await;
476
477 let _ = tokio::fs::remove_dir_all(&temp_dir).await;
478 }
479
480 #[tokio::test]
481 async fn test_server_pin_protection_and_cancellation() {
482 let temp_dir = std::env::temp_dir().join(format!("lanflare_pin_test_{}", uuid::Uuid::new_v4()));
483 tokio::fs::create_dir_all(&temp_dir).await.unwrap();
484
485 let config = AppConfig {
486 alias: "Protected Node".to_string(),
487 port: 0,
488 download_dir: temp_dir.clone(),
489 auto_accept: true,
490 pin: Some("54321".to_string()),
491 ..Default::default()
492 };
493
494 let registry = PeerRegistry::new();
495 let server_handle = start_server(config, None, registry, None)
496 .await
497 .expect("Server should bind");
498
499 let port = server_handle.bound_port();
500 let client = reqwest::Client::new();
501 let base_url = format!("http://127.0.0.1:{port}");
502
503 let mut files = HashMap::new();
504 files.insert(
505 "file_1".to_string(),
506 FileDto {
507 id: "file_1".to_string(),
508 file_name: "test.txt".to_string(),
509 size: 10,
510 file_type: "text/plain".to_string(),
511 sha256: None,
512 preview: None,
513 metadata: None,
514 },
515 );
516
517 let prep_req = PrepareUploadRequestDtoV2 {
518 info: RegisterDtoV2 {
519 alias: "Sender".to_string(),
520 version: "2.0".to_string(),
521 device_model: None,
522 device_type: None,
523 fingerprint: "FP".to_string(),
524 port: 53317,
525 protocol: ProtocolType::Http,
526 download: false,
527 },
528 files,
529 };
530
531 let resp_no_pin = client
533 .post(format!("{base_url}/api/localsend/v2/prepare-upload"))
534 .json(&prep_req)
535 .send()
536 .await
537 .unwrap();
538 assert_eq!(resp_no_pin.status(), reqwest::StatusCode::UNAUTHORIZED);
539
540 let resp_wrong_pin = client
542 .post(format!("{base_url}/api/localsend/v2/prepare-upload?pin=00000"))
543 .json(&prep_req)
544 .send()
545 .await
546 .unwrap();
547 assert_eq!(resp_wrong_pin.status(), reqwest::StatusCode::UNAUTHORIZED);
548
549 let resp_ok_pin = client
551 .post(format!("{base_url}/api/localsend/v2/prepare-upload?pin=54321"))
552 .json(&prep_req)
553 .send()
554 .await
555 .unwrap();
556 assert_eq!(resp_ok_pin.status(), reqwest::StatusCode::OK);
557
558 let prep_dto: PrepareUploadResponseDtoV2 = resp_ok_pin.json().await.unwrap();
559
560 let cancel_resp = client
562 .post(format!(
563 "{base_url}/api/localsend/v2/cancel?sessionId={}",
564 prep_dto.session_id
565 ))
566 .send()
567 .await
568 .unwrap();
569 assert_eq!(cancel_resp.status(), reqwest::StatusCode::OK);
570
571 let token = prep_dto.files.get("file_1").unwrap();
573 let upload_resp = client
574 .post(format!(
575 "{base_url}/api/localsend/v2/upload?sessionId={}&fileId=file_1&token={}",
576 prep_dto.session_id, token
577 ))
578 .body(vec![0u8; 10])
579 .send()
580 .await
581 .unwrap();
582 assert_eq!(upload_resp.status(), reqwest::StatusCode::FORBIDDEN);
583
584 server_handle.stop();
585 server_handle.wait().await;
586 let _ = tokio::fs::remove_dir_all(&temp_dir).await;
587 }
588
589 #[tokio::test]
590 async fn test_server_upload_checksum_mismatch_returns_422() {
591 let temp_dir = std::env::temp_dir().join(format!("lanflare_chk_test_{}", uuid::Uuid::new_v4()));
592 tokio::fs::create_dir_all(&temp_dir).await.unwrap();
593
594 let config = AppConfig {
595 alias: "Checksum Node".to_string(),
596 port: 0,
597 download_dir: temp_dir.clone(),
598 auto_accept: true,
599 pin: None,
600 ..Default::default()
601 };
602
603 let registry = PeerRegistry::new();
604 let server_handle = start_server(config, None, registry, None)
605 .await
606 .expect("Server should bind");
607
608 let port = server_handle.bound_port();
609 let client = reqwest::Client::new();
610 let base_url = format!("http://127.0.0.1:{port}");
611
612 let mut files = HashMap::new();
613 files.insert(
614 "file_chk".to_string(),
615 FileDto {
616 id: "file_chk".to_string(),
617 file_name: "corrupt.txt".to_string(),
618 size: 5,
619 file_type: "text/plain".to_string(),
620 sha256: Some("0000000000000000000000000000000000000000000000000000000000000000".to_string()),
622 preview: None,
623 metadata: None,
624 },
625 );
626
627 let prep_req = PrepareUploadRequestDtoV2 {
628 info: RegisterDtoV2 {
629 alias: "Sender".to_string(),
630 version: "2.0".to_string(),
631 device_model: None,
632 device_type: None,
633 fingerprint: "FP".to_string(),
634 port: 53317,
635 protocol: ProtocolType::Http,
636 download: false,
637 },
638 files,
639 };
640
641 let prep_resp = client
642 .post(format!("{base_url}/api/localsend/v2/prepare-upload"))
643 .json(&prep_req)
644 .send()
645 .await
646 .unwrap();
647 assert_eq!(prep_resp.status(), reqwest::StatusCode::OK);
648
649 let prep_dto: PrepareUploadResponseDtoV2 = prep_resp.json().await.unwrap();
650 let token = prep_dto.files.get("file_chk").unwrap();
651
652 let upload_resp = client
654 .post(format!(
655 "{base_url}/api/localsend/v2/upload?sessionId={}&fileId=file_chk&token={}",
656 prep_dto.session_id, token
657 ))
658 .body(b"hello".to_vec())
659 .send()
660 .await
661 .unwrap();
662
663 assert_eq!(upload_resp.status(), reqwest::StatusCode::UNPROCESSABLE_ENTITY);
665
666 server_handle.stop();
667 server_handle.wait().await;
668 let _ = tokio::fs::remove_dir_all(&temp_dir).await;
669 }
670
671 #[tokio::test]
672 async fn test_server_https_mtls() {
673 let temp_dir = std::env::temp_dir().join(format!("lanflare_tls_test_{}", uuid::Uuid::new_v4()));
674 tokio::fs::create_dir_all(&temp_dir).await.unwrap();
675
676 let server_ident = crate::crypto::cert::generate_self_signed().unwrap();
677 let client_ident = crate::crypto::cert::generate_self_signed().unwrap();
678
679 let config = AppConfig {
680 alias: "Secure TLS Node".to_string(),
681 port: 0,
682 download_dir: temp_dir.clone(),
683 auto_accept: true,
684 pin: None,
685 ..Default::default()
686 };
687
688 let registry = PeerRegistry::new();
689 let server_handle = start_server(config, Some(server_ident.clone()), registry, None)
690 .await
691 .expect("HTTPS Server should bind");
692
693 let port = server_handle.bound_port();
694
695 let client_cert_pkcs8 = reqwest::Identity::from_pem(
697 format!("{}\n{}", client_ident.cert_pem, client_ident.key_pem).as_bytes(),
698 )
699 .unwrap();
700
701 let client = reqwest::Client::builder()
702 .danger_accept_invalid_certs(true) .identity(client_cert_pkcs8)
704 .build()
705 .unwrap();
706
707 let base_url = format!("https://127.0.0.1:{port}");
708
709 let info_resp = client
710 .get(format!("{base_url}/api/localsend/v2/info"))
711 .send()
712 .await
713 .expect("TLS request should succeed");
714 assert_eq!(info_resp.status(), reqwest::StatusCode::OK);
715
716 let info_dto: InfoResponseDtoV2 = info_resp.json().await.unwrap();
717 assert_eq!(info_dto.alias, "Secure TLS Node");
718 assert_eq!(info_dto.fingerprint, server_ident.fingerprint);
719
720 server_handle.stop();
721 server_handle.wait().await;
722 let _ = tokio::fs::remove_dir_all(&temp_dir).await;
723 }
724}