Merge branch 'cedar2025:dev' into adapted-allowInsecure

This commit is contained in:
Cp0204 2024-12-16 22:44:19 +08:00 committed by GitHub
commit 2fdffab3c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 668 additions and 520 deletions

View File

@ -1,3 +1,7 @@
unixsocket /run/redis-socket/redis.sock unixsocket /run/redis-socket/redis.sock
unixsocketperm 777 unixsocketperm 777
port 0 port 0
save 900 1
save 300 10
save 60 10000

View File

@ -12,12 +12,16 @@ assignees: ''
The XBoard version number you are using The XBoard version number you are using
当前使用的XBoard版本号 当前使用的XBoard版本号(git commit id)
--------
Would you like to deploy using Docker?
你的部署方式是否为Docker
-------- --------
Briefly describe the problem you are experiencing Please briefly describe the issue you encountered (preferably with reproducible steps).
简单描述你遇到的问题 简单描述你遇到的问题(最好带上复现步骤)
-------- --------
@ -34,6 +38,6 @@ Screenshot of the reported error(Please do desensitization)
The latest log files in the storage/logs directory report from #1 (Please do desensitization) Run the php artisan log:export 7 command to export log files (where 7 represents logs for the last 7 days).
storage/logs 目录下最新的日志文件从 #1 开始报告(请做脱敏处理) 运行`php artisan log:export 7` 命令导出的日志文件(其中7为最近7天的日志)。
-------- --------

View File

@ -10,6 +10,7 @@ on:
branches: [ "dev" ] branches: [ "dev" ]
# Publish semver tags as releases. # Publish semver tags as releases.
tags: [ 'v*.*.*' ] tags: [ 'v*.*.*' ]
workflow_dispatch: # Enable manual trigger
env: env:
# Use docker.io for Docker Hub if empty # Use docker.io for Docker Hub if empty
@ -32,6 +33,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: true
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3

View File

@ -46,6 +46,9 @@ class XboardInstall extends Command
{ {
try { try {
$isDocker = env('docker', false); $isDocker = env('docker', false);
$enableSqlite = env('enable_sqlite', false);
$enableRedis = env('enable_redis', false);
$adminAccount = env('admin_account', '');
$this->info("__ __ ____ _ "); $this->info("__ __ ____ _ ");
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | "); $this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | "); $this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
@ -67,7 +70,7 @@ class XboardInstall extends Command
return; return;
} }
// 选择是否使用Sqlite // 选择是否使用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'; $sqliteFile = '.docker/.data/database.sqlite';
if (!file_exists(base_path($sqliteFile))) { if (!file_exists(base_path($sqliteFile))) {
// 创建空文件 // 创建空文件
@ -142,7 +145,7 @@ class XboardInstall extends Command
$isReidsValid = false; $isReidsValid = false;
while (!$isReidsValid) { while (!$isReidsValid) {
// 判断是否为Docker环境 // 判断是否为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_HOST'] = '/run/redis-socket/redis.sock';
$envConfig['REDIS_PORT'] = 0; $envConfig['REDIS_PORT'] = 0;
$envConfig['REDIS_PASSWORD'] = null; $envConfig['REDIS_PASSWORD'] = null;
@ -175,7 +178,7 @@ class XboardInstall extends Command
abort(500, '复制环境文件失败,请检查目录权限'); abort(500, '复制环境文件失败,请检查目录权限');
} }
; ;
$email = text( $email = !empty($adminAccount) ? $adminAccount : text(
label: '请输入管理员账号', label: '请输入管理员账号',
default: 'admin@demo.com', default: 'admin@demo.com',
required: true, required: true,

View File

@ -126,7 +126,7 @@ class StatController extends Controller
} }
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics); array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [ return [
'data' => $statistics 'data' => collect($statistics)->take(15)->all()
]; ];
} }
// 获取昨日节点流量排行 // 获取昨日节点流量排行

View File

@ -73,7 +73,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name']; $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([ return response([
'data' => $res, 'data' => $res,
@ -162,7 +162,7 @@ class UserController extends Controller
$transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0; $transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
$notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0; $notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
$planName = $user['plan_name'] ?? '无订阅'; $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"; $data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
} }
echo "\xEF\xBB\xBF" . $data; 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']); $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']); $createDate = date('Y-m-d H:i:s', $user['created_at']);
$password = $request->input('password') ?? $user['email']; $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"; $data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
} }
echo $data; echo $data;

View File

@ -8,136 +8,65 @@ use App\Services\ServerService;
use App\Services\UserService; use App\Services\UserService;
use App\Utils\Helper; use App\Utils\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ClientController extends Controller class ClientController extends Controller
{ {
// 支持hy2 的客户端版本列表
const SupportedHy2ClientVersions = [
'NekoBox' => '1.2.7',
'sing-box' => '1.5.0',
'stash' => '2.5.0',
'Shadowrocket' => '1993',
'ClashMetaForAndroid' => '2.9.0',
'Nekoray' => '3.24',
'verge' => '1.3.8',
'ClashX Meta' => '1.3.5',
'Hiddify' => '0.1.0',
'loon' => '637',
'v2rayng' => '1.9.5',
'v2rayN' => '6.31',
'surge' => '2398'
];
// allowed types
const AllowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
public function subscribe(Request $request) public function subscribe(Request $request)
{ {
// 节点类型筛选 // filter types
$allowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'hysteria2', 'shadowsocks']; $types = $request->input('types', 'all');
$types = $request->input('types', "vmess|vless|trojan|hysteria|shadowsocks"); $typesArr = $types === 'all' ? self::AllowedTypes : array_values(array_intersect(explode('|', str_replace(['|', '', ','], "|", $types)), self::AllowedTypes));
if ($types === "all") $types = implode('|', $allowedTypes); // filter keyword
$typesArr = $types ? collect(explode('|', str_replace(['|','',','], "|" , $types)))->reject(function($type) use ($allowedTypes){ $filterArr = mb_strlen($filter = $request->input('filter')) > 20 ? null : explode("|", str_replace(['|', '', ','], "|", $filter));
return !in_array($type, $allowedTypes); $flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
})->values()->all() : []; $ip = $request->input('ip', $request->ip());
// get client version
// 节点关键词筛选字段获取 $version = preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches) ? $matches[1] : null;
$filterArr = (mb_strlen($request->input('filter')) > 20) ? null : explode("|" ,str_replace(['|','',','], "|" , $request->input('filter'))); $supportHy2 = $version ? collect(self::SupportedHy2ClientVersions)
->contains(fn($minVersion, $client) => stripos($flag, $client) !== false && $this->versionCompare($version, $minVersion)) : true;
$flag = $request->input('flag') ?? $request->header('User-Agent', '');
$flag = strtolower($flag);
$ip = $request->input('ip') ?? $request->ip();
preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches);
$version = $matches[1]??null;
$supportedClientVersions = [
'NekoBox' => '1.2.7',
'sing-box' => '1.5.0',
'stash' => '2.5.0',
'Shadowrocket' => '1993',
'ClashMetaForAndroid' => '2.9.0',
'Nekoray' => '3.24',
'verge' => '1.3.8',
'ClashX Meta' => '1.3.5',
'Hiddify' => '0.1.0',
'loon' => '637',
'v2rayN' => '6.31',
'surge' => '2398'
];
$supportHy2 = true;
if ($version) {
$supportHy2 = collect($supportedClientVersions)
->contains(function ($minVersion, $client) use ($flag, $version) {
return stripos($flag, $client) !== false && $this->versionCompare($version, $minVersion);
});
}
$user = $request->user; $user = $request->user;
// account not expired and is not banned. // account not expired and is not banned.
$userService = new UserService(); $userService = new UserService();
if ($userService->isAvailable($user)) { if ($userService->isAvailable($user)) {
// 获取IP地址信息 // get ip location
$ip2region = new \Ip2Region(); $ip2region = new \Ip2Region();
$geo = filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? $ip2region->memorySearch($ip) : []; $region = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? ($ip2region->memorySearch($ip)['region'] ?? null) : null;
$region = $geo['region'] ?? null; // get available servers
// 获取服务器列表
$servers = ServerService::getAvailableServers($user); $servers = ServerService::getAvailableServers($user);
// filter servers
// 判断不满足,不满足的直接过滤掉 $serversFiltered = $this->serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2);
$serversFiltered = collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2){
// 过滤类型
if($typesArr){
// 默认过滤掉hysteria2 线路
if($server['type'] == "hysteria" && $server['version'] == 2 && !in_array('hysteria2', $typesArr)
&& !$supportHy2
){
return true;
}
if(!in_array($server['type'], $typesArr) && !($server['type'] == "hysteria" && $server['version'] == 2 && in_array('hysteria2', $typesArr))) return true;
}
// 过滤关键词
if($filterArr){
$rejectFlag = true;
foreach($filterArr as $filter){
if(stripos($server['name'],$filter) !== false
|| in_array($filter, $server['tags'] ?? [])
) $rejectFlag = false;
}
if($rejectFlag) return true;
}
// 过滤地区
if(strpos($region, '中国') !== false){
$excludes = $server['excludes'];
if(blank($excludes)) return false;
foreach($excludes as $v){
$excludeList = explode("|",str_replace(["",","," ",""],"|",$v));
$rejectFlag = false;
foreach($excludeList as $needle){
if(stripos($region, $needle) !== false){
return true;
}
}
};
}
})->values()->all();
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered)); $this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
$servers = $serversFiltered; $servers = $serversFiltered;
$this->addPrefixToServerName($servers);
// 线路名称增加协议类型
if (admin_setting('show_protocol_to_server_enable')){
$typePrefixes = [
'hysteria' => [1 => '[Hy]', 2 => '[Hy2]'],
'vless' => '[vless]',
'shadowsocks' => '[ss]',
'vmess' => '[vmess]',
'trojan' => '[trojan]',
];
$servers = collect($servers)->map(function($server)use ($typePrefixes){
if (isset($typePrefixes[$server['type']])) {
// 如果是 hysteria 类型,根据版本选择前缀
$prefix = is_array($typePrefixes[$server['type']]) ? $typePrefixes[$server['type']][$server['version']] : $typePrefixes[$server['type']];
// 设置服务器名称
$server['name'] = $prefix . $server['name'];
}
return $server;
})->toArray();
}
if ($flag) { if ($flag) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) { foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$file = 'App\\Protocols\\' . basename($file, '.php'); $file = 'App\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers); $class = new $file($user, $servers);
$classFlags = explode(',', $class->flag); $classFlags = explode(',', $class->flag);
$isMatch = function() use ($classFlags, $flag){ foreach ($classFlags as $classFlag) {
foreach ($classFlags as $classFlag){ if (stripos($flag, $classFlag) !== false) {
if(stripos($flag, $classFlag) !== false) return true; return $class->handle();
} }
return false;
};
// 判断是否匹配
if ($isMatch()) {
return $class->handle();
} }
} }
} }
@ -145,23 +74,98 @@ class ClientController extends Controller
return $class->handle(); return $class->handle();
} }
} }
/**
* Summary of serverFilter
* @param mixed $typesArr
* @param mixed $filterArr
* @param mixed $region
* @param mixed $supportHy2
* @return array
*/
private function serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2)
{
return collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2) {
if ($server['type'] == "hysteria" && $server['version'] == 2) {
if(!in_array('hysteria2', $typesArr)){
return true;
}elseif(false == $supportHy2){
return true;
}
}
if ($filterArr) {
foreach ($filterArr as $filter) {
if (stripos($server['name'], $filter) !== false || in_array($filter, $server['tags'] ?? [])) {
return false;
}
}
return true;
}
if (strpos($region, '中国') !== false) {
$excludes = $server['excludes'] ?? [];
if (empty($excludes)) {
return false;
}
foreach ($excludes as $v) {
$excludeList = explode("|", str_replace(["", ",", " ", ""], "|", $v));
foreach ($excludeList as $needle) {
if (stripos($region, $needle) !== false) {
return true;
}
}
}
}
})->values()->all();
}
/*
* add prefix to server name
*/
private function addPrefixToServerName(&$servers)
{
// 线路名称增加协议类型
if (admin_setting('show_protocol_to_server_enable')) {
$typePrefixes = [
'hysteria' => [1 => '[Hy]', 2 => '[Hy2]'],
'vless' => '[vless]',
'shadowsocks' => '[ss]',
'vmess' => '[vmess]',
'trojan' => '[trojan]',
];
$servers = collect($servers)->map(function ($server) use ($typePrefixes) {
if (isset($typePrefixes[$server['type']])) {
$prefix = is_array($typePrefixes[$server['type']]) ? $typePrefixes[$server['type']][$server['version']] : $typePrefixes[$server['type']];
$server['name'] = $prefix . $server['name'];
}
return $server;
})->toArray();
}
}
/**
* Summary of setSubscribeInfoToServers
* @param mixed $servers
* @param mixed $user
* @param mixed $rejectServerCount
* @return void
*/
private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0) private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0)
{ {
if (!isset($servers[0])) return; if (!isset($servers[0]))
if($rejectServerCount > 0){ return;
if ($rejectServerCount > 0) {
array_unshift($servers, array_merge($servers[0], [ array_unshift($servers, array_merge($servers[0], [
'name' => "去除{$rejectServerCount}条不合适线路", 'name' => "过滤掉{$rejectServerCount}线路",
])); ]));
} }
if (!(int)admin_setting('show_info_to_server_enable', 0)) return; if (!(int) admin_setting('show_info_to_server_enable', 0))
return;
$useTraffic = $user['u'] + $user['d']; $useTraffic = $user['u'] + $user['d'];
$totalTraffic = $user['transfer_enable']; $totalTraffic = $user['transfer_enable'];
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic); $remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效'; $expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
$userService = new UserService(); $userService = new UserService();
$resetDay = $userService->getResetDay($user); $resetDay = $userService->getResetDay($user);
// 筛选提示
array_unshift($servers, array_merge($servers[0], [ array_unshift($servers, array_merge($servers[0], [
'name' => "套餐到期:{$expiredDate}", 'name' => "套餐到期:{$expiredDate}",
])); ]));
@ -180,7 +184,8 @@ class ClientController extends Controller
* 判断版本号 * 判断版本号
*/ */
function versionCompare($version1, $version2) { function versionCompare($version1, $version2)
{
if (!preg_match('/^\d+(\.\d+){0,2}/', $version1) || !preg_match('/^\d+(\.\d+){0,2}/', $version2)) { if (!preg_match('/^\d+(\.\d+){0,2}/', $version1) || !preg_match('/^\d+(\.\d+){0,2}/', $version2)) {
return false; return false;
} }
@ -190,8 +195,8 @@ class ClientController extends Controller
$maxParts = max(count($v1Parts), count($v2Parts)); $maxParts = max(count($v1Parts), count($v2Parts));
for ($i = 0; $i < $maxParts; $i++) { for ($i = 0; $i < $maxParts; $i++) {
$part1 = isset($v1Parts[$i]) ? (int)$v1Parts[$i] : 0; $part1 = isset($v1Parts[$i]) ? (int) $v1Parts[$i] : 0;
$part2 = isset($v2Parts[$i]) ? (int)$v2Parts[$i] : 0; $part2 = isset($v2Parts[$i]) ? (int) $v2Parts[$i] : 0;
if ($part1 < $part2) { if ($part1 < $part2) {
return false; return false;

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\V1\Guest;
use App\Exceptions\ApiException; use App\Exceptions\ApiException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Order; use App\Models\Order;
use App\Models\Payment;
use App\Services\OrderService; use App\Services\OrderService;
use App\Services\PaymentService; use App\Services\PaymentService;
use App\Services\TelegramService; use App\Services\TelegramService;
@ -41,12 +42,22 @@ class PaymentController extends Controller
if (!$orderService->paid($callbackNo)) { if (!$orderService->paid($callbackNo)) {
return false; return false;
} }
$payment = Payment::where('id', $order->payment_id)->first();
$telegramService = new TelegramService(); $telegramService = new TelegramService();
$message = sprintf( $message = sprintf(
"💰成功收款%s元\n———————————————\n订单号:%s", "💰成功收款%s元\n" .
"———————————————\n" .
"支付接口:%s\n" .
"支付渠道:%s\n" .
"本站订单:`%s`"
,
$order->total_amount / 100, $order->total_amount / 100,
$payment->payment,
$payment->name,
$order->trade_no $order->trade_no
); );
$telegramService->sendMessageWithAdmin($message); $telegramService->sendMessageWithAdmin($message);
return true; return true;
} }

View File

@ -25,7 +25,7 @@ class KnowledgeController extends Controller
if (!$userService->isAvailable($user)) { if (!$userService->isAvailable($user)) {
$this->formatAccessData($knowledge['body']); $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('{{siteName}}', admin_setting('app_name', 'XBoard'), $knowledge['body']);
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']); $knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']); $knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);

View File

@ -13,7 +13,7 @@ class StatController extends Controller
{ {
public function getTrafficLog(Request $request) public function getTrafficLog(Request $request)
{ {
$startDate = now()->startOfMonth(); $startDate = now()->startOfMonth()->timestamp;
$records = StatUser::query() $records = StatUser::query()
->where('user_id', $request->user['id']) ->where('user_id', $request->user['id'])
->where('record_at', '>=', $startDate) ->where('record_at', '>=', $startDate)

View File

@ -140,7 +140,7 @@ class UserController extends Controller
return $this->fail([400, __('Subscription plan does not exist')]); 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(); $userService = new UserService();
$user['reset_day'] = $userService->getResetDay($user); $user['reset_day'] = $userService->getResetDay($user);
return $this->success($user); return $this->success($user);
@ -157,7 +157,7 @@ class UserController extends Controller
if (!$user->save()) { if (!$user->save()) {
return $this->fail([400, __('Reset failed')]); 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) public function update(UserUpdate $request)

View File

@ -26,6 +26,7 @@ class Server
$request->validate([ $request->validate([
'token' => [ 'token' => [
"string", "string",
"required",
function ($attribute, $value, $fail) { function ($attribute, $value, $fail) {
if ($value !== admin_setting('server_token')) { if ($value !== admin_setting('server_token')) {
$fail('The ' . $attribute . ' is invalid.'); $fail('The ' . $attribute . ' is invalid.');
@ -34,10 +35,11 @@ class Server
], ],
'node_id' => 'required', 'node_id' => 'required',
'node_type' => [ 'node_type' => [
'required',
'nullable', 'nullable',
'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/', 'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/',
function ($attribute, $value, $fail) use ($aliasTypes, $request) { 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)]);
}, },
] ]
], [ ], [

View File

@ -12,7 +12,7 @@ class ClientRoute
'middleware' => 'client' 'middleware' => 'client'
], function ($router) { ], function ($router) {
// Client // Client
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe'); $router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe');
// App // App
$router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig'); $router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig');
$router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion'); $router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion');

View File

@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -17,7 +18,7 @@ class BatchTrafficFetchJob implements ShouldQueue
protected $protocol; protected $protocol;
protected $timestamp; protected $timestamp;
public $tries = 1; public $tries = 1;
public $timeout = 10; public $timeout = 20;
/** /**
* Create a new job instance. * Create a new job instance.
@ -36,34 +37,16 @@ class BatchTrafficFetchJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
// 获取子节点
$targetServer = $this->childServer ?? $this->server; $targetServer = $this->childServer ?? $this->server;
foreach ($this->data as $uid => $v) { foreach ($this->data as $uid => $v) {
$u = $v[0]; User::where('id', $uid)
$d = $v[1]; ->incrementEach(
$result = \DB::transaction(function () use ($uid, $u, $d, $targetServer) { [
$user = \DB::table('v2_user')->lockForUpdate()->where('id', $uid)->first(); 'u' => $v[0] * $targetServer['rate'],
if (!$user) { 'd' => $v[1] * $targetServer['rate'],
return true; ],
} ['t' => time()]
$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);
}
} }
} }
} }

View File

@ -14,6 +14,7 @@ class OrderHandleJob implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $order; protected $order;
protected $tradeNo;
public $tries = 3; public $tries = 3;
public $timeout = 5; public $timeout = 5;
@ -25,9 +26,7 @@ class OrderHandleJob implements ShouldQueue
public function __construct($tradeNo) public function __construct($tradeNo)
{ {
$this->onQueue('order_handle'); $this->onQueue('order_handle');
$this->order = Order::where('trade_no', $tradeNo) $this->tradeNo = $tradeNo;
->lockForUpdate()
->first();
} }
/** /**
@ -37,12 +36,15 @@ class OrderHandleJob implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
if (!$this->order) return; $order = Order::where('trade_no', $this->tradeNo)
$orderService = new OrderService($this->order); ->lockForUpdate()
switch ($this->order->status) { ->first();
if (!$order) return;
$orderService = new OrderService($order);
switch ($order->status) {
// cancel // cancel
case 0: case 0:
if ($this->order->created_at <= (time() - 3600 * 2)) { if ($order->created_at <= (time() - 3600 * 2)) {
$orderService->cancel(); $orderService->cancel();
} }
break; break;

View File

@ -27,7 +27,12 @@ class EPay
'label' => 'KEY', 'label' => 'KEY',
'description' => '', 'description' => '',
'type' => 'input', 'type' => 'input',
] ],
'type' => [
'label' => 'TYPE',
'description' => 'alipay / qqpay / wxpay',
'type' => 'input',
],
]; ];
} }
@ -41,6 +46,9 @@ class EPay
'out_trade_no' => $order['trade_no'], 'out_trade_no' => $order['trade_no'],
'pid' => $this->config['pid'] 'pid' => $this->config['pid']
]; ];
if(optional($this->config)['type']){
$params['type'] = $this->config['type'];
}
ksort($params); ksort($params);
reset($params); reset($params);
$str = stripslashes(urldecode(http_build_query($params))) . $this->config['key']; $str = stripslashes(urldecode(http_build_query($params))) . $this->config['key'];

View File

@ -2,6 +2,7 @@
namespace App\Protocols; namespace App\Protocols;
use App\Utils\Helper;
use phpDocumentor\Reflection\Types\Self_; use phpDocumentor\Reflection\Types\Self_;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
@ -32,11 +33,6 @@ class Clash
$proxy = []; $proxy = [];
$proxies = []; $proxies = [];
// 增加不支持提示
// array_push($proxy, [ "name" => "您的客户端不支持", "type" => "vmess", "server" => "1.1.1.1", "port" => 80, "uuid" => "aaaaaaaa-bbbb-cccc-cccc-dddddddddddd", "alterId" => 0, "cipher" => "auto", "udp" => false, "tls" => false]);
// array_push($proxies, "您的客户端不支持");
// array_push($proxy, [ "name" => "请使用clash Meta内核的客户端", "type" => "vmess", "server" => "1.1.1.1", "port" => 80, "uuid" => "aaaaaaaa-bbbb-cccc-cccc-dddddddddddd", "alterId" => 0, "cipher" => "auto", "udp" => false, "tls" => false]);
// array_push($proxies, "请使用clash Meta内核的客户端");
foreach ($servers as $item) { foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks' if ($item['type'] === 'shadowsocks'
@ -83,11 +79,9 @@ class Clash
return $group['proxies']; return $group['proxies'];
}); });
$config['proxy-groups'] = array_values($config['proxy-groups']); $config['proxy-groups'] = array_values($config['proxy-groups']);
// Force the current subscription domain to be a direct rule
$subsDomain = request()->header('Host'); $config = $this->buildRules($config);
if ($subsDomain) {
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
}
$yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
$yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml); $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
@ -98,6 +92,27 @@ class Clash
->header('profile-web-page-url', admin_setting('app_url')); ->header('profile-web-page-url', admin_setting('app_url'));
} }
/**
* Build the rules for Clash.
*/
public function buildRules($config)
{
// Force the current subscription domain to be a direct rule
$subsDomain = request()->header('Host');
if ($subsDomain) {
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
}
// Force the nodes ip to be a direct rule
collect($this->servers)->pluck('host')->map(function($host){
$host = trim($host);
return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host);
})->flatten()->unique()->each(function($nodeIP) use ( &$config ) {
array_unshift($config['rules'], "IP-CIDR,{$nodeIP}/32,DIRECT,no-resolve");
});
return $config;
}
public static function buildShadowsocks($uuid, $server) public static function buildShadowsocks($uuid, $server)
{ {
$array = []; $array = [];

View File

@ -81,11 +81,7 @@ class ClashMeta
return $group['proxies']; return $group['proxies'];
}); });
$config['proxy-groups'] = array_values($config['proxy-groups']); $config['proxy-groups'] = array_values($config['proxy-groups']);
// Force the current subscription domain to be a direct rule $config = $this->buildRules($config);
$subsDomain = request()->header('Host');
if ($subsDomain) {
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
}
$yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
$yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml); $yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
@ -95,6 +91,27 @@ class ClashMeta
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName)); ->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
} }
/**
* Build the rules for Clash.
*/
public function buildRules($config)
{
// Force the current subscription domain to be a direct rule
$subsDomain = request()->header('Host');
if ($subsDomain) {
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
}
// Force the nodes ip to be a direct rule
collect($this->servers)->pluck('host')->map(function($host){
$host = trim($host);
return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host);
})->flatten()->unique()->each(function($nodeIP) use ( &$config ) {
array_unshift($config['rules'], "IP-CIDR,{$nodeIP}/32,DIRECT,no-resolve");
});
return $config;
}
public static function buildShadowsocks($password, $server) public static function buildShadowsocks($password, $server)
{ {
$array = []; $array = [];

View File

@ -36,6 +36,9 @@ class General
if ($item['type'] === 'trojan') { if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item); $uri .= self::buildTrojan($user['uuid'], $item);
} }
if ($item['type'] === 'hysteria') {
$uri .= self::buildHysteria($user['uuid'], $item);
}
} }
return base64_encode($uri); return base64_encode($uri);
} }
@ -174,4 +177,33 @@ class General
return $uri; 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;
}
} }

View File

@ -151,7 +151,7 @@ class Loon
$server['host'], $server['host'],
$server['port'], $server['port'],
$password, $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"; if ($server['insecure']) $config[] = "skip-cert-verify=true";
$config[] = "download-bandwidth=" . ($user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps']); $config[] = "download-bandwidth=" . ($user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps']);

View File

@ -34,6 +34,9 @@ class Passwall
if ($item['type'] === 'trojan') { if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item); $uri .= self::buildTrojan($user['uuid'], $item);
} }
if ($item['type'] === 'hysteria') {
$uri .= General::buildHysteria($user['uuid'], $item);
}
} }
return base64_encode($uri); return base64_encode($uri);
} }

View File

@ -34,6 +34,9 @@ class SSRPlus
if ($item['type'] === 'trojan') { if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item); $uri .= self::buildTrojan($user['uuid'], $item);
} }
if ($item['type'] === 'hysteria') {
$uri .= General::buildHysteria($user['uuid'], $item);
}
} }
return base64_encode($uri); return base64_encode($uri);
} }

View File

@ -58,7 +58,11 @@ class Shadowrocket
['-', '_', ''], ['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}") base64_encode("{$server['cipher']}:{$password}")
); );
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n"; $uri = "ss://{$str}@{$server['host']}:{$server['port']}";
if ($server['obfs'] == 'http') {
$uri .= "?plugin=obfs-local;obfs=http;obfs-host={$server['obfs-host']};obfs-uri={$server['obfs-path']}";
}
return $uri."#{$name}\r\n";
} }
public static function buildVmess($uuid, $server) public static function buildVmess($uuid, $server)

View File

@ -21,12 +21,14 @@ class SingBox
$appName = admin_setting('app_name', 'XBoard'); $appName = admin_setting('app_name', 'XBoard');
$this->config = $this->loadConfig(); $this->config = $this->loadConfig();
$this->buildOutbounds(); $this->buildOutbounds();
$this->buildRule();
$user = $this->user; $user = $this->user;
return response($this->config, 200) 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('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
->header('profile-update-interval', '24') ->header('profile-update-interval', '24');
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
} }
protected function loadConfig() protected function loadConfig()
@ -75,6 +77,21 @@ class SingBox
return $outbounds; return $outbounds;
} }
/**
* Build rule
*/
protected function buildRule(){
$rules = $this->config['route']['rules'];
// Force the nodes ip to be a direct rule
array_unshift($rules, [
'ip_cidr' => collect($this->servers)->pluck('host')->map(function($host){
return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host);
})->flatten()->unique()->values(),
'outbound' => 'direct',
]);
$this->config['route']['rules'] = $rules;
}
protected function buildShadowsocks($password, $server) protected function buildShadowsocks($password, $server)
{ {
$array = []; $array = [];
@ -293,6 +310,8 @@ class SingBox
$array['tag'] = $server['name']; $array['tag'] = $server['name'];
$array['type'] = 'hysteria2'; $array['type'] = 'hysteria2';
$array['password'] = $password; $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']) { if ($server['is_obfs']) {
$array['obfs']['type'] = 'salamander'; $array['obfs']['type'] = 'salamander';

View File

@ -63,7 +63,7 @@ class Surfboard
} }
// Subscription link // Subscription link
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}"); $subsURL = Helper::getSubscribeUrl($user['token']);
$subsDomain = request()->header('Host'); $subsDomain = request()->header('Host');
$config = str_replace('$subs_link', $subsURL, $config); $config = str_replace('$subs_link', $subsURL, $config);

View File

@ -69,9 +69,8 @@ class Surge
} }
// Subscription link // Subscription link
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
$subsDomain = request()->header('Host'); $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_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config); $config = str_replace('$subs_domain', $subsDomain, $config);

View File

@ -37,7 +37,7 @@ class V2rayN
$uri .= self::buildTrojan($user['uuid'], $item); $uri .= self::buildTrojan($user['uuid'], $item);
} }
if ($item['type'] === 'hysteria') { if ($item['type'] === 'hysteria') {
$uri .= self::buildHysteria($user['uuid'], $item); $uri .= General::buildHysteria($user['uuid'], $item);
} }
} }
@ -196,25 +196,5 @@ class V2rayN
return $uri; 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;
}
} }

View File

@ -34,6 +34,9 @@ class V2rayNG
if ($item['type'] === 'vless') { if ($item['type'] === 'vless') {
$uri .= self::buildVless($user['uuid'], $item); $uri .= self::buildVless($user['uuid'], $item);
} }
if ($item['type'] === 'hysteria') {
$uri .= General::buildHysteria($user['uuid'], $item);
}
} }
return base64_encode($uri); return base64_encode($uri);
} }
@ -46,7 +49,11 @@ class V2rayNG
['-', '_', ''], ['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}") base64_encode("{$server['cipher']}:{$password}")
); );
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n"; $uri = "ss://{$str}@{$server['host']}:{$server['port']}";
if ($server['obfs'] == 'http') {
$uri .= "?plugin=obfs-local;obfs=http;obfs-host={$server['obfs-host']};path={$server['obfs-path']}";
}
return $uri."#{$name}\r\n";
} }
public static function buildVmess($uuid, $server) public static function buildVmess($uuid, $server)
@ -190,5 +197,4 @@ class V2rayNG
return $uri; return $uri;
} }
} }

View File

@ -48,7 +48,7 @@ class PaymentService
return $this->payment->pay([ return $this->payment->pay([
'notify_url' => $notifyUrl, 'notify_url' => $notifyUrl,
'return_url' => admin_setting('app_url') . '/#/order/' . $order['trade_no'], 'return_url' => url('/#/order/' . $order['trade_no']),
'trade_no' => $order['trade_no'], 'trade_no' => $order['trade_no'],
'total_amount' => $order['total_amount'], 'total_amount' => $order['total_amount'],
'user_id' => $order['user_id'], 'user_id' => $order['user_id'],

View File

@ -161,6 +161,11 @@ class ServerService
$userKey = Helper::uuidToBase64($user['uuid'], $config['userKeySize']); $userKey = Helper::uuidToBase64($user['uuid'], $config['userKeySize']);
$shadowsocks[$key]['password'] = "{$serverKey}:{$userKey}"; $shadowsocks[$key]['password'] = "{$serverKey}:{$userKey}";
} }
if ($v['obfs'] === 'http') {
$shadowsocks[$key]['obfs'] = 'http';
$shadowsocks[$key]['obfs-host'] = $v['obfs_settings']['host'];
$shadowsocks[$key]['obfs-path'] = $v['obfs_settings']['path'];
}
$servers[] = $shadowsocks[$key]->toArray(); $servers[] = $shadowsocks[$key]->toArray();
} }
return $servers; return $servers;
@ -191,7 +196,7 @@ class ServerService
// 获取可用的用户列表 // 获取可用的用户列表
public static function getAvailableUsers($groupId): Collection public static function getAvailableUsers($groupId): Collection
{ {
return \DB::table('v2_user') return User::toBase()
->whereIn('group_id', $groupId) ->whereIn('group_id', $groupId)
->whereRaw('u + d < transfer_enable') ->whereRaw('u + d < transfer_enable')
->where(function ($query) { ->where(function ($query) {
@ -309,9 +314,11 @@ class ServerService
$servers[$k]['online'] = Cache::get(CacheKey::get("SERVER_{$serverType}_ONLINE_USER", $v['parent_id'] ?? $v['id'])) ?? 0; $servers[$k]['online'] = Cache::get(CacheKey::get("SERVER_{$serverType}_ONLINE_USER", $v['parent_id'] ?? $v['id'])) ?? 0;
// 如果是子节点,先尝试从缓存中获取 // 如果是子节点,先尝试从缓存中获取
if($pid = $v['parent_id']){ if($pid = $v['parent_id']){
// 获取缓存 $cacheKey = CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid);
$onlineUsers = Cache::get(CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid)) ?? []; $onlineUsers = Cache::get($cacheKey) ?? [];
$servers[$k]['online'] = (collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user')) . "|{$servers[$k]['online']}"; $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_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'])); $servers[$k]['last_push_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_PUSH_AT", $v['parent_id'] ?? $v['id']));

View File

@ -108,14 +108,14 @@ class StatisticalService
/** /**
* 获取指定用户的流量使用情况 * 获取指定用户的流量使用情况
*/ */
public function getStatUserByUserID($userId): array public function getStatUserByUserID(int|string $userId): array
{ {
$stats = []; $stats = [];
$statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true); $statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true);
foreach ($statsUser as $member => $value) { foreach ($statsUser as $member => $value) {
list($rate, $uid, $type) = explode('_', $member); list($rate, $uid, $type) = explode('_', $member);
if ($uid !== $userId) if (intval($uid) !== intval($userId))
continue; continue;
$key = "{$rate}_{$uid}"; $key = "{$rate}_{$uid}";
$stats[$key] = $stats[$key] ?? [ $stats[$key] = $stats[$key] ?? [

View File

@ -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')); $path = route('client.subscribe', ['token' => $token], false);
$subscribeUrl = $subscribeUrls[array_rand($subscribeUrls)]; if(!$subscribeUrl){
$subscribeUrl = self::replaceRandomNumber($subscribeUrl); $subscribeUrls = explode(',', admin_setting('subscribe_url'));
if ($subscribeUrl) return $subscribeUrl . $path; $subscribeUrl = \Arr::random($subscribeUrls);
return url($path); $subscribeUrl = self::replaceByPattern($subscribeUrl);
}
return $subscribeUrl ? rtrim($subscribeUrl, '/') . $path : url($path);
} }
public static function randomPort($range) { public static function randomPort($range) {
@ -129,25 +131,34 @@ class Helper
} }
/** /**
* 替换字符串中的 [num1-num2] 格式为介于 num1 num2 之间的随机数字 * 根据规则替换域名中对应的字符串
* *
* @param string $input 用户输入的字符串 * @param string $input 用户输入的字符串
* @return string 替换后的字符串 * @return string 替换后的字符串
*/ */
public static function replaceRandomNumber($input) { public static function replaceByPattern($input)
// 匹配 [1-4999] 格式的正则表达式 {
$pattern = '/\[(\d+)-(\d+)\]/'; $patterns = [
'/\[(\d+)-(\d+)\]/' => function ($matches) {
$min = intval($matches[1]);
$max = intval($matches[2]);
if ($min > $max) {
list($min, $max) = [$max, $min];
}
$randomNumber = rand($min, $max);
return $randomNumber;
},
'/\[uuid\]/' => function () {
return self::guid(true);
}
];
foreach ($patterns as $pattern => $callback) {
$input = preg_replace_callback($pattern, $callback, $input);
}
return $input;
}
// 使用 preg_replace_callback 替换匹配到的内容 public static function getIpByDomainName($domain) {
$result = preg_replace_callback($pattern, function ($matches) { return gethostbynamel($domain) ?: [];
// 提取最小和最大值
$min = intval($matches[1]);
$max = intval($matches[2]);
// 生成随机数
$randomNumber = rand($min, $max);
return $randomNumber;
}, $input);
return $result;
} }
} }

View File

@ -21,7 +21,7 @@
"guzzlehttp/guzzle": "^7.4.3", "guzzlehttp/guzzle": "^7.4.3",
"hhxsv5/laravel-s": "~3.7.0", "hhxsv5/laravel-s": "~3.7.0",
"joanhey/adapterman": "^0.6.1", "joanhey/adapterman": "^0.6.1",
"laravel/framework": "^10.0", "laravel/framework": "10.48.22",
"laravel/horizon": "^5.9.6", "laravel/horizon": "^5.9.6",
"laravel/tinker": "^2.5", "laravel/tinker": "^2.5",
"linfo/linfo": "^4.0", "linfo/linfo": "^4.0",

View File

@ -106,6 +106,8 @@ docker compose up -d
🎉: 到这里,你已经可以通过域名访问你的站点了。 🎉: 到这里,你已经可以通过域名访问你的站点了。
⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。
## 更新 ## 更新
1. 通过 SSH 登录到服务器后,访问站点路径如:`/opt/1panel/apps/openresty/openresty/www/sites/xboard/index`,然后在站点目录中执行以下命令: 1. 通过 SSH 登录到服务器后,访问站点路径如:`/opt/1panel/apps/openresty/openresty/www/sites/xboard/index`,然后在站点目录中执行以下命令:

View File

@ -6,6 +6,7 @@
``` ```
# 安装Docker # 安装Docker
curl -sSL https://get.docker.com | bash curl -sSL https://get.docker.com | bash
# Centos系统可能还需要执行下面命令来启动Docker
systemctl enable docker systemctl enable docker
systemctl start docker systemctl start docker
``` ```
@ -34,7 +35,7 @@ URL=https://www.aapanel.com/script/install_6.0_en.sh && if [ -f /usr/bin/curl ];
``` ```
# 删除目录下文件 # 删除目录下文件
chattr -i .user.ini chattr -i .user.ini
rm -rf .htaccess 404.html index.html .user.ini rm -rf .htaccess 404.html 502.html index.html .user.ini
``` ```
> 执行命令从 Github 克隆到当前目录。 > 执行命令从 Github 克隆到当前目录。
``` ```
@ -81,6 +82,8 @@ location ^~ / {
🎉: 到这里,你可以已经可以通过域名访问你的站点了 🎉: 到这里,你可以已经可以通过域名访问你的站点了
⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。
### 更新 ### 更新
1. 更新代码 1. 更新代码
>通过SSH登录到服务器后访问站点路径如/www/wwwroot/你的站点域名。 >通过SSH登录到服务器后访问站点路径如/www/wwwroot/你的站点域名。

View File

@ -44,7 +44,7 @@ URL=https://www.aapanel.com/script/install_6.0_en.sh && if [ -f /usr/bin/curl ];
``` ```
# 删除目录下文件 # 删除目录下文件
chattr -i .user.ini chattr -i .user.ini
rm -rf .htaccess 404.html index.html .user.ini rm -rf .htaccess 404.html 502.html index.html .user.ini
``` ```
> 执行命令从 Github 克隆到当前目录。 > 执行命令从 Github 克隆到当前目录。
``` ```

View File

@ -7,6 +7,9 @@
1. 安装docker 1. 安装docker
``` ```
curl -sSL https://get.docker.com | bash curl -sSL https://get.docker.com | bash
```
Centos系统可能需要执行下面命令来启动Docker。
```
systemctl enable docker systemctl enable docker
systemctl start docker systemctl start docker
``` ```
@ -18,6 +21,10 @@ cd Xboard
3. 执行数据库安装命令 3. 执行数据库安装命令
> 选择 **启用sqlite** 和 **Docker内置的Redis** > 选择 **启用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 docker compose run -it --rm xboard php artisan xboard:install
``` ```
> 执行这条命令之后,会返回你的后台地址和管理员账号密码(你需要记录下来) > 执行这条命令之后,会返回你的后台地址和管理员账号密码(你需要记录下来)

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -5,11 +5,6 @@
"outbound": ["any"], "outbound": ["any"],
"server": "local" "server": "local"
}, },
{
"disable_cache": false,
"geosite": ["category-ads-all"],
"server": "block"
},
{ {
"clash_mode": "global", "clash_mode": "global",
"server": "remote" "server": "remote"
@ -19,7 +14,7 @@
"server": "local" "server": "local"
}, },
{ {
"geosite": "cn", "rule_set": ["geosite-cn"],
"server": "local" "server": "local"
} }
], ],
@ -42,9 +37,11 @@
"strategy": "prefer_ipv4" "strategy": "prefer_ipv4"
}, },
"experimental": { "experimental": {
"clash_api": { "cache_file": {
"external_controller": "127.0.0.1:9090", "enabled": true,
"secret": "" "path": "cache.db",
"cache_id": "cache_db",
"store_fakeip": true
} }
}, },
"inbounds": [ "inbounds": [
@ -101,10 +98,6 @@
"route": { "route": {
"auto_detect_interface": true, "auto_detect_interface": true,
"rules": [ "rules": [
{
"geosite": "category-ads-all",
"outbound": "block"
},
{ {
"outbound": "dns-out", "outbound": "dns-out",
"protocol": "dns" "protocol": "dns"
@ -118,13 +111,29 @@
"outbound": "节点选择" "outbound": "节点选择"
}, },
{ {
"geoip": ["cn", "private"], "ip_is_private": true,
"outbound": "direct" "outbound": "direct"
}, },
{ {
"geosite": "cn", "rule_set": ["geosite-cn", "geoip-cn"],
"outbound": "direct" "outbound": "direct"
} }
],
"rule_set": [
{
"tag": "geosite-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "自动选择"
},
{
"tag": "geoip-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
"download_detour": "自动选择"
}
] ]
} }
} }