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: 6 additions & 2 deletions .github/workflows/decision-trajectories.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name: Private decision trajectories
on:
push:
branches: [master]
paths: [src/bitworld/decision_trajectory.nim, src/bitworld/runtime.nim, src/bitworld/native_http.nim, src/bitworld/native_stop.nim, src/bitworld/artifact_runtime.nim, tests/test_artifact_http.py, tests/support/artifact_http_probe.nim, tests/test_decision_trajectory.nim, tests/test_native_http.py, tests/support/native_http_probe.nim, .github/workflows/decision-trajectories.yml]
paths: [src/bitworld/decision_trajectory.nim, src/bitworld/runtime.nim, src/bitworld/native_http.nim, src/bitworld/native_stop.nim, src/bitworld/artifact_runtime.nim, tests/test_artifact_http.py, tests/support/artifact_http_probe.nim, tests/test_native_https.py, tests/support/native_https_probe.nim, tests/test_decision_trajectory.nim, tests/test_native_http.py, tests/support/native_http_probe.nim, .github/workflows/decision-trajectories.yml]
pull_request:
paths: [src/bitworld/decision_trajectory.nim, src/bitworld/runtime.nim, src/bitworld/native_http.nim, src/bitworld/native_stop.nim, src/bitworld/artifact_runtime.nim, tests/test_artifact_http.py, tests/support/artifact_http_probe.nim, tests/test_decision_trajectory.nim, tests/test_native_http.py, tests/support/native_http_probe.nim, .github/workflows/decision-trajectories.yml]
paths: [src/bitworld/decision_trajectory.nim, src/bitworld/runtime.nim, src/bitworld/native_http.nim, src/bitworld/native_stop.nim, src/bitworld/artifact_runtime.nim, tests/test_artifact_http.py, tests/support/artifact_http_probe.nim, tests/test_native_https.py, tests/support/native_https_probe.nim, tests/test_decision_trajectory.nim, tests/test_native_http.py, tests/support/native_http_probe.nim, .github/workflows/decision-trajectories.yml]

jobs:
test:
Expand Down Expand Up @@ -41,3 +41,7 @@ jobs:
run: |
nim c -d:release --threads:on --mm:orc --path:src -o:/tmp/artifact-http-probe tests/support/artifact_http_probe.nim
python3 tests/test_artifact_http.py /tmp/artifact-http-probe
- name: Verify configured HTTPS trust and hostname validation
run: |
nim c -d:release --threads:on --mm:orc --path:src -o:/tmp/native-https-probe tests/support/native_https_probe.nim
python3 tests/test_native_https.py /tmp/native-https-probe
4 changes: 3 additions & 1 deletion src/bitworld/native_http.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
## No provider parsing or game acceptance occurs here. All received bytes survive
## timeout/interruption; the handle is cleaned before the result can be sealed.

import std/[monotimes, options, posix, times]
import std/[monotimes, options, os, posix, times]
import libcurl except Option
import webby/httpheaders
import native_stop
Expand Down Expand Up @@ -99,6 +99,8 @@ proc performOwnedRequest(url: string, httpMethod: ArtifactHttpMethod,
requireCurl(handle.easy_setopt(OPT_FOLLOWLOCATION, clong(0)))
requireCurl(handle.easy_setopt(OptProtocols, clong(3))) # HTTP and HTTPS only.
requireCurl(handle.easy_setopt(OPT_NOSIGNAL, clong(1)))
if existsEnv("SSL_CERT_FILE"):
requireCurl(handle.easy_setopt(OPT_CAINFO, getEnv("SSL_CERT_FILE").cstring))
requireCurl(handle.easy_setopt(OPT_HEADERDATA, transfer.addr))
requireCurl(handle.easy_setopt(OPT_HEADERFUNCTION, receiveHeaders))
requireCurl(handle.easy_setopt(OPT_WRITEDATA, transfer.addr))
Expand Down
12 changes: 12 additions & 0 deletions tests/support/native_https_probe.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import std/[json, monotimes, options, os, times]
import bitworld/native_http

let args = commandLineParams()
let deadline = getMonoTime() + initDuration(seconds = 3)
let response = if args[1] == "inference":
performNativePost(args[0], @[], "private fixture", deadline)
else:
performArtifactUpload(args[0], ahPut, @[], "private fixture", deadline)
echo $(%*{"kind": $response.kind, "complete": response.transferComplete,
"status": (if response.httpStatus.isSome: %response.httpStatus.get() else: newJNull()),
"body": response.bodyBytes})
67 changes: 67 additions & 0 deletions tests/test_native_https.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Verify configured private CA trust without changing global certificate stores."""
import http.server
import json
import os
import ssl
import subprocess
import sys
import tempfile
import threading
from pathlib import Path

probe = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="bitworld-https-") as directory:
root = Path(directory)
subprocess.run(["openssl", "req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1",
"-nodes", "-days", "1", "-subj", "/CN=Owned fixture CA", "-keyout", str(root / "ca.key"),
"-out", str(root / "ca.pem")], check=True, capture_output=True)
subprocess.run(["openssl", "req", "-new", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1",
"-nodes", "-subj", "/CN=localhost", "-keyout", str(root / "server.key"),
"-out", str(root / "server.csr")], check=True, capture_output=True)
(root / "extensions").write_text("subjectAltName=DNS:localhost\n")
subprocess.run(["openssl", "x509", "-req", "-in", str(root / "server.csr"), "-CA", str(root / "ca.pem"),
"-CAkey", str(root / "ca.key"), "-CAcreateserial", "-days", "1", "-extfile", str(root / "extensions"),
"-out", str(root / "server.pem")], check=True, capture_output=True)
observed = []

class Fixture(http.server.BaseHTTPRequestHandler):
def upload(self):
observed.append((self.command, self.rfile.read(int(self.headers["Content-Length"]))))
self.send_response(200)
self.send_header("Content-Length", "2")
self.end_headers()
self.wfile.write(b"ok")

do_POST = upload
do_PUT = upload

def log_message(self, *_args):
pass

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(root / "server.pem", root / "server.key")
with http.server.ThreadingHTTPServer(("127.0.0.1", 0), Fixture) as server:
server.socket = context.wrap_socket(server.socket, server_side=True)
owner = threading.Thread(target=server.serve_forever)
owner.start()
try:
for transport in ("inference", "artifact"):
for trust in ("untrusted", "trusted", "wrong-host"):
env = {**os.environ, "NO_PROXY": "localhost,127.0.0.1"}
env.pop("SSL_CERT_FILE", None)
if trust != "untrusted": env["SSL_CERT_FILE"] = str(root / "ca.pem")
host = "127.0.0.1" if trust == "wrong-host" else "localhost"
process = subprocess.run([str(probe), f"https://{host}:{server.server_port}/", transport],
env=env, capture_output=True, text=True, timeout=4)
assert process.returncode == 0, process.stderr
result = json.loads(process.stdout)
if trust == "trusted":
assert result == {"kind": "nhComplete", "complete": True, "status": 200, "body": "ok"}
else:
assert result == {"kind": "nhTransportFailure", "complete": False, "status": None, "body": ""}
print(transport, trust, result["kind"], flush=True)
assert observed == [("POST", b"private fixture"), ("PUT", b"private fixture")]
finally:
server.shutdown()
owner.join(timeout=4)
assert not owner.is_alive()
Loading