use axum::{ extract::{ connect_info::ConnectInfo, ws::{Message, WebSocket, WebSocketUpgrade}, State, }, response::{Html, IntoResponse}, routing::get, Router, }; use clap::Parser; use dashmap::DashMap; use futures::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; use tokio::sync::broadcast; use tower_http::cors::CorsLayer; use tracing::{info, warn}; use uuid::Uuid; use axum_server::tls_rustls::RustlsConfig; /// LAN Share - Peer-to-peer screen sharing for local networks /// /// A lightweight WebRTC-based screen sharing application that runs entirely /// on your local network. No accounts, no cloud services — just open a /// browser and share. /// /// The server only handles signaling (peer discovery, connection setup). /// Video/audio streams flow directly between browsers via WebRTC P2P. #[derive(Parser, Debug)] #[command(name = "lanshare")] #[command(version, about, long_about = None)] struct Args { /// IP address to bind to /// /// Use 0.0.0.0 to listen on all network interfaces (default), /// 127.0.0.1 for localhost only, or a specific IP address. #[arg(short = 'H', long, default_value = "0.0.0.0", value_name = "IP")] host: String, /// Port to listen on #[arg(short, long, default_value_t = 8080, value_name = "PORT")] port: u16, /// Enable verbose/debug logging #[arg(short, long)] verbose: bool, /// Quiet mode - only show errors #[arg(short, long, conflicts_with = "verbose")] quiet: bool, /// TLS certificate file (PEM format) /// /// When provided along with --key, the server will use HTTPS/WSS. /// A plain HTTP listener is also started on --http-port for clients /// that cannot validate the certificate (e.g. iOS with self-signed certs). /// If omitted, the server runs over plain HTTP only. #[arg(short, long)] certificate: Option, /// TLS private key file (PEM format) #[arg(short, long)] key: Option, /// Port for the plain HTTP listener (used alongside HTTPS) /// /// When TLS is enabled, a second HTTP-only listener is started on this /// port so that iOS devices (which reject self-signed WSS certs) can /// connect via ws:// instead. #[arg(long, default_value_t = 8081, value_name = "PORT")] http_port: u16, /// Trust proxy headers (X-Forwarded-For, X-Real-IP) for client IPs /// /// Enable this when running behind a reverse proxy (e.g. nginx, caddy, /// traefik). The server will use the IP from proxy headers instead of /// the direct connection address. Do NOT enable this without a trusted /// proxy, as clients can spoof these headers. #[arg(long)] trust_proxy: bool, } /// Messages sent between peers via the signaling server #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] enum SignalMessage { /// Server assigns an ID to a new peer Welcome { peer_id: String, ip: String, peers: Vec, }, /// Peer announces themselves (with optional name) Announce { name: String, }, /// Broadcast: a peer joined PeerJoined { peer_id: String, name: String, ip: String, }, /// Broadcast: a peer left PeerLeft { peer_id: String, }, /// Peer started sharing their screen StartedSharing { peer_id: String, has_audio: bool, }, /// Peer stopped sharing StoppedSharing { peer_id: String, }, /// WebRTC offer (peer-to-peer, relayed by server) Offer { from: String, to: String, sdp: String, }, /// WebRTC answer Answer { from: String, to: String, sdp: String, }, /// ICE candidate IceCandidate { from: String, to: String, candidate: String, }, /// Request to connect to a sharing peer RequestStream { from: String, to: String, }, /// Error message Error { message: String, }, } #[derive(Debug, Clone, Serialize, Deserialize)] struct PeerInfo { peer_id: String, name: String, ip: String, is_sharing: bool, has_audio: bool, } /// State for a connected peer struct PeerState { name: String, ip: String, is_sharing: bool, has_audio: bool, tx: broadcast::Sender, } /// Application state shared across all connections struct AppState { peers: DashMap, trust_proxy: bool, } impl AppState { fn new(trust_proxy: bool) -> Self { Self { peers: DashMap::new(), trust_proxy, } } fn get_peer_list(&self) -> Vec { self.peers .iter() .map(|entry| PeerInfo { peer_id: entry.key().clone(), name: entry.value().name.clone(), ip: entry.value().ip.clone(), is_sharing: entry.value().is_sharing, has_audio: entry.value().has_audio, }) .collect() } fn broadcast(&self, msg: SignalMessage, exclude: Option<&str>) { for entry in self.peers.iter() { if exclude.map_or(true, |ex| entry.key() != ex) { let _ = entry.value().tx.send(msg.clone()); } } } fn send_to(&self, peer_id: &str, msg: SignalMessage) { if let Some(peer) = self.peers.get(peer_id) { let _ = peer.tx.send(msg); } } } #[tokio::main] async fn main() { let args = Args::parse(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() ) .init(); let state = Arc::new(AppState::new(args.trust_proxy)); let app = Router::new() .route("/", get(serve_index)) .route("/ws", get(ws_handler)) .layer(CorsLayer::permissive()) .with_state(state); let addr: SocketAddr = format!("{}:{}", args.host, args.port) .parse() .expect("Invalid host:port combination"); let use_tls = args.certificate.is_some() && args.key.is_some(); println!(); println!(" ╭─────────────────────────────────────────╮"); println!(" │ \x1b[1;33mLAN Share\x1b[0m │"); println!(" │ P2P Screen Sharing for Local Networks │"); println!(" ╰─────────────────────────────────────────╯"); println!(); println!(" \x1b[1mServer running at:\x1b[0m"); if use_tls { println!(" → https://{}", addr); let http_addr: SocketAddr = format!("{}:{}", args.host, args.http_port) .parse() .expect("Invalid host:http_port combination"); println!(" → http://{} \x1b[90m(plain HTTP for iOS)\x1b[0m", http_addr); if args.host == "0.0.0.0" { if let Ok(local_ip) = local_ip_address::local_ip() { println!(" → https://{}:{}", local_ip, args.port); println!(" → http://{}:{} \x1b[90m(plain HTTP for iOS)\x1b[0m", local_ip, args.http_port); } } } else { println!(" → http://{}", addr); if args.host == "0.0.0.0" { if let Ok(local_ip) = local_ip_address::local_ip() { println!(" → http://{}:{}", local_ip, args.port); } } } if args.trust_proxy { println!(" \x1b[1;36mProxy mode:\x1b[0m trusting X-Forwarded-For / X-Real-IP headers"); } println!(); println!(" \x1b[90mOpen this URL in browsers on your local network.\x1b[0m"); println!(" \x1b[90mPress Ctrl+C to stop the server.\x1b[0m"); println!(); info!("Server starting on {}", addr); if use_tls { let cert = args.certificate.unwrap(); let key = args.key.unwrap(); let config = RustlsConfig::from_pem_file(cert, key).await.unwrap(); // Start a plain HTTP listener alongside HTTPS so that iOS devices // (which silently reject self-signed WSS certificates) can connect // via ws:// instead. let http_addr: SocketAddr = format!("{}:{}", args.host, args.http_port) .parse() .expect("Invalid host:http_port combination"); let http_app = app.clone(); info!("Plain HTTP listener on {}", http_addr); tokio::spawn(async move { let listener = tokio::net::TcpListener::bind(http_addr).await.unwrap(); axum::serve(listener, http_app.into_make_service_with_connect_info::()) .await .unwrap(); }); axum_server::bind_rustls(addr, config) .serve(app.into_make_service_with_connect_info::()) .await .unwrap(); } else { let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app.into_make_service_with_connect_info::()) .await .unwrap(); } } async fn serve_index() -> impl IntoResponse { Html(include_str!("../static/index.html")) } async fn ws_handler( ws: WebSocketUpgrade, ConnectInfo(addr): ConnectInfo, headers: axum::http::HeaderMap, State(state): State>, ) -> impl IntoResponse { let peer_ip = if state.trust_proxy { extract_proxy_ip(&headers).unwrap_or_else(|| addr.ip().to_string()) } else { addr.ip().to_string() }; ws.on_upgrade(move |socket| handle_socket(socket, state, peer_ip)) } /// Extract the client IP from proxy headers. /// Checks X-Forwarded-For first (uses the leftmost/first IP), then X-Real-IP. fn extract_proxy_ip(headers: &axum::http::HeaderMap) -> Option { // X-Forwarded-For: client, proxy1, proxy2 if let Some(forwarded_for) = headers.get("x-forwarded-for") { if let Ok(value) = forwarded_for.to_str() { if let Some(first_ip) = value.split(',').next() { let trimmed = first_ip.trim(); if !trimmed.is_empty() { return Some(trimmed.to_string()); } } } } // X-Real-IP: client if let Some(real_ip) = headers.get("x-real-ip") { if let Ok(value) = real_ip.to_str() { let trimmed = value.trim(); if !trimmed.is_empty() { return Some(trimmed.to_string()); } } } None } async fn handle_socket(socket: WebSocket, state: Arc, peer_ip: String) { let peer_id = Uuid::new_v4().to_string(); let (tx, _) = broadcast::channel::(64); // Add peer to state with default name state.peers.insert( peer_id.clone(), PeerState { name: format!("Peer {}", &peer_id[..8]), ip: peer_ip.clone(), is_sharing: false, has_audio: false, tx: tx.clone(), }, ); let peers = state.get_peer_list(); info!("Peer {} connected ({} total peers)", &peer_id[..8], peers.len()); let (mut sender, mut receiver) = socket.split(); // Send welcome message with current peer list let welcome = SignalMessage::Welcome { peer_id: peer_id.clone(), ip: peer_ip, peers: peers.into_iter().filter(|p| p.peer_id != peer_id).collect(), }; if sender .send(Message::Text(serde_json::to_string(&welcome).unwrap().into())) .await .is_err() { state.peers.remove(&peer_id); return; } // Subscribe to broadcast messages for this peer let mut rx = tx.subscribe(); let peer_id_clone = peer_id.clone(); // Task to forward broadcast messages to this peer's websocket let mut send_task = tokio::spawn(async move { loop { let timer = tokio::time::sleep(Duration::from_secs(1)); tokio::select! { msg = rx.recv() => { if let Ok(msg) = msg { let text = serde_json::to_string(&msg).unwrap(); if sender.send(Message::Text(text.into())).await.is_err() { break; } } else { break } } _ = timer => { if sender.send(Message::Ping(Default::default())).await.is_err() { break; } } } } }); // Task to handle incoming messages from this peer let state_clone = state.clone(); let peer_id_for_recv = peer_id.clone(); let mut recv_task = tokio::spawn(async move { while let Some(Ok(msg)) = receiver.next().await { if let Message::Text(text) = msg { match serde_json::from_str::(&text) { Ok(signal) => { handle_signal(&state_clone, &peer_id_for_recv, signal).await; } Err(e) => { warn!("Failed to parse message from {}: {}", &peer_id_for_recv[..8], e); } } } } }); // Wait for either task to complete (connection closed) tokio::select! { _ = &mut send_task => recv_task.abort(), _ = &mut recv_task => send_task.abort(), } // Clean up: remove peer and notify others if let Some((_, peer_state)) = state.peers.remove(&peer_id_clone) { info!( "Peer {} ({}) disconnected", &peer_id_clone[..8], peer_state.name ); state.broadcast( SignalMessage::PeerLeft { peer_id: peer_id_clone, }, None, ); } } async fn handle_signal(state: &AppState, from_peer: &str, signal: SignalMessage) { match signal { SignalMessage::Announce { name } => { let ip = state .peers .get(from_peer) .map(|p| p.ip.clone()) .unwrap_or_default(); if let Some(mut peer) = state.peers.get_mut(from_peer) { peer.name = name.clone(); } state.broadcast( SignalMessage::PeerJoined { peer_id: from_peer.to_string(), name, ip, }, Some(from_peer), ); } SignalMessage::StartedSharing { has_audio, .. } => { if let Some(mut peer) = state.peers.get_mut(from_peer) { peer.is_sharing = true; peer.has_audio = has_audio; } state.broadcast( SignalMessage::StartedSharing { peer_id: from_peer.to_string(), has_audio, }, Some(from_peer), ); } SignalMessage::StoppedSharing { .. } => { if let Some(mut peer) = state.peers.get_mut(from_peer) { peer.is_sharing = false; peer.has_audio = false; } state.broadcast( SignalMessage::StoppedSharing { peer_id: from_peer.to_string(), }, Some(from_peer), ); } SignalMessage::Offer { to, sdp, .. } => { state.send_to( &to, SignalMessage::Offer { from: from_peer.to_string(), to: to.clone(), sdp, }, ); } SignalMessage::Answer { to, sdp, .. } => { state.send_to( &to, SignalMessage::Answer { from: from_peer.to_string(), to: to.clone(), sdp, }, ); } SignalMessage::IceCandidate { to, candidate, .. } => { state.send_to( &to, SignalMessage::IceCandidate { from: from_peer.to_string(), to: to.clone(), candidate, }, ); } SignalMessage::RequestStream { to, .. } => { state.send_to( &to, SignalMessage::RequestStream { from: from_peer.to_string(), to: to.clone(), }, ); } _ => {} } }