From 00f8fdc9e136e000ee4bf076f15b9ac1bd4d7041 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sat, 1 Aug 2020 22:42:44 -0400 Subject: [PATCH] shitpost: serve HTTP --- site/Cargo.toml | 15 ++++- site/build.rs | 15 +++++ site/src/http.rs | 102 +++++++++++++++++++++++++++++++ site/src/main.rs | 110 +++++++--------------------------- site/src/server.rs | 85 ++++++++++++++++++++++++++ site/static/gruvbox.css | 74 +++++++++++++++++++++++ site/templates/error.rs.html | 15 +++++ site/templates/footer.rs.html | 7 +++ site/templates/header.rs.html | 13 ++++ site/templates/page.rs.html | 13 ++++ 10 files changed, 358 insertions(+), 91 deletions(-) create mode 100644 site/build.rs create mode 100644 site/src/http.rs create mode 100644 site/src/server.rs create mode 100644 site/static/gruvbox.css create mode 100644 site/templates/error.rs.html create mode 100644 site/templates/footer.rs.html create mode 100644 site/templates/header.rs.html create mode 100644 site/templates/page.rs.html diff --git a/site/Cargo.toml b/site/Cargo.toml index 1c94bcf..d35815d 100644 --- a/site/Cargo.toml +++ b/site/Cargo.toml @@ -3,6 +3,7 @@ name = "majsite" version = "0.2.0" authors = ["Christine Dodrill "] edition = "2018" +build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,11 +11,19 @@ edition = "2018" anyhow = "1" async-std = "1.5" async-trait = "0" -log = "0" +dnd_dice_roller = "0.3" env_logger = "0" +log = "0" +mime = "0.3.0" percent-encoding = "2" rustls = { version = "0.18", features = ["dangerous_configuration"] } +smol = { version = "0.3", features = ["tokio02"] } structopt = "0.3" -dnd_dice_roller = "0.3" +url = "2" +warp = "0.2" -maj = { path = "..", features = ["server"], default-features = false } \ No newline at end of file +maj = { path = "..", features = ["server"], default-features = false } + +[build-dependencies] +anyhow = "1" +ructe = { version = "0.11", features = ["warp02"] } \ No newline at end of file diff --git a/site/build.rs b/site/build.rs new file mode 100644 index 0000000..dea6489 --- /dev/null +++ b/site/build.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use ructe::Ructe; +use std::process::Command; + +fn main() -> Result<()> { + let mut ructe = Ructe::from_env()?; + let mut statics = ructe.statics()?; + statics.add_files("static")?; + ructe.compile_templates("templates")?; + + let output = Command::new("git").args(&["rev-parse", "HEAD"]).output()?; + let git_hash = String::from_utf8(output.stdout)?; + println!("cargo:rustc-env=GITHUB_SHA={}", git_hash); + Ok(()) +} diff --git a/site/src/http.rs b/site/src/http.rs new file mode 100644 index 0000000..97a2bda --- /dev/null +++ b/site/src/http.rs @@ -0,0 +1,102 @@ +use crate::templates::{self, Html, RenderRucte, ToHtml, statics::StaticFile}; +use maj::server::Handler; +use maj::{gemini, server::Request as GemRequest, StatusCode}; +use std::io::Write; +use std::sync::Arc; +use url::Url; +use warp::{filters::path::FullPath, http::Response, Filter, Rejection, Reply,path}; + +const HOST: &'static str = "cetacean.club"; // XXX(cadey): HACK +const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +async fn route(args: (FullPath, Arc)) -> Result { + let (path, h) = args; + let u = Url::parse(&format!("gemini://{}{}", HOST, path.as_str())).unwrap(); + + let req = GemRequest { + url: u.clone(), + certs: None, + }; + + let resp = h.clone().handle(req).await.unwrap(); + + match resp.status { + StatusCode::Success => { + if resp.meta.starts_with("text/gemini") { + let (title, body) = gemtext_to_html(resp.body); + Response::builder().html(|o| templates::page_html(o, title, body)) + } else { + Response::builder() + .status(warp::http::StatusCode::INTERNAL_SERVER_ERROR) + .html(|o| { + templates::error_html(o, u.to_string(), "cannot proxy this yet".to_string()) + }) + } + } + + _ => Response::builder() + .status(warp::http::StatusCode::INTERNAL_SERVER_ERROR) + .html(|o| templates::error_html(o, u.to_string(), resp.meta)), + } +} + +fn gemtext_to_html(inp: Vec) -> (String, impl ToHtml) { + use gemini::Node::*; + let mut title: String = "Unknown Title".to_string(); + let inp = std::str::from_utf8(&inp).unwrap(); + let nodes = gemini::parse(inp); + let mut buf: Vec = Vec::new(); + + for node in &nodes { + match node { + Heading { level, body } => { + if *level == 1 { + title = body.to_string(); + } + + write!(buf, "{1}", level, body).unwrap(); + } + Text(body) => write!(buf, "{}\n
", body).unwrap(), + Link { to, name } => write!( + buf, + r#"{}"#, + to, + name.as_ref().or(Some(&to.to_string())).unwrap() + ) + .unwrap(), + Preformatted(body) => write!(buf, "
{}
", body).unwrap(), + ListItem(body) => write!(buf, "
  • {}
  • ", body).unwrap(), + Quote(body) => write!(buf, "
    {}
    ", body).unwrap(), + } + } + + (title, Html(String::from_utf8(buf).unwrap())) +} + +pub fn run(h: Arc, port: u16) { + smol::run(async { + let h = h.clone(); + let handler = warp::path::full() + .map(move |path: FullPath| (path, h.clone())).and_then(route); + let statics = path("static").and(path::param()).and_then(static_file); + let site = statics.or(handler) + .with(warp::log(APPLICATION_NAME)); + warp::serve(site).run(([0, 0, 0, 0], port)).await; + }); +} + +/// Handler for static files. +/// Create a response from the file data with a correct content type +/// and a far expires header (or a 404 if the file does not exist). +async fn static_file(name: String) -> Result { + if let Some(data) = StaticFile::get(&name) { + Ok(Response::builder() + .status(warp::http::StatusCode::OK) + .header("content-type", data.mime.as_ref()) + // TODO .header("expires", _far_expires) + .body(data.content)) + } else { + println!("Static file {} not found", name); + Err(warp::reject::not_found()) + } +} diff --git a/site/src/main.rs b/site/src/main.rs index 397747f..ae60123 100644 --- a/site/src/main.rs +++ b/site/src/main.rs @@ -1,12 +1,4 @@ use async_std::task; -use dnd_dice_roller::{dice::Dice, error::DiceError}; -use maj::{ - gemini::Builder, - route, seg, - server::{Error, Handler as MajHandler, Request}, - split, Response, -}; -use percent_encoding::percent_decode_str; use rustls::{ internal::pemfile::{certs, pkcs8_private_keys}, AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig, @@ -15,11 +7,14 @@ use std::{ fs::File, io::{self, BufReader}, path::{Path, PathBuf}, - str::FromStr, sync::Arc, + thread, }; use structopt::StructOpt; +mod http; +mod server; + #[derive(StructOpt, Debug)] struct Options { /// host to listen on @@ -49,6 +44,10 @@ struct Options { default_value = "cetacean.club" )] hostname: String, + + /// HTTP port + #[structopt(long, default_value="34587")] + http_port: u16, } fn load_certs(path: &Path) -> io::Result> { @@ -81,11 +80,19 @@ fn main() -> Result<(), maj::server::Error> { .set_single_cert(certs, keys.remove(0)) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + let h = Arc::new(server::Handler { + hostname: opts.hostname, + files: maj::server::files::Handler::new(opts.static_path), + }); + + { + let port = opts.http_port.clone(); + let h = h.clone(); + thread::spawn(move || http::run(h.clone(), port)); + } + task::block_on(maj::server::serve( - Arc::new(Handler { - hostname: opts.hostname, - files: maj::server::files::Handler::new(opts.static_path), - }), + h.clone(), config, opts.host, opts.port, @@ -94,78 +101,5 @@ fn main() -> Result<(), maj::server::Error> { Ok(()) } -struct Handler { - hostname: String, - files: maj::server::files::Handler, -} - -async fn dice(req: Request) -> Result { - fn dice_roll>(roll: T) -> Result { - let mut dice = Dice::from_str(&roll.into())?; - - if dice.number_of_dice_to_roll > 100 { - dice.number_of_dice_to_roll = 100; - } - - if dice.sides > 100 { - dice.sides = 100 - } - - if dice.sides == 0 { - dice.sides = 6; - } - - let res = dice.roll_dice(); - let reply = format!( - "{}{} = {}\n", - res.dice_results, - match dice.modifier { - Some(amt) => format!(" + {}", amt), - None => "".into(), - }, - res.final_result[0] - ); - Ok(reply) - } - - match req.url.query() { - None => Ok(Response::input( - "What do you want to roll? [n]dn[+n] [adv|dadv]", - )), - Some(q) => Ok({ - let dice = percent_decode_str(q).decode_utf8()?; - let b = Builder::new() - .heading(1, "Dice Results") - .text("") - .text(format!("You rolled {} and you got:", dice)) - .text("") - .preformatted(format!("{}", dice_roll(dice)?)) - .text("") - .link("/dice", Some("Do another roll".to_string())); - - Response::render(b.build()) - }), - } -} - -#[async_trait::async_trait] -impl MajHandler for Handler { - async fn handle(&self, req: Request) -> Result { - if req.url.has_host() && req.url.host_str().unwrap().to_string() != self.hostname { - return Ok(Response::no_proxy()); - } - - if req.url.path() == "" { - return Ok(Response::perm_redirect(format!( - "gemini://{}/", - self.hostname - ))); - } - - route!(req.url.path(), { - (/"dice") => dice(req).await; - }); - - self.files.handle(req).await - } -} +// And finally, include the generated code for templates and static files. +include!(concat!(env!("OUT_DIR"), "/templates.rs")); diff --git a/site/src/server.rs b/site/src/server.rs new file mode 100644 index 0000000..bbde4c1 --- /dev/null +++ b/site/src/server.rs @@ -0,0 +1,85 @@ +use dnd_dice_roller::{dice::Dice, error::DiceError}; +use maj::{ + gemini::Builder, + route, seg, + server::{Error, Handler as MajHandler, Request}, + split, Response, +}; +use percent_encoding::percent_decode_str; +use std::str::FromStr; + +pub struct Handler { + pub hostname: String, + pub files: maj::server::files::Handler, +} + +async fn dice(req: Request) -> Result { + fn dice_roll>(roll: T) -> Result { + let mut dice = Dice::from_str(&roll.into())?; + + if dice.number_of_dice_to_roll > 100 { + dice.number_of_dice_to_roll = 100; + } + + if dice.sides > 100 { + dice.sides = 100 + } + + if dice.sides == 0 { + dice.sides = 6; + } + + let res = dice.roll_dice(); + let reply = format!( + "{}{} = {}\n", + res.dice_results, + match dice.modifier { + Some(amt) => format!(" + {}", amt), + None => "".into(), + }, + res.final_result[0] + ); + Ok(reply) + } + + match req.url.query() { + None => Ok(Response::input( + "What do you want to roll? [n]dn[+n] [adv|dadv]", + )), + Some(q) => Ok({ + let dice = percent_decode_str(q).decode_utf8()?; + let b = Builder::new() + .heading(1, "Dice Results") + .text("") + .text(format!("You rolled {} and you got:", dice)) + .text("") + .preformatted(format!("{}", dice_roll(dice)?)) + .text("") + .link("/dice", Some("Do another roll".to_string())); + + Response::render(b.build()) + }), + } +} + +#[async_trait::async_trait] +impl MajHandler for Handler { + async fn handle(&self, req: Request) -> Result { + if req.url.has_host() && req.url.host_str().unwrap().to_string() != self.hostname { + return Ok(Response::no_proxy()); + } + + if req.url.path() == "" { + return Ok(Response::perm_redirect(format!( + "gemini://{}/", + self.hostname + ))); + } + + route!(req.url.path(), { + (/"dice") => dice(req).await; + }); + + self.files.handle(req).await + } +} diff --git a/site/static/gruvbox.css b/site/static/gruvbox.css new file mode 100644 index 0000000..618eb3b --- /dev/null +++ b/site/static/gruvbox.css @@ -0,0 +1,74 @@ +main { + font-family: monospace, monospace; + max-width: 38rem; + padding: 2rem; + margin: auto; +} + +@media only screen and (max-device-width: 736px) { + main { + padding: 0rem; + } +} + +::selection { + background: #d3869b; +} + +body { + background: #282828; + color: #ebdbb2; +} + +pre { + background-color: #3c3836; + padding: 1em; + border: 0; +} + +a, a:active, a:visited { + color: #b16286; + background-color: #1d2021; +} + +h1, h2, h3, h4, h5 { + margin-bottom: .1rem; +} + +blockquote { + border-left: 1px solid #bdae93; + margin: 0.5em 10px; + padding: 0.5em 10px; +} + +footer { + align: center; +} + +@media (prefers-color-scheme: light) { + body { + background: #fbf1c7; + color: #3c3836; + } + + pre { + background-color: #ebdbb2; + padding: 1em; + border: 0; + } + + a, a:active, a:visited { + color: #b16286; + background-color: #f9f5d7; + } + + h1, h2, h3, h4, h5 { + margin-bottom: .1rem; + } + + blockquote { + border-left: 1px solid #655c54; + margin: 0.5em 10px; + padding: 0.5em 10px; + } +} diff --git a/site/templates/error.rs.html b/site/templates/error.rs.html new file mode 100644 index 0000000..cb87510 --- /dev/null +++ b/site/templates/error.rs.html @@ -0,0 +1,15 @@ +@use super::{header_html, footer_html}; + +@(url: String, why: String) + +@:header_html("Error") + +

    Error

    + +

    + There was an error proxying @url: @why. +

    + +
    + +@:footer_html() diff --git a/site/templates/footer.rs.html b/site/templates/footer.rs.html new file mode 100644 index 0000000..7501df1 --- /dev/null +++ b/site/templates/footer.rs.html @@ -0,0 +1,7 @@ +@() + + + + diff --git a/site/templates/header.rs.html b/site/templates/header.rs.html new file mode 100644 index 0000000..ffe7dc8 --- /dev/null +++ b/site/templates/header.rs.html @@ -0,0 +1,13 @@ +@use super::statics::*; + +@(title: &str) + + + + + @title + + + + +
    diff --git a/site/templates/page.rs.html b/site/templates/page.rs.html new file mode 100644 index 0000000..0aa5972 --- /dev/null +++ b/site/templates/page.rs.html @@ -0,0 +1,13 @@ +@use super::{header_html, footer_html}; + +@(title: String, body: impl ToHtml) + +@:header_html(&title) + +

    + @body +

    + +
    + +@:footer_html()