forked from
microcosm.blue/microcosm-rs
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}