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