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:
commit
0df86e79ff
2 changed files with 92 additions and 17 deletions
|
|
@ -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<PathBuf>,
|
||||
|
||||
/// TLS private key file (PEM format)
|
||||
#[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
|
||||
|
|
@ -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::<SocketAddr>())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
axum_server::bind_rustls(addr, config)
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.unwrap();
|
||||
axum_server::bind_rustls(addr, config)
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.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 {
|
||||
|
|
|
|||
|
|
@ -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.<br>' +
|
||||
'Try the plain HTTP URL instead: <a href="' + httpUrl + '" style="color:#60a5fa;text-decoration:underline">' + httpUrl + '</a>';
|
||||
}
|
||||
}
|
||||
}, 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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue