The tag separator can only appear as the last path component, hence start the search there. Fixes an issue reproducible through: ``` > /usr/lib/systemd/systemd-pull oci quay.io:443/fedora/fedora fedora-without-tag OCI ref 'quay.io:443/fedora/fedora' is invalid. ``` (cherry picked from commit fe9fe613d92f2acffdf3f8b96d3af027f19d6b7e)
414 lines
15 KiB
C
414 lines
15 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include <string.h>
|
|
|
|
#include "sd-json.h"
|
|
|
|
#include "alloc-util.h"
|
|
#include "architecture.h"
|
|
#include "constants.h"
|
|
#include "dns-domain.h"
|
|
#include "fd-util.h"
|
|
#include "fileio.h"
|
|
#include "hexdecoct.h"
|
|
#include "log.h"
|
|
#include "oci-util.h"
|
|
#include "parse-util.h"
|
|
#include "string-table.h"
|
|
#include "string-util.h"
|
|
|
|
bool oci_image_is_valid(const char *n) {
|
|
bool slash = true;
|
|
|
|
/* The OCI spec suggests validating this regex:
|
|
*
|
|
* [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*
|
|
*
|
|
* We implement a generalization of this, i.e. do not insist on the single ".", "_", "__", "-", "+"
|
|
* separator, but allow any number of them. And we refuse leading dots, since if used in the fs this
|
|
* would make the files hidden, and we probably don't want that.
|
|
*/
|
|
|
|
for (const char *p = n; *p; p++) {
|
|
if (*p == '/') {
|
|
if (slash)
|
|
return false;
|
|
|
|
slash = true;
|
|
continue;
|
|
}
|
|
|
|
if (!strchr(slash ? LOWERCASE_LETTERS DIGITS "_-+" :
|
|
"." LOWERCASE_LETTERS DIGITS "_-+", *p))
|
|
return false;
|
|
|
|
slash = false;
|
|
}
|
|
|
|
return !slash;
|
|
}
|
|
|
|
int oci_registry_is_valid(const char *n) {
|
|
int r;
|
|
|
|
if (isempty(n))
|
|
return false;
|
|
|
|
const char *colon = strchr(n, ':');
|
|
if (!colon)
|
|
return dns_name_is_valid(n);
|
|
|
|
if (colon == n) /* empty host, e.g. ":5000" */
|
|
return false;
|
|
|
|
_cleanup_free_ char *s = strndup(n, colon - n);
|
|
if (!s)
|
|
return -ENOMEM;
|
|
|
|
r = dns_name_is_valid(s);
|
|
if (r <= 0)
|
|
return r;
|
|
|
|
uint16_t port;
|
|
return safe_atou16_full(colon + 1,
|
|
10 | SAFE_ATO_REFUSE_LEADING_WHITESPACE |
|
|
SAFE_ATO_REFUSE_PLUS_MINUS | SAFE_ATO_REFUSE_LEADING_ZERO,
|
|
&port) >= 0 && port != 0;
|
|
}
|
|
|
|
bool oci_tag_is_valid(const char *n) {
|
|
if (!n || isempty(n))
|
|
return false;
|
|
|
|
/* As per https://github.com/opencontainers/distribution-spec/blob/main/spec.md, accept the following regex:
|
|
*
|
|
* [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}
|
|
*/
|
|
|
|
if (!strchr(ALPHANUMERICAL "_", n[0]))
|
|
return false;
|
|
|
|
size_t l = strspn(n + 1, ALPHANUMERICAL "._-");
|
|
if (l > 127)
|
|
return false;
|
|
if (n[1+l] != 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
int oci_ref_parse(
|
|
const char *ref,
|
|
char **ret_registry,
|
|
char **ret_image,
|
|
char **ret_tag) {
|
|
|
|
int r;
|
|
|
|
assert(ref);
|
|
|
|
_cleanup_free_ char *without_tag = NULL, *tag = NULL;
|
|
|
|
/* Look for the tag separator only after the last "/", so that we don't mistake the port number of a
|
|
* registry for a tag, e.g. so that "quay.io:443/fedora/fedora" is parsed correctly. */
|
|
const char *slash = strrchr(ref, '/');
|
|
const char *t = strrchr(slash ?: ref, ':');
|
|
if (t) {
|
|
tag = strdup(t + 1);
|
|
if (!tag)
|
|
return -ENOMEM;
|
|
if (!oci_tag_is_valid(tag))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI tag specification '%s' is not valid.", tag);
|
|
|
|
without_tag = strndup(ref, t - ref);
|
|
if (!without_tag)
|
|
return -ENOMEM;
|
|
|
|
ref = without_tag;
|
|
}
|
|
|
|
_cleanup_free_ char *image = NULL, *registry = NULL;
|
|
t = strchr(ref, '/');
|
|
if (t) {
|
|
registry = strndup(ref, t - ref);
|
|
if (!registry)
|
|
return -ENOMEM;
|
|
|
|
r = oci_registry_is_valid(registry);
|
|
if (r < 0)
|
|
return r;
|
|
if (r == 0)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI registry specification '%s' is not valid.", registry);
|
|
|
|
image = strdup(t + 1);
|
|
} else
|
|
image = strdup(ref);
|
|
if (!image)
|
|
return -ENOMEM;
|
|
if (!oci_image_is_valid(image))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI image specification '%s' is not valid.", registry);
|
|
|
|
if (ret_registry)
|
|
*ret_registry = TAKE_PTR(registry);
|
|
if (ret_image)
|
|
*ret_image = TAKE_PTR(image);
|
|
if (ret_tag)
|
|
*ret_tag = TAKE_PTR(tag);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int oci_ref_normalize(char **protocol, char **registry, char **image, char **tag) {
|
|
int r;
|
|
|
|
assert(protocol);
|
|
assert(registry);
|
|
assert(image);
|
|
assert(*image);
|
|
assert(tag);
|
|
|
|
/* OCI container reference are supposed to have the form <registry>/<name>:<tag>. Except that it's
|
|
* all super messy, and for some registries the server name differs from the name people use in the
|
|
* references, and there are special rules for "short" container names (i.e. those which do not
|
|
* contain a "/"), and more. To deal with this, we devise a relatively simple scheme, to normalize
|
|
* such names. Specifically:
|
|
*
|
|
* If a registry is specified we look for
|
|
* /usr/lib/systemd/oci-registry/registry.<registry>.oci-registry for registry-specific rules, to
|
|
* enforce on the reference. If no registry is specified, we look for an
|
|
* /usr/lib/systemd/oci-registry/image.<name>.oci-registry file, which contains image-specific rules
|
|
* instead. If this is not found we load /usr/lib/systemd/oci-registry/default.oci-registry
|
|
* instead. The files are encoded in JSON.
|
|
*
|
|
* The rules we apply are relatively simple:
|
|
*
|
|
* • defaultProtocol controls which protocol to use if none is known. This should always be https
|
|
* (since OCI images are authenticated purely via HTTPS), but for testing purposes "file" might be
|
|
* useful too.
|
|
*
|
|
* • overrideRegistry encodes which registry server to actually use, overriding what might have been
|
|
* specified.
|
|
*
|
|
* • overrideImage encodes which image name to actually use, overriding what might have been specified.
|
|
*
|
|
* • shortImagePrefix encodes a name prefix to prepend to "short" container names. This has no effect
|
|
* if overrideImage is set too.
|
|
*
|
|
* • defaultTag contains a tag to use as default, if none is specified. If not configured this
|
|
* defaults to "latest".
|
|
*/
|
|
|
|
_cleanup_free_ char *fn = NULL;
|
|
if (*registry) {
|
|
/* If a registry is specified, we'll always respect it, and use it as only search key */
|
|
_cleanup_free_ char *e = urlescape(*registry);
|
|
if (!e)
|
|
return -ENOMEM;
|
|
|
|
fn = strjoin("registry.", e, ".oci-registry");
|
|
} else {
|
|
/* If no registry is specified, let's go by image name */
|
|
_cleanup_free_ char *e = urlescape(*image);
|
|
if (!e)
|
|
return -ENOMEM;
|
|
|
|
fn = strjoin("image.", e, ".oci-registry");
|
|
}
|
|
if (!fn)
|
|
return -ENOMEM;
|
|
|
|
_cleanup_fclose_ FILE *f = NULL;
|
|
_cleanup_free_ char *path = NULL;
|
|
r = search_and_fopen_nulstr(fn, "re", /* root= */ NULL, CONF_PATHS_NULSTR("systemd/oci-registry"), &f, &path);
|
|
if (r == -ENOENT && isempty(*registry))
|
|
r = search_and_fopen_nulstr("default.oci-registry", "re", /* root= */ NULL, CONF_PATHS_NULSTR("systemd/oci-registry"), &f, &path);
|
|
if (r < 0 && r != -ENOENT)
|
|
return log_debug_errno(r, "Failed to find suitable OCI registry file: %m");
|
|
|
|
/* if ENOENT is seen, we use the defaults below! */
|
|
|
|
struct {
|
|
const char *default_protocol;
|
|
const char *override_registry;
|
|
const char *default_registry;
|
|
const char *override_image;
|
|
const char *short_image_prefix;
|
|
const char *default_tag;
|
|
} data = {
|
|
.default_protocol = "https",
|
|
.default_tag = "latest",
|
|
};
|
|
|
|
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
|
|
if (f) {
|
|
unsigned line = 0, column = 0;
|
|
r = sd_json_parse_file(f, path, /* flags= */ 0, &v, &line, &column);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Parse failure at %s:%u:%u: %m", path, line, column);
|
|
|
|
static const sd_json_dispatch_field table[] = {
|
|
{ "defaultProtocol", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_protocol), 0 },
|
|
{ "overrideRegistry", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, override_registry), 0 },
|
|
{ "defaultRegistry", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_registry), 0 },
|
|
{ "overrideImage", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, override_image), 0 },
|
|
{ "shortImagePrefix", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, short_image_prefix), 0 },
|
|
{ "defaultTag", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_tag), 0 },
|
|
{},
|
|
};
|
|
r = sd_json_dispatch(v, table, SD_JSON_ALLOW_EXTENSIONS, &data);
|
|
if (r < 0)
|
|
return r;
|
|
}
|
|
|
|
_cleanup_free_ char *new_protocol = NULL;
|
|
if (data.default_protocol && isempty(*protocol)) {
|
|
new_protocol = strdup(data.default_protocol);
|
|
if (!new_protocol)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
_cleanup_free_ char *new_registry = NULL;
|
|
if (data.override_registry) {
|
|
if (!isempty(*registry))
|
|
log_debug("Overriding registry to '%s' (was '%s') based on OCI registry database.", data.override_registry, *registry);
|
|
|
|
new_registry = strdup(data.override_registry);
|
|
if (!new_registry)
|
|
return -ENOMEM;
|
|
} else if (data.default_registry && isempty(*registry)) {
|
|
new_registry = strdup(data.default_registry);
|
|
if (!new_registry)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
_cleanup_free_ char *new_image = NULL;
|
|
if (data.override_image) {
|
|
log_debug("Overriding image to '%s' (was '%s') based on OCI registry database.", data.override_registry, *image);
|
|
|
|
new_image = strdup(data.override_image);
|
|
if (!new_image)
|
|
return -ENOMEM;
|
|
} else if (data.short_image_prefix && !strchr(*image, '/')) {
|
|
new_image = strjoin(data.short_image_prefix, *image);
|
|
if (!new_image)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
_cleanup_free_ char *new_tag = NULL;
|
|
if (data.default_tag && isempty(*tag)) {
|
|
new_tag = strdup(data.default_tag);
|
|
if (!new_tag)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
if (!new_registry && isempty(*registry))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENODATA), "No suitable registry found.");
|
|
|
|
if (new_protocol)
|
|
free_and_replace(*protocol, new_protocol);
|
|
if (new_registry)
|
|
free_and_replace(*registry, new_registry);
|
|
if (new_image)
|
|
free_and_replace(*image, new_image);
|
|
if (new_tag)
|
|
free_and_replace(*tag, new_tag);
|
|
|
|
return 0;
|
|
}
|
|
|
|
char* oci_digest_string(const struct iovec *iovec) {
|
|
assert(iovec);
|
|
|
|
_cleanup_free_ char *h = hexmem(iovec->iov_base, iovec->iov_len);
|
|
if (!h)
|
|
return NULL;
|
|
|
|
return strjoin("sha256:", h);
|
|
}
|
|
|
|
int oci_make_manifest_url(
|
|
const char *protocol,
|
|
const char *repository,
|
|
const char *image,
|
|
const char *tag,
|
|
char **ret) {
|
|
|
|
assert(protocol);
|
|
assert(repository);
|
|
assert(image);
|
|
assert(tag);
|
|
assert(ret);
|
|
|
|
_cleanup_free_ char *url = strjoin(protocol, "://", repository, "/v2/", image, "/manifests/", tag);
|
|
if (!url)
|
|
return -ENOMEM;
|
|
|
|
*ret = TAKE_PTR(url);
|
|
return 0;
|
|
}
|
|
|
|
int oci_make_blob_url(
|
|
const char *protocol,
|
|
const char *repository,
|
|
const char *image,
|
|
const struct iovec *digest,
|
|
char **ret) {
|
|
|
|
assert(protocol);
|
|
assert(repository);
|
|
assert(image);
|
|
assert(digest);
|
|
assert(ret);
|
|
|
|
_cleanup_free_ char *d = oci_digest_string(digest);
|
|
if (!d)
|
|
return -ENOMEM;
|
|
|
|
_cleanup_free_ char *url = strjoin(protocol, "://", repository, "/v2/", image, "/blobs/", d);
|
|
if (!url)
|
|
return -ENOMEM;
|
|
|
|
*ret = TAKE_PTR(url);
|
|
return 0;
|
|
}
|
|
|
|
/* OCI uses the Go architecture IDs */
|
|
static const char *const go_arch_table[_ARCHITECTURE_MAX] = {
|
|
[ARCHITECTURE_ARM] = "arm",
|
|
[ARCHITECTURE_ARM64] = "arm64",
|
|
[ARCHITECTURE_MIPS] = "mips",
|
|
[ARCHITECTURE_MIPS64] = "mips64",
|
|
[ARCHITECTURE_MIPS64_LE] = "mips64le",
|
|
[ARCHITECTURE_MIPS_LE] = "mipsle",
|
|
[ARCHITECTURE_PPC64] = "ppc64",
|
|
[ARCHITECTURE_PPC64_LE] = "ppc64le",
|
|
[ARCHITECTURE_S390X] = "s390x",
|
|
[ARCHITECTURE_X86] = "386",
|
|
[ARCHITECTURE_X86_64] = "amd64",
|
|
};
|
|
|
|
DEFINE_STRING_TABLE_LOOKUP_FROM_STRING(go_arch, Architecture);
|
|
|
|
char* urlescape(const char *s) {
|
|
POINTER_MAY_BE_NULL(s);
|
|
size_t l = strlen_ptr(s);
|
|
|
|
_cleanup_free_ char *t = new(char, l * 3 + 1);
|
|
if (!t)
|
|
return NULL;
|
|
|
|
char *p = t;
|
|
for (; s && *s; s++) {
|
|
if (strchr(ALPHANUMERICAL ".-_", *s))
|
|
*(p++) = *s;
|
|
else {
|
|
*(p++) = '%';
|
|
*(p++) = hexchar((uint8_t) *s >> 4);
|
|
*(p++) = hexchar((uint8_t) *s & 15);
|
|
}
|
|
}
|
|
|
|
*p = 0;
|
|
return TAKE_PTR(t);
|
|
}
|