// 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 }),
}
}