Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

dropshot -> axum

whew ok

Changed files
+50 -265
who-am-i
+3 -10
Cargo.lock
···
[[package]]
name = "axum"
-
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
dependencies = [
"axum-core",
"bytes",
···
"atrium-api 0.25.4",
"atrium-identity",
"atrium-oauth",
"clap",
-
"dropshot",
-
"env_logger",
"hickory-resolver",
-
"http",
-
"log",
"metrics",
-
"schemars",
-
"semver",
"serde",
-
"serde_json",
-
"serde_qs",
"tokio",
"tokio-util",
]
···
[[package]]
name = "axum"
+
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
dependencies = [
"axum-core",
"bytes",
···
"atrium-api 0.25.4",
"atrium-identity",
"atrium-oauth",
+
"axum",
"clap",
"hickory-resolver",
"metrics",
"serde",
"tokio",
"tokio-util",
]
+1 -8
who-am-i/Cargo.toml
···
atrium-api = { version = "0.25.4", default-features = false }
atrium-identity = "0.1.5"
atrium-oauth = "0.1.3"
clap = { version = "4.5.40", features = ["derive"] }
-
dropshot = "0.16.2"
-
env_logger = "0.11.8"
hickory-resolver = "0.25.2"
-
http = "1.3.1"
-
log = "0.4.27"
metrics = "0.24.2"
-
schemars = "0.8.22"
-
semver = "1.0.26"
serde = { version = "1.0.219", features = ["derive"] }
-
serde_json = "1.0.140"
-
serde_qs = "1.0.0-rc.3"
tokio = { version = "1.45.1", features = ["full", "macros"] }
tokio-util = "0.7.15"
···
atrium-api = { version = "0.25.4", default-features = false }
atrium-identity = "0.1.5"
atrium-oauth = "0.1.3"
+
axum = "0.8.4"
clap = { version = "4.5.40", features = ["derive"] }
hickory-resolver = "0.25.2"
metrics = "0.24.2"
serde = { version = "1.0.219", features = ["derive"] }
tokio = { version = "1.45.1", features = ["full", "macros"] }
tokio-util = "0.7.15"
+1 -3
who-am-i/src/main.rs
···
#[tokio::main]
async fn main() {
-
env_logger::init();
-
let server_shutdown = CancellationToken::new();
-
serve(server_shutdown).await.unwrap();
}
···
#[tokio::main]
async fn main() {
let server_shutdown = CancellationToken::new();
+
serve(server_shutdown).await;
}
+45 -244
who-am-i/src/server.rs
···
use atrium_api::agent::SessionManager;
-
use dropshot::{
-
ApiDescription, Body, ConfigDropshot, ConfigLogging, ConfigLoggingLevel, HttpError,
-
HttpResponse, HttpResponseSeeOther, Query, RequestContext, ServerBuilder, ServerContext,
-
endpoint, http_response_see_other,
};
-
use http::{
-
Response, StatusCode,
-
header::{ORIGIN, USER_AGENT},
-
};
-
use metrics::{counter, histogram};
-
use std::error::Error;
use std::sync::Arc;
-
-
use atrium_oauth::CallbackParams;
-
use schemars::JsonSchema;
-
use serde::{Deserialize, Serialize};
-
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use crate::{Client, authorize, client};
···
const INDEX_HTML: &str = include_str!("../static/index.html");
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
-
pub async fn serve(shutdown: CancellationToken) -> Result<(), Box<dyn Error + Send + Sync>> {
-
let config_logging = ConfigLogging::StderrTerminal {
-
level: ConfigLoggingLevel::Info,
};
-
let log = config_logging.to_logger("example-basic")?;
-
let mut api = ApiDescription::new();
-
api.register(index).unwrap();
-
api.register(favicon).unwrap();
-
api.register(openapi).unwrap();
-
api.register(start_oauth).unwrap();
-
api.register(finish_oauth).unwrap();
-
-
// TODO: put spec in a once cell / lazy lock thing?
-
let spec = Arc::new(
-
api.openapi(
-
"Who-am-i",
-
env!("CARGO_PKG_VERSION")
-
.parse()
-
.inspect_err(|e| {
-
eprintln!("failed to parse cargo package version for openapi: {e:?}")
-
})
-
.unwrap_or(semver::Version::new(0, 0, 1)),
-
)
-
.description("An atproto identity verifier that is very much not ready for real use")
-
.contact_name("part of @microcosm.blue")
-
.contact_url("https://microcosm.blue")
-
.json()?,
-
);
-
-
let ctx = Context {
-
spec,
-
client: client().into(),
-
};
-
let server = ServerBuilder::new(api, ctx, log)
-
.config(ConfigDropshot {
-
bind_address: "0.0.0.0:9997".parse().unwrap(),
-
..Default::default()
-
})
-
.start()?;
-
-
tokio::select! {
-
s = server.wait_for_shutdown() => {
-
s?;
-
log::info!("server shut down normally.");
-
},
-
_ = shutdown.cancelled() => {
-
log::info!("shutting down: closing server");
-
server.close().await?;
-
},
-
}
-
Ok(())
}
#[derive(Clone)]
-
struct Context {
-
pub spec: Arc<serde_json::Value>,
pub client: Arc<Client>,
}
-
async fn instrument_handler<T, H, R>(ctx: &RequestContext<T>, handler: H) -> Result<R, HttpError>
-
where
-
R: HttpResponse,
-
H: Future<Output = Result<R, HttpError>>,
-
T: ServerContext,
-
{
-
let start = Instant::now();
-
let result = handler.await;
-
let latency = start.elapsed();
-
let status_code = match &result {
-
Ok(response) => response.status_code(),
-
Err(e) => e.status_code.as_status(),
-
}
-
.as_str() // just the number (.to_string()'s Display does eg `200 OK`)
-
.to_string();
-
let endpoint = ctx.endpoint.operation_id.clone();
-
let headers = ctx.request.headers();
-
let origin = headers
-
.get(ORIGIN)
-
.and_then(|v| v.to_str().ok())
-
.unwrap_or("")
-
.to_string();
-
let ua = headers
-
.get(USER_AGENT)
-
.and_then(|v| v.to_str().ok())
-
.map(|ua| {
-
if ua.starts_with("Mozilla/5.0 ") {
-
"browser"
-
} else {
-
ua
-
}
-
})
-
.unwrap_or("")
-
.to_string();
-
counter!("server_requests_total",
-
"endpoint" => endpoint.clone(),
-
"origin" => origin,
-
"ua" => ua,
-
"status_code" => status_code,
-
)
-
.increment(1);
-
histogram!("server_handler_latency", "endpoint" => endpoint).record(latency.as_micros() as f64);
-
result
-
}
-
-
use dropshot::{HttpResponseHeaders, HttpResponseOk};
-
-
pub type OkCorsResponse<T> = Result<HttpResponseHeaders<HttpResponseOk<T>>, HttpError>;
-
-
/// Helper for constructing Ok responses: return OkCors(T).into()
-
/// (not happy with this yet)
-
pub struct OkCors<T: Serialize + JsonSchema + Send + Sync>(pub T);
-
-
impl<T> From<OkCors<T>> for OkCorsResponse<T>
-
where
-
T: Serialize + JsonSchema + Send + Sync,
-
{
-
fn from(ok: OkCors<T>) -> OkCorsResponse<T> {
-
let mut res = HttpResponseHeaders::new_unnamed(HttpResponseOk(ok.0));
-
res.headers_mut()
-
.insert("access-control-allow-origin", "*".parse().unwrap());
-
Ok(res)
-
}
-
}
-
-
// TODO: cors for HttpError
-
-
/// Serve index page as html
-
#[endpoint {
-
method = GET,
-
path = "/",
-
/*
-
* not useful to have this in openapi
-
*/
-
unpublished = true,
-
}]
-
async fn index(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
-
instrument_handler(&ctx, async {
-
Ok(Response::builder()
-
.status(StatusCode::OK)
-
.header(http::header::CONTENT_TYPE, "text/html")
-
.body(INDEX_HTML.into())?)
-
})
-
.await
-
}
-
-
/// Serve index page as html
-
#[endpoint {
-
method = GET,
-
path = "/favicon.ico",
-
/*
-
* not useful to have this in openapi
-
*/
-
unpublished = true,
-
}]
-
async fn favicon(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
-
instrument_handler(&ctx, async {
-
Ok(Response::builder()
-
.status(StatusCode::OK)
-
.header(http::header::CONTENT_TYPE, "image/x-icon")
-
.body(FAVICON.to_vec().into())?)
-
})
-
.await
-
}
-
-
/// Meta: get the openapi spec for this api
-
#[endpoint {
-
method = GET,
-
path = "/openapi",
-
/*
-
* not useful to have this in openapi
-
*/
-
unpublished = true,
-
}]
-
async fn openapi(ctx: RequestContext<Context>) -> OkCorsResponse<serde_json::Value> {
-
instrument_handler(&ctx, async {
-
let spec = (*ctx.context().spec).clone();
-
OkCors(spec).into()
-
})
-
.await
-
}
-
-
#[derive(Debug, Deserialize, JsonSchema)]
-
struct BeginOauthQuery {
handle: String,
}
-
#[endpoint {
-
method = GET,
-
path = "/auth",
-
}]
async fn start_oauth(
-
ctx: RequestContext<Context>,
-
query: Query<BeginOauthQuery>,
-
) -> Result<HttpResponseSeeOther, HttpError> {
-
let BeginOauthQuery { handle } = query.into_inner();
-
-
instrument_handler(&ctx, async {
-
let Context { client, .. } = ctx.context();
-
-
let auth_url = authorize(client, &handle).await;
-
-
http_response_see_other(auth_url)
-
})
-
.await
-
}
-
-
#[derive(Debug, Deserialize, JsonSchema)]
-
struct AuthorizedCallbackQuery {
-
code: String,
-
state: Option<String>,
-
iss: Option<String>,
-
}
-
impl From<AuthorizedCallbackQuery> for CallbackParams {
-
fn from(q: AuthorizedCallbackQuery) -> Self {
-
let AuthorizedCallbackQuery { code, state, iss } = q;
-
Self { code, state, iss }
-
}
}
-
#[endpoint {
-
method = GET,
-
path = "/authorized",
-
}]
-
async fn finish_oauth(
-
ctx: RequestContext<Context>,
-
query: Query<AuthorizedCallbackQuery>,
-
) -> Result<Response<Body>, HttpError> {
-
instrument_handler(&ctx, async {
-
let Context { client, .. } = ctx.context();
-
let params = query.into_inner();
-
-
let Ok((oauth_session, _)) = client.callback(params.into()).await else {
-
panic!("failed to do client callback");
-
};
-
let did = oauth_session.did().await.expect("a did to be present");
-
-
Ok(Response::builder()
-
.status(StatusCode::OK)
-
.header(http::header::CONTENT_TYPE, "text/html")
-
.body(format!("sup: {did:?}").into())?)
-
})
-
.await
}
···
use atrium_api::agent::SessionManager;
+
use atrium_oauth::CallbackParams;
+
use axum::{
+
Router,
+
extract::{Query, State},
+
response::{Html, Redirect},
+
routing::get,
};
+
+
use serde::Deserialize;
use std::sync::Arc;
+
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use crate::{Client, authorize, client};
···
const INDEX_HTML: &str = include_str!("../static/index.html");
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
+
pub async fn serve(shutdown: CancellationToken) {
+
let state = AppState {
+
client: Arc::new(client()),
};
+
let app = Router::new()
+
.route("/", get(|| async { Html(INDEX_HTML) }))
+
.route("/favicon.ico", get(|| async { FAVICON })) // todo MIME
+
.route("/auth", get(start_oauth))
+
.route("/authorized", get(complete_oauth))
+
.with_state(state);
+
let listener = TcpListener::bind("0.0.0.0:9997")
+
.await
+
.expect("listener binding to work");
+
axum::serve(listener, app)
+
.with_graceful_shutdown(async move { shutdown.cancelled().await })
+
.await
+
.unwrap();
}
#[derive(Clone)]
+
struct AppState {
pub client: Arc<Client>,
}
+
#[derive(Debug, Deserialize)]
+
struct BeginOauthParams {
handle: String,
}
async fn start_oauth(
+
State(state): State<AppState>,
+
Query(params): Query<BeginOauthParams>,
+
) -> Redirect {
+
let AppState { client } = state;
+
let BeginOauthParams { handle } = params;
+
let auth_url = authorize(&client, &handle).await;
+
Redirect::to(&auth_url)
}
+
async fn complete_oauth(
+
State(state): State<AppState>,
+
Query(params): Query<CallbackParams>,
+
) -> Html<String> {
+
let AppState { client } = state;
+
let Ok((oauth_session, _)) = client.callback(params).await else {
+
panic!("failed to do client callback");
+
};
+
let did = oauth_session.did().await.expect("a did to be present");
+
Html(format!("sup: {did:?}"))
}