Skip to main content

lanflare_core/server/
mod.rs

1//! Hyper v1 HTTP & HTTPS server implementing LocalSend v2.2 specification.
2
3pub mod handlers;
4pub mod router;
5pub mod session;
6
7use std::net::SocketAddr;
8use std::sync::Arc;
9use hyper_util::rt::TokioIo;
10use rustls::pki_types::pem::PemObject;
11use rustls::pki_types::{CertificateDer, PrivateKeyDer};
12use tokio::io::{AsyncRead, AsyncWrite};
13use tokio::net::TcpListener;
14use tokio::sync::mpsc;
15use tokio_util::sync::CancellationToken;
16use tokio_util::task::TaskTracker;
17
18pub use handlers::{ServerState, ServerTransferEvent};
19pub use router::{route_request, ClientConnectionInfo};
20pub use session::{
21    DownloadFile, DownloadManager, DownloadSession, FileTransferStatus, PrepareUploadDecision,
22    SessionEndReason, SessionFile, SessionManager,
23};
24
25use crate::config::AppConfig;
26use crate::crypto::cert::TlsIdentity;
27use crate::crypto::verifier::PinnedClientCertVerifier;
28use crate::discovery::registry::PeerRegistry;
29
30/// Handle representing a running HTTP/HTTPS server instance.
31pub struct ServerHandle {
32    /// The actual port the server bound to.
33    bound_port: u16,
34    /// Cancellation token used to signal graceful shutdown.
35    cancel_token: CancellationToken,
36    /// Join handle of the background server task.
37    join_handle: tokio::task::JoinHandle<()>,
38    /// Download manager for registering/unregistering reverse transfer files.
39    download_manager: Arc<DownloadManager>,
40}
41
42impl ServerHandle {
43    /// Returns the port the server is listening on.
44    pub fn bound_port(&self) -> u16 {
45        self.bound_port
46    }
47
48    /// Returns a reference to the download manager.
49    pub fn download_manager(&self) -> &Arc<DownloadManager> {
50        &self.download_manager
51    }
52
53    /// Signals the server to stop accepting connections and terminate.
54    pub fn stop(&self) {
55        self.cancel_token.cancel();
56    }
57
58    /// Waits for the server task to finish completely.
59    pub async fn wait(self) {
60        let _ = self.join_handle.await;
61    }
62}
63
64/// Starts an HTTP/HTTPS server listening on the configured address.
65///
66/// If `tls_identity` is `Some`, the server terminates TLS using the identity
67/// and validates client certificates if present (mTLS).
68/// If `tls_identity` is `None`, standard plaintext HTTP/1.1 is served.
69pub async fn start_server(
70    config: AppConfig,
71    tls_identity: Option<TlsIdentity>,
72    registry: Arc<PeerRegistry>,
73    event_tx: Option<mpsc::Sender<ServerTransferEvent>>,
74) -> anyhow::Result<ServerHandle> {
75    let cancel_token = CancellationToken::new();
76    let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
77    let listener = TcpListener::bind(addr).await?;
78    let bound_port = listener.local_addr()?.port();
79
80    let cert_fingerprint = match tls_identity.as_ref() {
81        Some(ident) => ident.fingerprint.clone(),
82        None => String::new(),
83    };
84
85    let tls_acceptor = match tls_identity.as_ref() {
86        Some(ident) => {
87            let cert_der = CertificateDer::from_pem_slice(ident.cert_pem.as_bytes())?;
88            let key_der = PrivateKeyDer::from_pem_slice(ident.key_pem.as_bytes())?;
89
90            // LocalSend spec: Client cert is optional in HTTP v2 server, but verified if provided
91            let client_verifier = PinnedClientCertVerifier::new(&ident.cert_pem, false, None)?;
92
93            let server_config = rustls::ServerConfig::builder()
94                .with_client_cert_verifier(Arc::new(client_verifier))
95                .with_single_cert(vec![cert_der], key_der)?;
96
97            Some(tokio_rustls::TlsAcceptor::from(Arc::new(server_config)))
98        }
99        None => None,
100    };
101
102    let download_manager = Arc::new(DownloadManager::new());
103
104    let state = ServerState {
105        config: Arc::new(config),
106        cert_fingerprint,
107        session_manager: Arc::new(SessionManager::new()),
108        download_manager: download_manager.clone(),
109        registry,
110        event_tx,
111    };
112
113    let cancel = cancel_token.clone();
114    let join_handle = tokio::spawn(async move {
115        let tracker = TaskTracker::new();
116
117        loop {
118            tokio::select! {
119                _ = cancel.cancelled() => {
120                    tracing::info!("Server shutdown signaled, closing listener");
121                    break;
122                }
123                accept_res = listener.accept() => {
124                    let (tcp_stream, remote_addr) = match accept_res {
125                        Ok(res) => res,
126                        Err(err) => {
127                            tracing::warn!("Failed to accept incoming TCP connection: {err}");
128                            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
129                            continue;
130                        }
131                    };
132
133                    let _ = tcp_stream.set_nodelay(true);
134                    let tls_acceptor = tls_acceptor.clone();
135                    let state = state.clone();
136                    let conn_cancel = cancel.clone();
137
138                    tracker.spawn(async move {
139                        tokio::select! {
140                            _ = conn_cancel.cancelled() => {}
141                            _ = serve_connection(tcp_stream, remote_addr, tls_acceptor, state) => {}
142                        }
143                    });
144                }
145            }
146        }
147
148        tracker.close();
149        tracker.wait().await;
150        tracing::info!("Server terminated all active connections");
151    });
152
153    Ok(ServerHandle {
154        bound_port,
155        cancel_token,
156        join_handle,
157        download_manager,
158    })
159}
160
161async fn serve_connection(
162    tcp_stream: tokio::net::TcpStream,
163    remote_addr: SocketAddr,
164    tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
165    state: ServerState,
166) {
167    if let Some(acceptor) = tls_acceptor {
168        match acceptor.accept(tcp_stream).await {
169            Ok(tls_stream) => {
170                let (_, conn) = tls_stream.get_ref();
171                let client_cert_fingerprint = conn
172                    .peer_certificates()
173                    .and_then(|certs| certs.first())
174                    .map(|c| crate::crypto::cert::fingerprint_from_cert_der(c.as_ref()));
175
176                let client_info = ClientConnectionInfo {
177                    remote_ip: remote_addr.ip(),
178                    cert_fingerprint: client_cert_fingerprint,
179                };
180
181                serve_http_stream(tls_stream, client_info, state).await;
182            }
183            Err(err) => {
184                tracing::debug!("TLS handshake failed with {remote_addr}: {err}");
185            }
186        }
187    } else {
188        let client_info = ClientConnectionInfo {
189            remote_ip: remote_addr.ip(),
190            cert_fingerprint: None,
191        };
192        serve_http_stream(tcp_stream, client_info, state).await;
193    }
194}
195
196async fn serve_http_stream<IO>(stream: IO, client_info: ClientConnectionInfo, state: ServerState)
197where
198    IO: AsyncRead + AsyncWrite + Unpin + Send + 'static,
199{
200    let io = TokioIo::new(stream);
201    let service = hyper::service::service_fn(move |req| {
202        let client_info = client_info.clone();
203        let state = state.clone();
204        async move { route_request(req, client_info, &state).await }
205    });
206
207    if let Err(err) = hyper::server::conn::http1::Builder::new()
208        .max_buf_size(128 * 1024)
209        .serve_connection(io, service)
210        .await
211    {
212        tracing::debug!("HTTP/1.1 connection error: {err}");
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use std::collections::HashMap;
220    use sha2::{Digest, Sha256};
221    use lanflare_proto::discovery::{InfoResponseDtoV2, ProtocolType, RegisterDtoV2, RegisterResponseDtoV2};
222    use lanflare_proto::transfer::{FileDto, PrepareUploadRequestDtoV2, PrepareUploadResponseDtoV2};
223
224    #[tokio::test]
225    async fn test_server_plain_http_lifecycle_and_endpoints() {
226        let temp_dir = std::env::temp_dir().join(format!("lanflare_test_{}", uuid::Uuid::new_v4()));
227        tokio::fs::create_dir_all(&temp_dir).await.unwrap();
228
229        let config = AppConfig {
230            alias: "Test Node 1".to_string(),
231            port: 0, // OS assigns random port
232            download_dir: temp_dir.clone(),
233            auto_accept: true,
234            pin: None,
235        };
236
237        let registry = PeerRegistry::new();
238        let (event_tx, mut event_rx) = mpsc::channel(16);
239
240        let server_handle = start_server(config, None, registry, Some(event_tx))
241            .await
242            .expect("Server should bind");
243
244        let port = server_handle.bound_port();
245        assert!(port > 0);
246
247        let client = reqwest::Client::new();
248        let base_url = format!("http://127.0.0.1:{port}");
249
250        // 1. GET /api/localsend/v2/info
251        let info_resp = client
252            .get(format!("{base_url}/api/localsend/v2/info"))
253            .send()
254            .await
255            .expect("Info request should succeed");
256        assert_eq!(info_resp.status(), reqwest::StatusCode::OK);
257        let info_dto: InfoResponseDtoV2 = info_resp.json().await.unwrap();
258        assert_eq!(info_dto.alias, "Test Node 1");
259
260        // 2. POST /api/localsend/v2/register
261        let reg_payload = RegisterDtoV2 {
262            alias: "Remote Peer".to_string(),
263            version: "2.0".to_string(),
264            device_model: Some("Linux".to_string()),
265            device_type: Some(lanflare_proto::discovery::DeviceType::Desktop),
266            fingerprint: "PEER_FINGERPRINT_123".to_string(),
267            port: 53317,
268            protocol: ProtocolType::Http,
269            download: false,
270        };
271
272        let reg_resp = client
273            .post(format!("{base_url}/api/localsend/v2/register"))
274            .json(&reg_payload)
275            .send()
276            .await
277            .expect("Register request should succeed");
278        assert_eq!(reg_resp.status(), reqwest::StatusCode::OK);
279        let reg_res_dto: RegisterResponseDtoV2 = reg_resp.json().await.unwrap();
280        assert_eq!(reg_res_dto.alias, "Test Node 1");
281
282        // Verify DeviceRegistered event
283        match event_rx.recv().await {
284            Some(ServerTransferEvent::DeviceRegistered { ip, info }) => {
285                assert_eq!(ip.to_string(), "127.0.0.1");
286                assert_eq!(info.alias, "Remote Peer");
287            }
288            other => panic!("Expected DeviceRegistered event, got: {other:?}"),
289        }
290
291        // 3. POST /api/localsend/v2/prepare-upload
292        let file_content = b"Hello, LanFlare LocalSend Transfer Engine!";
293        let mut hasher = Sha256::new();
294        hasher.update(file_content);
295        let expected_sha256 = format!("{:x}", hasher.finalize());
296
297        let mut files = HashMap::new();
298        files.insert(
299            "file_1".to_string(),
300            FileDto {
301                id: "file_1".to_string(),
302                file_name: "greeting.txt".to_string(),
303                size: file_content.len() as u64,
304                file_type: "text/plain".to_string(),
305                sha256: Some(expected_sha256.clone()),
306                preview: None,
307                metadata: None,
308            },
309        );
310
311        let prep_req = PrepareUploadRequestDtoV2 {
312            info: reg_payload.clone(),
313            files,
314        };
315
316        let prep_resp = client
317            .post(format!("{base_url}/api/localsend/v2/prepare-upload"))
318            .json(&prep_req)
319            .send()
320            .await
321            .expect("Prepare upload should succeed");
322        assert_eq!(prep_resp.status(), reqwest::StatusCode::OK);
323
324        let prep_res_dto: PrepareUploadResponseDtoV2 = prep_resp.json().await.unwrap();
325        assert!(!prep_res_dto.session_id.is_empty());
326        let file_token = prep_res_dto.files.get("file_1").expect("Token for file_1");
327
328        // 4. POST /api/localsend/v2/upload (Streaming upload)
329        let upload_url = format!(
330            "{base_url}/api/localsend/v2/upload?sessionId={}&fileId=file_1&token={}",
331            prep_res_dto.session_id, file_token
332        );
333
334        let upload_resp = client
335            .post(upload_url)
336            .body(file_content.to_vec())
337            .send()
338            .await
339            .expect("Upload request should succeed");
340        assert_eq!(upload_resp.status(), reqwest::StatusCode::OK);
341
342        // Check disk file content
343        let saved_file = temp_dir.join("greeting.txt");
344        assert!(saved_file.exists(), "File must exist on disk");
345        let saved_bytes = tokio::fs::read(&saved_file).await.unwrap();
346        assert_eq!(saved_bytes, file_content);
347
348        // Verify FileStarted, FileCompleted, and SessionEnded events
349        let started = event_rx.recv().await.unwrap();
350        assert!(matches!(started, ServerTransferEvent::FileStarted { .. }));
351
352        let completed = event_rx.recv().await.unwrap();
353        assert!(matches!(completed, ServerTransferEvent::FileCompleted { .. }));
354
355        let ended = event_rx.recv().await.unwrap();
356        assert!(matches!(ended, ServerTransferEvent::SessionEnded { .. }));
357
358        // 5. Clean server shutdown
359        server_handle.stop();
360        server_handle.wait().await;
361
362        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
363    }
364
365    #[tokio::test]
366    async fn test_server_pin_protection_and_cancellation() {
367        let temp_dir = std::env::temp_dir().join(format!("lanflare_pin_test_{}", uuid::Uuid::new_v4()));
368        tokio::fs::create_dir_all(&temp_dir).await.unwrap();
369
370        let config = AppConfig {
371            alias: "Protected Node".to_string(),
372            port: 0,
373            download_dir: temp_dir.clone(),
374            auto_accept: true,
375            pin: Some("54321".to_string()),
376        };
377
378        let registry = PeerRegistry::new();
379        let server_handle = start_server(config, None, registry, None)
380            .await
381            .expect("Server should bind");
382
383        let port = server_handle.bound_port();
384        let client = reqwest::Client::new();
385        let base_url = format!("http://127.0.0.1:{port}");
386
387        let mut files = HashMap::new();
388        files.insert(
389            "file_1".to_string(),
390            FileDto {
391                id: "file_1".to_string(),
392                file_name: "test.txt".to_string(),
393                size: 10,
394                file_type: "text/plain".to_string(),
395                sha256: None,
396                preview: None,
397                metadata: None,
398            },
399        );
400
401        let prep_req = PrepareUploadRequestDtoV2 {
402            info: RegisterDtoV2 {
403                alias: "Sender".to_string(),
404                version: "2.0".to_string(),
405                device_model: None,
406                device_type: None,
407                fingerprint: "FP".to_string(),
408                port: 53317,
409                protocol: ProtocolType::Http,
410                download: false,
411            },
412            files,
413        };
414
415        // Prepare upload without PIN -> 401
416        let resp_no_pin = client
417            .post(format!("{base_url}/api/localsend/v2/prepare-upload"))
418            .json(&prep_req)
419            .send()
420            .await
421            .unwrap();
422        assert_eq!(resp_no_pin.status(), reqwest::StatusCode::UNAUTHORIZED);
423
424        // Prepare upload with wrong PIN -> 401
425        let resp_wrong_pin = client
426            .post(format!("{base_url}/api/localsend/v2/prepare-upload?pin=00000"))
427            .json(&prep_req)
428            .send()
429            .await
430            .unwrap();
431        assert_eq!(resp_wrong_pin.status(), reqwest::StatusCode::UNAUTHORIZED);
432
433        // Prepare upload with correct PIN -> 200
434        let resp_ok_pin = client
435            .post(format!("{base_url}/api/localsend/v2/prepare-upload?pin=54321"))
436            .json(&prep_req)
437            .send()
438            .await
439            .unwrap();
440        assert_eq!(resp_ok_pin.status(), reqwest::StatusCode::OK);
441
442        let prep_dto: PrepareUploadResponseDtoV2 = resp_ok_pin.json().await.unwrap();
443
444        // Cancel session via POST /api/localsend/v2/cancel?sessionId=...
445        let cancel_resp = client
446            .post(format!(
447                "{base_url}/api/localsend/v2/cancel?sessionId={}",
448                prep_dto.session_id
449            ))
450            .send()
451            .await
452            .unwrap();
453        assert_eq!(cancel_resp.status(), reqwest::StatusCode::OK);
454
455        // Subsequent upload on cancelled session should fail with 403
456        let token = prep_dto.files.get("file_1").unwrap();
457        let upload_resp = client
458            .post(format!(
459                "{base_url}/api/localsend/v2/upload?sessionId={}&fileId=file_1&token={}",
460                prep_dto.session_id, token
461            ))
462            .body(vec![0u8; 10])
463            .send()
464            .await
465            .unwrap();
466        assert_eq!(upload_resp.status(), reqwest::StatusCode::FORBIDDEN);
467
468        server_handle.stop();
469        server_handle.wait().await;
470        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
471    }
472
473    #[tokio::test]
474    async fn test_server_upload_checksum_mismatch_returns_422() {
475        let temp_dir = std::env::temp_dir().join(format!("lanflare_chk_test_{}", uuid::Uuid::new_v4()));
476        tokio::fs::create_dir_all(&temp_dir).await.unwrap();
477
478        let config = AppConfig {
479            alias: "Checksum Node".to_string(),
480            port: 0,
481            download_dir: temp_dir.clone(),
482            auto_accept: true,
483            pin: None,
484        };
485
486        let registry = PeerRegistry::new();
487        let server_handle = start_server(config, None, registry, None)
488            .await
489            .expect("Server should bind");
490
491        let port = server_handle.bound_port();
492        let client = reqwest::Client::new();
493        let base_url = format!("http://127.0.0.1:{port}");
494
495        let mut files = HashMap::new();
496        files.insert(
497            "file_chk".to_string(),
498            FileDto {
499                id: "file_chk".to_string(),
500                file_name: "corrupt.txt".to_string(),
501                size: 5,
502                file_type: "text/plain".to_string(),
503                // Wrong expected sha256
504                sha256: Some("0000000000000000000000000000000000000000000000000000000000000000".to_string()),
505                preview: None,
506                metadata: None,
507            },
508        );
509
510        let prep_req = PrepareUploadRequestDtoV2 {
511            info: RegisterDtoV2 {
512                alias: "Sender".to_string(),
513                version: "2.0".to_string(),
514                device_model: None,
515                device_type: None,
516                fingerprint: "FP".to_string(),
517                port: 53317,
518                protocol: ProtocolType::Http,
519                download: false,
520            },
521            files,
522        };
523
524        let prep_resp = client
525            .post(format!("{base_url}/api/localsend/v2/prepare-upload"))
526            .json(&prep_req)
527            .send()
528            .await
529            .unwrap();
530        assert_eq!(prep_resp.status(), reqwest::StatusCode::OK);
531
532        let prep_dto: PrepareUploadResponseDtoV2 = prep_resp.json().await.unwrap();
533        let token = prep_dto.files.get("file_chk").unwrap();
534
535        // Send 5 bytes
536        let upload_resp = client
537            .post(format!(
538                "{base_url}/api/localsend/v2/upload?sessionId={}&fileId=file_chk&token={}",
539                prep_dto.session_id, token
540            ))
541            .body(b"hello".to_vec())
542            .send()
543            .await
544            .unwrap();
545
546        // LocalSend spec Section 4.2: 422 Checksum mismatch
547        assert_eq!(upload_resp.status(), reqwest::StatusCode::UNPROCESSABLE_ENTITY);
548
549        server_handle.stop();
550        server_handle.wait().await;
551        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
552    }
553
554    #[tokio::test]
555    async fn test_server_https_mtls() {
556        let temp_dir = std::env::temp_dir().join(format!("lanflare_tls_test_{}", uuid::Uuid::new_v4()));
557        tokio::fs::create_dir_all(&temp_dir).await.unwrap();
558
559        let server_ident = crate::crypto::cert::generate_self_signed().unwrap();
560        let client_ident = crate::crypto::cert::generate_self_signed().unwrap();
561
562        let config = AppConfig {
563            alias: "Secure TLS Node".to_string(),
564            port: 0,
565            download_dir: temp_dir.clone(),
566            auto_accept: true,
567            pin: None,
568        };
569
570        let registry = PeerRegistry::new();
571        let server_handle = start_server(config, Some(server_ident.clone()), registry, None)
572            .await
573            .expect("HTTPS Server should bind");
574
575        let port = server_handle.bound_port();
576
577        // Build reqwest client with client cert and trusting server cert
578        let client_cert_pkcs8 = reqwest::Identity::from_pem(
579            format!("{}\n{}", client_ident.cert_pem, client_ident.key_pem).as_bytes(),
580        )
581        .unwrap();
582
583        let client = reqwest::Client::builder()
584            .danger_accept_invalid_certs(true) // Self-signed in test
585            .identity(client_cert_pkcs8)
586            .build()
587            .unwrap();
588
589        let base_url = format!("https://127.0.0.1:{port}");
590
591        let info_resp = client
592            .get(format!("{base_url}/api/localsend/v2/info"))
593            .send()
594            .await
595            .expect("TLS request should succeed");
596        assert_eq!(info_resp.status(), reqwest::StatusCode::OK);
597
598        let info_dto: InfoResponseDtoV2 = info_resp.json().await.unwrap();
599        assert_eq!(info_dto.alias, "Secure TLS Node");
600        assert_eq!(info_dto.fingerprint, server_ident.fingerprint);
601
602        server_handle.stop();
603        server_handle.wait().await;
604        let _ = tokio::fs::remove_dir_all(&temp_dir).await;
605    }
606}