1mod operational;
2mod protocol_routes;
3mod provider;
4mod provider_routes;
5mod reconstruction_helpers;
6mod reconstruction_routes;
7
8pub use provider::{
9 extract_provider_subject, latest_lifecycle_signal_at, reconciled_provider_repository_state,
10 validate_provider_name_path,
11};
12pub use reconstruction_helpers::{full_byte_stream_response, parse_batch_reconstruction_query};
13
14use std::{
15 num::NonZeroUsize,
16 sync::{
17 Arc,
18 atomic::{AtomicU64, Ordering},
19 },
20};
21
22use axum::{
23 Router,
24 extract::DefaultBodyLimit,
25 http::HeaderMap,
26 routing::{get, head, post},
27 serve as serve_http,
28};
29use shardline_protocol::{RepositoryScope, TokenScope};
30use tokio::net::TcpListener;
31use tokio::sync::OwnedSemaphorePermit;
32use tokio::sync::Semaphore;
33
34use crate::{
35 ServerConfig, ServerError,
36 auth::{AuthContext, ServerAuth},
37 backend::ServerBackend,
38 config::AuthProviderKind,
39 metrics::{MetricsLayer, metrics_routes},
40 provider::ProviderTokenService,
41 reconstruction_cache::ReconstructionCacheService,
42 server_frontend::ServerFrontend,
43 server_role::ServerRole,
44 transfer_limiter::TransferLimiter,
45 xet_adapter::{XET_READ_TOKEN_ROUTE, XET_WRITE_TOKEN_ROUTE, XORB_TRANSFER_ROUTE},
46};
47use operational::{
48 head_xorb, health, metrics, read_chunk, read_xorb_transfer, ready, stats, upload_shard,
49 upload_xorb,
50};
51use protocol_routes::{
52 bazel_get_ac, bazel_get_cas, bazel_put_ac, bazel_put_cas, lfs_batch, lfs_get_object,
53 lfs_head_object, lfs_put_object, oci_api_dispatch, oci_dispatch, oci_registry_token,
54 oci_transfer_dispatch, oci_v2_root,
55};
56pub(crate) use protocol_routes::{parse_oci_path, parse_upload_content_range};
57use provider_routes::{
58 git_lfs_authenticate, handle_provider_webhook, issue_provider_token, issue_xet_read_token,
59 issue_xet_write_token,
60};
61use reconstruction_routes::{batch_reconstruction, reconstruction, reconstruction_v2};
62
63pub const MAX_BATCH_RECONSTRUCTION_FILE_IDS: usize = 1024;
64pub const MAX_BATCH_RECONSTRUCTION_QUERY_BYTES: usize = 131_072;
65pub const MAX_LFS_BATCH_OBJECTS: usize = 1024;
66pub const MAX_OCI_MANIFEST_TAGS: usize = 128;
67pub const MAX_OCI_TAG_LIST_PAGE_SIZE: usize = 256;
68pub const MAX_PROTOCOL_QUERY_BYTES: usize = 16_384;
69pub const MAX_PROVIDER_TOKEN_REQUEST_BODY_BYTES: usize = 16_384;
70pub const MAX_PROVIDER_WEBHOOK_BODY_BYTES: usize = 1_048_576;
71pub const MAX_PROVIDER_NAME_BYTES: usize = 64;
72pub const MAX_PROVIDER_SUBJECT_BYTES: usize = 512;
73pub const MAX_PROVIDER_BASIC_AUTH_HEADER_BYTES: usize = 4096;
74
75#[derive(Debug)]
76pub struct AppState {
77 pub config: ServerConfig,
78 pub role: ServerRole,
79 pub backend: ServerBackend,
80 pub auth: Option<ServerAuth>,
81 pub provider_tokens: Option<ProviderTokenService>,
82 pub reconstruction_cache: ReconstructionCacheService,
83 pub transfer_limiter: TransferLimiter,
84 pub oci_registry_token_limiter: Arc<Semaphore>,
85 pub protocol_metrics: ProtocolMetrics,
86}
87
88#[derive(Debug, Default)]
89pub struct ProtocolMetrics {
90 oci_registry_token_requests_total: AtomicU64,
91 oci_registry_token_rate_limited_total: AtomicU64,
92 oci_registry_token_active_requests: AtomicU64,
93}
94
95impl ProtocolMetrics {
96 fn increment_oci_registry_token_requests(&self) {
97 let _previous = self
98 .oci_registry_token_requests_total
99 .fetch_add(1, Ordering::Relaxed);
100 }
101
102 fn increment_oci_registry_token_rate_limited(&self) {
103 let _previous = self
104 .oci_registry_token_rate_limited_total
105 .fetch_add(1, Ordering::Relaxed);
106 }
107
108 fn begin_oci_registry_token_request(&self) -> ActiveProtocolRequestGuard<'_> {
109 let _previous = self
110 .oci_registry_token_active_requests
111 .fetch_add(1, Ordering::Relaxed);
112 ActiveProtocolRequestGuard {
113 gauge: &self.oci_registry_token_active_requests,
114 }
115 }
116}
117
118#[derive(Debug)]
119struct ActiveProtocolRequestGuard<'metric> {
120 gauge: &'metric AtomicU64,
121}
122
123impl Drop for ActiveProtocolRequestGuard<'_> {
124 fn drop(&mut self) {
125 let _previous = self.gauge.fetch_sub(1, Ordering::Relaxed);
126 }
127}
128
129pub async fn router(config: ServerConfig) -> Result<Router, ServerError> {
135 config.validate_runtime_requirements()?;
136 let role = config.server_role();
137 let max_request_body_bytes = config.max_request_body_bytes();
138 let provider_token_body_limit = bounded_api_body_limit(
139 max_request_body_bytes,
140 MAX_PROVIDER_TOKEN_REQUEST_BODY_BYTES,
141 );
142 let provider_webhook_body_limit =
143 bounded_api_body_limit(max_request_body_bytes, MAX_PROVIDER_WEBHOOK_BODY_BYTES);
144 let backend = ServerBackend::from_config(&config).await?;
145 let auth = build_auth_provider(&config).await?;
146 let provider_tokens = if role.serves_api() {
147 match (
148 config.provider_config_path(),
149 config.provider_api_key(),
150 config.provider_token_issuer(),
151 config.provider_token_ttl_seconds(),
152 config.token_signing_key(),
153 ) {
154 (
155 Some(config_path),
156 Some(api_key),
157 Some(issuer),
158 Some(ttl_seconds),
159 Some(signing_key),
160 ) => Some(ProviderTokenService::from_file(
161 config_path,
162 api_key.to_vec(),
163 issuer,
164 ttl_seconds,
165 signing_key,
166 )?),
167 _ => None,
168 }
169 } else {
170 None
171 };
172 let reconstruction_cache = if role.uses_reconstruction_cache() {
173 ReconstructionCacheService::from_config(&config)?
174 } else {
175 ReconstructionCacheService::disabled()
176 };
177 let transfer_limiter =
178 TransferLimiter::new(config.chunk_size(), config.transfer_max_in_flight_chunks());
179 let oci_registry_token_limiter = Arc::new(Semaphore::new(
180 config.oci_registry_token_max_in_flight_requests().get(),
181 ));
182 let state = Arc::new(AppState {
183 config,
184 role,
185 backend,
186 auth,
187 provider_tokens,
188 reconstruction_cache,
189 transfer_limiter,
190 oci_registry_token_limiter,
191 protocol_metrics: ProtocolMetrics::default(),
192 });
193
194 let mut app = Router::new()
195 .route("/healthz", get(health))
196 .route("/readyz", get(ready))
197 .route("/metrics", get(metrics))
198 .layer(MetricsLayer);
199 if role.serves_api() {
200 app = app
201 .route(
202 "/v1/providers/{provider}/tokens",
203 post(issue_provider_token).layer(DefaultBodyLimit::max(provider_token_body_limit)),
204 )
205 .route(
206 "/v1/providers/{provider}/git-lfs-authenticate",
207 post(git_lfs_authenticate).layer(DefaultBodyLimit::max(provider_token_body_limit)),
208 )
209 .route(
210 "/v1/providers/{provider}/webhooks",
211 post(handle_provider_webhook)
212 .layer(DefaultBodyLimit::max(provider_webhook_body_limit)),
213 )
214 .route("/v1/stats", get(stats));
215 }
216 for frontend in state.config.server_frontends() {
217 app = register_frontend_routes(app, *frontend, role, &state)?;
218 }
219
220 Ok(app
221 .layer(DefaultBodyLimit::max(max_request_body_bytes.get()))
222 .with_state(state))
223}
224
225#[tracing::instrument(skip(config), fields(bind_addr = %config.bind_addr()))]
232pub async fn serve(config: ServerConfig) -> Result<(), ServerError> {
233 let listener = TcpListener::bind(config.bind_addr()).await?;
234 tracing::info!("listening on {}", config.bind_addr());
235 serve_with_listener(config, listener).await
236}
237
238pub async fn serve_with_listener(
245 config: ServerConfig,
246 listener: TcpListener,
247) -> Result<(), ServerError> {
248 let app = router(config).await?;
249 tracing::info!("router initialized, starting HTTP serve");
250 serve_http(listener, app).await?;
251 Ok(())
252}
253
254fn register_frontend_routes(
255 app: Router<Arc<AppState>>,
256 frontend: ServerFrontend,
257 role: ServerRole,
258 app_state: &AppState,
259) -> Result<Router<Arc<AppState>>, ServerError> {
260 match frontend {
261 ServerFrontend::Xet => Ok(register_xet_routes(app, role)),
262 ServerFrontend::Lfs => Ok(register_lfs_routes(app, role)),
263 ServerFrontend::BazelHttp => Ok(register_bazel_routes(app, role)),
264 ServerFrontend::Oci => Ok(register_oci_routes(app, role)),
265 ServerFrontend::Hub => register_hub_routes(app, app_state),
266 ServerFrontend::Metrics => Ok(app.merge(metrics_routes::<Arc<AppState>>())),
267 }
268}
269
270fn register_xet_routes(mut app: Router<Arc<AppState>>, role: ServerRole) -> Router<Arc<AppState>> {
271 if role.serves_api() {
272 app = app
273 .route(XET_READ_TOKEN_ROUTE, get(issue_xet_read_token))
274 .route(XET_WRITE_TOKEN_ROUTE, get(issue_xet_write_token))
275 .route("/reconstructions", get(batch_reconstruction))
276 .route("/v1/reconstructions", get(batch_reconstruction))
277 .route("/v1/reconstructions/{file_id}", get(reconstruction))
278 .route("/v2/reconstructions/{file_id}", get(reconstruction_v2))
279 .route("/shards", post(upload_shard))
280 .route("/v1/shards", post(upload_shard));
281 }
282 if role.serves_transfer() {
283 app = app
284 .route("/v1/chunks/default/{hash}", get(read_chunk))
285 .route("/v1/chunks/default-merkledb/{hash}", get(read_chunk))
286 .route(
287 "/v1/xorbs/default/{hash}",
288 head(head_xorb).post(upload_xorb),
289 )
290 .route(XORB_TRANSFER_ROUTE, get(read_xorb_transfer));
291 }
292 app
293}
294
295fn register_lfs_routes(mut app: Router<Arc<AppState>>, role: ServerRole) -> Router<Arc<AppState>> {
296 if role.serves_api() {
297 app = app.route("/v1/lfs/objects/batch", post(lfs_batch));
298 }
299 if role.serves_transfer() {
300 app = app.route(
301 "/v1/lfs/objects/{oid}",
302 get(lfs_get_object)
303 .head(lfs_head_object)
304 .put(lfs_put_object),
305 );
306 }
307 app
308}
309
310fn register_bazel_routes(
311 mut app: Router<Arc<AppState>>,
312 role: ServerRole,
313) -> Router<Arc<AppState>> {
314 if role.serves_transfer() {
315 app = app
316 .route(
317 "/v1/bazel/cache/ac/{hash}",
318 get(bazel_get_ac).put(bazel_put_ac),
319 )
320 .route(
321 "/v1/bazel/cache/cas/{hash}",
322 get(bazel_get_cas).put(bazel_put_cas),
323 );
324 }
325 app
326}
327
328fn register_oci_routes(mut app: Router<Arc<AppState>>, role: ServerRole) -> Router<Arc<AppState>> {
329 match role {
330 ServerRole::All => {
331 app = app
332 .route("/v2/token", get(oci_registry_token))
333 .route("/v2/", get(oci_v2_root))
334 .route("/v2/{*path}", axum::routing::any(oci_dispatch));
335 }
336 ServerRole::Api => {
337 app = app
338 .route("/v2/token", get(oci_registry_token))
339 .route("/v2/", get(oci_v2_root))
340 .route("/v2/{*path}", axum::routing::any(oci_api_dispatch));
341 }
342 ServerRole::Transfer => {
343 app = app.route("/v2/{*path}", axum::routing::any(oci_transfer_dispatch));
344 }
345 }
346 app
347}
348
349fn authorize(
350 state: &AppState,
351 headers: &HeaderMap,
352 required_scope: TokenScope,
353) -> Result<Option<AuthContext>, ServerError> {
354 if let Some(auth) = &state.auth {
355 return Ok(Some(auth.authorize(headers, required_scope)?));
356 }
357
358 Ok(None)
359}
360
361const fn scope_from_auth(auth: &AuthContext) -> &RepositoryScope {
362 auth.claims().repository()
363}
364
365#[must_use]
366pub fn bounded_api_body_limit(configured_limit: NonZeroUsize, endpoint_limit: usize) -> usize {
367 configured_limit.get().min(endpoint_limit)
368}
369
370fn register_hub_routes(
371 app: Router<Arc<AppState>>,
372 app_state: &AppState,
373) -> Result<Router<Arc<AppState>>, ServerError> {
374 let hub_auth = app_state
375 .auth
376 .as_ref()
377 .map(|sa| shardline_hub_api::auth::HubAuth::from_arc(sa.provider_arc()));
378
379 let root_dir = app_state.config.root_dir();
383 let store: shardline_index::hub::BoxedHubStore =
384 app_state.config.index_postgres_url().map_or_else(
385 || -> Result<shardline_index::hub::BoxedHubStore, ServerError> {
386 let hub_root = root_dir.join("hub");
387 if let Err(e) = std::fs::create_dir_all(&hub_root) {
388 tracing::warn!("failed to create hub directory: {e}");
389 }
390 let sqlite_store = shardline_index::LocalIndexStore::new(hub_root)
391 .map_err(|e| ServerError::Io(std::io::Error::other(e)))?;
392 Ok(shardline_index::hub::BoxedHubStore::from_store(
393 sqlite_store,
394 ))
395 },
396 |pg_url| -> Result<shardline_index::hub::BoxedHubStore, ServerError> {
397 let pool = sqlx::postgres::PgPoolOptions::new()
398 .max_connections(16)
399 .connect_lazy(pg_url)
400 .map_err(|e| ServerError::Io(std::io::Error::other(e)))?;
401 let pg_store = shardline_index::PostgresIndexStore::new(pool);
402 Ok(shardline_index::hub::BoxedHubStore::from_store(pg_store))
403 },
404 )?;
405
406 let http_client = reqwest::Client::builder()
408 .timeout(std::time::Duration::from_secs(10))
409 .build()
410 .ok();
411
412 let hub_state = shardline_hub_api::routes::HubState {
413 store,
414 auth: hub_auth,
415 http_client,
416 };
417 shardline_hub_api::init(hub_state);
418 Ok(app.merge(shardline_hub_api::hub_routes()))
419}
420
421fn endpoint_body_limit(
422 configured_limit: NonZeroUsize,
423 endpoint_limit: usize,
424) -> Result<NonZeroUsize, ServerError> {
425 NonZeroUsize::new(bounded_api_body_limit(configured_limit, endpoint_limit))
426 .ok_or(ServerError::Overflow)
427}
428
429pub async fn acquire_chunk_transfer_permit(
436 state: &AppState,
437 hash_hex: &str,
438) -> Result<OwnedSemaphorePermit, ServerError> {
439 let total_bytes = state.backend.chunk_length(hash_hex).await?;
440 state.transfer_limiter.acquire_bytes(total_bytes).await
441}
442
443async fn build_auth_provider(config: &ServerConfig) -> Result<Option<ServerAuth>, ServerError> {
444 match config.auth_provider() {
445 AuthProviderKind::Local => {
446 let Some(key) = config.token_signing_key() else {
447 return Ok(None);
448 };
449 Ok(Some(ServerAuth::new(key)?))
450 }
451 AuthProviderKind::Passthrough => {
452 let provider = Box::new(shardline_server_core::auth::PassthroughProvider);
453 Ok(Some(ServerAuth::from_provider(provider)))
454 }
455 AuthProviderKind::Oidc => {
456 let issuer = config.auth_oidc_issuer().ok_or_else(|| {
457 ServerError::Config(crate::config::ServerConfigError::InvalidAuthProvider)
458 })?;
459 let provider = crate::oidc_provider::OidcProvider::new(issuer, None)
460 .await
461 .map_err(|_e| {
462 ServerError::Config(crate::config::ServerConfigError::InvalidAuthProvider)
463 })?;
464 Ok(Some(ServerAuth::from_provider(Box::new(provider))))
465 }
466 AuthProviderKind::Jwks => {
467 let jwks_url = config.auth_jwks_url().ok_or_else(|| {
468 ServerError::Config(crate::config::ServerConfigError::InvalidAuthProvider)
469 })?;
470 let issuer = config.auth_jwks_issuer().unwrap_or("jwks");
471 let provider = crate::jwks_provider::JwksProvider::new(jwks_url, issuer)
472 .await
473 .map_err(|_e| {
474 ServerError::Config(crate::config::ServerConfigError::InvalidAuthProvider)
475 })?;
476 Ok(Some(ServerAuth::from_provider(Box::new(provider))))
477 }
478 }
479}
480
481#[cfg(test)]
482mod tests;