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

wip: better pages and errors

Changed files
+38 -66
who-am-i
+1 -1
who-am-i/readme.md
···
it's still nice to have an explicit opt-in on a per-demo basis for microcosm so it will be used for that. it's allow-listed for the microcosm domain however (so not deployed on any adversarial hosting pages), so it's simultaenously overkill and restrictive.
-
i will get back to oauth eventually and hopefully roll out a microcosm service to make it easy for clients, but there are a few more things in the pipeline to get to first.
···
it's still nice to have an explicit opt-in on a per-demo basis for microcosm so it will be used for that. it's allow-listed for the microcosm domain however (so not deployed on any adversarial hosting pages), so it's simultaenously overkill and restrictive.
+
i will get back to oauth eventually and hopefully roll out a microcosm service to make it easy for clients (and demos), but there are a few more things in the pipeline to get to first.
+37 -12
who-am-i/src/server.rs
···
extract::{FromRef, Query, State},
http::{
StatusCode,
-
header::{HeaderMap, REFERER},
},
-
response::{Html, IntoResponse, Json, Redirect, Response},
routing::get,
};
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
···
use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError};
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
-
const INDEX_HTML: &str = include_str!("../static/index.html");
const DID_COOKIE_KEY: &str = "did";
type AppEngine = Engine<Handlebars<'static>>;
#[derive(Clone)]
struct AppState {
···
};
let app = Router::new()
-
.route("/", get(|| async { Html(INDEX_HTML) }))
-
.route("/favicon.ico", get(|| async { FAVICON })) // todo MIME
.route("/prompt", get(prompt))
.route("/user-info", get(user_info))
.route("/auth", get(start_oauth))
···
.unwrap();
}
async fn prompt(
State(AppState {
allowed_hosts,
···
jar: SignedCookieJar,
headers: HeaderMap,
) -> impl IntoResponse {
let Some(referrer) = headers.get(REFERER) else {
-
return Html::<&'static str>("missing referrer, sorry").into_response();
};
let Ok(referrer) = referrer.to_str() else {
-
return "referer contained opaque bytes".into_response();
};
let Ok(url) = Url::parse(referrer) else {
-
return "referrer was not a url".into_response();
};
let Some(parent_host) = url.host_str() else {
-
return "could nto get host from url".into_response();
};
if !allowed_hosts.contains(parent_host) {
-
return format!("host {parent_host:?} not in allowed_hosts, disallowing for now")
-
.into_response();
}
if let Some(did) = jar.get(DID_COOKIE_KEY) {
let Ok(did) = Did::new(did.value_trimmed().to_string()) else {
-
return "did from cookie failed to parse".into_response();
};
let fetch_key = resolve_handles.dispatch(
···
extract::{FromRef, Query, State},
http::{
StatusCode,
+
header::{CONTENT_TYPE, HeaderMap, REFERER},
},
+
response::{IntoResponse, Json, Redirect, Response},
routing::get,
};
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
···
use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError};
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
+
const STYLE_CSS: &str = include_str!("../static/style.css");
const DID_COOKIE_KEY: &str = "did";
type AppEngine = Engine<Handlebars<'static>>;
+
type Rendered = RenderHtml<&'static str, AppEngine, Value>;
#[derive(Clone)]
struct AppState {
···
};
let app = Router::new()
+
.route("/", get(hello))
+
.route("/favicon.ico", get(favicon)) // todo MIME
+
.route("/style.css", get(css))
.route("/prompt", get(prompt))
.route("/user-info", get(user_info))
.route("/auth", get(start_oauth))
···
.unwrap();
}
+
async fn hello(State(AppState { engine, .. }): State<AppState>) -> Rendered {
+
RenderHtml("hello", engine, json!({}))
+
}
+
+
async fn css() -> impl IntoResponse {
+
let headers = [
+
(CONTENT_TYPE, "text/css"),
+
// (CACHE_CONTROL, "") // TODO
+
];
+
(headers, STYLE_CSS)
+
}
+
+
async fn favicon() -> impl IntoResponse {
+
([(CONTENT_TYPE, "image/x-icon")], FAVICON)
+
}
+
async fn prompt(
State(AppState {
allowed_hosts,
···
jar: SignedCookieJar,
headers: HeaderMap,
) -> impl IntoResponse {
+
let err = |reason, check_frame| {
+
let info = json!({
+
"reason": reason,
+
"check_frame": check_frame,
+
});
+
RenderHtml("prompt-error", engine.clone(), info).into_response()
+
};
+
let Some(referrer) = headers.get(REFERER) else {
+
return err("Missing referer", true);
};
let Ok(referrer) = referrer.to_str() else {
+
return err("Unreadable referer", true);
};
let Ok(url) = Url::parse(referrer) else {
+
return err("Bad referer", true);
};
let Some(parent_host) = url.host_str() else {
+
return err("Referer missing host", true);
};
if !allowed_hosts.contains(parent_host) {
+
return err("Login is not allowed on this page", false);
}
if let Some(did) = jar.get(DID_COOKIE_KEY) {
let Ok(did) = Did::new(did.value_trimmed().to_string()) else {
+
return err("Bad cookie", false);
};
let fetch_key = resolve_handles.dispatch(
-53
who-am-i/static/index.html
···
-
<!doctype html>
-
<html lang="en">
-
<head>
-
<meta charset="utf-8" />
-
<title>Who-am-i documentation</title>
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
-
<meta name="description" content="API Documentation who-am-i, a read-only atproto identity verifier" />
-
<style>
-
.custom-header {
-
height: 42px;
-
background-color: #221828;
-
box-shadow: inset 0 -1px 0 var(--scalar-border-color);
-
color: var(--scalar-color-1);
-
font-size: var(--scalar-font-size-3);
-
font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
-
padding: 0 18px;
-
justify-content: space-between;
-
}
-
.custom-header,
-
.custom-header nav {
-
display: flex;
-
align-items: center;
-
gap: 18px;
-
}
-
.custom-header a:hover {
-
color: var(--scalar-color-2);
-
}
-
</style>
-
</head>
-
<body>
-
<header class="custom-header scalar-app">
-
<p>
-
<a href="https://ufos.microcosm.blue">Launch who-am-i [todo]</a>
-
</p>
-
<nav>
-
<b>a <a href="https://microcosm.blue">microcosm</a> project</b>
-
<a href="https://bsky.app/profile/microcosm.blue">@microcosm.blue</a>
-
<a href="https://github.com/at-microcosm">github</a>
-
</nav>
-
</header>
-
-
<script id="api-reference" type="application/json" data-url="/openapi"></script>
-
-
<script>
-
var configuration = {
-
theme: 'purple',
-
}
-
document.getElementById('api-reference').dataset.configuration = JSON.stringify(configuration)
-
</script>
-
-
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
-
</body>
-
</html>
···