diff --git a/lanshare/src/main.rs b/lanshare/src/main.rs index 04fd728..bb53c99 100644 --- a/lanshare/src/main.rs +++ b/lanshare/src/main.rs @@ -50,11 +50,26 @@ struct Args { #[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: PathBuf, + certificate: Option, + /// TLS private key file (PEM format) #[arg(short, long)] - key: PathBuf, + 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, } /// Messages sent between peers via the signaling server @@ -199,6 +214,8 @@ async fn main() { .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 │"); @@ -206,10 +223,25 @@ async fn main() { println!(" ╰─────────────────────────────────────────╯"); println!(); println!(" \x1b[1mServer running at:\x1b[0m"); - 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 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); + } } } println!(); @@ -219,13 +251,36 @@ async fn main() { 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(); - let config = RustlsConfig::from_pem_file(args.certificate, args.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(); + 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 { diff --git a/lanshare/static/index.html b/lanshare/static/index.html index 9eb60e3..22ba69b 100644 --- a/lanshare/static/index.html +++ b/lanshare/static/index.html @@ -500,25 +500,44 @@ function connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; - + connectionStatus.textContent = 'Establishing WebSocket connection...'; state.ws = new WebSocket(wsUrl); - + + // iOS Safari silently rejects wss:// connections to servers with + // self-signed certificates — onopen never fires and the UI hangs. + // Use a timeout to detect this and show a helpful message. + const connectTimeout = setTimeout(() => { + if (state.ws && state.ws.readyState !== WebSocket.OPEN) { + console.warn('WebSocket connection timed out'); + state.ws.close(); + if (window.location.protocol === 'https:') { + const httpPort = parseInt(window.location.port || '443', 10) + 1; + const httpUrl = `http://${window.location.hostname}:${httpPort}`; + connectionStatus.innerHTML = + 'Connection failed — iOS may not support self-signed WSS certificates.
' + + 'Try the plain HTTP URL instead: ' + httpUrl + ''; + } + } + }, 5000); + state.ws.onopen = () => { + clearTimeout(connectTimeout); console.log('WebSocket connected'); state.reconnectAttempts = 0; connectionStatus.textContent = 'Connected, waiting for welcome...'; }; - + state.ws.onmessage = (event) => { const msg = JSON.parse(event.data); handleSignal(msg); }; - + state.ws.onclose = () => { + clearTimeout(connectTimeout); console.log('WebSocket disconnected'); updateConnectionStatus(false); - + if (state.reconnectAttempts < state.maxReconnectAttempts) { state.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000); @@ -527,8 +546,9 @@ setTimeout(connect, delay); } }; - + state.ws.onerror = (err) => { + clearTimeout(connectTimeout); console.error('WebSocket error:', err); }; }