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 return (
134 StatusCode::BAD_REQUEST,
135 Json(json!({
136 "error": "ExpiredToken",
137 "message": "Token has expired"
138 })),
139 )
140 .into_response();
141 }
142 }
143 }
144 }
145
146 if let Some(val) = auth_header_val {
147 request_builder = request_builder.header("Authorization", val);
148 }
149 for header_name in crate::api::proxy_client::HEADERS_TO_FORWARD {
150 if let Some(val) = headers.get(*header_name) {
151 request_builder = request_builder.header(*header_name, val);
152 }
153 }
154 if !body.is_empty() {
155 request_builder = request_builder.body(body);
156 }
157
158 match request_builder.send().await {
159 Ok(resp) => {
160 let status = resp.status();
161 let headers = resp.headers().clone();
162 let body = match resp.bytes().await {
163 Ok(b) => b,
164 Err(e) => {
165 error!("Error reading proxy response body: {:?}", e);
166 return (StatusCode::BAD_GATEWAY, "Error reading upstream response")
167 .into_response();
168 }
169 };
170 let mut response_builder = Response::builder().status(status);
171 for header_name in crate::api::proxy_client::RESPONSE_HEADERS_TO_FORWARD {
172 if let Some(val) = headers.get(*header_name) {
173 response_builder = response_builder.header(*header_name, val);
174 }
175 }
176 match response_builder.body(axum::body::Body::from(body)) {
177 Ok(r) => r,
178 Err(e) => {
179 error!("Error building proxy response: {:?}", e);
180 (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
181 }
182 }
183 }
184 Err(e) => {
185 error!("Error sending proxy request: {:?}", e);
186 if e.is_timeout() {
187 (StatusCode::GATEWAY_TIMEOUT, "Upstream Timeout").into_response()
188 } else {
189 (StatusCode::BAD_GATEWAY, "Upstream Error").into_response()
190 }
191 }
192 }
193}