From ee43d056c2a6db48e58e8a8fe107ca47180c4939 Mon Sep 17 00:00:00 2001 From: Trezy Date: Thu, 5 Mar 2026 10:36:23 -0600 Subject: [PATCH] feat: resolve and proxy unrecognized XRPCs --- Cargo.toml | 1 + docker-compose.yml | 5 ++- src/xrpc/mod.rs | 85 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e66d011..622e3e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ hickory-resolver = "0.25" mlua = { version = "0.11", features = ["lua54", "async", "serialize", "vendored", "send"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +urlencoding = "2.1.3" [dev-dependencies] wiremock = "0.6" diff --git a/docker-compose.yml b/docker-compose.yml index c4fadc8..0a49999 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,10 @@ services: retries: 5 tap: - image: ghcr.io/bluesky-social/indigo/tap:latest + build: + context: ../indigo + dockerfile: cmd/tap/Dockerfile + # image: ghcr.io/bluesky-social/indigo/tap:latest ports: - "2480:2480" environment: diff --git a/src/xrpc/mod.rs b/src/xrpc/mod.rs index 09298d9..9e6d627 100644 --- a/src/xrpc/mod.rs +++ b/src/xrpc/mod.rs @@ -2,7 +2,9 @@ mod procedure; mod query; use axum::Json; +use axum::body::Body; use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; use axum::response::Response; use std::collections::HashMap; @@ -10,6 +12,64 @@ use crate::AppState; use crate::auth::Claims; use crate::error::AppError; use crate::lexicon::LexiconType; +use crate::resolve::resolve_nsid_authority; + +/// Proxy an unrecognized XRPC method to its home AppView resolved via DNS. +async fn proxy_to_authority( + state: &AppState, + method: &str, + query_string: &str, + body: Option<&serde_json::Value>, +) -> Result { + let (_did, pds_endpoint) = resolve_nsid_authority(&state.http, &state.config.plc_url, method) + .await + .map_err(|e| { + AppError::BadGateway(format!("failed to resolve authority for {method}: {e}")) + })?; + + let mut url = format!("{}/xrpc/{method}", pds_endpoint.trim_end_matches('/'),); + if !query_string.is_empty() { + url.push('?'); + url.push_str(query_string); + } + + let request = if let Some(json_body) = body { + state.http.post(&url).json(json_body) + } else { + state.http.get(&url) + }; + + let upstream = request + .send() + .await + .map_err(|e| AppError::BadGateway(format!("upstream request failed for {method}: {e}")))?; + + let status = + StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); + + let content_type = upstream + .headers() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/json") + .to_string(); + + let bytes = upstream.bytes().await.map_err(|e| { + AppError::BadGateway(format!( + "failed to read upstream response for {method}: {e}" + )) + })?; + + if !status.is_success() { + return Err(AppError::PdsError(status, bytes)); + } + + Ok(Response::builder() + .status(status) + .header("content-type", content_type) + .body(Body::from(bytes)) + .unwrap()) +} /// Catch-all GET handler for XRPC queries. pub async fn xrpc_get( @@ -17,11 +77,17 @@ pub async fn xrpc_get( Path(method): Path, Query(params): Query>, ) -> Result { - let lexicon = state - .lexicons - .get(&method) - .await - .ok_or_else(|| AppError::BadRequest(format!("method not found: {method}")))?; + let lexicon = match state.lexicons.get(&method).await { + Some(l) => l, + None => { + let query_string: String = params + .iter() + .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v))) + .collect::>() + .join("&"); + return proxy_to_authority(&state, &method, &query_string, None).await; + } + }; if lexicon.lexicon_type != LexiconType::Query { return Err(AppError::BadRequest(format!( @@ -39,11 +105,10 @@ pub async fn xrpc_post( claims: Claims, Json(body): Json, ) -> Result { - let lexicon = state - .lexicons - .get(&method) - .await - .ok_or_else(|| AppError::BadRequest(format!("method not found: {method}")))?; + let lexicon = match state.lexicons.get(&method).await { + Some(l) => l, + None => return proxy_to_authority(&state, &method, "", Some(&body)).await, + }; if lexicon.lexicon_type != LexiconType::Procedure { return Err(AppError::BadRequest(format!( -- 2.51.2