use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::server::graceful::GracefulShutdown;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::net::UnixListener;
use tokio::sync::Mutex;
use crate::server::ShutdownCtx;
use crate::service::RouterService;
use crate::signals;
use crate::{Context, Result};
pub(super) async fn run<F: FnOnce()>(
listener: UnixListener,
socket_path: PathBuf,
router: RouterService,
addr_str: &str,
threads: usize,
ctx: ShutdownCtx,
_cancel_fn: F,
) -> Result {
let graceful = GracefulShutdown::new();
let builder = http1::Builder::new();
let signals =
signals::create_signals().with_context(|| "failed to register termination signals")?;
let handle = signals.handle();
let cancel_recv = Arc::new(Mutex::new(ctx.cancel_recv));
let shutdown = signals::wait_for_signals(signals, ctx.grace_period, cancel_recv);
tokio::pin!(shutdown);
tracing::info!(
parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
"http1 server is listening on unix socket {}",
addr_str
);
tracing::info!("press ctrl+c to shut down the server");
loop {
tokio::select! {
result = listener.accept() => {
let stream = match result {
Ok((stream, _addr)) => stream,
Err(e) => {
tracing::error!("failed to accept Unix socket connection: {:?}", e);
continue;
}
};
let svc = router.build(None);
let conn = builder.serve_connection(TokioIo::new(stream), svc);
tokio::spawn(graceful.watch(conn));
}
_ = &mut shutdown => { break; }
}
}
graceful.shutdown().await;
handle.close();
if let Err(err) = tokio::fs::remove_file(&socket_path).await
&& err.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!(
"failed to remove unix socket file {}: {:?}",
socket_path.display(),
err
);
}
tracing::warn!("termination signal caught, shutting down the server execution");
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::watch;
use crate::handler::RequestHandler;
use crate::server::{ShutdownCtx, uds};
use crate::service::RouterService;
use crate::testing::fixtures::{fixture_req_handler_opts, fixture_settings};
fn temp_sock_path(name: &str) -> std::path::PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("sws-{name}-{pid}-{nanos}.sock"))
}
fn build_router() -> RouterService {
let opts = fixture_settings("toml/handler.toml");
let req_handler_opts = fixture_req_handler_opts(opts.general, opts.advanced);
RouterService::new(RequestHandler {
opts: Arc::from(req_handler_opts),
})
}
async fn send_request(stream: &mut UnixStream, request: &[u8]) -> Vec<u8> {
stream.write_all(request).await.expect("write request");
stream.flush().await.ok();
let mut buf = Vec::with_capacity(4096);
stream.read_to_end(&mut buf).await.expect("read response");
buf
}
#[tokio::test]
async fn uds_serves_get_request_and_shuts_down_cleanly() {
let path = temp_sock_path("get");
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).expect("bind UDS listener");
let router = build_router();
let (cancel_tx, cancel_rx) = watch::channel(());
let ctx = ShutdownCtx {
grace_period: 1,
cancel_recv: Some(cancel_rx),
};
let addr_str = format!("unix:{}", path.display());
let server_path = path.clone();
let server = tokio::spawn(async move {
uds::run(listener, server_path, router, &addr_str, 1, ctx, || {}).await
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let mut stream = UnixStream::connect(&path).await.expect("connect UDS");
let resp = send_request(
&mut stream,
b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
)
.await;
let text = String::from_utf8_lossy(&resp);
assert!(
text.starts_with("HTTP/1.1 200"),
"expected 200 OK, got: {}",
text.lines().next().unwrap_or("")
);
assert!(
text.to_lowercase().contains("server: static web server"),
"expected Server header in response, got headers:\n{}",
text.split("\r\n\r\n").next().unwrap_or("")
);
let _ = cancel_tx.send(());
let result = tokio::time::timeout(std::time::Duration::from_secs(5), server)
.await
.expect("server did not shut down in time")
.expect("server task panicked");
result.expect("server returned an error");
assert!(
!path.exists(),
"expected socket file {} to be removed on shutdown",
path.display()
);
}
#[tokio::test]
async fn uds_returns_404_for_missing_path() {
let path = temp_sock_path("404");
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).expect("bind UDS listener");
let router = build_router();
let (cancel_tx, cancel_rx) = watch::channel(());
let ctx = ShutdownCtx {
grace_period: 1,
cancel_recv: Some(cancel_rx),
};
let addr_str = format!("unix:{}", path.display());
let server_path = path.clone();
let server = tokio::spawn(async move {
uds::run(listener, server_path, router, &addr_str, 1, ctx, || {}).await
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let mut stream = UnixStream::connect(&path).await.expect("connect UDS");
let resp = send_request(
&mut stream,
b"GET /this-does-not-exist HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
)
.await;
let text = String::from_utf8_lossy(&resp);
assert!(
text.starts_with("HTTP/1.1 404"),
"expected 404 Not Found, got: {}",
text.lines().next().unwrap_or("")
);
let _ = cancel_tx.send(());
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), server).await;
}
}
#[cfg(all(test, unix))]
mod uds_listener_tests {
use crate::server::listener::create_unix_listener;
use std::os::unix::fs::PermissionsExt;
fn temp_path(name: &str) -> std::path::PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("sws-uds-{name}-{pid}-{nanos}.sock"))
}
#[tokio::test]
async fn binds_when_path_is_free() {
let path = temp_path("binds");
let _ = std::fs::remove_file(&path);
let (listener, returned_path, addr_str) =
create_unix_listener(&path, None, false).expect("bind UDS");
assert_eq!(returned_path, path);
assert!(addr_str.starts_with("unix:"));
drop(listener);
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn fails_when_path_exists_and_force_is_false() {
let path = temp_path("nofroce");
let _ = std::fs::remove_file(&path);
let (listener, _, _) = create_unix_listener(&path, None, false).expect("first bind");
let err = create_unix_listener(&path, None, false).expect_err("expected EADDRINUSE error");
let msg = format!("{err:#}");
assert!(
msg.contains("failed to bind unix socket"),
"unexpected error message: {msg}"
);
drop(listener);
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn force_removes_stale_socket_then_binds() {
let path = temp_path("force");
let _ = std::fs::remove_file(&path);
let (listener, _, _) = create_unix_listener(&path, None, false).expect("seed bind");
drop(listener);
let (listener, _, _) = create_unix_listener(&path, None, true).expect("rebind with force");
drop(listener);
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn force_refuses_to_remove_non_socket_files() {
let path = temp_path("regular");
let _ = std::fs::remove_file(&path);
std::fs::write(&path, b"not a socket").expect("write regular file");
let err = create_unix_listener(&path, None, true).expect_err("expected refusal");
let msg = format!("{err:#}");
assert!(
msg.contains("refusing to overwrite non-socket file"),
"unexpected error message: {msg}"
);
assert!(path.exists());
std::fs::remove_file(&path).ok();
}
#[tokio::test]
async fn applies_requested_permission_bits() {
let path = temp_path("perms");
let _ = std::fs::remove_file(&path);
let (listener, _, _) =
create_unix_listener(&path, Some(0o600), false).expect("bind with mode");
let mode = std::fs::metadata(&path)
.expect("stat socket")
.permissions()
.mode()
& 0o7777;
assert_eq!(mode, 0o600, "expected mode 0o600, got {mode:o}");
drop(listener);
let _ = std::fs::remove_file(&path);
}
#[test]
fn rejects_paths_longer_than_sun_path_max() {
let long_name = "a".repeat(200);
let path = std::env::temp_dir().join(format!("{long_name}.sock"));
let err =
create_unix_listener(&path, None, false).expect_err("expected path-too-long error");
let msg = format!("{err:#}");
assert!(
msg.contains("is too long"),
"unexpected error message: {msg}"
);
}
}