mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 08:00:32 +02:00
Bundle a CA list so HTTPS works from Python on fresh Windows
The bundled Python only trusts roots already in the Windows certificate store, which Windows fills lazily, so on a new install Steam store search, F-Droid downloads and the compat DB failed with CERTIFICATE_VERIFY_FAILED. fetch-deps.js now also bundles curl's pinned copy of Mozilla's CA list, and the server adds it to the default HTTPS context on top of the system certificates (before any urlopen, since urllib keeps the context it first builds). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1a0e54d8bd
commit
770f26c703
5 files changed
+40
-8
No files matched your search
+10
-4
@@ -1,7 +1,7 @@
|
|||||||
// Downloads what the app bundles so users install nothing else: a standalone
|
// Downloads what the app bundles so users install nothing else: a standalone
|
||||||
// Python (python-build-standalone) and adb (Android platform-tools). Each goes in
|
// Python (python-build-standalone), adb (Android platform-tools) and a CA
|
||||||
// build/deps/<os>-<arch>/{python,tools}, which package.json copies into the app's
|
// bundle. Each goes in build/deps/<os>-<arch>/{python,tools}, which package.json
|
||||||
// resources. Everything is pinned by version and SHA-256.
|
// copies into the app's resources. Everything is pinned by version and SHA-256.
|
||||||
// node build/fetch-deps.js mac arm64 | win x64 | linux x64 arm64
|
// node build/fetch-deps.js mac arm64 | win x64 | linux x64 arm64
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
@@ -29,6 +29,11 @@ const TOOLS = {
|
|||||||
linux: ["linux", "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1", ["adb"]],
|
linux: ["linux", "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1", ["adb"]],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mozilla's CA list, as curl publishes it: Python on Windows only trusts roots
|
||||||
|
// already in the Windows store (see frame_host.trust_bundled_cas).
|
||||||
|
const CA = "2026-09-25";
|
||||||
|
const CA_SHA256 = "a41b5d356aea97a529fe27e0f7316d2f9d946d75927476cf9cf1b90637d00505";
|
||||||
|
|
||||||
// Parts of Python the server never imports (GUI, tests, packaging, headers).
|
// Parts of Python the server never imports (GUI, tests, packaging, headers).
|
||||||
const PRUNE = [
|
const PRUNE = [
|
||||||
"include", "share", "Scripts", "libs", "tcl", "lib/pkgconfig", "lib/itcl4", "lib/tcl8", "lib/tcl8.6",
|
"include", "share", "Scripts", "libs", "tcl", "lib/pkgconfig", "lib/itcl4", "lib/tcl8", "lib/tcl8.6",
|
||||||
@@ -79,7 +84,7 @@ async function fetch(os, arch) {
|
|||||||
if (!PYTHON[key]) throw new Error(`no bundle for ${key}`);
|
if (!PYTHON[key]) throw new Error(`no bundle for ${key}`);
|
||||||
const out = path.join(__dirname, "deps", key);
|
const out = path.join(__dirname, "deps", key);
|
||||||
const stamp = path.join(out, ".version");
|
const stamp = path.join(out, ".version");
|
||||||
const version = `python ${PY}, platform-tools ${PT}`;
|
const version = `python ${PY}, platform-tools ${PT}, CA ${CA}`;
|
||||||
if (fs.existsSync(stamp) && fs.readFileSync(stamp, "utf8") === version) {
|
if (fs.existsSync(stamp) && fs.readFileSync(stamp, "utf8") === version) {
|
||||||
console.log(`${key}: already fetched (${version})`);
|
console.log(`${key}: already fetched (${version})`);
|
||||||
return;
|
return;
|
||||||
@@ -107,6 +112,7 @@ async function fetch(os, arch) {
|
|||||||
if (os !== "win") fs.chmodSync(path.join(tools, "adb"), 0o755);
|
if (os !== "win") fs.chmodSync(path.join(tools, "adb"), 0o755);
|
||||||
fs.rmSync(tmp, { recursive: true, force: true });
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
await download(`https://curl.se/ca/cacert-${CA}.pem`, CA_SHA256, path.join(tools, "cacert.pem"));
|
||||||
fs.writeFileSync(stamp, version);
|
fs.writeFileSync(stamp, version);
|
||||||
console.log(`${key}: ${version} -> ${out}`);
|
console.log(`${key}: ${version} -> ${out}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ const IS_WIN = process.platform === "win32";
|
|||||||
|
|
||||||
// Packaged: <resources>/{ui,scripts,python}. Dev: the repo checkout.
|
// Packaged: <resources>/{ui,scripts,python}. Dev: the repo checkout.
|
||||||
const ROOT = app.isPackaged ? process.resourcesPath : path.join(__dirname, "..");
|
const ROOT = app.isPackaged ? process.resourcesPath : path.join(__dirname, "..");
|
||||||
const TOOLS = path.join(ROOT, "tools"); // bundled adb
|
const TOOLS = path.join(ROOT, "tools"); // bundled adb and CA certificates
|
||||||
const SERVER = path.join(ROOT, "ui", "server.py");
|
const SERVER = path.join(ROOT, "ui", "server.py");
|
||||||
const SCRIPTS = path.join(ROOT, "scripts");
|
const SCRIPTS = path.join(ROOT, "scripts");
|
||||||
const LOG_DIR = IS_MAC ? path.join(os.homedir(), "Library", "Logs", "Frame Control")
|
const LOG_DIR = IS_MAC ? path.join(os.homedir(), "Library", "Logs", "Frame Control")
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ and shows it in its own window; the server stops when you quit the app. The
|
|||||||
app bundles `ui/`, `scripts/`, `frame/android/` and the rated catalogue from
|
app bundles `ui/`, `scripts/`, `frame/android/` and the rated catalogue from
|
||||||
`apk-catalog/`, plus a standalone Python
|
`apk-catalog/`, plus a standalone Python
|
||||||
([python-build-standalone](https://github.com/astral-sh/python-build-standalone))
|
([python-build-standalone](https://github.com/astral-sh/python-build-standalone))
|
||||||
and `adb` from Google's platform-tools, so there's nothing else to install.
|
and `adb` from Google's platform-tools, so there's nothing else to install. It
|
||||||
|
also bundles curl's copy of Mozilla's CA list, because Python on Windows only
|
||||||
|
trusts root certificates already in the Windows store.
|
||||||
`app/build/fetch-deps.js` downloads both, pinned by SHA-256.
|
`app/build/fetch-deps.js` downloads both, pinned by SHA-256.
|
||||||
|
|
||||||
The server is Python stdlib only and listens on 127.0.0.1. It rejects requests
|
The server is Python stdlib only and listens on 127.0.0.1. It rejects requests
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ CLI (used by the Electron app, so terminal handling lives in one place):
|
|||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -108,6 +109,27 @@ def adb():
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def trust_bundled_cas():
|
||||||
|
"""Trust the app's CA bundle for HTTPS as well as the system's certificates.
|
||||||
|
|
||||||
|
Python on Windows only sees the root certificates already in the Windows
|
||||||
|
store, and a fresh install fetches those lazily, so Steam and F-Droid can
|
||||||
|
fail with CERTIFICATE_VERIFY_FAILED. The app bundles curl's copy of Mozilla's
|
||||||
|
CA list (app/build/fetch-deps.js); outside the app this does nothing. Call it
|
||||||
|
before the first urlopen: urllib keeps the HTTPS context it builds then.
|
||||||
|
"""
|
||||||
|
tools = os.environ.get("FRAME_CONTROL_TOOLS")
|
||||||
|
cafile = os.path.join(tools, "cacert.pem") if tools else None
|
||||||
|
if not cafile or not os.path.isfile(cafile):
|
||||||
|
return
|
||||||
|
|
||||||
|
def context(*args, **kwargs):
|
||||||
|
ctx = ssl.create_default_context(*args, **kwargs)
|
||||||
|
ctx.load_verify_locations(cafile)
|
||||||
|
return ctx
|
||||||
|
ssl._create_default_https_context = context # urllib's default for HTTPS
|
||||||
|
|
||||||
|
|
||||||
def open_path(path):
|
def open_path(path):
|
||||||
"""Show a folder or file in the file manager."""
|
"""Show a folder or file in the file manager."""
|
||||||
path = str(path)
|
path = str(path)
|
||||||
|
|||||||
+4
-2
@@ -28,8 +28,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, unquote, urlparse
|
||||||
|
|
||||||
# Windows' embedded Python (bundled with the app) doesn't put the script's own
|
# The app runs Python with -I, which leaves the script's own folder off
|
||||||
# folder on sys.path, so add it for the sibling modules below.
|
# sys.path, so add it for the sibling modules below.
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
|
||||||
import frame_android # noqa: E402
|
import frame_android # noqa: E402
|
||||||
@@ -37,6 +37,8 @@ import frame_catalog # noqa: E402
|
|||||||
import frame_host # noqa: E402
|
import frame_host # noqa: E402
|
||||||
import frame_store # noqa: E402
|
import frame_store # noqa: E402
|
||||||
|
|
||||||
|
frame_host.trust_bundled_cas()
|
||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
HERE = Path(__file__).resolve().parent
|
||||||
FRAME = os.environ.get("FRAME_ALIAS", "frame")
|
FRAME = os.environ.get("FRAME_ALIAS", "frame")
|
||||||
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", FRAME):
|
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", FRAME):
|
||||||
|
|||||||
Reference in new issue
Block a user