Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
1use clap::Parser; 2use poem::{ 3 EndpointExt, Response, Route, Server, get, handler, 4 http::StatusCode, 5 listener::TcpListener, 6 middleware::{AddData, Tracing}, 7 web::{Data, Json, Query, TypedHeader, headers::Host}, 8}; 9use serde::{Deserialize, Serialize}; 10 11#[handler] 12fn hello() -> String { 13 "ɹoʇɔǝʅⅎǝɹ".to_string() 14} 15 16#[derive(Debug, Serialize)] 17struct DidDoc { 18 id: String, 19 service: [DidService; 1], 20} 21 22#[derive(Debug, Clone, Serialize)] 23struct DidService { 24 id: String, 25 r#type: String, 26 service_endpoint: String, 27} 28 29#[handler] 30fn did_doc(TypedHeader(host): TypedHeader<Host>, service: Data<&DidService>) -> Json<DidDoc> { 31 Json(DidDoc { 32 id: format!("did:web:{}", host.hostname()), 33 service: [service.clone()], 34 }) 35} 36 37#[derive(Deserialize)] 38struct AskQuery { 39 domain: String, 40} 41#[handler] 42fn ask_caddy( 43 Data(parent): Data<&Option<String>>, 44 Query(AskQuery { domain }): Query<AskQuery>, 45) -> Response { 46 if let Some(parent) = parent { 47 if let Some(prefix) = domain.strip_suffix(&format!(".{parent}")) { 48 if !prefix.contains('.') { 49 // no sub-sub-domains allowed 50 return Response::builder().body("ok"); 51 } 52 } 53 }; 54 Response::builder() 55 .status(StatusCode::FORBIDDEN) 56 .body("nope") 57} 58 59/// Slingshot record edge cache 60#[derive(Parser, Debug, Clone)] 61#[command(version, about, long_about = None)] 62struct Args { 63 /// The DID document service ID to serve 64 /// 65 /// must start with a '#', like `#bsky_appview' 66 #[arg(long)] 67 id: String, 68 /// Service type 69 /// 70 /// Not sure exactly what its requirements are. 'BlueskyAppview' for example 71 #[arg(long)] 72 r#type: String, 73 /// The HTTPS endpoint for the service 74 #[arg(long)] 75 service_endpoint: String, 76 /// The parent domain; requests should come from subdomains of this 77 #[arg(long)] 78 domain: Option<String>, 79} 80 81impl From<Args> for DidService { 82 fn from(a: Args) -> Self { 83 Self { 84 id: a.id, 85 r#type: a.r#type, 86 service_endpoint: a.service_endpoint, 87 } 88 } 89} 90 91#[tokio::main(flavor = "current_thread")] 92async fn main() { 93 tracing_subscriber::fmt::init(); 94 log::info!("ɹoʇɔǝʅⅎǝɹ"); 95 96 let args = Args::parse(); 97 let domain = args.domain.clone(); 98 let service: DidService = args.into(); 99 100 Server::new(TcpListener::bind("0.0.0.0:3001")) 101 .run( 102 Route::new() 103 .at("/", get(hello)) 104 .at("/.well-known/did.json", get(did_doc)) 105 .at("/ask", get(ask_caddy)) 106 .with(AddData::new(service)) 107 .with(AddData::new(domain)) 108 .with(Tracing), 109 ) 110 .await 111 .unwrap() 112}