diff --git a/Cargo.toml b/Cargo.toml index 5c4e653..2c91276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,5 +55,6 @@ server = [ members = [ "./majc", "./majd", - "./site" + "./site", + "./pilno/karnycukta" ] diff --git a/pilno/karnycukta/Cargo.toml b/pilno/karnycukta/Cargo.toml new file mode 100644 index 0000000..5a75a51 --- /dev/null +++ b/pilno/karnycukta/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "karnycukta" +version = "0.1.0" +authors = ["Christine Dodrill "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +async-std = "1.5" +async-trait = "0" +atom_syndication = "0.9" +chrono = "*" +log = "0" +pretty_env_logger = "0.4" +webpki = "0.21.0" +rustls = { version = "0.18", features = ["dangerous_configuration"] } +structopt = "0.3" +tokio = { version = "0.2", features = ["full"] } + +maj = { path = "../..", features = ["server", "client"], default-features = false } diff --git a/pilno/karnycukta/src/main.rs b/pilno/karnycukta/src/main.rs new file mode 100644 index 0000000..c98633b --- /dev/null +++ b/pilno/karnycukta/src/main.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use structopt::StructOpt; + +mod selfu; +mod zbasu; + +#[derive(StructOpt, Debug)] +#[structopt(about = "la .karnycukta. cu finti lo samcukta fo lo zo .gemlogs.")] +enum Cmd { + /// selfu la samse'u + Selfu { + #[structopt(flatten)] + opts: selfu::Options, + }, + /// zbasu lo cukta + Zbasu { + #[structopt(long, short = "n")] + nuzyurli: Vec, + + /// How many days to look back + #[structopt(long, short = "d")] + seldei: usize, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + pretty_env_logger::init(); + let cmd = Cmd::from_args(); + log::debug!("{:?}", cmd); + + match cmd { + Cmd::Selfu { opts } => selfu::run(opts).await?, + Cmd::Zbasu { nuzyurli, seldei } => zbasu::run(nuzyurli, seldei).await?, + } + + Ok(()) +} diff --git a/pilno/karnycukta/src/selfu.rs b/pilno/karnycukta/src/selfu.rs new file mode 100644 index 0000000..2bc2b0a --- /dev/null +++ b/pilno/karnycukta/src/selfu.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use rustls::internal::pemfile::{certs, rsa_private_keys}; +use rustls::{ + AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig, +}; +use std::fs::File; +use std::io::{self, BufReader}; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +pub struct Options { + /// host to listen on + #[structopt(short = "H", long, env = "HOST", default_value = "10.77.2.8")] + host: String, + + /// port to listen on + #[structopt(short = "p", long, env = "PORT", default_value = "1965")] + port: u16, + + /// cert file + #[structopt(short = "c", long = "cert", env = "CERT_FILE")] + cert: PathBuf, + + /// key file + #[structopt(short = "k", long = "key", env = "KEY_FILE")] + key: PathBuf, + + /// server hostname + #[structopt( + long = "hostname", + env = "SERVER_HOSTNAME", + default_value = "shachi.wg.akua" + )] + hostname: String, +} + +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> { + rsa_private_keys(&mut BufReader::new(File::open(path)?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) +} + +pub async fn run(opts: Options) -> Result<()> { + let certs = load_certs(&opts.cert)?; + let mut keys = load_keys(&opts.key)?; + + log::info!( + "serving gemini://{} on {}:{}", + opts.hostname, + opts.host, + opts.port + ); + + 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))?; + Ok(()) +} diff --git a/pilno/karnycukta/src/zbasu/mod.rs b/pilno/karnycukta/src/zbasu/mod.rs new file mode 100644 index 0000000..c038ce9 --- /dev/null +++ b/pilno/karnycukta/src/zbasu/mod.rs @@ -0,0 +1,87 @@ +use anyhow::{anyhow, Result}; +use atom_syndication as atom; +use maj::gemini::Node; +use rustls::ClientConfig; +use std::io::{self, BufReader, Cursor, Write}; +use std::ops::Sub; +use std::str; +use chrono::{Duration, prelude::*}; + +mod tls; + +fn gem_to_md(tcana: Vec, out: &mut impl Write) -> io::Result<()> { + use Node::*; + + for tcan in tcana { + match tcan { + Text(body) => { + if body == "---" { + break; + } + + 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(()) +} + +async fn read_feed(gurl: String, cfg: ClientConfig) -> Result { + let resp = maj::get(gurl, cfg).await?; + if resp.status != maj::StatusCode::Success { + Err(anyhow!( + "expected success, got: {} {}", + resp.status as u8, + resp.meta + ))?; + } + + let body = Cursor::new(resp.body); + let feed = atom::Feed::read_from(BufReader::new(body))?; + + Ok(feed) +} + +pub async fn run(nuzyurli: Vec, seldei: usize) -> Result<()> { + let cfg = tls::config(); + let ca: atom::FixedDateTime = Utc::now().into(); + + for urli in nuzyurli { + let feed = read_feed(urli.clone(), cfg.clone()).await?; + log::info!("reading entries for {}: {}", urli, feed.title); + + for entry in feed.entries { + if ca.sub(entry.updated) > Duration::days(seldei as i64) { + continue; + } + let href: String = entry.links()[0].href.clone(); + let resp = maj::get(href, cfg.clone()).await?; + if resp.status != maj::StatusCode::Success { + return Err(anyhow!( + "expected success, got: {} {}", + resp.status as u8, + resp.meta + )); + } + + let body = str::from_utf8(&resp.body)?; + let body = maj::gemini::parse(body); + let mut buf: Vec = vec![]; + gem_to_md(body, &mut buf)?; + println!("{}", str::from_utf8(&buf)?); + } + } + + Ok(()) +} diff --git a/pilno/karnycukta/src/zbasu/tls.rs b/pilno/karnycukta/src/zbasu/tls.rs new file mode 100644 index 0000000..95a2b7b --- /dev/null +++ b/pilno/karnycukta/src/zbasu/tls.rs @@ -0,0 +1,24 @@ +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()) + } +} diff --git a/shell.nix b/shell.nix index 9714826..d71bcc1 100644 --- a/shell.nix +++ b/shell.nix @@ -1,10 +1,25 @@ -{ pkgs ? import {} }: +{ pkgs ? import { } }: -pkgs.mkShell { +let + nur = import (builtins.fetchTarball + "https://github.com/nix-community/NUR/archive/master.tar.gz") { + inherit pkgs; + }; + tex = with pkgs; + texlive.combine { inherit (texlive) scheme-medium bitter titlesec; }; +in pkgs.mkShell { buildInputs = with pkgs; [ - rustc cargo rls rustfmt cargo-watch + rustc + cargo + rls + rustfmt + cargo-watch pkg-config ncurses + + kindlegen + nur.repos.mic92.pandoc-bin + tex ]; } diff --git a/src/gemini.rs b/src/gemini.rs index da43635..3be2b5e 100644 --- a/src/gemini.rs +++ b/src/gemini.rs @@ -61,12 +61,12 @@ pub fn render(nodes: Vec, out: &mut impl Write) -> io::Result<()> { for node in nodes { match node { Text(body) => write!(out, "{}\n", body)?, - Link{to, name} => match name { + Link { to, name } => match name { Some(name) => write!(out, "{} {}\n", to, name)?, None => write!(out, "{}\n", to)?, }, Preformatted(body) => write!(out, "```\n{}\n```\n", body)?, - Heading { level, body} => write!(out, "{} {}\n", "#".repeat(level as usize), body)?, + Heading { level, body } => write!(out, "{} {}\n", "#".repeat(level as usize), body)?, ListItem(body) => write!(out, "* {}\n", body)?, Quote(body) => write!(out, "> {}\n", body)?, };