From 23b38f1a8b2f1d4bc1f07b1cba3b8fafb4a4d9fe Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sun, 2 Aug 2020 13:18:04 +0000 Subject: [PATCH] more words --- index.gmi | 4 - journal/08-01-2020-hosted-with-maj.gmi | 196 +++++++++++++++++++++++++ journal/atom.xml | 14 +- journal/index.gmi | 1 + 4 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 journal/08-01-2020-hosted-with-maj.gmi diff --git a/index.gmi b/index.gmi index 8de40e8..fecec1a 100644 --- a/index.gmi +++ b/index.gmi @@ -20,10 +20,6 @@ Here are some pages with information about some of my projects: => /journal Flight journal => /text Texts from around the internet -You may recognize me from my blog: - -=> https://christine.website/blog - --- Be well, Creator. diff --git a/journal/08-01-2020-hosted-with-maj.gmi b/journal/08-01-2020-hosted-with-maj.gmi new file mode 100644 index 0000000..5b9b1b3 --- /dev/null +++ b/journal/08-01-2020-hosted-with-maj.gmi @@ -0,0 +1,196 @@ +# This site is hosted with Maj + +Well, I've done a lot of work behind the scenes and now I can proudly announce that cetacean.club is now hosted using my own server code! I'll go into more detail below, but first I want to thank a few people and projects that helped make this happen: + +* Rustls for being such a flexible and decent TLS client/server framework for doing TLS operations in Rust +* The team behind async-std, which powers every single client socket connected to this site +* Matt Brubeck for writing agate, which has been a huge inspiration (and was previously what this site was hosted with) +* The people of #gemini for inspiration and moral support + +=> https://github.com/mbrubeck/agate agate - a Gemini server using Rust + +## What is Maj? + +Maj is a set of three opinionated frameworks in one small package. Maj includes a text/gemini parser, a Gemini response parser, a simple client framework and a simple server framework. + +Much of this is still in the earlier phases of development, and this site is going to be one of the biggest tests of Maj in production style workloads. With this post, I am happy to announce that Maj version 0.5.0 is mostly feature-complete and on the road to a stable release I'll call version 1.0.0. + +## The Name Maj + +The name Maj (sounds like the month of May) is a nonbinary name commonly used in Sweden and Nordic countries. It means "a pearl", but the name has no significance to the project or myself. + +## The text/gemini Parser + +The text/gemini parser takes a string of UTF-8 formatted gemtext and emits a list of Gemini document nodes. It is currently known to work on the biggest gemini sites, however if you find any issues with this, please let me know at cadey@firemail.cc. Essentially it takes a document that looks like this: + +``` +# Hello World! +Hello, world! This is a test. +``` + +into a list of nodes that looks like this: + +``` +vec![ + Node::Heading { level: 1, body: "Hello, World!" }, + Node::Text("Hello, world! This is a test.") +] +``` + +This kind of layout makes it easier to iterate over gemtext nodes and translate gemtext into any other arbitrary format, such as Markdown: + +``` +fn gem_to_md(nodes: Vec, out: &mut impl Write) -> io::Result<()> { + use Node::*; + + for node in nodes { + match node { + Text(body) => write!(out, "{}\n", body)?, + Link { to, name } => match name { + Some(name) => write!(out, "[{}]({})\n\n", name, to)?, + None => write!(out, "[{0}]({0})", to)?, + }, + Preformatted(body) => write!(out, "```\n{}\n```\n\n", body)?, + Heading { level, body } => { + write!(out, "##{} {}\n\n", "#".repeat(level as usize), body)? + } + ListItem(body) => write!(out, "* {}\n", body)?, + Quote(body) => write!(out, "> {}\n\n", body)?, + } + } + + Ok(()) +} +``` + +## The Client + +Maj's client feature can be enabled in Cargo.toml using the client feature: + +``` +[dependencies] +maj = { version = "0.5", default-features = false, features = ["client"] } +rustls = { version = "0.18", features = ["dangerous_configuration"] } +``` + +The client is made a feature so that users can pick and choose what parts of Maj they need for their application. This also allows users to avoid having to link in the entire server framework (and its dependencies) without having to chop up this crate into other crates. + +The client is exposed in a single function: + +``` +let resp = maj::get("gemini://cetacean.club/", cfg).await?; +``` + +The cfg argument is a rustls::ClientConfig instance, which allows users to specify any custom TLS configuration they want. The most common configuration will probably look something like this: + +``` +use std::sync::Arc; + +pub fn config() -> rustls::ClientConfig { + let mut config = rustls::ClientConfig::new(); + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + + config +} + +struct NoCertificateVerification {} + +impl rustls::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _roots: &rustls::RootCertStore, + _presented_certs: &[rustls::Certificate], + _dns_name: webpki::DNSNameRef<'_>, + _ocsp: &[u8], + ) -> Result { + Ok(rustls::ServerCertVerified::assertion()) + } +} +``` + +This is a bit awkward because rustls really does not like people disabling TLS certificate verification. However, this also gives implementors the freedom to implement a Trust On First Use (TOFU) model like the Gemini spec suggests. + +## The Server + +Maj also supplies a server framework behind the server feature: + +``` +[dependencies] +maj = { version = "0.5", features = ["server"], default-features = false } +rustls = { version = "0.18", features = ["dangerous_configuration"] } +``` + +The core of this framework is the Handler, which is similar to Go's net/http#Handler. Users pass their own Handler implementation to the maj::server::serve function and then wait for that to happen. There are also a few routing macros exposed by maj when the server feature is enabled, and I will go into detail about them below. + +=> https://godoc.org/net/http#Handler net/http#Handler + +Here is a minimal implementation that uses the built-in fileserver handler: + +``` +use async_std::task; +use maj::{route, seg, split, server::*}; +use rustls::{ + internal::pemfile::{certs, pkcs8_private_keys}, + AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig, +}; +use std::{ + fs::File, + io::{self, BufReader}, + path::Path, + sync::Arc, +}; + +fn load_certs(path: &Path) -> io::Result> { + certs(&mut BufReader::new(File::open(path)?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert")) +} + +fn load_keys(path: &Path) -> io::Result> { + pkcs8_private_keys(&mut BufReader::new(File::open(path)?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) +} + +fn main() -> Result<(), maj::server::Error> { + let certs = load_certs("./certs/cert.pem")?; + let mut keys = load_keys("./certs/key.pem")?; + + let mut config = + ServerConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new( + RootCertStore::empty(), + )); + config + .set_single_cert(certs, keys.remove(0)) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + + task::block_on(maj::server::serve( + Arc::new(files::Handler::new("./static")), + config, + "0.0.0.0", + 1965, + ))?; +} +``` + +If you need to do something more fancy, please replace these hard-coded constants with something like structopt. By default this will serve all files in ./static over Gemini with the cert in ./certs/cert.pem and the key in ./certs/key.pem. + +=> https://crates.io/crates/structopt structopt + +For an example of a dynamic route on this site, see /dice: + +=> /dice Dice rolling tool + +## Known Issues + +* For some reason, client certificates are NOT accepted by maj's server framework at this moment. This should be fixed in the future, but currently this is a known problem. + +## Next Steps + +Next I want to make a few applications on top of Maj in order to test where the boundaries are and give fundamental improvements to user experience with it. majc also needs to get a sqlite based history implementation and client certificate support. + +Either way, I hope I can make a valuable tool for creating Gemini sites with Maj. I am also starting to work on majd, which will be a vhost-aware Gemini server that should be safe for use on tilde servers (complete with ~user paths expanding to the given user's public_gemini folders). I also am working on ideas to have cetacean.club automatically served over HTTP as well as Gemini. + +--- + +=> . Go back diff --git a/journal/atom.xml b/journal/atom.xml index 9610cdf..b896b3f 100644 --- a/journal/atom.xml +++ b/journal/atom.xml @@ -2,7 +2,7 @@ gemini://cetacean.club/journal/ Flight Journal - 2020-07-31T11:44:17.156252+00:00 + 2020-08-01T22:50:39.809974+00:00 Cadey Alicia Ratio cadey@firemail.css @@ -10,12 +10,6 @@ python-feedgen - - gemini://cetacean.club/journal/the-cheese-dream.gmi - The Cheese Dream - 2020-07-27T14:40:30.446138+00:00 - - gemini://cetacean.club/journal/xanto.gmi le'i ka na viska kakne ku e le xanto @@ -70,4 +64,10 @@ 2020-07-31T11:44:17.156252+00:00 + + gemini://cetacean.club/journal/08-01-2020-hosted-with-maj.gmi + This site is hosted with Maj + 2020-08-01T22:50:39.809974+00:00 + + diff --git a/journal/index.gmi b/journal/index.gmi index 4eb43f8..5aa5d15 100644 --- a/journal/index.gmi +++ b/journal/index.gmi @@ -14,6 +14,7 @@ These logs will contain thoughts, feelings, major events in my life and other su => 07-29-2020-book-published.gmi 7/29/2020 - My Book is Published! => 07-29-2020-flavortext.gmi 7/29/2020 - Some Hacks for Writing Scenery and Characters => 07-31-2020-newsbook.gmi 7/31/2020 - Newsbook Experiment +=> 08-01-2020-hosted-with-maj.gmi 8/1/2020 - This site is hosted with Maj ## Stories Occasionally I write little stories that are not really connected to anything. I have not really had a good place to publish these in the past. This place seems as good as any.