this repo has no description

implement GET /keys and PUT /keys

Akshay e5a13e80 53f1f29c

Changed files
+327 -101
core
+108 -3
Cargo.lock
··· 18 18 checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 19 20 20 [[package]] 21 + name = "ahash" 22 + version = "0.8.11" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 + dependencies = [ 26 + "cfg-if", 27 + "once_cell", 28 + "version_check", 29 + "zerocopy", 30 + ] 31 + 32 + [[package]] 21 33 name = "aho-corasick" 22 34 version = "1.1.3" 23 35 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 152 164 "atrium-common", 153 165 "atrium-identity", 154 166 "atrium-xrpc", 155 - "base64", 167 + "base64 0.22.1", 156 168 "chrono", 157 169 "ecdsa", 158 170 "elliptic-curve", ··· 279 291 280 292 [[package]] 281 293 name = "base64" 294 + version = "0.21.7" 295 + source = "registry+https://github.com/rust-lang/crates.io-index" 296 + checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 297 + 298 + [[package]] 299 + name = "base64" 282 300 version = "0.22.1" 283 301 source = "registry+https://github.com/rust-lang/crates.io-index" 284 302 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ··· 409 427 version = "0.1.0" 410 428 dependencies = [ 411 429 "anyhow", 430 + "async-trait", 412 431 "atrium-api", 413 432 "atrium-common", 414 433 "atrium-identity", ··· 417 436 "atrium-xrpc-client", 418 437 "axum", 419 438 "hickory-resolver", 439 + "openssh-keys", 420 440 "serde", 441 + "serde_json", 421 442 "tokio", 443 + "tokio-rusqlite", 422 444 "tower-sessions", 423 445 ] 424 446 ··· 721 743 ] 722 744 723 745 [[package]] 746 + name = "fallible-iterator" 747 + version = "0.3.0" 748 + source = "registry+https://github.com/rust-lang/crates.io-index" 749 + checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 750 + 751 + [[package]] 752 + name = "fallible-streaming-iterator" 753 + version = "0.1.9" 754 + source = "registry+https://github.com/rust-lang/crates.io-index" 755 + checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 756 + 757 + [[package]] 724 758 name = "fastrand" 725 759 version = "1.9.0" 726 760 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 937 971 version = "0.14.5" 938 972 source = "registry+https://github.com/rust-lang/crates.io-index" 939 973 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 974 + dependencies = [ 975 + "ahash", 976 + ] 940 977 941 978 [[package]] 942 979 name = "hashbrown" ··· 947 984 "allocator-api2", 948 985 "equivalent", 949 986 "foldhash", 987 + ] 988 + 989 + [[package]] 990 + name = "hashlink" 991 + version = "0.9.1" 992 + source = "registry+https://github.com/rust-lang/crates.io-index" 993 + checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" 994 + dependencies = [ 995 + "hashbrown 0.14.5", 950 996 ] 951 997 952 998 [[package]] ··· 1451 1497 ] 1452 1498 1453 1499 [[package]] 1500 + name = "libsqlite3-sys" 1501 + version = "0.30.1" 1502 + source = "registry+https://github.com/rust-lang/crates.io-index" 1503 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 1504 + dependencies = [ 1505 + "cc", 1506 + "pkg-config", 1507 + "vcpkg", 1508 + ] 1509 + 1510 + [[package]] 1454 1511 name = "libz-sys" 1455 1512 version = "1.1.21" 1456 1513 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1548 1605 version = "0.8.4" 1549 1606 source = "registry+https://github.com/rust-lang/crates.io-index" 1550 1607 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 1608 + 1609 + [[package]] 1610 + name = "md-5" 1611 + version = "0.10.6" 1612 + source = "registry+https://github.com/rust-lang/crates.io-index" 1613 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 1614 + dependencies = [ 1615 + "cfg-if", 1616 + "digest", 1617 + ] 1551 1618 1552 1619 [[package]] 1553 1620 name = "memchr" ··· 1681 1748 version = "1.20.2" 1682 1749 source = "registry+https://github.com/rust-lang/crates.io-index" 1683 1750 checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 1751 + 1752 + [[package]] 1753 + name = "openssh-keys" 1754 + version = "0.6.4" 1755 + source = "registry+https://github.com/rust-lang/crates.io-index" 1756 + checksum = "abb830a82898b2ac17c9620ddce839ac3b34b9cb8a1a037cbdbfb9841c756c3e" 1757 + dependencies = [ 1758 + "base64 0.21.7", 1759 + "byteorder", 1760 + "md-5", 1761 + "sha2", 1762 + "thiserror 1.0.69", 1763 + ] 1684 1764 1685 1765 [[package]] 1686 1766 name = "openssl" ··· 1977 2057 checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" 1978 2058 dependencies = [ 1979 2059 "async-compression", 1980 - "base64", 2060 + "base64 0.22.1", 1981 2061 "bytes", 1982 2062 "futures-core", 1983 2063 "futures-util", ··· 2033 2113 ] 2034 2114 2035 2115 [[package]] 2116 + name = "rusqlite" 2117 + version = "0.32.1" 2118 + source = "registry+https://github.com/rust-lang/crates.io-index" 2119 + checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" 2120 + dependencies = [ 2121 + "bitflags 2.8.0", 2122 + "fallible-iterator", 2123 + "fallible-streaming-iterator", 2124 + "hashlink", 2125 + "libsqlite3-sys", 2126 + "smallvec", 2127 + ] 2128 + 2129 + [[package]] 2036 2130 name = "rustc-demangle" 2037 2131 version = "0.1.24" 2038 2132 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2516 2610 ] 2517 2611 2518 2612 [[package]] 2613 + name = "tokio-rusqlite" 2614 + version = "0.6.0" 2615 + source = "registry+https://github.com/rust-lang/crates.io-index" 2616 + checksum = "b65501378eb676f400c57991f42cbd0986827ab5c5200c53f206d710fb32a945" 2617 + dependencies = [ 2618 + "crossbeam-channel", 2619 + "rusqlite", 2620 + "tokio", 2621 + ] 2622 + 2623 + [[package]] 2519 2624 name = "tokio-util" 2520 2625 version = "0.7.13" 2521 2626 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2598 2703 dependencies = [ 2599 2704 "async-trait", 2600 2705 "axum-core", 2601 - "base64", 2706 + "base64 0.22.1", 2602 2707 "futures", 2603 2708 "http 1.2.0", 2604 2709 "parking_lot",
+4
core/Cargo.toml
··· 16 16 anyhow = "1.0.95" 17 17 tower-sessions = "0.14.0" 18 18 serde = "1.0.217" 19 + tokio-rusqlite = { version = "0.6.0", features = ["bundled"] } 20 + async-trait = "0.1.85" 21 + serde_json = "1.0.137" 22 + openssh-keys = "0.6.4" 19 23
+31
core/src/db.rs
··· 1 + use tokio_rusqlite::Connection; 2 + 3 + pub struct Db { 4 + pub conn: Connection, 5 + } 6 + 7 + impl Db { 8 + pub fn new(conn: Connection) -> Self { 9 + Self { conn } 10 + } 11 + 12 + pub async fn setup(&self) { 13 + self.conn 14 + .call(|c| { 15 + c.execute( 16 + " 17 + CREATE TABLE IF NOT EXISTS keys ( 18 + id INTEGER PRIMARY KEY AUTOINCREMENT, 19 + did TEXT NOT NULL, 20 + key TEXT NOT NULL, 21 + UNIQUE(did, key) 22 + ) 23 + ", 24 + [], 25 + )?; 26 + Ok(()) 27 + }) 28 + .await 29 + .expect("failed to create keys table"); 30 + } 31 + }
+30
core/src/dns.rs
··· 1 + use atrium_identity::handle::DnsTxtResolver; 2 + use hickory_resolver::TokioAsyncResolver; 3 + 4 + pub struct Resolver { 5 + resolver: TokioAsyncResolver, 6 + } 7 + 8 + impl Default for Resolver { 9 + fn default() -> Self { 10 + Self { 11 + resolver: TokioAsyncResolver::tokio_from_system_conf() 12 + .expect("failed to create resolver"), 13 + } 14 + } 15 + } 16 + 17 + impl DnsTxtResolver for Resolver { 18 + async fn resolve( 19 + &self, 20 + query: &str, 21 + ) -> core::result::Result<Vec<String>, Box<dyn std::error::Error + Send + Sync + 'static>> { 22 + Ok(self 23 + .resolver 24 + .txt_lookup(query) 25 + .await? 26 + .iter() 27 + .map(|txt| txt.to_string()) 28 + .collect()) 29 + } 30 + }
+19 -98
core/src/main.rs
··· 1 1 use std::sync::Arc; 2 2 3 - use atrium_api::{ 4 - agent::{AtpAgent, store::MemorySessionStore}, 5 - did_doc::DidDocument, 6 - types::string::AtIdentifier, 7 - }; 3 + use atrium_api::{did_doc::DidDocument, types::string::AtIdentifier}; 8 4 use atrium_common::resolver::Resolver; 9 5 use atrium_identity::{ 10 6 did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}, 11 - handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver}, 7 + handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig}, 12 8 }; 13 9 use atrium_oauth_client::DefaultHttpClient; 14 10 use atrium_xrpc::HttpClient; 15 - use atrium_xrpc_client::isahc::IsahcClient; 16 - use axum::{Router, routing}; 17 - use hickory_resolver::TokioAsyncResolver; 11 + use axum::{Extension, Router, routing}; 12 + use tokio_rusqlite::Connection; 13 + 14 + mod db; 15 + mod dns; 16 + mod routes; 17 + 18 + use db::Db; 19 + use routes::{index, keys, login}; 18 20 19 21 #[tokio::main] 20 22 async fn main() { ··· 22 24 let session_layer = tower_sessions::SessionManagerLayer::new(session_store) 23 25 .with_secure(false) 24 26 .with_expiry(tower_sessions::Expiry::OnSessionEnd); 27 + let db = Db::new(Connection::open("bild.db").await.unwrap()); 28 + db.setup().await; 25 29 26 30 let app_state = AppState::new(); 27 31 let service = Router::new() 28 32 .route("/", routing::get(index::get)) 29 - // .route("/test-auth", routing::get(test_atproto::get)) 30 33 .route("/login", routing::get(login::get).post(login::post)) 31 - // .route("/callback", routing::get(callback::get)) 34 + .route("/keys", routing::get(keys::get).put(keys::put)) 32 35 .layer(session_layer) 33 - .with_state(app_state); 36 + .with_state(app_state) 37 + .layer(Extension(Arc::new(db))); 34 38 35 39 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 36 40 axum::serve(listener, service).await.unwrap(); ··· 39 43 #[derive(Clone)] 40 44 struct AppState { 41 45 did_resolver: Arc<CommonDidResolver<DefaultHttpClient>>, 42 - handle_resolver: Arc<AtprotoHandleResolver<HickoryDnsTxtResolver, DefaultHttpClient>>, 46 + handle_resolver: Arc<AtprotoHandleResolver<dns::Resolver, DefaultHttpClient>>, 43 47 } 44 48 45 49 impl AppState { ··· 72 76 }) 73 77 } 74 78 75 - fn handle_resolver<H: HttpClient>( 76 - http_client: Arc<H>, 77 - ) -> AtprotoHandleResolver<HickoryDnsTxtResolver, H> { 79 + fn handle_resolver<H: HttpClient>(http_client: Arc<H>) -> AtprotoHandleResolver<dns::Resolver, H> { 78 80 AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 79 - dns_txt_resolver: HickoryDnsTxtResolver::default(), 81 + dns_txt_resolver: dns::Resolver::default(), 80 82 http_client, 81 83 }) 82 84 } 83 - 84 - mod login { 85 - use axum::{extract::Form, http::StatusCode, response::IntoResponse}; 86 - use serde::Deserialize; 87 - 88 - use super::*; 89 - 90 - pub async fn get() -> String { 91 - "hello world".to_owned() 92 - } 93 - 94 - #[derive(Deserialize, Debug)] 95 - pub struct Req { 96 - handle: AtIdentifier, 97 - app_password: String, 98 - } 99 - 100 - pub async fn post(session: tower_sessions::Session, Form(req): Form<Req>) -> impl IntoResponse { 101 - let agent = AtpAgent::new( 102 - IsahcClient::new("https://dummy.example"), 103 - MemorySessionStore::default(), 104 - ); 105 - let res = agent.login(req.handle, req.app_password).await.unwrap(); 106 - println!("logged in as {:?} ({:?})", res.handle, res.did); 107 - session.insert("at_session", res).await.unwrap(); 108 - println!("stored session"); 109 - StatusCode::OK 110 - } 111 - } 112 - 113 - // dummy endpoint to test if sessions are working 114 - mod index { 115 - use super::*; 116 - 117 - pub async fn get(session: tower_sessions::Session) -> &'static str { 118 - match session 119 - .get::<atrium_api::agent::Session>("at_session") 120 - .await 121 - .unwrap() 122 - { 123 - None => "no session", 124 - Some(s) => { 125 - let agent = AtpAgent::new( 126 - IsahcClient::new("https://dummy.example"), 127 - MemorySessionStore::default(), 128 - ); 129 - println!("resuming session of {:?} ({:?})", s.handle, s.did); 130 - agent.resume_session(s).await.unwrap(); 131 - "resuming session" 132 - } 133 - } 134 - } 135 - } 136 - 137 - struct HickoryDnsTxtResolver { 138 - resolver: TokioAsyncResolver, 139 - } 140 - 141 - impl Default for HickoryDnsTxtResolver { 142 - fn default() -> Self { 143 - Self { 144 - resolver: TokioAsyncResolver::tokio_from_system_conf() 145 - .expect("failed to create resolver"), 146 - } 147 - } 148 - } 149 - 150 - impl DnsTxtResolver for HickoryDnsTxtResolver { 151 - async fn resolve( 152 - &self, 153 - query: &str, 154 - ) -> core::result::Result<Vec<String>, Box<dyn std::error::Error + Send + Sync + 'static>> { 155 - Ok(self 156 - .resolver 157 - .txt_lookup(query) 158 - .await? 159 - .iter() 160 - .map(|txt| txt.to_string()) 161 - .collect()) 162 - } 163 - }
+135
core/src/routes.rs
··· 1 + pub mod login { 2 + use atrium_api::{ 3 + agent::{AtpAgent, store::MemorySessionStore}, 4 + types::string::AtIdentifier, 5 + }; 6 + use atrium_xrpc_client::isahc::IsahcClient; 7 + use axum::{extract::Form, http::StatusCode, response::IntoResponse}; 8 + use serde::Deserialize; 9 + 10 + pub async fn get() -> String { 11 + "hello world".to_owned() 12 + } 13 + 14 + #[derive(Deserialize, Debug)] 15 + pub struct Req { 16 + handle: AtIdentifier, 17 + app_password: String, 18 + } 19 + 20 + pub async fn post(session: tower_sessions::Session, Form(req): Form<Req>) -> impl IntoResponse { 21 + let agent = AtpAgent::new( 22 + IsahcClient::new("https://dummy.example"), 23 + MemorySessionStore::default(), 24 + ); 25 + let res = agent.login(req.handle, req.app_password).await.unwrap(); 26 + println!("logged in as {:?} ({:?})", res.handle, res.did); 27 + session.insert("bild_session", res).await.unwrap(); 28 + println!("stored session"); 29 + StatusCode::OK 30 + } 31 + } 32 + 33 + pub mod index { 34 + use atrium_api::agent::{AtpAgent, store::MemorySessionStore}; 35 + use atrium_xrpc_client::isahc::IsahcClient; 36 + 37 + pub async fn get(session: tower_sessions::Session) -> &'static str { 38 + match session 39 + .get::<atrium_api::agent::Session>("bild_session") 40 + .await 41 + .unwrap() 42 + { 43 + None => "no session", 44 + Some(s) => { 45 + let agent = AtpAgent::new( 46 + IsahcClient::new("https://dummy.example"), 47 + MemorySessionStore::default(), 48 + ); 49 + println!("resuming session of {:?} ({:?})", s.handle, s.did); 50 + agent.resume_session(s).await.unwrap(); 51 + "resuming session" 52 + } 53 + } 54 + } 55 + } 56 + 57 + pub mod keys { 58 + use crate::{AppState, db}; 59 + use atrium_api::types::string::AtIdentifier; 60 + use axum::http::StatusCode; 61 + use axum::{ 62 + extract::{Extension, Json, Query, State}, 63 + response::IntoResponse, 64 + }; 65 + use serde::{Deserialize, Serialize}; 66 + use std::sync::Arc; 67 + 68 + #[derive(Deserialize)] 69 + pub struct GetReq { 70 + id: AtIdentifier, 71 + } 72 + 73 + #[derive(Serialize)] 74 + pub struct GetRes { 75 + id: AtIdentifier, 76 + keys: Vec<String>, 77 + } 78 + 79 + pub async fn get( 80 + Extension(db): Extension<Arc<db::Db>>, 81 + State(state): State<AppState>, 82 + Query(req): Query<GetReq>, 83 + ) -> impl IntoResponse { 84 + let did_doc = state.resolve_did_document(&req.id).await.unwrap(); 85 + let keys = db 86 + .conn 87 + .call(|c| { 88 + let mut stmt = c.prepare("SELECT key FROM keys WHERE did = ?1")?; 89 + let keys = stmt 90 + .query_map([did_doc.id], |row| Ok(row.get::<usize, String>(0)?))? 91 + .filter_map(|r| r.ok()) 92 + .collect::<Vec<_>>(); 93 + Ok(keys) 94 + }) 95 + .await 96 + .unwrap(); 97 + Json(GetRes { 98 + id: req.id.clone(), 99 + keys, 100 + }) 101 + } 102 + 103 + #[derive(Deserialize)] 104 + pub struct PutReq { 105 + key: String, 106 + } 107 + 108 + pub async fn put( 109 + Extension(db): Extension<Arc<db::Db>>, 110 + session: tower_sessions::Session, 111 + Json(req): Json<PutReq>, 112 + ) -> impl IntoResponse { 113 + if openssh_keys::PublicKey::parse(&req.key).is_err() { 114 + return StatusCode::BAD_REQUEST; 115 + } 116 + match session 117 + .get::<atrium_api::agent::Session>("bild_session") 118 + .await 119 + .unwrap() 120 + { 121 + None => StatusCode::UNAUTHORIZED, 122 + Some(sess) => { 123 + let did = sess.did.clone().into(); 124 + db.conn 125 + .call(|c| { 126 + c.execute("INSERT INTO keys VALUES (?1, ?2)", [did, req.key])?; 127 + Ok(()) 128 + }) 129 + .await 130 + .unwrap(); 131 + StatusCode::OK 132 + } 133 + } 134 + } 135 + }