///|
enum RegistryRoute {
Root
Catalog
Blob(String, String)
UploadStart(String)
UploadChunk(String, String)
Manifest(String, String)
Referrers(String, String)
Tags(String)
Unknown
}
///|
fn without_query(path : String) -> String {
match path.find("?") {
Some(index) => path[:index].to_owned()
None => path
}
}
///|
fn query_value(path : String, key : String) -> String? {
match path.find("?") {
Some(index) => {
let query : StringView = path[index + 1:]
for field in query.split("&") {
match field.find("=") {
Some(equal) if field[:equal] == key =>
return decode_query_value(field[equal + 1:].to_owned())
_ => continue
}
}
None
}
None => None
}
}
///|
fn query_values(path : String, key : String) -> Array[String]? {
let values : Array[String] = []
match path.find("?") {
Some(index) => {
let query : StringView = path[index + 1:]
for field in query.split("&") {
match field.find("=") {
Some(equal) if field[:equal] == key =>
match decode_query_value(field[equal + 1:].to_owned()) {
Some(value) => values.push(value)
None => return None
}
_ => continue
}
}
Some(values)
}
None => Some(values)
}
}
///|
fn hex_nibble(value : Byte) -> Int? {
if value >= b'0' && value <= b'9' {
Some(value.to_int() - b'0'.to_int())
} else if value >= b'a' && value <= b'f' {
Some(value.to_int() - b'a'.to_int() + 10)
} else if value >= b'A' && value <= b'F' {
Some(value.to_int() - b'A'.to_int() + 10)
} else {
None
}
}
///|
fn decode_query_value(value : String) -> String? {
let input = @utf8.encode(value)
let output = Buffer()
let mut index = 0
while index < input.length() {
if input[index] == b'%' {
guard index + 2 < input.length() else { return None }
let high = match hex_nibble(input[index + 1]) {
Some(nibble) => nibble
None => return None
}
let low = match hex_nibble(input[index + 2]) {
Some(nibble) => nibble
None => return None
}
output.write_byte((high * 16 + low).to_byte())
index += 3
} else {
output.write_byte(if input[index] == b'+' { b' ' } else { input[index] })
index += 1
}
}
Some(@utf8.decode(output.contents())) catch {
_ => None
}
}
///|
fn uppercase_hex_digit(value : Int) -> Byte {
if value < 10 {
(b'0'.to_int() + value).to_byte()
} else {
(b'A'.to_int() + value - 10).to_byte()
}
}
///|
/// Encode one UTF-8 query value so pagination links remain valid for media
/// types containing reserved characters such as `+`, `&`, or `=`.
fn encode_query_value(value : String) -> String {
let input = @utf8.encode(value)
let output = Buffer()
for byte in input {
if (byte >= b'a' && byte <= b'z') ||
(byte >= b'A' && byte <= b'Z') ||
(byte >= b'0' && byte <= b'9') ||
byte == b'-' ||
byte == b'_' ||
byte == b'.' ||
byte == b'~' {
output.write_byte(byte)
} else {
let number = byte.to_int()
output.write_byte(b'%')
output.write_byte(uppercase_hex_digit(number / 16))
output.write_byte(uppercase_hex_digit(number % 16))
}
}
@utf8.decode(output.contents()) catch {
_ => ""
}
}
///|
fn query_digest(path : String) -> String? {
query_value(path, "digest")
}
///|
fn route_for(path : String) -> RegistryRoute {
let clean = without_query(path)
if clean == "/v2/" || clean == "/v2" {
return Root
}
if clean == "/v2/_catalog" {
return Catalog
}
guard clean.length() >= 4 && clean[:4] == "/v2/" else { return Unknown }
let rest : StringView = clean[4:]
if rest.rev_find("/blobs/uploads/") is Some(index) &&
rest[index + 15:].find("/") is None {
let repository = rest[:index].to_owned()
let upload_id = rest[index + 15:].to_owned()
if upload_id == "" {
UploadStart(repository)
} else {
UploadChunk(repository, upload_id)
}
} else if rest.rev_find("/blobs/") is Some(index) &&
rest[index + 7:].find("/") is None {
Blob(rest[:index].to_owned(), rest[index + 7:].to_owned())
} else if rest.rev_find("/manifests/") is Some(index) &&
rest[index + 11:].find("/") is None {
Manifest(rest[:index].to_owned(), rest[index + 11:].to_owned())
} else if rest.rev_find("/referrers/") is Some(index) &&
rest[index + 11:].find("/") is None {
Referrers(rest[:index].to_owned(), rest[index + 11:].to_owned())
} else if rest.rev_find("/tags/list") is Some(index) &&
index + 10 == rest.length() {
Tags(rest[:index].to_owned())
} else {
Unknown
}
}
///|
async fn send_error(
conn : @http.ServerConnection,
code : Int,
reason : String,
) -> Unit {
let body = "{\"errors\":[{\"code\":\"" +
reason +
"\",\"message\":\"" +
reason +
"\"}]}"
conn
..send_response(code, reason, extra_headers={
"Content-Type": "application/json",
})
..write(@utf8.encode(body))
.end_response()
}
///|
async fn send_empty(
conn : @http.ServerConnection,
code : Int,
reason : String,
) -> Unit {
conn.send_response(code, reason, extra_headers={
"Docker-Distribution-API-Version": "registry/2.0",
})
conn.end_response()
}
///|
async fn send_blob(
conn : @http.ServerConnection,
code : Int,
reason : String,
body : Bytes,
digest : String,
) -> Unit {
conn
..send_response(code, reason, extra_headers={
"Content-Type": "application/octet-stream",
"Content-Length": body.length().to_string(),
"Docker-Content-Digest": digest,
"Accept-Ranges": "bytes",
})
..write(body)
.end_response()
}
///|
enum BlobRange {
Full
Partial(Int, Int)
Unsatisfiable
}
///|
/// A registry can ignore unsupported or malformed ranges. A valid range
/// beyond the end of the blob gets a 416 response instead.
fn select_blob_range(headers : @http.Headers, size : Int) -> BlobRange {
let value = match headers.get("Range") {
Some(value) => value
None => return Full
}
guard value.length() >= 6 && value[:6] == "bytes=" else { return Full }
let spec : StringView = value[6:]
guard spec.find(",") is None else { return Full }
let dash = match spec.find("-") {
Some(index) => index
None => return Full
}
if dash == 0 {
let suffix = @string.parse_int(spec[1:]) catch { _ => return Full }
guard suffix > 0 && size > 0 else { return Unsatisfiable }
let start = if suffix >= size { 0 } else { size - suffix }
return Partial(start, size - 1)
}
let start = @string.parse_int(spec[:dash]) catch { _ => return Full }
guard start >= 0 else { return Full }
if dash + 1 == spec.length() {
guard start < size else { return Unsatisfiable }
return Partial(start, size - 1)
}
let end = @string.parse_int(spec[dash + 1:]) catch { _ => return Full }
guard end >= start else { return Full }
guard start < size else { return Unsatisfiable }
Partial(start, if end >= size { size - 1 } else { end })
}
///|
async fn send_manifest(
conn : @http.ServerConnection,
code : Int,
reason : String,
body : Bytes,
content_type : String,
) -> Unit {
let digest = sha256_digest(body)
conn
..send_response(code, reason, extra_headers={
"Content-Type": content_type,
"Content-Length": body.length().to_string(),
"Docker-Content-Digest": digest,
})
..write(body)
.end_response()
}
///|
/// OCI manifests and indexes are JSON objects. Decode as UTF-8 first so
/// malformed payloads cannot be persisted as apparently valid manifests.
fn valid_manifest_json(body : Bytes) -> Bool {
let text = @utf8.decode(body) catch { _ => return false }
let value = @json.parse(text) catch { _ => return false }
match value {
Object(object) => {
let has_schema_v2 = match object.get("schemaVersion") {
Some(Number(version, ..)) => version == 2.0
_ => false
}
has_schema_v2 && valid_manifest_fields(object)
}
_ => false
}
}
///|
fn valid_media_type_token(value : StringView) -> Bool {
guard value.length() > 0 else { return false }
for index in 0.. Bool {
guard value.find("\r") is None && value.find("\n") is None else {
return false
}
let slash = match value.find("/") {
Some(index) => index
None => return false
}
guard value[slash + 1:].find("/") is None else { return false }
valid_media_type_token(value[:slash]) &&
valid_media_type_token(value[slash + 1:])
}
///|
fn valid_descriptor(value : Json) -> Bool {
guard value is Object(object) else { return false }
let valid_type = match object.get("mediaType") {
Some(String(value)) => valid_media_type(value)
_ => false
}
let valid_digest_value = match object.get("digest") {
Some(String(value)) => valid_digest(value)
_ => false
}
let valid_size = match object.get("size") {
Some(Number(value, ..)) => value >= 0.0 && value.floor() == value
_ => false
}
valid_type && valid_digest_value && valid_size
}
///|
fn valid_optional_descriptor(object : Map[String, Json], key : String) -> Bool {
match object.get(key) {
Some(value) => valid_descriptor(value)
None => true
}
}
///|
fn valid_optional_descriptor_list(
object : Map[String, Json],
key : String,
) -> Bool {
match object.get(key) {
Some(Array(values)) => {
for value in values {
if !valid_descriptor(value) {
return false
}
}
true
}
None => true
_ => false
}
}
///|
fn valid_manifest_fields(object : Map[String, Json]) -> Bool {
match object.get("mediaType") {
Some(String(value)) if valid_media_type(value) => ()
None => ()
_ => return false
}
match object.get("artifactType") {
Some(String(value)) if valid_media_type(value) => ()
None => ()
_ => return false
}
valid_optional_descriptor(object, "config") &&
valid_optional_descriptor(object, "subject") &&
valid_optional_descriptor_list(object, "layers") &&
valid_optional_descriptor_list(object, "blobs") &&
valid_optional_descriptor_list(object, "manifests")
}
///|
fn descriptor_digest(value : Json) -> String? {
guard value is Object(object) else { return None }
match object.get("digest") {
Some(String(digest)) if valid_digest(digest) => Some(digest)
_ => None
}
}
///|
fn append_descriptor_digests(value : Json, output : Array[String]) -> Bool {
match value {
Array(values) => {
for descriptor in values {
match descriptor_digest(descriptor) {
Some(digest) => output.push(digest)
None => return false
}
}
true
}
_ => false
}
}
///|
/// Separate blob dependencies from nested manifests; subject is a relationship
/// target and is intentionally not required to exist for an artifact push.
fn manifest_dependencies(body : Bytes) -> (Array[String], Array[String])? {
let text = @utf8.decode(body) catch { _ => return None }
let value = @json.parse(text) catch { _ => return None }
guard value is Object(object) else { return None }
let blobs : Array[String] = []
let manifests : Array[String] = []
match object.get("config") {
Some(descriptor) =>
match descriptor_digest(descriptor) {
Some(digest) => blobs.push(digest)
None => return None
}
None => ()
}
for key in ["layers", "blobs"] {
match object.get(key) {
Some(value) if append_descriptor_digests(value, blobs) => ()
Some(_) => return None
None => ()
}
}
match object.get("manifests") {
Some(value) if append_descriptor_digests(value, manifests) => ()
Some(_) => return None
None => ()
}
Some((blobs, manifests))
}
///|
async fn manifest_dependencies_exist(
store : RegistryStore,
repository : String,
body : Bytes,
) -> Bool {
let (blobs, manifests) = match manifest_dependencies(body) {
Some(value) => value
None => return false
}
for digest in blobs {
if !store.has_repository_blob(repository, digest) {
return false
}
}
for digest in manifests {
if store.get_manifest(repository, digest) is None {
return false
}
}
true
}
///|
fn manifest_reference_matches(reference : String, digest : String) -> Bool {
!valid_digest(reference) || reference == digest
}
///|
fn trim_http_ows(value : StringView) -> String {
let mut start = 0
let mut end = value.length()
while start < end &&
(value[start].to_int() == 32 || value[start].to_int() == 9) {
start += 1
}
while end > start &&
(value[end - 1].to_int() == 32 || value[end - 1].to_int() == 9) {
end -= 1
}
value[start:end].to_owned()
}
///|
fn manifest_body_media_type(body : Bytes) -> String? {
let text = @utf8.decode(body) catch { _ => return None }
let value = @json.parse(text) catch { _ => return None }
guard value is Object(object) else { return None }
match object.get("mediaType") {
Some(String(value)) if valid_media_type(value) => Some(value)
_ => None
}
}
///|
fn valid_manifest_content_type(headers : @http.Headers, body : Bytes) -> Bool {
match headers.get("Content-Type") {
Some(raw) => {
guard raw.find("\r") is None && raw.find("\n") is None else {
return false
}
let base : StringView = match raw.find(";") {
Some(index) => raw[:index]
None => raw[:]
}
let normalized = trim_http_ows(base)
guard valid_media_type(normalized) else { return false }
match manifest_body_media_type(body) {
Some(media_type) => normalized == media_type
None => normalized != ""
}
}
None => true
}
}
///|
fn request_manifest_content_type(
headers : @http.Headers,
body : Bytes,
) -> String {
let default_type = "application/vnd.oci.image.manifest.v1+json"
let raw = match headers.get("Content-Type") {
Some(value) => value
None =>
return match manifest_body_media_type(body) {
Some(value) => value
None => default_type
}
}
guard raw.find("\r") is None && raw.find("\n") is None else {
return default_type
}
let base : StringView = match raw.find(";") {
Some(index) => raw[:index]
None => raw[:]
}
let normalized = trim_http_ows(base)
if normalized == "" {
default_type
} else {
normalized
}
}
///|
fn tag_header_value(tags : Array[String]) -> String {
let mut value = ""
for index, tag in tags {
if index > 0 {
value += ", "
}
value += tag
}
value
}
///|
fn manifest_subject(body : Bytes) -> String? {
let text = @utf8.decode(body) catch { _ => return None }
let value = @json.parse(text) catch { _ => return None }
guard value is Object(object) else { return None }
guard object.get("subject") is Some(Object(subject)) else { return None }
match subject.get("digest") {
Some(String(digest)) if valid_digest(digest) => Some(digest)
_ => None
}
}
///|
fn referrer_descriptor(
body : Bytes,
digest : String,
media_type : String,
) -> (String, String?, Json)? {
let text = @utf8.decode(body) catch { _ => return None }
let value = @json.parse(text) catch { _ => return None }
guard value is Object(object) else { return None }
let subject = match object.get("subject") {
Some(Object(subject)) =>
match subject.get("digest") {
Some(String(value)) if valid_digest(value) => value
_ => return None
}
_ => return None
}
let artifact_type = match object.get("artifactType") {
Some(String(value)) if value != "" => Some(value)
_ if media_type == "application/vnd.oci.image.index.v1+json" ||
media_type == "application/vnd.docker.distribution.manifest.list.v2+json" =>
None
_ =>
match object.get("config") {
Some(Object(config)) =>
match config.get("mediaType") {
Some(String(value)) if value != "" => Some(value)
_ => None
}
_ => None
}
}
let descriptor : Map[String, Json] = {
"mediaType": @json.to_json(media_type),
"digest": @json.to_json(digest),
"size": @json.to_json(body.length()),
}
if artifact_type is Some(value) {
descriptor["artifactType"] = @json.to_json(value)
}
if object.get("annotations") is Some(Object(_) as annotations) {
descriptor["annotations"] = annotations
}
Some((subject, artifact_type, Json::object(descriptor)))
}
///|
async fn handle_blob(
store : RegistryStore,
repository : String,
digest : String,
request_method : @http.RequestMethod,
headers : @http.Headers,
conn : @http.ServerConnection,
) -> Unit {
guard valid_repository(repository) && valid_digest(digest) else {
send_error(conn, 400, "DIGEST_INVALID")
return
}
if request_method is Delete {
guard store.delete_repository_blob(repository, digest) else {
send_error(conn, 404, "BLOB_UNKNOWN")
return
}
send_empty(conn, 202, "Accepted")
return
}
let body = match store.get_repository_blob(repository, digest) {
Some(value) => value
None => {
send_error(conn, 404, "BLOB_UNKNOWN")
return
}
}
match request_method {
Head => {
conn.send_response(200, "OK", extra_headers={
"Content-Length": body.length().to_string(),
"Docker-Content-Digest": digest,
"Accept-Ranges": "bytes",
})
conn.end_response()
}
Get =>
match select_blob_range(headers, body.length()) {
Full => send_blob(conn, 200, "OK", body, digest)
Partial(start, end) =>
conn
..send_response(206, "Partial Content", extra_headers={
"Content-Type": "application/octet-stream",
"Content-Length": (end - start + 1).to_string(),
"Content-Range": "bytes " +
start.to_string() +
"-" +
end.to_string() +
"/" +
body.length().to_string(),
"Docker-Content-Digest": digest,
"Accept-Ranges": "bytes",
})
..write(body[start:end + 1])
.end_response()
Unsatisfiable => {
conn.send_response(416, "Range Not Satisfiable", extra_headers={
"Content-Range": "bytes */" + body.length().to_string(),
"Accept-Ranges": "bytes",
})
conn.end_response()
}
}
_ => send_error(conn, 405, "UNSUPPORTED")
}
}
///|
async fn handle_manifest(
store : RegistryStore,
repository : String,
reference : String,
request_method : @http.RequestMethod,
path : String,
conn : @http.ServerConnection,
headers : @http.Headers,
body_reader : &@io.Reader,
) -> Unit {
guard valid_repository(repository) && valid_manifest_reference(reference) else {
send_error(conn, 400, "MANIFEST_INVALID")
return
}
if request_method is Put {
let requested_tags : Array[String] = if valid_digest(reference) {
match query_values(path, "tag") {
Some(values) => values
None => {
send_error(conn, 400, "TAG_INVALID")
return
}
}
} else {
[]
}
for tag in requested_tags {
guard valid_reference(tag) else {
send_error(conn, 400, "TAG_INVALID")
return
}
}
let body = body_reader.read_all().binary()
guard valid_manifest_json(body) else {
send_error(conn, 400, "MANIFEST_INVALID")
return
}
guard valid_manifest_content_type(headers, body) else {
send_error(conn, 400, "MANIFEST_INVALID")
return
}
let body_digest = sha256_digest(body)
guard manifest_reference_matches(reference, body_digest) else {
send_error(conn, 400, "DIGEST_INVALID")
return
}
guard manifest_dependencies_exist(store, repository, body) else {
send_error(conn, 400, "MANIFEST_BLOB_UNKNOWN")
return
}
let content_type = request_manifest_content_type(headers, body)
let digest = store.put_manifest_with_type(
repository, reference, body, content_type,
)
for tag in requested_tags {
ignore(store.tag_manifest(repository, tag, digest))
}
let response_headers : @http.Headers = {
"Location": "/v2/" + repository + "/manifests/" + digest,
"Docker-Content-Digest": digest,
}
if requested_tags.length() > 0 {
response_headers["OCI-Tag"] = tag_header_value(requested_tags)
}
if manifest_subject(body) is Some(subject) {
response_headers["OCI-Subject"] = subject
}
conn.send_response(201, "Created", extra_headers=response_headers)
conn.end_response()
return
}
if request_method is Delete {
guard store.delete_manifest(repository, reference) else {
send_error(conn, 404, "MANIFEST_UNKNOWN")
return
}
send_empty(conn, 202, "Accepted")
return
}
let value = match store.get_manifest(repository, reference) {
Some(value) => value
None => {
send_error(conn, 404, "MANIFEST_UNKNOWN")
return
}
}
match request_method {
Head => {
conn.send_response(200, "OK", extra_headers={
"Content-Type": store.manifest_content_type(
repository,
sha256_digest(value),
),
"Content-Length": value.length().to_string(),
"Docker-Content-Digest": sha256_digest(value),
})
conn.end_response()
}
Get =>
send_manifest(
conn,
200,
"OK",
value,
store.manifest_content_type(repository, sha256_digest(value)),
)
_ => send_error(conn, 405, "UNSUPPORTED")
}
}
///|
async fn handle_referrers(
store : RegistryStore,
repository : String,
subject : String,
request_method : @http.RequestMethod,
path : String,
conn : @http.ServerConnection,
) -> Unit {
guard valid_repository(repository) else {
send_error(conn, 400, "NAME_INVALID")
return
}
guard valid_digest(subject) else {
send_error(conn, 400, "DIGEST_INVALID")
return
}
guard request_method is Get else {
send_error(conn, 405, "UNSUPPORTED")
return
}
let filter : String? = match query_values(path, "artifactType") {
Some(values) if values.length() == 0 => None
Some(values) if values.length() == 1 && values[0] != "" => Some(values[0])
_ => {
send_error(conn, 400, "UNSUPPORTED")
return
}
}
let requested_limit : Int? = match query_values(path, "n") {
Some(values) if values.length() == 0 => None
Some(values) if values.length() == 1 => {
let parsed = @string.parse_int(values[0]) catch { _ => -1 }
guard parsed >= 0 else {
send_error(conn, 400, "UNSUPPORTED")
return
}
Some(parsed)
}
_ => {
send_error(conn, 400, "UNSUPPORTED")
return
}
}
let last : String? = match query_values(path, "last") {
Some(values) if values.length() == 0 => None
Some(values) if values.length() == 1 && valid_digest(values[0]) =>
Some(values[0])
_ => {
send_error(conn, 400, "UNSUPPORTED")
return
}
}
let limit = Some(
match requested_limit {
Some(value) => value
None => 1000
},
)
let matching_digests : Array[String] = []
let descriptor_by_digest : Map[String, Json] = Map([])
for
(digest, artifact_type, descriptor) in store.list_referrer_descriptors(
repository, subject,
) {
if filter is None || filter == artifact_type {
matching_digests.push(digest)
descriptor_by_digest[digest] = descriptor
}
}
let (page_digests, next) = list_page(matching_digests, limit, last)
let descriptors : Array[Json] = []
for digest in page_digests {
match descriptor_by_digest.get(digest) {
Some(descriptor) => descriptors.push(descriptor)
None => ()
}
}
let response = Json::object({
"schemaVersion": @json.to_json(2),
"mediaType": @json.to_json("application/vnd.oci.image.index.v1+json"),
"manifests": Json::array(descriptors),
}).stringify()
let response_headers : @http.Headers = {
"Content-Type": "application/vnd.oci.image.index.v1+json",
}
if filter is Some(_) {
response_headers["OCI-Filters-Applied"] = "artifactType"
}
match next {
Some(digest) => {
let page_size = match limit {
Some(value) => value
None => 1000
}
let filter_query = match filter {
Some(value) => "&artifactType=" + encode_query_value(value)
None => ""
}
response_headers["Link"] = "; rel=\"next\""
}
None => ()
}
conn
..send_response(200, "OK", extra_headers=response_headers)
..write(@utf8.encode(response))
.end_response()
}
///|
async fn handle_tags(
store : RegistryStore,
repository : String,
request_method : @http.RequestMethod,
path : String,
conn : @http.ServerConnection,
) -> Unit {
guard valid_repository(repository) else {
send_error(conn, 400, "NAME_INVALID")
return
}
guard request_method is Get else {
send_error(conn, 405, "UNSUPPORTED")
return
}
let limit = match query_value(path, "n") {
Some(value) => {
let parsed = @string.parse_int(value) catch { _ => -1 }
guard parsed >= 0 else {
send_error(conn, 400, "UNSUPPORTED")
return
}
Some(parsed)
}
None => None
}
let last = query_value(path, "last")
if last is Some(value) && !valid_reference(value) {
send_error(conn, 400, "UNSUPPORTED")
return
}
let (tags, next) = list_page(store.list_tags(repository), limit, last)
let mut body = "{\"name\":\"" + repository + "\",\"tags\":["
for i, tag in tags {
if i > 0 {
body += ","
}
body += "\"" + tag + "\""
}
body += "]}"
let response_headers : @http.Headers = { "Content-Type": "application/json" }
match next {
Some(tag) => {
let n = match limit {
Some(value) => value
None => 0
}
response_headers["Link"] = "; rel=\"next\""
}
None => ()
}
conn
..send_response(200, "OK", extra_headers=response_headers)
..write(@utf8.encode(body))
.end_response()
}
///|
async fn handle_catalog(
store : RegistryStore,
request_method : @http.RequestMethod,
path : String,
conn : @http.ServerConnection,
) -> Unit {
guard request_method is Get else {
send_error(conn, 405, "UNSUPPORTED")
return
}
let limit = match query_value(path, "n") {
Some(value) => {
let parsed = @string.parse_int(value) catch { _ => -1 }
guard parsed >= 0 else {
send_error(conn, 400, "UNSUPPORTED")
return
}
Some(parsed)
}
None => None
}
let last = query_value(path, "last")
if last is Some(value) && !valid_repository(value) {
send_error(conn, 400, "UNSUPPORTED")
return
}
let (repositories, next) = list_page(store.list_repositories(), limit, last)
let mut body = "{\"repositories\":["
for index, repository in repositories {
if index > 0 {
body += ","
}
body += "\"" + repository + "\""
}
body += "]}"
let response_headers : @http.Headers = { "Content-Type": "application/json" }
match next {
Some(repository) => {
let n = match limit {
Some(value) => value
None => 0
}
response_headers["Link"] = "; rel=\"next\""
}
None => ()
}
conn
..send_response(200, "OK", extra_headers=response_headers)
..write(@utf8.encode(body))
.end_response()
}
///|
fn parse_content_range(headers : @http.Headers) -> (Int, Int)? {
match headers.get("Content-Range") {
Some(value) =>
match value.find("-") {
Some(index) => {
let start = @string.parse_int(value[:index]) catch { _ => -1 }
let end = @string.parse_int(value[index + 1:]) catch { _ => -1 }
if start >= 0 && end >= start {
Some((start, end))
} else {
None
}
}
None => None
}
None => None
}
}
///|
fn content_range_matches(
headers : @http.Headers,
current : Int,
chunk_length : Int,
) -> Bool {
match headers.get("Content-Range") {
None => true
Some(_) =>
match parse_content_range(headers) {
Some((start, end)) =>
start == current && end - start + 1 == chunk_length
None => false
}
}
}
///|
fn upload_range(size : Int) -> String {
if size == 0 {
"0-0"
} else {
"0-" + (size - 1).to_string()
}
}
///|
async fn send_upload_created(
conn : @http.ServerConnection,
repository : String,
digest : String,
) -> Unit {
conn.send_response(201, "Created", extra_headers={
"Location": "/v2/" + repository + "/blobs/" + digest,
"Docker-Content-Digest": digest,
})
conn.end_response()
}
///|
async fn handle_upload(
store : RegistryStore,
repository : String,
upload_id : String?,
request_method : @http.RequestMethod,
path : String,
headers : @http.Headers,
body_reader : &@io.Reader,
conn : @http.ServerConnection,
) -> Unit {
guard valid_repository(repository) else {
send_error(conn, 400, "NAME_INVALID")
return
}
match (request_method, upload_id) {
(Post, None) => {
match (query_value(path, "mount"), query_value(path, "from")) {
(Some(digest), Some(source)) =>
if store.mount_blob(repository, source, digest) {
send_upload_created(conn, repository, digest)
return
}
_ => ()
}
let id = store.start_upload(repository)
let data = body_reader.read_all().binary()
if data.length() > 0 {
ignore(store.append_upload(repository, id, data))
}
match query_digest(path) {
Some(expected) => {
let (status, digest) = store.finalize_upload(repository, id, expected)
match (status, digest) {
(201, Some(value)) => send_upload_created(conn, repository, value)
(400, _) => send_error(conn, 400, "DIGEST_INVALID")
_ => send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
}
}
None => {
conn.send_response(202, "Accepted", extra_headers={
"Location": "/v2/" + repository + "/blobs/uploads/" + id,
"Docker-Upload-UUID": id,
"Range": upload_range(data.length()),
})
conn.end_response()
}
}
}
(Patch, Some(id)) => {
let current = match store.upload_size(repository, id) {
Some(value) => value
None => {
send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
return
}
}
let data = body_reader.read_all().binary()
guard content_range_matches(headers, current, data.length()) else {
send_error(conn, 416, "RANGE_INVALID")
return
}
guard store.append_upload(repository, id, data) else {
send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
return
}
conn.send_response(202, "Accepted", extra_headers={
"Location": "/v2/" + repository + "/blobs/uploads/" + id,
"Docker-Upload-UUID": id,
"Range": upload_range(current + data.length()),
})
conn.end_response()
}
(Get, Some(id)) => {
let current = match store.upload_size(repository, id) {
Some(value) => value
None => {
send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
return
}
}
conn.send_response(204, "No Content", extra_headers={
"Location": "/v2/" + repository + "/blobs/uploads/" + id,
"Docker-Upload-UUID": id,
"Range": upload_range(current),
})
conn.end_response()
}
(Delete, Some(id)) => {
guard store.delete_upload(repository, id) else {
send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
return
}
conn.send_response(204, "No Content")
conn.end_response()
}
(Put, Some(id)) => {
let expected = match query_digest(path) {
Some(value) => value
None => {
send_error(conn, 400, "DIGEST_INVALID")
return
}
}
let current = match store.upload_size(repository, id) {
Some(value) => value
None => {
send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
return
}
}
let data = body_reader.read_all().binary()
guard content_range_matches(headers, current, data.length()) else {
send_error(conn, 416, "RANGE_INVALID")
return
}
if data.length() > 0 {
guard store.append_upload(repository, id, data) else {
send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
return
}
}
let (status, digest) = store.finalize_upload(repository, id, expected)
match (status, digest) {
(201, Some(value)) => send_upload_created(conn, repository, value)
(400, Some(_)) => send_error(conn, 400, "DIGEST_INVALID")
(400, None) => send_error(conn, 400, "DIGEST_INVALID")
_ => send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
}
}
_ => send_error(conn, 404, "BLOB_UPLOAD_UNKNOWN")
}
}
///|
pub async fn serve(store : RegistryStore, host : String, port : Int) -> Unit {
store.ensure()
let server = @http.Server(@socket.Addr::parse(host + ":" + port.to_string()))
server.run_forever() <| ((request, body, conn) => {
let route = route_for(request.path)
match route {
Root =>
match request.meth {
Get | Head => send_empty(conn, 200, "OK")
_ => send_error(conn, 405, "UNSUPPORTED")
}
Catalog => handle_catalog(store, request.meth, request.path, conn)
Blob(repository, digest) =>
handle_blob(
store,
repository,
digest,
request.meth,
request.headers,
conn,
)
UploadStart(repository) =>
handle_upload(
store,
repository,
None,
request.meth,
request.path,
request.headers,
body,
conn,
)
UploadChunk(repository, id) =>
handle_upload(
store,
repository,
Some(id),
request.meth,
request.path,
request.headers,
body,
conn,
)
Manifest(repository, reference) =>
handle_manifest(
store,
repository,
reference,
request.meth,
request.path,
conn,
request.headers,
body,
)
Referrers(repository, digest) =>
handle_referrers(
store,
repository,
digest,
request.meth,
request.path,
conn,
)
Tags(repository) =>
handle_tags(store, repository, request.meth, request.path, conn)
Unknown => send_error(conn, 404, "NAME_UNKNOWN")
}
})
}