flatpak/flatpak-implement-etc-containers-certs-for-oci-registries.patch
2025-01-17 09:54:18 +01:00

2004 lines
75 KiB
Diff

From cec97aac1c9fad9b5bc18d1166b63edb13ca2bcc Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Tue, 17 Dec 2024 15:35:21 +0100
Subject: [PATCH 01/09] tests/oci-registry-client.py: Drop python2
compatibility
---
tests/oci-registry-client.py | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py
index 7f1eeb54..b9ca63d7 100644
--- a/tests/oci-registry-client.py
+++ b/tests/oci-registry-client.py
@@ -2,12 +2,9 @@
import sys
-if sys.version_info[0] >= 3:
- import http.client as http_client
- import urllib.parse as urllib_parse
-else:
- import http.client as http_client
- import urllib as urllib_parse
+import http.client
+import urllib.parse
+
if sys.argv[2] == 'add':
detach_icons = '--detach-icons' in sys.argv
@@ -16,8 +13,8 @@ if sys.argv[2] == 'add':
params = {'d': sys.argv[5]}
if detach_icons:
params['detach-icons'] = 1
- query = urllib_parse.urlencode(params)
- conn = http_client.HTTPConnection(sys.argv[1])
+ query = urllib.parse.urlencode(params)
+ conn = http.client.HTTPConnection(sys.argv[1])
path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3],
tag=sys.argv[4],
query=query)
@@ -28,7 +25,7 @@ if sys.argv[2] == 'add':
print("Failed: status={}".format(response.status), file=sys.stderr)
sys.exit(1)
elif sys.argv[2] == 'delete':
- conn = http_client.HTTPConnection(sys.argv[1])
+ conn = http.client.HTTPConnection(sys.argv[1])
path = "/testing/{repo}/{ref}".format(repo=sys.argv[3],
ref=sys.argv[4])
conn.request("DELETE", path)
--
2.47.1
From d8ce35c9d1c0b1c83127b07abe9b3479170cc8f6 Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Tue, 17 Dec 2024 15:41:53 +0100
Subject: [PATCH 02/09] tests/oci-registry-client.py: Parse URL parameter
---
tests/oci-registry-client.py | 9 +++++++--
tests/test-oci-registry.sh | 2 +-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py
index b9ca63d7..cd835609 100644
--- a/tests/oci-registry-client.py
+++ b/tests/oci-registry-client.py
@@ -6,6 +6,11 @@ import http.client
import urllib.parse
+def get_conn(url):
+ parsed = urllib.parse.urlparse(url)
+ return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port)
+
+
if sys.argv[2] == 'add':
detach_icons = '--detach-icons' in sys.argv
if detach_icons:
@@ -14,7 +19,7 @@ if sys.argv[2] == 'add':
if detach_icons:
params['detach-icons'] = 1
query = urllib.parse.urlencode(params)
- conn = http.client.HTTPConnection(sys.argv[1])
+ conn = get_conn(sys.argv[1])
path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3],
tag=sys.argv[4],
query=query)
@@ -25,7 +30,7 @@ if sys.argv[2] == 'add':
print("Failed: status={}".format(response.status), file=sys.stderr)
sys.exit(1)
elif sys.argv[2] == 'delete':
- conn = http.client.HTTPConnection(sys.argv[1])
+ conn = get_conn(sys.argv[1])
path = "/testing/{repo}/{ref}".format(repo=sys.argv[3],
ref=sys.argv[4])
conn.request("DELETE", path)
diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh
index 8eb154f5..51a6142f 100755
--- a/tests/test-oci-registry.sh
+++ b/tests/test-oci-registry.sh
@@ -29,7 +29,7 @@ echo "1..14"
httpd oci-registry-server.py .
port=$(cat httpd-port)
-client="python3 $test_srcdir/oci-registry-client.py 127.0.0.1:$port"
+client="python3 $test_srcdir/oci-registry-client.py http://127.0.0.1:$port"
setup_repo_no_add oci
--
2.47.1
From 0757171aa07f3b8d390881e0765de09ed77d4825 Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Tue, 17 Dec 2024 15:47:07 +0100
Subject: [PATCH 03/09] tests/oci-registry-client.py: Convert to argparse
---
tests/oci-registry-client.py | 57 +++++++++++++++++++++++-------------
tests/test-oci-registry.sh | 2 +-
2 files changed, 38 insertions(+), 21 deletions(-)
mode change 100644 => 100755 tests/oci-registry-client.py
diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py
old mode 100644
new mode 100755
index cd835609..5654062b
--- a/tests/oci-registry-client.py
+++ b/tests/oci-registry-client.py
@@ -1,45 +1,62 @@
#!/usr/bin/python3
+import argparse
import sys
import http.client
import urllib.parse
-def get_conn(url):
- parsed = urllib.parse.urlparse(url)
+def get_conn(args):
+ parsed = urllib.parse.urlparse(args.url)
return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port)
-if sys.argv[2] == 'add':
- detach_icons = '--detach-icons' in sys.argv
- if detach_icons:
- sys.argv.remove('--detach-icons')
- params = {'d': sys.argv[5]}
- if detach_icons:
- params['detach-icons'] = 1
+def run_add(args):
+ params = {"d": args.oci_dir}
+ if args.detach_icons:
+ params["detach-icons"] = "1"
query = urllib.parse.urlencode(params)
- conn = get_conn(sys.argv[1])
- path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3],
- tag=sys.argv[4],
- query=query)
+ conn = get_conn(args)
+ path = "/testing/{repo}/{tag}?{query}".format(
+ repo=args.repo, tag=args.tag, query=query
+ )
conn.request("POST", path)
response = conn.getresponse()
if response.status != 200:
print(response.read(), file=sys.stderr)
print("Failed: status={}".format(response.status), file=sys.stderr)
sys.exit(1)
-elif sys.argv[2] == 'delete':
- conn = get_conn(sys.argv[1])
- path = "/testing/{repo}/{ref}".format(repo=sys.argv[3],
- ref=sys.argv[4])
+
+
+def run_delete(args):
+ conn = get_conn(args)
+ path = "/testing/{repo}/{ref}".format(repo=args.repo, ref=args.ref)
conn.request("DELETE", path)
response = conn.getresponse()
if response.status != 200:
print(response.read(), file=sys.stderr)
print("Failed: status={}".format(response.status), file=sys.stderr)
sys.exit(1)
-else:
- print("Usage: oci-registry-client.py [add|remove] ARGS", file=sys.stderr)
- sys.exit(1)
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--url", required=True)
+
+subparsers = parser.add_subparsers()
+subparsers.required = True
+
+add_parser = subparsers.add_parser("add")
+add_parser.add_argument("repo")
+add_parser.add_argument("tag")
+add_parser.add_argument("oci_dir")
+add_parser.add_argument("--detach-icons", action="store_true", default=False)
+add_parser.set_defaults(func=run_add)
+
+delete_parser = subparsers.add_parser("delete")
+delete_parser.add_argument("repo")
+delete_parser.add_argument("ref")
+delete_parser.set_defaults(func=run_delete)
+
+args = parser.parse_args()
+args.func(args)
diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh
index 51a6142f..bc2f138b 100755
--- a/tests/test-oci-registry.sh
+++ b/tests/test-oci-registry.sh
@@ -29,7 +29,7 @@ echo "1..14"
httpd oci-registry-server.py .
port=$(cat httpd-port)
-client="python3 $test_srcdir/oci-registry-client.py http://127.0.0.1:$port"
+client="python3 $test_srcdir/oci-registry-client.py --url=http://127.0.0.1:$port"
setup_repo_no_add oci
--
2.47.1
From f38197c03dc77b9192c6dbc59e175b3cb640614a Mon Sep 17 00:00:00 2001
From: Sebastian Wick <sebastian.wick@redhat.com>
Date: Thu, 22 Aug 2024 02:43:13 -0400
Subject: [PATCH 04/09] tests/oci-registry-server.py: Clean up Python style
---
.flake8 | 2 +
tests/oci-registry-server.py | 170 ++++++++++++++++++-----------------
2 files changed, 88 insertions(+), 84 deletions(-)
create mode 100644 .flake8
diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000..7da1f960
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 100
diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py
index 33c3b646..3050a883 100755
--- a/tests/oci-registry-server.py
+++ b/tests/oci-registry-server.py
@@ -13,65 +13,61 @@ import http.server as http_server
repositories = {}
icons = {}
+
def get_index():
results = []
for repo_name in sorted(repositories.keys()):
repo = repositories[repo_name]
- results.append({
- 'Name': repo_name,
- 'Images': repo['images'],
- 'Lists': [],
- })
+ results.append(
+ {
+ "Name": repo_name,
+ "Images": repo["images"],
+ "Lists": [],
+ }
+ )
+
+ return json.dumps({"Registry": "/", "Results": results}, indent=4)
- return json.dumps({
- 'Registry': '/',
- 'Results': results
- }, indent=4)
def cache_icon(data_uri):
- prefix = 'data:image/png;base64,'
+ prefix = "data:image/png;base64,"
assert data_uri.startswith(prefix)
- data = base64.b64decode(data_uri[len(prefix):])
+ data = base64.b64decode(data_uri[len(prefix) :])
h = hashlib.sha256()
h.update(data)
digest = h.hexdigest()
- filename = digest + '.png'
+ filename = digest + ".png"
icons[filename] = data
- return '/icons/' + filename
+ return "/icons/" + filename
+
serial = 0
server_start_time = int(time.time())
+
def get_etag():
- return str(server_start_time) + '-' + str(serial)
+ return str(server_start_time) + "-" + str(serial)
+
def modified():
global serial
serial += 1
-def parse_http_date(date):
- parsed = parsedate(date)
- if parsed is not None:
- return timegm(parsed)
- else:
- return None
class RequestHandler(http_server.BaseHTTPRequestHandler):
def check_route(self, route):
- parts = self.path.split('?', 1)
- path = parts[0].split('/')
-
- result = []
+ parts = self.path.split("?", 1)
+ path = parts[0].split("/")
- route_path = route.split('/')
+ route_path = route.split("/")
print((route_path, path))
if len(route_path) != len(path):
return False
matches = {}
for i in range(1, len(route_path)):
- if route_path[i][0] == '@':
+ if route_path[i][0] == "@":
matches[route_path[i][1:]] = path[i]
elif route_path[i] != path[i]:
return False
@@ -92,24 +88,25 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
add_headers = {}
- if self.check_route('/v2/@repo_name/blobs/@digest'):
- repo_name = self.matches['repo_name']
- digest = self.matches['digest']
- response_file = repositories[repo_name]['blobs'][digest]
- elif self.check_route('/v2/@repo_name/manifests/@ref'):
- repo_name = self.matches['repo_name']
- ref = self.matches['ref']
- response_file = repositories[repo_name]['manifests'][ref]
- elif self.check_route('/index/static') or self.check_route('/index/dynamic'):
+ if self.check_route("/v2/@repo_name/blobs/@digest"):
+ repo_name = self.matches["repo_name"]
+ digest = self.matches["digest"]
+ response_file = repositories[repo_name]["blobs"][digest]
+ elif self.check_route("/v2/@repo_name/manifests/@ref"):
+ repo_name = self.matches["repo_name"]
+ ref = self.matches["ref"]
+ response_file = repositories[repo_name]["manifests"][ref]
+ elif self.check_route("/index/static") or self.check_route("/index/dynamic"):
etag = get_etag()
if self.headers.get("If-None-Match") == etag:
response = 304
else:
response_string = get_index()
- add_headers['Etag'] = etag
- elif self.check_route('/icons/@filename') :
- response_string = icons[self.matches['filename']]
- response_content_type = 'image/png'
+ add_headers["Etag"] = etag
+ elif self.check_route("/icons/@filename"):
+ response_string = icons[self.matches["filename"]]
+ assert isinstance(response_string, bytes)
+ response_content_type = "image/png"
else:
response = 404
@@ -121,86 +118,89 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
self.send_header("Content-Type", response_content_type)
if response == 200 or response == 304:
- self.send_header('Cache-Control', 'no-cache')
+ self.send_header("Cache-Control", "no-cache")
self.end_headers()
if response == 200:
if response_file:
- with open(response_file, 'rb') as f:
+ with open(response_file, "rb") as f:
response_string = f.read()
if isinstance(response_string, bytes):
self.wfile.write(response_string)
else:
- self.wfile.write(response_string.encode('utf-8'))
+ assert isinstance(response_string, str)
+ self.wfile.write(response_string.encode("utf-8"))
def do_HEAD(self):
return self.do_GET()
def do_POST(self):
- if self.check_route('/testing/@repo_name/@tag'):
- repo_name = self.matches['repo_name']
- tag = self.matches['tag']
- d = self.query['d'][0]
- detach_icons = 'detach-icons' in self.query
+ if self.check_route("/testing/@repo_name/@tag"):
+ repo_name = self.matches["repo_name"]
+ tag = self.matches["tag"]
+ d = self.query["d"][0]
+ detach_icons = "detach-icons" in self.query
repo = repositories.setdefault(repo_name, {})
- blobs = repo.setdefault('blobs', {})
- manifests = repo.setdefault('manifests', {})
- images = repo.setdefault('images', [])
+ blobs = repo.setdefault("blobs", {})
+ manifests = repo.setdefault("manifests", {})
+ images = repo.setdefault("images", [])
- with open(os.path.join(d, 'index.json')) as f:
+ with open(os.path.join(d, "index.json")) as f:
index = json.load(f)
- manifest_digest = index['manifests'][0]['digest']
- manifest_path = os.path.join(d, 'blobs', *manifest_digest.split(':'))
+ manifest_digest = index["manifests"][0]["digest"]
+ manifest_path = os.path.join(d, "blobs", *manifest_digest.split(":"))
manifests[manifest_digest] = manifest_path
manifests[tag] = manifest_path
with open(manifest_path) as f:
manifest = json.load(f)
- config_digest = manifest['config']['digest']
- config_path = os.path.join(d, 'blobs', *config_digest.split(':'))
+ config_digest = manifest["config"]["digest"]
+ config_path = os.path.join(d, "blobs", *config_digest.split(":"))
with open(config_path) as f:
config = json.load(f)
- for dig in os.listdir(os.path.join(d, 'blobs', 'sha256')):
- digest = 'sha256:' + dig
- path = os.path.join(d, 'blobs', 'sha256', dig)
+ for dig in os.listdir(os.path.join(d, "blobs", "sha256")):
+ digest = "sha256:" + dig
+ path = os.path.join(d, "blobs", "sha256", dig)
if digest != manifest_digest:
blobs[digest] = path
if detach_icons:
for size in (64, 128):
- annotation = 'org.freedesktop.appstream.icon-{}'.format(size)
- icon = manifest.get('annotations', {}).get(annotation)
+ annotation = "org.freedesktop.appstream.icon-{}".format(size)
+ icon = manifest.get("annotations", {}).get(annotation)
if icon:
path = cache_icon(icon)
- manifest['annotations'][annotation] = path
+ manifest["annotations"][annotation] = path
else:
- icon = config.get('config', {}).get('Labels', {}).get(annotation)
+ icon = (
+ config.get("config", {}).get("Labels", {}).get(annotation)
+ )
if icon:
path = cache_icon(icon)
- config['config']['Labels'][annotation] = path
+ config["config"]["Labels"][annotation] = path
image = {
"Tags": [tag],
"Digest": manifest_digest,
"MediaType": "application/vnd.oci.image.manifest.v1+json",
- "OS": config['os'],
- "Architecture": config['architecture'],
- "Annotations": manifest.get('annotations', {}),
- "Labels": config.get('config', {}).get('Labels', {}),
+ "OS": config["os"],
+ "Architecture": config["architecture"],
+ "Annotations": manifest.get("annotations", {}),
+ "Labels": config.get("config", {}).get("Labels", {}),
}
# Delete old versions
for i in images:
- if tag in i['Tags']:
+ if tag in i["Tags"]:
images.remove(i)
- del manifests[i['Digest']]
+ del manifests[i["Digest"]]
images.append(image)
@@ -214,26 +214,26 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
return
def do_DELETE(self):
- if self.check_route('/testing/@repo_name/@ref'):
- repo_name = self.matches['repo_name']
- ref = self.matches['ref']
+ if self.check_route("/testing/@repo_name/@ref"):
+ repo_name = self.matches["repo_name"]
+ ref = self.matches["ref"]
repo = repositories.setdefault(repo_name, {})
- blobs = repo.setdefault('blobs', {})
- manifests = repo.setdefault('manifests', {})
- images = repo.setdefault('images', [])
+ repo.setdefault("blobs", {})
+ manifests = repo.setdefault("manifests", {})
+ images = repo.setdefault("images", [])
image = None
for i in images:
- if i['Digest'] == ref or ref in i['Tags']:
+ if i["Digest"] == ref or ref in i["Tags"]:
image = i
break
assert image
images.remove(image)
- del manifests[image['Digest']]
- for t in image['Tags']:
+ del manifests[image["Digest"]]
+ for t in image["Tags"]:
del manifests[t]
modified()
@@ -245,22 +245,24 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
self.end_headers()
return
+
def run(dir):
RequestHandler.protocol_version = "HTTP/1.0"
- httpd = http_server.HTTPServer( ("127.0.0.1", 0), RequestHandler)
+ httpd = http_server.HTTPServer(("127.0.0.1", 0), RequestHandler)
host, port = httpd.socket.getsockname()[:2]
- with open("httpd-port", 'w') as file:
+ with open("httpd-port", "w") as file:
file.write("%d" % port)
try:
- os.write(3, bytes("Started\n", 'utf-8'));
- except:
+ os.write(3, bytes("Started\n", "utf-8"))
+ except OSError:
pass
- print("Serving HTTP on port %d" % port);
+ print("Serving HTTP on port %d" % port)
if dir:
os.chdir(dir)
httpd.serve_forever()
-if __name__ == '__main__':
+
+if __name__ == "__main__":
dir = None
if len(sys.argv) >= 2 and len(sys.argv[1]) > 0:
dir = sys.argv[1]
--
2.47.1
From 47ff75860097afe0b07758b2457a52f0e6b94bd7 Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Thu, 22 Aug 2024 02:47:19 -0400
Subject: [PATCH 05/09] tests/oci-registry-server.py: Convert to argparse
---
tests/oci-registry-server.py | 15 ++++++++-------
tests/test-oci-registry.sh | 2 +-
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py
index 3050a883..65421f34 100755
--- a/tests/oci-registry-server.py
+++ b/tests/oci-registry-server.py
@@ -1,5 +1,6 @@
#!/usr/bin/python3
+import argparse
import base64
import hashlib
import json
@@ -246,7 +247,7 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
return
-def run(dir):
+def run(args):
RequestHandler.protocol_version = "HTTP/1.0"
httpd = http_server.HTTPServer(("127.0.0.1", 0), RequestHandler)
host, port = httpd.socket.getsockname()[:2]
@@ -257,14 +258,14 @@ def run(dir):
except OSError:
pass
print("Serving HTTP on port %d" % port)
- if dir:
- os.chdir(dir)
+ if args.dir:
+ os.chdir(args.dir)
httpd.serve_forever()
if __name__ == "__main__":
- dir = None
- if len(sys.argv) >= 2 and len(sys.argv[1]) > 0:
- dir = sys.argv[1]
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--dir")
+ args = parser.parse_args()
- run(dir)
+ run(args)
diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh
index bc2f138b..12036358 100755
--- a/tests/test-oci-registry.sh
+++ b/tests/test-oci-registry.sh
@@ -27,7 +27,7 @@ echo "1..14"
# Start the fake registry server
-httpd oci-registry-server.py .
+httpd oci-registry-server.py --dir=.
port=$(cat httpd-port)
client="python3 $test_srcdir/oci-registry-client.py --url=http://127.0.0.1:$port"
--
2.47.1
From c2c2f3679608a32339dadb233dc11633f22b0793 Mon Sep 17 00:00:00 2001
From: Sebastian Wick <sebastian.wick@redhat.com>
Date: Tue, 17 Dec 2024 16:39:16 +0100
Subject: [PATCH 06/09] tests/oci-registry-server.py: Always get bytes for the
response
And sent the Content-Length header.
---
tests/oci-registry-server.py | 46 +++++++++++++++++++-----------------
1 file changed, 24 insertions(+), 22 deletions(-)
diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py
index 65421f34..13bf50b3 100755
--- a/tests/oci-registry-server.py
+++ b/tests/oci-registry-server.py
@@ -5,7 +5,6 @@ import base64
import hashlib
import json
import os
-import sys
import time
from urllib.parse import parse_qs
@@ -27,7 +26,7 @@ def get_index():
}
)
- return json.dumps({"Registry": "/", "Results": results}, indent=4)
+ return json.dumps({"Registry": "/", "Results": results}, indent=4).encode("UTF-8")
def cache_icon(data_uri):
@@ -62,7 +61,6 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
path = parts[0].split("/")
route_path = route.split("/")
- print((route_path, path))
if len(route_path) != len(path):
return False
@@ -82,34 +80,47 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
return True
def do_GET(self):
- response = 200
- response_string = None
+ response = 404
+ response_string = b""
response_content_type = "application/octet-stream"
- response_file = None
add_headers = {}
+ def get_file_contents(repo_name, type, ref):
+ try:
+ path = repositories[repo_name][type][ref]
+ with open(path, "rb") as f:
+ return 200, f.read()
+ except KeyError:
+ return 404, b""
+
if self.check_route("/v2/@repo_name/blobs/@digest"):
repo_name = self.matches["repo_name"]
digest = self.matches["digest"]
- response_file = repositories[repo_name]["blobs"][digest]
+ response, response_string = get_file_contents(repo_name, "blobs", digest)
elif self.check_route("/v2/@repo_name/manifests/@ref"):
repo_name = self.matches["repo_name"]
ref = self.matches["ref"]
- response_file = repositories[repo_name]["manifests"][ref]
+ response, response_string = get_file_contents(repo_name, "manifests", ref)
elif self.check_route("/index/static") or self.check_route("/index/dynamic"):
etag = get_etag()
+ add_headers["Etag"] = etag
if self.headers.get("If-None-Match") == etag:
response = 304
+ response_string = b""
else:
+ response = 200
response_string = get_index()
- add_headers["Etag"] = etag
elif self.check_route("/icons/@filename"):
+ response = 200
response_string = icons[self.matches["filename"]]
- assert isinstance(response_string, bytes)
response_content_type = "image/png"
- else:
- response = 404
+
+ assert isinstance(response, int)
+ assert isinstance(response_string, bytes)
+ assert isinstance(response_content_type, str)
+
+ add_headers["Content-Length"] = len(response_string)
self.send_response(response)
for k, v in list(add_headers.items()):
@@ -123,16 +134,7 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
self.end_headers()
- if response == 200:
- if response_file:
- with open(response_file, "rb") as f:
- response_string = f.read()
-
- if isinstance(response_string, bytes):
- self.wfile.write(response_string)
- else:
- assert isinstance(response_string, str)
- self.wfile.write(response_string.encode("utf-8"))
+ self.wfile.write(response_string)
def do_HEAD(self):
return self.do_GET()
--
2.47.1
From 85379a0fe6b79d216a29497980ce2c8e0ab46997 Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Tue, 17 Dec 2024 16:41:38 +0100
Subject: [PATCH 07/09] tests/oci-registry: Add support for SSL to client and
server
---
tests/oci-registry-client.py | 19 ++++++++++++++++++-
tests/oci-registry-server.py | 22 +++++++++++++++++++++-
2 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py
index 5654062b..c4707c07 100755
--- a/tests/oci-registry-client.py
+++ b/tests/oci-registry-client.py
@@ -1,6 +1,7 @@
#!/usr/bin/python3
import argparse
+import ssl
import sys
import http.client
@@ -9,7 +10,20 @@ import urllib.parse
def get_conn(args):
parsed = urllib.parse.urlparse(args.url)
- return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port)
+ if parsed.scheme == "http":
+ return http.client.HTTPConnection(host=parsed.hostname, port=parsed.port)
+ elif parsed.scheme == "https":
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+ if args.cert:
+ context.load_cert_chain(certfile=args.cert, keyfile=args.key)
+ if args.cacert:
+ context.load_verify_locations(cafile=args.cacert)
+ return http.client.HTTPSConnection(
+ host=parsed.hostname, port=parsed.port, context=context
+ )
+ else:
+ assert False, "Bad scheme: " + parsed.scheme
def run_add(args):
@@ -42,6 +56,9 @@ def run_delete(args):
parser = argparse.ArgumentParser()
parser.add_argument("--url", required=True)
+parser.add_argument("--cacert")
+parser.add_argument("--cert")
+parser.add_argument("--key")
subparsers = parser.add_subparsers()
subparsers.required = True
diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py
index 13bf50b3..2bbe8c6e 100755
--- a/tests/oci-registry-server.py
+++ b/tests/oci-registry-server.py
@@ -5,6 +5,7 @@ import base64
import hashlib
import json
import os
+import ssl
import time
from urllib.parse import parse_qs
@@ -252,6 +253,19 @@ class RequestHandler(http_server.BaseHTTPRequestHandler):
def run(args):
RequestHandler.protocol_version = "HTTP/1.0"
httpd = http_server.HTTPServer(("127.0.0.1", 0), RequestHandler)
+
+ if args.cert:
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+ context.load_cert_chain(certfile=args.cert, keyfile=args.key)
+
+ if args.mtls_cacert:
+ context.load_verify_locations(cafile=args.mtls_cacert)
+ # In a real application, we'd need to check the CN against authorized users
+ context.verify_mode = ssl.CERT_REQUIRED
+
+ httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
+
host, port = httpd.socket.getsockname()[:2]
with open("httpd-port", "w") as file:
file.write("%d" % port)
@@ -259,7 +273,10 @@ def run(args):
os.write(3, bytes("Started\n", "utf-8"))
except OSError:
pass
- print("Serving HTTP on port %d" % port)
+ if args.cert:
+ print("Serving HTTPS on port %d" % port)
+ else:
+ print("Serving HTTP on port %d" % port)
if args.dir:
os.chdir(args.dir)
httpd.serve_forever()
@@ -268,6 +285,9 @@ def run(args):
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--dir")
+ parser.add_argument("--cert")
+ parser.add_argument("--key")
+ parser.add_argument("--mtls-cacert")
args = parser.parse_args()
run(args)
--
2.47.1
From 68b3fdcc0b00ee1080a1a3cd15dff33f1de6c0bf Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Fri, 23 Aug 2024 09:48:26 -0400
Subject: [PATCH 08/09] common: Implement /etc/containers/certs.d for OCI
registries
Docker and podman can be configured to use mutual TLS authentication
to the registry by dropping files into system-wide and user
directories. Implement this in a largely compatible way.
(Because of the limitations of our underlying libraries, we
can't support multiple certificates within the same host config,
but I don't expect anybody actually needs that.)
The certs.d handling is extended so that certificates are separately
looked up when downloading the look-aside index. This is mostly
to simplify our tests, so we can use one web server for both -
in actual operation, we expect the indexes to be unauthenticated.
Also for testing purposes, FLATPAK_CONTAINER_CERTS_D is supported
to override the standard search path.
Co-authored-by: Sebastian Wick <sebastian.wick@redhat.com>
---
.github/workflows/check.yml | 2 +-
common/flatpak-oci-registry.c | 86 +++++--
common/flatpak-utils-http-private.h | 12 +
common/flatpak-utils-http.c | 368 +++++++++++++++++++++++++++-
doc/flatpak-remote.xml | 6 +
tests/httpcache.c | 2 +-
6 files changed, 440 insertions(+), 36 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index dbf44c1d..6cce2a6f 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -52,7 +52,7 @@ jobs:
libjson-glib-dev shared-mime-info desktop-file-utils libpolkit-agent-1-dev libpolkit-gobject-1-dev \
libseccomp-dev libsoup2.4-dev libcurl4-openssl-dev libsystemd-dev libxml2-utils libgpgme11-dev gobject-introspection \
libgirepository1.0-dev libappstream-dev libdconf-dev clang socat meson libdbus-1-dev e2fslibs-dev bubblewrap xdg-dbus-proxy \
- python3-pip meson ninja-build libyaml-dev libstemmer-dev gperf itstool libmalcontent-0-dev
+ python3-pip meson ninja-build libyaml-dev libstemmer-dev gperf itstool libmalcontent-0-dev openssl
# One of the tests wants this
sudo mkdir /tmp/flatpak-com.example.App-OwnedByRoot
- name: Check out flatpak
diff --git a/common/flatpak-oci-registry.c b/common/flatpak-oci-registry.c
index ad595e53..6d36de2a 100644
--- a/common/flatpak-oci-registry.c
+++ b/common/flatpak-oci-registry.c
@@ -76,7 +76,8 @@ struct FlatpakOciRegistry
/* Remote repos */
FlatpakHttpSession *http_session;
- GUri *base_uri;
+ GUri *base_uri;
+ FlatpakCertificates *certificates;
};
typedef struct
@@ -353,31 +354,29 @@ choose_alt_uri (GUri *base_uri,
}
static GBytes *
-remote_load_file (FlatpakHttpSession *http_session,
- GUri *base,
- const char *subpath,
- const char **alt_uris,
- const char *token,
- char **out_content_type,
- GCancellable *cancellable,
- GError **error)
+remote_load_file (FlatpakOciRegistry *self,
+ const char *subpath,
+ const char **alt_uris,
+ char **out_content_type,
+ GCancellable *cancellable,
+ GError **error)
{
g_autoptr(GBytes) bytes = NULL;
g_autofree char *uri_s = NULL;
- uri_s = choose_alt_uri (base, alt_uris);
+ uri_s = choose_alt_uri (self->base_uri, alt_uris);
if (uri_s == NULL)
{
- uri_s = parse_relative_uri (base, subpath, error);
+ uri_s = parse_relative_uri (self->base_uri, subpath, error);
if (uri_s == NULL)
return NULL;
}
- bytes = flatpak_load_uri (http_session,
- uri_s, FLATPAK_HTTP_FLAGS_ACCEPT_OCI,
- token,
- NULL, NULL, out_content_type,
- cancellable, error);
+ bytes = flatpak_load_uri_full (self->http_session,
+ uri_s, self->certificates, FLATPAK_HTTP_FLAGS_ACCEPT_OCI,
+ NULL, self->token,
+ NULL, NULL, NULL, out_content_type, NULL,
+ cancellable, error);
if (bytes == NULL)
return NULL;
@@ -395,7 +394,7 @@ flatpak_oci_registry_load_file (FlatpakOciRegistry *self,
if (self->dfd != -1)
return local_load_file (self->dfd, subpath, cancellable, error);
else
- return remote_load_file (self->http_session, self->base_uri, subpath, alt_uris, self->token, out_content_type, cancellable, error);
+ return remote_load_file (self, subpath, alt_uris, out_content_type, cancellable, error);
}
static JsonNode *
@@ -548,6 +547,7 @@ flatpak_oci_registry_ensure_remote (FlatpakOciRegistry *self,
GError **error)
{
g_autoptr(GUri) baseuri = NULL;
+ g_autoptr(GError) local_error = NULL;
if (for_write)
{
@@ -568,6 +568,13 @@ flatpak_oci_registry_ensure_remote (FlatpakOciRegistry *self,
self->is_docker = TRUE;
self->base_uri = g_steal_pointer (&baseuri);
+ self->certificates = flatpak_get_certificates_for_uri (self->uri, &local_error);
+ if (local_error)
+ {
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return FALSE;
+ }
+
return TRUE;
}
@@ -834,6 +841,7 @@ flatpak_oci_registry_download_blob (FlatpakOciRegistry *self,
return -1;
if (!flatpak_download_http_uri (self->http_session, uri_s,
+ self->certificates,
FLATPAK_HTTP_FLAGS_ACCEPT_OCI,
out_stream,
self->token,
@@ -925,7 +933,8 @@ flatpak_oci_registry_mirror_blob (FlatpakOciRegistry *self,
out_stream = g_unix_output_stream_new (tmpf.fd, FALSE);
- if (!flatpak_download_http_uri (source_registry->http_session, uri_s,
+ if (!flatpak_download_http_uri (source_registry->http_session,
+ uri_s, source_registry->certificates,
FLATPAK_HTTP_FLAGS_ACCEPT_OCI, out_stream,
self->token,
progress_cb, user_data,
@@ -1055,6 +1064,7 @@ get_token_for_www_auth (FlatpakOciRegistry *self,
body = flatpak_load_uri_full (self->http_session,
auth_uri_s,
+ self->certificates,
FLATPAK_HTTP_FLAGS_NOCHECK_STATUS,
auth, NULL,
NULL, NULL,
@@ -1146,7 +1156,7 @@ flatpak_oci_registry_get_token (FlatpakOciRegistry *self,
if (uri_s == NULL)
return NULL;
- body = flatpak_load_uri_full (self->http_session, uri_s,
+ body = flatpak_load_uri_full (self->http_session, uri_s, self->certificates,
FLATPAK_HTTP_FLAGS_ACCEPT_OCI | FLATPAK_HTTP_FLAGS_HEAD | FLATPAK_HTTP_FLAGS_NOCHECK_STATUS,
NULL, NULL,
NULL, NULL,
@@ -2080,11 +2090,11 @@ flatpak_oci_registry_find_delta_manifest (FlatpakOciRegistry *registry,
g_autofree char *uri_s = parse_relative_uri (registry->base_uri, delta_manifest_url, NULL);
if (uri_s != NULL)
- bytes = flatpak_load_uri (registry->http_session,
- uri_s, FLATPAK_HTTP_FLAGS_ACCEPT_OCI,
- registry->token,
- NULL, NULL, NULL,
- cancellable, NULL);
+ bytes = flatpak_load_uri_full (registry->http_session,
+ uri_s, registry->certificates, FLATPAK_HTTP_FLAGS_ACCEPT_OCI,
+ NULL, registry->token,
+ NULL, NULL, NULL, NULL, NULL,
+ cancellable, NULL);
if (bytes != NULL)
{
g_autoptr(FlatpakOciVersioned) versioned =
@@ -2760,6 +2770,7 @@ flatpak_oci_index_ensure_cached (FlatpakHttpSession *http_session,
g_autofree char *tag = NULL;
const char *oci_arch = NULL;
gboolean success = FALSE;
+ g_autoptr(FlatpakCertificates) certificates = NULL;
g_autoptr(GError) local_error = NULL;
GUri *tmp_uri;
@@ -2844,8 +2855,16 @@ flatpak_oci_index_ensure_cached (FlatpakHttpSession *http_session,
query_uri_s = g_uri_to_string_partial (query_uri, G_URI_HIDE_PASSWORD);
+ certificates = flatpak_get_certificates_for_uri (query_uri_s, &local_error);
+ if (local_error)
+ {
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return FALSE;
+ }
+
success = flatpak_cache_http_uri (http_session,
query_uri_s,
+ certificates,
FLATPAK_HTTP_FLAGS_STORE_COMPRESSED,
AT_FDCWD, index_path,
NULL, NULL,
@@ -3082,6 +3101,7 @@ flatpak_oci_index_make_summary (GFile *index,
static gboolean
add_icon_image (FlatpakHttpSession *http_session,
const char *index_uri,
+ FlatpakCertificates *certificates,
int icons_dfd,
GHashTable *used_icons,
const char *subdir,
@@ -3132,7 +3152,7 @@ add_icon_image (FlatpakHttpSession *http_session,
if (icon_uri_s == NULL)
return FALSE;
- if (!flatpak_cache_http_uri (http_session, icon_uri_s,
+ if (!flatpak_cache_http_uri (http_session, icon_uri_s, certificates,
0 /* flags */,
icons_dfd, icon_path,
NULL, NULL,
@@ -3152,6 +3172,7 @@ add_icon_image (FlatpakHttpSession *http_session,
static void
add_image_to_appstream (FlatpakHttpSession *http_session,
const char *index_uri,
+ FlatpakCertificates *certificates,
FlatpakXml *appstream_root,
int icons_dfd,
GHashTable *used_icons,
@@ -3243,6 +3264,7 @@ add_image_to_appstream (FlatpakHttpSession *http_session,
{
if (!add_icon_image (http_session,
index_uri,
+ certificates,
icons_dfd,
used_icons,
icon_sizes[i].subdir, id, icon_data,
@@ -3338,6 +3360,8 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session,
g_autoptr(FlatpakXml) appstream_root = NULL;
g_autoptr(GBytes) bytes = NULL;
g_autoptr(GHashTable) used_icons = NULL;
+ g_autoptr(FlatpakCertificates) certificates = NULL;
+ g_autoptr(GError) local_error = NULL;
int i;
const char *oci_arch = flatpak_arch_to_oci_arch (arch);
@@ -3351,6 +3375,14 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session,
appstream_root = flatpak_appstream_xml_new ();
+ certificates = flatpak_get_certificates_for_uri (index_uri, &local_error);
+ if (local_error)
+ {
+ g_print ("Failed to load certificates for %s: %s",
+ index_uri, local_error->message);
+ g_clear_error (&local_error);
+ }
+
for (i = 0; response->results != NULL && response->results[i] != NULL; i++)
{
FlatpakOciIndexRepository *r = response->results[i];
@@ -3361,7 +3393,7 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session,
FlatpakOciIndexImage *image = r->images[j];
if (g_strcmp0 (image->architecture, oci_arch) == 0)
add_image_to_appstream (http_session,
- index_uri,
+ index_uri, certificates,
appstream_root, icons_dfd, used_icons,
r, image,
cancellable);
@@ -3377,7 +3409,7 @@ flatpak_oci_index_make_appstream (FlatpakHttpSession *http_session,
FlatpakOciIndexImage *image = list->images[k];
if (g_strcmp0 (image->architecture, oci_arch) == 0)
add_image_to_appstream (http_session,
- index_uri,
+ index_uri, certificates,
appstream_root, icons_dfd, used_icons,
r, image,
cancellable);
diff --git a/common/flatpak-utils-http-private.h b/common/flatpak-utils-http-private.h
index 2c89ba40..a930ee79 100644
--- a/common/flatpak-utils-http-private.h
+++ b/common/flatpak-utils-http-private.h
@@ -39,6 +39,15 @@ void flatpak_http_session_free (FlatpakHttpSession* http_session);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FlatpakHttpSession, flatpak_http_session_free)
+typedef struct FlatpakCertificates FlatpakCertificates;
+
+FlatpakCertificates* flatpak_get_certificates_for_uri (const char *uri,
+ GError **error);
+FlatpakCertificates * flatpak_certificates_copy (FlatpakCertificates *other);
+void flatpak_certificates_free (FlatpakCertificates *certificates);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(FlatpakCertificates, flatpak_certificates_free)
+
typedef enum {
FLATPAK_HTTP_FLAGS_NONE = 0,
FLATPAK_HTTP_FLAGS_ACCEPT_OCI = 1 << 0,
@@ -52,6 +61,7 @@ typedef void (*FlatpakLoadUriProgress) (guint64 downloaded_bytes,
GBytes * flatpak_load_uri_full (FlatpakHttpSession *http_session,
const char *uri,
+ FlatpakCertificates *certificates,
FlatpakHTTPFlags flags,
const char *auth,
const char *token,
@@ -73,6 +83,7 @@ GBytes * flatpak_load_uri (FlatpakHttpSession *http_session,
GError **error);
gboolean flatpak_download_http_uri (FlatpakHttpSession *http_session,
const char *uri,
+ FlatpakCertificates *certificates,
FlatpakHTTPFlags flags,
GOutputStream *out,
const char *token,
@@ -82,6 +93,7 @@ gboolean flatpak_download_http_uri (FlatpakHttpSession *http_session,
GError **error);
gboolean flatpak_cache_http_uri (FlatpakHttpSession *http_session,
const char *uri,
+ FlatpakCertificates *certificates,
FlatpakHTTPFlags flags,
int dest_dfd,
const char *dest_subpath,
diff --git a/common/flatpak-utils-http.c b/common/flatpak-utils-http.c
index 27de9c7a..e8d5d724 100644
--- a/common/flatpak-utils-http.c
+++ b/common/flatpak-utils-http.c
@@ -73,6 +73,17 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (SoupURI, soup_uri_free)
G_DEFINE_QUARK (flatpak_http_error, flatpak_http_error)
+/* Holds information about CA and client certificates found in
+ * system-wide and per-user certificate directories as documented
+ * in container-certs.d(5).
+ */
+struct FlatpakCertificates
+{
+ char *ca_cert_file;
+ char *client_cert_file;
+ char *client_key_file;
+};
+
/* Information about the cache status of a file.
Encoded in an xattr on the cached file, or a file on the side if xattrs don't work.
*/
@@ -95,6 +106,7 @@ typedef struct
FlatpakHTTPFlags flags;
const char *auth;
const char *token;
+ FlatpakCertificates *certificates;
FlatpakLoadUriProgress progress;
GCancellable *cancellable;
gpointer user_data;
@@ -231,6 +243,157 @@ check_http_status (guint status_code,
return FALSE;
}
+FlatpakCertificates*
+flatpak_get_certificates_for_uri (const char *uri,
+ GError **error)
+{
+ g_autoptr(FlatpakCertificates) certificates = NULL;
+ g_autoptr(GUri) parsed_uri = NULL;
+ g_autofree char *hostport = NULL;
+ const char *system_certs_d = NULL;
+ g_autofree char *certs_path_str = NULL;
+ g_auto(GStrv) certs_path = NULL;
+
+ certificates = g_new0 (FlatpakCertificates, 1);
+
+ parsed_uri = g_uri_parse (uri, G_URI_FLAGS_PARSE_RELAXED, error);
+ if (!parsed_uri)
+ return NULL;
+
+ if (!g_uri_get_host (parsed_uri))
+ return NULL;
+
+ if (g_uri_get_port (parsed_uri) != -1)
+ hostport = g_strdup_printf ("%s:%d", g_uri_get_host (parsed_uri), g_uri_get_port (parsed_uri));
+ else
+ hostport = g_strdup (g_uri_get_host (parsed_uri));
+
+ system_certs_d = g_getenv ("FLATPAK_SYSTEM_CERTS_D");
+ if (system_certs_d == NULL || system_certs_d[0] == '\0')
+ system_certs_d = "/etc/containers/certs.d:/etc/docker/certs.d";
+
+ /* containers/image hardcodes ~/.config and doesn't honor XDG_CONFIG_HOME */
+ certs_path_str = g_strconcat (g_get_user_config_dir(), "/containers/certs.d:",
+ system_certs_d, NULL);
+ certs_path = g_strsplit (certs_path_str, ":", -1);
+
+ for (int i = 0; certs_path[i]; i++)
+ {
+ g_autoptr(GFile) certs_dir = g_file_new_for_path (certs_path[i]);
+ g_autoptr(GFile) host_dir = g_file_get_child (certs_dir, hostport);
+ g_autoptr(GFileEnumerator) enumerator;
+ g_autoptr(GError) local_error = NULL;
+
+ enumerator = g_file_enumerate_children (host_dir, G_FILE_ATTRIBUTE_STANDARD_NAME,
+ G_FILE_QUERY_INFO_NONE,
+ NULL, &local_error);
+ if (enumerator == NULL)
+ {
+ /* This matches libpod - missing certificate directory or a permission
+ * error causes the directory to be skipped; any other error is fatal
+ */
+ if (g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) ||
+ g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
+ {
+ g_clear_error (&local_error);
+ continue;
+ }
+ else
+ {
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return NULL;
+ }
+ }
+
+ while (TRUE)
+ {
+ GFile *child;
+ g_autofree char *basename = NULL;
+
+ if (!g_file_enumerator_iterate (enumerator, NULL, &child, NULL, error))
+ return NULL;
+
+ if (child == NULL)
+ break;
+
+ basename = g_file_get_basename (child);
+
+ /* In libpod, all CA certificates are added to the CA certificate
+ * database. We just use the first in readdir order.
+ */
+ if (g_str_has_suffix (basename, ".crt") && certificates->ca_cert_file == NULL)
+ certificates->ca_cert_file = g_file_get_path (child);
+
+ if (g_str_has_suffix (basename, ".cert"))
+ {
+ g_autofree char *nosuffix = g_strndup (basename, strlen (basename) - 5);
+ g_autofree char *key_basename = g_strconcat (nosuffix, ".key", NULL);
+ g_autoptr(GFile) key_file = g_file_get_child (host_dir, key_basename);
+
+ if (!g_file_query_exists (key_file, NULL))
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "missing key %s for client cert %s. "
+ "Note that CA certificates should use the extension .crt",
+ g_file_peek_path (key_file),
+ g_file_peek_path (child));
+ return NULL;
+ }
+
+ /* In libpod, all client certificates are added, and then the go TLS
+ * code selects the best based on TLS negotation. We just pick the first
+ * in readdir order
+ * */
+ if (certificates->client_cert_file == NULL)
+ {
+ certificates->client_cert_file = g_file_get_path (child);
+ certificates->client_key_file = g_file_get_path (key_file);
+ }
+ }
+
+ if (g_str_has_suffix (basename, ".key"))
+ {
+ g_autofree char *nosuffix = g_strndup (basename, strlen (basename) - 4);
+ g_autofree char *cert_basename = g_strconcat (nosuffix, ".cert", NULL);
+ g_autoptr(GFile) cert_file = g_file_get_child (host_dir, cert_basename);
+
+ if (!g_file_query_exists (cert_file, NULL))
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "missing client certificate %s for key %s",
+ g_file_peek_path (cert_file),
+ g_file_peek_path (child));
+ return NULL;
+ }
+ }
+ }
+ }
+
+ return g_steal_pointer (&certificates);
+}
+
+FlatpakCertificates *
+flatpak_certificates_copy (FlatpakCertificates *other)
+{
+ FlatpakCertificates *certificates = g_new0 (FlatpakCertificates, 1);
+
+ certificates->ca_cert_file = g_strdup (other->ca_cert_file);
+ certificates->client_cert_file = g_strdup (other->client_cert_file);
+ certificates->client_key_file = g_strdup (other->client_key_file);
+
+ return certificates;
+}
+
+void
+flatpak_certificates_free (FlatpakCertificates *certificates)
+{
+ g_clear_pointer (&certificates->ca_cert_file, g_free);
+ g_clear_pointer (&certificates->client_cert_file, g_free);
+ g_clear_pointer (&certificates->client_key_file, g_free);
+
+ g_free (certificates);
+}
+
#if defined(HAVE_CURL)
/************************************************************************
@@ -477,6 +640,18 @@ flatpak_download_http_uri_once (FlatpakHttpSession *session,
curl_easy_setopt (curl, CURLOPT_WRITEDATA, (void *)data);
curl_easy_setopt (curl, CURLOPT_HEADERDATA, (void *)data);
+ if (data->certificates)
+ {
+ if (data->certificates->ca_cert_file)
+ curl_easy_setopt (curl, CURLOPT_CAINFO, data->certificates->ca_cert_file);
+
+ if (data->certificates->client_cert_file)
+ {
+ curl_easy_setopt (curl, CURLOPT_SSLCERT, data->certificates->client_cert_file);
+ curl_easy_setopt (curl, CURLOPT_SSLKEY, data->certificates->client_key_file);
+ }
+ }
+
if (data->flags & FLATPAK_HTTP_FLAGS_HEAD)
curl_easy_setopt (curl, CURLOPT_NOBODY, 1L);
else
@@ -576,6 +751,73 @@ flatpak_download_http_uri_once (FlatpakHttpSession *session,
* Soup implementation *
***********************************************************************/
+/*
+ * The implementation of /etc/containers/certs.d for Soup is made tricky
+ * because the CA certificate database in Soup is global to the session,
+ * but we share a single sesssion between different hosts that might
+ * need different custom CA certs based on what's configured in certs.d.
+ *
+ * So what we do is make the FlatpakSoupSession multiplex multiple
+ * SoupSessions, depending on the certificates we use. The most common
+ * case, is of course, a single session with no custom certificates.
+ */
+
+typedef struct
+{
+ char *user_agent;
+ GHashTable *soup_sessions;
+} FlatpakSoupSession;
+
+static guint
+certificates_hash(const FlatpakCertificates *certificates)
+{
+ guint hash = 0;
+
+ if (certificates && certificates->ca_cert_file)
+ hash |= 13 * g_str_hash (certificates->ca_cert_file);
+ if (certificates && certificates->client_cert_file)
+ hash |= 17 * g_str_hash (certificates->client_cert_file);
+ if (certificates && certificates->client_key_file)
+ hash |= 23 * g_str_hash (certificates->client_key_file);
+
+ return hash;
+}
+
+static gboolean
+certificates_equal (const FlatpakCertificates *a,
+ const FlatpakCertificates *b)
+{
+ if (a && b)
+ {
+ return (g_strcmp0(a->ca_cert_file, b->ca_cert_file) == 0 &&
+ g_strcmp0(a->client_cert_file, b->client_cert_file) == 0 &&
+ g_strcmp0(a->client_key_file, b->client_key_file) == 0);
+ }
+ else
+ return a == b;
+}
+
+static void
+certificates_free (FlatpakCertificates *certificates)
+{
+ if (certificates)
+ flatpak_certificates_free (certificates);
+}
+
+static FlatpakSoupSession *
+flatpak_create_soup_session (const char *user_agent)
+{
+ FlatpakSoupSession *session = g_new0 (FlatpakSoupSession, 1);
+
+ session->user_agent = g_strdup (user_agent);
+ session->soup_sessions = g_hash_table_new_full ((GHashFunc)certificates_hash,
+ (GEqualFunc)certificates_equal,
+ (GDestroyNotify)certificates_free,
+ (GDestroyNotify)g_object_unref);
+
+ return session;
+}
+
static gboolean
check_soup_transfer_error (SoupMessage *msg, GError **error)
{
@@ -768,18 +1010,114 @@ load_uri_callback (GObject *source_object,
load_uri_read_cb, data);
}
+/* Inline class for providing a pre-configured client certificate; from
+ * libsoup/examples/get.c. By Colin Walters.
+ */
+struct _FlatpakTlsInteraction
+{
+ GTlsInteraction parent_instance;
+
+ GTlsCertificate *cert;
+};
+
+struct _FlatpakTlsInteractionClass
+{
+ GTlsInteractionClass parent_class;
+};
+
+G_DECLARE_FINAL_TYPE (FlatpakTlsInteraction,
+ flatpak_tls_interaction,
+ FLATPAK, TLS_INTERACTION,
+ GTlsInteraction)
+
+G_DEFINE_TYPE (FlatpakTlsInteraction,
+ flatpak_tls_interaction,
+ G_TYPE_TLS_INTERACTION)
+
+static GTlsInteractionResult
+flatpak_tls_interaction_request_certificate (GTlsInteraction *interaction,
+ GTlsConnection *connection,
+ GTlsCertificateRequestFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ FlatpakTlsInteraction *self = FLATPAK_TLS_INTERACTION (interaction);
+
+ g_tls_connection_set_certificate (connection, self->cert);
+
+ return G_TLS_INTERACTION_HANDLED;
+}
+
+static void
+flatpak_tls_interaction_finalize (GObject *object)
+{
+ FlatpakTlsInteraction *self = FLATPAK_TLS_INTERACTION (object);
+
+ g_clear_object (&self->cert);
+
+ G_OBJECT_CLASS (flatpak_tls_interaction_parent_class)->finalize (object);
+}
+
+static void
+flatpak_tls_interaction_init (FlatpakTlsInteraction *interaction)
+{
+}
+
+static void
+flatpak_tls_interaction_class_init (FlatpakTlsInteractionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GTlsInteractionClass *interaction_class = G_TLS_INTERACTION_CLASS (klass);
+
+ object_class->finalize = flatpak_tls_interaction_finalize;
+
+ interaction_class->request_certificate = flatpak_tls_interaction_request_certificate;
+}
+
+static FlatpakTlsInteraction *
+flatpak_tls_interaction_new (GTlsCertificate *cert)
+{
+ FlatpakTlsInteraction *self = g_object_new (flatpak_tls_interaction_get_type (), NULL);
+
+ self->cert = g_object_ref (cert);
+
+ return self;
+}
+
static SoupSession *
-flatpak_create_soup_session (const char *user_agent)
+get_soup_session (FlatpakSoupSession *session, FlatpakCertificates *certificates, GError **error)
{
SoupSession *soup_session;
const char *http_proxy;
- soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, user_agent,
- SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
+ soup_session = g_hash_table_lookup (session->soup_sessions, certificates);
+ if (soup_session)
+ return soup_session;
+
+ soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, session->user_agent,
SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
SOUP_SESSION_TIMEOUT, FLATPAK_HTTP_TIMEOUT_SECS,
SOUP_SESSION_IDLE_TIMEOUT, FLATPAK_HTTP_TIMEOUT_SECS,
NULL);
+ if (certificates && certificates->ca_cert_file)
+ g_object_set (soup_session, SOUP_SESSION_SSL_CA_FILE, certificates->ca_cert_file, NULL);
+ else
+ g_object_set (soup_session, SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE, NULL);
+
+ if (certificates && certificates->client_cert_file)
+ {
+ g_autoptr(GTlsCertificate) client_cert = NULL;
+ g_autoptr(GTlsInteraction) interaction = NULL;
+
+ client_cert = g_tls_certificate_new_from_files (certificates->client_cert_file,
+ certificates->client_key_file, error);
+ if (!client_cert)
+ return NULL;
+
+ interaction = G_TLS_INTERACTION (flatpak_tls_interaction_new (client_cert));
+ g_object_set (soup_session, SOUP_SESSION_TLS_INTERACTION, interaction, NULL);
+ }
+
http_proxy = g_getenv ("http_proxy");
if (http_proxy)
{
@@ -793,6 +1131,10 @@ flatpak_create_soup_session (const char *user_agent)
if (g_getenv ("OSTREE_DEBUG_HTTP"))
soup_session_add_feature (soup_session, (SoupSessionFeature *) soup_logger_new (SOUP_LOGGER_LOG_BODY, 500));
+ g_hash_table_replace (session->soup_sessions,
+ certificates ? flatpak_certificates_copy (certificates) : NULL,
+ soup_session);
+
return soup_session;
}
@@ -805,9 +1147,11 @@ flatpak_create_http_session (const char *user_agent)
void
flatpak_http_session_free (FlatpakHttpSession* http_session)
{
- SoupSession *soup_session = (SoupSession *)http_session;
+ FlatpakSoupSession *session = (FlatpakSoupSession *)http_session;
- g_object_unref (soup_session);
+ g_hash_table_destroy (session->soup_sessions);
+ g_free (session->user_agent);
+ g_free (session);
}
static gboolean
@@ -816,12 +1160,16 @@ flatpak_download_http_uri_once (FlatpakHttpSession *http_session,
const char *uri,
GError **error)
{
- SoupSession *soup_session = (SoupSession *)http_session;
+ SoupSession *soup_session;
g_autoptr(SoupRequestHTTP) request = NULL;
SoupMessage *m;
g_info ("Loading %s using libsoup", uri);
+ soup_session = get_soup_session ((FlatpakSoupSession *)http_session, data->certificates, error);
+ if (!soup_session)
+ return FALSE;
+
request = soup_session_request_http (soup_session,
(data->flags & FLATPAK_HTTP_FLAGS_HEAD) != 0 ? "HEAD" : "GET",
uri, error);
@@ -927,6 +1275,7 @@ flatpak_http_should_retry_request (const GError *error,
GBytes *
flatpak_load_uri_full (FlatpakHttpSession *http_session,
const char *uri,
+ FlatpakCertificates *certificates,
FlatpakHTTPFlags flags,
const char *auth,
const char *token,
@@ -965,6 +1314,7 @@ flatpak_load_uri_full (FlatpakHttpSession *http_session,
data.last_progress_time = g_get_monotonic_time ();
data.cancellable = cancellable;
data.flags = flags;
+ data.certificates = certificates;
data.auth = auth;
data.token = token;
@@ -1018,7 +1368,7 @@ flatpak_load_uri (FlatpakHttpSession *http_session,
GCancellable *cancellable,
GError **error)
{
- return flatpak_load_uri_full (http_session, uri, flags, NULL, token,
+ return flatpak_load_uri_full (http_session, uri, NULL, flags, NULL, token,
progress, user_data, NULL, out_content_type, NULL,
cancellable, error);
}
@@ -1026,6 +1376,7 @@ flatpak_load_uri (FlatpakHttpSession *http_session,
gboolean
flatpak_download_http_uri (FlatpakHttpSession *http_session,
const char *uri,
+ FlatpakCertificates *certificates,
FlatpakHTTPFlags flags,
GOutputStream *out,
const char *token,
@@ -1047,6 +1398,7 @@ flatpak_download_http_uri (FlatpakHttpSession *http_session,
data.user_data = user_data;
data.last_progress_time = g_get_monotonic_time ();
data.cancellable = cancellable;
+ data.certificates = certificates;
data.flags = flags;
data.token = token;
@@ -1384,6 +1736,7 @@ set_cache_http_data_from_headers (CacheHttpData *cache_data,
gboolean
flatpak_cache_http_uri (FlatpakHttpSession *http_session,
const char *uri,
+ FlatpakCertificates *certificates,
FlatpakHTTPFlags flags,
int dest_dfd,
const char *dest_subpath,
@@ -1444,6 +1797,7 @@ flatpak_cache_http_uri (FlatpakHttpSession *http_session,
data.last_progress_time = g_get_monotonic_time ();
data.cancellable = cancellable;
data.flags = flags;
+ data.certificates = certificates;
data.cache_data = cache_data;
diff --git a/doc/flatpak-remote.xml b/doc/flatpak-remote.xml
index 798f5c39..47a5a42a 100644
--- a/doc/flatpak-remote.xml
+++ b/doc/flatpak-remote.xml
@@ -80,6 +80,12 @@
is a Flatpak extension that indicates that the remote is not an ostree
repository, but is rather an URL to an index of OCI images that are stored
within a container image registry.
+ </para>
+ <para>
+ For OCI remotes, client and CA certificates are read from
+ <filename>/etc/containers/certs.d</filename> and
+ <filename>~/.config/containers/certs.d</filename> as documented in
+ <citerefentry><refentrytitle>containers-certs.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
</para></listitem>
</varlistentry>
<varlistentry>
diff --git a/tests/httpcache.c b/tests/httpcache.c
index a4550fb0..f6f9de64 100644
--- a/tests/httpcache.c
+++ b/tests/httpcache.c
@@ -32,7 +32,7 @@ main (int argc, char *argv[])
if (!flatpak_cache_http_uri (session,
- url,
+ url, NULL,
flags,
AT_FDCWD, dest,
NULL, NULL, NULL, &error))
--
2.47.1
From cf555f02fcc1cf410fdad7607ff83a6764864a14 Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Tue, 17 Dec 2024 17:47:35 +0100
Subject: [PATCH 09/09] tests: Add tests for https OCI remotes
---
tests/libtest.sh | 17 +++++--
tests/test-matrix/meson.build | 6 ++-
tests/test-oci-registry.sh | 90 ++++++++++++++++++++++++++++++-----
tests/test-wrapper.sh | 6 +++
tests/update-test-matrix | 2 +-
5 files changed, 101 insertions(+), 20 deletions(-)
diff --git a/tests/libtest.sh b/tests/libtest.sh
index d63810e1..7dad594f 100644
--- a/tests/libtest.sh
+++ b/tests/libtest.sh
@@ -332,12 +332,16 @@ make_runtime () {
}
httpd () {
- COMMAND=${1:-web-server.py}
- DIR=${2:-repos}
+ if [ $# -eq 0 ] ; then
+ set web-server.py repos
+ fi
+
+ COMMAND=$1
+ shift
rm -f httpd-pipe
mkfifo httpd-pipe
- PYTHONUNBUFFERED=1 $(dirname $0)/$COMMAND "$DIR" 3> httpd-pipe 2>&1 | tee -a httpd-log >&2 &
+ PYTHONUNBUFFERED=1 $(dirname $0)/$COMMAND "$@" 3> httpd-pipe 2>&1 | tee -a httpd-log >&2 &
read < httpd-pipe
}
@@ -589,10 +593,15 @@ skip_without_libsystemd () {
fi
}
+FLATPAK_SYSTEM_CERTS_D=$(pwd)/certs.d
+export FLATPAK_SYSTEM_CERTS_D
+
sed s#@testdir@#${test_builddir}# ${test_srcdir}/session.conf.in > session.conf
dbus-daemon --fork --config-file=session.conf --print-address=3 --print-pid=4 \
3> dbus-session-bus-address 4> dbus-session-bus-pid
-export DBUS_SESSION_BUS_ADDRESS="$(cat dbus-session-bus-address)"
+
+DBUS_SESSION_BUS_ADDRESS="$(cat dbus-session-bus-address)"
+export DBUS_SESSION_BUS_ADDRESS
DBUS_SESSION_BUS_PID="$(cat dbus-session-bus-pid)"
if ! /bin/kill -0 "$DBUS_SESSION_BUS_PID"; then
diff --git a/tests/test-matrix/meson.build b/tests/test-matrix/meson.build
index 15176048..fd0b5034 100644
--- a/tests/test-matrix/meson.build
+++ b/tests/test-matrix/meson.build
@@ -17,8 +17,10 @@ wrapped_tests += {'name' : 'test-sideload@system.wrap', 'script' : 'test-sideloa
wrapped_tests += {'name' : 'test-bundle@user.wrap', 'script' : 'test-bundle.sh'}
wrapped_tests += {'name' : 'test-bundle@system.wrap', 'script' : 'test-bundle.sh'}
wrapped_tests += {'name' : 'test-bundle@system-norevokefs.wrap', 'script' : 'test-bundle.sh'}
-wrapped_tests += {'name' : 'test-oci-registry@user.wrap', 'script' : 'test-oci-registry.sh'}
-wrapped_tests += {'name' : 'test-oci-registry@system.wrap', 'script' : 'test-oci-registry.sh'}
+wrapped_tests += {'name' : 'test-oci-registry@user,http.wrap', 'script' : 'test-oci-registry.sh'}
+wrapped_tests += {'name' : 'test-oci-registry@user,https.wrap', 'script' : 'test-oci-registry.sh'}
+wrapped_tests += {'name' : 'test-oci-registry@system,http.wrap', 'script' : 'test-oci-registry.sh'}
+wrapped_tests += {'name' : 'test-oci-registry@system,https.wrap', 'script' : 'test-oci-registry.sh'}
wrapped_tests += {'name' : 'test-update-remote-configuration@newsummary.wrap', 'script' : 'test-update-remote-configuration.sh'}
wrapped_tests += {'name' : 'test-update-remote-configuration@oldsummary.wrap', 'script' : 'test-update-remote-configuration.sh'}
wrapped_tests += {'name' : 'test-update-portal@user.wrap', 'script' : 'test-update-portal.sh'}
diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh
index 12036358..da234ded 100755
--- a/tests/test-oci-registry.sh
+++ b/tests/test-oci-registry.sh
@@ -27,9 +27,73 @@ echo "1..14"
# Start the fake registry server
-httpd oci-registry-server.py --dir=.
+if [ x${USE_HTTPS} = xyes ] ; then
+ cat > openssl.config <<EOF
+[req]
+distinguished_name=default_dn
+
+[v3_ca]
+basicConstraints=critical,CA:TRUE,pathlen:0
+
+[server_cert]
+basicConstraints=CA:FALSE
+subjectAltName=DNS:registry.example.com,IP:127.0.0.1
+
+[usr_cert]
+subjectAltName=email:copy
+basicConstraints=CA:FALSE
+keyUsage=digitalSignature
+extendedKeyUsage=clientAuth
+
+[default_dn]
+CN=Unused
+EOF
+
+ openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
+ -nodes -keyout example.com.ca.key -out example.com.ca.crt \
+ -subj="/CN=Example CA/O=example.com/emailAddress=nomail@example.com" \
+ -config openssl.config -extensions v3_ca
+
+ openssl req -newkey rsa:4096 -sha256 \
+ -nodes -keyout example.com.key -out example.com.csr \
+ -subj "/CN=registry.example.com"
+
+ openssl x509 -req -in example.com.csr -days 3650 \
+ -CA example.com.ca.crt -CAkey example.com.ca.key -CAcreateserial \
+ -extfile openssl.config -extensions server_cert \
+ -out example.com.crt
+
+ openssl req -newkey rsa:4096 -sha256 \
+ -nodes -keyout client.key -out client.csr \
+ -subj="/CN=User/O=example.com/emailAddress=user@example.com"
+
+ openssl x509 -req -in client.csr -days 3650 \
+ -CA example.com.ca.crt -CAkey example.com.ca.key -CAcreateserial \
+ -extfile openssl.config -extensions usr_cert \
+ -out client.cert
+
+ server_args="--cert=example.com.crt --key=example.com.key --mtls-cacert=example.com.ca.crt"
+else
+ server_args=
+ client_args=
+fi
+
+httpd oci-registry-server.py --dir=. $server_args
port=$(cat httpd-port)
-client="python3 $test_srcdir/oci-registry-client.py --url=http://127.0.0.1:$port"
+
+if [ x${USE_HTTPS} = xyes ] ; then
+ scheme=https
+ client_args="--cert=client.cert --key=client.key --cacert=example.com.ca.crt"
+
+ hostdir=$FLATPAK_SYSTEM_CERTS_D/127.0.0.1:${port}
+ mkdir -p $hostdir
+ cp example.com.ca.crt client.key client.cert $hostdir
+else
+ scheme=http
+ client_args=
+fi
+
+client="python3 $test_srcdir/oci-registry-client.py $client_args --url=${scheme}://127.0.0.1:${port}"
setup_repo_no_add oci
@@ -43,7 +107,7 @@ $client add hello latest $(pwd)/oci/app-image
# Add an OCI remote
-${FLATPAK} remote-add ${U} oci-registry "oci+http://127.0.0.1:${port}" >&2
+${FLATPAK} remote-add ${U} oci-registry "oci+${scheme}://127.0.0.1:${port}" >&2
# Check that the images we expect are listed
@@ -144,7 +208,7 @@ fi
assert_has_file $base/oci/oci-registry.index.gz
assert_has_file $base/oci/oci-registry.summary
assert_has_dir $base/appstream/oci-registry
-${FLATPAK} remote-modify ${U} --url=http://127.0.0.1:${port} oci-registry >&2
+${FLATPAK} remote-modify ${U} --url=${scheme}://127.0.0.1:${port} oci-registry >&2
assert_not_has_file $base/oci/oci-registry.index.gz
assert_not_has_file $base/oci/oci-registry.summary
assert_not_has_dir $base/appstream/oci-registry
@@ -153,7 +217,7 @@ ok "change remote to non-OCI"
# Change it back and refetch
-${FLATPAK} remote-modify ${U} --url=oci+http://127.0.0.1:${port} oci-registry >&2
+${FLATPAK} remote-modify ${U} --url=oci+${scheme}://127.0.0.1:${port} oci-registry >&2
${FLATPAK} update ${U} --appstream oci-registry >&2
# Delete the remote, check that everything was removed
@@ -177,7 +241,7 @@ ok "delete remote"
cat << EOF > runtime-repo.flatpakrepo
[Flatpak Repo]
Version=1
-Url=oci+http://localhost:${port}
+Url=oci+${scheme}://localhost:${port}
Title=The OCI Title
EOF
@@ -186,7 +250,7 @@ cat << EOF > org.test.Platform.flatpakref
Title=Test Platform
Name=org.test.Platform
Branch=master
-Url=oci+http://127.0.0.1:${port}
+Url=oci+${scheme}://127.0.0.1:${port}
IsRuntime=true
RuntimeRepo=file://$(pwd)/runtime-repo.flatpakrepo
EOF
@@ -214,12 +278,12 @@ ok "prune origin remote"
# Install from a (non-OCI) bundle, check that the repo-url is respected
-${FLATPAK} build-bundle --runtime --repo-url "oci+http://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Platform.flatpak org.test.Platform >&2
+${FLATPAK} build-bundle --runtime --repo-url "oci+${scheme}://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Platform.flatpak org.test.Platform >&2
${FLATPAK} ${U} install -y --bundle org.test.Platform.flatpak >&2
${FLATPAK} remotes -d > remotes-list
-assert_file_has_content remotes-list "^platform-origin.*[ ]oci+http://127\.0\.0\.1:${port}"
+assert_file_has_content remotes-list "^platform-origin.*[ ]oci+${scheme}://127\.0\.0\.1:${port}"
assert_has_file $base/oci/platform-origin.index.gz
@@ -227,12 +291,12 @@ ok "install via bundle"
# Install an app from a bundle
-${FLATPAK} build-bundle --repo-url "oci+http://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2
+${FLATPAK} build-bundle --repo-url "oci+${scheme}://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2
${FLATPAK} ${U} install -y --bundle org.test.Hello.flatpak >&2
${FLATPAK} remotes -d > remotes-list
-assert_file_has_content remotes-list "^hello-origin.*[ ]oci+http://127\.0\.0\.1:${port}"
+assert_file_has_content remotes-list "^hello-origin.*[ ]oci+${scheme}://127\.0\.0\.1:${port}"
assert_has_file $base/oci/hello-origin.index.gz
@@ -241,12 +305,12 @@ ok "app install via bundle"
# Install an updated app bundle with a different origin
make_updated_app oci
-${FLATPAK} build-bundle --repo-url "http://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2
+${FLATPAK} build-bundle --repo-url "${scheme}://127.0.0.1:${port}" $FL_GPGARGS repos/oci org.test.Hello.flatpak org.test.Hello >&2
${FLATPAK} ${U} install -y --bundle org.test.Hello.flatpak >&2
${FLATPAK} remotes -d > remotes-list
-assert_file_has_content remotes-list "^hello-origin.*[ ]http://127\.0\.0\.1:${port}"
+assert_file_has_content remotes-list "^hello-origin.*[ ]${scheme}://127\.0\.0\.1:${port}"
assert_not_has_file $base/oci/hello-origin.index.gz
diff --git a/tests/test-wrapper.sh b/tests/test-wrapper.sh
index be624256..2dacc1bc 100755
--- a/tests/test-wrapper.sh
+++ b/tests/test-wrapper.sh
@@ -30,6 +30,12 @@ for feature in $(echo $1 | sed "s/^.*@\(.*\).wrap/\1/" | tr "," "\n"); do
annotations)
export USE_OCI_ANNOTATIONS=yes
;;
+ https)
+ export USE_HTTPS=yes
+ ;;
+ http)
+ export USE_HTTPS=no
+ ;;
*)
echo unsupported test feature $feature
exit 1
diff --git a/tests/update-test-matrix b/tests/update-test-matrix
index 2aff6f00..3a51d0ba 100755
--- a/tests/update-test-matrix
+++ b/tests/update-test-matrix
@@ -23,7 +23,7 @@ TEST_MATRIX_SOURCE=(
'tests/test-extensions.sh' \
'tests/test-bundle.sh{user+system+system-norevokefs}' \
'tests/test-oci.sh' \
- 'tests/test-oci-registry.sh{user+system}' \
+ 'tests/test-oci-registry.sh{{user+system},{http+https}}' \
'tests/test-update-remote-configuration.sh{newsummary+oldsummary}' \
'tests/test-override.sh' \
'tests/test-update-portal.sh{user+system}' \
--
2.47.1