Eén app store, twee apps

umbrelOS leest per store één repo, dus twee apps in twee repo's kan niet. Deze
repo is de store en bevat vanaf nu Electrum Gate en het werk aan Evolu Relay.
Opgezet als verse repo op verzoek van de gebruiker: de historie van ElectrumTLS
en van EvoluRelay komt niet mee.

Dat heeft één gevolg dat verder gaat dan opruimen. In de historie van
ElectrumTLS staat het domein van de gebruiker en het certificaatpad, van vóór de
opschoning van 19-08. Die komt hier niet in. Zolang die repo op de Git-server
blijft staan verandert dat niets, dus het weghalen ervan is het laatste stuk van
open punt 3 van het plan Appstore, en geen bijzaak.

De store zelf hoefde niet te veranderen: store-id whatsnext, en dus blijft het
app-id whatsnext-electrum-gate. Dat hangt aan het store-id en niet aan de URL,
dus voor umbrelOS is dit dezelfde app in een andere store. Dat de store op 19-08
naar de maker genoemd werd in plaats van naar deze ene app, betaalt zich hier
uit.

Wat de documentatie betreft is dit één wortel voor beide apps, en dat was de
reden om samen te voegen en niet de prijs ervan: de appstore-spec, het pinnen
van images en de werkwijze golden al voor allebei en stonden in twee repo's naast
elkaar. De kruisverwijzing die daarvoor nodig was (Referenties/Umbrel-appstore.md
in de oude EvoluRelay-repo) is verdwenen; wat daarin stond over de plekken waar
de relay een ander geval is, staat nu als ontwerp in het masterplan Umbrelapp §4.

Botsende namen kregen een achtervoegsel met de app, en alleen die: Publicatie
werd Publicatie-Gate en Publicatie-Relay, CHANGELOG.md werd
CHANGELOG-electrum-gate.md. Proefopstelling kreeg 007, tussen de twee bestaande
nummers, zodat de bovenkant van de reeks op tier-orde blijft staan.
CONTINUE_HERE.md heeft een kolom App, maar de tiers lopen over beide apps heen:
er is één volgorde van werken.

Electrum Gate gaat naar 0.0.15, want website, repo, support, submission en icon
wijzen nu naar UmbrelApps en zonder versieverhoging rolt dat niet uit. De release
notes leggen aan de gebruiker uit dat hij de store opnieuw moet toevoegen. Of een
geïnstalleerde app een wisseling van store-URL overleeft is nog steeds niet
uitgezocht; dat blijkt bij het omzetten.

Twee dingen in de plannen van Electrum Gate waren door deze verhuizing niet meer
waar en zijn bijgewerkt: de taak "de repo hernoemen" in fase 7 is afgevinkt, en
de repo-vorm in PLAN.md §4a toonde nog de store-id electrumtls, die al sinds
fase 7 achterhaald was.

Tests: 39 goed 0 fout en 54 goed 0 fout, niets overgeslagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Harmen
2026-08-25 16:27:57 +02:00
co-authored by Claude Opus 5
commit 67ed9b603b
45 changed files with 8652 additions and 0 deletions
+625
View File
@@ -0,0 +1,625 @@
"""Toetst de certificaatlezer en de keuze-guard van de agent.
Waarom deze test bestaat: de agent leest een X.509-certificaat met een eigen
DER-lezer, omdat de standaardbibliotheek er geen heeft en de app bij het starten
niets bijinstalleert. Zo'n lezer faalt niet met een foutmelding maar met een
verkeerd antwoord, en een certificaatdatum die er een jaar naast zit valt nooit
op. Daarom wordt hij hier vergeleken met `ssl`, dat een onafhankelijke
implementatie is.
Draaien:
python tests/test_agent_certificates.py
De test laadt `agent.py.template` rechtstreeks. Dat kan omdat dat bestand geen
accolade-variabelen bevat en de invulling door umbreld hem dus onveranderd laat;
de eerste toets hieronder controleert precies dat.
"""
import sys
# Vóór de imports, want anders is het te laat: Python legt bytecode naast
# agent.py.template zodra die geïmporteerd wordt, en die rommel hoort niet in de
# app-map. Een keer is zo'n .pyc meegegaan in een commit.
sys.dont_write_bytecode = True
import importlib.machinery # noqa: E402
import importlib.util # noqa: E402
import os # noqa: E402
import shutil # noqa: E402
import socket # noqa: E402
import ssl # noqa: E402
import subprocess # noqa: E402
import tempfile # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__))
TEMPLATE = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate", "agent.py.template")
def load_agent():
"""Laadt agent.py.template als module.
Met een expliciete loader, want importlib kijkt normaal naar de extensie en
.template staat daar niet tussen.
"""
loader = importlib.machinery.SourceFileLoader("gate_agent", TEMPLATE)
spec = importlib.util.spec_from_file_location("gate_agent", TEMPLATE, loader=loader)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class Uitslag:
def __init__(self):
self.goed = 0
self.fout = []
def check(self, naam, gelukt, uitleg=""):
if gelukt:
self.goed += 1
else:
self.fout.append(naam + ((" - " + uitleg) if uitleg else ""))
def rapport(self):
print()
print("%d goed, %d fout" % (self.goed, len(self.fout)))
for f in self.fout:
print(" FOUT: " + f)
return 0 if not self.fout else 1
def der(tag, inhoud):
"""Eén DER-element, met de lengte in korte of lange vorm."""
if len(inhoud) < 0x80:
return bytes([tag, len(inhoud)]) + inhoud
lengte = len(inhoud).to_bytes((len(inhoud).bit_length() + 7) // 8, "big")
return bytes([tag, 0x80 | len(lengte)]) + lengte + inhoud
def nep_certificaat(tijdtag, tijdtekst):
"""Het kleinste certificaat waarin not_after zijn validity vindt."""
tijd = der(tijdtag, tijdtekst.encode("ascii"))
return der(0x30, der(0x30, der(0x30, tijd + tijd)))
def test_template_is_invulbaar_zonder_schade(u):
"""De aanname waar deze hele test op rust."""
with open(TEMPLATE, "r", encoding="utf-8") as f:
inhoud = f.read()
u.check("template bevat geen accolade-variabelen",
"${" not in inhoud,
"umbreld zou die invullen en de Python-code slopen")
def test_einddatum_gelijk_aan_ssl(agent, u):
"""De echte certificaten uit de certificaatwinkel van het besturingssysteem.
Dit is de belangrijkste toets, want `ssl` levert hier het antwoordblad. Zijn
er geen certificaten te vinden, dan wordt dit overgeslagen en gemeld, in
plaats van stil als geslaagd geteld.
"""
ctx = ssl.create_default_context()
geparseerd = ctx.get_ca_certs(binary_form=False)
ders = ctx.get_ca_certs(binary_form=True)
if not ders or len(ders) != len(geparseerd):
print("OVERGESLAGEN: geen CA-certificaten beschikbaar op dit systeem, "
"dus de vergelijking met ssl is niet gedaan")
return
afwijkingen = []
for i, d in enumerate(ders):
verwacht = int(ssl.cert_time_to_seconds(geparseerd[i]["notAfter"]))
try:
gekregen = agent.not_after(d)
except Exception as e: # noqa: BLE001
afwijkingen.append("cert %d wierp %r" % (i, e))
continue
if gekregen != verwacht:
afwijkingen.append("cert %d: verwacht %d, kreeg %d"
% (i, verwacht, gekregen))
u.check("einddatum van %d CA-certificaten gelijk aan ssl" % len(ders),
not afwijkingen, "; ".join(afwijkingen[:3]))
def test_beide_tijdvormen(agent, u):
"""UTCTime en GeneralizedTime, en beide takken van de eeuwregel.
Het CA-materiaal gebruikt uitsluitend UTCTime met jaren na 2000, dus zonder
deze zelfgebouwde certificaten blijven twee takken ongetoetst.
"""
gevallen = [
(0x18, "20700101000000Z", "Jan 1 00:00:00 2070 GMT"),
(0x18, "99991231235959Z", "Dec 31 23:59:59 9999 GMT"),
(0x17, "491231235959Z", "Dec 31 23:59:59 2049 GMT"),
(0x17, "500101000000Z", "Jan 1 00:00:00 1950 GMT"),
]
for tag, tekst, ssl_tekst in gevallen:
verwacht = int(ssl.cert_time_to_seconds(ssl_tekst))
gekregen = agent.not_after(nep_certificaat(tag, tekst))
u.check("tijd %s" % tekst, gekregen == verwacht,
"verwacht %d, kreeg %d" % (verwacht, gekregen))
def test_tijd_zonder_zulu_faalt_hard(agent, u):
"""Het niet-gelukkige pad: een tijd die DER niet toestaat.
Dit is met opzet een uitzondering en geen benadering. Een certificaatdatum die
er een uur naast zit, valt nooit op.
"""
slecht = nep_certificaat(0x18, "20700101000000")
try:
agent.not_after(slecht)
u.check("tijd zonder Z faalt", False, "er kwam een antwoord uit")
except ValueError:
u.check("tijd zonder Z faalt", True)
except Exception as e: # noqa: BLE001
u.check("tijd zonder Z faalt", False, "verkeerde uitzondering: %r" % e)
def test_certificaat_zonder_validity_faalt(agent, u):
"""Een certificaat waar niets in staat, mag geen datum opleveren."""
leeg = der(0x30, der(0x30, der(0x02, b"\x01")))
try:
agent.not_after(leeg)
u.check("geen validity faalt", False, "er kwam een antwoord uit")
except ValueError:
u.check("geen validity faalt", True)
def test_live_certificaat_met_san(agent, u):
"""Een echt servercertificaat, want CA-certificaten hebben geen dNSName.
Zonder netwerk wordt dit overgeslagen en blijft subjectAltName ongetoetst.
Dat is gemeld, niet verzwegen.
"""
try:
with ssl.create_default_context().wrap_socket(
socket.create_connection(("example.com", 443), timeout=6),
server_hostname="example.com",
) as s:
binair = s.getpeercert(binary_form=True)
info = s.getpeercert()
except Exception as e: # noqa: BLE001
print("OVERGESLAGEN: geen verbinding voor de SAN-toets (%s), "
"subjectAltName blijft dus ongetoetst" % e)
return
verwacht = sorted(v for k, v in info.get("subjectAltName", ()) if k == "DNS")
gekregen = sorted(agent.dns_names(binair))
u.check("subjectAltName gelijk aan ssl", gekregen == verwacht,
"verwacht %r, kreeg %r" % (verwacht[:3], gekregen[:3]))
u.check("einddatum van een levend certificaat gelijk aan ssl",
agent.not_after(binair) == int(ssl.cert_time_to_seconds(info["notAfter"])))
def test_keuze_negeert_onbekende_id(agent, u):
"""De guard. Dit is het pad waar een fout stil TLS zou uitschakelen.
Wijst de keuze naar iets wat niet bestaat, dan mag de app niet doen alsof.
Hij valt terug op wat er wel is, en meldt dat.
"""
with tempfile.TemporaryDirectory() as tmp:
agent.CONFIG_DIR = tmp
agent.SELECTED_FILE = os.path.join(tmp, "selected-cert")
nu = 2_000_000_000
certs = [
{"id": "zoraxy/kort", "source": "zoraxy", "name": "kort",
"cert": "/x", "key": "/y", "not_after": nu + 86400},
{"id": "zoraxy/lang", "source": "zoraxy", "name": "lang",
"cert": "/a", "key": "/b", "not_after": nu + 400 * 86400},
]
agent.write_selection("zoraxy/bestaat-niet")
gekozen, waarom = agent.choose(certs)
u.check("onbekende keuze wordt niet gehonoreerd",
gekozen is None, "koos %r" % (gekozen and gekozen["id"]))
u.check("en er wordt ook niet zomaar een ander gepakt",
"2 certificates found" in waarom, "waarom was %r" % waarom)
# De reden noemt het aantal en niet de namen. Op de Umbrel van de
# gebruiker zijn dat er veertien, en die opsomming maakte van de melding
# een muur tekst die zegt wat de keuzelijst eronder al toont.
u.check("de reden somt de kandidaten niet op",
"zoraxy/kort" not in waarom and "zoraxy/lang" not in waarom,
"waarom was %r" % waarom)
# Eén kandidaat is geen keuze, dus die mag wel automatisch.
gekozen, waarom = agent.choose(certs[:1])
u.check("bij precies één certificaat kiest de app zelf",
gekozen is not None and gekozen["id"] == "zoraxy/kort",
"koos %r" % (gekozen and gekozen["id"]))
agent.write_selection("zoraxy/kort")
gekozen, waarom = agent.choose(certs)
u.check("een bestaande keuze wint van de automaat",
gekozen["id"] == "zoraxy/kort", "koos %r" % gekozen["id"])
onleesbaar = [dict(certs[0], unreadable="stuk")]
gekozen, waarom = agent.choose(onleesbaar)
u.check("een onleesbaar certificaat wordt niet gekozen",
gekozen is None, "koos %r" % (gekozen and gekozen["id"]))
def test_keuze_zonder_geldig_certificaat(agent, u):
"""Alles verlopen: dan liever niets dan een verlopen certificaat opdringen."""
with tempfile.TemporaryDirectory() as tmp:
agent.CONFIG_DIR = tmp
agent.SELECTED_FILE = os.path.join(tmp, "selected-cert")
verlopen = [{"id": "own/oud", "source": "own", "name": "oud",
"cert": "/a", "key": "/b", "not_after": 1}]
gekozen, waarom = agent.choose(verlopen)
u.check("alles verlopen levert geen keuze op", gekozen is None)
u.check("en de reden wordt genoemd", "expired" in waarom,
"waarom was %r" % waarom)
gekozen, waarom = agent.choose([])
u.check("een lege lijst levert geen keuze op", gekozen is None)
def test_paren_vinden(agent, u):
"""De drie naamvormen van de bronnen, in een echte map."""
with tempfile.TemporaryDirectory() as tmp:
# Zoraxy-vorm
open(os.path.join(tmp, "a.example.org.pem"), "w").close()
open(os.path.join(tmp, "a.example.org.key"), "w").close()
# Een certificaat zonder sleutel hoort niet mee te komen
open(os.path.join(tmp, "b.example.org.pem"), "w").close()
# Nginx Proxy Manager-vorm
os.mkdir(os.path.join(tmp, "npm-7"))
open(os.path.join(tmp, "npm-7", "fullchain.pem"), "w").close()
open(os.path.join(tmp, "npm-7", "privkey.pem"), "w").close()
# Een map zonder sleutel hoort ook niet mee te komen
os.mkdir(os.path.join(tmp, "npm-8"))
open(os.path.join(tmp, "npm-8", "fullchain.pem"), "w").close()
namen = sorted(n for n, _, _ in agent._pairs_in(tmp))
u.check("alleen volledige paren", namen == ["a.example.org", "npm-7"],
"vond %r" % namen)
u.check("een map die niet bestaat levert een lege lijst",
agent._pairs_in(os.path.join(tmp, "weg")) == [])
def openssl_paar(map_, naam, cn, dagen=365, wachtwoord=None, san=True):
"""Een echt zelfondertekend paar. Geeft (certpad, keypad) of None.
Echt en niet zelfgebouwd, want de controle die hier getoetst wordt is die van
OpenSSL zelf: hoort deze sleutel bij dit certificaat. Een met de hand
geknutseld DER-certificaat heeft geen sleutel en bewijst daar dus niets over.
"""
if not shutil.which("openssl"):
return None
cert = os.path.join(map_, naam + ".crt")
key = os.path.join(map_, naam + ".key")
opdracht = [
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", key, "-out", cert, "-days", str(dagen),
# De schuine streep is in -subj het scheidingsteken tussen velden, dus een
# hostnaam die er zelf een bevat moet ontsnapt worden. Zonder dit sloeg de
# padtruc-toets zichzelf stil over, en dat is precies de toets die er moet
# zijn.
"-subj", "/CN=" + cn.replace("/", "\\/"),
]
if san:
opdracht += ["-addext", "subjectAltName=DNS:" + cn]
if wachtwoord:
opdracht += ["-passout", "pass:" + wachtwoord]
else:
opdracht += ["-nodes"]
klaar = subprocess.run(opdracht, capture_output=True)
if klaar.returncode != 0:
# De laatste regel, want openssl vult stderr met voortgangspuntjes en de
# echte reden staat onderaan.
regels = [r for r in klaar.stderr.decode("utf-8", "replace").splitlines()
if r.strip()]
print("OVERGESLAGEN: openssl gaf een fout bij %s (%s)"
% (naam, regels[-1][:160] if regels else "geen melding"))
return None
return cert, key
class Tijdreis:
"""Een tijdmodule die alleen over `time()` liegt.
Nodig om de vervaldatum-guard te toetsen: een certificaat dat al verlopen ís
kan openssl 3.1 niet maken, en de datum vooruitzetten kan wel.
"""
def __init__(self, echt, wanneer):
self._echt = echt
self._wanneer = wanneer
def time(self):
return self._wanneer
def __getattr__(self, naam):
return getattr(self._echt, naam)
def test_upload(agent, u):
"""Het uploadpad, en vooral wat het weigert.
Dit is het enige pad waarlangs iets van buiten bestanden neerzet, dus de
toetsen gaan over de niet-gelukkige gevallen. De duurste fout die hier
voorkomen moet worden is een sleutel die niet bij het certificaat hoort: die
levert een nginx op die niet meer herlaadt, en dat is dezelfde klasse storing
als waardoor 0.0.3 helemaal niet startte.
"""
with tempfile.TemporaryDirectory() as tmp:
bron = os.path.join(tmp, "bron")
doel = os.path.join(tmp, "own")
os.makedirs(bron)
agent.UPLOAD_DIR = doel
agent.SOURCES = [("Own folder", doel)]
# Uit zonder map. Eerst, want dit hoort niet stil door te vallen naar een
# pad dat toevallig bestaat.
agent.UPLOAD_DIR = ""
naam, fout = agent.accept_upload("x", "y")
u.check("zonder uploadmap weigert de agent",
naam is None and "switched off" in (fout or ""),
"gaf %r, %r" % (naam, fout))
agent.UPLOAD_DIR = doel
naam, fout = agent.accept_upload("hallo", "-----BEGIN PRIVATE KEY-----")
u.check("rommel in plaats van een certificaat wordt geweigerd",
naam is None and "does not contain a certificate" in (fout or ""),
"gaf %r, %r" % (naam, fout))
naam, fout = agent.accept_upload(
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----", "hallo")
u.check("rommel in plaats van een sleutel wordt geweigerd",
naam is None and "does not contain a private key" in (fout or ""),
"gaf %r, %r" % (naam, fout))
u.check("een weigering laat niets achter in de doelmap",
not os.path.isdir(doel) or os.listdir(doel) == [],
"er staat: %r" % (os.path.isdir(doel) and os.listdir(doel)))
paar = openssl_paar(bron, "goed", "gate.example.org")
if paar is None:
print("OVERGESLAGEN: geen openssl, dus het gelukkige pad en de "
"sleutelcontrole van deze toets blijven ongetoetst")
return
cert_tekst = open(paar[0], encoding="ascii").read()
key_tekst = open(paar[1], encoding="ascii").read()
# De belangrijkste toets van dit bestand na de datumvergelijking: een
# sleutel van een ánder paar hoort hier te stranden.
ander = openssl_paar(bron, "ander", "andere.example.org")
naam, fout = agent.accept_upload(cert_tekst,
open(ander[1], encoding="ascii").read())
u.check("een sleutel van een ander paar wordt geweigerd",
naam is None and "does not go with" in (fout or ""),
"gaf %r, %r" % (naam, fout))
u.check("en ook dan blijft de doelmap leeg",
not os.path.isdir(doel) or os.listdir(doel) == [],
"er staat: %r" % (os.path.isdir(doel) and os.listdir(doel)))
# Een versleutelde sleutel: nginx kan die niet openen zonder wachtwoord.
met_slot = openssl_paar(bron, "slot", "slot.example.org",
wachtwoord="geheim")
if met_slot:
naam, fout = agent.accept_upload(
open(met_slot[0], encoding="ascii").read(),
open(met_slot[1], encoding="ascii").read())
u.check("een versleutelde sleutel wordt geweigerd",
naam is None and "encrypted" in (fout or ""),
"gaf %r, %r" % (naam, fout))
# Verlopen. De klok gaat vooruit in plaats van het certificaat achteruit.
echt = agent.time
agent.time = Tijdreis(echt, echt.time() + 400 * 86400)
try:
naam, fout = agent.accept_upload(cert_tekst, key_tekst)
finally:
agent.time = echt
u.check("een verlopen certificaat wordt geweigerd",
naam is None and "expired on" in (fout or ""),
"gaf %r, %r" % (naam, fout))
# Een certificaat zonder bruikbare naam: er is dan niets om een wallet
# tegen te vergelijken, dus plaatsen heeft geen zin.
naamloos = openssl_paar(bron, "naamloos", "!!!", san=False)
if naamloos:
naam, fout = agent.accept_upload(
open(naamloos[0], encoding="ascii").read(),
open(naamloos[1], encoding="ascii").read())
u.check("een certificaat zonder hostnaam wordt geweigerd",
naam is None and "no host name" in (fout or ""),
"gaf %r, %r" % (naam, fout))
# En dan het gelukkige pad.
naam, fout = agent.accept_upload(cert_tekst, key_tekst)
u.check("een geldig paar wordt aangenomen",
naam == "gate.example.org" and fout is None,
"gaf %r, %r" % (naam, fout))
u.check("en staat als paar in de doelmap",
sorted(os.listdir(doel)) == ["gate.example.org.key",
"gate.example.org.pem"],
"er staat: %r" % sorted(os.listdir(doel)))
u.check("de scanner vindt het meteen",
[n for n, _, _ in agent._pairs_in(doel)] == ["gate.example.org"])
u.check("de id verwijst naar de bron uit de omgeving",
agent.upload_id(naam) == "Own folder/gate.example.org",
"gaf %r" % agent.upload_id(naam))
# Nog een keer hetzelfde: dat vervangt, en laat geen .tmp achter. Een
# vernieuwd certificaat is precies waarom dit pad bestaat.
naam, fout = agent.accept_upload(cert_tekst, key_tekst)
u.check("hetzelfde nog eens vervangt zonder klagen",
naam == "gate.example.org" and fout is None,
"gaf %r, %r" % (naam, fout))
u.check("en er blijft geen tijdelijk bestand liggen",
not [n for n in os.listdir(doel) if n.endswith(".tmp")],
"er staat: %r" % sorted(os.listdir(doel)))
# Een naam met padtrucs erin kan niet uit het doel ontsnappen, want de
# naam komt uit het certificaat en niet uit het verzoek.
stout = openssl_paar(bron, "stout", "../../etc/nginx/evil", san=False)
if stout:
naam, fout = agent.accept_upload(
open(stout[0], encoding="ascii").read(),
open(stout[1], encoding="ascii").read())
u.check("een hostnaam met padtekens levert een platte naam op",
fout is None and "/" not in (naam or "/") and ".." not in (naam or ".."),
"gaf %r, %r" % (naam, fout))
u.check("en het bestand staat in de doelmap zelf",
fout is not None or os.path.isfile(
os.path.join(doel, naam + ".pem")))
def test_sessieregels_indelen(agent, u):
"""De drie soorten sessieregels uit de nginx-log.
Het onderscheid tussen `probe` en `refused` is het punt: een doorgestuurde
poort wordt gescand, en zonder dat onderscheid gaf elke scan een rode regel
"refused" terwijl er niets geweigerd is. De grens ligt bij de bytes en niet bij
de duur of de status, en dat is precies wat hier vastligt.
"""
with tempfile.TemporaryDirectory() as tmp:
agent.STREAM_LOG = os.path.join(tmp, "stream.log")
with open(agent.STREAM_LOG, "w", encoding="utf-8") as f:
# Een echte sessie, een scan, en een verbinding die wél iets
# doorgaf en daarna omviel.
f.write("2026-08-20T12:39:29+02:00 200 95300 218000 240.5\n")
f.write("2026-08-20T12:38:29+02:00 500 0 0 1.001\n")
f.write("2026-08-20T12:40:29+02:00 502 120 0 0.5\n")
# Een scan die lang open bleef staan: nog steeds een scan.
f.write("2026-08-20T12:41:29+02:00 500 0 0 45.0\n")
f.write("rommel die geen sessieregel is\n")
regels = agent.read_stream_log(0)
soorten = [r["event"] for r in regels]
u.check("de drie soorten komen er in de juiste orde uit",
soorten == ["disconnect", "probe", "refused", "probe"],
"gaf %r" % soorten)
u.check("een onleesbare regel wordt overgeslagen", len(regels) == 4,
"gaf %d regels" % len(regels))
probe = regels[1]
u.check("de scan houdt zijn status in de notitie",
probe.get("note") == "nothing exchanged, status 500",
"gaf %r" % probe.get("note"))
u.check("en zijn bytes staan op nul",
probe["bytes_in"] == 0 and probe["bytes_out"] == 0)
stuk = regels[2]
u.check("een sessie die wél verkeer had heet refused",
stuk.get("note") == "session ended with status 502",
"gaf %r" % stuk.get("note"))
echt = regels[0]
# 240 en niet 241: round() in Python rondt een halve naar even af.
u.check("een geslaagde sessie heeft geen notitie nodig",
"note" not in echt and echt["seconds"] == 240,
"gaf %r" % echt)
def test_open_verbindingen(agent, u):
"""De verbindingsteller, en de logregel die eruit volgt.
Dit gaat via `build_status`, dus via de ronde die de agent echt draait, en
niet via een interne helper. Twee dingen moeten kloppen die makkelijk stil
fout gaan: een teller die er niet is mag geen nul worden, en een
connect-regel moet de volgende ronde overleven. Die laatste zat er eerst
naast: `build_status` neemt de eigen gebeurtenissen van de vorige ronde over
aan de hand van een opsomming van soorten, en `connect` stond er niet in.
Een regel die na een minuut weer verdwijnt is nutteloos.
"""
with tempfile.TemporaryDirectory() as tmp:
agent.SOURCES = []
agent.ELECTRUM_HOST = "" # dan faalt de probe meteen, zonder netwerk
agent.STATE_DIR = tmp
agent.CONFIG_DIR = os.path.join(tmp, "config")
agent.SELECTED_FILE = os.path.join(agent.CONFIG_DIR, "selected-cert")
agent.STATUS_FILE = os.path.join(tmp, "status.json")
agent.CERT_CONF = os.path.join(tmp, "cert.conf")
agent.RELOAD_FLAG = os.path.join(tmp, "reload")
agent.STREAM_LOG = os.path.join(tmp, "stream.log")
agent.SESSIONS_FILE = os.path.join(tmp, "sessions")
def ronde():
status = agent.build_status([])
agent.write_status(status)
return status
# Geen tellerbestand: dan hoort er niets in de status te staan. Nul zou
# hier "geen wallet verbonden" beweren op grond van niets.
status = ronde()
u.check("zonder teller geen bewering over verbindingen",
"open_connections" not in status["tls"],
"stond er: %r" % status["tls"])
def zet_teller(tekst):
with open(agent.SESSIONS_FILE, "w", encoding="utf-8") as f:
f.write(tekst)
zet_teller("0\n")
status = ronde()
u.check("een teller van nul komt wel in de status",
status["tls"].get("open_connections") == 0)
u.check("nul verbindingen levert geen connect-regel op",
not [e for e in status["log"] if e.get("event") == "connect"])
zet_teller("1\n")
status = ronde()
connects = [e for e in status["log"] if e.get("event") == "connect"]
u.check("een verbinding erbij levert één connect-regel op",
len(connects) == 1, "regels: %r" % connects)
u.check("en die regel noemt hoeveel er nu openstaan",
connects and connects[0].get("note") == "1 open now",
"regel: %r" % (connects[0] if connects else None))
# Zelfde aantal, dus geen nieuwe regel. En de vorige moet blijven staan:
# dit is de toets die de vergeten opsomming vond.
status = ronde()
connects = [e for e in status["log"] if e.get("event") == "connect"]
u.check("een gelijk aantal levert geen tweede regel op",
len(connects) == 1, "regels: %r" % connects)
zet_teller("rommel")
status = ronde()
u.check("een onleesbare teller wordt niets, niet nul",
"open_connections" not in status["tls"],
"stond er: %r" % status["tls"])
u.check("en de eerdere connect-regel staat er nog",
len([e for e in status["log"] if e.get("event") == "connect"]) == 1)
# Van onbekend naar een aantal hoort ook een regel op te leveren. Dit is
# het geval "de wallet hing er al voordat de agent begon", en dat is
# precies wanneer iemand op de pagina komt kijken. Zonder deze regel blijft
# het log dan uren stil.
zet_teller("2\n")
status = ronde()
connects = [e for e in status["log"] if e.get("event") == "connect"]
u.check("na een onbekend aantal levert een verbinding alsnog een regel op",
len(connects) == 2, "regels: %r" % connects)
u.check("en die noemt het aantal dat er nu staat",
connects[-1].get("note") == "2 open now",
"regel: %r" % connects[-1])
def main():
u = Uitslag()
test_template_is_invulbaar_zonder_schade(u)
agent = load_agent()
test_einddatum_gelijk_aan_ssl(agent, u)
test_beide_tijdvormen(agent, u)
test_tijd_zonder_zulu_faalt_hard(agent, u)
test_certificaat_zonder_validity_faalt(agent, u)
test_live_certificaat_met_san(agent, u)
test_keuze_negeert_onbekende_id(agent, u)
test_keuze_zonder_geldig_certificaat(agent, u)
test_paren_vinden(agent, u)
# Deze twee als laatste, want ze verzetten de module-instellingen naar een
# tijdelijke map.
test_upload(agent, u)
test_sessieregels_indelen(agent, u)
test_open_verbindingen(agent, u)
return u.rapport()
if __name__ == "__main__":
sys.exit(main())