Skip to main content

gday_server/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(clippy::all)]
3//! Runs a server for the [`gday_contact_exchange_protocol`].
4//! Lets two users exchange their public and (optionally) private socket
5//! addresses.
6
7mod connection_handler;
8mod state;
9
10use anyhow::Context;
11use anyhow::anyhow;
12use clap::Parser;
13use connection_handler::handle_connection;
14use log::{debug, error, info, warn};
15use socket2::{Domain, Protocol, TcpKeepalive, Type};
16use state::State;
17use std::future::Future;
18use std::net::SocketAddr;
19use std::{
20    io::BufReader,
21    path::{Path, PathBuf},
22    sync::Arc,
23    time::Duration,
24};
25use tokio::task::JoinSet;
26use tokio_rustls::{
27    TlsAcceptor,
28    rustls::{self, pki_types::CertificateDer},
29};
30
31#[derive(Parser, Debug)]
32#[command(author, version, about)]
33pub struct Args {
34    /// PEM file of private TLS server key
35    #[arg(short, long, required_unless_present("unencrypted"))]
36    pub key: Option<PathBuf>,
37
38    /// PEM file of signed TLS server certificate
39    #[arg(short, long, required_unless_present("unencrypted"))]
40    pub certificate: Option<PathBuf>,
41
42    /// Use unencrypted TCP instead of TLS
43    #[arg(short, long, conflicts_with_all(["key", "certificate"]))]
44    pub unencrypted: bool,
45
46    /// Socket addresses on which to listen.
47    #[arg(short, long, default_values = ["0.0.0.0:2311", "[::]:2311"])]
48    pub addresses: Vec<SocketAddr>,
49
50    /// Number of seconds before a new room is deleted
51    #[arg(short, long, default_value = "600")]
52    pub timeout: u64,
53
54    /// Max number of create room requests and
55    /// requests with an invalid room code
56    /// an IP address can send per minute
57    /// before they're rejected.
58    #[arg(short, long, default_value = "10")]
59    pub request_limit: u32,
60
61    /// Log verbosity. (trace, debug, info, warn, error)
62    #[arg(short, long, default_value = "debug")]
63    pub verbosity: log::LevelFilter,
64}
65
66/// Spawns a tokio server in the background.
67///
68/// Returns the addresses that the server is listening on and
69/// a handle that can be dropped to stop the server.
70///
71/// Must be called from a tokio async context.
72pub fn start_server(args: Args) -> anyhow::Result<(Vec<SocketAddr>, impl Future<Output = ()>)> {
73    // set the log level according to the command line argument
74    if let Err(err) = env_logger::builder()
75        .filter_level(args.verbosity)
76        .try_init()
77    {
78        error!("Non-fatal error. Couldn't initialize logger: {err}")
79    }
80
81    // get TCP listeners
82    let tcp_listeners: anyhow::Result<Vec<tokio::net::TcpListener>> =
83        args.addresses.into_iter().map(get_tcp_listener).collect();
84    let tcp_listeners = tcp_listeners?;
85
86    // get the addresses that we've actually bound to
87    let addresses: std::io::Result<Vec<SocketAddr>> =
88        tcp_listeners.iter().map(|l| l.local_addr()).collect();
89    let addresses = addresses.context("Couldn't determine local address")?;
90
91    // get the TLS acceptor if applicable
92    let tls_acceptor = if let (Some(key), Some(cert)) = (args.key, args.certificate) {
93        Some(get_tls_acceptor(&key, &cert)?)
94    } else {
95        None
96    };
97
98    // create the shared global state object
99    let state = State::new(
100        args.request_limit,
101        std::time::Duration::from_secs(args.timeout),
102    );
103
104    let mut joinset = JoinSet::new();
105
106    for tcp_listener in tcp_listeners {
107        joinset.spawn(run_single_server(
108            state.clone(),
109            tcp_listener,
110            tls_acceptor.clone(),
111        ));
112    }
113
114    let handle = async {
115        joinset.join_all().await;
116    };
117
118    // log the addresses being listened on
119    info!("Listening on these addresses: {addresses:?}");
120    info!("Is encrypted?: {}", tls_acceptor.is_some());
121    info!(
122        "Critical requests per minute per IP address limit: {}",
123        args.request_limit
124    );
125    info!(
126        "Number of seconds before a new room is deleted: {}",
127        args.timeout
128    );
129    info!("Server is now running.");
130
131    Ok((addresses, handle))
132}
133
134async fn run_single_server(
135    state: State,
136    tcp_listener: tokio::net::TcpListener,
137    tls_acceptor: Option<TlsAcceptor>,
138) {
139    loop {
140        // try to accept another connection
141        let (stream, origin) = match tcp_listener.accept().await {
142            Ok(ok) => ok,
143            Err(err) => {
144                error!("Error accepting incoming TCP connection: {err}.");
145                continue;
146            }
147        };
148        debug!("Accepted incoming TCP connection from {origin}.");
149
150        // spawn a thread to handle the connection
151        tokio::spawn(handle_connection(
152            stream,
153            origin,
154            tls_acceptor.clone(),
155            state.clone(),
156        ));
157    }
158}
159
160/// Returns a [`tokio::net::TcpListener`] with the provided address.
161///
162/// Sets the socket's TCP keepalive so that unresponsive
163/// connections close after 10 minutes to save resources.
164fn get_tcp_listener(addr: SocketAddr) -> anyhow::Result<tokio::net::TcpListener> {
165    // create a socket
166    let socket = socket2::Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))
167        .context("Couldn't create TCP socket")?;
168
169    // if this is an IPv6 listener, make it not listen
170    // to IPv4.
171    if addr.is_ipv6() {
172        socket
173            .set_only_v6(true)
174            .with_context(|| format!("Couldn't set IPV6_V6ONLY on {addr}"))?;
175    }
176
177    // sets the keepalive to 10 minutes
178    let tcp_keepalive = TcpKeepalive::new()
179        .with_time(Duration::from_secs(60))
180        .with_interval(Duration::from_secs(10));
181    socket
182        .set_tcp_keepalive(&tcp_keepalive)
183        .context("Couldn't set TCP keepalive")?;
184
185    socket
186        .bind(&addr.into())
187        .with_context(|| format!("Couldn't bind socket to address {addr}"))?;
188
189    socket
190        .listen(128)
191        .with_context(|| format!("Couldn't listen on {addr}"))?;
192
193    let listener: std::net::TcpListener = socket.into();
194
195    listener
196        .set_nonblocking(true)
197        .context("Couldn't set TCP socket to non blocking")?;
198
199    // convert to a tokio listener
200    let listener = tokio::net::TcpListener::from_std(listener)
201        .context("Couldn't create async TCP listener")?;
202
203    Ok(listener)
204}
205
206/// Takes paths to a PEM-encoded private key and signed certificate.
207/// Returns a [`TlsAcceptor`].
208fn get_tls_acceptor(key_path: &Path, cert_path: &Path) -> anyhow::Result<TlsAcceptor> {
209    // try reading the key file
210    let key = std::fs::File::open(key_path)
211        .with_context(|| format!("Couldn't open key file {key_path:?}."))?;
212    let mut key = BufReader::new(key);
213
214    // try parsing the key file
215    let key = rustls_pemfile::private_key(&mut key)
216        .with_context(|| format!("Couldn't parse key file {key_path:?}."))?
217        .ok_or(anyhow!("No private keys found in file {key_path:?}."))?;
218
219    // try reading the certificate file
220    let cert = std::fs::File::open(cert_path)
221        .with_context(|| format!("Couldn't open certificate file {cert_path:?}."))?;
222    let mut cert = BufReader::new(cert);
223
224    // try parsing the certificate file
225    let cert: Result<Vec<CertificateDer<'static>>, _> = rustls_pemfile::certs(&mut cert).collect();
226    let cert = cert.with_context(|| format!("Couldn't parse certificate file {cert_path:?}."))?;
227
228    // try creating tls config
229    let tls_config = rustls::ServerConfig::builder()
230        .with_no_client_auth()
231        .with_single_cert(cert, key)
232        .context("Couldn't configure TLS")?;
233
234    // create a tls acceptor
235    Ok(tokio_rustls::TlsAcceptor::from(Arc::new(tls_config)))
236}