An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

style: apply cargo fmt to relay codebase

authored by

Malpercio and committed by
Tangled
6a62b15f 628e65f2

+119 -67
+1 -4
crates/relay/src/app.rs
··· 151 151 "/xrpc/com.atproto.server.createSession", 152 152 post(create_session), 153 153 ) 154 - .route( 155 - "/xrpc/com.atproto.server.getSession", 156 - get(get_session), 157 - ) 154 + .route("/xrpc/com.atproto.server.getSession", get(get_session)) 158 155 .route( 159 156 "/xrpc/com.atproto.identity.resolveHandle", 160 157 get(resolve_handle_handler),
+1 -1
crates/relay/src/auth/dpop.rs
··· 1 1 // pattern: Mixed (unavoidable) 2 2 3 + use axum::http::Method; 3 4 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 4 5 use common::{ApiError, ErrorCode}; 5 6 use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; ··· 10 11 use std::sync::Arc; 11 12 use std::time::Instant; 12 13 use tokio::sync::Mutex; 13 - use axum::http::Method; 14 14 15 15 use super::jwt::AccessTokenClaims; 16 16
+8 -2
crates/relay/src/auth/jwt.rs
··· 63 63 } 64 64 65 65 /// Verify ES256 AT+JWT tokens issued by the OAuth token endpoint. 66 - pub fn verify_es256_access_token(token: &str, state: &AppState) -> Result<AccessTokenClaims, ApiError> { 66 + pub fn verify_es256_access_token( 67 + token: &str, 68 + state: &AppState, 69 + ) -> Result<AccessTokenClaims, ApiError> { 67 70 let invalid = || ApiError::new(ErrorCode::InvalidToken, "invalid token"); 68 71 let jwk: jsonwebtoken::jwk::Jwk = serde_json::from_value( 69 72 state.oauth_signing_keypair.public_key_jwk.clone(), ··· 95 98 } 96 99 97 100 /// Verify HS256 access/refresh JWT issued by this server (legacy tokens). 98 - pub fn verify_hs256_access_token(token: &str, state: &AppState) -> Result<AccessTokenClaims, ApiError> { 101 + pub fn verify_hs256_access_token( 102 + token: &str, 103 + state: &AppState, 104 + ) -> Result<AccessTokenClaims, ApiError> { 99 105 let decoding_key = DecodingKey::from_secret(&state.jwt_secret); 100 106 101 107 let mut validation = Validation::new(Algorithm::HS256);
+3 -1
crates/relay/src/auth/mod.rs
··· 23 23 #[cfg(test)] 24 24 pub(super) use bearer::extract_bearer_token; 25 25 #[cfg(test)] 26 - pub(super) use dpop::{dpop_alg_from_str, jwk_thumbprint, validate_and_consume_nonce, validate_dpop}; 26 + pub(super) use dpop::{ 27 + dpop_alg_from_str, jwk_thumbprint, validate_and_consume_nonce, validate_dpop, 28 + }; 27 29 #[cfg(test)] 28 30 pub(super) use jwt::{parse_scope, peek_jwt_typ, verify_access_token}; 29 31
+2 -8
crates/relay/src/auth/rate_limit.rs
··· 34 34 } 35 35 36 36 /// Record a new failed attempt timestamp for `identifier`. 37 - pub(crate) fn record_failure( 38 - attempts: &mut HashMap<String, VecDeque<Instant>>, 39 - identifier: &str, 40 - ) { 37 + pub(crate) fn record_failure(attempts: &mut HashMap<String, VecDeque<Instant>>, identifier: &str) { 41 38 attempts 42 39 .entry(identifier.to_string()) 43 40 .or_default() ··· 45 42 } 46 43 47 44 /// Clear the failure history for `identifier` on successful authentication. 48 - pub(crate) fn clear_failures( 49 - attempts: &mut HashMap<String, VecDeque<Instant>>, 50 - identifier: &str, 51 - ) { 45 + pub(crate) fn clear_failures(attempts: &mut HashMap<String, VecDeque<Instant>>, identifier: &str) { 52 46 attempts.remove(identifier); 53 47 }
+9 -7
crates/relay/src/db/accounts.rs
··· 52 52 ApiError::new(ErrorCode::InternalError, "failed to load account") 53 53 })?; 54 54 55 - Ok(row.map(|(email, email_confirmed_at, handle, did_doc)| SessionAccountRow { 56 - did: did.to_string(), 57 - email, 58 - email_confirmed: email_confirmed_at.is_some(), 59 - handle, 60 - did_doc, 61 - })) 55 + Ok(row.map( 56 + |(email, email_confirmed_at, handle, did_doc)| SessionAccountRow { 57 + did: did.to_string(), 58 + email, 59 + email_confirmed: email_confirmed_at.is_some(), 60 + handle, 61 + did_doc, 62 + }, 63 + )) 62 64 } 63 65 64 66 /// Resolve a handle or DID to an active (non-deactivated) account.
+19 -14
crates/relay/src/db/oauth.rs
··· 542 542 .unwrap() 543 543 .expect("PAR request should be found on first consume"); 544 544 545 - assert_eq!(row.client_id, "https://app.example.com/client-metadata.json"); 545 + assert_eq!( 546 + row.client_id, 547 + "https://app.example.com/client-metadata.json" 548 + ); 546 549 assert!(row.request_parameters.contains("redirect_uri")); 547 550 548 551 // Second consume must return None — single-use enforcement (RFC 9126 §4). 549 - let second = consume_par_request(&pool, "urn:ietf:params:oauth:request_uri:test-token-abc123") 550 - .await 551 - .unwrap(); 552 - assert!(second.is_none(), "consumed PAR request must not be found again"); 552 + let second = 553 + consume_par_request(&pool, "urn:ietf:params:oauth:request_uri:test-token-abc123") 554 + .await 555 + .unwrap(); 556 + assert!( 557 + second.is_none(), 558 + "consumed PAR request must not be found again" 559 + ); 553 560 } 554 561 555 562 #[tokio::test] ··· 584 591 .await 585 592 .unwrap(); 586 593 587 - let result = 588 - consume_par_request(&pool, "urn:ietf:params:oauth:request_uri:expired-token") 589 - .await 590 - .unwrap(); 594 + let result = consume_par_request(&pool, "urn:ietf:params:oauth:request_uri:expired-token") 595 + .await 596 + .unwrap(); 591 597 assert!(result.is_none(), "expired PAR request must return None"); 592 598 } 593 599 ··· 626 632 627 633 cleanup_expired_par_requests(&pool).await.unwrap(); 628 634 629 - let count: (i64,) = 630 - sqlx::query_as("SELECT COUNT(*) FROM oauth_par_requests") 631 - .fetch_one(&pool) 632 - .await 633 - .unwrap(); 635 + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM oauth_par_requests") 636 + .fetch_one(&pool) 637 + .await 638 + .unwrap(); 634 639 assert_eq!(count.0, 1, "only the valid PAR request should remain"); 635 640 } 636 641
+3 -2
crates/relay/src/routes/create_session.rs
··· 215 215 } 216 216 217 217 // ATProto spec: "handle.invalid" is the sentinel for accounts without a resolvable handle. 218 - let handle = account.handle.unwrap_or_else(|| "handle.invalid".to_string()); 218 + let handle = account 219 + .handle 220 + .unwrap_or_else(|| "handle.invalid".to_string()); 219 221 220 222 Ok(( 221 223 StatusCode::OK, ··· 228 230 }), 229 231 )) 230 232 } 231 - 232 233 233 234 /// Sign an HS256 access JWT with a 2-hour lifetime. 234 235 fn issue_access_jwt(secret: &[u8; 32], did: &str, aud: &str, now: u64) -> Result<String, ApiError> {
+46 -13
crates/relay/src/routes/get_session.rs
··· 59 59 }); 60 60 61 61 // ATProto spec: "handle.invalid" is the sentinel for accounts without a resolvable handle. 62 - let handle = account.handle.unwrap_or_else(|| "handle.invalid".to_string()); 62 + let handle = account 63 + .handle 64 + .unwrap_or_else(|| "handle.invalid".to_string()); 63 65 64 66 Ok(Json(GetSessionResponse { 65 67 did: account.did, ··· 197 199 #[tokio::test] 198 200 async fn valid_token_returns_session_info() { 199 201 let state = test_state().await; 200 - insert_account(&state.db, "did:plc:alice", "alice.test.example.com", "alice@example.com") 201 - .await; 202 + insert_account( 203 + &state.db, 204 + "did:plc:alice", 205 + "alice.test.example.com", 206 + "alice@example.com", 207 + ) 208 + .await; 202 209 let token = access_jwt(&state.jwt_secret, "did:plc:alice"); 203 210 204 211 let response = app(state) ··· 212 219 assert_eq!(json["handle"], "alice.test.example.com"); 213 220 assert_eq!(json["email"], "alice@example.com"); 214 221 assert_eq!(json["emailConfirmed"], false); 215 - assert!(json.get("didDoc").is_none(), "didDoc absent when no document stored"); 222 + assert!( 223 + json.get("didDoc").is_none(), 224 + "didDoc absent when no document stored" 225 + ); 216 226 } 217 227 218 228 #[tokio::test] 219 229 async fn confirmed_email_returns_true() { 220 230 let state = test_state().await; 221 - insert_account(&state.db, "did:plc:confirmed", "conf.test.example.com", "conf@example.com") 222 - .await; 231 + insert_account( 232 + &state.db, 233 + "did:plc:confirmed", 234 + "conf.test.example.com", 235 + "conf@example.com", 236 + ) 237 + .await; 223 238 sqlx::query("UPDATE accounts SET email_confirmed_at = datetime('now') WHERE did = ?") 224 239 .bind("did:plc:confirmed") 225 240 .execute(&state.db) ··· 240 255 #[tokio::test] 241 256 async fn did_doc_included_when_present() { 242 257 let state = test_state().await; 243 - insert_account(&state.db, "did:plc:withdoc", "doc.test.example.com", "doc@example.com") 244 - .await; 258 + insert_account( 259 + &state.db, 260 + "did:plc:withdoc", 261 + "doc.test.example.com", 262 + "doc@example.com", 263 + ) 264 + .await; 245 265 let doc = serde_json::json!({"id": "did:plc:withdoc", "@context": ["https://www.w3.org/ns/did/v1"]}); 246 266 insert_did_doc(&state.db, "did:plc:withdoc", doc.clone()).await; 247 267 let token = access_jwt(&state.jwt_secret, "did:plc:withdoc"); ··· 304 324 #[tokio::test] 305 325 async fn refresh_token_returns_401() { 306 326 let state = test_state().await; 307 - insert_account(&state.db, "did:plc:refresh", "refresh.test.example.com", "r@example.com") 308 - .await; 327 + insert_account( 328 + &state.db, 329 + "did:plc:refresh", 330 + "refresh.test.example.com", 331 + "r@example.com", 332 + ) 333 + .await; 309 334 let token = refresh_jwt(&state.jwt_secret, "did:plc:refresh"); 310 335 311 336 let response = app(state) ··· 321 346 #[tokio::test] 322 347 async fn deactivated_account_returns_401_with_invalid_token_code() { 323 348 let state = test_state().await; 324 - insert_account(&state.db, "did:plc:deact", "deact.test.example.com", "deact@example.com") 325 - .await; 349 + insert_account( 350 + &state.db, 351 + "did:plc:deact", 352 + "deact.test.example.com", 353 + "deact@example.com", 354 + ) 355 + .await; 326 356 sqlx::query("UPDATE accounts SET deactivated_at = datetime('now') WHERE did = ?") 327 357 .bind("did:plc:deact") 328 358 .execute(&state.db) ··· 432 462 433 463 assert_eq!(response.status(), StatusCode::OK); 434 464 let json = body_json(response).await; 435 - assert!(json.get("didDoc").is_none(), "malformed didDoc must be omitted"); 465 + assert!( 466 + json.get("didDoc").is_none(), 467 + "malformed didDoc must be omitted" 468 + ); 436 469 } 437 470 438 471 #[tokio::test]
+8 -3
crates/relay/src/routes/oauth_authorize.rs
··· 19 19 use crate::auth::password::{verify_password, VerifyResult, TIMING_DUMMY_HASH}; 20 20 use crate::auth::rate_limit::{clear_failures, is_rate_limited, record_failure}; 21 21 use crate::db::accounts::resolve_identifier; 22 - use crate::db::oauth::{consume_par_request, get_oauth_client, store_authorization_code, StoredPARParams}; 22 + use crate::db::oauth::{ 23 + consume_par_request, get_oauth_client, store_authorization_code, StoredPARParams, 24 + }; 23 25 use crate::routes::oauth_templates::{ 24 26 encode_param, error_page, error_redirect, render_consent_page, 25 27 }; ··· 116 118 if let Some(uri) = raw.request_uri { 117 119 let row = match consume_par_request(&state.db, &uri).await { 118 120 Ok(Some(r)) => r, 119 - Ok(None) => return Err(ResolveError::Client("request_uri is invalid or has expired")), 121 + Ok(None) => { 122 + return Err(ResolveError::Client( 123 + "request_uri is invalid or has expired", 124 + )) 125 + } 120 126 Err(e) => { 121 127 tracing::error!(error = %e, "db error consuming PAR request"); 122 128 return Err(ResolveError::Server( ··· 566 572 ); 567 573 Redirect::to(&redirect_url).into_response() 568 574 } 569 - 570 575 571 576 #[cfg(test)] 572 577 mod tests {
+19 -12
crates/relay/src/routes/oauth_par.rs
··· 89 89 pub async fn post_par(State(state): State<AppState>, Form(form): Form<PARForm>) -> Response { 90 90 let client_id = match form.client_id.as_deref().filter(|s| !s.is_empty()) { 91 91 Some(id) => id.to_string(), 92 - None => { 93 - return PARError::new("invalid_request", "client_id is required").into_response() 94 - } 92 + None => return PARError::new("invalid_request", "client_id is required").into_response(), 95 93 }; 96 94 97 95 let redirect_uri = match form.redirect_uri.as_deref().filter(|s| !s.is_empty()) { ··· 108 106 } 109 107 }; 110 108 111 - let code_challenge_method = match form.code_challenge_method.as_deref().filter(|s| !s.is_empty()) { 109 + let code_challenge_method = match form 110 + .code_challenge_method 111 + .as_deref() 112 + .filter(|s| !s.is_empty()) 113 + { 112 114 Some(m) => m.to_string(), 113 115 None => { 114 116 return PARError::new("invalid_request", "code_challenge_method is required") ··· 185 187 } 186 188 187 189 if code_challenge_method != "S256" { 188 - return PARError::new( 189 - "invalid_request", 190 - "code_challenge_method must be S256", 191 - ) 192 - .into_response(); 190 + return PARError::new("invalid_request", "code_challenge_method must be S256") 191 + .into_response(); 193 192 } 194 193 195 194 let params = StoredPARParams { ··· 315 314 .unwrap(); 316 315 let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); 317 316 318 - let request_uri = json["request_uri"].as_str().expect("request_uri must be present"); 317 + let request_uri = json["request_uri"] 318 + .as_str() 319 + .expect("request_uri must be present"); 319 320 assert!( 320 321 request_uri.starts_with("urn:ietf:params:oauth:request_uri:"), 321 322 "request_uri must use the OAuth PAR URN scheme" ··· 358 359 .method("POST") 359 360 .uri("/oauth/par") 360 361 .header("content-type", "application/x-www-form-urlencoded") 361 - .body(Body::from(par_body(&[("redirect_uri", "https://evil.example.com/cb")]))) 362 + .body(Body::from(par_body(&[( 363 + "redirect_uri", 364 + "https://evil.example.com/cb", 365 + )]))) 362 366 .unwrap(), 363 367 ) 364 368 .await ··· 604 608 let uri1 = call_par(state1, par_body(&[])).await; 605 609 let uri2 = call_par(state2, par_body(&[])).await; 606 610 607 - assert_ne!(uri1, uri2, "each PAR call must produce a unique request_uri"); 611 + assert_ne!( 612 + uri1, uri2, 613 + "each PAR call must produce a unique request_uri" 614 + ); 608 615 } 609 616 610 617 #[tokio::test]