use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use crate::plus::PlusRequest;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub selector: String,
pub search: Option<String>,
pub plus: Option<PlusRequest>,
pub peer: SocketAddr,
}
pub trait Handler: Send + Sync + 'static {
fn handle(&self, request: Request) -> impl Future<Output = Vec<u8>> + Send;
}
impl<F, Fut> Handler for F
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Vec<u8>> + Send,
{
fn handle(&self, request: Request) -> impl Future<Output = Vec<u8>> + Send {
self(request)
}
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub max_request_line: usize,
pub timeout: Duration,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
max_request_line: 2048,
timeout: Duration::from_secs(30),
}
}
}
pub async fn serve(
listener: TcpListener,
handler: impl Handler,
config: ServerConfig,
shutdown: impl Future<Output = ()>,
) -> std::io::Result<()> {
let handler = Arc::new(handler);
let config = Arc::new(config);
tokio::pin!(shutdown);
loop {
tokio::select! {
_ = &mut shutdown => break,
accepted = listener.accept() => match accepted {
Ok((stream, peer)) => {
let handler = handler.clone();
let config = config.clone();
tokio::spawn(async move {
if let Err(error) = handle_connection(stream, peer, handler, &config).await {
log::debug!("gopher: connection from {peer} failed: {error}");
}
});
}
Err(error) => log::warn!("gopher: accept failed: {error}"),
},
}
}
Ok(())
}
async fn handle_connection(
mut stream: TcpStream,
peer: SocketAddr,
handler: Arc<impl Handler>,
config: &ServerConfig,
) -> std::io::Result<()> {
let mut line = Vec::with_capacity(64);
let mut byte = [0u8; 1];
loop {
let count = tokio::time::timeout(config.timeout, stream.read(&mut byte))
.await
.map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "selector read"))??;
if count == 0 || byte[0] == b'\n' {
break;
}
line.push(byte[0]);
if line.len() >= config.max_request_line {
return stream.shutdown().await;
}
}
let (selector, search, plus) = parse_request(&line);
let is_plus = plus.is_some();
let request = Request {
selector,
search,
plus,
peer,
};
let body = handler.handle(request).await;
if is_plus {
stream
.write_all(format!("+{}\r\n", body.len()).as_bytes())
.await?;
}
stream.write_all(&body).await?;
stream.shutdown().await
}
pub fn parse_request(line: &[u8]) -> (String, Option<String>, Option<PlusRequest>) {
let text = String::from_utf8_lossy(line);
let text = text.trim_end_matches(['\r', '\n']);
let mut fields = text.split('\t');
let selector = fields.next().unwrap_or("").to_string();
let search = fields.next().and_then(|s| (!s.is_empty()).then(|| s.to_string()));
let plus = fields.next().and_then(PlusRequest::from_token);
(selector, search, plus)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_selector_is_neither_a_search_nor_gopher_plus() {
let (selector, search, plus) = parse_request(b"/about.txt\r\n");
assert_eq!(selector, "/about.txt");
assert_eq!(search, None);
assert_eq!(plus, None);
}
#[test]
fn a_type_seven_search_carries_its_terms() {
let (selector, search, plus) = parse_request(b"/search\trust lang\r\n");
assert_eq!(selector, "/search");
assert_eq!(search.as_deref(), Some("rust lang"));
assert_eq!(plus, None);
}
#[test]
fn the_third_field_is_read_as_a_gopher_plus_form() {
let (selector, search, plus) = parse_request(b"/arc\t\t+\r\n");
assert_eq!(selector, "/arc");
assert_eq!(search, None, "an empty search field is not a search");
assert_eq!(plus, Some(PlusRequest::Item(None)));
assert_eq!(
parse_request(b"/arc\t\t!\r\n").2,
Some(PlusRequest::Attributes)
);
assert_eq!(
parse_request(b"/dir\t\t$\r\n").2,
Some(PlusRequest::DirectoryAttributes)
);
assert_eq!(
parse_request(b"/arc\t\t+text/plain\r\n").2,
Some(PlusRequest::Item(Some("text/plain".into())))
);
}
#[test]
fn a_search_and_a_gopher_plus_form_can_arrive_together() {
let (selector, search, plus) = parse_request(b"/find\tterms\t!\r\n");
assert_eq!(selector, "/find");
assert_eq!(search.as_deref(), Some("terms"));
assert_eq!(plus, Some(PlusRequest::Attributes));
}
#[test]
fn what_the_client_writes_the_server_reads_back() {
for form in [
PlusRequest::Item(None),
PlusRequest::Item(Some("text/plain".into())),
PlusRequest::Attributes,
PlusRequest::DirectoryAttributes,
] {
let line = format!("/sel\t\t{}\r\n", form.token());
let (selector, _, parsed) = parse_request(line.as_bytes());
assert_eq!(selector, "/sel");
assert_eq!(parsed, Some(form));
}
}
#[test]
fn an_empty_selector_is_the_root_menu() {
assert_eq!(parse_request(b"\r\n").0, "");
assert_eq!(parse_request(b"").0, "");
}
#[test]
fn an_unrecognised_third_field_is_not_a_gopher_plus_request() {
assert_eq!(parse_request(b"/x\t\tnonsense\r\n").2, None);
}
#[test]
fn a_bare_lf_is_accepted_as_well_as_crlf() {
assert_eq!(parse_request(b"/about.txt\n").0, "/about.txt");
}
}