use crate::server::ServerState;
use rusty_time_api::{
ClientRow, ControlEndpoint, ControlRequest, ControlResponse, ServerStatsReport,
};
use std::io::{BufRead, BufReader};
use std::sync::{Arc, Mutex};
const MAX_REQUEST_BYTES: u64 = 64 * 1024;
pub fn default_path() -> String {
rusty_time_api::default_control_spec()
}
pub fn handle(request: &ControlRequest, state: &Arc<Mutex<ServerState>>) -> ControlResponse {
let Ok(guard) = state.lock() else {
return ControlResponse::Error {
message: "server state is poisoned".into(),
};
};
match request {
ControlRequest::Ping => ControlResponse::Pong {
version: env!("CARGO_PKG_VERSION").to_string(),
},
ControlRequest::ServerStats => {
let stats = guard.clients.stats;
let uptime_s = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().saturating_sub(guard.started_unix))
.unwrap_or(0);
ControlResponse::ServerStats(ServerStatsReport {
ntp_requests: stats.requests,
ntp_responses: stats.responses,
dropped_rate_limit: stats.dropped_rate_limit,
kiss_of_death: stats.kiss_of_death,
interleaved_responses: stats.interleaved_responses,
refused: stats.refused,
clients_tracked: guard.clients.len(),
clients_evicted: stats.evicted,
uptime_s,
stratum: guard.stratum,
})
}
ControlRequest::Clients { limit } => {
let now = rusty_time_clock::SystemClock.mono_s().unwrap_or_default();
let rows = guard
.clients
.most_recent((*limit).min(4096))
.into_iter()
.map(|(addr, rec)| ClientRow {
address: addr.to_string(),
last_seen_ago_s: (now - rec.last_seen).max(0.0),
requests: rec.requests,
responses: rec.responses,
dropped: rec.dropped,
interleaved: rec.interleaved_now,
})
.collect();
ControlResponse::Clients { rows }
}
ControlRequest::NtsData => ControlResponse::NtsData {
master_key_ids: guard.ring.key_ids(),
},
}
}
use rusty_time_clock::ClockRead;
pub fn serve(spec: &str, state: Arc<Mutex<ServerState>>) -> Result<(), String> {
match rusty_time_api::control_endpoint(spec) {
ControlEndpoint::UnixPath(path) => serve_unix(&path, state),
ControlEndpoint::Loopback(port) => serve_loopback(port, state),
}
}
#[cfg(unix)]
fn serve_unix(path: &str, state: Arc<Mutex<ServerState>>) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixListener;
let _ = std::fs::remove_file(path);
let listener = UnixListener::bind(path).map_err(|e| format!("binding {path}: {e}"))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.map_err(|e| format!("securing {path}: {e}"))?;
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
let state = Arc::clone(&state);
std::thread::spawn(move || {
let Ok(writer) = stream.try_clone() else {
return;
};
let _ = serve_connection(stream, writer, &state);
});
}
Ok(())
}
#[cfg(not(unix))]
fn serve_unix(_path: &str, _state: Arc<Mutex<ServerState>>) -> Result<(), String> {
Err("unix domain sockets are not available on this platform".to_string())
}
fn serve_loopback(port: u16, state: Arc<Mutex<ServerState>>) -> Result<(), String> {
use std::net::TcpListener;
let bind = format!("127.0.0.1:{port}");
let listener = TcpListener::bind(&bind).map_err(|e| format!("binding {bind}: {e}"))?;
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
let state = Arc::clone(&state);
std::thread::spawn(move || {
let Ok(writer) = stream.try_clone() else {
return;
};
let _ = serve_connection(stream, writer, &state);
});
}
Ok(())
}
fn serve_connection<R, W>(
read: R,
mut writer: W,
state: &Arc<Mutex<ServerState>>,
) -> Result<(), String>
where
R: std::io::Read,
W: std::io::Write,
{
let mut reader = BufReader::new(read.take(MAX_REQUEST_BYTES));
let mut line = String::new();
if reader.read_line(&mut line).map_err(|e| e.to_string())? == 0 {
return Ok(());
}
let response = match serde_json::from_str::<ControlRequest>(line.trim()) {
Ok(request) => handle(&request, state),
Err(e) => ControlResponse::Error {
message: format!("unrecognized request: {e}"),
},
};
let mut body = serde_json::to_string(&response).map_err(|e| e.to_string())?;
body.push('\n');
writer
.write_all(body.as_bytes())
.map_err(|e| e.to_string())?;
writer.flush().map_err(|e| e.to_string())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rusty_time_core::server::{ClientTable, RateLimitConfig};
use rusty_time_nts::cookie::{KeyRing, MasterKey};
fn state() -> Arc<Mutex<ServerState>> {
let mut ring = KeyRing::new(3);
ring.rotate_in(MasterKey {
id: 0xABCD,
key: [7; 32],
});
Arc::new(Mutex::new(ServerState {
clients: ClientTable::new(64, RateLimitConfig::default()),
ring,
stratum: 2,
started_unix: 1_000,
}))
}
#[test]
fn ping_reports_the_version() {
let s = state();
match handle(&ControlRequest::Ping, &s) {
ControlResponse::Pong { version } => {
assert_eq!(version, env!("CARGO_PKG_VERSION"));
}
other => panic!("expected pong, got {other:?}"),
}
}
#[test]
fn serverstats_reflects_real_traffic() {
let s = state();
{
let mut guard = s.lock().expect("lock");
let peer: std::net::IpAddr = "192.0.2.7".parse().expect("addr");
for _ in 0..12 {
let _ = guard.clients.admit(&peer, 0.0);
}
}
match handle(&ControlRequest::ServerStats, &s) {
ControlResponse::ServerStats(r) => {
assert_eq!(r.ntp_requests, 12);
assert!(r.ntp_responses > 0 && r.ntp_responses < 12);
assert!(
r.dropped_rate_limit > 0,
"rate limiting should show in the stats"
);
assert_eq!(r.clients_tracked, 1);
assert_eq!(r.stratum, 2);
}
other => panic!("expected serverstats, got {other:?}"),
}
}
#[test]
fn clients_op_lists_most_recent_first() {
let s = state();
{
let mut guard = s.lock().expect("lock");
for i in 0..5u8 {
let peer: std::net::IpAddr = format!("192.0.2.{i}").parse().expect("addr");
let _ = guard.clients.admit(&peer, i as f64);
}
}
let response = handle(&ControlRequest::Clients { limit: 3 }, &s);
match &response {
ControlResponse::Clients { rows } => {
assert_eq!(rows.len(), 3);
assert_eq!(rows[0].address, "192.0.2.4", "most recent first");
assert!(rows.iter().all(|r| r.requests == 1));
}
other => panic!("expected clients, got {other:?}"),
}
serde_json::to_string(&response).expect("clients response must serialize");
}
#[test]
fn client_ages_are_finite_even_when_clocks_disagree() {
let s = state();
{
let mut guard = s.lock().expect("lock");
let peer: std::net::IpAddr = "192.0.2.1".parse().expect("addr");
let _ = guard.clients.admit(&peer, f64::MAX);
}
let response = handle(&ControlRequest::Clients { limit: 4 }, &s);
if let ControlResponse::Clients { rows } = &response {
for r in rows {
assert!(
r.last_seen_ago_s.is_finite(),
"age must be finite, got {}",
r.last_seen_ago_s
);
}
}
serde_json::to_string(&response).expect("must serialize");
}
#[test]
fn ntsdata_reports_key_ids_only() {
let s = state();
match handle(&ControlRequest::NtsData, &s) {
ControlResponse::NtsData { master_key_ids } => {
assert_eq!(master_key_ids, vec![0xABCD]);
}
other => panic!("expected ntsdata, got {other:?}"),
}
}
#[test]
fn a_malformed_request_is_an_error_not_a_crash() {
let parsed = serde_json::from_str::<ControlRequest>("{\"op\":\"not_an_op\"}");
assert!(parsed.is_err());
}
}