Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dbuild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ class AppTestConfig:
https: bool = field(default=False, metadata={
"desc": "Use HTTPS for health and screenshot checks",
})
secure_context: bool = field(default=False, metadata={
"desc": "Open the screenshot page as a secure context over plain http. "
"For apps that need SharedArrayBuffer or other secure-only browser "
"APIs (sqlite in the browser, e.g. Actual); the container's "
"`http://<ip>:<port>` is not one otherwise.",
})
compose: bool = field(default=False, metadata={
"desc": "Start the service via `compose.yaml` instead of `podman run`",
})
Expand Down Expand Up @@ -779,6 +785,7 @@ def _parse_test_config(data: dict[str, Any], compose_data: dict[str, Any] | None
ssim_threshold = cit.get("ssim_threshold")
edge_threshold = cit.get("edge_threshold")
https = cit.get("https", False)
secure_context = cit.get("secure_context", False)
compose = cit.get("compose", False)
puid = cit.get("puid", True)
puid_ignore = cit.get("puid_ignore") or []
Expand Down Expand Up @@ -846,6 +853,7 @@ def normalize_anno(a: str) -> str:
ssim_threshold=ssim_threshold,
edge_threshold=edge_threshold,
https=https,
secure_context=secure_context,
compose=compose,
puid=puid,
puid_ignore=puid_ignore,
Expand Down
22 changes: 21 additions & 1 deletion dbuild/screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import sys
import time
from urllib.parse import urlsplit

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
Expand All @@ -22,7 +23,16 @@
WINDOW_SIZE = os.environ.get("SCREENSHOT_SIZE", "1920,1080")


def capture(url: str, output: str, timeout: int = 30, min_wait: int = 0) -> bool:
def secure_origin(url: str) -> str:
"""The origin to treat as secure for url: scheme://host:port, or "" for
one that already is (https) or that cannot be parsed."""
u = urlsplit(url)
if u.scheme != "http" or not u.netloc:
return ""
return f"{u.scheme}://{u.netloc}"


def capture(url: str, output: str, timeout: int = 30, min_wait: int = 0, secure_context: bool = False) -> bool:
"""Capture a screenshot of *url* and save to *output*.

Waits for ``document.readyState == "complete"`` then monitors for UI
Expand All @@ -38,6 +48,8 @@ def capture(url: str, output: str, timeout: int = 30, min_wait: int = 0) -> bool
Selenium page-load timeout in seconds.
min_wait:
Minimum seconds to wait before declaring stable.
secure_context:
Treat the page's plain-http origin as secure (cit.secure_context).

Returns
-------
Expand All @@ -51,6 +63,14 @@ def capture(url: str, output: str, timeout: int = 30, min_wait: int = 0) -> bool
options.add_argument("--disable-extensions")
options.add_argument(f"--window-size={WINDOW_SIZE}")
options.set_capability("acceptInsecureCerts", True)
# The container is reached at its own IP over plain http, which is not a
# secure context: the browser withholds SharedArrayBuffer, and apps that
# run sqlite in the browser (Actual) stop with a fatal error instead of
# their UI. An image that says so (cit.secure_context) gets just this
# origin treated as secure.
origin = secure_origin(url) if secure_context else ""
if origin:
options.add_argument(f"--unsafely-treat-insecure-origin-as-secure={origin}")
if CHROME_BIN:
options.binary_location = CHROME_BIN

Expand Down
6 changes: 4 additions & 2 deletions dbuild/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ def _test_screenshot(
port: int,
*,
https: bool = False,
secure_context: bool = False,
screenshot_path: str | None = None,
screenshot_wait: int = 0,
baseline: Path | None = None,
Expand Down Expand Up @@ -444,7 +445,7 @@ def _test_screenshot(
screenshot_file = tmp.name

try:
if not capture(url, screenshot_file, timeout=30, min_wait=screenshot_wait):
if not capture(url, screenshot_file, timeout=30, min_wait=screenshot_wait, secure_context=secure_context):
return False, "Screenshot capture failed", {}

# Basic verification
Expand Down Expand Up @@ -636,6 +637,7 @@ def _functional_checks(
ip,
port,
https=https,
secure_context=test.secure_context,
screenshot_path=test.screenshot_path,
screenshot_wait=test.screenshot_wait or 0,
baseline=baseline,
Expand Down Expand Up @@ -1045,7 +1047,7 @@ def run_screenshot(cfg: Config, args: argparse.Namespace) -> int:
url = f"{scheme}://{ip}:{port}{screenshot_path}"

log.info(f"Capturing: {url}")
if not capture(url, output, timeout=30, min_wait=screenshot_wait):
if not capture(url, output, timeout=30, min_wait=screenshot_wait, secure_context=test.secure_context):
log.error("Screenshot capture failed")
return 1

Expand Down
2 changes: 1 addition & 1 deletion docs/dbuild.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH DBUILD 1 "2026-10-02" "dbuild 1.10.1" "User Commands"
.TH DBUILD 1 "2026-10-03" "dbuild 1.10.1" "User Commands"
.SH NAME
dbuild \- FreeBSD OCI container image build tool
.SH SYNOPSIS
Expand Down
26 changes: 26 additions & 0 deletions tests/test_screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""The screenshot browser treats the container's plain-http origin as secure."""

import pytest

pytest.importorskip("selenium")

from dbuild.screenshot import secure_origin


def test_secure_origin_for_the_container_address():
# Actual needs SharedArrayBuffer, which the browser only gives a secure
# context; http://<container ip> is not one.
assert secure_origin("http://10.88.0.26:5006/") == "http://10.88.0.26:5006"
assert secure_origin("http://10.88.0.26:5006/some/path?x=1") == "http://10.88.0.26:5006"


def test_secure_origin_leaves_https_and_garbage_alone():
assert secure_origin("https://10.88.0.26:8443/") == ""
assert secure_origin("not a url") == ""


def test_secure_context_read_from_config():
from dbuild.config import _parse_test_config

assert _parse_test_config({"cit": {"port": 5006, "secure_context": True}}).secure_context is True
assert _parse_test_config({"cit": {"port": 5006}}).secure_context is False
Loading