diff options
21 files changed, 239 insertions, 85 deletions
diff --git a/.gitignore b/.gitignore index 6744025..ceff1aa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,10 @@ dump.rdb .\#* .*~ *~ +/.log /kittybox-rs/test-dir /kittybox-rs/media-store /kittybox-rs/auth-store /kittybox-rs/fonts/* +/kittybox-rs/companion-lite/dist /token.txt diff --git a/kittybox-rs/Cargo.lock b/kittybox-rs/Cargo.lock index 8815b41..e8e0bf3 100644 --- a/kittybox-rs/Cargo.lock +++ b/kittybox-rs/Cargo.lock @@ -171,9 +171,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de18bc5f2e9df8f52da03856bf40e29b747de5a84e43aefff90e3dc4a21529b" +checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" dependencies = [ "async-trait", "axum-core", @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4f44a0e6200e9d11a1cdc989e4b358f6e3d354fbf48478f345a17f4e43f8635" +checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" dependencies = [ "async-trait", "bytes", @@ -214,6 +214,8 @@ dependencies = [ "http", "http-body", "mime", + "tower-layer", + "tower-service", ] [[package]] @@ -1158,6 +1160,25 @@ dependencies = [ ] [[package]] +name = "include_dir" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482a2e29200b7eed25d7fdbd14423326760b7f6658d21a4cf12d55a50713c69f" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e074c19deab2501407c91ba1860fa3d6820bfde307db6d8cb851b55a10be89b" +dependencies = [ + "proc-macro2 1.0.38", + "quote 1.0.18", +] + +[[package]] name = "indexmap" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1229,8 +1250,8 @@ dependencies = [ "futures-util", "hex", "hyper", + "kittybox-frontend-renderer", "kittybox-indieauth", - "kittybox-templates", "kittybox-util", "lazy_static", "listenfd", @@ -1265,34 +1286,36 @@ dependencies = [ ] [[package]] -name = "kittybox-indieauth" +name = "kittybox-frontend-renderer" version = "0.1.0" dependencies = [ - "axum-core", - "data-encoding", + "axum", + "chrono", + "ellipse", + "faker_rand", "http", + "include_dir", + "kittybox-indieauth", + "kittybox-util", + "markup", + "microformats", "rand 0.8.5", - "serde", "serde_json", - "serde_urlencoded", - "sha2", - "url", ] [[package]] -name = "kittybox-templates" +name = "kittybox-indieauth" version = "0.1.0" dependencies = [ - "chrono", - "ellipse", - "faker_rand", + "axum-core", + "data-encoding", "http", - "kittybox-indieauth", - "kittybox-util", - "markup", - "microformats", "rand 0.8.5", + "serde", "serde_json", + "serde_urlencoded", + "sha2", + "url", ] [[package]] diff --git a/kittybox-rs/Cargo.toml b/kittybox-rs/Cargo.toml index 6b0057f..cf16896 100644 --- a/kittybox-rs/Cargo.toml +++ b/kittybox-rs/Cargo.toml @@ -42,7 +42,7 @@ default-members = [".", "./util", "./templates", "./indieauth"] version = "0.1.0" path = "./util" features = ["fs"] -[dependencies.kittybox-templates] +[dependencies.kittybox-frontend-renderer] version = "0.1.0" path = "./templates" [dependencies.kittybox-indieauth] diff --git a/kittybox-rs/build.rs b/kittybox-rs/build.rs index 3d4c62b..c639cf8 100644 --- a/kittybox-rs/build.rs +++ b/kittybox-rs/build.rs @@ -1,20 +1,6 @@ fn main() { use std::env; let out_dir = env::var("OUT_DIR").unwrap(); - println!("cargo:rerun-if-changed=javascript/"); - - if let Ok(exit) = std::process::Command::new("tsc") - .arg("--outDir") - .arg(std::path::Path::new(&out_dir).join("kittybox_js")) - .current_dir("javascript") - .spawn() - .unwrap() - .wait() - { - if !exit.success() { - std::process::exit(exit.code().unwrap_or(1)) - } - } println!("cargo:rerun-if-changed=companion-lite/"); let companion_out = std::path::Path::new(&out_dir).join("companion"); diff --git a/kittybox-rs/src/frontend/login.rs b/kittybox-rs/src/frontend/login.rs index 9665ce7..c693899 100644 --- a/kittybox-rs/src/frontend/login.rs +++ b/kittybox-rs/src/frontend/login.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use crate::frontend::templates::Template; use crate::frontend::{FrontendError, IndiewebEndpoints}; use crate::{database::Storage, ApplicationState}; -use kittybox_templates::LoginPage; +use kittybox_frontend_renderer::LoginPage; pub async fn form<S: Storage>(req: Request<ApplicationState<S>>) -> Result { let owner = req.url().origin().ascii_serialization() + "/"; diff --git a/kittybox-rs/src/frontend/mod.rs b/kittybox-rs/src/frontend/mod.rs index 58de39d..f0f4e5a 100644 --- a/kittybox-rs/src/frontend/mod.rs +++ b/kittybox-rs/src/frontend/mod.rs @@ -12,7 +12,12 @@ use tracing::{debug, error}; //pub mod login; pub mod onboarding; -use kittybox_templates::{Entry, ErrorPage, Feed, MainPage, Template, VCard, POSTS_PER_PAGE}; +use kittybox_frontend_renderer::{ + Entry, Feed, VCard, + ErrorPage, Template, MainPage, + POSTS_PER_PAGE +}; +pub use kittybox_frontend_renderer::assets::statics; #[derive(Debug, Deserialize)] pub struct QueryParams { @@ -266,43 +271,3 @@ pub async fn catchall<D: Storage>( } } } - -const STYLE_CSS: &[u8] = include_bytes!("./style.css"); -// XXX const path handling is ugly, and concat!() doesn't take -// constants, only literals... how annoying! -// -// This might break compiling on inferior operating systems that use -// backslashes as their path separator -const ONBOARDING_JS: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), "/", "kittybox_js", "/", "onboarding.js" -)); -const ONBOARDING_CSS: &[u8] = include_bytes!("./onboarding.css"); -const INDIEAUTH_JS: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), "/", "kittybox_js", "/", "indieauth.js" -)); -const LIB_JS: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), "/", "kittybox_js", "/", "lib.js" -)); -const JSLABELS_HTML: &[u8] = include_bytes!("../../javascript/jslicense.html"); -const MIME_JS: &str = "application/javascript"; -const MIME_CSS: &str = "text/css"; -const MIME_PLAIN: &str = "text/plain"; -const MIME_HTML: &str = "text/html; charset=utf-8"; - -pub async fn statics(Path(name): Path<String>) -> impl IntoResponse { - use axum::http::header::CONTENT_TYPE; - - match name.as_str() { - "style.css" => (StatusCode::OK, [(CONTENT_TYPE, MIME_CSS)], STYLE_CSS), - "onboarding.js" => (StatusCode::OK, [(CONTENT_TYPE, MIME_JS)], ONBOARDING_JS), - "onboarding.css" => (StatusCode::OK, [(CONTENT_TYPE, MIME_CSS)], ONBOARDING_CSS), - "indieauth.js" => (StatusCode::OK, [(CONTENT_TYPE, MIME_JS)], INDIEAUTH_JS), - "lib.js" => (StatusCode::OK, [(CONTENT_TYPE, MIME_JS)], LIB_JS), - "jslicense.html" => (StatusCode::OK, [(CONTENT_TYPE, MIME_HTML)], JSLABELS_HTML), - _ => ( - StatusCode::NOT_FOUND, - [(CONTENT_TYPE, MIME_PLAIN)], - "not found".as_bytes(), - ), - } -} diff --git a/kittybox-rs/src/frontend/onboarding.rs b/kittybox-rs/src/frontend/onboarding.rs index b460e6a..b4bae8e 100644 --- a/kittybox-rs/src/frontend/onboarding.rs +++ b/kittybox-rs/src/frontend/onboarding.rs @@ -5,7 +5,7 @@ use axum::{ response::{Html, IntoResponse}, Json, }; -use kittybox_templates::{ErrorPage, OnboardingPage, Template}; +use kittybox_frontend_renderer::{ErrorPage, OnboardingPage, Template}; use serde::Deserialize; use tracing::{debug, error}; diff --git a/kittybox-rs/src/indieauth/mod.rs b/kittybox-rs/src/indieauth/mod.rs index 6dc9ec6..aaa3301 100644 --- a/kittybox-rs/src/indieauth/mod.rs +++ b/kittybox-rs/src/indieauth/mod.rs @@ -150,12 +150,12 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>( let me = format!("https://{}/", host).parse().unwrap(); // TODO fetch h-app from client_id // TODO verify redirect_uri registration - Html(kittybox_templates::Template { + Html(kittybox_frontend_renderer::Template { title: "Confirm sign-in via IndieAuth", blog_name: "Kittybox", feeds: vec![], user: None, - content: kittybox_templates::AuthorizationRequestPage { + content: kittybox_frontend_renderer::AuthorizationRequestPage { request, credentials: auth.list_user_credential_types(&me).await.unwrap(), user: db.get_post(me.as_str()).await.unwrap().unwrap(), diff --git a/kittybox-rs/templates/Cargo.toml b/kittybox-rs/templates/Cargo.toml index a32a3a2..ffdfc25 100644 --- a/kittybox-rs/templates/Cargo.toml +++ b/kittybox-rs/templates/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "kittybox-templates" +name = "kittybox-frontend-renderer" version = "0.1.0" edition = "2021" @@ -12,10 +12,12 @@ rand = "^0.8.5" version="^0.2.0" [dependencies] -ellipse = "^0.2.0" # Truncate and ellipsize strings in a human-friendly way -http = "^0.2.7" # Hyper's strong HTTP types -markup = "^0.12.0" # HTML templating engine -serde_json = "^1.0.64" # A JSON serialization file format +ellipse = "^0.2.0" +http = "^0.2.7" +markup = "^0.12.0" +serde_json = "^1.0.64" +include_dir = "^0.7.2" +axum = "^0.5.16" [dependencies.chrono] version = "^0.4.19" features = ["serde"] diff --git a/kittybox-rs/javascript/jslicense.html b/kittybox-rs/templates/assets/jslicense.html index 90c681c..90c681c 100644 --- a/kittybox-rs/javascript/jslicense.html +++ b/kittybox-rs/templates/assets/jslicense.html diff --git a/kittybox-rs/src/frontend/onboarding.css b/kittybox-rs/templates/assets/onboarding.css index 6f191b9..6f191b9 100644 --- a/kittybox-rs/src/frontend/onboarding.css +++ b/kittybox-rs/templates/assets/onboarding.css diff --git a/kittybox-rs/src/frontend/style.css b/kittybox-rs/templates/assets/style.css index a8ef6e4..a8ef6e4 100644 --- a/kittybox-rs/src/frontend/style.css +++ b/kittybox-rs/templates/assets/style.css diff --git a/kittybox-rs/templates/build.rs b/kittybox-rs/templates/build.rs new file mode 100644 index 0000000..1140060 --- /dev/null +++ b/kittybox-rs/templates/build.rs @@ -0,0 +1,26 @@ +fn main() { + use std::env; + let out_dir = std::path::PathBuf::from(env::var("OUT_DIR").unwrap()); + println!("cargo:rerun-if-changed=assets/"); + let assets = std::fs::read_dir("assets").unwrap(); + for file in assets.map(|a| a.unwrap()) { + std::fs::copy( + file.path(), + out_dir.join(file.file_name()) + ) + .unwrap(); + } + println!("cargo::rerun-if-changed=javascript/"); + if let Ok(exit) = std::process::Command::new("tsc") + .arg("--outDir") + .arg(&out_dir) + .current_dir("javascript") + .spawn() + .unwrap() + .wait() + { + if !exit.success() { + std::process::exit(exit.code().unwrap_or(1)) + } + } +} diff --git a/kittybox-rs/templates/javascript/dist/indieauth.js b/kittybox-rs/templates/javascript/dist/indieauth.js new file mode 100644 index 0000000..297b4b5 --- /dev/null +++ b/kittybox-rs/templates/javascript/dist/indieauth.js @@ -0,0 +1,118 @@ +"use strict"; +const WEBAUTHN_TIMEOUT = 60 * 1000; +async function webauthn_create_credential() { + const response = await fetch("/.kittybox/webauthn/pre_register"); + const { challenge, rp, user } = await response.json(); + return await navigator.credentials.create({ + publicKey: { + challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)), + rp: rp, + user: { + id: Uint8Array.from(user.cred_id, (c) => c.charCodeAt(0)), + name: user.name, + displayName: user.displayName + }, + pubKeyCredParams: [{ alg: -7, type: "public-key" }], + authenticatorSelection: {}, + timeout: WEBAUTHN_TIMEOUT, + attestation: "none" + } + }); +} +async function webauthn_authenticate() { + const response = await fetch("/.kittybox/webauthn/pre_auth"); + const { challenge, credentials } = await response.json(); + try { + return await navigator.credentials.get({ + publicKey: { + challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)), + allowCredentials: credentials.map(cred => ({ + id: Uint8Array.from(cred.id, c => c.charCodeAt(0)), + type: cred.type + })), + timeout: WEBAUTHN_TIMEOUT + } + }); + } + catch (e) { + console.error("WebAuthn authentication failed:", e); + alert("Using your authenticator failed. (Check the DevTools for details)"); + throw e; + } +} +async function submit_handler(e) { + e.preventDefault(); + if (e.target != null && e.target instanceof HTMLFormElement) { + const form = e.target; + let scopes; + if (form.elements.namedItem("scope") === undefined) { + scopes = []; + } + else if (form.elements.namedItem("scope") instanceof Node) { + scopes = [form.elements.namedItem("scope")] + .filter((e) => e.checked) + .map((e) => e.value); + } + else { + scopes = Array.from(form.elements.namedItem("scope")) + .filter((e) => e.checked) + .map((e) => e.value); + } + const authorization_request = { + response_type: form.elements.namedItem("response_type").value, + client_id: form.elements.namedItem("client_id").value, + redirect_uri: form.elements.namedItem("redirect_uri").value, + state: form.elements.namedItem("state").value, + code_challenge: form.elements.namedItem("code_challenge").value, + code_challenge_method: form.elements.namedItem("code_challenge_method").value, + // I would love to leave that as a list, but such is the form of + // IndieAuth. application/x-www-form-urlencoded doesn't have + // lists, so scopes are space-separated instead. It is annoying. + scope: scopes.length > 0 ? scopes.join(" ") : undefined, + }; + let credential = null; + switch (form.elements.namedItem("auth_method").value) { + case "password": + credential = form.elements.namedItem("user_password").value; + if (credential.length == 0) { + alert("Please enter a password."); + return; + } + break; + case "webauthn": + // credential = await webauthn_authenticate(); + alert("WebAuthn isn't implemented yet!"); + return; + break; + default: + alert("Please choose an authentication method."); + return; + } + console.log("Authorization request:", authorization_request); + console.log("Authentication method:", credential); + const body = JSON.stringify({ + request: authorization_request, + authorization_method: credential + }); + console.log(body); + const response = await fetch(form.action, { + method: form.method, + body: body, + headers: { + "Content-Type": "application/json" + } + }); + if (response.ok) { + let location = response.headers.get("Location"); + if (location != null) { + window.location.href = location; + } + else { + throw "Error: didn't return a location"; + } + } + } + else { + return; + } +} diff --git a/kittybox-rs/templates/javascript/dist/webauthn/register.js b/kittybox-rs/templates/javascript/dist/webauthn/register.js new file mode 100644 index 0000000..3918c74 --- /dev/null +++ b/kittybox-rs/templates/javascript/dist/webauthn/register.js @@ -0,0 +1 @@ +"use strict"; diff --git a/kittybox-rs/javascript/src/indieauth.ts b/kittybox-rs/templates/javascript/src/indieauth.ts index 01732b7..01732b7 100644 --- a/kittybox-rs/javascript/src/indieauth.ts +++ b/kittybox-rs/templates/javascript/src/indieauth.ts diff --git a/kittybox-rs/javascript/src/lib.ts b/kittybox-rs/templates/javascript/src/lib.ts index 38ba65b..38ba65b 100644 --- a/kittybox-rs/javascript/src/lib.ts +++ b/kittybox-rs/templates/javascript/src/lib.ts diff --git a/kittybox-rs/javascript/src/onboarding.ts b/kittybox-rs/templates/javascript/src/onboarding.ts index 0b455eb..0b455eb 100644 --- a/kittybox-rs/javascript/src/onboarding.ts +++ b/kittybox-rs/templates/javascript/src/onboarding.ts diff --git a/kittybox-rs/templates/javascript/src/webauthn/register.ts b/kittybox-rs/templates/javascript/src/webauthn/register.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/kittybox-rs/templates/javascript/src/webauthn/register.ts diff --git a/kittybox-rs/javascript/tsconfig.json b/kittybox-rs/templates/javascript/tsconfig.json index 18b94c7..18b94c7 100644 --- a/kittybox-rs/javascript/tsconfig.json +++ b/kittybox-rs/templates/javascript/tsconfig.json diff --git a/kittybox-rs/templates/src/lib.rs b/kittybox-rs/templates/src/lib.rs index d58e831..5b3a8df 100644 --- a/kittybox-rs/templates/src/lib.rs +++ b/kittybox-rs/templates/src/lib.rs @@ -9,6 +9,37 @@ pub use login::LoginPage; mod mf2; pub use mf2::{Entry, VCard, Feed, Food, POSTS_PER_PAGE}; +pub mod assets { + use axum::response::{IntoResponse, Response}; + use axum::extract::Path; + use axum::http::StatusCode; + use axum::http::header::{CONTENT_TYPE, CACHE_CONTROL}; + + const ASSETS: include_dir::Dir<'static> = include_dir::include_dir!("$OUT_DIR"); + const CACHE_FOR_A_DAY: &str = "max-age=86400"; + + pub async fn statics(Path(path): Path<String>) -> Response { + + let content_type: &'static str = if path.ends_with(".js") { + "application/javascript" + } else if path.ends_with(".css") { + "text/css" + } else if path.ends_with(".html") { + "text/html; charset=\"utf-8\"" + } else { + "application/octet-stream" + }; + + match ASSETS.get_file(path) { + Some(file) => (StatusCode::OK, + [(CONTENT_TYPE, content_type), + (CACHE_CONTROL, CACHE_FOR_A_DAY)], + file.contents()).into_response(), + None => StatusCode::NOT_FOUND.into_response() + } + } +} + #[cfg(test)] mod tests { use faker_rand::en_us::internet::Domain; |