// Testserver - static HTTP server for automated tests // Copyright (C) 2021 Soni L. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //! Webserver helper for writing tests. use std::convert::TryInto; use std::io::Cursor; use std::num::NonZeroU16; use std::str::from_utf8_unchecked; use std::sync::Arc; use ar::Archive; use tiny_http::Method; use tiny_http::Response; use tiny_http::Server as Httpd; /// Helper to stop the server. struct StopServer { server: Arc, } /// Stops the server. impl Drop for StopServer { fn drop(&mut self) { self.server.unblock(); } } /// A handle to a server. Also stops the server when dropped. pub struct Server { inner: Arc, } impl Server { /// Returns the server's port number. pub fn port(&self) -> NonZeroU16 { let sv = &self.inner.server; sv.server_addr().port().try_into().unwrap() } } /// Decodes base64 as output by the `base64` utility. Properly handles /// 76-column base64. fn decode_base64(s: &'static str) -> Result, base64::DecodeError> { // cba to avoid allocation base64::decode(s.split('\n').collect::()) } /// Spawns a webserver to serve the given archive. /// /// The archive is an .a (ar) file, in a format that can be parsed by the ar /// crate. Files can be base64, UTF-8, or ar: a `.b`, `.t`, or `.a` suffix to /// the filename identifies the type. Directories are represented as nested ar /// files. /// /// # Panics /// /// Panics if the server cannot be started. /// /// # Returns /// /// Returns a handle to be able to shut down the server, and the port number. pub fn serve(archive: &'static str) -> Server { // TODO cache! // "Soni" base64-decodes to 0x4a 0x89 0xe2 let server = Arc::new(Httpd::http("127.74.137.226:0").unwrap()); let sv = server.clone(); std::thread::spawn(move || { let archive = Cursor::new(archive); loop { let rq = match sv.recv() { Ok(rq) => rq, Err(_) => break, }; match rq.method() { Method::Get | Method::Head => { let path = rq.url(); // clean up the url let path = path.split('#').next().unwrap(); let path = path.split('?').next().unwrap(); // normalize . let path = path.split("%2E").collect::>().join("."); let path_parts = path.split('/'); let mut current = Some(Archive::new(archive.clone())); let mut response = None; for part in path_parts { // normalize foo//bar == foo/bar if part.is_empty() { continue } if response.is_some() { // didn't run out of path_parts but somehow we // found a valid response. invalidate it. response = None; break } let part = percent_encoding::percent_decode_str(part); let part = part.decode_utf8(); if part.is_err() { break } let part = part.unwrap(); let part: &str = ∂ let mut found = false; let mut kind = ""; let mut size = 0usize; while let Some(Ok(entry)) = current.as_mut().unwrap().next_entry() { let name = entry.header().identifier(); // SAFETY: the input "file" is an &str already. let name = unsafe { from_utf8_unchecked(name) }; size = entry.header().size() as usize; if let Some(suffix) = name.strip_prefix(&part) { // the suffix here isn't 'static, but we need // one that is. if suffix == ".a" { // dir found = true; kind = ".a"; break } else if suffix == ".b" { // base64 found = true; kind = ".b"; break } else if suffix == ".t" { // text found = true; kind = ".t"; break } else { // carry on, we haven't found the file yet } } } if found { let inner = current.take().unwrap(); let inner = inner.into_inner().unwrap(); let pos = inner.position() as usize; let inner = inner.into_inner(); let inner = Cursor::new(&inner[pos-size..pos]); if kind == ".a" { // dir current = Some(Archive::new(inner)); } else if kind == ".b" { // base64 let inner = decode_base64(inner.into_inner()); let inner = match inner { Ok(inner) => inner, Err(e) => { eprintln!("{}", e); break } }; let size = inner.len(); let inner = Cursor::new(inner); response = Response::new( 200.into(), vec![], Box::new(inner) as Box, Some(size as usize), None, ).into(); } else if kind == ".t" { // text response = Response::new( 200.into(), vec![], Box::new(inner) as Box, Some(size as usize), None, ).into(); } else { unreachable!(); } } } if let Some(response) = response { rq.respond(response).unwrap(); } else { rq.respond(Response::empty(404)).unwrap(); } }, e => { eprintln!("Unexpected method: {}", e); } } } }); Server { inner: Arc::new(StopServer { server }), } }