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