this repo has no description
1use crate::api::proxy_client::proxy_client; 2use crate::state::AppState; 3use axum::{ 4 Json, 5 body::Bytes, 6 extract::{Path, RawQuery, State}, 7 http::{HeaderMap, Method, StatusCode}, 8 response::{IntoResponse, Response}, 9}; 10use serde_json::json; 11use tracing::{error, info, warn}; 12 13const PROTECTED_METHODS: &[&str] = &[ 14 "com.atproto.admin.sendEmail", 15 "com.atproto.identity.requestPlcOperationSignature", 16 "com.atproto.identity.signPlcOperation", 17 "com.atproto.identity.updateHandle", 18 "com.atproto.server.activateAccount", 19 "com.atproto.server.confirmEmail", 20 "com.atproto.server.createAppPassword", 21 "com.atproto.server.deactivateAccount", 22 "com.atproto.server.getAccountInviteCodes", 23 "com.atproto.server.getSession", 24 "com.atproto.server.listAppPasswords", 25 "com.atproto.server.requestAccountDelete", 26 "com.atproto.server.requestEmailConfirmation", 27 "com.atproto.server.requestEmailUpdate", 28 "com.atproto.server.revokeAppPassword", 29 "com.atproto.server.updateEmail", 30]; 31 32fn is_protected_method(method: &str) -> bool { 33 PROTECTED_METHODS.contains(&method) 34} 35 36pub async fn proxy_handler( 37 State(state): State<AppState>, 38 Path(method): Path<String>, 39 method_verb: Method, 40 headers: HeaderMap, 41 RawQuery(query): RawQuery, 42 body: Bytes, 43) -> Response { 44 if is_protected_method(&method) { 45 warn!(method = %method, "Attempted to proxy protected method"); 46 return ( 47 StatusCode::BAD_REQUEST, 48 Json(json!({ 49 "error": "InvalidRequest", 50 "message": format!("Cannot proxy protected method: {}", method) 51 })), 52 ) 53 .into_response(); 54 } 55 56 let proxy_header = match headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) { 57 Some(h) => h.to_string(), 58 None => { 59 return ( 60 StatusCode::BAD_REQUEST, 61 Json(json!({ 62 "error": "InvalidRequest", 63 "message": "Missing required atproto-proxy header" 64 })), 65 ) 66 .into_response(); 67 } 68 }; 69 70 let did = proxy_header.split('#').next().unwrap_or(&proxy_header); 71 let resolved = match state.did_resolver.resolve_did(did).await { 72 Some(r) => r, 73 None => { 74 error!(did = %did, "Could not resolve service DID"); 75 return ( 76 StatusCode::BAD_GATEWAY, 77 Json(json!({ 78 "error": "UpstreamFailure", 79 "message": "Could not resolve service DID" 80 })), 81 ) 82 .into_response(); 83 } 84 }; 85 86 let target_url = match &query { 87 Some(q) => format!("{}/xrpc/{}?{}", resolved.url, method, q), 88 None => format!("{}/xrpc/{}", resolved.url, method), 89 }; 90 info!("Proxying {} request to {}", method_verb, target_url); 91 92 let client = proxy_client(); 93 let mut request_builder = client.request(method_verb, &target_url); 94 95 let mut auth_header_val = headers.get("Authorization").cloned(); 96 if let Some(token) = crate::auth::extract_bearer_token_from_header( 97 headers.get("Authorization").and_then(|h| h.to_str().ok()), 98 ) { 99 match crate::auth::validate_bearer_token(&state.db, &token).await { 100 Ok(auth_user) => { 101 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 102 auth_user.is_oauth, 103 auth_user.scope.as_deref(), 104 &resolved.did, 105 &method, 106 ) { 107 return e; 108 } 109 110 if let Some(key_bytes) = auth_user.key_bytes { 111 match crate::auth::create_service_token( 112 &auth_user.did, 113 &resolved.did, 114 &method, 115 &key_bytes, 116 ) { 117 Ok(new_token) => { 118 if let Ok(val) = 119 axum::http::HeaderValue::from_str(&format!("Bearer {}", new_token)) 120 { 121 auth_header_val = Some(val); 122 } 123 } 124 Err(e) => { 125 warn!("Failed to create service token: {:?}", e); 126 } 127 } 128 } 129 } 130 Err(e) => { 131 warn!("Token validation failed: {:?}", e); 132 if matches!(e, crate::auth::TokenValidationError::TokenExpired) { 133 let auth_header_str = headers 134 .get("Authorization") 135 .and_then(|h| h.to_str().ok()) 136 .unwrap_or(""); 137 let is_dpop = auth_header_str 138 .trim() 139 .get(..5) 140 .is_some_and(|s| s.eq_ignore_ascii_case("dpop ")); 141 let scheme = if is_dpop { "DPoP" } else { "Bearer" }; 142 let www_auth = format!( 143 "{} error=\"invalid_token\", error_description=\"Token has expired\"", 144 scheme 145 ); 146 let mut response = ( 147 StatusCode::UNAUTHORIZED, 148 Json(json!({ 149 "error": "ExpiredToken", 150 "message": "Token has expired" 151 })), 152 ) 153 .into_response(); 154 response 155 .headers_mut() 156 .insert("WWW-Authenticate", www_auth.parse().unwrap()); 157 if is_dpop { 158 let nonce = crate::oauth::verify::generate_dpop_nonce(); 159 response 160 .headers_mut() 161 .insert("DPoP-Nonce", nonce.parse().unwrap()); 162 } 163 return response; 164 } 165 } 166 } 167 } 168 169 if let Some(val) = auth_header_val { 170 request_builder = request_builder.header("Authorization", val); 171 } 172 for header_name in crate::api::proxy_client::HEADERS_TO_FORWARD { 173 if let Some(val) = headers.get(*header_name) { 174 request_builder = request_builder.header(*header_name, val); 175 } 176 } 177 if !body.is_empty() { 178 request_builder = request_builder.body(body); 179 } 180 181 match request_builder.send().await { 182 Ok(resp) => { 183 let status = resp.status(); 184 let headers = resp.headers().clone(); 185 let body = match resp.bytes().await { 186 Ok(b) => b, 187 Err(e) => { 188 error!("Error reading proxy response body: {:?}", e); 189 return (StatusCode::BAD_GATEWAY, "Error reading upstream response") 190 .into_response(); 191 } 192 }; 193 let mut response_builder = Response::builder().status(status); 194 for header_name in crate::api::proxy_client::RESPONSE_HEADERS_TO_FORWARD { 195 if let Some(val) = headers.get(*header_name) { 196 response_builder = response_builder.header(*header_name, val); 197 } 198 } 199 match response_builder.body(axum::body::Body::from(body)) { 200 Ok(r) => r, 201 Err(e) => { 202 error!("Error building proxy response: {:?}", e); 203 (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response() 204 } 205 } 206 } 207 Err(e) => { 208 error!("Error sending proxy request: {:?}", e); 209 if e.is_timeout() { 210 (StatusCode::GATEWAY_TIMEOUT, "Upstream Timeout").into_response() 211 } else { 212 (StatusCode::BAD_GATEWAY, "Upstream Error").into_response() 213 } 214 } 215 } 216}