didbot_serve/routes/mod.rs
1//! The router: every route the development server answers.
2
3use std::sync::Arc;
4
5use axum::body::Bytes;
6use axum::extract::rejection::QueryRejection;
7use axum::extract::ws::{close_code, CloseFrame, Message, WebSocket, WebSocketUpgrade};
8use axum::extract::{Query, RawQuery, State};
9use axum::http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
10use axum::response::{IntoResponse, Response};
11use axum::routing::{get, post};
12use axum::{Json, Router};
13use didbot_identity::{canonical_did, ATPROTO_DID_PATH};
14use didbot_pds::{
15 Actor, Delta, Estop, EstopRefusal, Lock, Precondition, ProvisionError, Registry,
16 ServerLifecycle, ServerPolicy,
17};
18use serde::de::DeserializeOwned;
19use serde_json::json;
20use tower_http::cors::{Any, CorsLayer};
21use tower_http::timeout::TimeoutLayer;
22use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, MakeSpan, TraceLayer};
23use tower_http::LatencyUnit;
24use tracing::{debug, info, warn, Level};
25
26use crate::auth;
27use crate::error::ApiError;
28use crate::health::HealthState;
29use crate::oauth::scope::Action;
30use crate::rate_limit::{caller_key, Peer, RateLimiter};
31use crate::subscribe::Repos;
32use crate::wire::{
33 record_uri, refuse_skipped_validation, repeated, require_known_lexicon, AccountSummary,
34 ApplyWritesRequest, CreateRecordRequest, CreateRecordResponse, DeleteRecordRequest,
35 DescribeRepoQuery, DescribeRepoResponse, DescribeServerResponse, DidRequest, GetRecordQuery,
36 GetRecordResponse, GetRepoQuery, LedgerQuery, ListRecordsQuery, ListReposQuery,
37 PutRecordRequest, RecordView, RepoWrite, StatsResponse, SyncGetRecordQuery, WriteResult,
38};
39
40mod accounts;
41mod identity;
42mod oauth;
43mod preferences;
44mod repo;
45mod session;
46mod sync;
47
48pub(crate) use accounts::*;
49pub(crate) use identity::*;
50pub(crate) use oauth::*;
51pub(crate) use preferences::*;
52pub(crate) use repo::*;
53pub(crate) use session::*;
54pub(crate) use sync::*;
55
56/// Session storage, the e-stop latch and the operator credential, bundled so
57/// a caller building [`app_with_auth`] hands them over together.
58///
59/// Separate from [`Registry`] on purpose: neither a session nor an e-stop
60/// engagement is provisioning, and bundling them into the same object-safe
61/// trait every backend must implement would grow that trait for every
62/// deployment whether or not it uses a local stop. The
63/// default is what every other builder here gets: fresh, empty, in-memory
64/// state and no operator credential configured, so `app` keeps behaving
65/// exactly as before this existed.
66#[derive(Clone)]
67pub struct AuthState {
68 /// The local emergency stop.
69 pub estop: Arc<Estop>,
70 /// Signing in as this deployment's operator, when it has one. `None`
71 /// when no `--operator` was given: there is no DID a sign-in could match,
72 /// so the operator routes answer that they are not configured rather
73 /// than offering a sign-in that can only fail. See
74 /// [`crate::operator::OperatorAuth`].
75 pub operator: Option<Arc<crate::operator::OperatorAuth>>,
76 /// The relay an operator may announce this server to. `None` when no
77 /// relay was named, and under a `.localhost` zone regardless; see
78 /// [`crate::relay`].
79 pub relay: Option<crate::relay::RelayTarget>,
80 /// Bounds `GET /dashboard/login` per caller address.
81 ///
82 /// That route takes no credential — signing in is how a caller gets one
83 /// — and answering it costs an identity resolution and a pushed
84 /// authorization request against a host the *caller* named. That is the
85 /// same shape as `POST /oauth/par`, which is already bounded this way,
86 /// and rather more outbound work.
87 pub operator_login_limiter: Arc<RateLimiter>,
88 /// Which `Credential::Disclosure` routes this deployment has closed.
89 /// Defaults to every route open; see `auth::Disclosure`.
90 pub disclosure: auth::Disclosure,
91 /// PAR, `authorize`, `token` and discovery; see [`crate::oauth::OAuthState`].
92 /// Defaults to in-memory stores and every policy hook refusing — see
93 /// that type's own doc.
94 pub oauth: crate::oauth::OAuthState,
95 /// Bounds `bot.did.createAccount` and `bot.did.createSession` calls per
96 /// caller address, the two together; see this module's
97 /// `PROVISION_RATE_LIMIT` and `crate::rate_limit`'s own doc for where
98 /// that address comes from and [`AuthState::trust_forwarded_headers`] for
99 /// when a caller-supplied header may stand in for it.
100 ///
101 /// `authorize` is keyed this way already;
102 /// `createAccount` mints an account — a signing key, a hostname, a blob
103 /// quota, a permanent ledger and firehose entry — and the claim
104 /// `Credential::Attested` checks is verified only after this budget, so
105 /// it needs the same address-keyed budget at least as much as either.
106 /// Both routes read a proof, which can fetch an issuer's keys or a DID
107 /// document before any signature is checked.
108 pub provision_rate_limiter: Arc<RateLimiter>,
109 /// Whether `bot.did.createAccount` with no credential creates beneath
110 /// the deployment's own account; see `tree::prove`.
111 ///
112 /// `false` unless a deployment turns it on: a public deployment creates
113 /// only for a creator that proved itself. `didbot-pds` turns it on
114 /// under `.localhost`, where nothing holds a credential yet, and
115 /// nowhere else.
116 pub unauthenticated_creates: bool,
117 /// How many accounts this deployment may hold before
118 /// `bot.did.createAccount` refuses to mint another; see
119 /// this module's `DEFAULT_MAX_ACCOUNTS` and `create_account`.
120 ///
121 /// Checked against [`didbot_pds::RegistryStats::accounts`] — the same
122 /// count `crate::health`'s periodic line already reports — rather than a
123 /// second counter kept here, so a cap and a dashboard can never disagree
124 /// about what "how many accounts" means.
125 pub max_accounts: u64,
126 /// Whether this deployment answers only callers on this host.
127 ///
128 /// A `.localhost` zone is a development run: `createAccount` mints with
129 /// no attestation claim there, and without a `--data` directory every
130 /// blob is held in memory — so a run reachable from the network hands
131 /// both to anybody sharing it. The listener binds every address because
132 /// that is the only single bind answering both `127.0.0.1` and `::1`
133 /// (see `didbot-pds`'s own comment), so the restriction is here: a
134 /// request whose peer address is not a loopback address is refused
135 /// before it reaches a route.
136 ///
137 /// `false` unless a deployment turns it on, which is the posture a real
138 /// zone wants: a deployment nobody can reach serves no agents.
139 pub loopback_callers_only: bool,
140 /// The `Strict-Transport-Security` header every response carries, if
141 /// any; see [`StrictTransportSecurity`].
142 ///
143 /// `None` unless a deployment turns it on. `didbot-pds` turns it on only
144 /// when it serves a production ACME certificate.
145 pub strict_transport_security: Option<StrictTransportSecurity>,
146 /// Bounds how many never-ending streams — `com.atproto.sync.subscribeRepos`
147 /// — this server holds open at once,
148 /// in total and per caller address; see
149 /// [`crate::subscribers::SubscriberLimit`].
150 ///
151 /// [`crate::MAX_CONCURRENT_REQUESTS`] does not cover these: its permit
152 /// is released when the response head is sent, which for a stream is
153 /// before the stream has carried a single frame.
154 pub subscriber_limit: Arc<crate::subscribers::SubscriberLimit>,
155 /// Bounds `bot.did.parkKey` and `describeParkedKey` per caller address;
156 /// see `tree::PARK_RATE_LIMIT`.
157 pub park_rate_limiter: Arc<RateLimiter>,
158 /// Bounds the `com.atproto.sync.*` reads per caller address; see
159 /// `routes::SYNC_READ_LIMIT`. Those reads are unauthenticated and each
160 /// can rebuild and sign a repository.
161 pub sync_rate_limiter: Arc<RateLimiter>,
162 /// Bounds each account's record writes and blob uploads, by operation
163 /// and by request byte; see [`crate::write_meter`].
164 pub write_meter: Arc<crate::write_meter::WriteMeter>,
165 /// Bounds each account's `atproto-proxy` calls, and caches where their
166 /// targets live; see [`crate::ProxyLimits`].
167 pub proxy: Arc<crate::ProxyLimits>,
168 /// The shortest `idleFor` a `neverWrote` account delete takes; see
169 /// [`crate::NEVER_WROTE_MIN_IDLE`].
170 pub never_wrote_min_idle: std::time::Duration,
171 /// Parked keys, spent proofs and issuers' keys; see [`crate::tree`].
172 pub tree: Arc<crate::tree::TreeState>,
173 /// Whether `X-Forwarded-For`/`X-Real-IP` may be trusted as the caller's
174 /// address for every per-address budget this router keeps —
175 /// [`AuthState::provision_rate_limiter`],
176 /// [`crate::oauth::OAuthState::par_rate_limiter`] and
177 /// [`crate::oauth::OAuthState::authorize_rate_limiter`] — instead of the
178 /// TCP peer address `crate::rate_limit::caller_key` gets by default.
179 ///
180 /// `false` unless a deployment turns it on — see `crate::rate_limit`'s
181 /// own doc for why trusting a caller-supplied header by default would
182 /// make every per-address budget bypassable rather than merely
183 /// degraded. Turn this on only once a real reverse proxy sits in front
184 /// of this server and sets whichever header it reads.
185 pub trust_forwarded_headers: bool,
186 /// Where this deployment is in its own lifecycle; see
187 /// [`didbot_pds::ServerLifecycle`] and this module's own
188 /// `require_lifecycle`, which is the single point every capability
189 /// question goes through.
190 ///
191 /// Defaults to [`ServerLifecycle::claimed`] — a server that exists and
192 /// works — because that is what every builder in this crate other than
193 /// `didbot-pds`'s own path is: a test, or a `.localhost` run. A real
194 /// deployment hands in one built by
195 /// [`ServerLifecycle::booting`](didbot_pds::ServerLifecycle::booting) and
196 /// driven by a store read and an operator poll.
197 pub lifecycle: Arc<ServerLifecycle>,
198 /// Where `bot.did.pollOperatorClaim` delivers its "look now" request;
199 /// see [`crate::PollNudge`] and this module's own `poll_operator_claim`.
200 ///
201 /// Defaults to a channel with no poll task attached, which absorbs
202 /// nudges and does nothing — the correct behaviour for a build with no
203 /// operator repository to read.
204 pub nudge: Arc<crate::PollNudge>,
205 /// What the operator poll's last listing of the operator's repository
206 /// read, for the dashboard's provenance panel. Shared with the poll,
207 /// which is the only thing that writes to it; see
208 /// [`crate::OperatorRecords`].
209 pub operator_records: Arc<crate::OperatorRecords>,
210 /// The operator's handle as the operator poll last verified it, for the
211 /// homepage. Shared with the poll, which is the only thing that writes
212 /// to it; see [`crate::OperatorHandle`].
213 pub operator_handle: Arc<crate::OperatorHandle>,
214 /// Where `com.atproto.identity.resolveHandle` looks up a handle no
215 /// account here holds; see [`crate::handles`]. Defaults to the network,
216 /// through Cloudflare's resolver.
217 pub handles: Arc<dyn crate::handles::HandleDid>,
218 /// Bounds those lookups per caller address; see
219 /// `routes::HANDLE_LOOKUP_LIMIT`.
220 pub handle_lookup_limiter: Arc<RateLimiter>,
221 /// Bounds `com.atproto.identity.resolveDid`'s lookups of DIDs hosted
222 /// elsewhere per caller address; see `routes::DID_LOOKUP_LIMIT`.
223 pub did_lookup_limiter: Arc<RateLimiter>,
224 /// The lookups this deployment's policies declare, shared with the
225 /// policy poll, which is the only thing that writes to it; see
226 /// [`crate::DeclaredLookups`].
227 pub declared_lookups: Arc<crate::DeclaredLookups>,
228}
229
230impl Default for AuthState {
231 fn default() -> Self {
232 Self {
233 estop: Arc::new(Estop::default()),
234 operator: None,
235 relay: None,
236 operator_login_limiter: Arc::new(RateLimiter::new(
237 OPERATOR_LOGIN_LIMIT,
238 OPERATOR_LOGIN_WINDOW,
239 )),
240 disclosure: auth::Disclosure::default(),
241 oauth: crate::oauth::OAuthState::default(),
242 provision_rate_limiter: Arc::new(RateLimiter::new(
243 PROVISION_RATE_LIMIT,
244 PROVISION_RATE_WINDOW,
245 )),
246 max_accounts: DEFAULT_MAX_ACCOUNTS,
247 subscriber_limit: Arc::new(crate::subscribers::SubscriberLimit::default()),
248 park_rate_limiter: Arc::new(RateLimiter::new(
249 crate::tree::PARK_RATE_LIMIT,
250 crate::tree::PARK_RATE_WINDOW,
251 )),
252 sync_rate_limiter: Arc::new(RateLimiter::new(SYNC_READ_LIMIT, SYNC_READ_WINDOW)),
253 write_meter: Arc::default(),
254 proxy: Arc::new(crate::ProxyLimits::default()),
255 never_wrote_min_idle: crate::NEVER_WROTE_MIN_IDLE,
256 tree: Arc::new(crate::tree::TreeState::default()),
257 unauthenticated_creates: false,
258 loopback_callers_only: false,
259 strict_transport_security: None,
260 trust_forwarded_headers: false,
261 lifecycle: Arc::new(ServerLifecycle::claimed()),
262 nudge: Arc::new(crate::PollNudge::new()),
263 operator_records: Arc::new(crate::OperatorRecords::new()),
264 operator_handle: Arc::default(),
265 handles: Arc::new(crate::handles::Network::default()),
266 handle_lookup_limiter: Arc::new(RateLimiter::new(
267 HANDLE_LOOKUP_LIMIT,
268 HANDLE_LOOKUP_WINDOW,
269 )),
270 did_lookup_limiter: Arc::new(RateLimiter::new(DID_LOOKUP_LIMIT, DID_LOOKUP_WINDOW)),
271 declared_lookups: Arc::new(crate::DeclaredLookups::new()),
272 }
273 }
274}
275
276/// Everything a handler needs.
277///
278/// `Registry` is deliberately an `Arc<dyn ..>`: the HTTP layer must not be
279/// generic over the provisioner's three type parameters, or every handler
280/// signature grows them.
281#[derive(Clone)]
282pub(crate) struct AppState {
283 pub(crate) registry: Arc<dyn Registry>,
284 repos: Repos,
285 pub(crate) estop: Arc<Estop>,
286 /// See [`AuthState::operator`].
287 pub(crate) operator: Option<Arc<crate::operator::OperatorAuth>>,
288 /// See [`AuthState::relay`].
289 pub(crate) relay: Option<crate::relay::RelayTarget>,
290 /// See [`AuthState::operator_login_limiter`].
291 pub(crate) operator_login_limiter: Arc<RateLimiter>,
292 /// Which `Credential::Disclosure` routes are closed; see
293 /// `auth::Disclosure`.
294 pub(crate) disclosure: auth::Disclosure,
295 pub(crate) oauth: crate::oauth::OAuthState,
296 /// See [`AuthState::provision_rate_limiter`].
297 pub(crate) provision_rate_limiter: Arc<RateLimiter>,
298 /// See [`AuthState::max_accounts`].
299 pub(crate) max_accounts: u64,
300 /// See [`AuthState::unauthenticated_creates`].
301 pub(crate) unauthenticated_creates: bool,
302 /// See [`AuthState::park_rate_limiter`].
303 pub(crate) park_rate_limiter: Arc<RateLimiter>,
304 /// See [`AuthState::sync_rate_limiter`].
305 pub(crate) sync_rate_limiter: Arc<RateLimiter>,
306 /// See [`AuthState::write_meter`].
307 pub(crate) write_meter: Arc<crate::write_meter::WriteMeter>,
308 /// See [`AuthState::proxy`].
309 pub(crate) proxy: Arc<crate::ProxyLimits>,
310 /// See [`AuthState::never_wrote_min_idle`].
311 pub(crate) never_wrote_min_idle: std::time::Duration,
312 /// See [`AuthState::tree`].
313 pub(crate) tree: Arc<crate::tree::TreeState>,
314 /// See [`AuthState::subscriber_limit`].
315 pub(crate) subscriber_limit: Arc<crate::subscribers::SubscriberLimit>,
316 /// See [`AuthState::trust_forwarded_headers`].
317 pub(crate) trust_forwarded_headers: bool,
318 pub(crate) health: Arc<HealthState>,
319 /// See [`AuthState::lifecycle`] and [`require_lifecycle`].
320 pub(crate) lifecycle: Arc<ServerLifecycle>,
321 /// See [`AuthState::nudge`].
322 pub(crate) nudge: Arc<crate::PollNudge>,
323 /// See [`AuthState::operator_records`].
324 pub(crate) operator_records: Arc<crate::OperatorRecords>,
325 /// See [`AuthState::operator_handle`].
326 pub(crate) operator_handle: Arc<crate::OperatorHandle>,
327 /// See [`AuthState::handles`].
328 pub(crate) handles: Arc<dyn crate::handles::HandleDid>,
329 /// See [`AuthState::handle_lookup_limiter`].
330 pub(crate) handle_lookup_limiter: Arc<RateLimiter>,
331 /// See [`AuthState::did_lookup_limiter`].
332 pub(crate) did_lookup_limiter: Arc<RateLimiter>,
333 /// The one reader for records in repositories this deployment does not
334 /// hold; see `crate::foreign`. Built here rather than carried on
335 /// [`AuthState`] because the two facts it needs — which repositories
336 /// are ours and which names we answer to — are the registry's, and the
337 /// registry arrives with the router rather than with the auth state.
338 pub(crate) foreign: Arc<didbot_lookup::ForeignRecords>,
339 /// The two-phase read of the records a write's policies declared they
340 /// read: this half prefetches, before the write reaches the store's
341 /// synchronous section. See `crate::lookups`.
342 pub(crate) lookups: Arc<crate::LookupReader>,
343}
344
345/// How many `bot.did.createAccount` and `bot.did.createSession` calls one
346/// caller address may make per [`PROVISION_RATE_WINDOW`], the two together.
347///
348/// Sized against a legitimate caller rather than against a read route:
349/// `crates/didbot-swarm`, the local agent simulator, defaults to 8 events per
350/// second (`didbot-swarm --rate`, `scripts/dev-swarm.sh`) and spends most of
351/// them on `Spawn` while its population is below target, all from the one
352/// address a local run shares.
353/// Sustained, that is up to 480 provisioning calls a minute from a single
354/// caller with nothing wrong — the case `plan/pds-xrpc.md`'s "what \[rate
355/// limits\] do to a swarm sharing one address" names. This limit clears that
356/// with better than double the headroom, while still bounding what a caller
357/// this route does not authenticate can spend of this deployment's signing
358/// keys, Route53 record-set quota and blob storage per minute.
359pub const PROVISION_RATE_LIMIT: u32 = 1000;
360
361/// The window [`PROVISION_RATE_LIMIT`] is measured over.
362pub const PROVISION_RATE_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
363
364/// How many `GET /dashboard/login` calls one caller address may make per
365/// [`OPERATOR_LOGIN_WINDOW`]; see [`AuthState::operator_login_limiter`].
366///
367/// A person signing in makes one, retries a handful of times at worst, and
368/// is done. The number is small because this route is unauthenticated and
369/// each call costs an outbound identity resolution plus a pushed
370/// authorization request to a host the caller named.
371pub const OPERATOR_LOGIN_LIMIT: u32 = 20;
372
373/// The window [`OPERATOR_LOGIN_LIMIT`] is measured over.
374pub const OPERATOR_LOGIN_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
375
376/// How many `com.atproto.sync.*` reads one caller address may make per
377/// [`SYNC_READ_WINDOW`]; see [`AuthState::sync_rate_limiter`].
378///
379/// **Reasonable range: 60 to 60000.** Each of these reads can rebuild and
380/// sign a repository on the blocking pool, and none of them is
381/// authenticated. Six hundred a minute is ten a second sustained from one
382/// address, which clears a relay backfilling this deployment's repositories
383/// and a `didbot-swarm` run sharing one address, and still bounds what a
384/// stranger can spend of the pool.
385pub const SYNC_READ_LIMIT: u32 = 600;
386
387/// The window [`SYNC_READ_LIMIT`] is measured over.
388pub const SYNC_READ_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
389
390/// How many handles no account here holds one caller address may ask
391/// `com.atproto.identity.resolveHandle` to look up per
392/// [`HANDLE_LOOKUP_WINDOW`]; see [`AuthState::handle_lookup_limiter`].
393///
394/// **Reasonable range: 10 to 600.** Each one costs a DNS-over-HTTPS query
395/// and can cost an HTTPS fetch from a host the caller named, and none is
396/// authenticated. A browser client resolves one handle per sign-in. A
397/// handle hosted here costs no lookup and spends nothing.
398pub const HANDLE_LOOKUP_LIMIT: u32 = 60;
399
400/// The window [`HANDLE_LOOKUP_LIMIT`] is measured over.
401pub const HANDLE_LOOKUP_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
402
403/// How many DIDs hosted elsewhere one caller address may ask
404/// `com.atproto.identity.resolveDid` to fetch per [`DID_LOOKUP_WINDOW`]; see
405/// [`AuthState::did_lookup_limiter`].
406///
407/// **Reasonable range: 10 to 600.** Each one costs an HTTPS fetch from a
408/// host the caller named or from the PLC directory, and none is
409/// authenticated. A client resolves a few DIDs as it opens a view. A DID
410/// hosted here costs no lookup and spends nothing.
411pub const DID_LOOKUP_LIMIT: u32 = 60;
412
413/// The window [`DID_LOOKUP_LIMIT`] is measured over.
414pub const DID_LOOKUP_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
415
416/// How many `com.atproto.sync.*` reads may hold a blocking-pool thread at
417/// once, across every caller.
418///
419/// The same bound `crate::blobs`'s `BLOB_READ_CONCURRENCY` puts on
420/// `getBlob`, and for the same reason: these reads run on the pool session
421/// creation, provisioning, deletion and firehose publishing also draw from,
422/// and each can rebuild the Merkle search tree and sign a commit. Without a
423/// bound, [`crate::MAX_CONCURRENT_REQUESTS`] is the only one, and the
424/// deployment runs two async workers.
425///
426/// A caller that finds every permit taken waits for one, and
427/// [`REQUEST_TIMEOUT`]'s layer answers `503` if that wait outlasts the
428/// request.
429static SYNC_READ_CONCURRENCY: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(32);
430
431/// How many accounts a deployment may hold at once, absent `--max-accounts`
432/// or a `[capacity]` section; see [`AuthState::max_accounts`].
433///
434/// `plan/capacity.md` computes the raw ceiling this has to stay under: two
435/// Route53 record sets per account against the default 10,000-record quota
436/// is five thousand accounts before AWS's own quota — not this server's —
437/// refuses a write mid-provisioning. A thousand is well under half of that,
438/// leaving headroom for the zone's fixed `_acme-challenge` overhead and for
439/// the time a quota increase takes to land, and it is already generous for
440/// the fleet this project's own README describes: an agent operator's own
441/// instance, not a multi-tenant population.
442pub const DEFAULT_MAX_ACCOUNTS: u64 = 1000;
443/// How long any one request may take before this router gives up on it and
444/// answers `503` with an empty body.
445///
446/// Applies to every route, not only the anonymous ones the rate limiters
447/// single out: a slow backend or a stalled write should not be able to hold
448/// a connection (and the concurrency slot it occupies) open indefinitely.
449/// The body is empty rather than this crate's usual `{"error":..,"message":..}`
450/// shape — `tower_http::timeout::TimeoutLayer` answers with a bare status and
451/// a default body directly, without going through this router's handlers or
452/// its error type at all, which is what makes it a backstop that keeps
453/// working even if a handler itself is the thing hanging.
454pub const REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
455
456/// The XRPC methods this server serves: for each one, the handler behind it
457/// **and the credential it accepts**.
458///
459/// One list producing three things — the routes themselves, the names of
460/// them, and the credential table `crate::auth` resolves against. That is
461/// the whole point of the macro: there is no way to write a `.route()` call
462/// for an XRPC method except by adding an entry here, and an entry with no
463/// `as <Credential>` is a syntax error rather than a route that quietly
464/// defaults to open. The three lists cannot drift because there is only one
465/// list.
466///
467/// Expands to `$methods` (the NSIDs, for the conformance harness),
468/// `$credentials` (the NSID → `auth::Credential` table, which
469/// `auth::ROUTE_CREDENTIALS` is assembled from) and `$router` (the
470/// routes those names stand for).
471macro_rules! xrpc_methods {
472 (
473 $(#[$methods_meta:meta])*
474 $methods:ident,
475 $(#[$credentials_meta:meta])*
476 $credentials:ident,
477 $router:ident,
478 { $($nsid:literal => $verb:ident($handler:path) as $credential:ident),* $(,)? }
479 ) => {
480 $(#[$methods_meta])*
481 pub const $methods: &[&str] = &[$($nsid),*];
482
483 $(#[$credentials_meta])*
484 pub const $credentials: &[(&str, auth::Credential)] =
485 &[$(($nsid, auth::Credential::$credential)),*];
486
487 /// The routes those names stand for.
488 fn $router() -> Router<AppState> {
489 Router::new()$(.route(concat!("/xrpc/", $nsid), $verb($handler)))*
490 }
491 };
492}
493
494xrpc_methods! {
495 /// Every `com.atproto.*` method this router answers, in NSID order.
496 ///
497 /// Public because it is what
498 /// `crates/didbot/tests/conformance/wire.rs` reads: every name
499 /// here must be a method in the vendored upstream documents, so a
500 /// route this server invents fails a test rather than shipping.
501 ATPROTO_METHODS,
502 /// The credential each of those methods accepts. See
503 /// `auth::Credential` for what each kind means, and that module's
504 /// "Why every public route is public" for why the read surface is
505 /// `Public` on purpose: protocol-required for `com.atproto.sync.*` and
506 /// `com.atproto.repo.*`'s reads, structurally required for
507 /// `describeServer` and `resolveHandle`. The write half takes the agent
508 /// token `createAccount` hands back, and `routes::write_record` /
509 /// `routes::apply_writes` are where the authenticated DID is checked
510 /// against the repository a write names. `getSession` takes the same
511 /// credentials, and names the account they sign in as.
512 ATPROTO_CREDENTIALS,
513 atproto_routes,
514 {
515 "com.atproto.identity.resolveDid" => get(resolve_did) as Public,
516 "com.atproto.identity.resolveHandle" => get(resolve_handle) as Public,
517 "com.atproto.repo.applyWrites" => post(apply_writes) as AccountToken,
518 "com.atproto.repo.createRecord" => post(create_record) as AccountToken,
519 "com.atproto.repo.deleteRecord" => post(delete_record) as AccountToken,
520 "com.atproto.repo.describeRepo" => get(describe_repo) as Public,
521 "com.atproto.repo.getRecord" => get(get_record) as Public,
522 "com.atproto.repo.listRecords" => get(list_records) as Public,
523 "com.atproto.repo.putRecord" => post(put_record) as AccountToken,
524 "com.atproto.repo.uploadBlob" => post(crate::blobs::upload_blob) as AccountToken,
525 "com.atproto.server.describeServer" => get(describe_server) as Public,
526 "com.atproto.server.getServiceAuth" => get(crate::service_auth::get_service_auth) as AccountToken,
527 "com.atproto.server.getSession" => get(get_session) as AccountToken,
528 "com.atproto.sync.getBlob" => get(crate::blobs::get_blob) as Public,
529 "com.atproto.sync.getBlocks" => get(get_blocks) as Public,
530 "com.atproto.sync.getLatestCommit" => get(get_latest_commit) as Public,
531 "com.atproto.sync.getRecord" => get(sync_get_record) as Public,
532 "com.atproto.sync.getRepo" => get(get_repo) as Public,
533 "com.atproto.sync.getRepoStatus" => get(get_repo_status) as Public,
534 "com.atproto.sync.listBlobs" => get(crate::blobs::list_blobs) as Public,
535 "com.atproto.sync.listRepos" => get(list_repos) as Public,
536 "com.atproto.sync.subscribeRepos" => get(subscribe_repos) as Public,
537 }
538}
539
540xrpc_methods! {
541 /// The `app.bsky.*` methods this server serves itself rather than
542 /// proxies, as the reference PDS does: the account's preferences.
543 ///
544 /// Its own invocation for the reason [`BOT_DID_METHODS`] has one: the
545 /// vendored documents are `com.atproto.*` alone.
546 APP_BSKY_METHODS,
547 /// The credential each of those methods accepts: the ones `getSession`
548 /// takes.
549 APP_BSKY_CREDENTIALS,
550 app_bsky_routes,
551 {
552 "app.bsky.actor.getPreferences" => get(get_preferences) as AccountToken,
553 "app.bsky.actor.putPreferences" => post(put_preferences) as AccountToken,
554 }
555}
556
557xrpc_methods! {
558 /// The `bot.did.*` methods this server serves — its own account admin
559 /// surface, alongside the `com.atproto.*` one [`ATPROTO_METHODS`] names.
560 ///
561 /// Its own invocation rather than folded into the one above: these are
562 /// not vendored anywhere, so `crates/didbot/tests/conformance/wire.rs`'s
563 /// "every route is a vendored method" check must not see them.
564 ///
565 /// `bot.did.hardDeleteAccount` is absent on purpose. It frees a burned
566 /// name, so self-service is exactly wrong for it. The operator reaches
567 /// `Registry::hard_delete` through the dashboard's
568 /// `POST /dashboard/api/accounts/{did}/delete` instead.
569 BOT_DID_METHODS,
570 /// The credential each of those methods accepts.
571 ///
572 /// `createAccount` is `Credential::Creator`: the credential names the
573 /// account creating, in any of the three shapes `tree::prove` resolves,
574 /// and `createSession` is `Credential::Proof`, the two of those that
575 /// are not a session. `updateCredentials` is
576 /// `Credential::KeyProofOrOperator`: a JWT one of the account's keys
577 /// signed, or the operator session. Every
578 /// account-lifecycle mutation names an account that already exists and
579 /// so takes `Credential::AccountSelf` — an agent acting on
580 /// *itself*, the only caller this server can authenticate for them. The
581 /// four authorization-decision routes take it for the same reason from
582 /// the other side: they name no account at all, and the account they act
583 /// on is the one the presented token authenticates as, so a decision can
584 /// only ever be read, approved or declined by the agent it was addressed
585 /// to. `createBrowserSignIn` takes it the same way, and signs a browser in
586 /// as that account alone.
587 /// The four reads are `Credential::Disclosure`: public by
588 /// default, narrowable per route, never widenable. `pollOperatorClaim`
589 /// is `Public` because it confers nothing — see the `auth` module's doc.
590 /// `parkKey` and `describeParkedKey` are `Public` for the same reason:
591 /// a park mints nothing until the operator admits it from their own
592 /// side, and both are bounded per address rather than by a credential.
593 BOT_DID_CREDENTIALS,
594 bot_did_routes,
595 {
596 "bot.did.activateAccount" => post(activate_account) as AccountSelf,
597 "bot.did.approveAuthorization" => post(approve_authorization) as AccountSelf,
598 "bot.did.deactivateAccount" => post(deactivate_account) as AccountSelf,
599 "bot.did.declineAuthorization" => post(decline_authorization) as AccountSelf,
600 "bot.did.deleteAccount" => post(delete_account) as AccountSelf,
601 "bot.did.freezeAccount" => post(freeze_account) as AccountSelf,
602 "bot.did.getAccountLedger" => get(get_account_ledger) as Disclosure,
603 "bot.did.getAuthorization" => get(get_authorization) as AccountSelf,
604 "bot.did.listAccountLedgers" => get(list_ledgers) as Disclosure,
605 "bot.did.listAccounts" => get(list_accounts) as Disclosure,
606 "bot.did.listPendingAuthorizations" => get(list_pending_authorizations) as AccountSelf,
607 "bot.did.pollOperatorClaim" => post(poll_operator_claim) as Public,
608 "bot.did.createAccount" => post(crate::tree::create_account) as Creator,
609 "bot.did.createBrowserSignIn" => post(create_browser_sign_in) as AccountSelf,
610 "bot.did.createSession" => post(crate::tree::create_session) as Proof,
611 "bot.did.describeParkedKey" => get(crate::tree::describe_parked_key) as Public,
612 "bot.did.parkKey" => post(crate::tree::park_key) as Public,
613 "bot.did.stats" => get(stats) as Disclosure,
614 "bot.did.unfreezeAccount" => post(unfreeze_account) as AccountSelf,
615 "bot.did.updateCredentials" => post(crate::tree::update_credentials) as KeyProofOrOperator,
616 }
617}
618
619/// The header a client may name the turn a request belongs to in.
620///
621/// A record crosses three processes — the hook that stamps it, the record
622/// host that writes it, and this server that stores it — and each logs what it
623/// did. Until this existed nothing joined those three lines together at a
624/// finer grain than the agent, and an agent that has been working for an hour
625/// has hundreds of lines under one DID.
626///
627/// It is logged and nothing else. Nothing here parses it, no behaviour depends
628/// on it, and it never reaches a record: it is a harness-internal string, and
629/// writing it into a repository anybody can read would publish an operational
630/// detail permanently for the benefit of nobody reading it there.
631///
632/// The producing half is whatever client sends it, which is why the name and
633/// the bound below are stated here rather than inferred from a caller.
634pub const TURN_HEADER: &str = "quernstone-turn";
635
636/// The longest turn identifier this server will put in a log line.
637///
638/// A sanity bound rather than an agreement: anything longer is dropped, so a
639/// client cannot make an arbitrarily long log line out of a header.
640pub const TURN_HEADER_MAX_LENGTH: usize = 256;
641
642/// The request span, carrying the turn when the client named one.
643///
644/// A [`MakeSpan`] rather than a separate middleware because a span is already
645/// being made here, and putting the field on it means every line emitted while
646/// the request is being served carries it — including the refusals, which are
647/// the lines somebody correlating is most often looking for.
648#[derive(Debug, Clone, Copy, Default)]
649struct TurnSpan;
650
651impl<B> MakeSpan<B> for TurnSpan {
652 fn make_span(&mut self, request: &axum::http::Request<B>) -> tracing::Span {
653 let turn = request
654 .headers()
655 .get(TURN_HEADER)
656 .and_then(|value| value.to_str().ok())
657 .filter(|value| !value.is_empty() && value.chars().count() <= TURN_HEADER_MAX_LENGTH)
658 .unwrap_or("-");
659 // The same three fields `DefaultMakeSpan` puts on a request span, plus
660 // the turn. Named "request" rather than "http.request" for the same
661 // reason the rest of this log is short: it is read by a person
662 // watching a terminal.
663 //
664 // The path, not the URI: `/oauth/authorize` and
665 // `bot.did.getAuthorization` carry a pushed-request reference in their
666 // query, and that reference redeems a pending sign-in. Logs are shipped
667 // and copied like operational data, so nothing that spends a credential
668 // goes in one.
669 tracing::info_span!(
670 "request",
671 method = %request.method(),
672 uri = %request.uri().path(),
673 version = ?request.version(),
674 turn = %turn,
675 )
676 }
677}
678
679/// Builds the router.
680pub fn app(registry: Arc<dyn Registry>) -> Router {
681 app_with_health(registry, HealthState::new())
682}
683
684/// Builds the router with the health pulse wired to an existing producer.
685///
686/// The e-stop and the operator credential stay the
687/// [`AuthState::default`] every other builder but [`app_with_auth`] uses;
688/// call that one directly to wire real ones alongside a real `health`.
689pub fn app_with_health(registry: Arc<dyn Registry>, health: Arc<HealthState>) -> Router {
690 app_with_auth(registry, AuthState::default(), health)
691}
692
693/// Builds the router with every collaborator supplied explicitly.
694///
695/// The full builder every other one in this module calls. A deployment that
696/// wants a durable session store, a configured operator credential, or a
697/// file-backed e-stop latch calls this one directly; everything else keeps
698/// working exactly as it did before [`AuthState`] existed.
699///
700/// `health` should be [`HealthTick::state`](crate::HealthTick::state) from
701/// the tick a caller has spawned for this registry. A router built any other
702/// way here answers `/health`'s `progress` from a state nothing ever ticks.
703pub fn app_with_auth(
704 registry: Arc<dyn Registry>,
705 auth: AuthState,
706 health_state: Arc<HealthState>,
707) -> Router {
708 app_with_repos(registry, Repos::default(), auth, health_state)
709}
710
711/// Builds the router with `com.atproto.sync.subscribeRepos` attached too.
712///
713/// The full builder. `repos` must be the same [`Repos`] handed to
714/// [`Provisioner::with_repo_sink`](didbot_pds::Provisioner::with_repo_sink),
715/// or the method answers with a stream nothing publishes into — and, unlike
716/// the other two streams, one whose sequence numbers do not survive a restart
717/// either. [`app_with_auth`] builds one with an in-memory sequence, which is
718/// the right thing for a test and the wrong thing for a deployment; see
719/// [`Durable::sequence`](didbot_pds::Durable::sequence).
720pub fn app_with_repos(
721 registry: Arc<dyn Registry>,
722 repos: Repos,
723 auth: AuthState,
724 health_state: Arc<HealthState>,
725) -> Router {
726 // Wired here rather than left to whoever built `AuthState`: a Revoke
727 // must tear down outstanding credentials or releasing it resurrects them,
728 // and every router this crate builds needs that true regardless of who
729 // constructed its `Estop` and `OAuthState`. The durable issuer writes
730 // each revocation to the log before this returns, so a Revoke outlives
731 // the restart that follows it. See `Estop::set_revoke_hook`.
732 let auth_loopback_only = auth.loopback_callers_only;
733 let strict_transport_security = auth
734 .strict_transport_security
735 .map(|hsts| hsts.header_value());
736 let nonces = auth.oauth.dpop.clone();
737 let revoke_grants = auth.oauth.tokens.clone();
738 let revoke_browsers = auth.oauth.browser_sessions.clone();
739 let revoke_remembered = auth.oauth.decision_store.clone();
740 auth.estop.set_revoke_hook(Arc::new(move || {
741 revoke_grants.revoke_all();
742 revoke_browsers.revoke_all();
743 revoke_remembered.forget_all();
744 }));
745 // The other credential an account answers with: the agent token every
746 // createAccount and createSession hands out. Ended when the latch
747 // leaves Revoke rather than when it is thrown, because
748 // `bot.did.freezeAccount` and `bot.did.deactivateAccount` are reached
749 // with that token and are exempt from the stop — a stop must never
750 // stand between a caller and restraining itself. The latch refuses the
751 // token for everything else while it stands, and this is what keeps it
752 // refused afterwards.
753 let revoke_tokens = registry.clone();
754 auth.estop.set_release_hook(Arc::new(move || {
755 revoke_tokens.revoke_all_agent_tokens();
756 }));
757 // And the same wiring for a single account, at the other end of an
758 // account's life. The registry holds the agent token store and revokes
759 // through it directly; every other credential bound to a DID is here,
760 // beside the router, so a deletion tells this closure and this closure
761 // ends them. Wired here for the reason above: every router this crate
762 // builds needs it true, and the stores it names are the ones `AuthState`
763 // carries. See `Registry::set_account_revoke_hook`.
764 //
765 // A credential is anything that answers for the account: its OAuth
766 // grants, the authorization codes one hop short of a grant, and the
767 // browsers signed in as it. Its remembered approvals go with them. The
768 // codes, the browsers and the approvals go whatever the grant store
769 // answers.
770 let deleted_tokens = auth.oauth.tokens.clone();
771 let deleted_codes = auth.oauth.code_store.clone();
772 let deleted_browsers = auth.oauth.browser_sessions.clone();
773 let deleted_remembered = auth.oauth.decision_store.clone();
774 registry.set_account_revoke_hook(Arc::new(move |did: &str| {
775 let ended = deleted_tokens.revoke_account(did);
776 deleted_codes.revoke_account(did);
777 deleted_browsers.revoke_account(did);
778 deleted_remembered.forget_account(did);
779 ended
780 }));
781 let registry_for_foreign = registry.clone();
782 let private_addresses = auth.tree.private_addresses();
783 let foreign = crate::foreign::reader(registry_for_foreign, private_addresses);
784 // The reader needs the registry to tell a repository this deployment
785 // holds from a stranger's, so it cannot be built until the registry
786 // is; the registry is told about it here, once, and both halves of the
787 // read -- the route's prefetch and the write path's own -- share it.
788 let lookups = Arc::new(crate::LookupReader::new(
789 auth.declared_lookups.clone(),
790 foreign.clone(),
791 registry.clone(),
792 ));
793 registry.set_lookups(lookups.clone());
794 let state = AppState {
795 registry,
796 repos,
797 estop: auth.estop,
798 operator: auth.operator,
799 relay: auth.relay,
800 operator_login_limiter: auth.operator_login_limiter,
801 disclosure: auth.disclosure,
802 oauth: auth.oauth,
803 provision_rate_limiter: auth.provision_rate_limiter,
804 max_accounts: auth.max_accounts,
805 unauthenticated_creates: auth.unauthenticated_creates,
806 park_rate_limiter: auth.park_rate_limiter,
807 sync_rate_limiter: auth.sync_rate_limiter,
808 write_meter: auth.write_meter,
809 proxy: auth.proxy,
810 never_wrote_min_idle: auth.never_wrote_min_idle,
811 tree: auth.tree,
812 subscriber_limit: auth.subscriber_limit,
813 trust_forwarded_headers: auth.trust_forwarded_headers,
814 health: health_state,
815 lifecycle: auth.lifecycle,
816 nudge: auth.nudge,
817 operator_records: auth.operator_records,
818 operator_handle: auth.operator_handle,
819 handles: auth.handles,
820 handle_lookup_limiter: auth.handle_lookup_limiter,
821 did_lookup_limiter: auth.did_lookup_limiter,
822 foreign,
823 lookups,
824 };
825
826 // The routes a stranger's browser tab may call from its own origin: the
827 // protocol surface, its discovery documents, and the two OAuth endpoints
828 // a web client fetches rather than navigates to. `/oauth/authorize` and
829 // `/oauth/browser` are pages a browser is sent to and the dashboard holds
830 // a person's session — none answers cross-origin, and widening
831 // this set is a security decision, not a convenience (see
832 // `plan/web-launch.md`).
833 let cross_origin = Router::new()
834 .route("/xrpc/_health", get(xrpc_health))
835 .route("/.well-known/did.json", get(did_json))
836 .route(ATPROTO_DID_PATH, get(atproto_did))
837 .route(
838 "/.well-known/oauth-authorization-server",
839 get(oauth_server_metadata),
840 )
841 .route(
842 "/.well-known/oauth-protected-resource",
843 get(oauth_protected_resource_metadata),
844 )
845 .route(
846 "/oauth/par",
847 post(oauth_par).layer(axum::middleware::from_fn(never_cached)),
848 )
849 .route(
850 "/oauth/token",
851 post(oauth_token).layer(axum::middleware::from_fn(never_cached)),
852 )
853 .merge(bot_did_routes())
854 .merge(atproto_routes())
855 .merge(app_bsky_routes())
856 // Set here, before the layer, and not only on the outer router
857 // below: axum layers a route's method-not-allowed slot along with
858 // its handlers, but the outer `method_not_allowed_fallback` replaces
859 // every slot still holding the default — layered or not — and a
860 // preflight is an `OPTIONS` on a route that serves no `OPTIONS`, so
861 // it is exactly that slot the layer has to be wrapped around.
862 .method_not_allowed_fallback(wrong_method)
863 .layer(cross_origin_layer());
864
865 let loopback_callers_only = auth_loopback_only;
866 Router::new()
867 .route("/health", get(health))
868 .route("/oauth/authorize", get(oauth_authorize))
869 .route(crate::oauth::browser::PATH, get(oauth_browser))
870 .merge(crate::home::routes())
871 .merge(crate::dashboard::routes())
872 .merge(crate::analytics::routes())
873 .merge(cross_origin)
874 // A path nothing serves, and a method nothing serves on a path that
875 // exists, are both failures a client has to be able to read. axum's
876 // own answers are an empty 404 and an empty 405, which are not the
877 // shape every other failure here takes. The fallback is also where
878 // an XRPC call this server does not serve is proxied; see
879 // `crate::proxy`.
880 .fallback(crate::proxy::fallback)
881 .with_state(state)
882 .method_not_allowed_fallback(wrong_method)
883 // Requests are logged at INFO because this server exists to be watched.
884 // The default for `TraceLayer` is DEBUG, which would mean turning on
885 // DEBUG for everything else to see them.
886 .layer(
887 TraceLayer::new_for_http()
888 .make_span_with(TurnSpan)
889 .on_request(DefaultOnRequest::new().level(Level::DEBUG))
890 .on_response(
891 DefaultOnResponse::new()
892 .level(Level::INFO)
893 .latency_unit(LatencyUnit::Millis),
894 ),
895 )
896 // A concurrency bound is deliberately *not* layered on here. This
897 // router is built and handed to `crate::serve_router` or
898 // `crate::serve_tls`, which both apply that bound through
899 // `crate::limited_make_service` instead — see
900 // `crate::MAX_CONCURRENT_REQUESTS`'s own doc for why `Router::layer`
901 // cannot be trusted with a stateful limiter like
902 // `tower::limit::ConcurrencyLimitLayer`.
903 .layer(TimeoutLayer::with_status_code(
904 StatusCode::SERVICE_UNAVAILABLE,
905 REQUEST_TIMEOUT,
906 ))
907 // The buffering bound, pinned rather than inherited; see
908 // `MAX_REQUEST_BODY`. `uploadBlob` is unaffected because it takes the
909 // body as a stream, which this layer does not reach.
910 .layer(axum::extract::DefaultBodyLimit::max(MAX_REQUEST_BODY))
911 // Outside the body limit and the timeout, so their refusals carry a
912 // nonce too. See `hand_out_nonce`.
913 .layer(axum::middleware::from_fn(move |request, next| {
914 hand_out_nonce(nonces.clone(), request, next)
915 }))
916 // Outside the body limit, the request log and the timeout, so a
917 // caller this deployment does not answer is refused before any of
918 // them spends anything on it. `security_headers` below is the one
919 // layer outside this one, so the refusal carries them too. See
920 // `AuthState::loopback_callers_only`.
921 .layer(axum::middleware::from_fn(move |request, next| {
922 only_loopback_callers(loopback_callers_only, request, next)
923 }))
924 .layer(axum::middleware::from_fn(move |request, next| {
925 security_headers(strict_transport_security.clone(), request, next)
926 }))
927}
928
929/// A `Strict-Transport-Security` header; see `[tls] hsts_max_age_secs` and
930/// `[tls] hsts_include_subdomains` in `didbot-config`.
931///
932/// `preload` is never sent. The preload list takes only registrable domains,
933/// and getting off it takes months.
934#[derive(Debug, Clone, Copy, PartialEq, Eq)]
935pub struct StrictTransportSecurity {
936 /// How long a browser keeps the header, in seconds.
937 pub max_age_secs: u64,
938 /// Whether the header covers every name below the one it was served on.
939 pub include_subdomains: bool,
940}
941
942impl StrictTransportSecurity {
943 /// Two years, the same as did.bot's own header.
944 pub const DEFAULT_MAX_AGE_SECS: u64 = 63_072_000;
945
946 /// The header's value.
947 pub fn header_value(&self) -> HeaderValue {
948 let value = if self.include_subdomains {
949 format!("max-age={}; includeSubDomains", self.max_age_secs)
950 } else {
951 format!("max-age={}", self.max_age_secs)
952 };
953 HeaderValue::try_from(value).expect("digits and ASCII words are a valid header value")
954 }
955}
956
957/// The headers every response this server sends carries.
958///
959/// This process terminates its own TLS and renders its own pages, so there is
960/// nothing in front of it to add them: `/oauth/authorize` and `/dashboard`
961/// are HTML a browser is handed directly. The set is the one a
962/// layer can apply without knowing the route — a consent page that cannot be
963/// framed, a content type that is believed rather than sniffed, and a request
964/// that says nothing about where the reader came from.
965///
966/// Applied to every response rather than to the HTML routes alone: the JSON,
967/// the dashboard's assets and a blob are each served with the content type
968/// they actually are, and none of them is meant to be put in a frame either.
969///
970/// `frame-ancestors` is the whole of the content security policy. A
971/// `script-src` here would have to admit the `new Function` and the
972/// `WebAssembly` that `dashboard.js` loads its field animation through, and a
973/// policy written to permit `eval` buys nothing; framing is what the consent
974/// page needs refused. `X-Frame-Options` says the same thing again for a
975/// browser that reads only that one.
976///
977/// A route that sets a policy of its own keeps it: `getBlob` sets a sandbox
978/// (see [`crate::blobs`]), and `X-Frame-Options` still refuses framing there.
979///
980/// `no-referrer`, where the static site sends
981/// `strict-origin-when-cross-origin`: `/oauth/authorize`'s own URL carries the
982/// authorization request, and no page here has a reason to pass its address to
983/// anything it links to.
984///
985/// `Strict-Transport-Security` is added when the deployment set one; see
986/// [`AuthState::strict_transport_security`]. Every response goes out over
987/// TLS, so it is never sent over plain HTTP.
988async fn security_headers(
989 strict_transport_security: Option<HeaderValue>,
990 request: axum::extract::Request,
991 next: axum::middleware::Next,
992) -> Response {
993 let mut response = next.run(request).await;
994 let headers = response.headers_mut();
995 if let Some(value) = strict_transport_security {
996 headers.insert(header::STRICT_TRANSPORT_SECURITY, value);
997 }
998 headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
999 if !headers.contains_key(header::CONTENT_SECURITY_POLICY) {
1000 headers.insert(
1001 header::CONTENT_SECURITY_POLICY,
1002 HeaderValue::from_static("frame-ancestors 'none'"),
1003 );
1004 }
1005 headers.insert(
1006 header::X_CONTENT_TYPE_OPTIONS,
1007 HeaderValue::from_static("nosniff"),
1008 );
1009 headers.insert(
1010 header::REFERRER_POLICY,
1011 HeaderValue::from_static("no-referrer"),
1012 );
1013 response
1014}
1015
1016/// Refuses a caller that is not on this host, when this deployment answers
1017/// only callers that are; see [`AuthState::loopback_callers_only`].
1018///
1019/// A request that arrived with no peer address — a test driving the
1020/// [`Router`] directly, or a deployment that wired neither
1021/// [`crate::serve_router`] nor [`crate::serve_tls`] — is served, the same
1022/// posture `crate::rate_limit::caller_key` takes for the same absence: this
1023/// cannot tell such a caller apart from any other, and refusing every
1024/// request on a router nobody put a listener under would make the router
1025/// untestable rather than safe.
1026async fn only_loopback_callers(
1027 enabled: bool,
1028 request: axum::extract::Request,
1029 next: axum::middleware::Next,
1030) -> Response {
1031 if enabled {
1032 let peer = request
1033 .extensions()
1034 .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
1035 .map(|axum::extract::ConnectInfo(addr)| addr.ip());
1036 if let Some(ip) = peer {
1037 if !ip.to_canonical().is_loopback() {
1038 info!(
1039 %ip,
1040 path = %request.uri().path(),
1041 "refused: this deployment answers only callers on this host"
1042 );
1043 return ApiError::new(
1044 StatusCode::FORBIDDEN,
1045 "NotServedHere",
1046 "this deployment answers only callers on the host it runs on",
1047 )
1048 .into_response();
1049 }
1050 }
1051 }
1052 next.run(request).await
1053}
1054
1055/// Hands the client a DPoP nonce on the answer to any request that carried
1056/// a `DPoP` proof, success and refusal alike, unless the answer names one
1057/// already, as a nonce challenge does.
1058///
1059/// The atproto OAuth profile has a client refuse an answer to a DPoP request
1060/// that carries no `DPoP-Nonce`, and RFC 9449 §8.2 lets every answer carry
1061/// the nonce for the next request.
1062async fn hand_out_nonce(
1063 verifier: Arc<dyn crate::oauth::dpop_seam::DpopVerifier>,
1064 request: axum::extract::Request,
1065 next: axum::middleware::Next,
1066) -> Response {
1067 let carried = request.headers().contains_key(crate::oauth::dpop::DPOP);
1068 let mut response = next.run(request).await;
1069 if carried
1070 && !response
1071 .headers()
1072 .contains_key(crate::oauth::dpop::DPOP_NONCE)
1073 {
1074 if let Some(value) = verifier
1075 .nonce()
1076 .and_then(|nonce| HeaderValue::from_str(&nonce).ok())
1077 {
1078 response
1079 .headers_mut()
1080 .insert(crate::oauth::dpop::DPOP_NONCE, value);
1081 }
1082 }
1083 response
1084}
1085
1086/// How long a browser may cache a preflight answer before asking again.
1087///
1088/// A day, which is what the reference PDS sets. The answer never varies by
1089/// caller, so there is nothing for a shorter cache to pick up sooner.
1090pub const CORS_MAX_AGE: std::time::Duration = std::time::Duration::from_secs(24 * 60 * 60);
1091
1092/// The request headers an atproto client sends and a preflight must name.
1093///
1094/// `authorization` and `dpop` carry the two credentials, `content-type` is
1095/// on every procedure call, and `atproto-proxy` / `atproto-accept-labelers`
1096/// are what the official client adds to every request whether or not the
1097/// server it reaches acts on them. A header left off this list makes the
1098/// browser refuse the request before it is sent, so the list is the
1099/// client's, not this server's.
1100pub const CORS_ALLOWED_HEADERS: [HeaderName; 5] = [
1101 header::AUTHORIZATION,
1102 header::CONTENT_TYPE,
1103 crate::oauth::dpop::DPOP,
1104 HeaderName::from_static("atproto-accept-labelers"),
1105 HeaderName::from_static("atproto-proxy"),
1106];
1107
1108/// The response headers a browser is allowed to show a script.
1109///
1110/// Anything not named here is stripped by the browser before the calling
1111/// page sees it, and both of these carry the next step of a flow: the
1112/// nonce a DPoP client must echo on its retry, and the challenge that says
1113/// a token was refused and why.
1114pub const CORS_EXPOSED_HEADERS: [HeaderName; 2] =
1115 [crate::oauth::dpop::DPOP_NONCE, header::WWW_AUTHENTICATE];
1116
1117/// The cross-origin policy on the public protocol surface.
1118///
1119/// Any origin, without credentials: the browser then never attaches a
1120/// cookie, so a page on somebody else's origin can present only what its
1121/// own script holds — a bearer or DPoP token it was issued — which is how
1122/// the atproto OAuth flow from a web app and every browser-based checker
1123/// reaches a PDS. What it is layered on is decided in [`app_with_repos`].
1124pub fn cross_origin_layer() -> CorsLayer {
1125 CorsLayer::new()
1126 .allow_origin(Any)
1127 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
1128 .allow_headers(CORS_ALLOWED_HEADERS)
1129 .expose_headers(CORS_EXPOSED_HEADERS)
1130 .max_age(CORS_MAX_AGE)
1131}
1132
1133/// Parses a JSON body, mapping any failure to a 400 in the atproto shape.
1134///
1135/// `axum::Json` is not used as an extractor here because its rejection is
1136/// axum's own plain-text body, not `{"error":..,"message":..}`.
1137pub(crate) fn parse_body<T: DeserializeOwned>(body: &Bytes) -> Result<T, ApiError> {
1138 serde_json::from_slice(body).map_err(|err| ApiError::bad_request(err.to_string()))
1139}
1140
1141/// The 500 for a `spawn_blocking` task that panicked instead of answering.
1142///
1143/// A panic inside a blocking task never reaches the worker that awaited it —
1144/// it surfaces as a `JoinError` instead — so this is the one place that
1145/// becomes a response rather than a connection that hangs up with nothing
1146/// said.
1147///
1148/// A `JoinError`'s own text is the panic payload, which is whatever the line
1149/// that panicked passed to `panic!` or to `expect`: a file it was reading, a
1150/// row it was holding, an assertion written for whoever is reading the log.
1151/// `route` and that payload go to the log line, and the caller — who may be
1152/// anyone, since `com.atproto.sync.getBlob` reaches here without a
1153/// credential — is told that the request was not answered and nothing else.
1154pub(crate) fn blocking_task_failed(route: &str, error: tokio::task::JoinError) -> ApiError {
1155 tracing::error!(route, %error, "a blocking task panicked before answering");
1156 ApiError::new(
1157 StatusCode::INTERNAL_SERVER_ERROR,
1158 "InternalServerError",
1159 "the server did not answer this request".to_owned(),
1160 )
1161}
1162
1163/// Runs a `com.atproto.sync` read on the blocking pool.
1164///
1165/// Every read this is used for can reach `Registry::repository`, which
1166/// rebuilds the Merkle search tree and signs a commit when the cached one no
1167/// longer names the head — and then serialises it. That is milliseconds to
1168/// seconds of CPU with no await in it, and the deployment in `infra/pds`
1169/// runs two async workers, so two such reads left inline are every other
1170/// route waiting. `spawn_blocking` is the same seam `getBlob` and
1171/// `createAccount` already cross.
1172///
1173/// `route` names the method in the log line a panicking task leaves; see
1174/// [`blocking_task_failed`].
1175pub(crate) async fn sync_read<T, F>(
1176 state: &AppState,
1177 route: &'static str,
1178 work: F,
1179) -> Result<T, Response>
1180where
1181 F: for<'a> FnOnce(&'a dyn Registry) -> Result<T, ProvisionError> + Send + 'static,
1182 T: Send + 'static,
1183{
1184 // Acquired before `spawn_blocking`, not inside it: what it bounds is
1185 // blocking-pool threads, and claiming one of those first would hold it
1186 // idle while this request waits its turn. Released when this function
1187 // returns, which is when the read is done.
1188 let _permit = SYNC_READ_CONCURRENCY
1189 .acquire()
1190 .await
1191 .expect("this semaphore is never closed");
1192 let registry = state.registry.clone();
1193 match tokio::task::spawn_blocking(move || work(registry.as_ref())).await {
1194 Ok(Ok(value)) => Ok(value),
1195 Ok(Err(err)) => Err(sync_error(state, &err).into_response()),
1196 Err(err) => Err(blocking_task_failed(route, err).into_response()),
1197 }
1198}
1199
1200/// Spends one of the caller's [`SYNC_READ_LIMIT`] sync reads, or answers
1201/// the refusal.
1202///
1203/// These routes take no credential, so the address is the only caller there
1204/// is. Checked before the work rather than after, which is the whole point:
1205/// the cost this bounds is the rebuild, not the response.
1206pub(crate) fn require_sync_budget(
1207 state: &AppState,
1208 headers: &axum::http::HeaderMap,
1209 peer: crate::rate_limit::Peer,
1210) -> Result<(), ApiError> {
1211 let key = crate::rate_limit::caller_key(headers, peer.0, state.trust_forwarded_headers);
1212 if state.sync_rate_limiter.check(&key) {
1213 return Ok(());
1214 }
1215 info!(caller = %key, "sync read refused: too many from this address");
1216 Err(
1217 ApiError::rate_limited("too many sync reads from this address; slow down").with_header(
1218 axum::http::header::RETRY_AFTER,
1219 SYNC_READ_WINDOW.as_secs().to_string(),
1220 ),
1221 )
1222}
1223
1224/// The largest request body this server will hold in memory.
1225///
1226/// Two mebibytes, which is what axum bounds a buffering extractor to when
1227/// nothing says otherwise. It is written down here rather than inherited
1228/// silently, because it is the only thing standing between an anonymous
1229/// caller and this process's memory, and a bound that arrives from a
1230/// dependency's default is one nothing in this repository would notice
1231/// changing.
1232///
1233/// It is not a bound on a *blob*: `com.atproto.repo.uploadBlob` takes the
1234/// body as a stream and never buffers it, and its size limit is the blob
1235/// store's own. This is the bound on the JSON every other procedure sends.
1236///
1237/// Enforcement is streaming rather than after the fact — the body is refused
1238/// as it arrives, at the first frame that carries the request past the bound,
1239/// so a caller announcing or sending a gigabyte never has a gigabyte of it
1240/// resident here.
1241pub const MAX_REQUEST_BODY: usize = 2 * 1024 * 1024;
1242
1243/// A buffered request body, refused in the atproto shape.
1244///
1245/// `Bytes` on its own is an extractor whose rejection is axum's own
1246/// plain-text sentence with no `error` name at all — the same reason
1247/// `parse_body` exists rather than `axum::Json`, and the same reason every
1248/// query below is extracted as a `Result`. A body over
1249/// [`MAX_REQUEST_BODY`] is the one failure on this surface a caller cannot
1250/// avoid by writing a better request, so it is the one that most has to be
1251/// machine-readable.
1252pub struct XrpcBody(pub Bytes);
1253
1254impl<S> axum::extract::FromRequest<S> for XrpcBody
1255where
1256 S: Send + Sync,
1257{
1258 type Rejection = ApiError;
1259
1260 async fn from_request(
1261 request: axum::extract::Request,
1262 state: &S,
1263 ) -> Result<Self, Self::Rejection> {
1264 match Bytes::from_request(request, state).await {
1265 Ok(body) => Ok(Self(body)),
1266 // `RequestTooLarge` names no error the lexicons declare, the
1267 // same position `RateLimitExceeded` and `ServerNotReady` are in:
1268 // 413 is the standard status and every client already knows it,
1269 // and the name is there for the ones that switch on it. The
1270 // message is this crate's own rather than the rejection's, so
1271 // nothing about how the body was buffered reaches the caller.
1272 Err(rejection) if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE => {
1273 Err(request_too_large())
1274 }
1275 Err(rejection) => Err(ApiError::bad_request(rejection.body_text())),
1276 }
1277 }
1278}
1279
1280/// `RequestTooLarge`: a body over [`MAX_REQUEST_BODY`].
1281pub(crate) fn request_too_large() -> ApiError {
1282 ApiError::new(
1283 StatusCode::PAYLOAD_TOO_LARGE,
1284 "RequestTooLarge",
1285 format!("a request body may not exceed {MAX_REQUEST_BODY} bytes"),
1286 )
1287}
1288
1289/// Resolves a repo write route's credential — an agent token or a DPoP-bound
1290/// OAuth access token. See `auth::require_agent_token_or_dpop`.
1291fn resource_credential(
1292 state: &AppState,
1293 headers: &HeaderMap,
1294 http_method: &str,
1295 path: &str,
1296) -> Result<auth::Writer, ApiError> {
1297 auth::require_agent_token_or_dpop(
1298 state.registry.as_ref(),
1299 &state.oauth,
1300 headers,
1301 http_method,
1302 path,
1303 )
1304}
1305
1306/// Checks a write against the scope of the token that presented it — see
1307/// `auth::Writer::authorize` — and writes a refusal to the evaluation log
1308/// beside the policy's own, so what a token was refused reads in one place.
1309///
1310/// A collection that is not an NSID is refused first, as the store would
1311/// refuse it, so a caller cannot write arbitrary strings into that log.
1312fn authorize_write<'a>(
1313 state: &AppState,
1314 writer: &'a auth::Writer,
1315 writes: &[(&str, Action)],
1316) -> Result<auth::Authorized<'a>, ApiError> {
1317 for (collection, _) in writes {
1318 didbot_pds::validate_collection(collection)
1319 .map_err(|err| crate::error::repo_surface(&didbot_pds::ProvisionError::Record(err)))?;
1320 }
1321 writer.authorize(writes).map_err(|missing| {
1322 record_scope_refusal(state, writer.did(), &missing);
1323 auth::insufficient_scope(writer.did(), &missing)
1324 })
1325}
1326
1327/// Checks one upload against the scope of the token that presented it, and
1328/// writes a refusal down the way [`authorize_write`] does.
1329pub(crate) fn authorize_upload<'a>(
1330 state: &AppState,
1331 writer: &'a auth::Writer,
1332 mime_type: &str,
1333) -> Result<auth::Authorized<'a>, ApiError> {
1334 writer.authorize_blob(mime_type).map_err(|missing| {
1335 record_scope_refusal(state, writer.did(), &missing);
1336 auth::insufficient_scope(writer.did(), &missing)
1337 })
1338}
1339
1340/// The evaluation log's row for a write outside its token's scope, named
1341/// [`auth::TOKEN_SCOPE_RULE`] where a tree's row names a policy.
1342fn record_scope_refusal(state: &AppState, did: &str, missing: &auth::ScopeMissing) {
1343 let Some(account) = state.registry.account(did) else {
1344 return;
1345 };
1346 let pds_did = deployment_did(state);
1347 let subject = match &missing.wanted {
1348 auth::Wanted::Record { collection, action } => didbot_pds::policy::write_subject(
1349 account.did.as_str(),
1350 account.kind,
1351 account.handle.as_deref(),
1352 &pds_did,
1353 state.registry.zone(),
1354 None,
1355 Some(&missing.client_id),
1356 collection,
1357 match action {
1358 Action::Create => didbot_pds::policy::WriteAction::Create,
1359 Action::Update | Action::Manage => didbot_pds::policy::WriteAction::Update,
1360 Action::Delete => didbot_pds::policy::WriteAction::Delete,
1361 },
1362 &[],
1363 None,
1364 None,
1365 &[],
1366 ),
1367 auth::Wanted::Upload { mime_type } => didbot_pds::policy::blob_subject(
1368 account.did.as_str(),
1369 account.kind,
1370 account.handle.as_deref(),
1371 &pds_did,
1372 state.registry.zone(),
1373 Some(&missing.client_id),
1374 mime_type,
1375 None,
1376 None,
1377 ),
1378 // A call at another service is no write or upload a policy judges,
1379 // so it has no subject to log; `service_auth::authorize` logs it.
1380 auth::Wanted::Rpc { .. } => return,
1381 };
1382 state
1383 .oauth
1384 .evaluation_log
1385 .record_denial(didbot_pds::evaluation_log::Denial {
1386 account: account.did.as_str(),
1387 policies_fired: &[auth::TOKEN_SCOPE_RULE.to_owned()],
1388 verdict: didbot_pds::evaluation_log::Verdict::Reject,
1389 reason: &format!("the token's scope does not cover `{}`", missing.needed()),
1390 cut: &[],
1391 payload_hash: &state.oauth.evaluation_log.hash_subject(&subject),
1392 policy_version: &didbot_pds::policy::PolicyVersion(auth::TOKEN_SCOPE_RULE.to_owned()),
1393 now: subject.universal().now,
1394 subject: &subject,
1395 });
1396}
1397
1398/// `POST /xrpc/bot.did.pollOperatorClaim`
1399///
1400/// Asks this server to look for its operator's claim sooner than its timer
1401/// would. **Urgency, never authority.**
1402///
1403/// # Why this exists, and why it cannot be more than a hint
1404///
1405/// A server reaches [`ServerState::Claimed`](didbot_pds::ServerState) only
1406/// by *observing* a standing `bot.did.operator` record through its own poll.
1407/// Nothing here changes that: this method takes no body, reads no
1408/// credential, names no operator, and its entire effect is to wake a task
1409/// that was going to run anyway. A server that took anybody's word for a
1410/// claim would be a server anybody could claim.
1411///
1412/// What it buys is the gap between an operator running `didbot operate` and
1413/// the next scheduled poll, which is otherwise minutes of staring at a page
1414/// that says `unclaimed` after the work is already done.
1415///
1416/// # Why it cannot be turned into a poll amplifier
1417///
1418/// The bound is on this server's *outbound* rate, not on inbound requests:
1419/// `crate::operator_poll::MIN_POLL_INTERVAL` is a floor between two polls,
1420/// and every nudge arriving inside one window coalesces into the same single
1421/// poll. So an unbounded flood of nudges costs the operator's PDS at most
1422/// one extra request per window — which is what the timer was already going
1423/// to spend — and costs this server a wake-up on a task that exists. A rate
1424/// limiter on this route would bound how many nudges are *accepted* while
1425/// still letting an accepted burst multiply outbound requests, which is the
1426/// wrong quantity to bound.
1427///
1428/// It answers `202` — accepted, nothing has happened yet, which is literally
1429/// true — with a `Retry-After` naming the floor, so a caller that wants to
1430/// nudge again knows when it could matter.
1431async fn poll_operator_claim(State(state): State<AppState>) -> Response {
1432 state.nudge.nudge();
1433 (
1434 StatusCode::ACCEPTED,
1435 [(
1436 header::RETRY_AFTER,
1437 crate::operator_poll::MIN_POLL_INTERVAL
1438 .as_secs()
1439 .to_string(),
1440 )],
1441 Json(json!({})),
1442 )
1443 .into_response()
1444}
1445
1446/// `GET /health`
1447///
1448/// `status` is liveness: it is `"ok"` whenever this process can schedule a
1449/// task to answer at all, which is what a load balancer's health check
1450/// should gate traffic on — see `crates/didbot-serve/src/health.rs` for why
1451/// that is a different claim from `progress`, which reports whether the
1452/// deployment's own heartbeat tick is still advancing rather than merely
1453/// whether a socket accepted.
1454///
1455/// `certificates` is the second claim, and the one an alarm can be pointed
1456/// at: the deployment's health checks read it for certificate state,
1457/// and a renewal that keeps failing reaches expiry — a total outage for
1458/// every hostname in the zone — over weeks. Carried in this same response
1459/// rather than on a route of its own so the check already being made says
1460/// it; see `crates/didbot-serve/src/health.rs`.
1461///
1462/// `oauthPushes` is the third, and it is here because `POST /oauth/par`
1463/// cannot say it anywhere else: that endpoint is unauthenticated, so it
1464/// answers a flood aimed at one account exactly as it answers an ordinary
1465/// push, and the log line it writes is sampled. These are counts since this
1466/// process started, so an operator watches them move rather than reading a
1467/// level; see `crate::oauth::par::DroppedPushes`.
1468///
1469/// `foreignRecords` is the fourth, and the same shape: counts since the
1470/// process started, from the one reader policy evaluation reads other
1471/// people's repositories through. `refusals` is separated from `unknowns`
1472/// because a refusal is this deployment's own decision and an unknown is
1473/// somebody else's outage, and an operator watching either number climb
1474/// wants to know which it is looking at. See `didbot_lookup::Counts`.
1475///
1476/// `disk` is the fifth: the data volume's `freeBytes` and `totalBytes`,
1477/// read as this request is answered, and `alert`, the lowest free-space
1478/// watermark it is below. It is null for a run with no `--data`. See
1479/// `crate::health::HealthState::disk`.
1480///
1481/// `firehose` is the sixth: how many `subscribeRepos` consumers are
1482/// attached right now, and the newest sequence number assigned. A relay
1483/// that has stopped reading is `consumers` at zero while `newestSeq` moves.
1484///
1485/// `revision` is the commit the binary was built from, or null for a build
1486/// that was not told; see `crate::health::REVISION`. A deploy waits for it
1487/// to name the commit it shipped.
1488async fn health(State(state): State<AppState>) -> Response {
1489 let pushes = crate::oauth::par::dropped_pushes();
1490 let foreign = state.foreign.counts();
1491 Json(json!({
1492 "status": "ok",
1493 "revision": crate::health::REVISION,
1494 "zone": state.registry.zone(),
1495 "progress": state.health.snapshot(),
1496 "certificates": state.health.certificates().await,
1497 "oauthPushes": {
1498 "displaced": pushes.displaced,
1499 "unresolvedHint": pushes.unresolved_hint,
1500 "serverFull": pushes.server_full,
1501 },
1502 "foreignRecords": {
1503 "hits": foreign.hits,
1504 "misses": foreign.misses,
1505 "unknowns": foreign.unknowns,
1506 "refusals": foreign.refusals,
1507 },
1508 "disk": state.health.disk().await,
1509 "firehose": {
1510 "consumers": state.repos.subscribers(),
1511 "newestSeq": state.repos.newest(),
1512 },
1513 }))
1514 .into_response()
1515}
1516
1517/// `GET /xrpc/_health`
1518///
1519/// The reference PDS's health route, which pdscheck and debug.hose.cam probe
1520/// and read a server's version from. The same answer that server gives
1521/// (`packages/pds/src/basic-routes.ts` in bluesky-social/atproto at
1522/// `388dac9f`): `{"version": ...}`, or a 503 adding
1523/// `"error": "Service Unavailable"` while this deployment serves no reads.
1524/// `version` is the `revision` [`health`] reports, and is left out, as the
1525/// reference leaves out an unset one, for a build that was not told.
1526async fn xrpc_health(State(state): State<AppState>) -> Response {
1527 let mut body = serde_json::Map::new();
1528 if let Some(revision) = crate::health::REVISION {
1529 body.insert("version".to_owned(), json!(revision));
1530 }
1531 if state.lifecycle.snapshot().policy.serves_reads {
1532 return Json(body).into_response();
1533 }
1534 body.insert("error".to_owned(), json!("Service Unavailable"));
1535 (
1536 StatusCode::SERVICE_UNAVAILABLE,
1537 [(
1538 axum::http::header::RETRY_AFTER,
1539 didbot_pds::RETRY_AFTER_SECONDS.to_string(),
1540 )],
1541 Json(body),
1542 )
1543 .into_response()
1544}
1545
1546/// `GET /.well-known/did.json`
1547///
1548/// Every agent gets its own hostname, and locally they all land on one
1549/// listener, so the Host header is the only thing that says which account the
1550/// caller means.
1551async fn did_json(
1552 State(state): State<AppState>,
1553 uri: axum::http::Uri,
1554 headers: HeaderMap,
1555) -> Response {
1556 // A `404` here says "no such identity", which during `booting` or
1557 // `provisioning` is not yet true -- and for a `did:web` document the
1558 // difference matters, since a verifier treats an absent document as the
1559 // identity never having existed.
1560 if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "/.well-known/did.json") {
1561 return err.into_response();
1562 }
1563 let Some(host) = request_authority(&uri, &headers) else {
1564 return ApiError::bad_request("request names no host").into_response();
1565 };
1566 let host = host_without_port(host);
1567
1568 match state.registry.did_document(host) {
1569 // `application/json`, the type the did:web method expects a
1570 // document to be served with. atproto's own resolver refuses any
1571 // type but that one and `application/did+ld+json`.
1572 Some(document) => Json(document).into_response(),
1573 None => ApiError::new(
1574 StatusCode::NOT_FOUND,
1575 "NotFound",
1576 format!("no DID document for host `{host}`"),
1577 )
1578 .into_response(),
1579 }
1580}
1581
1582/// `GET /.well-known/atproto-did`
1583///
1584/// The other direction from `did.json`: that one says what a DID resolves to,
1585/// this says which DID answers to a name. A deployment that issues its agents
1586/// handles publishes a second hostname per account, and this is what makes it
1587/// a handle rather than a decoration — the plain-text DID, which is what the
1588/// atproto handle resolution rule asks for.
1589///
1590/// It answers exactly when the account's DID document claims the host as its
1591/// handle, because the registry derives both from one function. That is
1592/// atproto's bidirectional rule holding by construction rather than by
1593/// agreement: a resolver that follows this DID to its document and looks for
1594/// the handle in `alsoKnownAs` finds it, for every account, always. A name
1595/// nobody holds is a 404, which is the whole of what a resolver reads here.
1596async fn atproto_did(
1597 State(state): State<AppState>,
1598 uri: axum::http::Uri,
1599 headers: HeaderMap,
1600) -> Response {
1601 let Some(host) = request_authority(&uri, &headers) else {
1602 return ApiError::bad_request("request names no host").into_response();
1603 };
1604 let host = host_without_port(host);
1605
1606 match state.registry.handle_did(host) {
1607 // Plain text, not JSON: the response body is the DID and nothing else.
1608 Some(did) => ([(header::CONTENT_TYPE, "text/plain")], did).into_response(),
1609 None => ApiError::new(
1610 StatusCode::NOT_FOUND,
1611 "NotFound",
1612 format!("no account answers to the handle `{host}`"),
1613 )
1614 .into_response(),
1615 }
1616}
1617
1618/// The authority a request names, however its protocol version spells it.
1619///
1620/// HTTP/1.1 carries it in `Host`. HTTP/2 carries it as the `:authority`
1621/// pseudo-header, which arrives in the request URI and never as a header —
1622/// so a route that picks an account by hostname reads both, or answers only
1623/// the callers that spoke the older version. This server offers both over
1624/// TLS.
1625pub(crate) fn request_authority<'a>(
1626 uri: &'a axum::http::Uri,
1627 headers: &'a HeaderMap,
1628) -> Option<&'a str> {
1629 uri.authority()
1630 .map(axum::http::uri::Authority::as_str)
1631 .or_else(|| headers.get(header::HOST).and_then(|v| v.to_str().ok()))
1632}
1633
1634/// Strips a port from a Host header value.
1635///
1636/// A development zone is served on an explicit port, so `Host` arrives as
1637/// `agent.agents.localhost:3000` while the DID is registered under the bare
1638/// hostname. IPv6 literals arrive bracketed, and the brackets are dropped with
1639/// the port.
1640pub(crate) fn host_without_port(host: &str) -> &str {
1641 if let Some(rest) = host.strip_prefix('[') {
1642 return rest.split(']').next().unwrap_or(rest);
1643 }
1644 match host.rsplit_once(':') {
1645 Some((name, port)) if !port.is_empty() && port.bytes().all(|b| b.is_ascii_digit()) => name,
1646 _ => host,
1647 }
1648}
1649
1650/// The answer for a path nothing serves, and for an XRPC call nothing
1651/// proxies; see `crate::proxy::fallback`.
1652///
1653/// `MethodNotImplemented` under `/xrpc/`, which is the name atproto's XRPC
1654/// layer uses for a procedure a server does not implement, and a 501 to go
1655/// with it: a client that asked for a method this server does not serve should
1656/// learn that this server has not built it, not that the URL was a typo.
1657/// Anything outside `/xrpc/` is an ordinary 404.
1658pub(crate) fn unknown_route(uri: &axum::http::Uri) -> Response {
1659 if uri.path().starts_with("/xrpc/") {
1660 let nsid = uri.path().trim_start_matches("/xrpc/");
1661 return ApiError::new(
1662 StatusCode::NOT_IMPLEMENTED,
1663 "MethodNotImplemented",
1664 format!("this server does not implement `{nsid}`"),
1665 )
1666 .into_response();
1667 }
1668 ApiError::new(
1669 StatusCode::NOT_FOUND,
1670 "NotFound",
1671 format!("nothing is served at `{}`", uri.path()),
1672 )
1673 .into_response()
1674}
1675
1676/// The fallback for the right path and the wrong method.
1677///
1678/// A query fetched with POST, or a procedure called with GET. It is a real
1679/// mistake a hand-written client makes and an empty 405 does not say so.
1680async fn wrong_method(uri: axum::http::Uri) -> Response {
1681 ApiError::new(
1682 StatusCode::METHOD_NOT_ALLOWED,
1683 "InvalidRequest",
1684 format!("`{}` is not served with that HTTP method", uri.path()),
1685 )
1686 .into_response()
1687}
1688
1689/// `GET /xrpc/bot.did.stats`
1690///
1691/// `Credential::Disclosure`: open by default, this deployment's own
1692/// aggregate inventory — see `auth::Disclosure`. The health tick logs the
1693/// same figures on its own schedule by calling `Registry::stats`
1694/// in-process, so nothing internal reaches this route over HTTP.
1695async fn stats(State(state): State<AppState>) -> Response {
1696 if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.stats") {
1697 return err.into_response();
1698 }
1699 if let Err(err) = auth::require_disclosure(state.disclosure.stats, "bot.did.stats") {
1700 return err.into_response();
1701 }
1702 let enforced = state.registry.enforced_policies();
1703 Json(StatsResponse::from_stats(&state.registry.stats()).with_policy(enforced.as_deref()))
1704 .into_response()
1705}
1706
1707/// Resolves `identifier` — a handle or a DID — to an account DID this
1708/// deployment hosts.
1709///
1710/// What `POST /oauth/par` resolves a `login_hint` with: [`resolve_repo`]
1711/// under another name, kept separate because a sign-in failure and a
1712/// repository lookup failure want different error shapes, and folding them
1713/// into one function would make one of the two callers wrong.
1714fn resolve_identifier(state: &AppState, identifier: &str) -> Option<String> {
1715 if identifier.starts_with("did:") {
1716 state
1717 .registry
1718 .account(&canonical_did(identifier))
1719 .map(|a| a.did.as_str().to_owned())
1720 } else {
1721 state
1722 .registry
1723 .handle_did(identifier)
1724 .filter(|did| state.registry.account(did).is_some())
1725 }
1726}
1727
1728pub(crate) fn too_many_provisioning_attempts() -> Response {
1729 ApiError::rate_limited("too many provisioning attempts; slow down").into_response()
1730}
1731
1732/// `GET /xrpc/com.atproto.server.describeServer`
1733///
1734/// Discovery: what this server is, before a client has any account on it.
1735/// `availableUserDomains` is read out of the registry's real zone
1736/// configuration rather than stated as a fixed string, so a deployment with
1737/// several zones reports all of them — see
1738/// [`Registry::available_user_domains`]. Every other field the lexicon
1739/// declares optional is left off rather than answered with an invented
1740/// value; see [`DescribeServerResponse`]'s own doc for which and why.
1741async fn describe_server(State(state): State<AppState>) -> Response {
1742 // One of the two reads `didbot operate` makes before it can write a claim;
1743 // the other is the DID document above. Both must answer in `unclaimed`
1744 // or the handshake deadlocks, which `didbot_pds::server_state`'s
1745 // `the_handshake_cannot_deadlock` is what actually holds.
1746 if let Err(err) = require_lifecycle(
1747 &state,
1748 |p| p.serves_reads,
1749 "com.atproto.server.describeServer",
1750 ) {
1751 return err.into_response();
1752 }
1753 Json(DescribeServerResponse {
1754 did: state.registry.service_did(),
1755 available_user_domains: state.registry.available_user_domains(),
1756 })
1757 .into_response()
1758}