Merge pull request #5 from TheNeikos/claude/fix-ios-websocket-xDpcb

Make TLS certificate optional and add dual HTTP/HTTPS support
This commit is contained in:
Marcel Müller 2026-02-09 10:44:46 +01:00 committed by GitHub
commit 0df86e79ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 92 additions and 17 deletions

View file

@ -50,11 +50,26 @@ struct Args {
#[arg(short, long, conflicts_with = "verbose")] #[arg(short, long, conflicts_with = "verbose")]
quiet: bool, 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)] #[arg(short, long)]
certificate: PathBuf, certificate: Option<PathBuf>,
/// TLS private key file (PEM format)
#[arg(short, long)] #[arg(short, long)]
key: PathBuf, key: Option<PathBuf>,
/// 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 /// Messages sent between peers via the signaling server
@ -199,6 +214,8 @@ async fn main() {
.parse() .parse()
.expect("Invalid host:port combination"); .expect("Invalid host:port combination");
let use_tls = args.certificate.is_some() && args.key.is_some();
println!(); println!();
println!(" ╭─────────────────────────────────────────╮"); println!(" ╭─────────────────────────────────────────╮");
println!("\x1b[1;33mLAN Share\x1b[0m │"); println!("\x1b[1;33mLAN Share\x1b[0m │");
@ -206,10 +223,25 @@ async fn main() {
println!(" ╰─────────────────────────────────────────╯"); println!(" ╰─────────────────────────────────────────╯");
println!(); println!();
println!(" \x1b[1mServer running at:\x1b[0m"); println!(" \x1b[1mServer running at:\x1b[0m");
println!(" → http://{}", addr);
if args.host == "0.0.0.0" { if use_tls {
if let Ok(local_ip) = local_ip_address::local_ip() { println!(" → https://{}", addr);
println!(" → http://{}:{}", local_ip, args.port); 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!(); println!();
@ -219,13 +251,36 @@ async fn main() {
info!("Server starting on {}", addr); 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::<SocketAddr>())
.await
.unwrap();
});
axum_server::bind_rustls(addr, config) axum_server::bind_rustls(addr, config)
.serve(app.into_make_service_with_connect_info::<SocketAddr>()) .serve(app.into_make_service_with_connect_info::<SocketAddr>())
.await .await
.unwrap(); .unwrap();
} else {
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.await
.unwrap();
}
} }
async fn serve_index() -> impl IntoResponse { async fn serve_index() -> impl IntoResponse {

View file

@ -504,7 +504,25 @@
connectionStatus.textContent = 'Establishing WebSocket connection...'; connectionStatus.textContent = 'Establishing WebSocket connection...';
state.ws = new WebSocket(wsUrl); 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.<br>' +
'Try the plain HTTP URL instead: <a href="' + httpUrl + '" style="color:#60a5fa;text-decoration:underline">' + httpUrl + '</a>';
}
}
}, 5000);
state.ws.onopen = () => { state.ws.onopen = () => {
clearTimeout(connectTimeout);
console.log('WebSocket connected'); console.log('WebSocket connected');
state.reconnectAttempts = 0; state.reconnectAttempts = 0;
connectionStatus.textContent = 'Connected, waiting for welcome...'; connectionStatus.textContent = 'Connected, waiting for welcome...';
@ -516,6 +534,7 @@
}; };
state.ws.onclose = () => { state.ws.onclose = () => {
clearTimeout(connectTimeout);
console.log('WebSocket disconnected'); console.log('WebSocket disconnected');
updateConnectionStatus(false); updateConnectionStatus(false);
@ -529,6 +548,7 @@
}; };
state.ws.onerror = (err) => { state.ws.onerror = (err) => {
clearTimeout(connectTimeout);
console.error('WebSocket error:', err); console.error('WebSocket error:', err);
}; };
} }