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/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6fe489e..00dd386 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -10,6 +10,7 @@ on: branches: [ "dev" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] + workflow_dispatch: # Enable manual trigger env: # Use docker.io for Docker Hub if empty @@ -60,7 +61,7 @@ jobs: 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 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/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/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/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/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/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/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/SingBox.php b/app/Protocols/SingBox.php index a0e1e18..3c79a01 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'); } @@ -318,4 +319,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 ab2dc54..12e2f26 100644 --- a/app/Protocols/Surge.php +++ b/app/Protocols/Surge.php @@ -71,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..391157a 100644 --- a/app/Services/ServerService.php +++ b/app/Services/ServerService.php @@ -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/Utils/Helper.php b/app/Utils/Helper.php index 6844327..341df4e 100644 --- a/app/Utils/Helper.php +++ b/app/Utils/Helper.php @@ -108,13 +108,15 @@ 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); + $path = route('client.subscribe', ['token' => $token], false); + if(!$subscribeUrl){ + $subscribeUrls = explode(',', admin_setting('subscribe_url')); + $subscribeUrl = \Arr::random($subscribeUrls); + $subscribeUrl = self::replaceByPattern($subscribeUrl); + } + return $subscribeUrl ? rtrim($subscribeUrl, '/') . $path : url($path); } public static function randomPort($range) { diff --git a/composer.json b/composer.json index 70c2c94..b7768ab 100755 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "license": "MIT", "require": { "php": "^8.1", - "cedar2025/http-foundation": "6.4.x-dev", "cweagans/composer-patches": "^1.7", "doctrine/dbal": "^3.7", "firebase/php-jwt": "^6.3", 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..5094da8 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 ``` @@ -79,7 +80,9 @@ location ^~ / { } ``` -🎉: 到这里,你可以已经可以通过域名访问你的站点了 +🎉: 到这里,你可以已经可以通过域名访问你的站点了 + +⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。 ### 更新 1. 更新代码 diff --git a/docs/docker-compose安装指南.md b/docs/docker-compose安装指南.md index 4bc4c34..ac25f8b 100644 --- a/docs/docker-compose安装指南.md +++ b/docs/docker-compose安装指南.md @@ -7,6 +7,9 @@ 1. 安装docker ``` curl -sSL https://get.docker.com | bash +``` +Centos系统可能需要执行下面命令来启动Docker。 +``` systemctl enable docker systemctl start docker ``` @@ -18,6 +21,10 @@ cd Xboard 3. 执行数据库安装命令 > 选择 **启用sqlite** 和 **Docker内置的Redis** ``` +docker compose run -it --rm -e enable_sqlite=true -e enable_redis=true -e admin_account=your_admin_email@example.com xboard php artisan xboard:install +``` +> 或者根据自己的需要在运行时选择 +``` docker compose run -it --rm xboard php artisan xboard:install ``` > 执行这条命令之后,会返回你的后台地址和管理员账号密码(你需要记录下来) @@ -67,4 +74,4 @@ docker compose up -d ``` ### 注意 -启用webman后做的任何代码修改都需要重启生效 \ No newline at end of file +启用webman后做的任何代码修改都需要重启生效