Rust implementation of OCI Distribution Spec with granular access control

add manifest retrieval

+160 -38
+132 -24
src/manifests.rs
··· 4 4 // | end-7 | `PUT` | `/v2/<name>/manifests/<reference>` | `201` | `404` | 5 5 // | end-9 | `DELETE` | `/v2/<name>/manifests/<reference>` | `202` | `404`/`400`/`405` | 6 6 7 - use serde_json::{json, Value}; 7 + use serde_json::Value; 8 8 use std::sync::Arc; 9 9 10 - use crate::{ 11 - response::{not_found, not_implemented}, 12 - state, 13 - storage::write_manifest, 14 - }; 10 + use crate::{auth, state, storage}; 15 11 use axum::{ 16 12 body::Body, 17 13 extract::{Path, State}, 18 - http::Request, 19 - response::{Json, Response}, 14 + http::{HeaderMap, Request, StatusCode}, 15 + response::Response, 20 16 }; 21 17 18 + fn detect_manifest_content_type(manifest_data: &[u8]) -> String { 19 + if let Ok(json_str) = std::str::from_utf8(manifest_data) { 20 + if let Ok(parsed) = serde_json::from_str::<Value>(json_str) { 21 + if let Some(media_type) = parsed.get("mediaType").and_then(|v| v.as_str()) { 22 + return media_type.to_string(); 23 + } 24 + } 25 + } 26 + "application/vnd.oci.image.manifest.v1+json".to_string() 27 + } 28 + 22 29 // end-3 GET /v2/:name/manifests/:reference 23 30 pub(crate) async fn get_manifest_by_reference( 24 - State(data): State<Arc<state::App>>, 31 + State(state): State<Arc<state::App>>, 25 32 Path((org, repo, reference)): Path<(String, String, String)>, 26 - ) -> Json<Value> { 27 - let status = data.server_status.lock().await; 33 + headers: HeaderMap, 34 + ) -> Response<Body> { 35 + let host = &state.args.host; 36 + 37 + if auth::get(State(state.clone()), headers.clone()) 38 + .await 39 + .status() 40 + != StatusCode::OK 41 + { 42 + return Response::builder() 43 + .status(StatusCode::UNAUTHORIZED) 44 + .header( 45 + "WWW-Authenticate", 46 + format!("Basic realm=\"{}\", charset=\"UTF-8\"", host), 47 + ) 48 + .body(Body::from("401 Unauthorized")) 49 + .unwrap(); 50 + } 51 + 52 + let clean_reference = reference.strip_prefix("sha256:").unwrap_or(&reference); 53 + 28 54 log::info!( 29 55 "manifests/get_manifest_by_reference: org: {}, repo: {}, reference: {}", 30 56 org, 31 57 repo, 32 - reference 58 + clean_reference 33 59 ); 34 - Json(json!({ 35 - "not_implemented": format!("org {} repo {} reference {} server_status {}", org, repo, reference, status) 36 - })) 60 + 61 + match storage::read_manifest(&org, &repo, clean_reference) { 62 + Ok(manifest_data) => { 63 + let digest = sha256::digest(&manifest_data); 64 + let content_type = detect_manifest_content_type(&manifest_data); 65 + 66 + Response::builder() 67 + .status(StatusCode::OK) 68 + .header("Content-Length", manifest_data.len().to_string()) 69 + .header("Content-Type", content_type) 70 + .header("Docker-Content-Digest", format!("sha256:{}", digest)) 71 + .body(Body::from(manifest_data)) 72 + .unwrap() 73 + } 74 + Err(e) => { 75 + log::error!( 76 + "Failed to read manifest {}/{}/{}: {}", 77 + org, 78 + repo, 79 + clean_reference, 80 + e 81 + ); 82 + Response::builder() 83 + .status(StatusCode::NOT_FOUND) 84 + .body(Body::from("404 Not Found")) 85 + .unwrap() 86 + } 87 + } 37 88 } 38 89 39 90 // end-3 HEAD /v2/:name/manifests/:reference 40 91 pub(crate) async fn head_manifest_by_reference( 92 + State(state): State<Arc<state::App>>, 41 93 Path((org, repo, reference)): Path<(String, String, String)>, 42 - ) -> Response<String> { 94 + headers: HeaderMap, 95 + ) -> Response<Body> { 96 + let host = &state.args.host; 97 + 98 + if auth::get(State(state.clone()), headers.clone()) 99 + .await 100 + .status() 101 + != StatusCode::OK 102 + { 103 + return Response::builder() 104 + .status(StatusCode::UNAUTHORIZED) 105 + .header( 106 + "WWW-Authenticate", 107 + format!("Basic realm=\"{}\", charset=\"UTF-8\"", host), 108 + ) 109 + .body(Body::from("401 Unauthorized")) 110 + .unwrap(); 111 + } 112 + 113 + let clean_reference = reference.strip_prefix("sha256:").unwrap_or(&reference); 114 + 43 115 log::info!( 44 116 "manifests/head_manifest_by_reference: org: {}, repo: {}, reference: {}", 45 117 org, 46 118 repo, 47 - reference 119 + clean_reference 48 120 ); 49 121 50 - not_found() 122 + if !storage::manifest_exists(&org, &repo, clean_reference) { 123 + return Response::builder() 124 + .status(StatusCode::NOT_FOUND) 125 + .body(Body::from("404 Not Found")) 126 + .unwrap(); 127 + } 128 + 129 + match storage::read_manifest(&org, &repo, clean_reference) { 130 + Ok(manifest_data) => { 131 + let digest = sha256::digest(&manifest_data); 132 + let content_type = detect_manifest_content_type(&manifest_data); 133 + 134 + Response::builder() 135 + .status(StatusCode::OK) 136 + .header("Content-Length", manifest_data.len().to_string()) 137 + .header("Content-Type", content_type) 138 + .header("Docker-Content-Digest", format!("sha256:{}", digest)) 139 + .body(Body::empty()) 140 + .unwrap() 141 + } 142 + Err(e) => { 143 + log::error!( 144 + "Failed to read manifest {}/{}/{}: {}", 145 + org, 146 + repo, 147 + clean_reference, 148 + e 149 + ); 150 + Response::builder() 151 + .status(StatusCode::NOT_FOUND) 152 + .body(Body::from("404 Not Found")) 153 + .unwrap() 154 + } 155 + } 51 156 } 52 157 53 158 // end-7 PUT /v2/:name/manifests/:reference ··· 55 160 pub(crate) async fn put_manifest_by_reference( 56 161 Path((org, repo, reference)): Path<(String, String, String)>, 57 162 body: Request<Body>, 58 - ) -> Response<String> { 163 + ) -> Response { 59 164 log::info!( 60 165 "manifests/put_manifest_by_reference: org: {}, repo: {}, reference: {}", 61 166 org, ··· 63 168 reference 64 169 ); 65 170 66 - let success = write_manifest(&org, &repo, &reference, body.into_body()).await; 171 + let success = storage::write_manifest(&org, &repo, &reference, body.into_body()).await; 67 172 if !success { 68 173 return Response::builder() 69 174 .status(400) 70 - .body("400 Bad Request".to_string()) 175 + .body(Body::from("400 Bad Request")) 71 176 .expect("Failed to build response"); 72 177 } 73 178 ··· 77 182 "Location", 78 183 format!("/v2/{}/{}/manifests/{}", org, repo, reference), 79 184 ) 80 - .body("201 Created".to_string()) 185 + .body(Body::empty()) 81 186 .expect("Failed to build response") 82 187 } 83 188 ··· 85 190 pub(crate) async fn delete_manifest_by_reference( 86 191 Path(name): Path<String>, 87 192 Path(reference): Path<String>, 88 - ) -> Response<String> { 193 + ) -> Response<Body> { 89 194 log::info!( 90 195 "manifests/delete_manifest_by_reference: name: {}, reference: {}", 91 196 name, 92 197 reference 93 198 ); 94 - not_implemented() 199 + Response::builder() 200 + .status(StatusCode::NOT_IMPLEMENTED) 201 + .body(Body::from("501 Not Implemented")) 202 + .unwrap() 95 203 }
-14
src/response.rs
··· 1 1 use axum::http::Response; 2 2 3 - pub(crate) fn not_found() -> Response<String> { 4 - Response::builder() 5 - .status(404) 6 - .body("404 Not Found".to_string()) 7 - .unwrap() 8 - } 9 - 10 - pub(crate) fn not_implemented() -> Response<String> { 11 - Response::builder() 12 - .status(501) 13 - .body("501 Not Implemented".to_string()) 14 - .unwrap() 15 - } 16 - 17 3 pub(crate) fn unauthorized(host: &str) -> Response<String> { 18 4 Response::builder() 19 5 .status(401)
+28
src/storage.rs
··· 122 122 ); 123 123 std::fs::metadata(blob_path) 124 124 } 125 + 126 + pub(crate) fn read_manifest( 127 + org: &str, 128 + repo: &str, 129 + reference: &str, 130 + ) -> Result<Vec<u8>, std::io::Error> { 131 + let sanitized_org = sanitize_string(org); 132 + let sanitized_repo = sanitize_string(repo); 133 + let sanitized_reference = sanitize_string(reference); 134 + 135 + let manifest_path = format!( 136 + "./tmp/manifests/{}/{}/{}", 137 + sanitized_org, sanitized_repo, sanitized_reference 138 + ); 139 + std::fs::read(manifest_path) 140 + } 141 + 142 + pub(crate) fn manifest_exists(org: &str, repo: &str, reference: &str) -> bool { 143 + let sanitized_org = sanitize_string(org); 144 + let sanitized_repo = sanitize_string(repo); 145 + let sanitized_reference = sanitize_string(reference); 146 + 147 + let manifest_path = format!( 148 + "./tmp/manifests/{}/{}/{}", 149 + sanitized_org, sanitized_repo, sanitized_reference 150 + ); 151 + std::path::Path::new(&manifest_path).exists() 152 + }