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)] 23#[serde(rename_all = "camelCase")] 24struct DidService { 25 id: String, 26 r#type: String, 27 service_endpoint: String, 28} 29 30#[handler] 31fn did_doc(TypedHeader(host): TypedHeader<Host>, service: Data<&DidService>) -> Json<DidDoc> { 32 Json(DidDoc { 33 id: format!("did:web:{}", host.hostname()), 34 service: [service.clone()], 35 }) 36} 37 38#[derive(Deserialize)] 39struct AskQuery { 40 domain: String, 41} 42#[handler] 43fn ask_caddy( 44 Data(parent): Data<&Option<String>>, 45 Query(AskQuery { domain }): Query<AskQuery>, 46) -> Response { 47 if let Some(parent) = parent 48 && let Some(prefix) = domain.strip_suffix(&format!(".{parent}")) 49 && !prefix.contains('.') 50 { 51 // no sub-sub-domains allowed 52 return Response::builder().body("ok"); 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}