More local methods to not proxy

This commit is contained in:
lewis
2026-01-04 18:38:15 +02:00
parent 3954189c22
commit 938a9841b6
8 changed files with 187 additions and 15 deletions
Generated
+21
View File
@@ -1109,6 +1109,17 @@ dependencies = [
"slab",
]
[[package]]
name = "cfb"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
"uuid",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -3005,6 +3016,15 @@ dependencies = [
"rustversion",
]
[[package]]
name = "infer"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
dependencies = [
"cfb",
]
[[package]]
name = "inout"
version = "0.1.4"
@@ -6323,6 +6343,7 @@ dependencies = [
"hmac",
"http 1.4.0",
"image",
"infer",
"ipld-core",
"iroh-car",
"jacquard",
+1
View File
@@ -22,6 +22,7 @@ governor = "0.10"
hex = "0.4"
hkdf = "0.12"
hmac = "0.12"
infer = "0.19"
aes-gcm = "0.10"
jacquard = { version = "0.9.5", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] }
jacquard-axum = "0.9.6"
@@ -227,6 +227,28 @@ export class AtprotoClient {
});
}
async getBlobWithContentType(
did: string,
cid: string,
): Promise<{ data: Uint8Array; contentType: string }> {
const url = `${this.baseUrl}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
const headers: Record<string, string> = {};
if (this.accessToken) {
headers["Authorization"] = `Bearer ${this.accessToken}`;
}
const res = await fetch(url, { headers });
if (!res.ok) {
const err = await res.json().catch(() => ({
error: "Unknown",
message: res.statusText,
}));
throw new Error(err.message || err.error || res.statusText);
}
const contentType = res.headers.get("content-type") || "application/octet-stream";
const data = new Uint8Array(await res.arrayBuffer());
return { data, contentType };
}
async uploadBlob(
data: Uint8Array,
mimeType: string,
+5 -3
View File
@@ -87,15 +87,17 @@ export async function migrateBlobs(
});
console.log("[blob-migration] Fetching blob", cid, "from source");
const blobData = await sourceClient.getBlob(userDid, cid);
const { data: blobData, contentType } = await sourceClient.getBlobWithContentType(userDid, cid);
console.log(
"[blob-migration] Got blob",
cid,
"size:",
blobData.byteLength,
"contentType:",
contentType,
);
await localClient.uploadBlob(blobData, "application/octet-stream");
console.log("[blob-migration] Uploaded blob", cid);
await localClient.uploadBlob(blobData, contentType);
console.log("[blob-migration] Uploaded blob", cid, "with contentType:", contentType);
migrated++;
onProgress({ blobsMigrated: migrated });
} catch (e) {
+1 -1
View File
@@ -480,7 +480,7 @@ impl From<crate::auth::extractor::AuthError> for ApiError {
Self::AuthenticationFailed(None)
}
crate::auth::extractor::AuthError::TokenExpired => {
Self::AuthenticationFailed(Some("Token has expired".to_string()))
Self::ExpiredToken(Some("Token has expired".to_string()))
}
crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated,
crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown,
+76 -7
View File
@@ -15,22 +15,87 @@ use tower::{Service, util::BoxCloneSyncService};
use tracing::{error, info, warn};
const PROTECTED_METHODS: &[&str] = &[
"app.bsky.actor.getPreferences",
"app.bsky.actor.putPreferences",
"com.atproto.admin.deleteAccount",
"com.atproto.admin.disableAccountInvites",
"com.atproto.admin.disableInviteCodes",
"com.atproto.admin.enableAccountInvites",
"com.atproto.admin.getAccountInfo",
"com.atproto.admin.getAccountInfos",
"com.atproto.admin.getInviteCodes",
"com.atproto.admin.getSubjectStatus",
"com.atproto.admin.searchAccounts",
"com.atproto.admin.sendEmail",
"com.atproto.admin.updateAccountEmail",
"com.atproto.admin.updateAccountHandle",
"com.atproto.admin.updateAccountPassword",
"com.atproto.admin.updateSubjectStatus",
"com.atproto.identity.getRecommendedDidCredentials",
"com.atproto.identity.requestPlcOperationSignature",
"com.atproto.identity.signPlcOperation",
"com.atproto.identity.submitPlcOperation",
"com.atproto.identity.updateHandle",
"com.atproto.repo.applyWrites",
"com.atproto.repo.createRecord",
"com.atproto.repo.deleteRecord",
"com.atproto.repo.importRepo",
"com.atproto.repo.putRecord",
"com.atproto.repo.uploadBlob",
"com.atproto.server.activateAccount",
"com.atproto.server.checkAccountStatus",
"com.atproto.server.confirmEmail",
"com.atproto.server.confirmSignup",
"com.atproto.server.createAccount",
"com.atproto.server.createAppPassword",
"com.atproto.server.createInviteCode",
"com.atproto.server.createInviteCodes",
"com.atproto.server.createSession",
"com.atproto.server.createTotpSecret",
"com.atproto.server.deactivateAccount",
"com.atproto.server.deleteAccount",
"com.atproto.server.deletePasskey",
"com.atproto.server.deleteSession",
"com.atproto.server.describeServer",
"com.atproto.server.disableTotp",
"com.atproto.server.enableTotp",
"com.atproto.server.finishPasskeyRegistration",
"com.atproto.server.getAccountInviteCodes",
"com.atproto.server.getServiceAuth",
"com.atproto.server.getSession",
"com.atproto.server.getTotpStatus",
"com.atproto.server.listAppPasswords",
"com.atproto.server.listPasskeys",
"com.atproto.server.refreshSession",
"com.atproto.server.regenerateBackupCodes",
"com.atproto.server.requestAccountDelete",
"com.atproto.server.requestEmailConfirmation",
"com.atproto.server.requestEmailUpdate",
"com.atproto.server.requestPasswordReset",
"com.atproto.server.resendMigrationVerification",
"com.atproto.server.resendVerification",
"com.atproto.server.reserveSigningKey",
"com.atproto.server.resetPassword",
"com.atproto.server.revokeAppPassword",
"com.atproto.server.startPasskeyRegistration",
"com.atproto.server.updateEmail",
"com.atproto.server.updatePasskey",
"com.atproto.server.verifyMigrationEmail",
"com.atproto.sync.getBlob",
"com.atproto.sync.getBlocks",
"com.atproto.sync.getCheckout",
"com.atproto.sync.getHead",
"com.atproto.sync.getLatestCommit",
"com.atproto.sync.getRecord",
"com.atproto.sync.getRepo",
"com.atproto.sync.getRepoStatus",
"com.atproto.sync.listBlobs",
"com.atproto.sync.listRepos",
"com.atproto.sync.notifyOfUpdate",
"com.atproto.sync.requestCrawl",
"com.atproto.sync.subscribeRepos",
"com.atproto.temp.checkSignupQueue",
"com.atproto.temp.dereferenceScope",
];
fn is_protected_method(method: &str) -> bool {
@@ -89,13 +154,17 @@ impl<S: Service<Request, Response = Response, Error = Infallible>> Service<Reque
.headers()
.contains_key(http::HeaderName::from(jacquard::xrpc::Header::AtprotoProxy))
{
// If the age assurance override is set and this is an age assurance call then we dont want to proxy even if the client requests it.
if !std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err()
&& (req.uri().path().ends_with("app.bsky.ageassurance.getState")
|| req
.uri()
.path()
.ends_with("app.bsky.unspecced.getAgeAssuranceState"))
let path = req.uri().path();
let method = path.trim_start_matches("/");
if is_protected_method(method) {
return Either::Right(self.inner.call(req));
}
// If the age assurance override is set and this is an age assurance call then we dont want to proxy even if the client requests it
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok()
&& (path.ends_with("app.bsky.ageassurance.getState")
|| path.ends_with("app.bsky.unspecced.getAgeAssuranceState"))
{
return Either::Right(self.inner.call(req));
}
+31 -4
View File
@@ -17,7 +17,27 @@ use multihash::Multihash;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::pin::Pin;
use tracing::{debug, error, info};
use tracing::{debug, error, info, warn};
fn detect_mime_type(data: &[u8], client_hint: &str) -> String {
if let Some(kind) = infer::get(data) {
let detected = kind.mime_type().to_string();
if detected != client_hint {
debug!(
"MIME type detection: client sent '{}', detected '{}'",
client_hint, detected
);
}
detected
} else {
if client_hint == "*/*" || client_hint.is_empty() {
warn!("Could not detect MIME type and client sent invalid hint: '{}'", client_hint);
"application/octet-stream".to_string()
} else {
client_hint.to_string()
}
}
}
pub async fn upload_blob(
State(state): State<AppState>,
@@ -91,11 +111,10 @@ pub async fn upload_blob(
return ApiError::Forbidden.into_response();
}
let mime_type = headers
let client_mime_hint = headers
.get("content-type")
.and_then(|h| h.to_str().ok())
.unwrap_or("application/octet-stream")
.to_string();
.unwrap_or("application/octet-stream");
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
@@ -136,6 +155,14 @@ pub async fn upload_blob(
.into_response();
}
let mime_type = match state.blob_store.get_head(&temp_key, 8192).await {
Ok(head_bytes) => detect_mime_type(&head_bytes, client_mime_hint),
Err(e) => {
warn!("Failed to read blob head for MIME detection: {:?}", e);
client_mime_hint.to_string()
}
};
let multihash = match Multihash::wrap(0x12, &upload_result.sha256_hash) {
Ok(mh) => mh,
Err(e) => {
+30
View File
@@ -34,6 +34,7 @@ pub trait BlobStorage: Send + Sync {
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError>;
async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError>;
async fn get_bytes(&self, key: &str) -> Result<Bytes, StorageError>;
async fn get_head(&self, key: &str, size: usize) -> Result<Bytes, StorageError>;
async fn delete(&self, key: &str) -> Result<(), StorageError>;
async fn put_stream(
&self,
@@ -233,6 +234,35 @@ impl BlobStorage for S3BlobStorage {
Ok(data)
}
async fn get_head(&self, key: &str, size: usize) -> Result<Bytes, StorageError> {
let range = format!("bytes=0-{}", size.saturating_sub(1));
let resp = self
.client
.get_object()
.bucket(&self.bucket)
.key(key)
.range(range)
.send()
.await
.map_err(|e| {
crate::metrics::record_s3_operation("get_head", "error");
StorageError::S3(e.to_string())
})?;
let data = resp
.body
.collect()
.await
.map_err(|e| {
crate::metrics::record_s3_operation("get_head", "error");
StorageError::S3(e.to_string())
})?
.into_bytes();
crate::metrics::record_s3_operation("get_head", "success");
Ok(data)
}
async fn delete(&self, key: &str) -> Result<(), StorageError> {
let result = self
.client