diff --git a/.docker/etc/nginx/http.d/default.conf b/.docker/etc/nginx/http.d/default.conf index 5c0acb1..9b1d49d 100644 --- a/.docker/etc/nginx/http.d/default.conf +++ b/.docker/etc/nginx/http.d/default.conf @@ -38,4 +38,7 @@ server { location ~ /\.ht { deny all; } -} \ No newline at end of file + + access_log off; + error_log /dev/null crit; +} diff --git a/.docker/etc/supervisor/supervisord.conf b/.docker/etc/supervisor/supervisord.conf index 018e54b..8919bd4 100644 --- a/.docker/etc/supervisor/supervisord.conf +++ b/.docker/etc/supervisor/supervisord.conf @@ -17,6 +17,7 @@ directory=/www command=sh -c "chown -R www:www /www && chmod -R 775 /www" autostart=true autorestart=false +priority=1 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr @@ -25,6 +26,7 @@ stderr_logfile_maxbytes=0 [program:nginx] command=nginx -g 'daemon off;' user=root +priority=5 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr @@ -36,6 +38,7 @@ startretries=10 [program:cron] command=crond -f -l 8 user=root +priority=4 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr @@ -62,6 +65,8 @@ command=php -c php.ini webman.php start directory=/www user=www numprocs=1 +priority=2 +startsecs=3 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr @@ -74,6 +79,7 @@ startretries=10 command=php artisan horizon directory=/www user=www +priority=3 stdout_logfile=/www/storage/logs/queue.log stdout_logfile_maxbytes=0 stderr_logfile=/www/storage/logs/queue_error.log diff --git a/.docker/services/redis/redis.conf b/.docker/services/redis/redis.conf index c7115b6..b067b4c 100644 --- a/.docker/services/redis/redis.conf +++ b/.docker/services/redis/redis.conf @@ -1,3 +1,7 @@ unixsocket /run/redis-socket/redis.sock unixsocketperm 777 -port 0 \ No newline at end of file +port 0 + +save 900 1 +save 300 10 +save 60 10000 diff --git a/.env.example b/.env.example old mode 100755 new mode 100644 index 2bffe56..93f2d35 --- a/.env.example +++ b/.env.example @@ -40,4 +40,4 @@ GOOGLE_CLOUD_KEY_FILE=config/googleCloudStorageKey.json GOOGLE_CLOUD_STORAGE_BUCKET= # 用于阻止重复安装 -INSTALLED=false \ No newline at end of file +INSTALLED=false diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6fe489e..f6c2443 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,88 +1,94 @@ -name: Docker - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. +name: Docker Build and Publish on: push: - branches: [ "dev" ] - # Publish semver tags as releases. - tags: [ 'v*.*.*' ] + branches: ["legacy", "dev"] + workflow_dispatch: env: - # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io - # github.repository as / IMAGE_NAME: ${{ github.repository }} - jobs: build: - runs-on: ubuntu-latest permissions: contents: read packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. id-token: write steps: - - name: Checkout repository - uses: actions/checkout@v3 - - uses: satackey/action-docker-layer-caching@v0.0.11 - continue-on-error: true + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 + with: + platforms: 'arm64,amd64' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + driver-opts: | + image=moby/buildkit:latest + + - name: Login to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,format=long + type=raw,value=new + + - name: Get version + id: get_version + run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT + + - name: Update version in app.php + run: | + VERSION=$(date '+%Y%m%d')-$(git rev-parse --short HEAD) + sed -i "s/'version' => '.*'/'version' => '$VERSION'/g" config/app.php + echo "Updated version to: $VERSION" + + - name: Build and push + id: build-and-push + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:legacy + ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:dev + ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard + ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:latest + ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:${{ steps.get_version.outputs.version }} + build-args: | + BUILDKIT_INLINE_CACHE=1 + provenance: false - name: Install cosign uses: sigstore/cosign-installer@v3.4.0 with: cosign-release: 'v2.2.2' - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.2.0 - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@v3.1.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v5.5.1 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Get version - id: get_version - run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT - - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v5.3.0 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:latest,${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard,${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:${{ steps.get_version.outputs.version }} - # Sign the resulting Docker image digest except on PRs. - # This will only write to the public Rekor transparency log when the Docker - # repository is public to avoid leaking data. If you would like to publish - # transparency data even for private images, pass --force to cosign below. - # https://github.com/sigstore/cosign - - name: Sign the published Docker image + - name: Sign image + if: steps.build-and-push.outputs.digest != '' env: - # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} + COSIGN_EXPERIMENTAL: 1 + run: | + echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes "{}@${{ steps.build-and-push.outputs.digest }}" diff --git a/Dockerfile b/Dockerfile index 3ce274a..f0102c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,8 @@ COPY .docker / COPY . /www RUN composer install --optimize-autoloader --no-cache --no-dev \ && php artisan storage:link \ +&& cp /www/.env.example /www/.env \ && chown -R www:www /www \ && chmod -R 775 /www -CMD /usr/bin/supervisord --nodaemon -c /etc/supervisor/supervisord.conf \ No newline at end of file +CMD ["/usr/bin/supervisord", "--nodaemon", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/README.md b/README.md index 740050d..5561913 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,27 @@ -# 关于Xboard -Xboard是基于V2board二次开发,在性能上和功能上都有大部分增强的**面板 +# About Xboard +Xboard is a panel based on V2board's secondary development, with significant enhancements in both performance and functionality. -# 免责声明 -本项目只是本人个人学习开发并维护,本人不保证任何可用性,也不对使用本软件造成的任何后果负责。 +# Disclaimer +This project is personally developed and maintained by me for learning purposes. I do not guarantee any availability and am not responsible for any consequences resulting from the use of this software. -# Xboard 特点 -基于V2board 二次开发,增加了以下特性 -- 升级Laravel10 -- 适配Laravels (提升至10+倍并发) -- 适配Webman (比laravels快50%左右) -- 修改配置从数据库中获取 -- 支持Docker部署、分布式部署 -- 支持根据用户IP归属地来下发订阅 -- 增加Hy2支持 -- 增加sing-box下发 -- 支持直接从cloudflare获取访问者真实IP -- 支持根据客户端版本自动下发新协议 -- 支持线路筛选(订阅地址后面增加 &filter=香港|美国) -- 支持Sqlite安装(代替Mysql,自用用户福音) -- 使用Vue3 + TypeScript + NaiveUI + Unocss + Pinia重构用户前端 -- 修复大量BUG +# Xboard Features +Based on V2board's secondary development, with the following added features: +- Upgraded to Laravel 10 +- Adapted to Laravels (10+ times concurrent improvement) +- Adapted to Webman (about 50% faster than laravels) +- Modified configuration retrieval from database +- Support for Docker deployment and distributed deployment +- Support for subscription distribution based on user IP location +- Added Hy2 support +- Added sing-box distribution +- Support for obtaining real visitor IP directly from Cloudflare +- Support for automatic new protocol distribution based on client version +- Support for route filtering (add &filter=HongKong|USA after subscription URL) +- Support for Sqlite installation (alternative to MySQL, great for personal use) +- User frontend rebuilt using Vue3 + TypeScript + NaiveUI + Unocss + Pinia +- Fixed numerous bugs -# **系统架构** +# **System Architecture** - PHP8.1+ - Composer @@ -29,36 +29,36 @@ Xboard是基于V2board二次开发,在性能上和功能上都有大部分增 - Redis - Laravel -## 性能对比 [查看详情](./docs/性能对比.md) -> xboard 无论前端还是后端性能都有巨大的提升 +## Performance Comparison [View Details](./docs/性能对比.md) +> xboard shows tremendous performance improvements in both frontend and backend -|场景 | php-fpm(传统) | php-fpm(传统开启opcache) | laravels | webman(docker)| -|---- | ---- |---- |----| ---| -|首页 | 6请求/秒 | 157请求/秒 | 477请求/秒 | 803请求/秒 | -|用户订阅 | 6请求/秒 | 196请求/秒 | 586请求/秒 | 1064请求/秒 | -|用户首页延迟| 308ms | 110ms | 101ms | 98ms | +|Scenario | php-fpm(traditional) | php-fpm(traditional with opcache) | laravels | webman(docker)| +|---- | ---- |---- |---- | ---| +|Homepage | 6 req/s | 157 req/s | 477 req/s| 803 req/s| +|User Subscription| 6 req/s | 196 req/s | 586 req/s| 1064 req/s| +|User Homepage Latency| 308ms | 110ms | 101ms | 98ms| -## 页面展示 -![示例图片](./docs/images/dashboard.png) +## Page Display +![Example Image](./docs/images/dashboard.png) -## 安装 / 更新 / 回滚 -你可以点击查看下列方式的**安装、更新**步骤 -- [1panel 部署](./docs/1panel安装指南.md) -- [Docker Compose 纯命令行快速部署](./docs/docker-compose安装指南.md) -- [aapanel + Docker Compose (推荐)](./docs/aapanel+docker安装指南.md) -- [aapanel 部署](./docs/aapanel安装指南.md) -### 从其他版本迁移 -#### 数据库迁移 -**根据你的版本查看对应的迁移指南进行迁移** -- v2board dev 23/10/27的版本 [点击跳转迁移指南](./docs/v2b_dev迁移指南.md) -- v2board 1.7.4 [点击跳转迁移指南](./docs/v2b_1.7.4迁移指南.md) -- v2board 1.7.3 [点击跳转迁移指南](./docs/v2b_1.7.3迁移指南.md) -- v2board wyx2685 [点击跳转迁移指南](./docs/v2b_wyx2685迁移指南.md) +## Installation / Update / Rollback +You can click to view the **installation and update** steps for the following methods: +- [1panel Deployment](./docs/1panel安装指南.md) +- [Docker Compose Command-line Quick Deployment](./docs/docker-compose安装指南.md) +- [aapanel + Docker Compose (Recommended)](./docs/aapanel+docker安装指南.md) +- [aapanel Deployment](./docs/aapanel安装指南.md) +### Migrating from Other Versions +#### Database Migration +**Check the corresponding migration guide according to your version** +- v2board dev version 23/10/27 [Jump to Migration Guide](./docs/v2b_dev迁移指南.md) +- v2board 1.7.4 [Jump to Migration Guide](./docs/v2b_1.7.4迁移指南.md) +- v2board 1.7.3 [Jump to Migration Guide](./docs/v2b_1.7.3迁移指南.md) +- v2board wyx2685 [Jump to Migration Guide](./docs/v2b_wyx2685迁移指南.md) -### 注意 -> 修改后台路径需要重启才能生效 +### Note +> Modifying the admin path requires a restart to take effect ``` docker compose restart ``` -> 如果是是aapanel安装则需要重启 webman守护进程 +> If using aapanel installation, you need to restart the webman daemon process diff --git a/app/Console/Commands/BackupDatabase.php b/app/Console/Commands/BackupDatabase.php index 22a3c3c..377a6e6 100644 --- a/app/Console/Commands/BackupDatabase.php +++ b/app/Console/Commands/BackupDatabase.php @@ -26,9 +26,10 @@ class BackupDatabase extends Command } // 数据库备份逻辑 + $databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql'); + $compressedBackupPath = $databaseBackupPath . '.gz'; try{ if (config('database.default') === 'mysql'){ - $databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql'); $this->info("1️⃣:开始备份Mysql"); \Spatie\DbDumper\Databases\MySql::create() ->setHost(config('database.connections.mysql.host')) @@ -83,7 +84,7 @@ class BackupDatabase extends Command $bucket->upload(fopen($compressedBackupPath, 'r'), [ 'name' => $objectName, ]); - + // 输出文件链接 \Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName"); $this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName"); diff --git a/app/Console/Commands/MigrateFromV2b.php b/app/Console/Commands/MigrateFromV2b.php index 2209282..0fca35e 100644 --- a/app/Console/Commands/MigrateFromV2b.php +++ b/app/Console/Commands/MigrateFromV2b.php @@ -51,15 +51,16 @@ class MigrateFromV2b extends Command ], '1.7.3' => [ 'ALTER TABLE `v2_stat_order` RENAME TO `v2_stat`;', - "ALTER TABLE `v2_stat` CHANGE COLUMN order_amount order_total INT COMMENT '订单合计';", + "ALTER TABLE `v2_stat` CHANGE COLUMN order_amount paid_total INT COMMENT '订单合计';", + "ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';", "ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';", "ALTER TABLE `v2_stat` - ADD COLUMN paid_count INT NULL, - ADD COLUMN paid_total INT NULL, + ADD COLUMN order_count INT NULL, + ADD COLUMN order_total INT NULL, ADD COLUMN register_count INT NULL, ADD COLUMN invite_count INT NULL, ADD COLUMN transfer_used_total VARCHAR(32) NULL; - ", + ", "CREATE TABLE `v2_log` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `title` TEXT NOT NULL, diff --git a/app/Console/Commands/XboardInstall.php b/app/Console/Commands/XboardInstall.php index e124736..434dd10 100644 --- a/app/Console/Commands/XboardInstall.php +++ b/app/Console/Commands/XboardInstall.php @@ -46,6 +46,9 @@ class XboardInstall extends Command { try { $isDocker = env('docker', false); + $enableSqlite = env('enable_sqlite', false); + $enableRedis = env('enable_redis', false); + $adminAccount = env('admin_account', ''); $this->info("__ __ ____ _ "); $this->info("\ \ / /| __ ) ___ __ _ _ __ __| | "); $this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | "); @@ -67,7 +70,7 @@ class XboardInstall extends Command return; } // 选择是否使用Sqlite - if (confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) { + if ($enableSqlite || confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) { $sqliteFile = '.docker/.data/database.sqlite'; if (!file_exists(base_path($sqliteFile))) { // 创建空文件 @@ -142,7 +145,7 @@ class XboardInstall extends Command $isReidsValid = false; while (!$isReidsValid) { // 判断是否为Docker环境 - if ($isDocker == 'true' && (confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) { + if ($isDocker == 'true' && ($enableRedis || confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) { $envConfig['REDIS_HOST'] = '/run/redis-socket/redis.sock'; $envConfig['REDIS_PORT'] = 0; $envConfig['REDIS_PASSWORD'] = null; @@ -175,7 +178,7 @@ class XboardInstall extends Command abort(500, '复制环境文件失败,请检查目录权限'); } ; - $email = text( + $email = !empty($adminAccount) ? $adminAccount : text( label: '请输入管理员账号', default: 'admin@demo.com', required: true, diff --git a/app/Http/Controllers/V1/Admin/UserController.php b/app/Http/Controllers/V1/Admin/UserController.php index c3ed2bc..8117aa1 100644 --- a/app/Http/Controllers/V1/Admin/UserController.php +++ b/app/Http/Controllers/V1/Admin/UserController.php @@ -73,7 +73,7 @@ class UserController extends Controller $res[$i]['plan_name'] = $plan[$k]['name']; } } - $res[$i]['subscribe_url'] = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $res[$i]['token']); + $res[$i]['subscribe_url'] = Helper::getSubscribeUrl( $res[$i]['token']); } return response([ 'data' => $res, @@ -162,7 +162,7 @@ class UserController extends Controller $transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0; $notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0; $planName = $user['plan_name'] ?? '无订阅'; - $subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']); + $subscribeUrl = Helper::getSubscribeUrl($user['token']); $data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n"; } echo "\xEF\xBB\xBF" . $data; @@ -240,7 +240,7 @@ class UserController extends Controller $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); $createDate = date('Y-m-d H:i:s', $user['created_at']); $password = $request->input('password') ?? $user['email']; - $subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']); + $subscribeUrl = Helper::getSubscribeUrl($user['token']); $data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n"; } echo $data; diff --git a/app/Http/Controllers/V1/Client/ClientController.php b/app/Http/Controllers/V1/Client/ClientController.php index 913e4d6..903490f 100644 --- a/app/Http/Controllers/V1/Client/ClientController.php +++ b/app/Http/Controllers/V1/Client/ClientController.php @@ -24,6 +24,7 @@ class ClientController extends Controller 'ClashX Meta' => '1.3.5', 'Hiddify' => '0.1.0', 'loon' => '637', + 'v2rayng' => '1.9.5', 'v2rayN' => '6.31', 'surge' => '2398' ]; diff --git a/app/Http/Controllers/V1/Guest/PaymentController.php b/app/Http/Controllers/V1/Guest/PaymentController.php index b7cf604..0f11867 100644 --- a/app/Http/Controllers/V1/Guest/PaymentController.php +++ b/app/Http/Controllers/V1/Guest/PaymentController.php @@ -57,7 +57,7 @@ class PaymentController extends Controller $payment->name, $order->trade_no ); - + $telegramService->sendMessageWithAdmin($message); return true; } diff --git a/app/Http/Controllers/V1/User/KnowledgeController.php b/app/Http/Controllers/V1/User/KnowledgeController.php index ac412aa..6861e81 100644 --- a/app/Http/Controllers/V1/User/KnowledgeController.php +++ b/app/Http/Controllers/V1/User/KnowledgeController.php @@ -25,7 +25,7 @@ class KnowledgeController extends Controller if (!$userService->isAvailable($user)) { $this->formatAccessData($knowledge['body']); } - $subscribeUrl = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}"); + $subscribeUrl = Helper::getSubscribeUrl($user['token']); $knowledge['body'] = str_replace('{{siteName}}', admin_setting('app_name', 'XBoard'), $knowledge['body']); $knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']); $knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']); diff --git a/app/Http/Controllers/V1/User/StatController.php b/app/Http/Controllers/V1/User/StatController.php index a7845d4..f93bab5 100644 --- a/app/Http/Controllers/V1/User/StatController.php +++ b/app/Http/Controllers/V1/User/StatController.php @@ -13,7 +13,7 @@ class StatController extends Controller { public function getTrafficLog(Request $request) { - $startDate = now()->startOfMonth(); + $startDate = now()->startOfMonth()->timestamp; $records = StatUser::query() ->where('user_id', $request->user['id']) ->where('record_at', '>=', $startDate) diff --git a/app/Http/Controllers/V1/User/UserController.php b/app/Http/Controllers/V1/User/UserController.php index 0dbe572..abb97bf 100755 --- a/app/Http/Controllers/V1/User/UserController.php +++ b/app/Http/Controllers/V1/User/UserController.php @@ -140,7 +140,7 @@ class UserController extends Controller return $this->fail([400, __('Subscription plan does not exist')]); } } - $user['subscribe_url'] = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}"); + $user['subscribe_url'] = Helper::getSubscribeUrl($user['token']); $userService = new UserService(); $user['reset_day'] = $userService->getResetDay($user); return $this->success($user); @@ -157,7 +157,7 @@ class UserController extends Controller if (!$user->save()) { return $this->fail([400, __('Reset failed')]); } - return $this->success(Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user->token)); + return $this->success(Helper::getSubscribeUrl($user->token)); } public function update(UserUpdate $request) diff --git a/app/Http/Middleware/Server.php b/app/Http/Middleware/Server.php index 1cec74f..3d551fd 100644 --- a/app/Http/Middleware/Server.php +++ b/app/Http/Middleware/Server.php @@ -31,6 +31,7 @@ class Server $request->validate([ 'token' => [ "string", + "required", function ($attribute, $value, $fail) { if ($value !== admin_setting('server_token')) { $fail('The ' . $attribute . ' is invalid.'); @@ -39,10 +40,11 @@ class Server ], 'node_id' => 'required', 'node_type' => [ + 'required', 'nullable', 'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/', function ($attribute, $value, $fail) use ($aliasTypes, $request) { - $request->merge([$attribute => strtolower(isset ($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]); + $request->merge([$attribute => strtolower(isset($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]); }, ] ], [ diff --git a/app/Http/Routes/V1/ClientRoute.php b/app/Http/Routes/V1/ClientRoute.php index 81644ac..ef5e4d1 100644 --- a/app/Http/Routes/V1/ClientRoute.php +++ b/app/Http/Routes/V1/ClientRoute.php @@ -12,7 +12,7 @@ class ClientRoute 'middleware' => 'client' ], function ($router) { // Client - $router->get('/subscribe', 'V1\\Client\\ClientController@subscribe'); + $router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe'); // App $router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig'); $router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion'); diff --git a/app/Jobs/BatchTrafficFetchJob.php b/app/Jobs/BatchTrafficFetchJob.php index 0cf8599..18a456f 100644 --- a/app/Jobs/BatchTrafficFetchJob.php +++ b/app/Jobs/BatchTrafficFetchJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -17,7 +18,7 @@ class BatchTrafficFetchJob implements ShouldQueue protected $protocol; protected $timestamp; public $tries = 1; - public $timeout = 10; + public $timeout = 20; /** * Create a new job instance. @@ -36,34 +37,16 @@ class BatchTrafficFetchJob implements ShouldQueue public function handle(): void { - // 获取子节点 $targetServer = $this->childServer ?? $this->server; foreach ($this->data as $uid => $v) { - $u = $v[0]; - $d = $v[1]; - $result = \DB::transaction(function () use ($uid, $u, $d, $targetServer) { - $user = \DB::table('v2_user')->lockForUpdate()->where('id', $uid)->first(); - if (!$user) { - return true; - } - $newTime = time(); - $newU = $user->u + ($u * $targetServer['rate']); - $newD = $user->d + ($d * $targetServer['rate']); - $rows = \DB::table('v2_user') - ->where('id', $uid) - ->update([ - 't' => $newTime, - 'u' => $newU, - 'd' => $newD, - ]); - if ($rows === 0) { - return false; - } - return true; - }, 3); - if (!$result) { - TrafficFetchJob::dispatch($u, $d, $uid, $targetServer, $this->protocol); - } + User::where('id', $uid) + ->incrementEach( + [ + 'u' => $v[0] * $targetServer['rate'], + 'd' => $v[1] * $targetServer['rate'], + ], + ['t' => time()] + ); } } } diff --git a/app/Payments/BTCPay.php b/app/Payments/BTCPay.php index f2db1d1..60ca039 100644 --- a/app/Payments/BTCPay.php +++ b/app/Payments/BTCPay.php @@ -76,14 +76,13 @@ class BTCPay //NOT BTCPay-Sig //API doc is WRONG! $headerName = 'Btcpay-Sig'; - $signraturHeader = isset($headers[$headerName]) ? $headers[$headerName] : ''; + $signatureHeader = isset($headers[$headerName]) ? $headers[$headerName] : ''; $json_param = json_decode($payload, true); $computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->config['btcpay_webhook_key']); - if (!self::hashEqual($signraturHeader, $computedSignature)) { + if (!self::hashEqual($signatureHeader, $computedSignature)) { throw new ApiException('HMAC signature does not match', 400); - return false; } //get order id store in metadata diff --git a/app/Payments/EPay.php b/app/Payments/EPay.php index cf60128..2835c3f 100644 --- a/app/Payments/EPay.php +++ b/app/Payments/EPay.php @@ -27,7 +27,12 @@ class EPay 'label' => 'KEY', 'description' => '', 'type' => 'input', - ] + ], + 'type' => [ + 'label' => 'TYPE', + 'description' => 'alipay / qqpay / wxpay', + 'type' => 'input', + ], ]; } @@ -41,6 +46,9 @@ class EPay 'out_trade_no' => $order['trade_no'], 'pid' => $this->config['pid'] ]; + if(optional($this->config)['type']){ + $params['type'] = $this->config['type']; + } ksort($params); reset($params); $str = stripslashes(urldecode(http_build_query($params))) . $this->config['key']; diff --git a/app/Payments/StripeALLInOne.php b/app/Payments/StripeALLInOne.php new file mode 100644 index 0000000..70e5b5b --- /dev/null +++ b/app/Payments/StripeALLInOne.php @@ -0,0 +1,210 @@ +config = $config; + } + + public function form() + { + return [ + 'currency' => [ + 'label' => '货币单位', + 'description' => '请使用符合ISO 4217标准的三位字母,例如GBP', + 'type' => 'input', + ], + 'stripe_sk_live' => [ + 'label' => 'SK_LIVE', + 'description' => '', + 'type' => 'input', + ], + 'stripe_webhook_key' => [ + 'label' => 'WebHook密钥签名', + 'description' => 'whsec_....', + 'type' => 'input', + ], + 'description' => [ + 'label' => '自定义商品介绍', + 'description' => '', + 'type' => 'input', + ], + 'payment_method' => [ + 'label' => '支付方式', + 'description' => '请输入alipay, wechat_pay, cards', + 'type' => 'input', + ] + ]; + } + + public function pay($order) + { + $currency = $this->config['currency']; + $exchange = $this->exchange('CNY', strtoupper($currency)); + if (!$exchange) { + throw new ApiException('Currency conversion has timed out, please try again later', 500); + } + //jump url + $jumpUrl = null; + $actionType = 0; + $stripe = new \Stripe\StripeClient($this->config['stripe_sk_live']); + + if ($this->config['payment_method'] != "cards"){ + $stripePaymentMethod = $stripe->paymentMethods->create([ + 'type' => $this->config['payment_method'], + ]); + // 准备支付意图的基础参数 + $params = [ + 'amount' => floor($order['total_amount'] * $exchange), + 'currency' => $currency, + 'confirm' => true, + 'payment_method' => $stripePaymentMethod->id, + 'automatic_payment_methods' => ['enabled' => true], + 'statement_descriptor_suffix' => 'sub-' . $order['user_id'] . '-' . substr($order['trade_no'], -8), + 'description' => $this->config['description'], + 'metadata' => [ + 'user_id' => $order['user_id'], + 'out_trade_no' => $order['trade_no'], + 'identifier' => '' + ], + 'return_url' => $order['return_url'] + ]; + + // 如果支付方式为 wechat_pay,添加相应的支付方式选项 + if ($this->config['payment_method'] === 'wechat_pay') { + $params['payment_method_options'] = [ + 'wechat_pay' => [ + 'client' => 'web' + ], + ]; + } + //更新支持最新的paymentIntents方法,Sources API将在今年被彻底替 + $stripeIntents = $stripe->paymentIntents->create($params); + + $nextAction = null; + + if (!$stripeIntents['next_action']) { + throw new ApiException(__('Payment gateway request failed')); + }else { + $nextAction = $stripeIntents['next_action']; + } + + switch ($this->config['payment_method']){ + case "alipay": + if (isset($nextAction['alipay_handle_redirect'])){ + $jumpUrl = $nextAction['alipay_handle_redirect']['url']; + $actionType = 1; + }else { + throw new ApiException('unable get Alipay redirect url', 500); + } + break; + case "wechat_pay": + if (isset($nextAction['wechat_pay_display_qr_code'])){ + $jumpUrl = $nextAction['wechat_pay_display_qr_code']['data']; + }else { + throw new ApiException('unable get WeChat Pay redirect url', 500); + } + } + } else { + $creditCheckOut = $stripe->checkout->sessions->create([ + 'success_url' => $order['return_url'], + 'client_reference_id' => $order['trade_no'], + 'payment_method_types' => ['card'], + 'line_items' => [ + [ + 'price_data' => [ + 'currency' => $currency, + 'unit_amount' => floor($order['total_amount'] * $exchange), + 'product_data' => [ + 'name' => 'sub-' . $order['user_id'] . '-' . substr($order['trade_no'], -8), + 'description' => $this->config['description'], + ] + ], + 'quantity' => 1, + ], + ], + 'mode' => 'payment', + ]); + $jumpUrl = $creditCheckOut['url']; + $actionType = 1; + } + + return [ + 'type' => $actionType, + 'data' => $jumpUrl + ]; + } + + public function notify($params) + { + try { + \Stripe\Stripe::setApiKey($this->config['stripe_sk_live']); + //Workerman不支持使用php://input, stripe同时要求验证签名的payload不能经过修改,所以使用这个方法 + $payload = $GLOBALS['HTTP_RAW_POST_DATA']; + $headers = getallheaders(); + $headerName = 'Stripe-Signature'; + $signatureHeader = $headers[$headerName] ?? ''; + $event = \Stripe\Webhook::constructEvent( + $payload, + $signatureHeader, + $this->config['stripe_webhook_key'] + ); + + } catch (\UnexpectedValueException $e){ + throw new ApiException('Error parsing payload', 400); + } + catch (\Stripe\Exception\SignatureVerificationException $e) { + throw new ApiException('signature not match', 400); + } + switch ($event->type) { + case 'payment_intent.succeeded': + $object = $event->data->object; + if ($object->status === 'succeeded') { + if (!isset($object->metadata->out_trade_no)) { + return('order error'); + } + $metaData = $object->metadata; + $tradeNo = $metaData->out_trade_no; + return [ + 'trade_no' => $tradeNo, + 'callback_no' => $object->id + ]; + } + break; + case 'checkout.session.completed': + $object = $event->data->object; + if ($object->payment_status === 'paid') { + return [ + 'trade_no' => $object->client_reference_id, + 'callback_no' => $object->payment_intent + ]; + } + break; + case 'checkout.session.async_payment_succeeded': + $object = $event->data->object; + return [ + 'trade_no' => $object->client_reference_id, + 'callback_no' => $object->payment_intent + ]; + break; + default: + throw new ApiException('event is not support'); + } + return('success'); + } + + private function exchange($from, $to) + { + $from = strtolower($from); + $to = strtolower($to); + $result = file_get_contents("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/" . $from . ".min.json"); + $result = json_decode($result, true); + return $result[$from][$to]; + } +} diff --git a/app/Plugins/Telegram/Commands/GetLatestUrl.php b/app/Plugins/Telegram/Commands/GetLatestUrl.php index 0d34b77..44afdfb 100644 --- a/app/Plugins/Telegram/Commands/GetLatestUrl.php +++ b/app/Plugins/Telegram/Commands/GetLatestUrl.php @@ -7,7 +7,7 @@ use App\Plugins\Telegram\Telegram; class GetLatestUrl extends Telegram { public $command = '/getlatesturl'; - public $description = '将Telegram账号绑定到网站'; + public $description = '获取网站最新网址'; public function handle($message, $match = []) { $telegramService = $this->telegramService; diff --git a/app/Plugins/Telegram/Commands/Start.php b/app/Plugins/Telegram/Commands/Start.php new file mode 100644 index 0000000..add3db0 --- /dev/null +++ b/app/Plugins/Telegram/Commands/Start.php @@ -0,0 +1,17 @@ +is_private) return; + $telegramService = $this->telegramService; + $text = "/start 显示所有可用指令\n /bind+空格+订阅链接,将telegram绑定至账户\n /traffic 获取当前使用流量 \n /getlatesturl 获取网站最新网址 \n /unbind 解绑telegram账户"; + $telegramService->sendMessage($message->chat_id, $text, 'markdown'); + } +} diff --git a/app/Plugins/Telegram/Commands/UnBind.php b/app/Plugins/Telegram/Commands/UnBind.php index ebd9325..67fd022 100644 --- a/app/Plugins/Telegram/Commands/UnBind.php +++ b/app/Plugins/Telegram/Commands/UnBind.php @@ -12,11 +12,17 @@ class UnBind extends Telegram { public function handle($message, $match = []) { if (!$message->is_private) return; - $user = User::where('telegram_id', $message->chat_id)->first(); - $telegramService = $this->telegramService; - if (!$user) { - $telegramService->sendMessage($message->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown'); - return; + if (!isset($message->args[0])) { + $user = User::where('telegram_id', $message->chat_id)->first(); + } else { + $chat = User::where('telegram_id', $message->chat_id)->first(); + if (!$chat) return; + if (!($chat->is_admin || $chat->is_staff)) return; + if (strpos($message->args[0], '@') !== true) { + $user = User::where('email', $message->args[0])->first(); + } else { + $user = User::where('telegram_id', $message->args[0])->first(); + } } $user->telegram_id = NULL; if (!$user->save()) { diff --git a/app/Protocols/ClashMeta.php b/app/Protocols/ClashMeta.php index aff8982..6719efc 100644 --- a/app/Protocols/ClashMeta.php +++ b/app/Protocols/ClashMeta.php @@ -88,7 +88,8 @@ class ClashMeta return response($yaml, 200) ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}") ->header('profile-update-interval', '24') - ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName)); + ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName)) + ->header('profile-web-page-url', admin_setting('app_url')); } /** @@ -194,8 +195,6 @@ class ClashMeta $array['server'] = $server['host']; $array['port'] = $server['port']; $array['uuid'] = $password; - $array['alterId'] = 0; - $array['cipher'] = 'auto'; $array['udp'] = true; // XTLS流控算法 @@ -207,8 +206,8 @@ class ClashMeta $array['tls'] = true; if ($server['tls_settings']) { $tlsSettings = $server['tls_settings']; - if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure'])) - $array['skip-cert-verify'] = ($tlsSettings['allowInsecure'] ? true : false); + if (isset($tlsSettings['allow_insecure']) && !empty($tlsSettings['allow_insecure'])) + $array['skip-cert-verify'] = ($tlsSettings['allow_insecure'] ? true : false); if (isset($tlsSettings['server_name']) && !empty($tlsSettings['server_name'])) $array['servername'] = $tlsSettings['server_name']; } @@ -242,10 +241,8 @@ class ClashMeta $array['ws-opts']['path'] = $wsSettings['path']; if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) $array['ws-opts']['headers'] = ['Host' => $wsSettings['headers']['Host']]; - if (isset($wsSettings['path']) && !empty($wsSettings['path'])) - $array['ws-path'] = $wsSettings['path']; - if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) - $array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']]; + $array['ws-opts']['max-early-data'] = 2560; + $array['ws-opts']['early-data-header-name'] = 'Sec-WebSocket-Protocol'; } } if ($server['network'] === 'grpc') { diff --git a/app/Protocols/General.php b/app/Protocols/General.php index 7de9186..0a3f2a5 100644 --- a/app/Protocols/General.php +++ b/app/Protocols/General.php @@ -36,6 +36,9 @@ class General if ($item['type'] === 'trojan') { $uri .= self::buildTrojan($user['uuid'], $item); } + if ($item['type'] === 'hysteria') { + $uri .= self::buildHysteria($user['uuid'], $item); + } } return base64_encode($uri); } @@ -170,4 +173,33 @@ class General return $uri; } + public static function buildHysteria($password, $server) + { + $params = []; + // Return empty if version is not 2 + if ($server['version'] !== 2) { + return ''; + } + + if ($server['server_name']) { + $params['sni'] = $server['server_name']; + $params['security'] = 'tls'; + } + + if ($server['is_obfs']) { + $params['obfs'] = 'salamander'; + $params['obfs-password'] = $server['server_key']; + } + + $params['insecure'] = $server['insecure'] ? 1 : 0; + + $query = http_build_query($params); + $name = rawurlencode($server['name']); + + $uri = "hysteria2://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}"; + $uri .= "\r\n"; + + return $uri; + } + } diff --git a/app/Protocols/Loon.php b/app/Protocols/Loon.php index 570a380..56290ea 100644 --- a/app/Protocols/Loon.php +++ b/app/Protocols/Loon.php @@ -151,7 +151,7 @@ class Loon $server['host'], $server['port'], $password, - $server['server_name'] ? "tls={$server['server_name']}" : "(null)" + $server['server_name'] ? "sni={$server['server_name']}" : "(null)" ]; if ($server['insecure']) $config[] = "skip-cert-verify=true"; $config[] = "download-bandwidth=" . ($user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps']); diff --git a/app/Protocols/Passwall.php b/app/Protocols/Passwall.php index 66efd33..2799a61 100644 --- a/app/Protocols/Passwall.php +++ b/app/Protocols/Passwall.php @@ -34,6 +34,9 @@ class Passwall if ($item['type'] === 'trojan') { $uri .= self::buildTrojan($user['uuid'], $item); } + if ($item['type'] === 'hysteria') { + $uri .= General::buildHysteria($user['uuid'], $item); + } } return base64_encode($uri); } diff --git a/app/Protocols/SSRPlus.php b/app/Protocols/SSRPlus.php index 1d32a61..e2e25b6 100644 --- a/app/Protocols/SSRPlus.php +++ b/app/Protocols/SSRPlus.php @@ -34,6 +34,9 @@ class SSRPlus if ($item['type'] === 'trojan') { $uri .= self::buildTrojan($user['uuid'], $item); } + if ($item['type'] === 'hysteria') { + $uri .= General::buildHysteria($user['uuid'], $item); + } } return base64_encode($uri); } diff --git a/app/Protocols/Shadowrocket.php b/app/Protocols/Shadowrocket.php index c4cc8db..345783e 100644 --- a/app/Protocols/Shadowrocket.php +++ b/app/Protocols/Shadowrocket.php @@ -190,7 +190,7 @@ class Shadowrocket if ($server['network_settings']) { $wsSettings = $server['network_settings']; if (isset($wsSettings['path']) && !empty($wsSettings['path'])) - $config['path'] = $wsSettings['path']; + $config['path'] = $wsSettings['path'] . '?ed=2560'; if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) $config['obfsParam'] = $wsSettings['headers']['Host']; } diff --git a/app/Protocols/SingBox.php b/app/Protocols/SingBox.php index a0e1e18..ab50545 100644 --- a/app/Protocols/SingBox.php +++ b/app/Protocols/SingBox.php @@ -26,6 +26,7 @@ class SingBox return response() ->json($this->config) + ->header('profile-title', 'base64:'. base64_encode($appName)) ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}") ->header('profile-update-interval', '24'); } @@ -148,7 +149,7 @@ class SingBox $array['transport']['path'] = $wsSettings['path']; if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) $array['transport']['headers'] = ['Host' => array($wsSettings['headers']['Host'])]; - $array['transport']['max_early_data'] = 2048; + $array['transport']['max_early_data'] = 2560; $array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol'; } } @@ -216,7 +217,7 @@ class SingBox $array['transport']['path'] = $wsSettings['path']; if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) $array['transport']['headers'] = ['Host' => array($wsSettings['headers']['Host'])]; - $array['transport']['max_early_data'] = 2048; + $array['transport']['max_early_data'] = 2560; $array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol'; } } @@ -271,7 +272,7 @@ class SingBox if (isset($server['network_settings']['headers']['Host'])) { $array['transport']['headers'] = ['Host' => array($server['network_settings']['headers']['Host'])]; } - $array['transport']['max_early_data'] = 2048; + $array['transport']['max_early_data'] = 2560; $array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol'; } } @@ -309,6 +310,8 @@ class SingBox $array['tag'] = $server['name']; $array['type'] = 'hysteria2'; $array['password'] = $password; + $array['up_mbps'] = $user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps']; + $array['down_mbps'] = $user->speed_limit ? min($server['up_mbps'], $user->speed_limit) : $server['up_mbps']; if ($server['is_obfs']) { $array['obfs']['type'] = 'salamander'; @@ -318,4 +321,4 @@ class SingBox return $array; } -} \ No newline at end of file +} diff --git a/app/Protocols/Surfboard.php b/app/Protocols/Surfboard.php index 007e3eb..be44640 100644 --- a/app/Protocols/Surfboard.php +++ b/app/Protocols/Surfboard.php @@ -63,7 +63,7 @@ class Surfboard } // Subscription link - $subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}"); + $subsURL = Helper::getSubscribeUrl($user['token']); $subsDomain = request()->header('Host'); $config = str_replace('$subs_link', $subsURL, $config); diff --git a/app/Protocols/Surge.php b/app/Protocols/Surge.php index 4e09b82..12e2f26 100644 --- a/app/Protocols/Surge.php +++ b/app/Protocols/Surge.php @@ -32,7 +32,9 @@ class Surge 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', - 'chacha20-ietf-poly1305' + 'chacha20-ietf-poly1305', + '2022-blake3-aes-128-gcm', + '2022-blake3-aes-256-gcm', ]) ) { // [Proxy] @@ -69,9 +71,8 @@ class Surge } // Subscription link - $subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}"); $subsDomain = request()->header('Host'); - $subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token']; + $subsURL = Helper::getSubscribeUrl($user['token'], $subsDomain ? 'https://' . $subsDomain : null); $config = str_replace('$subs_link', $subsURL, $config); $config = str_replace('$subs_domain', $subsDomain, $config); diff --git a/app/Protocols/V2rayN.php b/app/Protocols/V2rayN.php index cb353f4..9d2ae6c 100644 --- a/app/Protocols/V2rayN.php +++ b/app/Protocols/V2rayN.php @@ -37,7 +37,7 @@ class V2rayN $uri .= self::buildTrojan($user['uuid'], $item); } if ($item['type'] === 'hysteria') { - $uri .= self::buildHysteria($user['uuid'], $item); + $uri .= General::buildHysteria($user['uuid'], $item); } } @@ -192,25 +192,5 @@ class V2rayN return $uri; } - public static function buildHysteria($password, $server) - { - $name = rawurlencode($server['name']); - $params = []; - if ($server['server_name']) $params['sni'] = $server['server_name']; - $params['insecure'] = $server['insecure'] ? 1 : 0; - if($server['is_obfs']) { - $params['obfs'] = 'salamander'; - $params['obfs-password'] = $server['server_key']; - } - $query = http_build_query($params); - if ($server['version'] == 2) { - $uri = "hysteria2://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}"; - $uri .= "\r\n"; - } else { - // V2rayN似乎不支持v1, 返回空 - $uri = ""; - } - return $uri; - } } diff --git a/app/Protocols/V2rayNG.php b/app/Protocols/V2rayNG.php index 7b1ba53..7d16c95 100644 --- a/app/Protocols/V2rayNG.php +++ b/app/Protocols/V2rayNG.php @@ -34,6 +34,9 @@ class V2rayNG if ($item['type'] === 'vless') { $uri .= self::buildVless($user['uuid'], $item); } + if ($item['type'] === 'hysteria') { + $uri .= General::buildHysteria($user['uuid'], $item); + } } return base64_encode($uri); } @@ -190,5 +193,4 @@ class V2rayNG return $uri; } - } diff --git a/app/Services/ServerService.php b/app/Services/ServerService.php index 8da9a3f..60bcefa 100644 --- a/app/Services/ServerService.php +++ b/app/Services/ServerService.php @@ -196,7 +196,7 @@ class ServerService // 获取可用的用户列表 public static function getAvailableUsers($groupId): Collection { - return \DB::table('v2_user') + return User::toBase() ->whereIn('group_id', $groupId) ->whereRaw('u + d < transfer_enable') ->where(function ($query) { @@ -314,9 +314,11 @@ class ServerService $servers[$k]['online'] = Cache::get(CacheKey::get("SERVER_{$serverType}_ONLINE_USER", $v['parent_id'] ?? $v['id'])) ?? 0; // 如果是子节点,先尝试从缓存中获取 if($pid = $v['parent_id']){ - // 获取缓存 - $onlineUsers = Cache::get(CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid)) ?? []; - $servers[$k]['online'] = (collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user')) . "|{$servers[$k]['online']}"; + $cacheKey = CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid); + $onlineUsers = Cache::get($cacheKey) ?? []; + $onlineUserSum = collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user'); + $online = ($onlineUserSum > 0 ? $onlineUserSum . "|" : "") . $servers[$k]['online']; + $servers[$k]['online'] = $online; } $servers[$k]['last_check_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_CHECK_AT", $v['parent_id'] ?? $v['id'])); $servers[$k]['last_push_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_PUSH_AT", $v['parent_id'] ?? $v['id'])); diff --git a/app/Services/StatisticalService.php b/app/Services/StatisticalService.php index 3ec016e..3a727f0 100644 --- a/app/Services/StatisticalService.php +++ b/app/Services/StatisticalService.php @@ -108,14 +108,14 @@ class StatisticalService /** * 获取指定用户的流量使用情况 */ - public function getStatUserByUserID($userId): array + public function getStatUserByUserID(int|string $userId): array { $stats = []; $statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true); foreach ($statsUser as $member => $value) { list($rate, $uid, $type) = explode('_', $member); - if ($uid !== $userId) + if (intval($uid) !== intval($userId)) continue; $key = "{$rate}_{$uid}"; $stats[$key] = $stats[$key] ?? [ diff --git a/app/Utils/Helper.php b/app/Utils/Helper.php index 6844327..c88272a 100644 --- a/app/Utils/Helper.php +++ b/app/Utils/Helper.php @@ -108,13 +108,24 @@ class Helper } } - public static function getSubscribeUrl($path) + public static function getSubscribeUrl(string $token, $subscribeUrl = null) { - $subscribeUrls = explode(',', admin_setting('subscribe_url')); - $subscribeUrl = $subscribeUrls[array_rand($subscribeUrls)]; - $subscribeUrl = self::replaceByPattern($subscribeUrl); - if ($subscribeUrl) return $subscribeUrl . $path; - return url($path); + $strs = 'QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm'; + $randstr = substr(str_shuffle($strs), 0, rand(4,8)); + + $path = route('client.subscribe', ['token' => $token], false); + if(!$subscribeUrl){ + $subscribeUrls = explode(',', admin_setting('subscribe_url')); + $subscribeUrl = \Arr::random($subscribeUrls); + $subscribeUrl = self::replaceByPattern($subscribeUrl); + if (strpos($subscribeUrl, "*") !== false) { + $subscribeUrl = str_replace("*", $randstr, $subscribeUrl); + } elseif (strpos($subscribeUrl, '{uuid}') !== false) { + $user = User::where('token', $token)->first(); + $subscribeUrl = str_replace('{uuid}', $user->uuid, $subscribeUrl); + } + } + return $subscribeUrl ? rtrim($subscribeUrl, '/') . $path : url($path); } public static function randomPort($range) { diff --git a/composer.json b/composer.json index bfccc63..8dad9fe 100755 --- a/composer.json +++ b/composer.json @@ -21,14 +21,14 @@ "guzzlehttp/guzzle": "^7.4.3", "hhxsv5/laravel-s": "~3.7.0", "joanhey/adapterman": "^0.6.1", - "laravel/framework": "^10.0", + "laravel/framework": "10.48.22", "laravel/horizon": "^5.9.6", "laravel/tinker": "^2.5", "linfo/linfo": "^4.0", "paragonie/sodium_compat": "^1.20", "php-curl-class/php-curl-class": "^8.6", "spatie/db-dumper": "^3.4", - "stripe/stripe-php": "^7.36.1", + "stripe/stripe-php": "^v14.9.0", "symfony/http-client": "^6.4", "symfony/mailgun-mailer": "^6.4", "symfony/yaml": "*", diff --git a/docker-compose.sample.yaml b/docker-compose.sample.yaml index 87960b5..e16db3a 100644 --- a/docker-compose.sample.yaml +++ b/docker-compose.sample.yaml @@ -3,7 +3,7 @@ services: xboard: # build: # context: . - image: ghcr.io/cedar2025/xboard:latest + image: ghcr.io/cedar2025/xboard:legacy volumes: - ./:/www/ # - ./.env:/www/.env diff --git a/docs/1panel安装指南.md b/docs/1panel安装指南.md index 4a37cb6..74ae6de 100644 --- a/docs/1panel安装指南.md +++ b/docs/1panel安装指南.md @@ -106,6 +106,8 @@ docker compose up -d 🎉: 到这里,你已经可以通过域名访问你的站点了。 +⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。 + ## 更新 1. 通过 SSH 登录到服务器后,访问站点路径如:`/opt/1panel/apps/openresty/openresty/www/sites/xboard/index`,然后在站点目录中执行以下命令: diff --git a/docs/aapanel+docker安装指南.md b/docs/aapanel+docker安装指南.md index 3a7513c..b835760 100644 --- a/docs/aapanel+docker安装指南.md +++ b/docs/aapanel+docker安装指南.md @@ -6,6 +6,7 @@ ``` # 安装Docker curl -sSL https://get.docker.com | bash +# Centos系统可能还需要执行下面命令来启动Docker systemctl enable docker systemctl start docker ``` @@ -38,7 +39,7 @@ rm -rf .htaccess 404.html 502.html index.html .user.ini ``` > 执行命令从 Github 克隆到当前目录。 ``` -git clone https://github.com/cedar2025/Xboard.git ./ +git clone -b dev https://github.com/cedar2025/Xboard.git ./ ``` > 复制一份docker-compose.yaml文件 ``` @@ -79,7 +80,9 @@ location ^~ / { } ``` -🎉: 到这里,你可以已经可以通过域名访问你的站点了 +🎉: 到这里,你可以已经可以通过域名访问你的站点了 + +⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。 ### 更新 1. 更新代码 diff --git a/docs/aapanel安装指南.md b/docs/aapanel安装指南.md index 430e054..c9bf194 100644 --- a/docs/aapanel安装指南.md +++ b/docs/aapanel安装指南.md @@ -48,7 +48,7 @@ rm -rf .htaccess 404.html 502.html index.html .user.ini ``` > 执行命令从 Github 克隆到当前目录。 ``` -git clone https://github.com/cedar2025/Xboard.git ./ +git clone -b dev https://github.com/cedar2025/Xboard.git ./ ``` > 执行命令安装依赖包以及V2board ``` diff --git a/docs/docker-compose安装指南.md b/docs/docker-compose安装指南.md index 4bc4c34..d7c3a1f 100644 --- a/docs/docker-compose安装指南.md +++ b/docs/docker-compose安装指南.md @@ -1,70 +1,119 @@ -## Docker-Compose 部署教程 -本文教你如何在命令行使用docker-compose + sqlite来快速部署Xboard -如果你需要使用Mysql,你需要自行处理好Mysql的安装。 -### 部署 (使用docker-compose 2分钟部署) -> 在此提供Xboard安装、快速体验Xboard的步骤。 -使用docker compose + sqlite 快速部署站点(**无需安装Mysql以及redis**) -1. 安装docker -``` +## Docker-Compose 快速部署指南 + +### 环境要求 +- Docker (最新稳定版) +- 至少 1GB 可用内存 +- 至少 10GB 可用磁盘空间 +- 系统支持: Linux/macOS/Windows +- 开放端口: 7001 (默认) + +### 部署步骤 + +#### 1. 安装 Docker +```bash +# 安装 Docker curl -sSL https://get.docker.com | bash + +# CentOS 系统需要执行以下命令启动 Docker systemctl enable docker systemctl start docker ``` -2. 获取Docker compose 文件 -``` -git clone -b docker-compose --depth 1 https://github.com/cedar2025/Xboard + +#### 2. 获取部署文件 +```bash +git clone -b docker-compose --depth 1 https://github.com/cedar2025/Xboard cd Xboard ``` -3. 执行数据库安装命令 -> 选择 **启用sqlite** 和 **Docker内置的Redis** + +#### 3. 初始化安装 +> 提供两种安装方式,选择其一即可: + +**方式一:快速安装** (推荐) +```bash +# 使用 SQLite + Docker内置Redis +docker compose run -it --rm \ + -e enable_sqlite=true \ + -e enable_redis=true \ + -e admin_account=admin@demo.com \ + xboard php artisan xboard:install ``` + +**方式二:自定义安装** +```bash +# 根据提示自定义配置 docker compose run -it --rm xboard php artisan xboard:install ``` -> 执行这条命令之后,会返回你的后台地址和管理员账号密码(你需要记录下来) -> 你需要执行下面的 **启动xborad** 步骤之后才能访问后台 -4. 启动Xboard -``` +> **重要提示:** +> - 安装完成后会显示后台地址和管理员账号密码,请务必保存 +> - 如需使用 MySQL,请先自行安装并配置 MySQL 后再部署 + +#### 4. 启动服务 +```bash docker compose up -d ``` -> 安装完成之后即可访问你的站点 -5. 访问站点 -> 启动之后网站端口默认为7001, 你可以配置nginx反向代理使用80端口 -网站地址: http://你的IP:7001/ -在此你已经成功部署了, 你可以访问网址体验Xboard的完整功能, +#### 5. 访问站点 +- 网站地址:`http://服务器IP:7001` +- 后台地址:安装时提供的地址 -> 如果你需要使用mysql,请自行安装Mysql后重新部署 +### 更新指南 -### **更新** -1. 修改版本 -``` +#### 方式一:快速更新(保持最新版本) +```bash cd Xboard -vi docker-compose.yaml -``` -> 修改docker-compose.yaml 当中image后面的版本号为你需要的版本 -> 如果为版本为latest 则可以忽略这一步,直接进行第二步 - -2. 更新数据库(可以执行多次都是安全的) -``` docker compose pull docker compose down docker compose run -it --rm xboard php artisan xboard:update docker compose up -d ``` -> 即可更新成功 -### **回滚** -> 此回滚不回滚数据库,是否回滚数据库请查看相关文档 -1. 回退版本 -``` +#### 方式二:更新至指定版本 +1. 修改版本号 +```bash +# 编辑 docker-compose.yaml,修改 image 的版本号 vi docker-compose.yaml ``` -> 修改docker-compose.yaml 当中image后面的版本号为更新前的版本号 -2. 启动 -``` + +2. 执行更新 +```bash +docker compose pull +docker compose down +docker compose run -it --rm xboard php artisan xboard:update docker compose up -d ``` -### 注意 -启用webman后做的任何代码修改都需要重启生效 \ No newline at end of file +### 版本回滚 +```bash +# 1. 修改 docker-compose.yaml 中的版本号为目标版本 +vi docker-compose.yaml + +# 2. 重启服务 +docker compose up -d +``` + +### 常见问题 + +1. **端口配置** +- 默认端口为 7001 +- 可通过 Nginx 反向代理使用 80/443 端口 +- 如需修改端口,请编辑 docker-compose.yaml + +2. **数据持久化** +- 数据默认存储在 ./data 目录 +- 建议定期备份 data 目录 + +3. **性能优化** +- 启用 webman 后的代码修改需要重启服务才能生效 +- 可根据实际需求调整容器资源限制 + +### 安全建议 +1. 及时更新到最新版本 +2. 修改默认管理员账号 +3. 使用强密码 +4. 建议配置 SSL 证书 +5. 定期备份数据 + +### 技术支持 +- GitHub Issues: https://github.com/cedar2025/Xboard/issues +- 官方文档:[文档链接] diff --git a/resources/rules/default.sing-box.json b/resources/rules/default.sing-box.json index 1cf83a8..b762ef6 100644 --- a/resources/rules/default.sing-box.json +++ b/resources/rules/default.sing-box.json @@ -49,8 +49,10 @@ "auto_route": true, "domain_strategy": "prefer_ipv4", "endpoint_independent_nat": true, - "inet4_address": "172.19.0.1/30", - "inet6_address": "2001:0470:f9da:fdfa::1/64", + "address": [ + "172.19.0.1/30", + "2001:0470:f9da:fdfa::1/64" + ], "mtu": 9000, "sniff": true, "sniff_override_destination": true, diff --git a/update.sh b/update.sh index 02e7769..ae37c6e 100755 --- a/update.sh +++ b/update.sh @@ -11,7 +11,7 @@ if ! command -v git &> /dev/null; then fi git config --global --add safe.directory $(pwd) -git fetch --all && git reset --hard origin/dev && git pull origin dev +git fetch --all && git reset --hard origin/legacy && git pull origin legacy rm -rf composer.lock composer.phar wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar php composer.phar update -vvv