Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ rand = "0.9.0"
rbtag = "0.3.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
socket2 = "0.5"
strum = "0.27.1"
strum_macros = "0.27.1"
thiserror = "2.0.11"
Expand Down
43 changes: 36 additions & 7 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener};

use actix_cors::Cors;
use actix_web::{web, App, HttpServer};
use log::info;
use log::{info, warn};
use services::{download::download, upload::upload, version::version, websocket::websocket};
use socket2::{Domain, Protocol, Socket, Type};

use crate::http::services::keep::keep;

mod response;
mod services;

/// Binds a dual-stack listener on `[::]:port` that accepts both IPv6 and IPv4
/// (via IPv4-mapped addresses). If IPv6 is unavailable (e.g. disabled in the
/// container), falls back to an IPv4-only listener on `0.0.0.0:port`.
fn bind_dual_stack(port: u16) -> std::io::Result<TcpListener> {
let v6 = (|| -> std::io::Result<Socket> {
let socket = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?;
// Explicitly accept IPv4 too; the OS default differs (e.g. Windows/BSD).
socket.set_only_v6(false)?;
#[cfg(unix)]
socket.set_reuse_address(true)?;
socket.bind(&SocketAddr::from((Ipv6Addr::UNSPECIFIED, port)).into())?;
socket.listen(1024)?;
Ok(socket)
})();

let listener = match v6 {
Ok(socket) => TcpListener::from(socket),
Err(e) => {
warn!("IPv6 dual-stack bind failed ({e}); falling back to 0.0.0.0:{port}");
TcpListener::bind((Ipv4Addr::UNSPECIFIED, port))?
}
};
listener.set_nonblocking(true)?;
Ok(listener)
}

pub async fn start_http() -> anyhow::Result<()> {
let server = HttpServer::new(|| {
App::new()
Expand All @@ -27,11 +56,11 @@ pub async fn start_http() -> anyhow::Result<()> {
)
});
let port = std::env::var("PORT").unwrap_or_else(|_| "24153".to_string());
if !port.chars().all(char::is_numeric) {
anyhow::bail!("PORT must be a number");
}
let ip = format!("0.0.0.0:{}", port);
info!("http server listening on {}", ip);
server.bind(ip)?.run().await?;
let port: u16 = port
.parse()
.map_err(|_| anyhow::anyhow!("PORT must be a number between 0 and 65535"))?;
let listener = bind_dual_stack(port)?;
info!("http server listening on {}", listener.local_addr()?);
server.listen(listener)?.run().await?;
Ok(())
}