forked from cadey/maj
shitpost: serve HTTP
This commit is contained in:
parent
ff88b28688
commit
00f8fdc9e1
|
@ -3,6 +3,7 @@ name = "majsite"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
authors = ["Christine Dodrill <me@christine.website>"]
|
authors = ["Christine Dodrill <me@christine.website>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
@ -10,11 +11,19 @@ edition = "2018"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-std = "1.5"
|
async-std = "1.5"
|
||||||
async-trait = "0"
|
async-trait = "0"
|
||||||
log = "0"
|
dnd_dice_roller = "0.3"
|
||||||
env_logger = "0"
|
env_logger = "0"
|
||||||
|
log = "0"
|
||||||
|
mime = "0.3.0"
|
||||||
percent-encoding = "2"
|
percent-encoding = "2"
|
||||||
rustls = { version = "0.18", features = ["dangerous_configuration"] }
|
rustls = { version = "0.18", features = ["dangerous_configuration"] }
|
||||||
|
smol = { version = "0.3", features = ["tokio02"] }
|
||||||
structopt = "0.3"
|
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"] }
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
108
site/src/main.rs
108
site/src/main.rs
|
@ -1,12 +1,4 @@
|
||||||
use async_std::task;
|
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::{
|
use rustls::{
|
||||||
internal::pemfile::{certs, pkcs8_private_keys},
|
internal::pemfile::{certs, pkcs8_private_keys},
|
||||||
AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig,
|
AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig,
|
||||||
|
@ -15,11 +7,14 @@ use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self, BufReader},
|
io::{self, BufReader},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
thread,
|
||||||
};
|
};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
mod http;
|
||||||
|
mod server;
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
struct Options {
|
struct Options {
|
||||||
/// host to listen on
|
/// host to listen on
|
||||||
|
@ -49,6 +44,10 @@ struct Options {
|
||||||
default_value = "cetacean.club"
|
default_value = "cetacean.club"
|
||||||
)]
|
)]
|
||||||
hostname: String,
|
hostname: String,
|
||||||
|
|
||||||
|
/// HTTP port
|
||||||
|
#[structopt(long, default_value="34587")]
|
||||||
|
http_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> {
|
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))
|
.set_single_cert(certs, keys.remove(0))
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||||
|
|
||||||
task::block_on(maj::server::serve(
|
let h = Arc::new(server::Handler {
|
||||||
Arc::new(Handler {
|
|
||||||
hostname: opts.hostname,
|
hostname: opts.hostname,
|
||||||
files: maj::server::files::Handler::new(opts.static_path),
|
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(
|
||||||
|
h.clone(),
|
||||||
config,
|
config,
|
||||||
opts.host,
|
opts.host,
|
||||||
opts.port,
|
opts.port,
|
||||||
|
@ -94,78 +101,5 @@ fn main() -> Result<(), maj::server::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Handler {
|
// And finally, include the generated code for templates and static files.
|
||||||
hostname: String,
|
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
@use super::{header_html, footer_html};
|
||||||
|
|
||||||
|
@(title: String, body: impl ToHtml)
|
||||||
|
|
||||||
|
@:header_html(&title)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
@body
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
@:footer_html()
|
Loading…
Reference in New Issue