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
unixsocketperm 777
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
当前使用的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)
storage/logs 目录下最新的日志文件从 #1 开始报告(请做脱敏处理)
Run the php artisan log:export 7 command to export log files (where 7 represents logs for the last 7 days).
运行`php artisan log:export 7` 命令导出的日志文件(其中7为最近7天的日志)。
--------

View File

@ -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
@ -32,6 +33,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

View File

@ -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,

View File

@ -126,7 +126,7 @@ class StatController extends Controller
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
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]['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;

View File

@ -8,30 +8,12 @@ use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ClientController extends Controller
{
public function subscribe(Request $request)
{
// 节点类型筛选
$allowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'hysteria2', 'shadowsocks'];
$types = $request->input('types', "vmess|vless|trojan|hysteria|shadowsocks");
if ($types === "all") $types = implode('|', $allowedTypes);
$typesArr = $types ? collect(explode('|', str_replace(['|','',','], "|" , $types)))->reject(function($type) use ($allowedTypes){
return !in_array($type, $allowedTypes);
})->values()->all() : [];
// 节点关键词筛选字段获取
$filterArr = (mb_strlen($request->input('filter')) > 20) ? null : explode("|" ,str_replace(['|','',','], "|" , $request->input('filter')));
$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 = [
// 支持hy2 的客户端版本列表
const SupportedHy2ClientVersions = [
'NekoBox' => '1.2.7',
'sing-box' => '1.5.0',
'stash' => '2.5.0',
@ -42,126 +24,148 @@ class ClientController extends Controller
'ClashX Meta' => '1.3.5',
'Hiddify' => '0.1.0',
'loon' => '637',
'v2rayng' => '1.9.5',
'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);
});
}
// allowed types
const AllowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
public function subscribe(Request $request)
{
// filter types
$types = $request->input('types', 'all');
$typesArr = $types === 'all' ? self::AllowedTypes : array_values(array_intersect(explode('|', str_replace(['|', '', ','], "|", $types)), self::AllowedTypes));
// filter keyword
$filterArr = mb_strlen($filter = $request->input('filter')) > 20 ? null : explode("|", str_replace(['|', '', ','], "|", $filter));
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
$ip = $request->input('ip', $request->ip());
// get client version
$version = preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches) ? $matches[1] : null;
$supportHy2 = $version ? collect(self::SupportedHy2ClientVersions)
->contains(fn($minVersion, $client) => stripos($flag, $client) !== false && $this->versionCompare($version, $minVersion)) : true;
$user = $request->user;
// account not expired and is not banned.
$userService = new UserService();
if ($userService->isAvailable($user)) {
// 获取IP地址信息
// get ip location
$ip2region = new \Ip2Region();
$geo = filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? $ip2region->memorySearch($ip) : [];
$region = $geo['region'] ?? null;
// 获取服务器列表
$region = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? ($ip2region->memorySearch($ip)['region'] ?? null) : null;
// get available servers
$servers = ServerService::getAvailableServers($user);
// 判断不满足,不满足的直接过滤掉
$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();
// filter servers
$serversFiltered = $this->serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2);
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
$servers = $serversFiltered;
// 线路名称增加协议类型
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();
}
$this->addPrefixToServerName($servers);
if ($flag) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$file = 'App\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers);
$classFlags = explode(',', $class->flag);
$isMatch = function() use ($classFlags, $flag){
foreach ($classFlags as $classFlag){
if(stripos($flag, $classFlag) !== false) return true;
}
return false;
};
// 判断是否匹配
if ($isMatch()) {
foreach ($classFlags as $classFlag) {
if (stripos($flag, $classFlag) !== false) {
return $class->handle();
}
}
}
}
$class = new General($user, $servers);
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)
{
if (!isset($servers[0])) return;
if($rejectServerCount > 0){
if (!isset($servers[0]))
return;
if ($rejectServerCount > 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'];
$totalTraffic = $user['transfer_enable'];
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
$userService = new UserService();
$resetDay = $userService->getResetDay($user);
// 筛选提示
array_unshift($servers, array_merge($servers[0], [
'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)) {
return false;
}
@ -190,8 +195,8 @@ class ClientController extends Controller
$maxParts = max(count($v1Parts), count($v2Parts));
for ($i = 0; $i < $maxParts; $i++) {
$part1 = isset($v1Parts[$i]) ? (int)$v1Parts[$i] : 0;
$part2 = isset($v2Parts[$i]) ? (int)$v2Parts[$i] : 0;
$part1 = isset($v1Parts[$i]) ? (int) $v1Parts[$i] : 0;
$part2 = isset($v2Parts[$i]) ? (int) $v2Parts[$i] : 0;
if ($part1 < $part2) {
return false;

View File

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

View File

@ -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']);

View File

@ -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)

View File

@ -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)

View File

@ -26,6 +26,7 @@ class Server
$request->validate([
'token' => [
"string",
"required",
function ($attribute, $value, $fail) {
if ($value !== admin_setting('server_token')) {
$fail('The ' . $attribute . ' is invalid.');
@ -34,10 +35,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)]);
},
]
], [

View File

@ -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');

View File

@ -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()]
);
}
}
}

View File

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

View File

@ -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'];

View File

@ -2,6 +2,7 @@
namespace App\Protocols;
use App\Utils\Helper;
use phpDocumentor\Reflection\Types\Self_;
use Symfony\Component\Yaml\Yaml;
@ -32,11 +33,6 @@ class Clash
$proxy = [];
$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) {
if ($item['type'] === 'shadowsocks'
@ -83,11 +79,9 @@ class Clash
return $group['proxies'];
});
$config['proxy-groups'] = array_values($config['proxy-groups']);
// Force the current subscription domain to be a direct rule
$subsDomain = request()->header('Host');
if ($subsDomain) {
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
}
$config = $this->buildRules($config);
$yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
$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'));
}
/**
* 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)
{
$array = [];

View File

@ -81,11 +81,7 @@ class ClashMeta
return $group['proxies'];
});
$config['proxy-groups'] = array_values($config['proxy-groups']);
// Force the current subscription domain to be a direct rule
$subsDomain = request()->header('Host');
if ($subsDomain) {
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
}
$config = $this->buildRules($config);
$yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
$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));
}
/**
* 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)
{
$array = [];

View File

@ -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);
}
@ -174,4 +177,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;
}
}

View File

@ -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']);

View File

@ -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);
}

View File

@ -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);
}

View File

@ -58,7 +58,11 @@ class Shadowrocket
['-', '_', ''],
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)

View File

@ -21,12 +21,14 @@ class SingBox
$appName = admin_setting('app_name', 'XBoard');
$this->config = $this->loadConfig();
$this->buildOutbounds();
$this->buildRule();
$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('profile-update-interval', '24')
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
->header('profile-update-interval', '24');
}
protected function loadConfig()
@ -75,6 +77,21 @@ class SingBox
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)
{
$array = [];
@ -293,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';

View File

@ -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);

View File

@ -69,9 +69,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);

View File

@ -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);
}
}
@ -196,25 +196,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;
}
}

View File

@ -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);
}
@ -46,7 +49,11 @@ class V2rayNG
['-', '_', ''],
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)
@ -190,5 +197,4 @@ class V2rayNG
return $uri;
}
}

View File

@ -48,7 +48,7 @@ class PaymentService
return $this->payment->pay([
'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'],
'total_amount' => $order['total_amount'],
'user_id' => $order['user_id'],

View File

@ -161,6 +161,11 @@ class ServerService
$userKey = Helper::uuidToBase64($user['uuid'], $config['userKeySize']);
$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();
}
return $servers;
@ -191,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) {
@ -309,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']));

View File

@ -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] ?? [

View File

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

View File

@ -21,7 +21,7 @@
"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",

View File

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

View File

@ -6,6 +6,7 @@
```
# 安装Docker
curl -sSL https://get.docker.com | bash
# Centos系统可能还需要执行下面命令来启动Docker
systemctl enable 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
rm -rf .htaccess 404.html index.html .user.ini
rm -rf .htaccess 404.html 502.html index.html .user.ini
```
> 执行命令从 Github 克隆到当前目录。
```
@ -81,6 +82,8 @@ location ^~ / {
🎉: 到这里,你可以已经可以通过域名访问你的站点了
⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。
### 更新
1. 更新代码
>通过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
rm -rf .htaccess 404.html index.html .user.ini
rm -rf .htaccess 404.html 502.html index.html .user.ini
```
> 执行命令从 Github 克隆到当前目录。
```

View File

@ -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
```
> 执行这条命令之后,会返回你的后台地址和管理员账号密码(你需要记录下来)

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"],
"server": "local"
},
{
"disable_cache": false,
"geosite": ["category-ads-all"],
"server": "block"
},
{
"clash_mode": "global",
"server": "remote"
@ -19,7 +14,7 @@
"server": "local"
},
{
"geosite": "cn",
"rule_set": ["geosite-cn"],
"server": "local"
}
],
@ -42,9 +37,11 @@
"strategy": "prefer_ipv4"
},
"experimental": {
"clash_api": {
"external_controller": "127.0.0.1:9090",
"secret": ""
"cache_file": {
"enabled": true,
"path": "cache.db",
"cache_id": "cache_db",
"store_fakeip": true
}
},
"inbounds": [
@ -101,10 +98,6 @@
"route": {
"auto_detect_interface": true,
"rules": [
{
"geosite": "category-ads-all",
"outbound": "block"
},
{
"outbound": "dns-out",
"protocol": "dns"
@ -118,13 +111,29 @@
"outbound": "节点选择"
},
{
"geoip": ["cn", "private"],
"ip_is_private": true,
"outbound": "direct"
},
{
"geosite": "cn",
"rule_set": ["geosite-cn", "geoip-cn"],
"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": "自动选择"
}
]
}
}