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"
|
||||
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 }
|
||||
|
||||
[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())
|
||||
}
|
||||
}
|
110
site/src/main.rs
110
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<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"));
|
||||
|
|
|
@ -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