From 11279d97ac2f5945a282c6d2bbff6343e76875d5 Mon Sep 17 00:00:00 2001 From: phil Date: Wed, 3 Sep 2025 15:00:57 -0400 Subject: [PATCH 1/3] basic token verification works --- Cargo.lock | 276 +++++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + pocket/Cargo.toml | 19 +++ pocket/src/lib.rs | 5 + pocket/src/main.rs | 9 ++ pocket/src/server.rs | 208 +++++++++++++++++++++++++++++++ pocket/src/token.rs | 72 +++++++++++ quasar/src/storage.rs | 4 + 8 files changed, 561 insertions(+), 33 deletions(-) create mode 100644 pocket/Cargo.toml create mode 100644 pocket/src/lib.rs create mode 100644 pocket/src/main.rs create mode 100644 pocket/src/server.rs create mode 100644 pocket/src/token.rs diff --git a/Cargo.lock b/Cargo.lock index 7fc7650..eaf4675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,7 +192,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", ] @@ -370,6 +370,19 @@ dependencies = [ "web-time", ] +[[package]] +name = "atrium-crypto" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a3da430c71dd9006d61072c20771f264e5c498420a49c32305ceab8bd71955" +dependencies = [ + "ecdsa", + "k256", + "multibase", + "p256", + "thiserror 1.0.69", +] + [[package]] name = "atrium-identity" version = "0.1.5" @@ -628,7 +641,7 @@ dependencies = [ "axum", "handlebars", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -773,6 +786,22 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -906,6 +935,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cid" version = "0.11.1" @@ -1171,6 +1227,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1514,7 +1576,7 @@ dependencies = [ "slog-bunyan", "slog-json", "slog-term", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-rustls 0.25.0", "toml", @@ -1683,6 +1745,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1814,7 +1888,7 @@ dependencies = [ "mixtrics", "pin-project", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -1834,7 +1908,7 @@ dependencies = [ "parking_lot", "pin-project", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "twox-hash", ] @@ -1867,7 +1941,7 @@ dependencies = [ "parking_lot", "pin-project", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -1899,7 +1973,7 @@ dependencies = [ "pin-project", "rand 0.9.1", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "twox-hash", @@ -2120,6 +2194,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "handlebars" version = "6.3.2" @@ -2133,7 +2217,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "walkdir", ] @@ -2169,6 +2253,15 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] + [[package]] name = "headers" version = "0.4.0" @@ -2222,6 +2315,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + [[package]] name = "hickory-proto" version = "0.25.2" @@ -2240,7 +2342,7 @@ dependencies = [ "once_cell", "rand 0.9.1", "ring", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tokio", "tracing", @@ -2263,7 +2365,7 @@ dependencies = [ "rand 0.9.1", "resolv-conf", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -2757,7 +2859,7 @@ dependencies = [ "metrics", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-tungstenite 0.26.2", "url", @@ -2859,6 +2961,39 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "jwt-compact" +version = "0.9.0-beta.1" +source = "git+https://github.com/fatfingers23/jwt-compact.git#aed088b8ff5ad44ef2785c453f6a4b7916728b1c" +dependencies = [ + "anyhow", + "base64ct", + "chrono", + "ciborium", + "hmac", + "lazy_static", + "rand_core 0.6.4", + "secp256k1", + "serde", + "serde_json", + "sha2", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + [[package]] name = "langtag" version = "0.3.4" @@ -2951,6 +3086,16 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.22" @@ -2969,7 +3114,7 @@ dependencies = [ "anyhow", "fluent-uri", "nom", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyjson", ] @@ -3235,7 +3380,7 @@ dependencies = [ "metrics", "metrics-util 0.20.0", "quanta", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -3784,7 +3929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", "ucd-trie", ] @@ -3880,6 +4025,25 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "pocket" +version = "0.1.0" +dependencies = [ + "atrium-crypto", + "clap", + "jwt-compact", + "log", + "poem", + "poem-openapi", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tracing-subscriber", +] + [[package]] name = "poem" version = "3.1.12" @@ -3918,7 +4082,7 @@ dependencies = [ "smallvec", "sync_wrapper", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-rustls 0.26.2", "tokio-stream", @@ -3962,7 +4126,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "serde_yaml", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", ] @@ -3981,7 +4145,7 @@ dependencies = [ "quote", "regex", "syn 2.0.103", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -4130,7 +4294,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls 0.23.31", "socket2 0.5.9", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -4151,7 +4315,7 @@ dependencies = [ "rustls 0.23.31", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -4504,6 +4668,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4732,6 +4910,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -4865,7 +5063,7 @@ dependencies = [ "percent-encoding", "ryu", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -5008,7 +5206,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", ] @@ -5050,7 +5248,7 @@ dependencies = [ "rustls 0.23.31", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", "tokio", "tokio-util", @@ -5161,7 +5359,7 @@ dependencies = [ "serde", "serde_json", "serde_qs", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyjson", "tokio", "tokio-tungstenite 0.27.0", @@ -5327,11 +5525,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -5347,9 +5545,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -5742,7 +5940,7 @@ dependencies = [ "native-tls", "rand 0.9.1", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.16", "url", "utf-8", ] @@ -5760,7 +5958,7 @@ dependencies = [ "log", "rand 0.9.1", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.16", "utf-8", ] @@ -5813,7 +6011,7 @@ dependencies = [ "serde_qs", "sha2", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "tikv-jemallocator", "tokio", "tokio-util", @@ -6147,7 +6345,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-util", "url", @@ -6540,7 +6738,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", ] @@ -6651,6 +6849,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "serde", + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e87ea96..25cbcc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ members = [ "who-am-i", "slingshot", "quasar", + "pocket", ] diff --git a/pocket/Cargo.toml b/pocket/Cargo.toml new file mode 100644 index 0000000..3b785ff --- /dev/null +++ b/pocket/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pocket" +version = "0.1.0" +edition = "2024" + +[dependencies] +atrium-crypto = "0.1.2" +clap = { version = "4.5.41", features = ["derive"] } +jwt-compact = { git = "https://github.com/fatfingers23/jwt-compact.git", features = ["es256k"] } +log = "0.4.27" +poem = { version = "3.1.12", features = ["acme", "static-files"] } +poem-openapi = { version = "5.1.16", features = ["scalar"] } +reqwest = { version = "0.12.22", features = ["json"] } +rusqlite = "0.37.0" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = { version = "1.0.141" } +thiserror = "2.0.16" +tokio = { version = "1.47.0", features = ["full"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/pocket/src/lib.rs b/pocket/src/lib.rs new file mode 100644 index 0000000..1a7a0be --- /dev/null +++ b/pocket/src/lib.rs @@ -0,0 +1,5 @@ +mod server; +mod token; + +pub use server::serve; +pub use token::verify; diff --git a/pocket/src/main.rs b/pocket/src/main.rs new file mode 100644 index 0000000..a68f1b8 --- /dev/null +++ b/pocket/src/main.rs @@ -0,0 +1,9 @@ +use pocket::serve; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + println!("Hello, world!"); + serve("mac.cinnebar-tet.ts.net").await +} + diff --git a/pocket/src/server.rs b/pocket/src/server.rs new file mode 100644 index 0000000..940630d --- /dev/null +++ b/pocket/src/server.rs @@ -0,0 +1,208 @@ +use poem::{ + endpoint::make_sync, + Endpoint, + Route, + Server, + EndpointExt, + http::{Method, HeaderMap}, + middleware::{CatchPanic, Cors, Tracing}, + listener::TcpListener, +}; +use poem_openapi::{ + ContactObject, + ExternalDocumentObject, + OpenApi, + OpenApiService, + Tags, + Object, + ApiResponse, + types::Example, + auth::Bearer, + payload::Json, + SecurityScheme, +}; +use crate::verify; +use serde::Serialize; +use serde_json::{Value, json}; + + +#[derive(Debug, SecurityScheme)] +#[oai(ty = "bearer")] +struct BlahAuth(Bearer); + + +#[derive(Tags)] +enum ApiTags { + /// Bluesky-compatible APIs. + #[oai(rename = "app.bsky.* queries")] + AppBsky, +} + +#[derive(Object)] +#[oai(example = true)] +struct XrpcErrorResponseObject { + /// Should correspond an error `name` in the lexicon errors array + error: String, + /// Human-readable description and possibly additonal context + message: String, +} +impl Example for XrpcErrorResponseObject { + fn example() -> Self { + Self { + error: "PreferencesNotFound".to_string(), + message: "No preferences were found for this user".to_string(), + } + } +} +type XrpcError = Json; +fn xrpc_error(error: impl AsRef, message: impl AsRef) -> XrpcError { + Json(XrpcErrorResponseObject { + error: error.as_ref().to_string(), + message: message.as_ref().to_string(), + }) +} + +#[derive(Object)] +#[oai(example = true)] +struct GetBskyPrefsResponseObject { + /// at-uri for this record + preferences: Value, +} +impl Example for GetBskyPrefsResponseObject { + fn example() -> Self { + Self { + preferences: json!({ + "hello": "world", + }), + } + } +} + +#[derive(ApiResponse)] +enum GetBskyPrefsResponse { + /// Record found + #[oai(status = 200)] + Ok(Json), + /// Bad request or no preferences to return + #[oai(status = 400)] + BadRequest(XrpcError), + // /// Server errors + // #[oai(status = 500)] + // ServerError(XrpcError), +} + +struct Xrpc { + domain: String, +} + +#[OpenApi] +impl Xrpc { + /// app.bsky.actor.getPreferences + /// + /// get stored bluesky prefs + #[oai( + path = "/app.bsky.actor.getPreferences", + method = "get", + tag = "ApiTags::AppBsky" + )] + async fn app_bsky_get_prefs( + &self, + BlahAuth(auth): BlahAuth, + m: &HeaderMap, + ) -> GetBskyPrefsResponse { + log::warn!("hm: {m:?}"); + match verify( + &format!("did:web:{}#bsky_appview", self.domain), + "app.bsky.actor.getPreferences", + &auth.token, + ).await { + Ok(did) => log::info!("wooo! {did}"), + Err(err) => return GetBskyPrefsResponse::BadRequest(xrpc_error("booo", err)), + }; + log::warn!("got bearer: {:?}", auth.token); + GetBskyPrefsResponse::Ok(Json(GetBskyPrefsResponseObject::example())) + } + + /// app.bsky.actor.putPreferences + /// + /// store bluesky prefs + #[oai( + path = "/app.bsky.actor.putPreferences", + method = "post", + tag = "ApiTags::AppBsky" + )] + async fn app_bsky_put_prefs( + &self, + Json(prefs): Json, + ) -> () { + log::warn!("received prefs: {prefs:?}"); + () + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct AppViewService { + id: String, + r#type: String, + service_endpoint: String, +} +#[derive(Debug, Clone, Serialize)] +struct AppViewDoc { + id: String, + service: [AppViewService; 1], +} +/// Serve a did document for did:web for this to be an xrpc appview +fn get_did_doc(domain: &str) -> impl Endpoint + use<> { + let doc = poem::web::Json(AppViewDoc { + id: format!("did:web:{domain}"), + service: [AppViewService { + id: "#bsky_appview".to_string(), + r#type: "PocketBlueskyPreferences".to_string(), + service_endpoint: format!("https://{domain}"), + }], + }); + make_sync(move |_| doc.clone()) +} + +pub async fn serve( + domain: &str, +) -> () { + let api_service = OpenApiService::new( + Xrpc { domain: domain.to_string() }, + "Pocket", + env!("CARGO_PKG_VERSION"), + ) + .server(domain) + .url_prefix("/xrpc") + .contact( + ContactObject::new() + .name("@microcosm.blue") + .url("https://bsky.app/profile/microcosm.blue"), + ) + // .description(include_str!("../api-description.md")) + .external_document(ExternalDocumentObject::new( + "https://microcosm.blue/pocket", + )); + + let app = Route::new() + .at("/.well-known/did.json", get_did_doc(&domain)) + .nest("/xrpc/", api_service) + // .at("/", StaticFileEndpoint::new("./static/index.html")) + // .nest("/openapi", api_service.spec_endpoint()) + .with( + Cors::new() + .allow_method(Method::GET) + .allow_method(Method::POST) + ) + .with(CatchPanic::new()) + .with(Tracing); + + let listener = TcpListener::bind("127.0.0.1:3000"); + Server::new(listener) + .name("pocket") + .run(app) + .await + .unwrap(); + +} diff --git a/pocket/src/token.rs b/pocket/src/token.rs new file mode 100644 index 0000000..23e513f --- /dev/null +++ b/pocket/src/token.rs @@ -0,0 +1,72 @@ +use jwt_compact::{Claims, UntrustedToken}; +use atrium_crypto::did::parse_multikey; +use atrium_crypto::verify::Verifier; +use std::collections::HashMap; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct MiniDoc { + signing_key: String, +} + +pub async fn verify( + expected_aud: &str, + expected_lxm: &str, + token: &str, +) -> Result { + let untrusted = UntrustedToken::new(token).unwrap(); + + let claims: Claims> = untrusted.deserialize_claims_unchecked().unwrap(); + + let Some(did) = claims.custom.get("iss") else { + return Err("jwt must include the user's did in `iss`"); + }; + + if !did.starts_with("did:") { + return Err("iss should be a did"); + } + if did.contains("#") { + return Err("iss should be a user did without a service identifier"); + } + + println!("Claims: {claims:#?}"); + println!("did: {did:#?}"); + + let endpoint = "https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc"; + let doc: MiniDoc = reqwest::get(format!("{endpoint}?identifier={did}")) + .await + .unwrap() + .error_for_status() + .unwrap() + .json() + .await + .unwrap(); + + log::info!("got minidoc response: {doc:?}"); + + let (alg, public_key) = parse_multikey(&doc.signing_key).unwrap(); + log::info!("parsed key: {public_key:?}"); + + Verifier::default().verify( + alg, + &public_key, + &untrusted.signed_data, + untrusted.signature_bytes(), + ).unwrap(); + // if this passes, then our claims were trustworthy after all(??) + + let Some(aud) = claims.custom.get("aud") else { + return Err("missing aud"); + }; + if aud != expected_aud { + return Err("wrong aud"); + } + let Some(lxm) = claims.custom.get("lxm") else { + return Err("missing lxm"); + }; + if lxm != expected_lxm { + return Err("wrong lxm"); + } + + Ok(did.to_string()) +} diff --git a/quasar/src/storage.rs b/quasar/src/storage.rs index e69de29..4a26f98 100644 --- a/quasar/src/storage.rs +++ b/quasar/src/storage.rs @@ -0,0 +1,4 @@ + +trait Storage { + +} -- 2.51.2 From 2b3a4515b60e94f49fafc52836b570280099b5e6 Mon Sep 17 00:00:00 2001 From: phil Date: Wed, 3 Sep 2025 20:00:55 -0400 Subject: [PATCH 2/3] little cleanup basic api stuff --- .github/workflows/checks.yml | 2 +- Makefile | 2 +- pocket/api-description.md | 17 ++++ pocket/src/lib.rs | 2 +- pocket/src/main.rs | 1 - pocket/src/server.rs | 150 +++++++++++++++---------------- pocket/src/token.rs | 165 ++++++++++++++++++++++++----------- pocket/static/index.html | 67 ++++++++++++++ quasar/src/lib.rs | 2 + quasar/src/storage.rs | 2 +- 10 files changed, 275 insertions(+), 135 deletions(-) create mode 100644 pocket/api-description.md create mode 100644 pocket/static/index.html diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index f26327e..2a1b2cb 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -28,7 +28,7 @@ jobs: - name: get nightly toolchain for jetstream fmt run: rustup toolchain install nightly --allow-downgrade -c rustfmt - name: fmt - run: cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot -- --check + run: cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot --package pocket -- --check - name: fmt jetstream (nightly) run: cargo +nightly fmt --package jetstream -- --check - name: clippy diff --git a/Makefile b/Makefile index eb29af2..7e52ceb 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ test: cargo test --all-features fmt: - cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot + cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot --package pocket cargo +nightly fmt --package jetstream clippy: diff --git a/pocket/api-description.md b/pocket/api-description.md new file mode 100644 index 0000000..3d2a5be --- /dev/null +++ b/pocket/api-description.md @@ -0,0 +1,17 @@ +_A pocket dimension to stash a bit of non-public user data._ + + +# Pocket: user preference storage + +This API leverages atproto service proxying to offer a bit of per-user per-app non-public data storage. +Perfect for things like application preferences that might be better left out of the public PDS data. + +The intent is to use oauth scopes to isolate storage on a per-application basis, and to allow easy data migration from a community hosted instance to your own if you end up needing that. + + +### Current status + +> [!important] +> Pocket is currently in a **v0, pre-release state**. There is one production instance and you can use it! Expect short downtimes for restarts as development progresses and occaisional data loss until it's stable. + +ATProto might end up adding a similar feature to [PDSs](https://atproto.com/guides/glossary#pds-personal-data-server). If/when that happens, you should use it instead of this! diff --git a/pocket/src/lib.rs b/pocket/src/lib.rs index 1a7a0be..c6a7fd8 100644 --- a/pocket/src/lib.rs +++ b/pocket/src/lib.rs @@ -2,4 +2,4 @@ mod server; mod token; pub use server::serve; -pub use token::verify; +pub use token::TokenVerifier; diff --git a/pocket/src/main.rs b/pocket/src/main.rs index a68f1b8..46a7ad9 100644 --- a/pocket/src/main.rs +++ b/pocket/src/main.rs @@ -6,4 +6,3 @@ async fn main() { println!("Hello, world!"); serve("mac.cinnebar-tet.ts.net").await } - diff --git a/pocket/src/server.rs b/pocket/src/server.rs index 940630d..9c031e0 100644 --- a/pocket/src/server.rs +++ b/pocket/src/server.rs @@ -1,41 +1,30 @@ +use crate::TokenVerifier; use poem::{ - endpoint::make_sync, - Endpoint, - Route, - Server, - EndpointExt, - http::{Method, HeaderMap}, - middleware::{CatchPanic, Cors, Tracing}, + Endpoint, EndpointExt, Route, Server, + endpoint::{StaticFileEndpoint, make_sync}, + http::Method, listener::TcpListener, + middleware::{CatchPanic, Cors, Tracing}, }; use poem_openapi::{ - ContactObject, - ExternalDocumentObject, - OpenApi, - OpenApiService, - Tags, - Object, - ApiResponse, - types::Example, + ApiResponse, ContactObject, ExternalDocumentObject, Object, OpenApi, OpenApiService, + SecurityScheme, Tags, auth::Bearer, - payload::Json, - SecurityScheme, + payload::{Json, PlainText}, + types::Example, }; -use crate::verify; use serde::Serialize; use serde_json::{Value, json}; - #[derive(Debug, SecurityScheme)] #[oai(ty = "bearer")] -struct BlahAuth(Bearer); - +struct XrpcAuth(Bearer); #[derive(Tags)] enum ApiTags { - /// Bluesky-compatible APIs. - #[oai(rename = "app.bsky.* queries")] - AppBsky, + /// Custom pocket APIs + #[oai(rename = "Pocket APIs")] + Pocket, } #[derive(Object)] @@ -86,57 +75,74 @@ enum GetBskyPrefsResponse { /// Bad request or no preferences to return #[oai(status = 400)] BadRequest(XrpcError), +} + +#[derive(ApiResponse)] +enum PutBskyPrefsResponse { + /// Record found + #[oai(status = 200)] + Ok(PlainText), + /// Bad request or no preferences to return + #[oai(status = 400)] + BadRequest(XrpcError), // /// Server errors // #[oai(status = 500)] // ServerError(XrpcError), } struct Xrpc { - domain: String, + verifier: TokenVerifier, } #[OpenApi] impl Xrpc { - /// app.bsky.actor.getPreferences + /// com.bad-example.pocket.getPreferences /// /// get stored bluesky prefs #[oai( - path = "/app.bsky.actor.getPreferences", + path = "/com.bad-example.pocket.getPreferences", method = "get", - tag = "ApiTags::AppBsky" + tag = "ApiTags::Pocket" )] - async fn app_bsky_get_prefs( - &self, - BlahAuth(auth): BlahAuth, - m: &HeaderMap, - ) -> GetBskyPrefsResponse { - log::warn!("hm: {m:?}"); - match verify( - &format!("did:web:{}#bsky_appview", self.domain), - "app.bsky.actor.getPreferences", - &auth.token, - ).await { - Ok(did) => log::info!("wooo! {did}"), - Err(err) => return GetBskyPrefsResponse::BadRequest(xrpc_error("booo", err)), + async fn app_bsky_get_prefs(&self, XrpcAuth(auth): XrpcAuth) -> GetBskyPrefsResponse { + let did = match self + .verifier + .verify("app.bsky.actor.getPreferences", &auth.token) + .await + { + Ok(d) => d, + Err(e) => return GetBskyPrefsResponse::BadRequest(xrpc_error("boooo", e.to_string())), }; - log::warn!("got bearer: {:?}", auth.token); + log::info!("verified did: {did}"); + // TODO: fetch from storage GetBskyPrefsResponse::Ok(Json(GetBskyPrefsResponseObject::example())) } - /// app.bsky.actor.putPreferences + /// com.bad-example.pocket.putPreferences /// /// store bluesky prefs #[oai( - path = "/app.bsky.actor.putPreferences", + path = "/com.bad-example.pocket.putPreferences", method = "post", - tag = "ApiTags::AppBsky" + tag = "ApiTags::Pocket" )] async fn app_bsky_put_prefs( &self, + XrpcAuth(auth): XrpcAuth, Json(prefs): Json, - ) -> () { + ) -> PutBskyPrefsResponse { + let did = match self + .verifier + .verify("app.bsky.actor.getPreferences", &auth.token) + .await + { + Ok(d) => d, + Err(e) => return PutBskyPrefsResponse::BadRequest(xrpc_error("boooo", e.to_string())), + }; + log::info!("verified did: {did}"); log::warn!("received prefs: {prefs:?}"); - () + // TODO: put prefs into storage + PutBskyPrefsResponse::Ok(PlainText("hiiiiii".to_string())) } } @@ -157,52 +163,40 @@ fn get_did_doc(domain: &str) -> impl Endpoint + use<> { let doc = poem::web::Json(AppViewDoc { id: format!("did:web:{domain}"), service: [AppViewService { - id: "#bsky_appview".to_string(), - r#type: "PocketBlueskyPreferences".to_string(), + id: "#pocket_prefs".to_string(), + r#type: "PocketPreferences".to_string(), service_endpoint: format!("https://{domain}"), }], }); make_sync(move |_| doc.clone()) } -pub async fn serve( - domain: &str, -) -> () { - let api_service = OpenApiService::new( - Xrpc { domain: domain.to_string() }, - "Pocket", - env!("CARGO_PKG_VERSION"), - ) - .server(domain) - .url_prefix("/xrpc") - .contact( - ContactObject::new() - .name("@microcosm.blue") - .url("https://bsky.app/profile/microcosm.blue"), - ) - // .description(include_str!("../api-description.md")) - .external_document(ExternalDocumentObject::new( - "https://microcosm.blue/pocket", - )); +pub async fn serve(domain: &str) -> () { + let verifier = TokenVerifier::new(domain); + let api_service = OpenApiService::new(Xrpc { verifier }, "Pocket", env!("CARGO_PKG_VERSION")) + .server(domain) + .url_prefix("/xrpc") + .contact( + ContactObject::new() + .name("@microcosm.blue") + .url("https://bsky.app/profile/microcosm.blue"), + ) + .description(include_str!("../api-description.md")) + .external_document(ExternalDocumentObject::new("https://microcosm.blue/pocket")); let app = Route::new() - .at("/.well-known/did.json", get_did_doc(&domain)) + .nest("/openapi", api_service.spec_endpoint()) .nest("/xrpc/", api_service) - // .at("/", StaticFileEndpoint::new("./static/index.html")) - // .nest("/openapi", api_service.spec_endpoint()) + .at("/.well-known/did.json", get_did_doc(domain)) + .at("/", StaticFileEndpoint::new("./static/index.html")) .with( Cors::new() .allow_method(Method::GET) - .allow_method(Method::POST) + .allow_method(Method::POST), ) .with(CatchPanic::new()) .with(Tracing); let listener = TcpListener::bind("127.0.0.1:3000"); - Server::new(listener) - .name("pocket") - .run(app) - .await - .unwrap(); - + Server::new(listener).name("pocket").run(app).await.unwrap(); } diff --git a/pocket/src/token.rs b/pocket/src/token.rs index 23e513f..33a7394 100644 --- a/pocket/src/token.rs +++ b/pocket/src/token.rs @@ -1,72 +1,133 @@ -use jwt_compact::{Claims, UntrustedToken}; use atrium_crypto::did::parse_multikey; use atrium_crypto::verify::Verifier; -use std::collections::HashMap; +use jwt_compact::UntrustedToken; use serde::Deserialize; +use std::collections::HashMap; +use std::time::Duration; +use thiserror::Error; #[derive(Debug, Deserialize)] struct MiniDoc { signing_key: String, + did: String, } -pub async fn verify( - expected_aud: &str, - expected_lxm: &str, - token: &str, -) -> Result { - let untrusted = UntrustedToken::new(token).unwrap(); - - let claims: Claims> = untrusted.deserialize_claims_unchecked().unwrap(); +#[derive(Error, Debug)] +pub enum VerifyError { + #[error("The cross-service authorization token failed verification: {0}")] + VerificationFailed(&'static str), + #[error("Error trying to resolve the DID to a signing key, retry in a moment: {0}")] + ResolutionFailed(&'static str), +} - let Some(did) = claims.custom.get("iss") else { - return Err("jwt must include the user's did in `iss`"); - }; +pub struct TokenVerifier { + domain: String, + client: reqwest::Client, +} - if !did.starts_with("did:") { - return Err("iss should be a did"); - } - if did.contains("#") { - return Err("iss should be a user did without a service identifier"); +impl TokenVerifier { + pub fn new(domain: &str) -> Self { + let client = reqwest::Client::builder() + .user_agent(format!( + "microcosm pocket v{} (dev: @bad-example.com)", + env!("CARGO_PKG_VERSION") + )) + .no_proxy() + .timeout(Duration::from_secs(12)) // slingshot timeout is 10s + .build() + .unwrap(); + Self { + client, + domain: domain.to_string(), + } } - println!("Claims: {claims:#?}"); - println!("did: {did:#?}"); + pub async fn verify(&self, expected_lxm: &str, token: &str) -> Result { + let untrusted = UntrustedToken::new(token).unwrap(); - let endpoint = "https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc"; - let doc: MiniDoc = reqwest::get(format!("{endpoint}?identifier={did}")) - .await - .unwrap() - .error_for_status() - .unwrap() - .json() - .await - .unwrap(); + // danger! unfortunately we need to decode the DID from the jwt body before we have a public key to verify the jwt with + let Ok(untrusted_claims) = + untrusted.deserialize_claims_unchecked::>() + else { + return Err(VerifyError::VerificationFailed( + "could not deserialize jtw claims", + )); + }; - log::info!("got minidoc response: {doc:?}"); + // get the (untrusted!) claimed DID + let Some(untrusted_did) = untrusted_claims.custom.get("iss") else { + return Err(VerifyError::VerificationFailed( + "jwt must include the user's did in `iss`", + )); + }; - let (alg, public_key) = parse_multikey(&doc.signing_key).unwrap(); - log::info!("parsed key: {public_key:?}"); + // bail if it's not even a user-ish did + if !untrusted_did.starts_with("did:") { + return Err(VerifyError::VerificationFailed("iss should be a did")); + } + if untrusted_did.contains("#") { + return Err(VerifyError::VerificationFailed( + "iss should be a user did without a service identifier", + )); + } - Verifier::default().verify( - alg, - &public_key, - &untrusted.signed_data, - untrusted.signature_bytes(), - ).unwrap(); - // if this passes, then our claims were trustworthy after all(??) + let endpoint = + "https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc"; + let doc: MiniDoc = self + .client + .get(format!("{endpoint}?identifier={untrusted_did}")) + .send() + .await + .map_err(|_| VerifyError::ResolutionFailed("failed to fetch minidoc"))? + .error_for_status() + .map_err(|_| VerifyError::ResolutionFailed("non-ok response for minidoc"))? + .json() + .await + .map_err(|_| VerifyError::ResolutionFailed("failed to parse json to minidoc"))?; - let Some(aud) = claims.custom.get("aud") else { - return Err("missing aud"); - }; - if aud != expected_aud { - return Err("wrong aud"); - } - let Some(lxm) = claims.custom.get("lxm") else { - return Err("missing lxm"); - }; - if lxm != expected_lxm { - return Err("wrong lxm"); - } + // sanity check before we go ahead with this signing key + if doc.did != *untrusted_did { + return Err(VerifyError::VerificationFailed( + "wtf, resolveMiniDoc returned a doc for a different DID, slingshot bug", + )); + } + + let Ok((alg, public_key)) = parse_multikey(&doc.signing_key) else { + return Err(VerifyError::VerificationFailed( + "could not parse signing key form minidoc", + )); + }; + + // i _guess_ we've successfully bootstrapped the verification of the jwt unless this fails + if let Err(e) = Verifier::default().verify( + alg, + &public_key, + &untrusted.signed_data, + untrusted.signature_bytes(), + ) { + log::warn!("jwt verification failed: {e}"); + return Err(VerifyError::VerificationFailed( + "jwt signature verification failed", + )); + } - Ok(did.to_string()) + // past this point we're should have established trust. crossing ts and dotting is. + let did = &untrusted_did; + let claims = &untrusted_claims; + + let Some(aud) = claims.custom.get("aud") else { + return Err(VerifyError::VerificationFailed("missing aud")); + }; + if *aud != format!("did:web:{}#bsky_appview", self.domain) { + return Err(VerifyError::VerificationFailed("wrong aud")); + } + let Some(lxm) = claims.custom.get("lxm") else { + return Err(VerifyError::VerificationFailed("missing lxm")); + }; + if lxm != expected_lxm { + return Err(VerifyError::VerificationFailed("wrong lxm")); + } + + Ok(did.to_string()) + } } diff --git a/pocket/static/index.html b/pocket/static/index.html new file mode 100644 index 0000000..05f8bd3 --- /dev/null +++ b/pocket/static/index.html @@ -0,0 +1,67 @@ + + + + + Pocket: atproto user preference storage + + + + + +
+

+ TODO: thing +

+ +
+ + + + + + + + diff --git a/quasar/src/lib.rs b/quasar/src/lib.rs index 80aaac2..851d8a6 100644 --- a/quasar/src/lib.rs +++ b/quasar/src/lib.rs @@ -1 +1,3 @@ mod storage; + +pub use storage::Storage; diff --git a/quasar/src/storage.rs b/quasar/src/storage.rs index 4a26f98..e243f5f 100644 --- a/quasar/src/storage.rs +++ b/quasar/src/storage.rs @@ -1,4 +1,4 @@ -trait Storage { +pub trait Storage { } -- 2.51.2 From b515a72c279f7295b4c3376a64b85b67a389d8d9 Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 4 Sep 2025 16:05:52 -0400 Subject: [PATCH 3/3] reflector: an any-domain did:web->service mapper --- Cargo.lock | 78 ++++++++++++++++++---------------------- Cargo.toml | 1 + Makefile | 9 ++++- reflector/Cargo.toml | 12 +++++++ reflector/readme.md | 9 +++++ reflector/src/main.rs | 83 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 147 insertions(+), 45 deletions(-) create mode 100644 reflector/Cargo.toml create mode 100644 reflector/readme.md create mode 100644 reflector/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index eaf4675..15af227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -989,9 +989,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.46" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -999,9 +999,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.46" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -1011,9 +1011,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", @@ -3148,9 +3148,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" @@ -3300,11 +3300,11 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -3603,12 +3603,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3810,12 +3809,6 @@ dependencies = [ "hashbrown 0.13.2", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -4499,6 +4492,18 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "reflector" +version = "0.1.0" +dependencies = [ + "clap", + "log", + "poem", + "serde", + "tokio", + "tracing-subscriber", +] + [[package]] name = "regex" version = "1.11.1" @@ -4507,17 +4512,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -4528,15 +4524,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -5650,9 +5640,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -5893,14 +5883,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", diff --git a/Cargo.toml b/Cargo.toml index 25cbcc0..a119dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ members = [ "slingshot", "quasar", "pocket", + "reflector", ] diff --git a/Makefile b/Makefile index 7e52ceb..a495ad9 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,14 @@ test: cargo test --all-features fmt: - cargo fmt --package links --package constellation --package ufos --package spacedust --package who-am-i --package slingshot --package pocket + cargo fmt --package links \ + --package constellation \ + --package ufos \ + --package spacedust \ + --package who-am-i \ + --package slingshot \ + --package pocket \ + --package reflector cargo +nightly fmt --package jetstream clippy: diff --git a/reflector/Cargo.toml b/reflector/Cargo.toml new file mode 100644 index 0000000..9911736 --- /dev/null +++ b/reflector/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "reflector" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.47", features = ["derive"] } +log = "0.4.28" +poem = "3.1.12" +serde = { version = "1.0.219", features = ["derive"] } +tokio = "1.47.1" +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } diff --git a/reflector/readme.md b/reflector/readme.md new file mode 100644 index 0000000..18ea187 --- /dev/null +++ b/reflector/readme.md @@ -0,0 +1,9 @@ +# reflector + +a tiny did:web service server that maps subdomains to a single service endpoint + +receiving requests from multiple subdomains is left as a problem for the reverse proxy to solve, since acme wildcard certificates (ie. letsencrypt) require the most complicated and involved challenge type (DNS). + +caddy [has good support for](https://caddyserver.com/docs/caddyfile/patterns#wildcard-certificates) configuring the wildcard DNS challenge with various DNS providers, and also supports [on-demand](https://caddyserver.com/docs/automatic-https#using-on-demand-tls) provisioning via the simpler methods. + +if you only need a small fixed number of subdomains, you can also use certbot or otherwise individually configure them in your reverse proxy. diff --git a/reflector/src/main.rs b/reflector/src/main.rs new file mode 100644 index 0000000..6edadac --- /dev/null +++ b/reflector/src/main.rs @@ -0,0 +1,83 @@ +use clap::Parser; +use poem::{ + EndpointExt, Route, Server, get, handler, + listener::TcpListener, + middleware::{AddData, Tracing}, + web::{Data, Json, TypedHeader, headers::Host}, +}; +use serde::Serialize; + +#[handler] +fn hello() -> String { + "ɹoʇɔǝʅⅎǝɹ".to_string() +} + +#[derive(Debug, Serialize)] +struct DidDoc { + id: String, + service: [DidService; 1], +} + +#[derive(Debug, Clone, Serialize)] +struct DidService { + id: String, + r#type: String, + service_endpoint: String, +} + +#[handler] +fn did_doc(TypedHeader(host): TypedHeader, service: Data<&DidService>) -> Json { + Json(DidDoc { + id: format!("did:web:{}", host.hostname()), + service: [service.clone()], + }) +} + +/// Slingshot record edge cache +#[derive(Parser, Debug, Clone)] +#[command(version, about, long_about = None)] +struct Args { + /// The DID document service ID to serve + /// + /// must start with a '#', like `#bsky_appview' + #[arg(long)] + id: String, + /// Service type + /// + /// Not sure exactly what its requirements are. 'BlueskyAppview' for example + #[arg(long)] + r#type: String, + /// The HTTPS endpoint for the service + #[arg(long)] + service_endpoint: String, +} + +impl From for DidService { + fn from(a: Args) -> Self { + Self { + id: a.id, + r#type: a.r#type, + service_endpoint: a.service_endpoint, + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + tracing_subscriber::fmt::init(); + log::info!("ɹoʇɔǝʅⅎǝɹ"); + + let args = Args::parse(); + let service: DidService = args.into(); + + Server::new(TcpListener::bind("0.0.0.0:3001")) + .run( + Route::new() + .at("/", get(hello)) + .at("/.well-known/did.json", get(did_doc)) + .with(AddData::new(service)) + .with(Tracing), + ) + .await + .unwrap() +} -- 2.51.2