+4
Cargo.lock
+4
Cargo.lock
···
6315
"dotenvy",
6316
"ed25519-dalek",
6317
"futures",
6318
"governor",
6319
"hex",
6320
"hickory-resolver",
6321
"hkdf",
6322
"hmac",
6323
"image",
6324
"ipld-core",
6325
"iroh-car",
···
6352
"tokio",
6353
"tokio-tungstenite",
6354
"totp-rs",
6355
"tower-http",
6356
"tracing",
6357
"tracing-subscriber",
6358
"urlencoding",
···
6315
"dotenvy",
6316
"ed25519-dalek",
6317
"futures",
6318
+
"futures-util",
6319
"governor",
6320
"hex",
6321
"hickory-resolver",
6322
"hkdf",
6323
"hmac",
6324
+
"http 1.4.0",
6325
"image",
6326
"ipld-core",
6327
"iroh-car",
···
6354
"tokio",
6355
"tokio-tungstenite",
6356
"totp-rs",
6357
+
"tower",
6358
"tower-http",
6359
+
"tower-layer",
6360
"tracing",
6361
"tracing-subscriber",
6362
"urlencoding",
+4
Cargo.toml
+4
Cargo.toml
···
64
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] }
65
webauthn-rs-proto = "0.5.4"
66
zip = { version = "7.0.0", default-features = false, features = ["deflate"] }
67
[features]
68
external-infra = []
69
[dev-dependencies]
···
64
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] }
65
webauthn-rs-proto = "0.5.4"
66
zip = { version = "7.0.0", default-features = false, features = ["deflate"] }
67
+
tower = "0.5.2"
68
+
tower-layer = "0.3.3"
69
+
futures-util = "0.3.31"
70
+
http = "1.4.0"
71
[features]
72
external-infra = []
73
[dev-dependencies]
+4
-55
src/api/age_assurance.rs
+4
-55
src/api/age_assurance.rs
···
2
use crate::state::AppState;
3
use axum::{
4
Json,
5
-
body::Bytes,
6
-
extract::{Path, RawQuery, State},
7
-
http::{HeaderMap, Method, StatusCode},
8
response::{IntoResponse, Response},
9
};
10
use serde_json::json;
11
12
-
pub async fn get_state(
13
-
State(state): State<AppState>,
14
-
headers: HeaderMap,
15
-
RawQuery(query): RawQuery,
16
-
) -> Response {
17
-
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() {
18
-
return proxy_to_appview(state, headers, "app.bsky.ageassurance.getState", query).await;
19
-
}
20
-
21
let created_at = get_account_created_at(&state, &headers).await;
22
let now = chrono::Utc::now().to_rfc3339();
23
···
37
.into_response()
38
}
39
40
-
pub async fn get_age_assurance_state(
41
-
State(state): State<AppState>,
42
-
headers: HeaderMap,
43
-
RawQuery(query): RawQuery,
44
-
) -> Response {
45
-
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() {
46
-
return proxy_to_appview(
47
-
state,
48
-
headers,
49
-
"app.bsky.unspecced.getAgeAssuranceState",
50
-
query,
51
-
)
52
-
.await;
53
-
}
54
-
55
(StatusCode::OK, Json(json!({"status": "assured"}))).into_response()
56
}
57
···
89
90
row.map(|r| r.created_at.to_rfc3339())
91
}
92
-
93
-
async fn proxy_to_appview(
94
-
state: AppState,
95
-
headers: HeaderMap,
96
-
method: &str,
97
-
query: Option<String>,
98
-
) -> Response {
99
-
if headers.get("atproto-proxy").is_none() {
100
-
return (
101
-
StatusCode::BAD_REQUEST,
102
-
Json(json!({
103
-
"error": "InvalidRequest",
104
-
"message": "Missing required atproto-proxy header"
105
-
})),
106
-
)
107
-
.into_response();
108
-
}
109
-
110
-
crate::api::proxy::proxy_handler(
111
-
State(state),
112
-
Path(method.to_string()),
113
-
Method::GET,
114
-
headers,
115
-
RawQuery(query),
116
-
Bytes::new(),
117
-
)
118
-
.await
119
-
}
···
2
use crate::state::AppState;
3
use axum::{
4
Json,
5
+
extract::State,
6
+
http::{HeaderMap, StatusCode},
7
response::{IntoResponse, Response},
8
};
9
use serde_json::json;
10
11
+
pub async fn get_state(State(state): State<AppState>, headers: HeaderMap) -> Response {
12
let created_at = get_account_created_at(&state, &headers).await;
13
let now = chrono::Utc::now().to_rfc3339();
14
···
28
.into_response()
29
}
30
31
+
pub async fn get_age_assurance_state() -> Response {
32
(StatusCode::OK, Json(json!({"status": "assured"}))).into_response()
33
}
34
···
66
67
row.map(|r| r.created_at.to_rfc3339())
68
}
+80
-3
src/api/proxy.rs
+80
-3
src/api/proxy.rs
···
1
use crate::api::proxy_client::proxy_client;
2
use crate::state::AppState;
3
use axum::{
4
Json,
5
body::Bytes,
6
-
extract::{Path, RawQuery, State},
7
http::{HeaderMap, Method, StatusCode},
8
response::{IntoResponse, Response},
9
};
10
use serde_json::json;
11
use tracing::{error, info, warn};
12
13
const PROTECTED_METHODS: &[&str] = &[
···
33
PROTECTED_METHODS.contains(&method)
34
}
35
36
-
pub async fn proxy_handler(
37
State(state): State<AppState>,
38
-
Path(method): Path<String>,
39
method_verb: Method,
40
headers: HeaderMap,
41
RawQuery(query): RawQuery,
42
body: Bytes,
43
) -> Response {
44
if is_protected_method(&method) {
45
warn!(method = %method, "Attempted to proxy protected method");
46
return (
···
1
+
use std::convert::Infallible;
2
+
3
use crate::api::proxy_client::proxy_client;
4
use crate::state::AppState;
5
use axum::{
6
Json,
7
body::Bytes,
8
+
extract::{RawQuery, Request, State},
9
+
handler::Handler,
10
http::{HeaderMap, Method, StatusCode},
11
response::{IntoResponse, Response},
12
};
13
+
use futures_util::future::Either;
14
use serde_json::json;
15
+
use tower::{Service, util::BoxCloneSyncService};
16
use tracing::{error, info, warn};
17
18
const PROTECTED_METHODS: &[&str] = &[
···
38
PROTECTED_METHODS.contains(&method)
39
}
40
41
+
pub struct XrpcProxyLayer {
42
+
state: AppState,
43
+
}
44
+
45
+
impl XrpcProxyLayer {
46
+
pub fn new(state: AppState) -> Self {
47
+
XrpcProxyLayer { state }
48
+
}
49
+
}
50
+
51
+
impl<S> tower_layer::Layer<S> for XrpcProxyLayer {
52
+
type Service = XrpcProxyingService<S>;
53
+
54
+
fn layer(&self, inner: S) -> Self::Service {
55
+
XrpcProxyingService {
56
+
inner,
57
+
// TODO(nel): make our own service here instead of boxing a HandlerService
58
+
handler: BoxCloneSyncService::new(proxy_handler.with_state(self.state.clone())),
59
+
}
60
+
}
61
+
}
62
+
63
+
#[derive(Clone)]
64
+
pub struct XrpcProxyingService<S> {
65
+
inner: S,
66
+
handler: BoxCloneSyncService<Request, Response, Infallible>,
67
+
}
68
+
69
+
impl<S: Service<Request, Response = Response, Error = Infallible>> Service<Request>
70
+
for XrpcProxyingService<S>
71
+
{
72
+
type Response = Response;
73
+
74
+
type Error = Infallible;
75
+
76
+
type Future = Either<
77
+
<BoxCloneSyncService<Request, Response, Infallible> as Service<Request>>::Future,
78
+
S::Future,
79
+
>;
80
+
81
+
fn poll_ready(
82
+
&mut self,
83
+
cx: &mut std::task::Context<'_>,
84
+
) -> std::task::Poll<Result<(), Self::Error>> {
85
+
self.inner.poll_ready(cx)
86
+
}
87
+
88
+
fn call(&mut self, req: Request) -> Self::Future {
89
+
if req
90
+
.headers()
91
+
.contains_key(http::HeaderName::from(jacquard::xrpc::Header::AtprotoProxy))
92
+
{
93
+
// 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.
94
+
if !std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err()
95
+
&& (req.uri().path().ends_with("app.bsky.ageassurance.getState")
96
+
|| req
97
+
.uri()
98
+
.path()
99
+
.ends_with("app.bsky.unspecced.getAgeAssuranceState"))
100
+
{
101
+
return Either::Right(self.inner.call(req));
102
+
}
103
+
104
+
Either::Left(self.handler.call(req))
105
+
} else {
106
+
Either::Right(self.inner.call(req))
107
+
}
108
+
}
109
+
}
110
+
111
+
async fn proxy_handler(
112
State(state): State<AppState>,
113
+
uri: http::Uri,
114
method_verb: Method,
115
headers: HeaderMap,
116
RawQuery(query): RawQuery,
117
body: Bytes,
118
) -> Response {
119
+
// This layer is nested under /xrpc in an axum router so the extracted uri will look like /<method> and thus we can just strip the /
120
+
let method = uri.path().trim_start_matches("/");
121
if is_protected_method(&method) {
122
warn!(method = %method, "Attempted to proxy protected method");
123
return (
+1
-57
src/api/repo/record/read.rs
+1
-57
src/api/repo/record/read.rs
···
1
-
use crate::api::proxy_client::proxy_client;
2
use crate::state::AppState;
3
use axum::{
4
Json,
···
14
use serde_json::{Map, Value, json};
15
use std::collections::HashMap;
16
use std::str::FromStr;
17
-
use tracing::{error, info};
18
19
fn ipld_to_json(ipld: Ipld) -> Value {
20
match ipld {
···
78
let user_id: uuid::Uuid = match user_id_opt {
79
Ok(Some(id)) => id,
80
Ok(None) => {
81
-
if let Some(proxy_header) = headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) {
82
-
let did = proxy_header.split('#').next().unwrap_or(proxy_header);
83
-
if let Some(resolved) = state.did_resolver.resolve_did(did).await {
84
-
let mut url = format!(
85
-
"{}/xrpc/com.atproto.repo.getRecord?repo={}&collection={}&rkey={}",
86
-
resolved.url.trim_end_matches('/'),
87
-
urlencoding::encode(&input.repo),
88
-
urlencoding::encode(&input.collection),
89
-
urlencoding::encode(&input.rkey)
90
-
);
91
-
if let Some(cid) = &input.cid {
92
-
url.push_str(&format!("&cid={}", urlencoding::encode(cid)));
93
-
}
94
-
info!("Proxying getRecord to {}: {}", did, url);
95
-
match proxy_client().get(&url).send().await {
96
-
Ok(resp) => {
97
-
let status = resp.status();
98
-
let body = match resp.bytes().await {
99
-
Ok(b) => b,
100
-
Err(e) => {
101
-
error!("Error reading proxy response: {:?}", e);
102
-
return (
103
-
StatusCode::BAD_GATEWAY,
104
-
Json(json!({"error": "UpstreamFailure", "message": "Error reading upstream response"})),
105
-
)
106
-
.into_response();
107
-
}
108
-
};
109
-
return Response::builder()
110
-
.status(status)
111
-
.header("content-type", "application/json")
112
-
.body(axum::body::Body::from(body))
113
-
.unwrap_or_else(|_| {
114
-
(StatusCode::INTERNAL_SERVER_ERROR, "Internal error")
115
-
.into_response()
116
-
});
117
-
}
118
-
Err(e) => {
119
-
error!("Error proxying request: {:?}", e);
120
-
return (
121
-
StatusCode::BAD_GATEWAY,
122
-
Json(json!({"error": "UpstreamFailure", "message": "Failed to reach upstream service"})),
123
-
)
124
-
.into_response();
125
-
}
126
-
}
127
-
} else {
128
-
error!("Could not resolve DID from atproto-proxy header: {}", did);
129
-
return (
130
-
StatusCode::BAD_GATEWAY,
131
-
Json(json!({"error": "UpstreamFailure", "message": "Could not resolve proxy DID"})),
132
-
)
133
-
.into_response();
134
-
}
135
-
}
136
return (
137
StatusCode::NOT_FOUND,
138
Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),
···
1
use crate::state::AppState;
2
use axum::{
3
Json,
···
13
use serde_json::{Map, Value, json};
14
use std::collections::HashMap;
15
use std::str::FromStr;
16
+
use tracing::error;
17
18
fn ipld_to_json(ipld: Ipld) -> Value {
19
match ipld {
···
77
let user_id: uuid::Uuid = match user_id_opt {
78
Ok(Some(id)) => id,
79
Ok(None) => {
80
return (
81
StatusCode::NOT_FOUND,
82
Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),
+11
-5
src/lib.rs
+11
-5
src/lib.rs
···
22
pub mod util;
23
pub mod validation;
24
25
use axum::{
26
-
Router,
27
extract::DefaultBodyLimit,
28
http::Method,
29
middleware,
30
-
routing::{any, get, post},
31
};
32
use state::AppState;
33
use tower_http::cors::{Any, CorsLayer};
34
use tower_http::services::{ServeDir, ServeFile};
35
···
494
.route(
495
"/app.bsky.unspecced.getAgeAssuranceState",
496
get(api::age_assurance::get_age_assurance_state),
497
-
)
498
-
.route("/{*method}", any(api::proxy::proxy_handler));
499
500
let oauth_router = Router::new()
501
.route("/jwks", get(oauth::endpoints::oauth_jwks))
···
559
);
560
561
let router = Router::new()
562
-
.nest("/xrpc", xrpc_router)
563
.nest("/oauth", oauth_router)
564
.route("/metrics", get(metrics::metrics_handler))
565
.route("/health", get(api::server::health))
···
22
pub mod util;
23
pub mod validation;
24
25
+
use api::proxy::XrpcProxyLayer;
26
use axum::{
27
+
Json, Router,
28
extract::DefaultBodyLimit,
29
http::Method,
30
middleware,
31
+
routing::{get, post},
32
};
33
+
use http::StatusCode;
34
+
use serde_json::json;
35
use state::AppState;
36
+
use tower::{Layer, ServiceBuilder};
37
use tower_http::cors::{Any, CorsLayer};
38
use tower_http::services::{ServeDir, ServeFile};
39
···
498
.route(
499
"/app.bsky.unspecced.getAgeAssuranceState",
500
get(api::age_assurance::get_age_assurance_state),
501
+
);
502
+
let xrpc_service = ServiceBuilder::new()
503
+
.layer(XrpcProxyLayer::new(state.clone()))
504
+
.service(xrpc_router.with_state(state.clone()));
505
506
let oauth_router = Router::new()
507
.route("/jwks", get(oauth::endpoints::oauth_jwks))
···
565
);
566
567
let router = Router::new()
568
+
.nest_service("/xrpc", xrpc_service)
569
.nest("/oauth", oauth_router)
570
.route("/metrics", get(metrics::metrics_handler))
571
.route("/health", get(api::server::health))