diff options
author | bryan newbold <bnewbold@robocracy.org> | 2023-02-19 17:01:07 -0800 |
---|---|---|
committer | bryan newbold <bnewbold@robocracy.org> | 2023-02-19 17:19:39 -0800 |
commit | ec2bf0c54245cd84f492847d2a1e070919b14a53 (patch) | |
tree | dbeb5b28c8b7e06eb9ac192d14ea4fdec81bb1e7 | |
parent | b8ba815b4cafdff48694d14c994e862738d342ef (diff) | |
download | adenosine-ec2bf0c54245cd84f492847d2a1e070919b14a53.tar.gz adenosine-ec2bf0c54245cd84f492847d2a1e070919b14a53.zip |
more refactoring of common code and APIs
-rw-r--r-- | adenosine-cli/src/bin/adenosine.rs | 4 | ||||
-rw-r--r-- | adenosine-pds/Cargo.toml | 32 | ||||
-rw-r--r-- | adenosine-pds/src/bin/adenosine-pds.rs | 6 | ||||
-rw-r--r-- | adenosine-pds/src/db.rs | 15 | ||||
-rw-r--r-- | adenosine-pds/src/db_bsky.rs (renamed from adenosine-pds/src/bsky.rs) | 71 | ||||
-rw-r--r-- | adenosine-pds/src/lib.rs | 134 | ||||
-rw-r--r-- | adenosine-pds/src/web.rs | 11 | ||||
-rw-r--r-- | adenosine/Cargo.toml | 37 | ||||
-rw-r--r-- | adenosine/src/app_bsky/mod.rs (renamed from adenosine-pds/src/models.rs) | 83 | ||||
-rw-r--r-- | adenosine/src/auth.rs | 37 | ||||
-rw-r--r-- | adenosine/src/com_atproto/mod.rs | 29 | ||||
-rw-r--r-- | adenosine/src/com_atproto/repo/mod.rs | 53 | ||||
-rw-r--r-- | adenosine/src/ipld.rs | 47 | ||||
-rw-r--r-- | adenosine/src/lib.rs | 231 | ||||
-rw-r--r-- | adenosine/src/plc.rs (renamed from adenosine/src/did.rs) | 0 | ||||
-rw-r--r-- | adenosine/src/xrpc.rs | 186 | ||||
-rw-r--r-- | adenosine/tests/bigger.car (renamed from adenosine-pds/tests/bigger.car) | bin | 60050 -> 60050 bytes | |||
-rw-r--r-- | adenosine/tests/example_repo.car (renamed from adenosine-pds/tests/example_repo.car) | bin | 7390 -> 7390 bytes | |||
-rw-r--r-- | adenosine/tests/test_mst_interop.rs (renamed from adenosine-pds/tests/test_mst_interop.rs) | 6 | ||||
-rw-r--r-- | adenosine/tests/test_repro_mst.rs (renamed from adenosine-pds/tests/test_repro_mst.rs) | 2 |
20 files changed, 498 insertions, 486 deletions
diff --git a/adenosine-cli/src/bin/adenosine.rs b/adenosine-cli/src/bin/adenosine.rs index 4164181..e35d634 100644 --- a/adenosine-cli/src/bin/adenosine.rs +++ b/adenosine-cli/src/bin/adenosine.rs @@ -1,5 +1,7 @@ +use adenosine::auth::parse_did_from_jwt; +use adenosine::created_at_now; use adenosine::identifiers::*; -use adenosine::*; +use adenosine::xrpc::{XrpcClient, XrpcMethod}; use adenosine_cli::*; use anyhow::anyhow; use serde_json::{json, Value}; diff --git a/adenosine-pds/Cargo.toml b/adenosine-pds/Cargo.toml index 41cfaae..f9dba1a 100644 --- a/adenosine-pds/Cargo.toml +++ b/adenosine-pds/Cargo.toml @@ -13,32 +13,24 @@ readme = "README.md" repository = "https://gitlab.com/bnewbold/adenosine" [dependencies] +adenosine = { version = "0.2.0", path = "../adenosine" } anyhow = "1" -structopt = "0.3" -serde = "1" -serde_json = "1" +askama = { version = "0.11", features = ["serde-json"] } +bcrypt = "0.13" +data-encoding = "2" +dotenvy = "0.15" +#ipfs-sqlite-block-store = "0.13" +lazy_static = "1" +libipld = { version = "0.14", features = ["dag-cbor", "derive"] } log = "0.4" pretty_env_logger = "0.4" -libipld = "0.14" -ipfs-sqlite-block-store = "0.13" +rouille = "3" rusqlite = { version = "0.26", features = ["bundled"] } rusqlite_migration = "1" -# NOTE: lexicon validation not implemented yet -#jsonschema = "*" -#schemafy = "*" -rouille = "3" -# NOTE: vendored for now -#iroh-car = "*" -adenosine = { version = "0.2.0", path = "../adenosine" } +serde = "1" +serde_json = "1" +structopt = "0.3" tokio = { version = "1", features = ["full"] } -futures = "0.3" -lazy_static = "1" -bcrypt = "0.13" -data-encoding = "2" -# TODO: replace this with data-encoding or similar; this is only needed for ucan_p256 stuff -async-trait = "0.1" -dotenvy = "0.15" -askama = { version = "0.11", features = ["serde-json"] } [package.metadata.deb] maintainer = "Bryan Newbold <bnewbold@robocracy.org>" diff --git a/adenosine-pds/src/bin/adenosine-pds.rs b/adenosine-pds/src/bin/adenosine-pds.rs index c16e7e0..c004098 100644 --- a/adenosine-pds/src/bin/adenosine-pds.rs +++ b/adenosine-pds/src/bin/adenosine-pds.rs @@ -1,5 +1,7 @@ +use adenosine::com_atproto; +use adenosine::crypto::KeyPair; use adenosine::mst; -use adenosine_pds::models::AccountRequest; +use adenosine::repo::RepoStore; use adenosine_pds::*; use anyhow::Result; use serde_json::json; @@ -208,7 +210,7 @@ fn main() -> Result<()> { public_url, did_plc, } => { - let req = AccountRequest { + let req = com_atproto::AccountRequest { email, handle: handle.clone(), password, diff --git a/adenosine-pds/src/db.rs b/adenosine-pds/src/db.rs index b694bb5..4cbece6 100644 --- a/adenosine-pds/src/db.rs +++ b/adenosine-pds/src/db.rs @@ -1,6 +1,7 @@ -use crate::models::{FollowRecord, Post, RefRecord}; /// ATP database (as distinct from blockstore) -use crate::{created_at_now, ipld_into_json_value, AtpSession, Did, KeyPair, Tid}; +use crate::{created_at_now, ipld_into_json_value, Did, KeyPair, Tid}; +use adenosine::app_bsky; +use adenosine::com_atproto; use anyhow::{anyhow, Result}; use lazy_static::lazy_static; use libipld::cbor::DagCborCodec; @@ -105,7 +106,7 @@ impl AtpDatabase { handle: &str, password: &str, keypair: &KeyPair, - ) -> Result<AtpSession> { + ) -> Result<com_atproto::Session> { let mut stmt = self .conn .prepare_cached("SELECT did, password_bcrypt FROM account WHERE handle = ?1")?; @@ -120,7 +121,7 @@ impl AtpDatabase { .conn .prepare_cached("INSERT INTO session (did, jwt) VALUES (?1, ?2)")?; stmt.execute(params!(did.to_string(), jwt))?; - Ok(AtpSession { + Ok(com_atproto::Session { did: did.to_string(), name: handle.to_string(), accessJwt: jwt.to_string(), @@ -192,7 +193,7 @@ impl AtpDatabase { // need to re-compute the CID from DagCbor re-encoding, I guess. bleh. let block = Block::<DefaultParams>::encode(DagCborCodec, Code::Sha2_256, &val)?; let cid = *block.cid(); - let post: Post = serde_json::from_value(ipld_into_json_value(val))?; + let post: app_bsky::Post = serde_json::from_value(ipld_into_json_value(val))?; let (reply_to_parent_uri, reply_to_root_uri) = match post.reply { Some(ref reply) => (Some(reply.parent.uri.clone()), Some(reply.root.uri.clone())), None => (None, None), @@ -226,7 +227,7 @@ impl AtpDatabase { val: Option<Ipld>, ) -> Result<()> { if let Some(val) = val { - let ref_obj: RefRecord = serde_json::from_value(ipld_into_json_value(val))?; + let ref_obj: app_bsky::RefRecord = serde_json::from_value(ipld_into_json_value(val))?; let mut stmt = self .conn .prepare_cached("INSERT INTO bsky_ref (ref_type, did, tid, subject_uri, subject_cid, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)")?; @@ -253,7 +254,7 @@ impl AtpDatabase { pub fn bsky_upsert_follow(&mut self, did: &Did, tid: &Tid, val: Option<Ipld>) -> Result<()> { if let Some(val) = val { - let follow: FollowRecord = serde_json::from_value(ipld_into_json_value(val))?; + let follow: app_bsky::FollowRecord = serde_json::from_value(ipld_into_json_value(val))?; let mut stmt = self .conn .prepare_cached("INSERT INTO bsky_follow (did, tid, subject_did, created_at) VALUES (?1, ?2, ?3, ?4)")?; diff --git a/adenosine-pds/src/bsky.rs b/adenosine-pds/src/db_bsky.rs index caa16f6..ee8e0f3 100644 --- a/adenosine-pds/src/bsky.rs +++ b/adenosine-pds/src/db_bsky.rs @@ -1,11 +1,9 @@ -use crate::models::*; /// Helper functions for doing database and repo operations relating to bluesky endpoints and /// records -use crate::{ - ipld_into_json_value, json_value_into_ipld, AtpDatabase, AtpService, Did, Result, Tid, - XrpcError, -}; -use adenosine::identifiers::{AtUri, DidOrHost, Nsid}; +use crate::{AtpDatabase, AtpService, Result, XrpcError}; +use adenosine::app_bsky; +use adenosine::identifiers::{AtUri, Did, DidOrHost, Nsid, Tid}; +use adenosine::ipld::{ipld_into_json_value, json_value_into_ipld}; use adenosine::repo::Mutation; use anyhow::anyhow; use libipld::Cid; @@ -61,7 +59,7 @@ pub fn bsky_mutate_db(db: &mut AtpDatabase, did: &Did, mutations: Vec<Mutation>) } // TODO: should probably return Result<Option<Profile>>? -pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> { +pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<app_bsky::Profile> { // first get the profile record let mut profile_cid: Option<Cid> = None; let commit_cid = match srv.repo.lookup_commit(did)? { @@ -78,7 +76,7 @@ pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> { } let (display_name, description): (Option<String>, Option<String>) = if let Some(cid) = profile_cid { - let record: ProfileRecord = + let record: app_bsky::ProfileRecord = serde_json::from_value(ipld_into_json_value(srv.repo.get_ipld(&cid)?))?; (Some(record.displayName), record.description) } else { @@ -104,11 +102,11 @@ pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> { .conn .prepare_cached("SELECT COUNT(*) FROM bsky_follow WHERE subject_did = $1")?; let followers_count: u64 = stmt.query_row(params!(did.to_string()), |row| row.get(0))?; - let decl = DeclRef { + let decl = app_bsky::DeclRef { actorType: "app.bsky.system.actorUser".to_string(), cid: "bafyreid27zk7lbis4zw5fz4podbvbs4fc5ivwji3dmrwa6zggnj4bnd57u".to_string(), }; - Ok(Profile { + Ok(app_bsky::Profile { did: did.to_string(), handle, creator: did.to_string(), @@ -123,7 +121,11 @@ pub fn bsky_get_profile(srv: &mut AtpService, did: &Did) -> Result<Profile> { }) } -pub fn bsky_update_profile(srv: &mut AtpService, did: &Did, profile: ProfileRecord) -> Result<()> { +pub fn bsky_update_profile( + srv: &mut AtpService, + did: &Did, + profile: app_bsky::ProfileRecord, +) -> Result<()> { // get the profile record let mut profile_tid: Option<Tid> = None; let commit_cid = match srv.repo.lookup_commit(did)? { @@ -175,7 +177,7 @@ fn feed_row(row: &rusqlite::Row) -> Result<FeedRow> { }) } -fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<FeedItem> { +fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<app_bsky::FeedItem> { let record_ipld = srv.repo.get_ipld(&row.item_post_cid)?; let uri = format!( "at://{}/{}/{}", @@ -198,10 +200,10 @@ fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<FeedItem> { .prepare_cached("SELECT COUNT(*) FROM bsky_post WHERE reply_to_parent_uri = $1")?; let reply_count: u64 = stmt.query_row(params!(uri), |row| row.get(0))?; - let feed_item = FeedItem { + let feed_item = app_bsky::FeedItem { uri, cid: row.item_post_cid.to_string(), - author: User { + author: app_bsky::User { did: row.item_did.to_string(), handle: row.item_handle, displayName: None, // TODO: fetch from profile (or cache) @@ -219,8 +221,8 @@ fn feed_row_to_item(srv: &mut AtpService, row: FeedRow) -> Result<FeedItem> { Ok(feed_item) } -pub fn bsky_get_timeline(srv: &mut AtpService, did: &Did) -> Result<GenericFeed> { - let mut feed: Vec<FeedItem> = vec![]; +pub fn bsky_get_timeline(srv: &mut AtpService, did: &Did) -> Result<app_bsky::GenericFeed> { + let mut feed: Vec<app_bsky::FeedItem> = vec![]; // TODO: also handle reposts let rows = { let mut stmt = srv.atp_db @@ -237,11 +239,11 @@ pub fn bsky_get_timeline(srv: &mut AtpService, did: &Did) -> Result<GenericFeed> for row in rows { feed.push(feed_row_to_item(srv, row)?); } - Ok(GenericFeed { feed }) + Ok(app_bsky::GenericFeed { feed }) } -pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<GenericFeed> { - let mut feed: Vec<FeedItem> = vec![]; +pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<app_bsky::GenericFeed> { + let mut feed: Vec<app_bsky::FeedItem> = vec![]; // TODO: also handle reposts let rows = { let mut stmt = srv.atp_db @@ -258,7 +260,7 @@ pub fn bsky_get_author_feed(srv: &mut AtpService, did: &Did) -> Result<GenericFe for row in rows { feed.push(feed_row_to_item(srv, row)?); } - Ok(GenericFeed { feed }) + Ok(app_bsky::GenericFeed { feed }) } // TODO: this is a partial implementation @@ -267,7 +269,7 @@ pub fn bsky_get_thread( srv: &mut AtpService, uri: &AtUri, _depth: Option<u64>, -) -> Result<PostThread> { +) -> Result<app_bsky::PostThread> { // parse the URI let did = match uri.repository { DidOrHost::Did(ref did_type, ref did_body) => { @@ -283,7 +285,7 @@ pub fn bsky_get_thread( _ => Err(anyhow!("expected a record in uri: {}", uri))?, }; - // post itself, as a FeedItem + // post itself, as a app_bsky::FeedItem let post_items = { let mut stmt = srv.atp_db .conn @@ -320,7 +322,7 @@ pub fn bsky_get_thread( }; for row in rows { let item = feed_row_to_item(srv, row)?; - children.push(ThreadItem { + children.push(app_bsky::ThreadItem { uri: item.uri, cid: item.cid, author: item.author, @@ -339,7 +341,7 @@ pub fn bsky_get_thread( }); } - let post = ThreadItem { + let post = app_bsky::ThreadItem { uri: post_item.uri, cid: post_item.cid, author: post_item.author, @@ -354,19 +356,20 @@ pub fn bsky_get_thread( indexedAt: post_item.indexedAt, myState: None, }; - Ok(PostThread { thread: post }) + Ok(app_bsky::PostThread { thread: post }) } #[test] fn test_bsky_profile() { use crate::{create_account, created_at_now}; + use adenosine::com_atproto; use libipld::ipld; let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap(); let follow_nsid = Nsid::from_str("app.bsky.graph.follow").unwrap(); let mut srv = AtpService::new_ephemeral().unwrap(); - let req = AccountRequest { + let req = com_atproto::AccountRequest { email: "test@bogus.com".to_string(), handle: "handle.test".to_string(), password: "bogus".to_string(), @@ -384,7 +387,7 @@ fn test_bsky_profile() { assert_eq!(profile.followsCount, 0); assert_eq!(profile.postsCount, 0); - let record = ProfileRecord { + let record = app_bsky::ProfileRecord { displayName: "Test Name".to_string(), description: Some("short description".to_string()), }; @@ -393,7 +396,7 @@ fn test_bsky_profile() { assert_eq!(profile.displayName, Some(record.displayName)); assert_eq!(profile.description, record.description); - let record = ProfileRecord { + let record = app_bsky::ProfileRecord { displayName: "New Test Name".to_string(), description: Some("longer description".to_string()), }; @@ -444,6 +447,7 @@ fn test_bsky_profile() { fn test_bsky_feeds() { // TODO: test that displayName comes through in feeds and timelines (it does not currently) use crate::{create_account, created_at_now}; + use adenosine::com_atproto; use libipld::ipld; let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap(); @@ -453,7 +457,7 @@ fn test_bsky_feeds() { let mut srv = AtpService::new_ephemeral().unwrap(); let alice_did = { - let req = AccountRequest { + let req = com_atproto::AccountRequest { email: "alice@bogus.com".to_string(), handle: "alice.test".to_string(), password: "bogus".to_string(), @@ -464,7 +468,7 @@ fn test_bsky_feeds() { Did::from_str(&session.did).unwrap() }; let bob_did = { - let req = AccountRequest { + let req = com_atproto::AccountRequest { email: "bob@bogus.com".to_string(), handle: "bob.test".to_string(), password: "bogus".to_string(), @@ -475,7 +479,7 @@ fn test_bsky_feeds() { Did::from_str(&session.did).unwrap() }; let carol_did = { - let req = AccountRequest { + let req = com_atproto::AccountRequest { email: "carol@bogus.com".to_string(), handle: "carol.test".to_string(), password: "bogus".to_string(), @@ -672,13 +676,14 @@ fn test_bsky_feeds() { #[test] fn test_bsky_thread() { use crate::create_account; + use adenosine::com_atproto; use libipld::ipld; let post_nsid = Nsid::from_str("app.bsky.feed.post").unwrap(); let mut srv = AtpService::new_ephemeral().unwrap(); let alice_did = { - let req = AccountRequest { + let req = com_atproto::AccountRequest { email: "alice@bogus.com".to_string(), handle: "alice.test".to_string(), password: "bogus".to_string(), @@ -689,7 +694,7 @@ fn test_bsky_thread() { Did::from_str(&session.did).unwrap() }; let bob_did = { - let req = AccountRequest { + let req = com_atproto::AccountRequest { email: "bob@bogus.com".to_string(), handle: "bob.test".to_string(), password: "bogus".to_string(), diff --git a/adenosine-pds/src/lib.rs b/adenosine-pds/src/lib.rs index 07905f9..cca9b0d 100644 --- a/adenosine-pds/src/lib.rs +++ b/adenosine-pds/src/lib.rs @@ -2,33 +2,51 @@ use adenosine::created_at_now; use adenosine::identifiers::{AtUri, Did, Nsid, Ticker, Tid}; use anyhow::{anyhow, Result}; use askama::Template; -use libipld::Cid; -use libipld::Ipld; use log::{debug, error, info, warn}; use rouille::{router, Request, Response}; use serde_json::{json, Value}; -use std::collections::BTreeMap; use std::fmt; use std::io::Read; use std::path::PathBuf; use std::str::FromStr; use std::sync::Mutex; -mod bsky; mod db; -pub mod models; +mod db_bsky; mod web; -pub use adenosine::crypto::{KeyPair, PubKey}; -pub use adenosine::did; -pub use adenosine::did::DidDocMeta; -pub use adenosine::repo::{Mutation, RepoCommit, RepoStore}; -pub use adenosine::ucan_p256::P256KeyMaterial; -use bsky::*; -pub use db::AtpDatabase; -pub use models::*; +use adenosine::app_bsky; +use adenosine::com_atproto; +use adenosine::crypto::KeyPair; +use adenosine::ipld::{ipld_into_json_value, json_value_into_ipld}; +use adenosine::plc; +use adenosine::plc::DidDocMeta; +use adenosine::repo::{Mutation, RepoStore}; +use db::AtpDatabase; +use db_bsky::*; use web::*; +#[derive(Debug)] +pub enum XrpcError { + BadRequest(String), + NotFound(String), + Forbidden(String), + MutexPoisoned, +} + +impl std::error::Error for XrpcError {} + +impl fmt::Display for XrpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadRequest(msg) | Self::NotFound(msg) | Self::Forbidden(msg) => { + write!(f, "{}", msg) + } + Self::MutexPoisoned => write!(f, "service mutex poisoned"), + } + } +} + pub struct AtpService { pub repo: RepoStore, pub atp_db: AtpDatabase, @@ -58,27 +76,6 @@ impl Default for AtpServiceConfig { } } -#[derive(Debug)] -enum XrpcError { - BadRequest(String), - NotFound(String), - Forbidden(String), - MutexPoisoned, -} - -impl std::error::Error for XrpcError {} - -impl fmt::Display for XrpcError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::BadRequest(msg) | Self::NotFound(msg) | Self::Forbidden(msg) => { - write!(f, "{}", msg) - } - Self::MutexPoisoned => write!(f, "service mutex poisoned"), - } - } -} - /// Helper to take an XRPC result (always a JSON object), and transform it to a rouille response fn xrpc_wrap<S: serde::Serialize>(resp: Result<S>) -> Response { match resp { @@ -256,49 +253,6 @@ impl AtpService { } } -/// Intentionally serializing with this instead of DAG-JSON, because ATP schemas don't encode CID -/// links in any special way, they just pass the CID as a string. -fn ipld_into_json_value(val: Ipld) -> Value { - match val { - Ipld::Null => Value::Null, - Ipld::Bool(b) => Value::Bool(b), - Ipld::Integer(v) => json!(v), - Ipld::Float(v) => json!(v), - Ipld::String(s) => Value::String(s), - Ipld::Bytes(b) => Value::String(data_encoding::BASE64_NOPAD.encode(&b)), - Ipld::List(l) => Value::Array(l.into_iter().map(ipld_into_json_value).collect()), - Ipld::Map(m) => Value::Object(serde_json::Map::from_iter( - m.into_iter().map(|(k, v)| (k, ipld_into_json_value(v))), - )), - Ipld::Link(c) => Value::String(c.to_string()), - } -} - -/// Crude reverse generation -/// -/// Does not handle base64 to bytes, and the link generation is pretty simple (object elements with -/// key "car"). Numbers always come through as f64 (float). -fn json_value_into_ipld(val: Value) -> Ipld { - match val { - Value::Null => Ipld::Null, - Value::Bool(b) => Ipld::Bool(b), - Value::String(s) => Ipld::String(s), - // TODO: handle numbers better? - Value::Number(v) => Ipld::Float(v.as_f64().unwrap()), - Value::Array(l) => Ipld::List(l.into_iter().map(json_value_into_ipld).collect()), - Value::Object(m) => { - let map: BTreeMap<String, Ipld> = BTreeMap::from_iter(m.into_iter().map(|(k, v)| { - if k == "car" && v.is_string() { - (k, Ipld::Link(Cid::from_str(v.as_str().unwrap()).unwrap())) - } else { - (k, json_value_into_ipld(v)) - } - })); - Ipld::Map(map) - } - } -} - fn xrpc_required_param(request: &Request, key: &str) -> Result<String> { Ok(request.get_param(key).ok_or(XrpcError::BadRequest(format!( "require '{}' query parameter", @@ -424,7 +378,7 @@ fn xrpc_get_handler( let mut srv = srv.lock().or(Err(XrpcError::MutexPoisoned))?; let did_doc = srv.atp_db.get_did_doc(&did)?; let collections: Vec<String> = srv.repo.collections(&did)?; - let desc = RepoDescribe { + let desc = com_atproto::repo::Describe { name: did.to_string(), // TODO: handle? did: did.to_string(), didDoc: did_doc, @@ -506,9 +460,9 @@ fn xrpc_get_repo_handler(srv: &Mutex<AtpService>, request: &Request) -> Result<V pub fn create_account( srv: &mut AtpService, - req: &AccountRequest, + req: &com_atproto::AccountRequest, create_did_plc: bool, -) -> Result<AtpSession> { +) -> Result<com_atproto::Session> { // check if account already exists (fast path, also confirmed by database schema) if srv.atp_db.account_exists(&req.handle, &req.email)? { Err(XrpcError::BadRequest( @@ -520,7 +474,7 @@ pub fn create_account( let (did, did_doc) = if create_did_plc { // generate DID - let create_op = did::CreateOp::new( + let create_op = plc::CreateOp::new( req.handle.clone(), srv.config.public_url.clone(), &srv.pds_keypair, @@ -576,7 +530,7 @@ fn xrpc_post_handler( match method { "com.atproto.account.create" => { // validate account request - let req: AccountRequest = rouille::input::json_input(request) + let req: com_atproto::AccountRequest = rouille::input::json_input(request) .map_err(|e| XrpcError::BadRequest(format!("failed to parse JSON body: {}", e)))?; // TODO: validate handle, email, recoverykey let mut srv = srv.lock().unwrap(); @@ -602,7 +556,7 @@ fn xrpc_post_handler( Ok(json!(sess)) } "com.atproto.session.create" => { - let req: SessionRequest = rouille::input::json_input(request) + let req: com_atproto::SessionRequest = rouille::input::json_input(request) .map_err(|e| XrpcError::BadRequest(format!("failed to parse JSON body: {}", e)))?; let mut srv = srv.lock().unwrap(); let keypair = srv.pds_keypair.clone(); @@ -628,7 +582,7 @@ fn xrpc_post_handler( .resolve_did(&did)? .expect("DID matches to a handle"); - Ok(json!(AtpSession { + Ok(json!(com_atproto::Session { did: did.to_string(), name: handle, accessJwt: jwt.to_string(), @@ -653,7 +607,7 @@ fn xrpc_post_handler( Ok(json!({})) } "com.atproto.repo.batchWrite" => { - let batch: RepoBatchWriteBody = rouille::input::json_input(request)?; + let batch: com_atproto::repo::BatchWriteBody = rouille::input::json_input(request)?; // TODO: validate edits against schemas let did = Did::from_str(&batch.did)?; let mut srv = srv.lock().unwrap(); @@ -690,7 +644,7 @@ fn xrpc_post_handler( } "com.atproto.repo.createRecord" => { // TODO: validate edits against schemas - let create: RepoCreateRecord = rouille::input::json_input(request)?; + let create: com_atproto::repo::CreateRecord = rouille::input::json_input(request)?; let did = Did::from_str(&create.did)?; let collection = Nsid::from_str(&create.collection)?; let mut srv = srv.lock().unwrap(); @@ -707,7 +661,7 @@ fn xrpc_post_handler( } "com.atproto.repo.putRecord" => { // TODO: validate edits against schemas - let put: RepoPutRecord = rouille::input::json_input(request)?; + let put: com_atproto::repo::PutRecord = rouille::input::json_input(request)?; let did = Did::from_str(&put.did)?; let collection = Nsid::from_str(&put.collection)?; let tid = Tid::from_str(&put.rkey)?; @@ -725,7 +679,7 @@ fn xrpc_post_handler( Ok(json!({})) } "com.atproto.repo.deleteRecord" => { - let delete: RepoDeleteRecord = rouille::input::json_input(request)?; + let delete: com_atproto::repo::DeleteRecord = rouille::input::json_input(request)?; let did = Did::from_str(&delete.did)?; let collection = Nsid::from_str(&delete.collection)?; let tid = Tid::from_str(&delete.rkey)?; @@ -754,7 +708,7 @@ fn xrpc_post_handler( } // =========== app.bsky methods "app.bsky.actor.updateProfile" => { - let profile: ProfileRecord = rouille::input::json_input(request)?; + let profile: app_bsky::ProfileRecord = rouille::input::json_input(request)?; let mut srv = srv.lock().unwrap(); let auth_did = &xrpc_check_auth_header(&mut srv, request, None)?; bsky_update_profile(&mut srv, auth_did, profile)?; @@ -865,7 +819,7 @@ fn repo_view_handler(srv: &Mutex<AtpService>, did: &str, request: &Request) -> R let commit_cid = &srv.repo.lookup_commit(&did)?.unwrap(); let commit = srv.repo.get_commit(commit_cid)?; let collections: Vec<String> = srv.repo.collections(&did)?; - let desc = RepoDescribe { + let desc = com_atproto::repo::Describe { name: did.to_string(), // TODO did: did.to_string(), didDoc: did_doc, diff --git a/adenosine-pds/src/web.rs b/adenosine-pds/src/web.rs index 0d80b4e..8f15a49 100644 --- a/adenosine-pds/src/web.rs +++ b/adenosine-pds/src/web.rs @@ -1,4 +1,5 @@ -use crate::models::*; +use adenosine::app_bsky; +use adenosine::com_atproto; use adenosine::identifiers::{Did, Nsid, Tid}; use adenosine::repo::RepoCommit; use askama::Template; @@ -28,8 +29,8 @@ pub struct AboutView { pub struct AccountView { pub domain: String, pub did: Did, - pub profile: Profile, - pub feed: Vec<FeedItem>, + pub profile: app_bsky::Profile, + pub feed: Vec<app_bsky::FeedItem>, } #[derive(Template)] @@ -39,7 +40,7 @@ pub struct ThreadView { pub did: Did, pub collection: Nsid, pub tid: Tid, - pub post: ThreadItem, + pub post: app_bsky::ThreadItem, } #[derive(Template)] @@ -48,7 +49,7 @@ pub struct RepoView { pub domain: String, pub did: Did, pub commit: RepoCommit, - pub describe: RepoDescribe, + pub describe: com_atproto::repo::Describe, } #[derive(Template)] diff --git a/adenosine/Cargo.toml b/adenosine/Cargo.toml index 456b0fe..f4705ae 100644 --- a/adenosine/Cargo.toml +++ b/adenosine/Cargo.toml @@ -15,33 +15,32 @@ repository = "https://gitlab.com/bnewbold/adenosine" [dependencies] # NOTE: could try 'rustls-tls' feature instead of default native TLS? -reqwest = { version = "0.11", features = ["blocking", "json"] } -serde = { version = "1", features = ["serde_derive"] } -serde_json = "1" +anyhow = "1" +async-trait = "0.1" base64 = "0.13" -regex = "1" -lazy_static = "1" +bs58 = "0.4" data-encoding = "2" +futures = "0.3" +ipfs-sqlite-block-store = "0.13" +lazy_static = "1" +libipld = { version = "0.14", features = ["dag-cbor", "derive"] } +log = "0.4" +multibase = "0.9" rand = "0.8" +regex = "1" +reqwest = { version = "0.11", features = ["blocking", "json"] } +serde = { version = "1", features = ["serde_derive"] } +serde_json = "1" +sha256 = "1" time = { version = "0.3", features = ["formatting"] } +tokio = { version = "1", features = ["full"] } + +# crypto/auth stuff k256 = { version = "0.11", features = ["ecdsa"] } p256 = { version = "0.11", features = ["ecdsa"] } -bs58 = "0.4" -libipld = "0.14" -ipfs-sqlite-block-store = "0.13" -tokio = { version = "1", features = ["full"] } -futures = "0.3" -sha256 = "1" -multibase = "0.9" ucan = "0.7.0-alpha.1" -async-trait = "0.1" # for vendored iroh-car -thiserror = "1.0" integer-encoding = { version = "3", features = ["tokio_async"] } multihash = "0.16" - -# uncertain about these... -anyhow = "1" -log = "0.4" -env_logger = "0.7" +thiserror = "1.0" diff --git a/adenosine-pds/src/models.rs b/adenosine/src/app_bsky/mod.rs index f827a7a..18a0449 100644 --- a/adenosine-pds/src/models.rs +++ b/adenosine/src/app_bsky/mod.rs @@ -1,87 +1,6 @@ +/// app.bsky types (manually entered) use serde_json::Value; -// =========== com.atproto types (manually entered) - -#[allow(non_snake_case)] -#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] -pub struct AccountRequest { - pub email: String, - pub handle: String, - pub password: String, - pub inviteCode: Option<String>, - pub recoveryKey: Option<String>, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] -pub struct SessionRequest { - pub handle: String, - pub password: String, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct AtpSession { - pub did: String, - pub name: String, - pub accessJwt: String, - pub refreshJwt: String, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct RepoDescribe { - pub name: String, - pub did: String, - pub didDoc: serde_json::Value, - pub collections: Vec<String>, - pub nameIsCorrect: bool, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct RepoCreateRecord { - pub did: String, - pub collection: String, - pub record: serde_json::Value, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct RepoPutRecord { - pub did: String, - pub collection: String, - pub rkey: String, - pub record: serde_json::Value, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct RepoDeleteRecord { - pub did: String, - pub collection: String, - pub rkey: String, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct RepoBatchWriteBody { - pub did: String, - pub writes: Vec<RepoBatchWrite>, -} - -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct RepoBatchWrite { - #[serde(rename = "type")] - pub op_type: String, - pub collection: String, - pub rkey: Option<String>, - pub value: serde_json::Value, -} - -// =========== app.bsky types (manually entered) - #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] pub struct Subject { pub uri: String, diff --git a/adenosine/src/auth.rs b/adenosine/src/auth.rs new file mode 100644 index 0000000..ff931ef --- /dev/null +++ b/adenosine/src/auth.rs @@ -0,0 +1,37 @@ +use anyhow::anyhow; +pub use anyhow::Result; +use serde_json::Value; + +/// Tries to parse a DID internal identifier from a JWT (as base64-encoded token) +pub fn parse_did_from_jwt(jwt: &str) -> Result<String> { + let second_b64 = jwt.split('.').nth(1).ok_or(anyhow!("couldn't parse JWT"))?; + let second_json: Vec<u8> = base64::decode_config(second_b64, base64::URL_SAFE)?; + let obj: Value = serde_json::from_slice(&second_json)?; + // trying to also support pulling "aud" as DID; not sure this is actually correct use of + // UCAN/JWT semantics? + let did = obj["sub"] + .as_str() + .or(obj["aud"].as_str()) + .ok_or(anyhow!("couldn't find DID subject in JWT"))? + .to_string(); + if !did.starts_with("did:") { + return Err(anyhow!("couldn't find DID subject in JWT")); + } + Ok(did) +} + +#[test] +fn test_parse_jwt() { + assert!(parse_did_from_jwt(".").is_err()); + // JWT from atproto ("sub") + assert_eq!( + parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOmV4M3NpNTI3Y2QyYW9nYnZpZGtvb296YyIsImlhdCI6MTY2NjgyOTM5M30.UvZgTqvaJICONa1wIUT1bny7u3hqVAqWhWy3qeuyZrE").unwrap(), + "did:plc:ex3si527cd2aogbvidkooozc", + ); + // UCAN from adenosine-pds ("aud") + assert_eq!( + parse_did_from_jwt("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6cGxjOnM3b25ieWphN2MzeXJzZ3Zob2xrbHM1YiIsImV4cCI6MTY3NTM4Mzg2NywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6RG5hZWRHVGJkb0Frb1NlOG96a3k1WHAzMjZTVFpUSm50aDlHY2dxaTZQYjNzYjczIiwibm5jIjoiTnZURDhENWZjNXFpalIyMWJ1V2Z1ZE02dzlBM2drSy1ac3RtUW03b21pdyIsInByZiI6W119.QwZkb9R17tNhXnY_roqFYgdiIgUnSC18FYWQb3PcH6BU1R5l4W_T4XdACyczPGfM-jAnF2r2loBXDntYVS6N5A").unwrap(), + "did:plc:s7onbyja7c3yrsgvholkls5b", + ); + assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err()); +} diff --git a/adenosine/src/com_atproto/mod.rs b/adenosine/src/com_atproto/mod.rs new file mode 100644 index 0000000..8e2317a --- /dev/null +++ b/adenosine/src/com_atproto/mod.rs @@ -0,0 +1,29 @@ +// com.atproto types (manually entered) + +pub mod repo; + +#[allow(non_snake_case)] +#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub struct AccountRequest { + pub email: String, + pub handle: String, + pub password: String, + pub inviteCode: Option<String>, + pub recoveryKey: Option<String>, +} + +#[allow(non_snake_case)] +#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub struct SessionRequest { + pub handle: String, + pub password: String, +} + +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct Session { + pub did: String, + pub name: String, + pub accessJwt: String, + pub refreshJwt: String, +} diff --git a/adenosine/src/com_atproto/repo/mod.rs b/adenosine/src/com_atproto/repo/mod.rs new file mode 100644 index 0000000..aa66e98 --- /dev/null +++ b/adenosine/src/com_atproto/repo/mod.rs @@ -0,0 +1,53 @@ +/// com.atproto.repo types (manually entered) + +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct Describe { + pub name: String, + pub did: String, + pub didDoc: serde_json::Value, + pub collections: Vec<String>, + pub nameIsCorrect: bool, +} + +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct CreateRecord { + pub did: String, + pub collection: String, + pub record: serde_json::Value, +} + +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct PutRecord { + pub did: String, + pub collection: String, + pub rkey: String, + pub record: serde_json::Value, +} + +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct DeleteRecord { + pub did: String, + pub collection: String, + pub rkey: String, +} + +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct BatchWriteBody { + pub did: String, + pub writes: Vec<BatchWrite>, +} + +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct BatchWrite { + #[serde(rename = "type")] + pub op_type: String, + pub collection: String, + pub rkey: Option<String>, + pub value: serde_json::Value, +} diff --git a/adenosine/src/ipld.rs b/adenosine/src/ipld.rs new file mode 100644 index 0000000..65a56e4 --- /dev/null +++ b/adenosine/src/ipld.rs @@ -0,0 +1,47 @@ +use libipld::{Cid, Ipld}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use std::str::FromStr; + +/// Intentionally serializing with this instead of DAG-JSON, because ATP schemas don't encode CID +/// links in any special way, they just pass the CID as a string. +pub fn ipld_into_json_value(val: Ipld) -> Value { + match val { + Ipld::Null => Value::Null, + Ipld::Bool(b) => Value::Bool(b), + Ipld::Integer(v) => json!(v), + Ipld::Float(v) => json!(v), + Ipld::String(s) => Value::String(s), + Ipld::Bytes(b) => Value::String(data_encoding::BASE64_NOPAD.encode(&b)), + Ipld::List(l) => Value::Array(l.into_iter().map(ipld_into_json_value).collect()), + Ipld::Map(m) => Value::Object(serde_json::Map::from_iter( + m.into_iter().map(|(k, v)| (k, ipld_into_json_value(v))), + )), + Ipld::Link(c) => Value::String(c.to_string()), + } +} + +/// Crude reverse generation +/// +/// Does not handle base64 to bytes, and the link generation is pretty simple (object elements with +/// key "car"). Numbers always come through as f64 (float). +pub fn json_value_into_ipld(val: Value) -> Ipld { + match val { + Value::Null => Ipld::Null, + Value::Bool(b) => Ipld::Bool(b), + Value::String(s) => Ipld::String(s), + // TODO: handle numbers better? + Value::Number(v) => Ipld::Float(v.as_f64().unwrap()), + Value::Array(l) => Ipld::List(l.into_iter().map(json_value_into_ipld).collect()), + Value::Object(m) => { + let map: BTreeMap<String, Ipld> = BTreeMap::from_iter(m.into_iter().map(|(k, v)| { + if k == "car" && v.is_string() { + (k, Ipld::Link(Cid::from_str(v.as_str().unwrap()).unwrap())) + } else { + (k, json_value_into_ipld(v)) + } + })); + Ipld::Map(map) + } + } +} diff --git a/adenosine/src/lib.rs b/adenosine/src/lib.rs index dc4a1b9..49701a1 100644 --- a/adenosine/src/lib.rs +++ b/adenosine/src/lib.rs @@ -1,232 +1,17 @@ -use anyhow::anyhow; -pub use anyhow::Result; -use reqwest::header; -use serde_json::Value; -use std::collections::HashMap; -use std::str::FromStr; -use std::time::Duration; - +pub mod app_bsky; +pub mod auth; pub mod car; +pub mod com_atproto; pub mod crypto; -pub mod did; pub mod identifiers; +pub mod ipld; pub mod mst; +pub mod plc; pub mod repo; -pub mod ucan_p256; -pub mod vendored; -use identifiers::Nsid; - -static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum XrpcMethod { - Get, - Post, -} - -impl FromStr for XrpcMethod { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "get" => Ok(XrpcMethod::Get), - "post" => Ok(XrpcMethod::Post), - _ => Err(anyhow!("unknown method: {}", s)), - } - } -} - -pub struct XrpcClient { - http_client: reqwest::blocking::Client, - host: String, -} - -impl XrpcClient { - pub fn new(host: String, auth_token: Option<String>) -> Result<Self> { - let mut headers = header::HeaderMap::new(); - if let Some(token) = &auth_token { - let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {}", token))?; - auth_value.set_sensitive(true); - headers.insert(header::AUTHORIZATION, auth_value); - }; - - let http_client = reqwest::blocking::Client::builder() - .default_headers(headers) - .user_agent(APP_USER_AGENT) - .timeout(Duration::from_secs(30)) - //.danger_accept_invalid_certs(true) - .build() - .expect("ERROR :: Could not build reqwest client"); +pub mod xrpc; - Ok(XrpcClient { http_client, host }) - } - - pub fn get( - &self, - nsid: &Nsid, - params: Option<HashMap<String, String>>, - ) -> Result<Option<Value>> { - log::debug!("XRPC GET endpoint={} params={:?}", nsid, params); - let params: HashMap<String, String> = params.unwrap_or_default(); - let res = self - .http_client - .get(format!("{}/xrpc/{}", self.host, nsid)) - .query(¶ms) - .send()?; - // TODO: refactor this error handling stuff into single method - if res.status() == 400 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Bad Request (400): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } else if res.status() == 500 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Internal Error (500): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } - let res = res.error_for_status()?; - Ok(res.json()?) - } - - pub fn get_to_writer<W: std::io::Write>( - &self, - nsid: &Nsid, - params: Option<HashMap<String, String>>, - output: &mut W, - ) -> Result<u64> { - let params: HashMap<String, String> = params.unwrap_or_default(); - let res = self - .http_client - .get(format!("{}/xrpc/{}", self.host, nsid)) - .query(¶ms) - .send()?; - if res.status() == 400 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Bad Request (400): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } else if res.status() == 500 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Internal Error (500): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } - let mut res = res.error_for_status()?; - Ok(res.copy_to(output)?) - } - - pub fn post( - &self, - nsid: &Nsid, - params: Option<HashMap<String, String>>, - body: Option<Value>, - ) -> Result<Option<Value>> { - let params: HashMap<String, String> = params.unwrap_or_default(); - log::debug!( - "XRPC POST endpoint={} params={:?} body={:?}", - nsid, - params, - body - ); - let mut req = self - .http_client - .post(format!("{}/xrpc/{}", self.host, nsid)) - .query(¶ms); - req = if let Some(b) = body { - req.json(&b) - } else { - req - }; - let res = req.send()?; - if res.status() == 400 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Bad Request (400): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } else if res.status() == 500 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Internal Error (500): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } - let res = res.error_for_status()?; - if res.content_length() == Some(0) { - Ok(None) - } else { - Ok(res.json()?) - } - } - - pub fn post_cbor_from_reader<R: std::io::Read>( - &self, - nsid: &Nsid, - params: Option<HashMap<String, String>>, - input: &mut R, - ) -> Result<Option<Value>> { - let params: HashMap<String, String> = params.unwrap_or_default(); - let mut buf: Vec<u8> = Vec::new(); - input.read_to_end(&mut buf)?; - let res = self - .http_client - .post(format!("{}/xrpc/{}", self.host, nsid)) - .query(¶ms) - .header(reqwest::header::CONTENT_TYPE, "application/cbor") - .body(buf) - .send()?; - if res.status() == 400 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Bad Request: {}", - val["message"].as_str().unwrap_or("unknown") - )); - } - let res = res.error_for_status()?; - Ok(res.json()?) - } - - // reqwest::blocking::Body -} - -/// Tries to parse a DID internal identifier from a JWT (as base64-encoded token) -pub fn parse_did_from_jwt(jwt: &str) -> Result<String> { - let second_b64 = jwt.split('.').nth(1).ok_or(anyhow!("couldn't parse JWT"))?; - let second_json: Vec<u8> = base64::decode_config(second_b64, base64::URL_SAFE)?; - let obj: Value = serde_json::from_slice(&second_json)?; - // trying to also support pulling "aud" as DID; not sure this is actually correct use of - // UCAN/JWT semantics? - let did = obj["sub"] - .as_str() - .or(obj["aud"].as_str()) - .ok_or(anyhow!("couldn't find DID subject in JWT"))? - .to_string(); - if !did.starts_with("did:") { - return Err(anyhow!("couldn't find DID subject in JWT")); - } - Ok(did) -} - -#[test] -fn test_parse_jwt() { - assert!(parse_did_from_jwt(".").is_err()); - // JWT from atproto ("sub") - assert_eq!( - parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOmV4M3NpNTI3Y2QyYW9nYnZpZGtvb296YyIsImlhdCI6MTY2NjgyOTM5M30.UvZgTqvaJICONa1wIUT1bny7u3hqVAqWhWy3qeuyZrE").unwrap(), - "did:plc:ex3si527cd2aogbvidkooozc", - ); - // UCAN from adenosine-pds ("aud") - assert_eq!( - parse_did_from_jwt("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6cGxjOnM3b25ieWphN2MzeXJzZ3Zob2xrbHM1YiIsImV4cCI6MTY3NTM4Mzg2NywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6RG5hZWRHVGJkb0Frb1NlOG96a3k1WHAzMjZTVFpUSm50aDlHY2dxaTZQYjNzYjczIiwibm5jIjoiTnZURDhENWZjNXFpalIyMWJ1V2Z1ZE02dzlBM2drSy1ac3RtUW03b21pdyIsInByZiI6W119.QwZkb9R17tNhXnY_roqFYgdiIgUnSC18FYWQb3PcH6BU1R5l4W_T4XdACyczPGfM-jAnF2r2loBXDntYVS6N5A").unwrap(), - "did:plc:s7onbyja7c3yrsgvholkls5b", - ); - assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err()); -} +mod ucan_p256; +mod vendored; /// Helper to generate the current timestamp as right now, UTC, formatted as an RFC 3339 string. /// diff --git a/adenosine/src/did.rs b/adenosine/src/plc.rs index c7d7d10..c7d7d10 100644 --- a/adenosine/src/did.rs +++ b/adenosine/src/plc.rs diff --git a/adenosine/src/xrpc.rs b/adenosine/src/xrpc.rs new file mode 100644 index 0000000..382c7fb --- /dev/null +++ b/adenosine/src/xrpc.rs @@ -0,0 +1,186 @@ +use crate::identifiers::Nsid; +use anyhow::anyhow; +pub use anyhow::Result; +use reqwest::header; +use serde_json::Value; +use std::collections::HashMap; +use std::str::FromStr; +use std::time::Duration; + +static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum XrpcMethod { + Get, + Post, +} + +impl FromStr for XrpcMethod { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "get" => Ok(XrpcMethod::Get), + "post" => Ok(XrpcMethod::Post), + _ => Err(anyhow!("unknown method: {}", s)), + } + } +} + +pub struct XrpcClient { + http_client: reqwest::blocking::Client, + host: String, +} + +impl XrpcClient { + pub fn new(host: String, auth_token: Option<String>) -> Result<Self> { + let mut headers = header::HeaderMap::new(); + if let Some(token) = &auth_token { + let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {}", token))?; + auth_value.set_sensitive(true); + headers.insert(header::AUTHORIZATION, auth_value); + }; + + let http_client = reqwest::blocking::Client::builder() + .default_headers(headers) + .user_agent(APP_USER_AGENT) + .timeout(Duration::from_secs(30)) + //.danger_accept_invalid_certs(true) + .build() + .expect("ERROR :: Could not build reqwest client"); + + Ok(XrpcClient { http_client, host }) + } + + pub fn get( + &self, + nsid: &Nsid, + params: Option<HashMap<String, String>>, + ) -> Result<Option<Value>> { + log::debug!("XRPC GET endpoint={} params={:?}", nsid, params); + let params: HashMap<String, String> = params.unwrap_or_default(); + let res = self + .http_client + .get(format!("{}/xrpc/{}", self.host, nsid)) + .query(¶ms) + .send()?; + // TODO: refactor this error handling stuff into single method + if res.status() == 400 { + let val: Value = res.json()?; + return Err(anyhow!( + "XRPC Bad Request (400): {}", + val["message"].as_str().unwrap_or("unknown") + )); + } else if res.status() == 500 { + let val: Value = res.json()?; + return Err(anyhow!( + "XRPC Internal Error (500): {}", + val["message"].as_str().unwrap_or("unknown") + )); + } + let res = res.error_for_status()?; + Ok(res.json()?) + } + + pub fn get_to_writer<W: std::io::Write>( + &self, + nsid: &Nsid, + params: Option<HashMap<String, String>>, + output: &mut W, + ) -> Result<u64> { + let params: HashMap<String, String> = params.unwrap_or_default(); + let res = self + .http_client + .get(format!("{}/xrpc/{}", self.host, nsid)) + .query(¶ms) + .send()?; + if res.status() == 400 { + let val: Value = res.json()?; + return Err(anyhow!( + "XRPC Bad Request (400): {}", + val["message"].as_str().unwrap_or("unknown") + )); + } else if res.status() == 500 { + let val: Value = res.json()?; + return Err(anyhow!( + "XRPC Internal Error (500): {}", + val["message"].as_str().unwrap_or("unknown") + )); + } + let mut res = res.error_for_status()?; + Ok(res.copy_to(output)?) + } + + pub fn post( + &self, + nsid: &Nsid, + params: Option<HashMap<String, String>>, + body: Option<Value>, + ) -> Result<Option<Value>> { + let params: HashMap<String, String> = params.unwrap_or_default(); + log::debug!( + "XRPC POST endpoint={} params={:?} body={:?}", + nsid, + params, + body + ); + let mut req = self + .http_client + .post(format!("{}/xrpc/{}", self.host, nsid)) + .query(¶ms); + req = if let Some(b) = body { + req.json(&b) + } else { + req + }; + let res = req.send()?; + if res.status() == 400 { + let val: Value = res.json()?; + return Err(anyhow!( + "XRPC Bad Request (400): {}", + val["message"].as_str().unwrap_or("unknown") + )); + } else if res.status() == 500 { + let val: Value = res.json()?; + return Err(anyhow!( + "XRPC Internal Error (500): {}", + val["message"].as_str().unwrap_or("unknown") + )); + } + let res = res.error_for_status()?; + if res.content_length() == Some(0) { + Ok(None) + } else { + Ok(res.json()?) + } + } + + pub fn post_cbor_from_reader<R: std::io::Read>( + &self, + nsid: &Nsid, + params: Option<HashMap<String, String>>, + input: &mut R, + ) -> Result<Option<Value>> { + let params: HashMap<String, String> = params.unwrap_or_default(); + let mut buf: Vec<u8> = Vec::new(); + input.read_to_end(&mut buf)?; + let res = self + .http_client + .post(format!("{}/xrpc/{}", self.host, nsid)) + .query(¶ms) + .header(reqwest::header::CONTENT_TYPE, "application/cbor") + .body(buf) + .send()?; + if res.status() == 400 { + let val: Value = res.json()?; + return Err(anyhow!( + "XRPC Bad Request: {}", + val["message"].as_str().unwrap_or("unknown") + )); + } + let res = res.error_for_status()?; + Ok(res.json()?) + } + + // reqwest::blocking::Body +} diff --git a/adenosine-pds/tests/bigger.car b/adenosine/tests/bigger.car Binary files differindex 7169013..7169013 100644 --- a/adenosine-pds/tests/bigger.car +++ b/adenosine/tests/bigger.car diff --git a/adenosine-pds/tests/example_repo.car b/adenosine/tests/example_repo.car Binary files differindex b2ae723..b2ae723 100644 --- a/adenosine-pds/tests/example_repo.car +++ b/adenosine/tests/example_repo.car diff --git a/adenosine-pds/tests/test_mst_interop.rs b/adenosine/tests/test_mst_interop.rs index 8a5becc..ee45019 100644 --- a/adenosine-pds/tests/test_mst_interop.rs +++ b/adenosine/tests/test_mst_interop.rs @@ -1,4 +1,4 @@ -use adenosine_pds::RepoStore; +use adenosine::repo::RepoStore; use libipld::Cid; use std::collections::BTreeMap; use std::str::FromStr; @@ -65,7 +65,7 @@ fn test_tricky_map() { fn test_trims_top() { // "trims top of tree on delete" - use adenosine_pds::mst::print_mst_keys; + use adenosine::mst::print_mst_keys; let mut repo = RepoStore::open_ephemeral().unwrap(); let cid1 = Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap(); @@ -126,7 +126,7 @@ fn test_insertion() { fn test_higher_layers() { // "handles new layers that are two higher than existing" - use adenosine_pds::mst::print_mst_keys; + use adenosine::mst::print_mst_keys; let mut repo = RepoStore::open_ephemeral().unwrap(); let cid1 = Cid::from_str("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454").unwrap(); diff --git a/adenosine-pds/tests/test_repro_mst.rs b/adenosine/tests/test_repro_mst.rs index df88559..692c41b 100644 --- a/adenosine-pds/tests/test_repro_mst.rs +++ b/adenosine/tests/test_repro_mst.rs @@ -1,4 +1,4 @@ -use adenosine_pds::RepoStore; +use adenosine::repo::RepoStore; use std::path::PathBuf; use std::str::FromStr; |