shardline-server 1.0.0

HTTP server boundary, runtime, and operator workflows for Shardline.
Documentation
mod blob_upload;
mod manifest;
pub(crate) mod path;
mod tags;
mod token;

pub(crate) use path::{OciPath, parse_oci_path};
pub(crate) use token::oci_registry_token;

use std::sync::Arc;

use axum::{
    body::Body,
    extract::{Path, State},
    http::{HeaderMap, Method, StatusCode, Uri},
    response::{IntoResponse, Response},
};
use shardline_protocol::TokenScope;

use crate::ServerError;

use super::{AppState, direct_object_response, scope_from_auth};
use blob_upload::{
    oci_delete_blob_upload, oci_get_blob_upload, oci_patch_blob_upload, oci_post_blob_upload,
    oci_put_blob_upload,
};
use manifest::{oci_delete_manifest, oci_get_manifest, oci_put_manifest};
use tags::oci_tags_list;
use token::oci_authorize;

pub(crate) async fn oci_v2_root(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
) -> Result<impl IntoResponse, ServerError> {
    let _auth = oci_authorize(&state, &headers, None, TokenScope::Read)?;
    Ok((
        StatusCode::OK,
        [("Docker-Distribution-API-Version", "registry/2.0")],
    ))
}

pub(crate) async fn oci_dispatch(
    method: Method,
    State(state): State<Arc<AppState>>,
    Path(path): Path<String>,
    headers: HeaderMap,
    uri: Uri,
    body: Body,
) -> Result<Response, ServerError> {
    let parsed = parse_oci_path(&path)?;
    oci_dispatch_parsed(&state, method, headers, uri, body, parsed).await
}

pub(crate) async fn oci_api_dispatch(
    method: Method,
    State(state): State<Arc<AppState>>,
    Path(path): Path<String>,
    headers: HeaderMap,
    uri: Uri,
    body: Body,
) -> Result<Response, ServerError> {
    let parsed = parse_oci_path(&path)?;
    if !oci_route_served_by_api(&method, &parsed) {
        return Err(ServerError::NotFound);
    }
    oci_dispatch_parsed(&state, method, headers, uri, body, parsed).await
}

pub(crate) async fn oci_transfer_dispatch(
    method: Method,
    State(state): State<Arc<AppState>>,
    Path(path): Path<String>,
    headers: HeaderMap,
    uri: Uri,
    body: Body,
) -> Result<Response, ServerError> {
    let parsed = parse_oci_path(&path)?;
    if !oci_route_served_by_transfer(&method, &parsed) {
        return Err(ServerError::NotFound);
    }
    oci_dispatch_parsed(&state, method, headers, uri, body, parsed).await
}

async fn oci_dispatch_parsed(
    state: &Arc<AppState>,
    method: Method,
    headers: HeaderMap,
    uri: Uri,
    body: Body,
    parsed: OciPath,
) -> Result<Response, ServerError> {
    match (method, parsed) {
        (
            Method::GET,
            OciPath::Blob {
                repository,
                digest_hex,
            },
        ) => {
            let auth = oci_authorize(state, &headers, Some(&repository), TokenScope::Read)?;
            let object_key = crate::oci_adapter::oci_blob_key(
                &repository,
                &digest_hex,
                auth.as_ref().map(scope_from_auth),
            )?;
            direct_object_response(
                state,
                &headers,
                &object_key,
                "application/octet-stream",
                Some(format!("sha256:{digest_hex}")),
            )
            .await
        }
        (
            Method::HEAD,
            OciPath::Blob {
                repository,
                digest_hex,
            },
        ) => {
            let auth = oci_authorize(state, &headers, Some(&repository), TokenScope::Read)?;
            let object_key = crate::oci_adapter::oci_blob_key(
                &repository,
                &digest_hex,
                auth.as_ref().map(scope_from_auth),
            )?;
            let total_length = state.backend.object_length(&object_key).await?;
            Ok(Response::builder()
                .status(StatusCode::OK)
                .header(axum::http::header::CONTENT_LENGTH, total_length.to_string())
                .header("Docker-Content-Digest", format!("sha256:{digest_hex}"))
                .body(Body::empty())
                .map_err(|_error| ServerError::Overflow)?)
        }
        (
            Method::GET,
            OciPath::Manifest {
                repository,
                reference,
            },
        ) => oci_get_manifest(state, &headers, &repository, &reference, false).await,
        (
            Method::HEAD,
            OciPath::Manifest {
                repository,
                reference,
            },
        ) => oci_get_manifest(state, &headers, &repository, &reference, true).await,
        (Method::GET, OciPath::TagsList { repository }) => {
            oci_tags_list(state, &headers, &uri, &repository).await
        }
        (Method::POST, OciPath::BlobUploads { repository }) => {
            oci_post_blob_upload(state, &headers, &uri, &repository, body).await
        }
        (
            Method::PATCH,
            OciPath::BlobUploadSession {
                repository,
                session_id,
            },
        ) => oci_patch_blob_upload(state, &headers, &headers, &repository, &session_id, body).await,
        (
            Method::PUT,
            OciPath::BlobUploadSession {
                repository,
                session_id,
            },
        ) => oci_put_blob_upload(state, &headers, &uri, &repository, &session_id, body).await,
        (
            Method::GET,
            OciPath::BlobUploadSession {
                repository,
                session_id,
            },
        ) => oci_get_blob_upload(state, &headers, &repository, &session_id).await,
        (
            Method::DELETE,
            OciPath::BlobUploadSession {
                repository,
                session_id,
            },
        ) => oci_delete_blob_upload(state, &headers, &repository, &session_id).await,
        (
            Method::PUT,
            OciPath::Manifest {
                repository,
                reference,
            },
        ) => oci_put_manifest(state, &headers, &uri, &repository, &reference, body).await,
        (
            Method::DELETE,
            OciPath::Manifest {
                repository,
                reference,
            },
        ) => oci_delete_manifest(state, &headers, &repository, &reference).await,
        _ => Err(ServerError::NotFound),
    }
}

#[allow(clippy::missing_const_for_fn)]
fn oci_route_served_by_api(method: &Method, path: &OciPath) -> bool {
    matches!(
        (method, path),
        (
            &Method::GET,
            OciPath::Manifest { .. } | OciPath::TagsList { .. }
        ) | (&Method::HEAD, OciPath::Manifest { .. })
            | (&Method::PUT, OciPath::Manifest { .. })
            | (&Method::DELETE, OciPath::Manifest { .. })
    )
}

#[allow(clippy::missing_const_for_fn)]
fn oci_route_served_by_transfer(method: &Method, path: &OciPath) -> bool {
    matches!(
        (method, path),
        (
            &Method::GET,
            OciPath::Blob { .. } | OciPath::BlobUploadSession { .. }
        ) | (&Method::HEAD, OciPath::Blob { .. })
            | (&Method::POST, OciPath::BlobUploads { .. })
            | (&Method::PATCH, OciPath::BlobUploadSession { .. })
            | (&Method::PUT, OciPath::BlobUploadSession { .. })
            | (&Method::DELETE, OciPath::BlobUploadSession { .. })
    )
}