From 1ed85db46fe2b4a4609ec07cb4e4368636257f3f Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Fri, 14 Nov 2025 23:24:10 +0100 Subject: [PATCH] add manifest retrieval --- src/manifests.rs | 156 +++++++++++++++++++++++++++++++++++++++-------- src/response.rs | 14 ----- src/storage.rs | 28 +++++++++ 3 files changed, 160 insertions(+), 38 deletions(-) diff --git a/src/manifests.rs b/src/manifests.rs index 339a8cc..a0e0dd6 100644 --- a/src/manifests.rs +++ b/src/manifests.rs @@ -4,50 +4,155 @@ // | end-7 | `PUT` | `/v2//manifests/` | `201` | `404` | // | end-9 | `DELETE` | `/v2//manifests/` | `202` | `404`/`400`/`405` | -use serde_json::{json, Value}; +use serde_json::Value; use std::sync::Arc; -use crate::{ - response::{not_found, not_implemented}, - state, - storage::write_manifest, -}; +use crate::{auth, state, storage}; use axum::{ body::Body, extract::{Path, State}, - http::Request, - response::{Json, Response}, + http::{HeaderMap, Request, StatusCode}, + response::Response, }; +fn detect_manifest_content_type(manifest_data: &[u8]) -> String { + if let Ok(json_str) = std::str::from_utf8(manifest_data) { + if let Ok(parsed) = serde_json::from_str::(json_str) { + if let Some(media_type) = parsed.get("mediaType").and_then(|v| v.as_str()) { + return media_type.to_string(); + } + } + } + "application/vnd.oci.image.manifest.v1+json".to_string() +} + // end-3 GET /v2/:name/manifests/:reference pub(crate) async fn get_manifest_by_reference( - State(data): State>, + State(state): State>, Path((org, repo, reference)): Path<(String, String, String)>, -) -> Json { - let status = data.server_status.lock().await; + headers: HeaderMap, +) -> Response { + let host = &state.args.host; + + if auth::get(State(state.clone()), headers.clone()) + .await + .status() + != StatusCode::OK + { + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header( + "WWW-Authenticate", + format!("Basic realm=\"{}\", charset=\"UTF-8\"", host), + ) + .body(Body::from("401 Unauthorized")) + .unwrap(); + } + + let clean_reference = reference.strip_prefix("sha256:").unwrap_or(&reference); + log::info!( "manifests/get_manifest_by_reference: org: {}, repo: {}, reference: {}", org, repo, - reference + clean_reference ); - Json(json!({ - "not_implemented": format!("org {} repo {} reference {} server_status {}", org, repo, reference, status) - })) + + match storage::read_manifest(&org, &repo, clean_reference) { + Ok(manifest_data) => { + let digest = sha256::digest(&manifest_data); + let content_type = detect_manifest_content_type(&manifest_data); + + Response::builder() + .status(StatusCode::OK) + .header("Content-Length", manifest_data.len().to_string()) + .header("Content-Type", content_type) + .header("Docker-Content-Digest", format!("sha256:{}", digest)) + .body(Body::from(manifest_data)) + .unwrap() + } + Err(e) => { + log::error!( + "Failed to read manifest {}/{}/{}: {}", + org, + repo, + clean_reference, + e + ); + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("404 Not Found")) + .unwrap() + } + } } // end-3 HEAD /v2/:name/manifests/:reference pub(crate) async fn head_manifest_by_reference( + State(state): State>, Path((org, repo, reference)): Path<(String, String, String)>, -) -> Response { + headers: HeaderMap, +) -> Response { + let host = &state.args.host; + + if auth::get(State(state.clone()), headers.clone()) + .await + .status() + != StatusCode::OK + { + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header( + "WWW-Authenticate", + format!("Basic realm=\"{}\", charset=\"UTF-8\"", host), + ) + .body(Body::from("401 Unauthorized")) + .unwrap(); + } + + let clean_reference = reference.strip_prefix("sha256:").unwrap_or(&reference); + log::info!( "manifests/head_manifest_by_reference: org: {}, repo: {}, reference: {}", org, repo, - reference + clean_reference ); - not_found() + if !storage::manifest_exists(&org, &repo, clean_reference) { + return Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("404 Not Found")) + .unwrap(); + } + + match storage::read_manifest(&org, &repo, clean_reference) { + Ok(manifest_data) => { + let digest = sha256::digest(&manifest_data); + let content_type = detect_manifest_content_type(&manifest_data); + + Response::builder() + .status(StatusCode::OK) + .header("Content-Length", manifest_data.len().to_string()) + .header("Content-Type", content_type) + .header("Docker-Content-Digest", format!("sha256:{}", digest)) + .body(Body::empty()) + .unwrap() + } + Err(e) => { + log::error!( + "Failed to read manifest {}/{}/{}: {}", + org, + repo, + clean_reference, + e + ); + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("404 Not Found")) + .unwrap() + } + } } // end-7 PUT /v2/:name/manifests/:reference @@ -55,7 +160,7 @@ pub(crate) async fn head_manifest_by_reference( pub(crate) async fn put_manifest_by_reference( Path((org, repo, reference)): Path<(String, String, String)>, body: Request, -) -> Response { +) -> Response { log::info!( "manifests/put_manifest_by_reference: org: {}, repo: {}, reference: {}", org, @@ -63,11 +168,11 @@ pub(crate) async fn put_manifest_by_reference( reference ); - let success = write_manifest(&org, &repo, &reference, body.into_body()).await; + let success = storage::write_manifest(&org, &repo, &reference, body.into_body()).await; if !success { return Response::builder() .status(400) - .body("400 Bad Request".to_string()) + .body(Body::from("400 Bad Request")) .expect("Failed to build response"); } @@ -77,7 +182,7 @@ pub(crate) async fn put_manifest_by_reference( "Location", format!("/v2/{}/{}/manifests/{}", org, repo, reference), ) - .body("201 Created".to_string()) + .body(Body::empty()) .expect("Failed to build response") } @@ -85,11 +190,14 @@ pub(crate) async fn put_manifest_by_reference( pub(crate) async fn delete_manifest_by_reference( Path(name): Path, Path(reference): Path, -) -> Response { +) -> Response { log::info!( "manifests/delete_manifest_by_reference: name: {}, reference: {}", name, reference ); - not_implemented() + Response::builder() + .status(StatusCode::NOT_IMPLEMENTED) + .body(Body::from("501 Not Implemented")) + .unwrap() } diff --git a/src/response.rs b/src/response.rs index 55a37f1..4b73215 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,19 +1,5 @@ use axum::http::Response; -pub(crate) fn not_found() -> Response { - Response::builder() - .status(404) - .body("404 Not Found".to_string()) - .unwrap() -} - -pub(crate) fn not_implemented() -> Response { - Response::builder() - .status(501) - .body("501 Not Implemented".to_string()) - .unwrap() -} - pub(crate) fn unauthorized(host: &str) -> Response { Response::builder() .status(401) diff --git a/src/storage.rs b/src/storage.rs index c216147..55af9c7 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -122,3 +122,31 @@ pub(crate) fn blob_metadata( ); std::fs::metadata(blob_path) } + +pub(crate) fn read_manifest( + org: &str, + repo: &str, + reference: &str, +) -> Result, std::io::Error> { + let sanitized_org = sanitize_string(org); + let sanitized_repo = sanitize_string(repo); + let sanitized_reference = sanitize_string(reference); + + let manifest_path = format!( + "./tmp/manifests/{}/{}/{}", + sanitized_org, sanitized_repo, sanitized_reference + ); + std::fs::read(manifest_path) +} + +pub(crate) fn manifest_exists(org: &str, repo: &str, reference: &str) -> bool { + let sanitized_org = sanitize_string(org); + let sanitized_repo = sanitize_string(repo); + let sanitized_reference = sanitize_string(reference); + + let manifest_path = format!( + "./tmp/manifests/{}/{}/{}", + sanitized_org, sanitized_repo, sanitized_reference + ); + std::path::Path::new(&manifest_path).exists() +} -- 2.51.2