use std::borrow::Cow; use std::io::Cursor; use std::process::exit; use ascii::AsciiStr; use tiny_http::Header; use tiny_http::Request; use tiny_http::Response; use tiny_http::Server; use tiny_http::StatusCode; /// Start the server on a given port /// /// Runs the server in a single thread, bound to 0.0.0.0 on the provided port. Requests are /// handled first-come, first-serve pub fn go(domains: &[&AsciiStr], port: u16) { let server = match Server::http(("0.0.0.0", port)) { Ok(s) => s, Err(e) => { eprintln!("Couldn't create server on 0.0.0.0:{port}\n{e}"); exit(1004); } }; println!("Running on 0.0.0.0:{port}"); for req in server.incoming_requests() { let route = Route::parse_request(&req); let resp = route.render(domains); if let Err(e) = req.respond(resp) { eprintln!("WARN: {}", e); } } } /// An endpoint and the associated data /// /// Each route represents a set of logic that should be used to respond to the request, /// and the necessary information from the request in order to do so pub enum Route<'a> { PrevOrNext(&'a str, isize), NotFound, } impl<'a> Route<'a> { /// Given a request, parse out the appropriate Route for that request pub fn parse_request(r: &'a Request) -> Self { let (first_segment, referrer_domain) = r.url() .trim_matches('/') .split_once('/') .unwrap_or((r.url(), "missing_referrer_domain")); match first_segment { "next" => { Route::PrevOrNext(referrer_domain, 1) }, "prev" => { Route::PrevOrNext(referrer_domain, -1) }, _ => Route::NotFound, } } /// Generate a Response for this route pub fn render<'b>(&self, domains: &[&'b AsciiStr]) -> Response>> { match self { Self::PrevOrNext(this_domain, offset) => { let destination = domains.iter() .position(|d| d.as_str().starts_with(this_domain)) .map(|i| (i as isize + offset).rem_euclid(domains.len() as isize)) .map(|i| domains[i as usize]); // safe to unwrap: we just did a rem_euclid if let Some(url) = destination { let destination = format!( "https://{}", url ); Response::new( StatusCode(303), vec![ Header::from_bytes( b"Location".to_owned(), destination.as_bytes(), ).unwrap(), // safe to unwrap: both of these are guaranteed ASCII Header::from_bytes( b"Cache-Control".to_owned(), b"max-age=86400,stale-while-revalidate=172800,stale-if-error=3155760000".to_owned(), ).unwrap(),// safe to unwrap: both of these are guaranteed ASCII ], Cursor::new(Cow::Borrowed(b"")), Some(0), None, ) } else { let bytes = format!( "Misconfigured server. Website {} not found in webring.", this_domain, ); let blen = bytes.len(); Response::new( StatusCode(400), Vec::new(), Cursor::new(Cow::Owned(bytes.into_bytes())), Some(blen), None, ) } }, Self::NotFound => { Response::new( StatusCode(404), Vec::new(), Cursor::new(Cow::Borrowed(b"Page not found")), Some(14), None, ) } } } }