summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2021-06-12 18:41:16 -0300
committerSoniEx2 <endermoneymod@gmail.com>2021-06-12 18:41:16 -0300
commit370aa8f9140476dd39d26831494b2de737edb359 (patch)
treeae8aed74c03c8a328d22fa77d5875f280f6ea088 /src
[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.
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs190
1 files changed, 190 insertions, 0 deletions
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 <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 }),
+    }
+}