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

sucks but kind of works now in safariiiiiii

Changed files
+60 -22
who-am-i
+43 -15
who-am-i/src/server.rs
··· 5 extract::{FromRef, Json as ExtractJson, Query, State}, 6 http::{ 7 StatusCode, 8 - header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER}, 9 }, 10 response::{IntoResponse, Json, Redirect, Response}, 11 routing::{get, post}, ··· 211 .into() 212 } 213 214 async fn prompt( 215 State(AppState { 216 allowed_hosts, ··· 221 tokens, 222 .. 223 }): State<AppState>, 224 jar: SignedCookieJar, 225 headers: HeaderMap, 226 ) -> impl IntoResponse { 227 - let err = |reason, check_frame| { 228 metrics::counter!("whoami_auth_prompt", "ok" => "false", "reason" => reason).increment(1); 229 - let info = json!({ "reason": reason, "check_frame": check_frame }); 230 let html = RenderHtml("prompt-error", engine.clone(), info); 231 (StatusCode::BAD_REQUEST, html).into_response() 232 }; 233 234 - let Some(referrer) = headers.get(REFERER) else { 235 - return err("Missing referer", true); 236 }; 237 - let Ok(referrer) = referrer.to_str() else { 238 - return err("Unreadable referer", true); 239 }; 240 - let Ok(url) = Url::parse(referrer) else { 241 - return err("Bad referer", true); 242 }; 243 let Some(parent_host) = url.host_str() else { 244 - return err("Referer missing host", true); 245 }; 246 if !allowed_hosts.contains(parent_host) { 247 - return err("Login is not allowed on this page", false); 248 } 249 let parent_origin = url.origin().ascii_serialization(); 250 if parent_origin == "null" { 251 - return err("Referer origin is opaque", true); 252 } 253 254 - let csp = format!("frame-ancestors {parent_origin}"); 255 let frame_headers = [(CONTENT_SECURITY_POLICY, &csp)]; 256 257 if let Some(did) = jar.get(DID_COOKIE_KEY) { 258 let Ok(did) = Did::new(did.value_trimmed().to_string()) else { 259 - return err("Bad cookie", false); 260 }; 261 262 // push cookie expiry ··· 266 Ok(t) => t, 267 Err(e) => { 268 eprintln!("failed to create JWT: {e:?}"); 269 - return err("failed to create JWT", false); 270 } 271 }; 272 ··· 286 "fetch_key": fetch_key, 287 "parent_host": parent_host, 288 "parent_origin": parent_origin, 289 }); 290 (frame_headers, jar, RenderHtml("prompt", engine, info)).into_response() 291 } else {
··· 5 extract::{FromRef, Json as ExtractJson, Query, State}, 6 http::{ 7 StatusCode, 8 + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, ORIGIN, REFERER}, 9 }, 10 response::{IntoResponse, Json, Redirect, Response}, 11 routing::{get, post}, ··· 211 .into() 212 } 213 214 + #[derive(Debug, Deserialize)] 215 + struct PromptQuery { 216 + // this must *ONLY* be used for the postmessage target origin 217 + app: Option<String>, 218 + } 219 async fn prompt( 220 State(AppState { 221 allowed_hosts, ··· 226 tokens, 227 .. 228 }): State<AppState>, 229 + Query(params): Query<PromptQuery>, 230 jar: SignedCookieJar, 231 headers: HeaderMap, 232 ) -> impl IntoResponse { 233 + let err = |reason, check_frame, detail| { 234 metrics::counter!("whoami_auth_prompt", "ok" => "false", "reason" => reason).increment(1); 235 + let info = json!({ 236 + "reason": reason, 237 + "check_frame": check_frame, 238 + "detail": detail, 239 + }); 240 let html = RenderHtml("prompt-error", engine.clone(), info); 241 (StatusCode::BAD_REQUEST, html).into_response() 242 }; 243 244 + let Some(parent) = headers.get(ORIGIN).or_else(|| { 245 + eprintln!("referrer fallback"); 246 + // TODO: referer should only be used for localhost?? 247 + headers.get(REFERER) 248 + }) else { 249 + return err("Missing origin and no referrer for fallback", true, None); 250 }; 251 + let Ok(parent) = parent.to_str() else { 252 + return err("Unreadable origin or referrer", true, None); 253 }; 254 + eprintln!( 255 + "rolling with parent: {parent:?} (from origin? {})", 256 + headers.get(ORIGIN).is_some() 257 + ); 258 + let Ok(url) = Url::parse(parent) else { 259 + return err("Bad origin or referrer", true, None); 260 }; 261 let Some(parent_host) = url.host_str() else { 262 + return err("Origin or referrer missing host", true, None); 263 }; 264 if !allowed_hosts.contains(parent_host) { 265 + return err( 266 + "Login is not allowed on this page", 267 + false, 268 + Some(parent_host), 269 + ); 270 } 271 let parent_origin = url.origin().ascii_serialization(); 272 if parent_origin == "null" { 273 + return err("Origin or referrer header value is opaque", true, None); 274 } 275 276 + let all_allowed = allowed_hosts 277 + .iter() 278 + .map(|h| format!("https://{h}")) 279 + .collect::<Vec<_>>() 280 + .join(" "); 281 + let csp = format!("frame-ancestors 'self' {parent_origin} {all_allowed}"); 282 let frame_headers = [(CONTENT_SECURITY_POLICY, &csp)]; 283 284 if let Some(did) = jar.get(DID_COOKIE_KEY) { 285 let Ok(did) = Did::new(did.value_trimmed().to_string()) else { 286 + return err("Bad cookie", false, None); 287 }; 288 289 // push cookie expiry ··· 293 Ok(t) => t, 294 Err(e) => { 295 eprintln!("failed to create JWT: {e:?}"); 296 + return err("failed to create JWT", false, None); 297 } 298 }; 299 ··· 313 "fetch_key": fetch_key, 314 "parent_host": parent_host, 315 "parent_origin": parent_origin, 316 + "parent_target": params.app.map(|h| format!("https://{h}")), 317 }); 318 (frame_headers, jar, RenderHtml("prompt", engine, info)).into_response() 319 } else {
+1 -1
who-am-i/static/style.css
··· 12 overflow: hidden; 13 display: flex; 14 flex-direction: column; 15 - height: 100vh; 16 } 17 .wrap.unframed { 18 border-radius: 0;
··· 12 overflow: hidden; 13 display: flex; 14 flex-direction: column; 15 + min-height: 100vh; 16 } 17 .wrap.unframed { 18 border-radius: 0;
+1
who-am-i/templates/prompt-error.hbs
··· 2 <div class="prompt-error"> 3 <p class="went-wrong">Something went wrong :(</p> 4 <p class="reason">{{ reason }}</p> 5 <p id="maybe-not-in-iframe" class="hidden"> 6 Possibly related: this prompt is meant to be shown in an iframe, but it seems like it's not. 7 </p>
··· 2 <div class="prompt-error"> 3 <p class="went-wrong">Something went wrong :(</p> 4 <p class="reason">{{ reason }}</p> 5 + <p class="reason detail">{{ detail }}</p> 6 <p id="maybe-not-in-iframe" class="hidden"> 7 Possibly related: this prompt is meant to be shown in an iframe, but it seems like it's not. 8 </p>
+15 -6
who-am-i/templates/prompt.hbs
··· 89 cookies: true, 90 localStorage: true, 91 }).then( 92 - () => desperation.textContent = "(maybe helped?)", 93 () => desperation.textContent = "(doubtful)", 94 ); 95 }) ··· 157 return info.handle; 158 } 159 160 const shareAllow = (handle, token) => { 161 - top.postMessage( 162 - { action: "allow", handle, token }, 163 - {{{json parent_origin}}}, 164 - ); 165 promptEl.textContent = '✔️ shared'; 166 } 167 168 const shareDeny = reason => { 169 top.postMessage( 170 { action: "deny", reason }, 171 - {{{json parent_origin}}}, 172 ); 173 } 174 </script>
··· 89 cookies: true, 90 localStorage: true, 91 }).then( 92 + () => { 93 + desperation.textContent = "(maybe helped?)"; 94 + setTimeout(() => location.reload(), 350); 95 + }, 96 () => desperation.textContent = "(doubtful)", 97 ); 98 }) ··· 160 return info.handle; 161 } 162 163 + const parentTarget = {{{json parent_target}}} ?? {{{json parent_origin}}}; 164 + 165 const shareAllow = (handle, token) => { 166 + try { 167 + top.postMessage( 168 + { action: "allow", handle, token }, 169 + parentTarget, 170 + ); 171 + } catch (e) { 172 + err(e, 'Identity verified but failed to connect with app'); 173 + }; 174 promptEl.textContent = '✔️ shared'; 175 } 176 177 const shareDeny = reason => { 178 top.postMessage( 179 { action: "deny", reason }, 180 + parentTarget, 181 ); 182 } 183 </script>