pub mod actions;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use bytes::Bytes;
use http_body_util::Full;
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use serde_json::{json, Value};
use tokio::sync::mpsc;
use tracing::{debug, error, trace, warn};
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::llm::ActionResult;
use crate::logging::emit::Log;
use crate::protocol::{Event, EventType};
use crate::server::accept_bounded::{
accept_bounded, watch_idle, ConnectionActivity, ConnectionLimiter,
};
use crate::server::connection::ConnectionId;
use crate::server::oci_registry::actions::{
oci_error_body, sha256_digest, OciRegistryProtocol, DEFAULT_BLOB_MEDIA_TYPE,
};
use crate::state::app_state::AppState;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VersionCheckMode {
Auto,
Llm,
}
impl VersionCheckMode {
pub fn parse(s: &str) -> anyhow::Result<Self> {
match s.to_ascii_lowercase().as_str() {
"auto" | "static" => Ok(Self::Auto),
"llm" | "model" => Ok(Self::Llm),
other => Err(anyhow::anyhow!(
"version_check must be \"auto\" or \"llm\", got \"{}\"",
other
)),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OciRoute {
VersionCheck,
Catalog,
TagsList { name: String },
Manifest { name: String, reference: String },
Blob { name: String, digest: String },
BlobUpload { name: String },
Referrers { name: String },
}
pub fn parse_v2_path(path: &str) -> Option<OciRoute> {
if path == "/v2" || path == "/v2/" {
return Some(OciRoute::VersionCheck);
}
let rest = path.strip_prefix("/v2/")?;
if rest == "_catalog" || rest == "_catalog/" {
return Some(OciRoute::Catalog);
}
if let Some(name) = rest.strip_suffix("/tags/list") {
return Some(OciRoute::TagsList {
name: name.to_string(),
});
}
if let Some(i) = rest.rfind("/blobs/uploads") {
return Some(OciRoute::BlobUpload {
name: rest[..i].to_string(),
});
}
if let Some(i) = rest.rfind("/manifests/") {
let reference = &rest[i + "/manifests/".len()..];
if !reference.is_empty() && !reference.contains('/') {
return Some(OciRoute::Manifest {
name: rest[..i].to_string(),
reference: reference.to_string(),
});
}
}
if let Some(i) = rest.rfind("/blobs/") {
let digest = &rest[i + "/blobs/".len()..];
if !digest.is_empty() && !digest.contains('/') {
return Some(OciRoute::Blob {
name: rest[..i].to_string(),
digest: digest.to_string(),
});
}
}
if let Some(i) = rest.rfind("/referrers/") {
return Some(OciRoute::Referrers {
name: rest[..i].to_string(),
});
}
None
}
pub fn is_valid_repository_name(name: &str) -> bool {
if name.is_empty() || name.len() > 255 {
return false;
}
name.split('/').all(|component| {
let bytes = component.as_bytes();
let alphanumeric = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit();
let mut i = 0;
loop {
let start = i;
while i < bytes.len() && alphanumeric(bytes[i]) {
i += 1;
}
if i == start {
return false;
}
if i == bytes.len() {
return true;
}
match bytes[i] {
b'.' => i += 1,
b'_' => {
i += 1;
if bytes.get(i) == Some(&b'_') {
i += 1;
}
}
b'-' => {
while bytes.get(i) == Some(&b'-') {
i += 1;
}
}
_ => return false,
}
}
})
}
pub fn is_digest_reference(reference: &str) -> bool {
reference.contains(':')
}
pub fn validate_sha256_digest(digest: &str) -> Result<(), String> {
let (algo, hex_part) = digest
.split_once(':')
.ok_or_else(|| format!("digest '{}' is not in <algorithm>:<hex> form", digest))?;
if algo != "sha256" {
return Err(format!(
"digest algorithm '{}' is not supported by this registry (only sha256)",
algo
));
}
if hex_part.len() != 64 || !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(format!(
"digest '{}' is not 64 lowercase hex characters",
digest
));
}
if hex_part.chars().any(|c| c.is_ascii_uppercase()) {
return Err(format!("digest '{}' must be lowercase hex", digest));
}
Ok(())
}
const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
pub struct OciRegistryServer;
impl OciRegistryServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
startup_params: Option<crate::protocol::StartupParams>,
) -> anyhow::Result<SocketAddr> {
let version_check = match startup_params.as_ref() {
Some(params) => match params.get_optional_string("version_check") {
Ok(Some(s)) => VersionCheckMode::parse(&s)?,
Ok(None) => VersionCheckMode::Auto,
Err(e) => {
return Err(anyhow::anyhow!(
"OCI registry startup parameter error: {}",
e
))
}
},
None => VersionCheckMode::Auto,
};
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"OCI registry listening on {} (version_check={:?}, pull-only)",
local_addr, version_check
));
let protocol = Arc::new(OciRegistryProtocol::new());
let task_registrar = app_state.clone();
let limiter = ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"OCI",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
Log::new(Some(&status_tx)).debug(format!(
"OCI registry connection {} from {}",
connection_id, remote_addr
));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_client = llm_client.clone();
let app_state_conn = app_state.clone();
let status_tx_conn = status_tx.clone();
let protocol = protocol.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
let spoke = matches!(
tokio::time::timeout(
FIRST_BYTE_READ_TIMEOUT,
stream.peek(&mut [0u8; 1]),
)
.await,
Ok(Ok(n)) if n > 0
);
if spoke {
let io = TokioIo::new(stream);
let app_state_service = app_state_conn.clone();
let status_tx_service = status_tx_conn.clone();
let activity = Arc::new(ConnectionActivity::new());
let activity_for_service = Arc::clone(&activity);
let service = service_fn(move |req: Request<Incoming>| {
let activity = Arc::clone(&activity_for_service);
let llm_client = llm_client.clone();
let app_state_service = app_state_service.clone();
let status_tx_service = status_tx_service.clone();
let protocol = protocol.clone();
async move {
let _busy = activity.busy();
handle_oci_request(
req,
connection_id,
remote_addr,
llm_client,
app_state_service,
status_tx_service,
protocol,
server_id,
version_check,
)
.await
}
});
let conn = http1::Builder::new().serve_connection(io, service);
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
if let Err(err) = result {
debug!("OCI registry connection ended: {:?}", err);
}
}
_ = watch_idle(
Arc::clone(&activity),
IDLE_BETWEEN_REQUESTS_TIMEOUT,
) => {
Log::new(Some(&status_tx_conn)).debug(format!(
"OCI registry connection {} idle for {}s; closing",
connection_id,
IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
));
}
}
} else {
Log::new(Some(&status_tx_conn)).debug(format!(
"OCI registry peer {} sent no request within {}s; closing \
decision=fail_closed_first_byte_timeout",
remote_addr,
FIRST_BYTE_READ_TIMEOUT.as_secs()
));
}
app_state_conn
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx_conn.send("__UPDATE_UI__".to_string());
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept OCI registry connection: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
fn build(status: u16, headers: Vec<(&str, String)>, body: Vec<u8>) -> Response<Full<Bytes>> {
let mut builder = Response::builder()
.status(StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR));
for (name, value) in headers {
if value.contains(['\r', '\n']) {
warn!("OCI registry: dropping header '{}' containing CR/LF", name);
continue;
}
builder = builder.header(name, value);
}
builder
.header("Content-Length", body.len().to_string())
.body(Full::new(Bytes::from(body)))
.unwrap_or_else(|e| {
error!("OCI registry: failed to build response: {}", e);
Response::new(Full::new(Bytes::new()))
})
}
fn error_response(
status: u16,
code: &str,
message: &str,
detail: Option<Value>,
) -> Response<Full<Bytes>> {
let body = oci_error_body(code, message, detail.as_ref());
build(
status,
vec![
("Content-Type", "application/json".to_string()),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
],
body.into_bytes(),
)
}
fn llm_error_response(err: &anyhow::Error) -> Response<Full<Bytes>> {
let failure = crate::utils::WireFailure::classify(err);
if failure.is_overloaded() {
return build(
503,
vec![
("Content-Type", "application/json".to_string()),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
("Retry-After", "1".to_string()),
],
oci_error_body("UNKNOWN", failure.text(), None).into_bytes(),
);
}
error_response(500, "UNKNOWN", failure.text(), None)
}
fn no_answer_response(what: &str) -> Response<Full<Bytes>> {
error_response(
500,
"UNKNOWN",
&format!(
"the registry backend returned no usable response for the {} request",
what
),
None,
)
}
fn approximate_request_bytes(req: &Request<Incoming>) -> u64 {
use hyper::body::Body;
let head: usize = req.method().as_str().len()
+ req.uri().to_string().len()
+ 12 + req
.headers()
.iter()
.map(|(name, value)| name.as_str().len() + value.len() + 4)
.sum::<usize>();
head as u64 + req.body().size_hint().lower()
}
#[allow(clippy::too_many_arguments)]
async fn handle_oci_request(
req: Request<Incoming>,
connection_id: ConnectionId,
remote_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OciRegistryProtocol>,
server_id: crate::state::ServerId,
version_check: VersionCheckMode,
) -> Result<Response<Full<Bytes>>, Infallible> {
let bytes_received = approximate_request_bytes(&req);
let response = handle_oci_request_inner(
req,
connection_id,
remote_addr,
llm_client,
app_state.clone(),
status_tx,
protocol,
server_id,
version_check,
)
.await;
let bytes_sent = response
.as_ref()
.ok()
.and_then(|resp| {
use hyper::body::Body;
resp.body().size_hint().exact()
})
.unwrap_or(0);
app_state
.update_connection_stats(
server_id,
connection_id,
Some(bytes_received),
Some(bytes_sent),
Some(1),
Some(1),
)
.await;
response
}
#[allow(clippy::too_many_arguments)]
async fn handle_oci_request_inner(
req: Request<Incoming>,
connection_id: ConnectionId,
remote_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OciRegistryProtocol>,
server_id: crate::state::ServerId,
version_check: VersionCheckMode,
) -> Result<Response<Full<Bytes>>, Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let path = uri.path().to_string();
let query = uri.query().unwrap_or("").to_string();
let accept = req
.headers()
.get_all(hyper::header::ACCEPT)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|v| v.split(',').map(|s| s.trim().to_string()))
.filter(|s| !s.is_empty())
.collect::<Vec<_>>();
let user_agent = req
.headers()
.get(hyper::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let authorization = req
.headers()
.get(hyper::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
Log::new(Some(&status_tx)).debug(format!("OCI {} {}", method, path));
trace!(
"OCI registry request: {} {} accept={:?} ua={}",
method,
path,
accept,
user_agent
);
let route = match parse_v2_path(&path) {
Some(r) => r,
None => {
return Ok(error_response(
404,
"UNSUPPORTED",
&format!(
"'{}' is not an OCI Distribution v2 endpoint; this registry serves /v2/",
path
),
None,
))
}
};
if !matches!(method, Method::GET | Method::HEAD) {
Log::new(Some(&status_tx)).debug(format!(
"OCI {} {} refused: this registry is pull-only",
method, path
));
return Ok(error_response(
405,
"UNSUPPORTED",
"this registry implements the OCI Distribution v1.1 pull path only; \
push (blob uploads and manifest PUT) is not supported",
Some(json!({"method": method.as_str(), "path": path})),
));
}
match &route {
OciRoute::BlobUpload { .. } => {
return Ok(error_response(
405,
"UNSUPPORTED",
"blob uploads are not supported: this registry has no storage and could not \
serve back what you pushed",
None,
))
}
OciRoute::Referrers { .. } => {
return Ok(error_response(
404,
"UNSUPPORTED",
"the referrers API is not implemented by this registry",
None,
))
}
_ => {}
}
let repo_name = match &route {
OciRoute::TagsList { name }
| OciRoute::Manifest { name, .. }
| OciRoute::Blob { name, .. } => Some(name.clone()),
_ => None,
};
if let Some(name) = &repo_name {
if !is_valid_repository_name(name) {
return Ok(error_response(
400,
"NAME_INVALID",
&format!("'{}' is not a valid OCI repository name", name),
None,
));
}
}
if let OciRoute::Blob { digest, .. } = &route {
if let Err(msg) = validate_sha256_digest(digest) {
return Ok(error_response(400, "DIGEST_INVALID", &msg, None));
}
}
if let OciRoute::Manifest { reference, .. } = &route {
if is_digest_reference(reference) {
if let Err(msg) = validate_sha256_digest(reference) {
return Ok(error_response(400, "DIGEST_INVALID", &msg, None));
}
}
}
if matches!(route, OciRoute::VersionCheck) && version_check == VersionCheckMode::Auto {
trace!("OCI /v2/ answered in-process (version_check=auto)");
return Ok(build(
200,
vec![
("Content-Type", "application/json".to_string()),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
],
b"{}".to_vec(),
));
}
if app_state.get_instruction(server_id).await.is_none() {
error!("OCI registry: server {} not found", server_id);
return Ok(error_response(
500,
"UNKNOWN",
"registry instance is no longer running",
None,
));
}
let query_params = parse_query(&query);
let (event_type, event_data): (&'static EventType, Value) = match &route {
OciRoute::VersionCheck => (
&actions::OCI_VERSION_CHECK,
json!({
"method": method.as_str(),
"path": path,
"authorization": authorization,
"user_agent": user_agent,
"client": remote_addr.to_string(),
}),
),
OciRoute::Catalog => (
&actions::OCI_CATALOG_REQUEST,
json!({
"method": method.as_str(),
"path": path,
"page_size": query_params.get("n"),
"last": query_params.get("last"),
"authorization": authorization,
"user_agent": user_agent,
"client": remote_addr.to_string(),
}),
),
OciRoute::TagsList { name } => (
&actions::OCI_TAGS_REQUEST,
json!({
"method": method.as_str(),
"path": path,
"name": name,
"page_size": query_params.get("n"),
"last": query_params.get("last"),
"authorization": authorization,
"user_agent": user_agent,
"client": remote_addr.to_string(),
}),
),
OciRoute::Manifest { name, reference } => (
&actions::OCI_MANIFEST_REQUEST,
json!({
"method": method.as_str(),
"path": path,
"name": name,
"reference": reference,
"by_digest": is_digest_reference(reference),
"accept": accept,
"authorization": authorization,
"user_agent": user_agent,
"client": remote_addr.to_string(),
}),
),
OciRoute::Blob { name, digest } => (
&actions::OCI_BLOB_REQUEST,
json!({
"method": method.as_str(),
"path": path,
"name": name,
"digest": digest,
"accept": accept,
"authorization": authorization,
"user_agent": user_agent,
"client": remote_addr.to_string(),
}),
),
OciRoute::BlobUpload { .. } | OciRoute::Referrers { .. } => unreachable!(),
};
let event = Event::new(event_type, event_data);
let llm_result = call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await;
let execution = match llm_result {
Ok(e) => e,
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
error!(
"OCI registry {} {} decision=fail_closed_llm_error category={:?}: {}",
method, path, failure, e
);
Log::new(Some(&status_tx)).warn(format!(
"OCI registry {} {} decision=fail_closed_llm_error category={:?}",
method, path, failure
));
return Ok(llm_error_response(&e));
}
};
for result in execution.protocol_results {
if let Some(response) = build_from_action(result, &route, &status_tx) {
return Ok(response);
}
}
Log::new(Some(&status_tx)).warn(format!(
"OCI registry {} {} decision=fail_closed_no_action - no usable action, refusing",
method, path
));
Ok(no_answer_response(route_label(&route)))
}
fn route_label(route: &OciRoute) -> &'static str {
match route {
OciRoute::VersionCheck => "version check",
OciRoute::Catalog => "catalog",
OciRoute::TagsList { .. } => "tag list",
OciRoute::Manifest { .. } => "manifest",
OciRoute::Blob { .. } => "blob",
OciRoute::BlobUpload { .. } => "blob upload",
OciRoute::Referrers { .. } => "referrers",
}
}
fn parse_query(query: &str) -> std::collections::HashMap<String, String> {
query
.split('&')
.filter(|s| !s.is_empty())
.filter_map(|pair| {
let (k, v) = pair.split_once('=')?;
Some((k.to_string(), v.to_string()))
})
.collect()
}
fn build_from_action(
result: ActionResult,
route: &OciRoute,
status_tx: &mpsc::UnboundedSender<String>,
) -> Option<Response<Full<Bytes>>> {
let (name, data) = match result {
ActionResult::Custom { name, data } => (name, data),
_ => return None,
};
match name.as_str() {
"oci_version_ok" => Some(build(
200,
vec![
("Content-Type", "application/json".to_string()),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
],
b"{}".to_vec(),
)),
"oci_auth_challenge" => {
let realm = data.get("realm").and_then(|v| v.as_str()).unwrap_or("");
let mut challenge = format!("Bearer realm=\"{}\"", realm);
if let Some(service) = data.get("service").and_then(|v| v.as_str()) {
challenge.push_str(&format!(",service=\"{}\"", service));
}
if let Some(scope) = data.get("scope").and_then(|v| v.as_str()) {
challenge.push_str(&format!(",scope=\"{}\"", scope));
}
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("authentication required");
Log::new(Some(status_tx)).debug(format!("OCI -> 401 auth challenge realm={}", realm));
Some(build(
401,
vec![
("Content-Type", "application/json".to_string()),
("WWW-Authenticate", challenge),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
],
oci_error_body("UNAUTHORIZED", message, None).into_bytes(),
))
}
"oci_catalog" => {
let repositories = data
.get("repositories")
.cloned()
.unwrap_or_else(|| json!([]));
let body = json!({"repositories": repositories}).to_string();
let mut headers = vec![
("Content-Type", "application/json".to_string()),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
];
if let Some(last) = data.get("next_last").and_then(|v| v.as_str()) {
headers.push((
"Link",
format!("</v2/_catalog?last={}>; rel=\"next\"", last),
));
}
Log::new(Some(status_tx)).debug(format!(
"OCI -> catalog ({} repositories)",
repositories.as_array().map(|a| a.len()).unwrap_or(0)
));
Some(build(200, headers, body.into_bytes()))
}
"oci_tags" => {
let requested_name = match route {
OciRoute::TagsList { name } => name.clone(),
_ => String::new(),
};
let name = data
.get("name")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.unwrap_or(requested_name);
let tags = data.get("tags").cloned().unwrap_or_else(|| json!([]));
let body = json!({"name": name, "tags": tags}).to_string();
Log::new(Some(status_tx)).debug(format!(
"OCI -> tag list for {} ({} tags)",
name,
tags.as_array().map(|a| a.len()).unwrap_or(0)
));
Some(build(
200,
vec![
("Content-Type", "application/json".to_string()),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
],
body.into_bytes(),
))
}
"oci_manifest" => {
let body = data.get("body").and_then(|v| v.as_str()).unwrap_or("");
let digest = data.get("digest").and_then(|v| v.as_str()).unwrap_or("");
let media_type = data
.get("media_type")
.and_then(|v| v.as_str())
.unwrap_or(actions::DEFAULT_MANIFEST_MEDIA_TYPE);
let computed = sha256_digest(body.as_bytes());
if computed != digest {
error!(
"OCI manifest digest desynchronised: action said {} but the body hashes \
to {}; refusing",
digest, computed
);
return Some(error_response(
500,
"MANIFEST_INVALID",
"internal digest mismatch while building the manifest response",
None,
));
}
if let OciRoute::Manifest { reference, .. } = route {
if is_digest_reference(reference) && reference != &computed {
Log::new(Some(status_tx)).warn(format!(
"OCI -> 404 MANIFEST_UNKNOWN: requested {} but content hashes to {}",
reference, computed
));
return Some(error_response(
404,
"MANIFEST_UNKNOWN",
"the content supplied for this manifest does not hash to the requested \
digest, so it cannot be served under it",
Some(json!({"requested": reference, "computed": computed})),
));
}
}
Log::new(Some(status_tx)).debug(format!(
"OCI -> manifest {} ({}, {} bytes)",
computed,
media_type,
body.len()
));
Some(build(
200,
vec![
("Content-Type", media_type.to_string()),
("Docker-Content-Digest", computed.clone()),
("Etag", format!("\"{}\"", computed)),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
],
body.as_bytes().to_vec(),
))
}
"oci_blob" => {
let bytes = match data.get("body_hex").and_then(|v| v.as_str()) {
Some(h) => match hex::decode(h) {
Ok(b) => b,
Err(e) => {
error!("OCI blob: internal hex decode failed: {}", e);
return Some(error_response(
500,
"UNKNOWN",
"internal error decoding blob content",
None,
));
}
},
None => return Some(no_answer_response("blob")),
};
let computed = sha256_digest(&bytes);
let media_type = data
.get("media_type")
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_BLOB_MEDIA_TYPE);
if let OciRoute::Blob { digest, .. } = route {
if digest != &computed {
Log::new(Some(status_tx)).warn(format!(
"OCI -> 404 BLOB_UNKNOWN: requested {} but content hashes to {}",
digest, computed
));
return Some(error_response(
404,
"BLOB_UNKNOWN",
"the content supplied for this blob does not hash to the requested \
digest, so it cannot be served under it",
Some(json!({"requested": digest, "computed": computed})),
));
}
}
Log::new(Some(status_tx)).debug(format!(
"OCI -> blob {} ({} bytes)",
computed,
bytes.len()
));
Some(build(
200,
vec![
("Content-Type", media_type.to_string()),
("Docker-Content-Digest", computed),
(
"Docker-Distribution-Api-Version",
"registry/2.0".to_string(),
),
],
bytes,
))
}
"oci_error" => {
let code = data
.get("code")
.and_then(|v| v.as_str())
.unwrap_or("UNKNOWN");
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("request refused");
let status = data
.get("status")
.and_then(|v| v.as_u64())
.map(|s| s as u16)
.unwrap_or(500);
let detail = data.get("detail").cloned().filter(|d| !d.is_null());
Log::new(Some(status_tx)).debug(format!(
"OCI -> {} {} decision=model_reject: {}",
status, code, message
));
Some(error_response(status, code, message, detail))
}
other => {
trace!("OCI registry: ignoring non-OCI action result '{}'", other);
None
}
}
}