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 if let Some(prefix) = domain.strip_suffix(&format!(".{parent}")) {
49 if !prefix.contains('.') {
50 // no sub-sub-domains allowed
51 return Response::builder().body("ok");
52 }
53 }
54 };
55 Response::builder()
56 .status(StatusCode::FORBIDDEN)
57 .body("nope")
58}
59
60/// Slingshot record edge cache
61#[derive(Parser, Debug, Clone)]
62#[command(version, about, long_about = None)]
63struct Args {
64 /// The DID document service ID to serve
65 ///
66 /// must start with a '#', like `#bsky_appview'
67 #[arg(long)]
68 id: String,
69 /// Service type
70 ///
71 /// Not sure exactly what its requirements are. 'BlueskyAppview' for example
72 #[arg(long)]
73 r#type: String,
74 /// The HTTPS endpoint for the service
75 #[arg(long)]
76 service_endpoint: String,
77 /// The parent domain; requests should come from subdomains of this
78 #[arg(long)]
79 domain: Option<String>,
80}
81
82impl From<Args> for DidService {
83 fn from(a: Args) -> Self {
84 Self {
85 id: a.id,
86 r#type: a.r#type,
87 service_endpoint: a.service_endpoint,
88 }
89 }
90}
91
92#[tokio::main(flavor = "current_thread")]
93async fn main() {
94 tracing_subscriber::fmt::init();
95 log::info!("ɹoʇɔǝʅⅎǝɹ");
96
97 let args = Args::parse();
98 let domain = args.domain.clone();
99 let service: DidService = args.into();
100
101 Server::new(TcpListener::bind("0.0.0.0:3001"))
102 .run(
103 Route::new()
104 .at("/", get(hello))
105 .at("/.well-known/did.json", get(did_doc))
106 .at("/ask", get(ask_caddy))
107 .with(AddData::new(service))
108 .with(AddData::new(domain))
109 .with(Tracing),
110 )
111 .await
112 .unwrap()
113}