summaryrefslogblamecommitdiffstats
path: root/src/lib.rs
blob: 31c7aa9c6cf916a5a75a872dac82213063fa2724 (plain) (tree)





























































































































































































                                                                               
// 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 <https://www.gnu.org/licenses/>.

//! 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<Httpd>,
}

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

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::<Vec<_>>().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 = &part;
                        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<dyn std::io::Read>,
                                    Some(size as usize),
                                    None,
                                ).into();
                            } else if kind == ".t" {
                                // text
                                response = Response::new(
                                    200.into(),
                                    vec![],
                                    Box::new(inner) as Box<dyn std::io::Read>,
                                    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 }),
    }
}