From 370aa8f9140476dd39d26831494b2de737edb359 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Sat, 12 Jun 2021 18:41:16 -0300 Subject: [Project] Rust Testserver A static webserver helper for writing automated tests. Distinguishes itself from other projects by storing the document root as a string literal. --- src/lib.rs | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/lib.rs (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..31c7aa9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,190 @@ +// 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() + } +} + +/// 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 { + 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 = base64::decode(inner.into_inner()); + let inner = Cursor::new(inner.unwrap()); + 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 }), + } +} -- cgit v1.2.3