1mod limits;
24
25pub use limits::{ProxyLimits, ProxySettings};
26
27use std::convert::Infallible;
28use std::time::Duration;
29
30use axum::body::Body;
31use axum::extract::{Request, State};
32use axum::http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
33use axum::response::{IntoResponse, Response};
34use didbot_identity::DidDocument;
35use didbot_pds::service_auth::{ServiceAuth, DEFAULT_LIFETIME};
36use didbot_pds::EstopRefusal;
37use http_body_util::BodyExt;
38use serde_json::Value;
39use time::OffsetDateTime;
40use tower::{Layer, ServiceExt};
41use tracing::{info, warn};
42use url::Url;
43
44use crate::error::ApiError;
45use crate::routes::{AppState, MAX_REQUEST_BODY};
46use crate::service_auth::{authorize, is_protected, sign};
47
48pub const PROXY_HEADER: &str = "atproto-proxy";
50
51pub const MAX_PROXIED_RESPONSE: usize = 10 * 1024 * 1024;
57
58pub const PROXY_TIMEOUT: Duration = Duration::from_secs(25);
65
66const FORWARDED_REQUEST_HEADERS: [HeaderName; 3] = [
69 header::ACCEPT_LANGUAGE,
70 HeaderName::from_static("atproto-accept-labelers"),
71 HeaderName::from_static("x-bsky-topics"),
72];
73
74const BODY_HEADERS: [HeaderName; 2] = [header::CONTENT_TYPE, header::CONTENT_ENCODING];
76
77const FORWARDED_RESPONSE_HEADERS: [HeaderName; 3] = [
79 HeaderName::from_static("atproto-repo-rev"),
80 HeaderName::from_static("atproto-content-labelers"),
81 header::RETRY_AFTER,
82];
83
84const CONTENT_HEADERS: [HeaderName; 4] = [
86 header::CONTENT_LENGTH,
87 header::CONTENT_ENCODING,
88 header::CONTENT_TYPE,
89 header::CONTENT_LANGUAGE,
90];
91
92pub(crate) async fn fallback(State(state): State<AppState>, request: Request) -> Response {
101 if !request.uri().path().starts_with("/xrpc/") {
102 return crate::routes::unknown_route(request.uri());
103 }
104 let forwarding = tower::service_fn(move |request: Request| {
105 let state = state.clone();
106 async move { Ok::<_, Infallible>(forward(&state, request).await) }
107 });
108 match crate::routes::cross_origin_layer()
109 .layer(forwarding)
110 .oneshot(request)
111 .await
112 {
113 Ok(response) => response,
114 Err(never) => match never {},
115 }
116}
117
118async fn forward(state: &AppState, request: Request) -> Response {
120 if !request.headers().contains_key(PROXY_HEADER)
121 && app_view(state, request.uri().path()).is_none()
122 {
123 return crate::routes::unknown_route(request.uri());
124 }
125 match proxy(state, request).await {
126 Ok(response) => response,
127 Err(err) => err.into_response(),
128 }
129}
130
131fn app_view<'a>(state: &'a AppState, path: &str) -> Option<&'a str> {
134 path.strip_prefix("/xrpc/")?.strip_prefix("app.bsky.")?;
135 state.proxy.settings().app_view.as_deref()
136}
137
138struct Target<'a> {
140 reference: &'a str,
143 did: &'a str,
144 service: &'a str,
145}
146
147impl<'a> Target<'a> {
148 fn parse(value: &'a str) -> Result<Self, ApiError> {
151 let Some((did, service)) = value.split_once('#') else {
152 return Err(ApiError::bad_request(
153 "the atproto-proxy header names no service id",
154 ));
155 };
156 if did.is_empty() {
157 return Err(ApiError::bad_request(
158 "the atproto-proxy header names no DID",
159 ));
160 }
161 if service.is_empty() {
162 return Err(ApiError::bad_request(
163 "the atproto-proxy header names no service id",
164 ));
165 }
166 if service.contains('#') {
167 return Err(ApiError::bad_request(
168 "the atproto-proxy header holds more than one `#`",
169 ));
170 }
171 if value.contains(' ') {
172 return Err(ApiError::bad_request(
173 "the atproto-proxy header may not contain spaces",
174 ));
175 }
176 Ok(Self {
177 reference: value,
178 did,
179 service,
180 })
181 }
182}
183
184async fn proxy(state: &AppState, request: Request) -> Result<Response, ApiError> {
185 state.estop.check_use(EstopRefusal::Operation)?;
186 let (parts, body) = request.into_parts();
187 let path = parts.uri.path();
188 let lxm = path.trim_start_matches("/xrpc/");
189 let http_method = match parts.method {
190 Method::GET => "GET",
191 Method::POST => "POST",
192 _ => {
193 return Err(ApiError::bad_request(
194 "an XRPC call is a GET or a POST, and nothing else is proxied",
195 ))
196 }
197 };
198 didbot_lexicon::nsid_syntax::validate(lxm)
199 .map_err(|_| ApiError::bad_request(format!("`{lxm}` is not an XRPC method")))?;
200 if is_protected(lxm) {
201 return Err(ApiError::new(
202 StatusCode::BAD_REQUEST,
203 "InvalidToken",
204 format!("`{lxm}` manages an account, and a call to it is not proxied"),
205 ));
206 }
207 let header = match parts.headers.get(PROXY_HEADER) {
208 Some(value) => value
209 .to_str()
210 .map_err(|_| ApiError::bad_request("the atproto-proxy header is not text"))?,
211 None => app_view(state, path).ok_or_else(|| {
212 ApiError::bad_request("the call names no service in an atproto-proxy header")
213 })?,
214 };
215 let target = Target::parse(header)?;
216
217 let caller = authorize(
218 state,
219 &parts.headers,
220 http_method,
221 path,
222 Some(lxm),
223 target.reference,
224 )?;
225 let body = axum::body::to_bytes(body, MAX_REQUEST_BODY)
226 .await
227 .map_err(|err| {
228 if std::error::Error::source(&err)
229 .is_some_and(|source| source.is::<http_body_util::LengthLimitError>())
230 {
231 crate::routes::request_too_large()
232 } else {
233 ApiError::bad_request(format!("reading the request body: {err}"))
234 }
235 })?;
236 let held = admit(state, &caller.did, &target, lxm)?;
237 let now = OffsetDateTime::now_utc();
238 let token = sign(
239 state,
240 &caller.did,
241 ServiceAuth {
242 aud: target.reference,
243 lxm: Some(lxm),
244 iat: now,
245 exp: now + DEFAULT_LIFETIME,
246 },
247 )?;
248 let (endpoint, client) = resolve(state, &target).await?;
249
250 let mut url = endpoint;
251 url.set_path(path);
252 url.set_query(parts.uri.query());
253 let mut outbound = client
254 .request(parts.method.clone(), url.clone())
255 .timeout(PROXY_TIMEOUT)
256 .headers(forwarded_request_headers(&parts.headers))
257 .header(header::AUTHORIZATION, format!("Bearer {token}"));
258 if parts.method == Method::POST {
259 for name in BODY_HEADERS {
260 if let Some(value) = parts.headers.get(&name) {
261 outbound = outbound.header(name, value.clone());
262 }
263 }
264 outbound = outbound.body(body);
265 }
266 let upstream = outbound.send().await.map_err(|error| {
267 info!(did = %caller.did, lxm, target = target.reference, %error, "proxied call failed");
268 ApiError::new(
269 StatusCode::BAD_GATEWAY,
270 "UpstreamFailure",
271 format!("{} could not be reached: {error}", target.reference),
272 )
273 })?;
274 let status = upstream.status();
275 info!(
276 did = %caller.did,
277 client = caller.client_id.as_ref().map(didbot_pds::policy::ClientId::as_str),
278 lxm,
279 target = target.reference,
280 status = status.as_u16(),
281 "proxied a call"
282 );
283 if status.is_client_error() || status.is_server_error() {
284 return Err(upstream_error(target.reference, upstream).await);
285 }
286 answer(target.reference, upstream, held)
287}
288
289fn admit(
294 state: &AppState,
295 did: &str,
296 target: &Target<'_>,
297 lxm: &str,
298) -> Result<limits::InFlight, ApiError> {
299 let tree = tree_of(state, did);
300 let refusal = match state.proxy.admit(did, &tree) {
301 Ok(held) => return Ok(held),
302 Err(refusal) => refusal,
303 };
304 let settings = state.proxy.settings();
305 match refusal {
306 limits::Refusal::Rate(wait) => {
307 warn!(
308 did,
309 target = target.reference,
310 lxm,
311 calls_per_minute = settings.calls_per_minute,
312 "refused a proxied call: the account is over its rate"
313 );
314 let retry = wait.as_secs() + u64::from(wait.subsec_nanos() > 0);
317 Err(ApiError::rate_limited(format!(
318 "this account may make {} proxied calls a minute; retry in {retry}s",
319 settings.calls_per_minute
320 ))
321 .with_header(header::RETRY_AFTER, retry.max(1).to_string()))
322 }
323 limits::Refusal::InFlight(open) => {
324 warn!(
325 did,
326 target = target.reference,
327 lxm,
328 open,
329 "refused a proxied call: the account has as many open as it may"
330 );
331 Err(ApiError::rate_limited(format!(
332 "this account already has {open} proxied calls open; retry when one finishes"
333 )))
334 }
335 limits::Refusal::Tree(open) => {
336 warn!(
337 did,
338 tree,
339 target = target.reference,
340 lxm,
341 open,
342 "refused a proxied call: the account's tree has as many open as it may"
343 );
344 Err(ApiError::rate_limited(format!(
345 "the accounts under {tree} already have {open} proxied calls open; retry when \
346 one finishes"
347 )))
348 }
349 limits::Refusal::Full(open) => {
350 warn!(
351 did,
352 target = target.reference,
353 lxm,
354 open,
355 "refused a proxied call: this server has as many open as it will hold"
356 );
357 Err(ApiError::rate_limited(format!(
358 "this server already has {open} proxied calls open; retry shortly"
359 )))
360 }
361 }
362}
363
364fn tree_of(state: &AppState, did: &str) -> String {
368 let service = state.registry.service_did();
369 state
370 .registry
371 .boundary(did)
372 .ok()
373 .and_then(|chain| chain.into_iter().rev().find(|account| *account != service))
374 .unwrap_or_else(|| did.to_owned())
375}
376
377fn forwarded_request_headers(headers: &HeaderMap) -> HeaderMap {
380 let mut forwarded = HeaderMap::new();
381 forwarded.insert(
382 header::ACCEPT_ENCODING,
383 headers
384 .get(header::ACCEPT_ENCODING)
385 .cloned()
386 .unwrap_or_else(|| HeaderValue::from_static("identity")),
387 );
388 for (name, value) in headers {
389 if FORWARDED_REQUEST_HEADERS.contains(name) || name.as_str().starts_with("x-atproto-") {
390 forwarded.append(name.clone(), value.clone());
391 }
392 }
393 forwarded
394}
395
396async fn resolve(
400 state: &AppState,
401 target: &Target<'_>,
402) -> Result<(Url, reqwest::Client), ApiError> {
403 let private = state.tree.private_addresses();
404 let endpoint = match state.proxy.cached_target(target.reference, private) {
405 Some(endpoint) => endpoint,
406 None => {
407 let endpoint = resolve_endpoint(state, target, private).await?;
408 state
409 .proxy
410 .cache_target(target.reference, private, &endpoint);
411 endpoint
412 }
413 };
414 refuse_this_server(state, target, &endpoint)?;
417 let client = didbot_http::pinned_client(&endpoint, private)
418 .await
419 .map_err(|error| unreachable_target(target, &error))?;
420 Ok((endpoint, client))
421}
422
423async fn resolve_endpoint(
426 state: &AppState,
427 target: &Target<'_>,
428 private: didbot_http::PrivateAddresses,
429) -> Result<Url, ApiError> {
430 let unresolved = |reason: String| {
431 ApiError::bad_request(format!("could not resolve {}: {reason}", target.did))
432 };
433 let document_url = didbot_identity::document_url(target.did)
434 .map_err(|error| unresolved(error.to_string()))
435 .and_then(|url| Url::parse(&url).map_err(|error| unresolved(error.to_string())))?;
436 refuse_this_server(state, target, &document_url)?;
437 let document = match crate::foreign::fetch_document(&document_url, target.did, private).await {
438 Ok(crate::foreign::Document::Found(document)) => document,
439 Ok(crate::foreign::Document::NotFound | crate::foreign::Document::Gone) => {
440 return Err(unresolved(format!("{document_url} holds no document")))
441 }
442 Err(crate::foreign::DocumentError::Address(error)) => {
443 return Err(unreachable_target(target, &error))
444 }
445 Err(crate::foreign::DocumentError::Failed(reason)) => return Err(unresolved(reason)),
446 };
447 let document: DidDocument = serde_json::from_value(document)
448 .map_err(|error| unresolved(format!("parsing {document_url}: {error}")))?;
449 let endpoint = document
450 .service_endpoint(target.service)
451 .ok_or_else(|| unresolved(format!("its document names no #{} service", target.service)))?;
452 let mut endpoint = Url::parse(endpoint)
453 .ok()
454 .filter(|url| url.scheme() == "https" && url.host().is_some())
455 .ok_or_else(|| {
456 unresolved(format!(
457 "its #{} service is at {endpoint:?}, which is not an https URL",
458 target.service
459 ))
460 })?;
461 endpoint.set_path("");
463 endpoint.set_query(None);
464 endpoint.set_fragment(None);
465 let _ = endpoint.set_username("");
466 let _ = endpoint.set_password(None);
467 Ok(endpoint)
468}
469
470fn refuse_this_server(state: &AppState, target: &Target<'_>, url: &Url) -> Result<(), ApiError> {
473 let host = url.host_str().unwrap_or_default();
474 if !crate::foreign::serves(state.registry.as_ref(), host) {
475 return Ok(());
476 }
477 info!(
478 target = target.reference,
479 host, "refused: the proxy target is this server"
480 );
481 Err(ApiError::bad_request(format!(
482 "{} is served by this server, and a call is not proxied to itself",
483 target.reference
484 )))
485}
486
487fn unreachable_target(target: &Target<'_>, error: &didbot_http::AddressError) -> ApiError {
489 info!(target = target.reference, %error, "refused a proxy target");
490 match error {
491 didbot_http::AddressError::Refused { .. } => ApiError::bad_request(format!(
492 "{} resolves to an address this server does not reach: {error}",
493 target.reference
494 )),
495 _ => ApiError::bad_request(format!("could not resolve {}: {error}", target.reference)),
496 }
497}
498
499fn answer(
504 target: &str,
505 upstream: reqwest::Response,
506 held: limits::InFlight,
507) -> Result<Response, ApiError> {
508 if upstream
509 .content_length()
510 .is_some_and(|length| length > MAX_PROXIED_RESPONSE as u64)
511 {
512 return Err(ApiError::new(
513 StatusCode::BAD_GATEWAY,
514 "UpstreamFailure",
515 format!("{target} answered with more than {MAX_PROXIED_RESPONSE} bytes"),
516 ));
517 }
518 let upstream = http::Response::<reqwest::Body>::from(upstream);
519 let mut response = Response::builder().status(upstream.status());
520 for name in CONTENT_HEADERS.iter().chain(&FORWARDED_RESPONSE_HEADERS) {
521 if let Some(value) = upstream.headers().get(name) {
522 response = response.header(name, value.clone());
523 }
524 }
525 let body = http_body_util::Limited::new(upstream.into_body(), MAX_PROXIED_RESPONSE).map_frame(
526 move |frame| {
527 let _held = &held;
528 frame
529 },
530 );
531 response.body(Body::new(body)).map_err(|error| {
532 ApiError::new(
533 StatusCode::BAD_GATEWAY,
534 "UpstreamFailure",
535 format!("{target} answered with a response this server cannot pass on: {error}"),
536 )
537 })
538}
539
540async fn upstream_error(target: &str, upstream: reqwest::Response) -> ApiError {
543 let status = upstream.status();
544 let forwarded: Vec<(HeaderName, String)> = FORWARDED_RESPONSE_HEADERS
545 .iter()
546 .filter_map(|name| {
547 let value = upstream.headers().get(name)?.to_str().ok()?;
548 Some((name.clone(), value.to_owned()))
549 })
550 .collect();
551 let body = didbot_http::read_bounded(upstream, didbot_http::MAX_JSON_BODY)
552 .await
553 .unwrap_or_default();
554 let named = serde_json::from_slice::<Value>(&body).ok();
555 let field = |name: &str| {
556 named
557 .as_ref()
558 .and_then(|value| value.get(name))
559 .and_then(Value::as_str)
560 .map(str::to_owned)
561 };
562 let status = if status == StatusCode::INTERNAL_SERVER_ERROR {
563 StatusCode::BAD_GATEWAY
564 } else {
565 status
566 };
567 let error = field("error").unwrap_or_else(|| status_name(status).to_owned());
568 let message = field("message").unwrap_or_else(|| format!("{target} answered {status}"));
569 forwarded.into_iter().fold(
570 ApiError::new(status, error, message),
571 |err, (name, value)| err.with_header(name, value),
572 )
573}
574
575fn status_name(status: StatusCode) -> &'static str {
578 match status.as_u16() {
579 401 => "AuthenticationRequired",
580 403 => "Forbidden",
581 404 => "XRPCNotSupported",
582 406 => "NotAcceptable",
583 413 => "PayloadTooLarge",
584 415 => "UnsupportedMediaType",
585 429 => "RateLimitExceeded",
586 501 => "MethodNotImplemented",
587 502 => "UpstreamFailure",
588 503 => "NotEnoughResources",
589 504 => "UpstreamTimeout",
590 400..=499 => "InvalidRequest",
591 _ => "InternalServerError",
592 }
593}