shitpost: serve HTTP
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Cadey Ratio 2020-08-01 22:42:44 -04:00
parent ff88b28688
commit 00f8fdc9e1
10 changed files with 358 additions and 91 deletions

View File

@ -3,6 +3,7 @@ name = "majsite"
version = "0.2.0"
authors = ["Christine Dodrill <me@christine.website>"]
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 }
maj = { path = "..", features = ["server"], default-features = false }
[build-dependencies]
anyhow = "1"
ructe = { version = "0.11", features = ["warp02"] }

15
site/build.rs Normal file
View File

@ -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(())
}

102
site/src/http.rs Normal file
View File

@ -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<crate::server::Handler>)) -> Result<impl Reply, Rejection> {
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<u8>) -> (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<u8> = Vec::new();
for node in &nodes {
match node {
Heading { level, body } => {
if *level == 1 {
title = body.to_string();
}
write!(buf, "<h{0}>{1}</h{0}>", level, body).unwrap();
}
Text(body) => write!(buf, "{}\n<br />", body).unwrap(),
Link { to, name } => write!(
buf,
r#"<a href="{}">{}</a>"#,
to,
name.as_ref().or(Some(&to.to_string())).unwrap()
)
.unwrap(),
Preformatted(body) => write!(buf, "<code><pre>{}</pre></code>", body).unwrap(),
ListItem(body) => write!(buf, "<li>{}</li>", body).unwrap(),
Quote(body) => write!(buf, "<blockquote>{}</blockquote>", body).unwrap(),
}
}
(title, Html(String::from_utf8(buf).unwrap()))
}
pub fn run(h: Arc<crate::server::Handler>, 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<impl Reply, Rejection> {
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())
}
}

View File

@ -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<Vec<Certificate>> {
@ -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<Response, Error> {
fn dice_roll<T: Into<String>>(roll: T) -> Result<String, DiceError> {
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<Response, Error> {
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"));

85
site/src/server.rs Normal file
View File

@ -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<Response, Error> {
fn dice_roll<T: Into<String>>(roll: T) -> Result<String, DiceError> {
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<Response, Error> {
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
}
}

74
site/static/gruvbox.css Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,15 @@
@use super::{header_html, footer_html};
@(url: String, why: String)
@:header_html("Error")
<h1>Error</h1>
<p>
There was an error proxying <code>@url</code>: @why.
</p>
<hr />
@:footer_html()

View File

@ -0,0 +1,7 @@
@()
<footer>
<p>Be well, Creator - <a href="https://tulpa.dev/cadey/maj">Source code here</a> - From <a href="gemini://cetacean.club">Within</a></p>
</footer>
</main>
</body>
</html>

View File

@ -0,0 +1,13 @@
@use super::statics::*;
@(title: &str)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" href="/static/@gruvbox_css.name">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body id="top">
<main>

View File

@ -0,0 +1,13 @@
@use super::{header_html, footer_html};
@(title: String, body: impl ToHtml)
@:header_html(&title)
<p>
@body
</p>
<hr />
@:footer_html()