#!/usr/bin/env python3 # Dewey row 74 mutants: single-site edits in the mutant worktree (never the build tree), # each run against the preview and serve tests, then restored from the index. Round 2 runs in # mut2 (17ea0dbc plus the round 2 patch) and adds the repeated-Host, proxy-Host, HEAD and HEADER # mutants after the round 1 list. import os, subprocess, sys R = os.path.expanduser('~/dewey-scratch/r74') T = R + '/mut2' OUT = R + '/out2' SERVE = 'packages/webui/src/serve.mjs' CLI = 'packages/webui/src/cli.mjs' TESTS = ['node', '--test', 'packages/webui/tests/preview.test.mjs', 'packages/webui/tests/serve.test.mjs'] MUTANTS = [ ('partial-allowed', SERVE, "if (given.length !== 3) throw", "if (given.length < 1) throw", TESTS), ('host-not-lowercased', SERVE, "const host = String(publicHost).toLowerCase(), header", "const host = String(publicHost), header", TESTS), ('header-not-lowercased', SERVE, "header = String(authHeader).toLowerCase();", "header = String(authHeader);", TESTS), ('ip-allowed', SERVE, "if (!HOSTNAME.test(host) || isIP(host)) throw", "if (!HOSTNAME.test(host)) throw", TESTS), ('host-header-allowed', SERVE, "['host', 'origin'].includes(header)", "['origin'].includes(header)", TESTS), ('user-comma-allowed', SERVE, "const USER = /^[\\x21-\\x2b\\x2d-\\x7e]{1,256}$/;", "const USER = /^[\\x21-\\x7e]{1,256}$/;", TESTS), ('empty-object-serves', SERVE, "if (preview !== null && !(preview = previewOptions(", "if (preview !== null && false && !(preview = previewOptions(", TESTS), ('named-case-sensitive', SERVE, "(req.headers.host ?? '').toLowerCase() === preview.host", "(req.headers.host ?? '') === preview.host", TESTS), ('named-prefix', SERVE, "(req.headers.host ?? '').toLowerCase() === preview.host", "(req.headers.host ?? '').toLowerCase().startsWith(preview.host)", TESTS), ('named-hostname-only', SERVE, "(req.headers.host ?? '').toLowerCase() === preview.host", "(req.headers.host ?? '').toLowerCase().split(':')[0] === preview.host", TESTS), ('origin-http-allowed', SERVE, "(named ? `https://${preview.host}` : `http://${req.headers.host}`)", "(named ? `http://${preview.host}` : `http://${req.headers.host}`)", TESTS), ('origin-public-on-loopback', SERVE, "req.headers.origin !== (named ? `https://${preview.host}`", "req.headers.origin !== `https://${preview?.host}` && req.headers.origin !== (named ? `https://${preview.host}`", TESTS), ('header-check-skipped', SERVE, " if (named) {\n // One value", " if (false) {\n // One value", TESTS), ('two-values-allowed', SERVE, "values.length !== 1 || values[0] !== preview.user", "!values.includes(preview.user)", TESTS), ('value-prefix', SERVE, "values[0] !== preview.user", "!values[0].startsWith(preview.user)", TESTS), ('value-case-insensitive', SERVE, "values[0] !== preview.user", "values[0].toLowerCase() !== preview.user.toLowerCase()", TESTS), ('header-check-raw-name', SERVE, "const values = req.headersDistinct[preview.header];", "const values = req.headersDistinct[preview.header.toUpperCase()];", TESTS), ('readonly-named-only', SERVE, "if (preview !== null && req.method !== 'GET' && req.method !== 'HEAD') return", "if (named && req.method !== 'GET' && req.method !== 'HEAD') return", TESTS), ('readonly-post-only', SERVE, "if (preview !== null && req.method !== 'GET' && req.method !== 'HEAD') return", "if (preview !== null && req.method === 'POST') return", TESTS), ('readonly-always', SERVE, "if (preview !== null && req.method !== 'GET' && req.method !== 'HEAD') return", "if (req.method !== 'GET' && req.method !== 'HEAD') return", TESTS), ('cli-preview-dropped', CLI, "bus, reads, preview: options.preview });", "bus, reads });", TESTS), ('cli-checked-late', CLI, " options.preview = previewOptions(preview);\n", "", TESTS), # Round 2. ('repeated-host-allowed', SERVE, "(req.headersDistinct.host?.length ?? 0) > 1", "(req.headersDistinct.host?.length ?? 0) > 2", TESTS), ('repeated-host-preview-only', SERVE, "if ((req.headersDistinct.host?.length ?? 0) > 1)", "if (preview !== null && (req.headersDistinct.host?.length ?? 0) > 1)", TESTS), ('proxy-host-skipped', SERVE, "if (preview !== null && !named && req.headersDistinct[preview.header])", "if (false && preview !== null && !named && req.headersDistinct[preview.header])", TESTS), ('proxy-host-wrong-user-only', SERVE, "!named && req.headersDistinct[preview.header])", "!named && req.headersDistinct[preview.header]?.some(v => v !== preview.user))", TESTS), ('proxy-host-nonempty-only', SERVE, "!named && req.headersDistinct[preview.header])", "!named && req.headersDistinct[preview.header]?.[0])", TESTS), ('proxy-host-get-only', SERVE, "if (preview !== null && !named && req.headersDistinct", "if (preview !== null && !named && req.method === 'GET' && req.headersDistinct", TESTS), ('proxy-host-loopback-only', SERVE, "if (preview !== null && !named && req.headersDistinct", "if (preview !== null && !named && /^(127\\.|localhost|\\[)/.test(req.headers.host ?? '') && req.headersDistinct", TESTS), ('proxy-host-raw-name', SERVE, "!named && req.headersDistinct[preview.header])", "!named && req.headersDistinct[preview.header.toUpperCase()])", TESTS), ('header-check-skips-head', SERVE, " if (named) {\n // One value", " if (named && req.method !== 'HEAD') {\n // One value", TESTS), ('head-refused', SERVE, "req.method !== 'GET' && req.method !== 'HEAD') return json(res, 403, { error: 'read-only'", "req.method !== 'GET') return json(res, 403, { error: 'read-only'", TESTS), ('header-leading-dash', SERVE, "const HEADER = /^[a-z0-9][a-z0-9-]{0,63}$/;", "const HEADER = /^[a-z0-9-]{1,64}$/;", TESTS), ('header-underscore', SERVE, "const HEADER = /^[a-z0-9][a-z0-9-]{0,63}$/;", "const HEADER = /^[a-z0-9_][a-z0-9_-]{0,63}$/;", TESTS), ] def run(cmd): try: p = subprocess.run(cmd, cwd=T, capture_output=True, text=True, timeout=120, env={**os.environ, 'TMPDIR': R + '/tmp'}) except subprocess.TimeoutExpired as e: return 'timeout', str(e.stdout or '') return p.returncode, p.stdout + p.stderr want = sys.argv[1:] results = [] for name, f, old, new, tests in MUTANTS: if want and name not in want: continue path = os.path.join(T, f) src = open(path).read() if src.count(old) != 1: results.append(f'{name} BAD-SITE ({src.count(old)} matches)') print(results[-1], flush=True) continue open(path, 'w').write(src.replace(old, new)) try: rc, out = run(tests) finally: subprocess.run(['git', 'checkout', '--', f], cwd=T, check=True) open(f'{OUT}/mut-{name}.txt', 'w').write(out) results.append(f"{name} {'killed (timeout)' if rc == 'timeout' else 'killed' if rc else 'SURVIVED'}") print(results[-1], flush=True) rc, diff = run(['git', 'diff', '--stat']) print('worktree diff after:', diff.strip() or 'clean') killed = sum(1 for r in results if ' killed' in r) print(f'summary {killed} of {len(results)} killed')