mirror of
https://github.com/cedar2025/Xboard.git
synced 2025-01-22 10:38:14 -05:00
Merge branch 'cedar2025:dev' into adapted-allowInsecure
This commit is contained in:
commit
2fdffab3c6
@ -1,3 +1,7 @@
|
||||
unixsocket /run/redis-socket/redis.sock
|
||||
unixsocketperm 777
|
||||
port 0
|
||||
port 0
|
||||
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
|
14
.github/ISSUE_TEMPLATE/bug-report----问题反馈.md
vendored
14
.github/ISSUE_TEMPLATE/bug-report----问题反馈.md
vendored
@ -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天的日志)。
|
||||
--------
|
||||
|
5
.github/workflows/docker-publish.yml
vendored
5
.github/workflows/docker-publish.yml
vendored
@ -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
|
||||
@ -58,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
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
];
|
||||
}
|
||||
// 获取昨日节点流量排行
|
||||
|
@ -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;
|
||||
|
@ -8,136 +8,65 @@ 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
|
||||
{
|
||||
|
||||
// 支持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)
|
||||
{
|
||||
// 节点类型筛选
|
||||
$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 = [
|
||||
'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);
|
||||
});
|
||||
}
|
||||
// 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;
|
||||
foreach ($classFlags as $classFlag) {
|
||||
if (stripos($flag, $classFlag) !== false) {
|
||||
return $class->handle();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// 判断是否匹配
|
||||
if ($isMatch()) {
|
||||
return $class->handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,23 +74,98 @@ class ClientController extends Controller
|
||||
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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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']);
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)]);
|
||||
},
|
||||
]
|
||||
], [
|
||||
|
@ -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');
|
||||
|
@ -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()]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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'];
|
||||
|
@ -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 = [];
|
||||
|
@ -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 = [];
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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']);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
@ -283,4 +287,4 @@ class Shadowrocket
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
@ -302,4 +321,4 @@ class SingBox
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -45,10 +45,10 @@ class PaymentService
|
||||
$parseUrl = parse_url($notifyUrl);
|
||||
$notifyUrl = $this->config['notify_domain'] . $parseUrl['path'];
|
||||
}
|
||||
|
||||
|
||||
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'],
|
||||
|
@ -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']));
|
||||
|
@ -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] ?? [
|
||||
|
@ -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::replaceRandomNumber($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) {
|
||||
@ -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) {
|
||||
// 提取最小和最大值
|
||||
$min = intval($matches[1]);
|
||||
$max = intval($matches[2]);
|
||||
// 生成随机数
|
||||
$randomNumber = rand($min, $max);
|
||||
return $randomNumber;
|
||||
}, $input);
|
||||
|
||||
return $result;
|
||||
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;
|
||||
},
|
||||
'/\[uuid\]/' => function () {
|
||||
return self::guid(true);
|
||||
}
|
||||
];
|
||||
foreach ($patterns as $pattern => $callback) {
|
||||
$input = preg_replace_callback($pattern, $callback, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function getIpByDomainName($domain) {
|
||||
return gethostbynamel($domain) ?: [];
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -106,6 +106,8 @@ docker compose up -d
|
||||
|
||||
🎉: 到这里,你已经可以通过域名访问你的站点了。
|
||||
|
||||
⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。
|
||||
|
||||
## 更新
|
||||
|
||||
1. 通过 SSH 登录到服务器后,访问站点路径如:`/opt/1panel/apps/openresty/openresty/www/sites/xboard/index`,然后在站点目录中执行以下命令:
|
||||
|
@ -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 克隆到当前目录。
|
||||
```
|
||||
@ -79,7 +80,9 @@ location ^~ / {
|
||||
}
|
||||
```
|
||||
|
||||
🎉: 到这里,你可以已经可以通过域名访问你的站点了
|
||||
🎉: 到这里,你可以已经可以通过域名访问你的站点了
|
||||
|
||||
⚠️: 请务必开启防火墙防止7001端口暴露到公网当中。
|
||||
|
||||
### 更新
|
||||
1. 更新代码
|
||||
@ -96,4 +99,4 @@ docker compose restart
|
||||
🎉: 在此你已完成Xboard的更新
|
||||
|
||||
### 注意
|
||||
启用webman后做的任何代码修改都需要重启生效
|
||||
启用webman后做的任何代码修改都需要重启生效
|
||||
|
@ -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 克隆到当前目录。
|
||||
```
|
||||
|
@ -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后做的任何代码修改都需要重启生效
|
||||
启用webman后做的任何代码修改都需要重启生效
|
||||
|
498
public/theme/Xboard/assets/umi.js
vendored
498
public/theme/Xboard/assets/umi.js
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@ -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": "自动选择"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user