Fix regression with appview proxying

This commit is contained in:
lewis
2025-12-18 21:30:10 +02:00
parent e929cf5af5
commit 27d794d46f
6 changed files with 75 additions and 19 deletions
+13 -13
View File
@@ -53,19 +53,6 @@ Modern passwordless authentication using WebAuthn/FIDO2, plus TOTP for defense i
- [ ] Rate limit 2FA attempts
- [ ] Re-auth for sensitive actions (email change, adding new auth methods)
### Private/encrypted data
Records that only authorized parties can see and decrypt. Requires key federation between PDSes.
- [ ] Survey current ATProto discourse on private data
- [ ] Document Bluesky team's likely approach
- [ ] Design key management strategy
- [ ] Per-user encryption keys (separate from signing keys)
- [ ] Key derivation for per-record or per-collection encryption
- [ ] Encrypted record storage format
- [ ] Transparent encryption/decryption in repo operations
- [ ] Protocol for sharing decryption keys between PDSes
- [ ] Handle key rotation and revocation
### Plugin system
Extensible architecture allowing third-party plugins to add functionality, like minecraft mods or browser extensions.
@@ -82,6 +69,19 @@ Extensible architecture allowing third-party plugins to add functionality, like
- [ ] Example plugins: custom feed algorithm, content filter, S3 backup
- [ ] Plugin registry with signature verification and version compatibility
### Plugin: Private/encrypted data
Records that only authorized parties can see and decrypt. Requires key federation between PDSes. Implemented as a plugin using the plugin system above.
- [ ] Survey current ATProto discourse on private data
- [ ] Document Bluesky team's likely approach
- [ ] Design key management strategy
- [ ] Per-user encryption keys (separate from signing keys)
- [ ] Key derivation for per-record or per-collection encryption
- [ ] Encrypted record storage format
- [ ] Transparent encryption/decryption in repo operations
- [ ] Protocol for sharing decryption keys between PDSes
- [ ] Handle key rotation and revocation
---
## Completed
-1
View File
@@ -21,7 +21,6 @@ services:
JWT_SECRET: "${JWT_SECRET:?JWT_SECRET is required (min 32 chars)}"
DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}"
MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}"
APPVIEW_URL: "${APPVIEW_URL:-https://api.bsky.app}"
CRAWLERS: "${CRAWLERS:-https://bsky.network}"
FRONTEND_DIR: "/app/frontend/dist"
depends_on:
+1 -1
View File
@@ -141,7 +141,7 @@ start_pre() {
. /etc/bspds/bspds.env
export SERVER_HOST SERVER_PORT PDS_HOSTNAME DATABASE_URL
export S3_ENDPOINT AWS_REGION S3_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY APPVIEW_URL CRAWLERS
export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY CRAWLERS
}
EOF
chmod +x /etc/init.d/bspds
-1
View File
@@ -15,7 +15,6 @@ The container image expects:
- `VALKEY_URL` - redis:// connection string
- `PDS_HOSTNAME` - your PDS hostname (without protocol)
- `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48`
- `APPVIEW_URL` - typically `https://api.bsky.app`
- `CRAWLERS` - typically `https://bsky.network`
and more, check the .env.example.
-1
View File
@@ -389,7 +389,6 @@ JWT_SECRET=${JWT_SECRET}
DPOP_SECRET=${DPOP_SECRET}
MASTER_KEY=${MASTER_KEY}
PLC_DIRECTORY_URL=https://plc.directory
APPVIEW_URL=https://api.bsky.app
CRAWLERS=https://bsky.network
AVAILABLE_USER_DOMAINS=${PDS_DOMAIN}
MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN}
+61 -2
View File
@@ -1,8 +1,9 @@
use crate::api::proxy_client::proxy_client;
use crate::state::AppState;
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use cid::Cid;
@@ -11,7 +12,7 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::str::FromStr;
use tracing::error;
use tracing::{error, info};
#[derive(Deserialize)]
pub struct GetRecordInput {
@@ -23,6 +24,7 @@ pub struct GetRecordInput {
pub async fn get_record(
State(state): State<AppState>,
headers: HeaderMap,
Query(input): Query<GetRecordInput>,
) -> Response {
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
@@ -46,6 +48,63 @@ pub async fn get_record(
let user_id: uuid::Uuid = match user_id_opt {
Ok(Some(id)) => id,
Ok(None) => {
if let Some(proxy_header) = headers
.get("atproto-proxy")
.and_then(|h| h.to_str().ok())
{
let did = proxy_header.split('#').next().unwrap_or(proxy_header);
if let Some(resolved) = state.did_resolver.resolve_did(did).await {
let mut url = format!(
"{}/xrpc/com.atproto.repo.getRecord?repo={}&collection={}&rkey={}",
resolved.url.trim_end_matches('/'),
urlencoding::encode(&input.repo),
urlencoding::encode(&input.collection),
urlencoding::encode(&input.rkey)
);
if let Some(cid) = &input.cid {
url.push_str(&format!("&cid={}", urlencoding::encode(cid)));
}
info!("Proxying getRecord to {}: {}", did, url);
match proxy_client().get(&url).send().await {
Ok(resp) => {
let status = resp.status();
let body = match resp.bytes().await {
Ok(b) => b,
Err(e) => {
error!("Error reading proxy response: {:?}", e);
return (
StatusCode::BAD_GATEWAY,
Json(json!({"error": "UpstreamFailure", "message": "Error reading upstream response"})),
)
.into_response();
}
};
return Response::builder()
.status(status)
.header("content-type", "application/json")
.body(axum::body::Body::from(body))
.unwrap_or_else(|_| {
(StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
});
}
Err(e) => {
error!("Error proxying request: {:?}", e);
return (
StatusCode::BAD_GATEWAY,
Json(json!({"error": "UpstreamFailure", "message": "Failed to reach upstream service"})),
)
.into_response();
}
}
} else {
error!("Could not resolve DID from atproto-proxy header: {}", did);
return (
StatusCode::BAD_GATEWAY,
Json(json!({"error": "UpstreamFailure", "message": "Could not resolve proxy DID"})),
)
.into_response();
}
}
return (
StatusCode::NOT_FOUND,
Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),