this repo has no description

Fix regression with appview proxying

lewis a050e405 f7cc1a3c

+13 -13
TODO.md
··· 53 53 - [ ] Rate limit 2FA attempts 54 54 - [ ] Re-auth for sensitive actions (email change, adding new auth methods) 55 55 56 - ### Private/encrypted data 57 - Records that only authorized parties can see and decrypt. Requires key federation between PDSes. 58 - 59 - - [ ] Survey current ATProto discourse on private data 60 - - [ ] Document Bluesky team's likely approach 61 - - [ ] Design key management strategy 62 - - [ ] Per-user encryption keys (separate from signing keys) 63 - - [ ] Key derivation for per-record or per-collection encryption 64 - - [ ] Encrypted record storage format 65 - - [ ] Transparent encryption/decryption in repo operations 66 - - [ ] Protocol for sharing decryption keys between PDSes 67 - - [ ] Handle key rotation and revocation 68 - 69 56 ### Plugin system 70 57 Extensible architecture allowing third-party plugins to add functionality, like minecraft mods or browser extensions. 71 58 ··· 81 68 - [ ] Plugin SDK crate with traits and helpers 82 69 - [ ] Example plugins: custom feed algorithm, content filter, S3 backup 83 70 - [ ] Plugin registry with signature verification and version compatibility 71 + 72 + ### Plugin: Private/encrypted data 73 + Records that only authorized parties can see and decrypt. Requires key federation between PDSes. Implemented as a plugin using the plugin system above. 74 + 75 + - [ ] Survey current ATProto discourse on private data 76 + - [ ] Document Bluesky team's likely approach 77 + - [ ] Design key management strategy 78 + - [ ] Per-user encryption keys (separate from signing keys) 79 + - [ ] Key derivation for per-record or per-collection encryption 80 + - [ ] Encrypted record storage format 81 + - [ ] Transparent encryption/decryption in repo operations 82 + - [ ] Protocol for sharing decryption keys between PDSes 83 + - [ ] Handle key rotation and revocation 84 84 85 85 --- 86 86
-1
docker-compose.prod.yml
··· 21 21 JWT_SECRET: "${JWT_SECRET:?JWT_SECRET is required (min 32 chars)}" 22 22 DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}" 23 23 MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}" 24 - APPVIEW_URL: "${APPVIEW_URL:-https://api.bsky.app}" 25 24 CRAWLERS: "${CRAWLERS:-https://bsky.network}" 26 25 FRONTEND_DIR: "/app/frontend/dist" 27 26 depends_on:
+1 -1
docs/install-alpine.md
··· 141 141 . /etc/bspds/bspds.env 142 142 export SERVER_HOST SERVER_PORT PDS_HOSTNAME DATABASE_URL 143 143 export S3_ENDPOINT AWS_REGION S3_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY 144 - export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY APPVIEW_URL CRAWLERS 144 + export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY CRAWLERS 145 145 } 146 146 EOF 147 147 chmod +x /etc/init.d/bspds
-1
docs/install-kubernetes.md
··· 15 15 - `VALKEY_URL` - redis:// connection string 16 16 - `PDS_HOSTNAME` - your PDS hostname (without protocol) 17 17 - `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48` 18 - - `APPVIEW_URL` - typically `https://api.bsky.app` 19 18 - `CRAWLERS` - typically `https://bsky.network` 20 19 and more, check the .env.example. 21 20
-1
scripts/install-debian.sh
··· 389 389 DPOP_SECRET=${DPOP_SECRET} 390 390 MASTER_KEY=${MASTER_KEY} 391 391 PLC_DIRECTORY_URL=https://plc.directory 392 - APPVIEW_URL=https://api.bsky.app 393 392 CRAWLERS=https://bsky.network 394 393 AVAILABLE_USER_DOMAINS=${PDS_DOMAIN} 395 394 MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN}
+61 -2
src/api/repo/record/read.rs
··· 1 + use crate::api::proxy_client::proxy_client; 1 2 use crate::state::AppState; 2 3 use axum::{ 3 4 Json, 4 5 extract::{Query, State}, 5 - http::StatusCode, 6 + http::{HeaderMap, StatusCode}, 6 7 response::{IntoResponse, Response}, 7 8 }; 8 9 use cid::Cid; ··· 11 12 use serde_json::json; 12 13 use std::collections::HashMap; 13 14 use std::str::FromStr; 14 - use tracing::error; 15 + use tracing::{error, info}; 15 16 16 17 #[derive(Deserialize)] 17 18 pub struct GetRecordInput { ··· 23 24 24 25 pub async fn get_record( 25 26 State(state): State<AppState>, 27 + headers: HeaderMap, 26 28 Query(input): Query<GetRecordInput>, 27 29 ) -> Response { 28 30 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); ··· 46 48 let user_id: uuid::Uuid = match user_id_opt { 47 49 Ok(Some(id)) => id, 48 50 Ok(None) => { 51 + if let Some(proxy_header) = headers 52 + .get("atproto-proxy") 53 + .and_then(|h| h.to_str().ok()) 54 + { 55 + let did = proxy_header.split('#').next().unwrap_or(proxy_header); 56 + if let Some(resolved) = state.did_resolver.resolve_did(did).await { 57 + let mut url = format!( 58 + "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection={}&rkey={}", 59 + resolved.url.trim_end_matches('/'), 60 + urlencoding::encode(&input.repo), 61 + urlencoding::encode(&input.collection), 62 + urlencoding::encode(&input.rkey) 63 + ); 64 + if let Some(cid) = &input.cid { 65 + url.push_str(&format!("&cid={}", urlencoding::encode(cid))); 66 + } 67 + info!("Proxying getRecord to {}: {}", did, url); 68 + match proxy_client().get(&url).send().await { 69 + Ok(resp) => { 70 + let status = resp.status(); 71 + let body = match resp.bytes().await { 72 + Ok(b) => b, 73 + Err(e) => { 74 + error!("Error reading proxy response: {:?}", e); 75 + return ( 76 + StatusCode::BAD_GATEWAY, 77 + Json(json!({"error": "UpstreamFailure", "message": "Error reading upstream response"})), 78 + ) 79 + .into_response(); 80 + } 81 + }; 82 + return Response::builder() 83 + .status(status) 84 + .header("content-type", "application/json") 85 + .body(axum::body::Body::from(body)) 86 + .unwrap_or_else(|_| { 87 + (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response() 88 + }); 89 + } 90 + Err(e) => { 91 + error!("Error proxying request: {:?}", e); 92 + return ( 93 + StatusCode::BAD_GATEWAY, 94 + Json(json!({"error": "UpstreamFailure", "message": "Failed to reach upstream service"})), 95 + ) 96 + .into_response(); 97 + } 98 + } 99 + } else { 100 + error!("Could not resolve DID from atproto-proxy header: {}", did); 101 + return ( 102 + StatusCode::BAD_GATEWAY, 103 + Json(json!({"error": "UpstreamFailure", "message": "Could not resolve proxy DID"})), 104 + ) 105 + .into_response(); 106 + } 107 + } 49 108 return ( 50 109 StatusCode::NOT_FOUND, 51 110 Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),