merge: sync upstream/master from cedar2025/Xboard
合并上游 cedar2025/Xboard 的 master,并按交互决策保留本地改动。
This commit is contained in:
@@ -61,4 +61,21 @@ stopwaitsecs=3
|
|||||||
stopsignal=TERM
|
stopsignal=TERM
|
||||||
stopasgroup=true
|
stopasgroup=true
|
||||||
killasgroup=true
|
killasgroup=true
|
||||||
priority=300
|
priority=300
|
||||||
|
|
||||||
|
[program:ws-server]
|
||||||
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
|
command=php /www/artisan ws-server start
|
||||||
|
autostart=%(ENV_ENABLE_WS_SERVER)s
|
||||||
|
autorestart=true
|
||||||
|
user=www
|
||||||
|
redirect_stderr=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stdout_logfile_backups=0
|
||||||
|
numprocs=1
|
||||||
|
stopwaitsecs=5
|
||||||
|
stopsignal=SIGINT
|
||||||
|
stopasgroup=true
|
||||||
|
killasgroup=true
|
||||||
|
priority=400
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
APP_NAME=XBoard
|
APP_NAME=XBoard
|
||||||
APP_ENV=local
|
APP_ENV=production
|
||||||
APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60=
|
APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60=
|
||||||
APP_DEBUG=false
|
APP_DEBUG=false
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=sha,format=long
|
type=sha,format=short,prefix=,enable=true
|
||||||
type=raw,value=new,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=new,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=${{ steps.get_version.outputs.version }}
|
type=raw,value=${{ steps.get_version.outputs.version }}
|
||||||
@@ -98,12 +98,3 @@ jobs:
|
|||||||
allow: |
|
allow: |
|
||||||
network.host
|
network.host
|
||||||
|
|
||||||
- name: Install cosign
|
|
||||||
uses: sigstore/cosign-installer@v3.4.0
|
|
||||||
with:
|
|
||||||
cosign-release: 'v2.2.2'
|
|
||||||
|
|
||||||
- name: Sign image
|
|
||||||
if: steps.build-and-push.outputs.digest != ''
|
|
||||||
run: |
|
|
||||||
echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes "{}@${{ steps.build-and-push.outputs.digest }}"
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "public/assets/admin"]
|
||||||
|
path = public/assets/admin
|
||||||
|
url = https://github.com/cedar2025/xboard-admin-dist.git
|
||||||
+6
-3
@@ -25,12 +25,14 @@ RUN echo "Attempting to clone branch: ${BRANCH_NAME} from ${REPO_URL} with CACHE
|
|||||||
rm -rf ./* && \
|
rm -rf ./* && \
|
||||||
rm -rf .git && \
|
rm -rf .git && \
|
||||||
git config --global --add safe.directory /www && \
|
git config --global --add safe.directory /www && \
|
||||||
git clone --depth 1 --branch ${BRANCH_NAME} ${REPO_URL} .
|
git clone --depth 1 --branch ${BRANCH_NAME} ${REPO_URL} . && \
|
||||||
|
git submodule update --init --recursive --force
|
||||||
|
|
||||||
COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
RUN composer install --no-cache --no-dev \
|
RUN composer install --no-cache --no-dev \
|
||||||
&& php artisan storage:link \
|
&& php artisan storage:link \
|
||||||
|
&& cp -r plugins/ /opt/default-plugins/ \
|
||||||
&& chown -R www:www /www \
|
&& chown -R www:www /www \
|
||||||
&& chmod -R 775 /www \
|
&& chmod -R 775 /www \
|
||||||
&& mkdir -p /data \
|
&& mkdir -p /data \
|
||||||
@@ -38,7 +40,8 @@ RUN composer install --no-cache --no-dev \
|
|||||||
|
|
||||||
ENV ENABLE_WEB=true \
|
ENV ENABLE_WEB=true \
|
||||||
ENABLE_HORIZON=true \
|
ENABLE_HORIZON=true \
|
||||||
ENABLE_REDIS=false
|
ENABLE_REDIS=false \
|
||||||
|
ENABLE_WS_SERVER=false
|
||||||
|
|
||||||
EXPOSE 7001
|
EXPOSE 7001
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ class CheckCommission extends Command
|
|||||||
$commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
|
$commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
|
||||||
if (!$commissionBalance) continue;
|
if (!$commissionBalance) continue;
|
||||||
if ((int)admin_setting('withdraw_close_enable', 0)) {
|
if ((int)admin_setting('withdraw_close_enable', 0)) {
|
||||||
$inviter->balance = $inviter->balance + $commissionBalance;
|
$inviter->increment('balance', $commissionBalance);
|
||||||
} else {
|
} else {
|
||||||
$inviter->commission_balance = $inviter->commission_balance + $commissionBalance;
|
$inviter->increment('commission_balance', $commissionBalance);
|
||||||
}
|
}
|
||||||
if (!$inviter->save()) {
|
if (!$inviter->save()) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
|
|||||||
@@ -43,12 +43,11 @@ class CheckOrder extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
ini_set('memory_limit', -1);
|
Order::whereIn('status', [Order::STATUS_PENDING, Order::STATUS_PROCESSING])
|
||||||
$orders = Order::whereIn('status', [Order::STATUS_PENDING, Order::STATUS_PROCESSING])
|
|
||||||
->orderBy('created_at', 'ASC')
|
->orderBy('created_at', 'ASC')
|
||||||
->get();
|
->lazyById(200)
|
||||||
foreach ($orders as $order) {
|
->each(function ($order) {
|
||||||
OrderHandleJob::dispatch($order->trade_no);
|
OrderHandleJob::dispatch($order->trade_no);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,15 +38,14 @@ class CheckTicket extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
ini_set('memory_limit', -1);
|
Ticket::where('status', 0)
|
||||||
$tickets = Ticket::where('status', 0)
|
|
||||||
->where('updated_at', '<=', time() - 24 * 3600)
|
->where('updated_at', '<=', time() - 24 * 3600)
|
||||||
->where('reply_status', 0)
|
->where('reply_status', 0)
|
||||||
->get();
|
->lazyById(200)
|
||||||
foreach ($tickets as $ticket) {
|
->each(function ($ticket) {
|
||||||
if ($ticket->user_id === $ticket->last_reply_user_id) continue;
|
if ($ticket->user_id === $ticket->last_reply_user_id) return;
|
||||||
$ticket->status = Ticket::STATUS_CLOSED;
|
$ticket->status = Ticket::STATUS_CLOSED;
|
||||||
$ticket->save();
|
$ticket->save();
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class ExportV2Log extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'log:export {days=1 : The number of days to export logs for}';
|
|
||||||
protected $description = 'Export v2_log table records of the specified number of days to a file';
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$days = $this->argument('days');
|
|
||||||
$date = Carbon::now()->subDays((float) $days)->startOfDay();
|
|
||||||
|
|
||||||
$logs = DB::table('v2_log')
|
|
||||||
->where('created_at', '>=', $date->timestamp)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv";
|
|
||||||
$handle = fopen(storage_path("logs/$fileName"), 'w');
|
|
||||||
|
|
||||||
// 根据您的表结构
|
|
||||||
fputcsv($handle, ['Level', 'ID', 'Title', 'Host', 'URI', 'Method', 'Data', 'IP', 'Context', 'Created At', 'Updated At']);
|
|
||||||
|
|
||||||
foreach ($logs as $log) {
|
|
||||||
fputcsv($handle, [
|
|
||||||
$log->level,
|
|
||||||
$log->id,
|
|
||||||
$log->title,
|
|
||||||
$log->host,
|
|
||||||
$log->uri,
|
|
||||||
$log->method,
|
|
||||||
$log->data,
|
|
||||||
$log->ip,
|
|
||||||
$log->context,
|
|
||||||
Carbon::createFromTimestamp($log->created_at)->toDateTimeString(),
|
|
||||||
Carbon::createFromTimestamp($log->updated_at)->toDateTimeString()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose($handle);
|
|
||||||
$this->info("日志成功导出到: " . storage_path("logs/$fileName"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Services\NodeSyncService;
|
||||||
|
use App\Services\NodeRegistry;
|
||||||
|
use App\Services\ServerService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Workerman\Connection\TcpConnection;
|
||||||
|
use Workerman\Timer;
|
||||||
|
use Workerman\Worker;
|
||||||
|
|
||||||
|
class NodeWebSocketServer extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'ws-server
|
||||||
|
{action=start : start | stop | restart | reload | status}
|
||||||
|
{--d : Start in daemon mode}
|
||||||
|
{--host=0.0.0.0 : Listen address}
|
||||||
|
{--port=8076 : Listen port}';
|
||||||
|
|
||||||
|
protected $description = 'Start the WebSocket server for node-panel synchronization';
|
||||||
|
|
||||||
|
/** Auth timeout in seconds — close unauthenticated connections */
|
||||||
|
private const AUTH_TIMEOUT = 10;
|
||||||
|
|
||||||
|
/** Ping interval in seconds */
|
||||||
|
private const PING_INTERVAL = 55;
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
global $argv;
|
||||||
|
$action = $this->argument('action');
|
||||||
|
|
||||||
|
// 重新构建 argv 供 Workerman 解析
|
||||||
|
$argv[1] = $action;
|
||||||
|
if ($this->option('d')) {
|
||||||
|
$argv[2] = '-d';
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $this->option('host');
|
||||||
|
$port = $this->option('port');
|
||||||
|
|
||||||
|
$worker = new Worker("websocket://{$host}:{$port}");
|
||||||
|
$worker->count = 1;
|
||||||
|
$worker->name = 'xboard-ws-server';
|
||||||
|
|
||||||
|
// 设置日志和 PID 文件路径
|
||||||
|
$logPath = storage_path('logs');
|
||||||
|
if (!is_dir($logPath)) {
|
||||||
|
mkdir($logPath, 0777, true);
|
||||||
|
}
|
||||||
|
Worker::$logFile = $logPath . '/xboard-ws-server.log'; // 指向具体文件,避免某些环境 php://stdout 的 stat 失败
|
||||||
|
Worker::$pidFile = $logPath . '/xboard-ws-server.pid';
|
||||||
|
|
||||||
|
$worker->onWorkerStart = function (Worker $worker) {
|
||||||
|
$this->info("[WS] Worker started, pid={$worker->id}");
|
||||||
|
$this->subscribeRedis();
|
||||||
|
|
||||||
|
// Periodic ping to detect dead connections
|
||||||
|
Timer::add(self::PING_INTERVAL, function () {
|
||||||
|
foreach (NodeRegistry::getConnectedNodeIds() as $nodeId) {
|
||||||
|
$conn = NodeRegistry::get($nodeId);
|
||||||
|
if ($conn) {
|
||||||
|
$conn->send(json_encode(['event' => 'ping']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$worker->onConnect = function (TcpConnection $conn) {
|
||||||
|
// Set auth timeout — must authenticate within N seconds or get disconnected
|
||||||
|
$conn->authTimer = Timer::add(self::AUTH_TIMEOUT, function () use ($conn) {
|
||||||
|
if (empty($conn->nodeId)) {
|
||||||
|
$conn->close(json_encode([
|
||||||
|
'event' => 'error',
|
||||||
|
'data' => ['message' => 'auth timeout'],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}, [], false);
|
||||||
|
};
|
||||||
|
|
||||||
|
$worker->onWebSocketConnect = function (TcpConnection $conn, $httpMessage) {
|
||||||
|
// Parse query string from the WebSocket upgrade request
|
||||||
|
// In Workerman 4.x/5.x with onWebSocketConnect, the second arg can be a string or Request object
|
||||||
|
$queryString = '';
|
||||||
|
if (is_string($httpMessage)) {
|
||||||
|
$queryString = parse_url($httpMessage, PHP_URL_QUERY) ?? '';
|
||||||
|
} elseif ($httpMessage instanceof \Workerman\Protocols\Http\Request) {
|
||||||
|
$queryString = $httpMessage->queryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_str($queryString, $params);
|
||||||
|
|
||||||
|
$token = $params['token'] ?? '';
|
||||||
|
$nodeId = (int) ($params['node_id'] ?? 0);
|
||||||
|
|
||||||
|
// Authenticate
|
||||||
|
$serverToken = admin_setting('server_token', '');
|
||||||
|
if ($token === '' || $serverToken === '' || !hash_equals($serverToken, $token)) {
|
||||||
|
$conn->close(json_encode([
|
||||||
|
'event' => 'error',
|
||||||
|
'data' => ['message' => 'invalid token'],
|
||||||
|
]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = Server::find($nodeId);
|
||||||
|
if (!$node) {
|
||||||
|
$conn->close(json_encode([
|
||||||
|
'event' => 'error',
|
||||||
|
'data' => ['message' => 'node not found'],
|
||||||
|
]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth passed — cancel timeout, register connection
|
||||||
|
if (isset($conn->authTimer)) {
|
||||||
|
Timer::del($conn->authTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->nodeId = $nodeId;
|
||||||
|
NodeRegistry::add($nodeId, $conn);
|
||||||
|
Cache::put("node_ws_alive:{$nodeId}", true, 86400);
|
||||||
|
|
||||||
|
Log::debug("[WS] Node#{$nodeId} connected", [
|
||||||
|
'remote' => $conn->getRemoteIp(),
|
||||||
|
'total' => NodeRegistry::count(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Send auth success
|
||||||
|
$conn->send(json_encode([
|
||||||
|
'event' => 'auth.success',
|
||||||
|
'data' => ['node_id' => $nodeId],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Push full sync (config + users) immediately to this specific connection
|
||||||
|
$this->pushFullSync($conn, $node);
|
||||||
|
};
|
||||||
|
|
||||||
|
$worker->onMessage = function (TcpConnection $conn, $data) {
|
||||||
|
$msg = json_decode($data, true);
|
||||||
|
if (!is_array($msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event = $msg['event'] ?? '';
|
||||||
|
$nodeId = $conn->nodeId ?? null;
|
||||||
|
|
||||||
|
switch ($event) {
|
||||||
|
case 'pong':
|
||||||
|
// Heartbeat response — node is alive
|
||||||
|
if ($nodeId) {
|
||||||
|
Cache::put("node_ws_alive:{$nodeId}", true, 86400);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'node.status':
|
||||||
|
if ($nodeId && isset($msg['data'])) {
|
||||||
|
$this->handleNodeStatus($nodeId, $msg['data']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Future: handle other node-initiated messages if needed
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$worker->onClose = function (TcpConnection $conn) {
|
||||||
|
if (!empty($conn->nodeId)) {
|
||||||
|
$nodeId = $conn->nodeId;
|
||||||
|
NodeRegistry::remove($nodeId);
|
||||||
|
Cache::forget("node_ws_alive:{$nodeId}");
|
||||||
|
Log::debug("[WS] Node#{$nodeId} disconnected", [
|
||||||
|
'total' => NodeRegistry::count(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Worker::runAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle status data pushed from node via WebSocket
|
||||||
|
*/
|
||||||
|
private function handleNodeStatus(int $nodeId, array $data): void
|
||||||
|
{
|
||||||
|
$node = Server::find($nodeId);
|
||||||
|
if (!$node) return;
|
||||||
|
|
||||||
|
$nodeType = strtoupper($node->type);
|
||||||
|
|
||||||
|
// Update last check-in cache
|
||||||
|
Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600);
|
||||||
|
|
||||||
|
// Update metrics cache via Service
|
||||||
|
ServerService::updateMetrics($node, $data);
|
||||||
|
|
||||||
|
Log::debug("[WS] Node#{$nodeId} status updated via WebSocket");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to Redis pub/sub channel for receiving push commands from Laravel.
|
||||||
|
* Laravel app publishes to "node:push" channel, Workerman picks it up and forwards to the right node.
|
||||||
|
*/
|
||||||
|
private function subscribeRedis(): void
|
||||||
|
{
|
||||||
|
$host = config('database.redis.default.host', '127.0.0.1');
|
||||||
|
$port = config('database.redis.default.port', 6379);
|
||||||
|
|
||||||
|
// Handle Unix Socket connection
|
||||||
|
if (str_starts_with($host, '/')) {
|
||||||
|
$redisUri = "unix://{$host}";
|
||||||
|
} else {
|
||||||
|
$redisUri = "redis://{$host}:{$port}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$redis = new \Workerman\Redis\Client($redisUri);
|
||||||
|
|
||||||
|
$password = config('database.redis.default.password');
|
||||||
|
if ($password) {
|
||||||
|
$redis->auth($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Laravel Redis prefix to match publish()
|
||||||
|
$prefix = config('database.redis.options.prefix', '');
|
||||||
|
$channel = $prefix . 'node:push';
|
||||||
|
|
||||||
|
$redis->subscribe([$channel], function ($chan, $message) {
|
||||||
|
$payload = json_decode($message, true);
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nodeId = $payload['node_id'] ?? null;
|
||||||
|
$event = $payload['event'] ?? '';
|
||||||
|
$data = $payload['data'] ?? [];
|
||||||
|
|
||||||
|
if (!$nodeId || !$event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sent = NodeRegistry::send((int) $nodeId, $event, $data);
|
||||||
|
if ($sent) {
|
||||||
|
Log::debug("[WS] Pushed {$event} to node#{$nodeId}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->info("[WS] Subscribed to Redis channel: {$channel}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push full config + users to a newly connected node.
|
||||||
|
*/
|
||||||
|
private function pushFullSync(TcpConnection $conn, Server $node): void
|
||||||
|
{
|
||||||
|
$nodeId = $conn->nodeId;
|
||||||
|
// Push config
|
||||||
|
$config = ServerService::buildNodeConfig($node);
|
||||||
|
Log::debug("[WS] Node#{$nodeId} config: ", $config);
|
||||||
|
$conn->send(json_encode([
|
||||||
|
'event' => 'sync.config',
|
||||||
|
'data' => ['config' => $config]
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Push users
|
||||||
|
$users = ServerService::getAvailableUsers($node)->toArray();
|
||||||
|
$conn->send(json_encode([
|
||||||
|
'event' => 'sync.users',
|
||||||
|
'data' => ['users' => $users]
|
||||||
|
]));
|
||||||
|
|
||||||
|
Log::info("[WS] Full sync pushed to node#{$nodeId}", [
|
||||||
|
'users' => count($users),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Log;
|
use App\Models\AdminAuditLog;
|
||||||
use App\Models\StatServer;
|
use App\Models\StatServer;
|
||||||
use App\Models\StatUser;
|
use App\Models\StatUser;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
@@ -43,6 +43,6 @@ class ResetLog extends Command
|
|||||||
{
|
{
|
||||||
StatUser::where('record_at', '<', strtotime('-2 month', time()))->delete();
|
StatUser::where('record_at', '<', strtotime('-2 month', time()))->delete();
|
||||||
StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
|
StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
|
||||||
Log::where('created_at', '<', strtotime('-1 month', time()))->delete();
|
AdminAuditLog::where('created_at', '<', strtotime('-3 month', time()))->delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,9 +160,7 @@ class XboardInstall extends Command
|
|||||||
if (!self::registerAdmin($email, $password)) {
|
if (!self::registerAdmin($email, $password)) {
|
||||||
abort(500, '管理员账号注册失败,请重试');
|
abort(500, '管理员账号注册失败,请重试');
|
||||||
}
|
}
|
||||||
if (function_exists('exec')) {
|
self::restoreProtectedPlugins($this);
|
||||||
self::restoreProtectedPlugins($this);
|
|
||||||
}
|
|
||||||
$this->info('正在安装默认插件...');
|
$this->info('正在安装默认插件...');
|
||||||
PluginManager::installDefaultPlugins();
|
PluginManager::installDefaultPlugins();
|
||||||
$this->info('默认插件安装完成');
|
$this->info('默认插件安装完成');
|
||||||
@@ -369,61 +367,31 @@ class XboardInstall extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 还原内置受保护插件(可在安装和更新时调用)
|
* 还原内置受保护插件(可在安装和更新时调用)
|
||||||
|
* Docker 部署时 plugins/ 目录被外部挂载覆盖,需要从镜像备份中还原默认插件
|
||||||
*/
|
*/
|
||||||
public static function restoreProtectedPlugins(Command $console = null)
|
public static function restoreProtectedPlugins(Command $console = null)
|
||||||
{
|
{
|
||||||
exec("git config core.filemode false", $output, $returnVar);
|
$backupBase = '/opt/default-plugins';
|
||||||
$cmd = "git status --porcelain plugins/ 2>/dev/null";
|
$pluginsBase = base_path('plugins');
|
||||||
exec($cmd, $output, $returnVar);
|
|
||||||
if (!empty($output)) {
|
if (!File::isDirectory($backupBase)) {
|
||||||
$hasNonNewFiles = false;
|
$console?->info('非 Docker 环境或备份目录不存在,跳过插件还原。');
|
||||||
foreach ($output as $line) {
|
return;
|
||||||
$status = trim(substr($line, 0, 2));
|
}
|
||||||
if ($status !== 'A') {
|
|
||||||
$hasNonNewFiles = true;
|
foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) {
|
||||||
break;
|
$dirName = Str::studly($pluginCode);
|
||||||
}
|
$source = "{$backupBase}/{$dirName}";
|
||||||
|
$target = "{$pluginsBase}/{$dirName}";
|
||||||
|
|
||||||
|
if (!File::isDirectory($source)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if ($hasNonNewFiles) {
|
|
||||||
if ($console)
|
|
||||||
$console->info("检测到 plugins 目录有变更,正在还原...");
|
|
||||||
|
|
||||||
foreach ($output as $line) {
|
// 先清除旧文件再复制,避免重命名后残留旧文件
|
||||||
$status = trim(substr($line, 0, 2));
|
File::deleteDirectory($target);
|
||||||
$filePath = trim(substr($line, 3));
|
File::copyDirectory($source, $target);
|
||||||
|
$console?->info("已同步默认插件 [{$dirName}]");
|
||||||
if (strpos($filePath, 'plugins/') === 0 && $status !== 'A') {
|
|
||||||
$relativePath = substr($filePath, 8);
|
|
||||||
if ($console) {
|
|
||||||
$action = match ($status) {
|
|
||||||
'M' => '修改',
|
|
||||||
'D' => '删除',
|
|
||||||
'R' => '重命名',
|
|
||||||
'C' => '复制',
|
|
||||||
default => '变更'
|
|
||||||
};
|
|
||||||
$console->info("还原插件文件 [{$relativePath}] ({$action})");
|
|
||||||
}
|
|
||||||
|
|
||||||
$cmd = "git checkout HEAD -- {$filePath}";
|
|
||||||
exec($cmd, $gitOutput, $gitReturnVar);
|
|
||||||
|
|
||||||
if ($gitReturnVar === 0) {
|
|
||||||
if ($console)
|
|
||||||
$console->info("插件文件 [{$relativePath}] 已还原。");
|
|
||||||
} else {
|
|
||||||
if ($console)
|
|
||||||
$console->error("插件文件 [{$relativePath}] 还原失败。");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($console)
|
|
||||||
$console->info("plugins 目录状态正常,无需还原。");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($console)
|
|
||||||
$console->info("plugins 目录状态正常,无需还原。");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class XboardUpdate extends Command
|
|||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$this->info('正在导入数据库请稍等...');
|
$this->info('正在导入数据库请稍等...');
|
||||||
Artisan::call("migrate");
|
Artisan::call("migrate", ['--force' => true]);
|
||||||
$this->info(Artisan::output());
|
$this->info(Artisan::output());
|
||||||
$this->info('正在检查内置插件文件...');
|
$this->info('正在检查内置插件文件...');
|
||||||
XboardInstall::restoreProtectedPlugins($this);
|
XboardInstall::restoreProtectedPlugins($this);
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ class Kernel extends ConsoleKernel
|
|||||||
// v2board
|
// v2board
|
||||||
$schedule->command('xboard:statistics')->dailyAt('0:10')->onOneServer();
|
$schedule->command('xboard:statistics')->dailyAt('0:10')->onOneServer();
|
||||||
// check
|
// check
|
||||||
$schedule->command('check:order')->everyMinute()->onOneServer();
|
$schedule->command('check:order')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||||
$schedule->command('check:commission')->everyMinute()->onOneServer();
|
$schedule->command('check:commission')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||||
$schedule->command('check:ticket')->everyMinute()->onOneServer();
|
$schedule->command('check:ticket')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||||
// reset
|
// reset
|
||||||
$schedule->command('reset:traffic')->everyMinute()->onOneServer();
|
$schedule->command('reset:traffic')->everyMinute()->onOneServer()->withoutOverlapping(10);
|
||||||
$schedule->command('reset:log')->daily()->onOneServer();
|
$schedule->command('reset:log')->daily()->onOneServer();
|
||||||
// send
|
// send
|
||||||
$schedule->command('send:remindMail', ['--force'])->dailyAt('11:30')->onOneServer();
|
$schedule->command('send:remindMail', ['--force'])->dailyAt('11:30')->onOneServer();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
use App\Support\Setting;
|
use App\Support\Setting;
|
||||||
use Illuminate\Support\Facades\App;
|
|
||||||
|
|
||||||
if (!function_exists('admin_setting')) {
|
if (!function_exists('admin_setting')) {
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +27,16 @@ if (!function_exists('admin_setting')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('subscribe_template')) {
|
||||||
|
/**
|
||||||
|
* Get subscribe template content by protocol name.
|
||||||
|
*/
|
||||||
|
function subscribe_template(string $name): ?string
|
||||||
|
{
|
||||||
|
return \App\Models\SubscribeTemplate::getContent($name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!function_exists('admin_settings_batch')) {
|
if (!function_exists('admin_settings_batch')) {
|
||||||
/**
|
/**
|
||||||
* 批量获取配置参数,性能优化版本
|
* 批量获取配置参数,性能优化版本
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
namespace App\Http\Controllers\V1\Server;
|
namespace App\Http\Controllers\V1\Server;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Jobs\UpdateAliveDataJob;
|
use App\Jobs\UserAliveSyncJob;
|
||||||
|
use App\Services\NodeSyncService;
|
||||||
use App\Services\ServerService;
|
use App\Services\ServerService;
|
||||||
use App\Services\UserService;
|
use App\Services\UserService;
|
||||||
use App\Utils\CacheKey;
|
use App\Utils\CacheKey;
|
||||||
@@ -88,117 +89,13 @@ class UniProxyController extends Controller
|
|||||||
public function config(Request $request)
|
public function config(Request $request)
|
||||||
{
|
{
|
||||||
$node = $this->getNodeInfo($request);
|
$node = $this->getNodeInfo($request);
|
||||||
$nodeType = $node->type;
|
$response = ServerService::buildNodeConfig($node);
|
||||||
$protocolSettings = $node->protocol_settings;
|
|
||||||
|
|
||||||
$serverPort = $node->server_port;
|
|
||||||
$host = $node->host;
|
|
||||||
|
|
||||||
$baseConfig = [
|
|
||||||
'protocol' => $nodeType,
|
|
||||||
'listen_ip' => '0.0.0.0',
|
|
||||||
'server_port' => (int) $serverPort,
|
|
||||||
'network' => data_get($protocolSettings, 'network'),
|
|
||||||
'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
|
|
||||||
];
|
|
||||||
|
|
||||||
$response = match ($nodeType) {
|
|
||||||
'shadowsocks' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'cipher' => $protocolSettings['cipher'],
|
|
||||||
'plugin' => $protocolSettings['plugin'],
|
|
||||||
'plugin_opts' => $protocolSettings['plugin_opts'],
|
|
||||||
'server_key' => match ($protocolSettings['cipher']) {
|
|
||||||
'2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
|
|
||||||
'2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
|
|
||||||
default => null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'vmess' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'tls' => (int) $protocolSettings['tls']
|
|
||||||
],
|
|
||||||
'trojan' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'host' => $host,
|
|
||||||
'server_name' => $protocolSettings['server_name'],
|
|
||||||
],
|
|
||||||
'vless' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'tls' => (int) $protocolSettings['tls'],
|
|
||||||
'flow' => $protocolSettings['flow'],
|
|
||||||
'tls_settings' =>
|
|
||||||
match ((int) $protocolSettings['tls']) {
|
|
||||||
2 => $protocolSettings['reality_settings'],
|
|
||||||
default => $protocolSettings['tls_settings']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'hysteria' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'server_port' => (int) $serverPort,
|
|
||||||
'version' => (int) $protocolSettings['version'],
|
|
||||||
'host' => $host,
|
|
||||||
'server_name' => $protocolSettings['tls']['server_name'],
|
|
||||||
'up_mbps' => (int) $protocolSettings['bandwidth']['up'],
|
|
||||||
'down_mbps' => (int) $protocolSettings['bandwidth']['down'],
|
|
||||||
...match ((int) $protocolSettings['version']) {
|
|
||||||
1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null],
|
|
||||||
2 => [
|
|
||||||
'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null,
|
|
||||||
'obfs-password' => $protocolSettings['obfs']['password'] ?? null
|
|
||||||
],
|
|
||||||
default => []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'tuic' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'version' => (int) $protocolSettings['version'],
|
|
||||||
'server_port' => (int) $serverPort,
|
|
||||||
'server_name' => $protocolSettings['tls']['server_name'],
|
|
||||||
'congestion_control' => $protocolSettings['congestion_control'],
|
|
||||||
'auth_timeout' => '3s',
|
|
||||||
'zero_rtt_handshake' => false,
|
|
||||||
'heartbeat' => "3s",
|
|
||||||
],
|
|
||||||
'anytls' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'server_port' => (int) $serverPort,
|
|
||||||
'server_name' => $protocolSettings['tls']['server_name'],
|
|
||||||
'padding_scheme' => $protocolSettings['padding_scheme'],
|
|
||||||
],
|
|
||||||
'socks' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'server_port' => (int) $serverPort,
|
|
||||||
],
|
|
||||||
'naive' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'server_port' => (int) $serverPort,
|
|
||||||
'tls' => (int) $protocolSettings['tls'],
|
|
||||||
'tls_settings' => $protocolSettings['tls_settings']
|
|
||||||
],
|
|
||||||
'http' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'server_port' => (int) $serverPort,
|
|
||||||
'tls' => (int) $protocolSettings['tls'],
|
|
||||||
'tls_settings' => $protocolSettings['tls_settings']
|
|
||||||
],
|
|
||||||
'mieru' => [
|
|
||||||
...$baseConfig,
|
|
||||||
'server_port' => (string) $serverPort,
|
|
||||||
'protocol' => (int) $protocolSettings['protocol'],
|
|
||||||
],
|
|
||||||
default => []
|
|
||||||
};
|
|
||||||
|
|
||||||
$response['base_config'] = [
|
$response['base_config'] = [
|
||||||
'push_interval' => (int) admin_setting('server_push_interval', 60),
|
'push_interval' => (int) admin_setting('server_push_interval', 60),
|
||||||
'pull_interval' => (int) admin_setting('server_pull_interval', 60)
|
'pull_interval' => (int) admin_setting('server_pull_interval', 60)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!empty($node['route_ids'])) {
|
|
||||||
$response['routes'] = ServerService::getRoutes($node['route_ids']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$eTag = sha1(json_encode($response));
|
$eTag = sha1(json_encode($response));
|
||||||
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
|
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
|
||||||
return response(null, 304);
|
return response(null, 304);
|
||||||
@@ -226,7 +123,7 @@ class UniProxyController extends Controller
|
|||||||
'error' => 'Invalid online data'
|
'error' => 'Invalid online data'
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
UpdateAliveDataJob::dispatch($data, $node->type, $node->id);
|
UserAliveSyncJob::dispatch($data, $node->type, $node->id);
|
||||||
return response()->json(['data' => true]);
|
return response()->json(['data' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use App\Services\PaymentService;
|
|||||||
use App\Services\PlanService;
|
use App\Services\PlanService;
|
||||||
use App\Services\UserService;
|
use App\Services\UserService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class OrderController extends Controller
|
class OrderController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class TicketController extends Controller
|
|||||||
if ($ticket->status) {
|
if ($ticket->status) {
|
||||||
return $this->fail([400, __('The ticket is closed and cannot be replied')]);
|
return $this->fail([400, __('The ticket is closed and cannot be replied')]);
|
||||||
}
|
}
|
||||||
if ($request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
|
if ((int) admin_setting('ticket_must_wait_reply', 0) && $request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
|
||||||
return $this->fail(codeResponse: [400, __('Please wait for the technical enginneer to reply')]);
|
return $this->fail(codeResponse: [400, __('Please wait for the technical enginneer to reply')]);
|
||||||
}
|
}
|
||||||
$ticketService = new TicketService();
|
$ticketService = new TicketService();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use App\Utils\CacheKey;
|
|||||||
use App\Utils\Helper;
|
use App\Utils\Helper;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
@@ -31,20 +32,14 @@ class UserController extends Controller
|
|||||||
|
|
||||||
public function getActiveSession(Request $request)
|
public function getActiveSession(Request $request)
|
||||||
{
|
{
|
||||||
$user = User::find($request->user()->id);
|
$user = $request->user();
|
||||||
if (!$user) {
|
|
||||||
return $this->fail([400, __('The user does not exist')]);
|
|
||||||
}
|
|
||||||
$authService = new AuthService($user);
|
$authService = new AuthService($user);
|
||||||
return $this->success($authService->getSessions());
|
return $this->success($authService->getSessions());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeActiveSession(Request $request)
|
public function removeActiveSession(Request $request)
|
||||||
{
|
{
|
||||||
$user = User::find($request->user()->id);
|
$user = $request->user();
|
||||||
if (!$user) {
|
|
||||||
return $this->fail([400, __('The user does not exist')]);
|
|
||||||
}
|
|
||||||
$authService = new AuthService($user);
|
$authService = new AuthService($user);
|
||||||
return $this->success($authService->removeSession($request->input('session_id')));
|
return $this->success($authService->removeSession($request->input('session_id')));
|
||||||
}
|
}
|
||||||
@@ -62,10 +57,7 @@ class UserController extends Controller
|
|||||||
|
|
||||||
public function changePassword(UserChangePassword $request)
|
public function changePassword(UserChangePassword $request)
|
||||||
{
|
{
|
||||||
$user = User::find($request->user()->id);
|
$user = $request->user();
|
||||||
if (!$user) {
|
|
||||||
return $this->fail([400, __('The user does not exist')]);
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
!Helper::multiPasswordVerify(
|
!Helper::multiPasswordVerify(
|
||||||
$user->password_algo,
|
$user->password_algo,
|
||||||
@@ -163,10 +155,7 @@ class UserController extends Controller
|
|||||||
|
|
||||||
public function resetSecurity(Request $request)
|
public function resetSecurity(Request $request)
|
||||||
{
|
{
|
||||||
$user = User::find($request->user()->id);
|
$user = $request->user();
|
||||||
if (!$user) {
|
|
||||||
return $this->fail([400, __('The user does not exist')]);
|
|
||||||
}
|
|
||||||
$user->uuid = Helper::guid(true);
|
$user->uuid = Helper::guid(true);
|
||||||
$user->token = Helper::guid();
|
$user->token = Helper::guid();
|
||||||
if (!$user->save()) {
|
if (!$user->save()) {
|
||||||
@@ -182,10 +171,7 @@ class UserController extends Controller
|
|||||||
'remind_traffic'
|
'remind_traffic'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::find($request->user()->id);
|
$user = $request->user();
|
||||||
if (!$user) {
|
|
||||||
return $this->fail([400, __('The user does not exist')]);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
$user->update($updateData);
|
$user->update($updateData);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@@ -197,27 +183,31 @@ class UserController extends Controller
|
|||||||
|
|
||||||
public function transfer(UserTransfer $request)
|
public function transfer(UserTransfer $request)
|
||||||
{
|
{
|
||||||
$user = User::find($request->user()->id);
|
$amount = $request->input('transfer_amount');
|
||||||
if (!$user) {
|
try {
|
||||||
return $this->fail([400, __('The user does not exist')]);
|
DB::transaction(function () use ($request, $amount) {
|
||||||
}
|
$user = User::lockForUpdate()->find($request->user()->id);
|
||||||
if ($request->input('transfer_amount') > $user->commission_balance) {
|
if (!$user) {
|
||||||
return $this->fail([400, __('Insufficient commission balance')]);
|
throw new \Exception(__('The user does not exist'));
|
||||||
}
|
}
|
||||||
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
|
if ($amount > $user->commission_balance) {
|
||||||
$user->balance = $user->balance + $request->input('transfer_amount');
|
throw new \Exception(__('Insufficient commission balance'));
|
||||||
if (!$user->save()) {
|
}
|
||||||
return $this->fail([400, __('Transfer failed')]);
|
$user->commission_balance -= $amount;
|
||||||
|
$user->balance += $amount;
|
||||||
|
if (!$user->save()) {
|
||||||
|
throw new \Exception(__('Transfer failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->fail([400, $e->getMessage()]);
|
||||||
}
|
}
|
||||||
return $this->success(true);
|
return $this->success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getQuickLoginUrl(Request $request)
|
public function getQuickLoginUrl(Request $request)
|
||||||
{
|
{
|
||||||
$user = User::find($request->user()->id);
|
$user = $request->user();
|
||||||
if (!$user) {
|
|
||||||
return $this->fail([400, __('The user does not exist')]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect'));
|
$url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect'));
|
||||||
return $this->success($url);
|
return $this->success($url);
|
||||||
|
|||||||
@@ -4,20 +4,12 @@ namespace App\Http\Controllers\V2\Admin;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\ConfigSave;
|
use App\Http\Requests\Admin\ConfigSave;
|
||||||
use App\Protocols\Clash;
|
use App\Models\SubscribeTemplate;
|
||||||
use App\Protocols\ClashMeta;
|
|
||||||
use App\Protocols\SingBox;
|
|
||||||
use App\Protocols\Stash;
|
|
||||||
use App\Protocols\Surfboard;
|
|
||||||
use App\Protocols\Surge;
|
|
||||||
use App\Services\MailService;
|
use App\Services\MailService;
|
||||||
use App\Services\TelegramService;
|
use App\Services\TelegramService;
|
||||||
use App\Services\ThemeService;
|
use App\Services\ThemeService;
|
||||||
use App\Utils\Dict;
|
use App\Utils\Dict;
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
|
|
||||||
class ConfigController extends Controller
|
class ConfigController extends Controller
|
||||||
{
|
{
|
||||||
@@ -57,31 +49,24 @@ class ConfigController extends Controller
|
|||||||
'data' => $mailLog,
|
'data' => $mailLog,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 获取规则模板内容
|
|
||||||
*
|
|
||||||
* @param string $file 文件路径
|
|
||||||
* @return string 文件内容
|
|
||||||
*/
|
|
||||||
private function getTemplateContent(string $file): string
|
|
||||||
{
|
|
||||||
$path = base_path($file);
|
|
||||||
return File::exists($path) ? File::get($path) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setTelegramWebhook(Request $request)
|
public function setTelegramWebhook(Request $request)
|
||||||
{
|
{
|
||||||
$app_url = admin_setting('app_url');
|
$hookUrl = $this->resolveTelegramWebhookUrl();
|
||||||
if (blank($app_url))
|
if (blank($hookUrl)) {
|
||||||
return $this->fail([422, '请先设置站点网址']);
|
return $this->fail([422, 'Telegram Webhook地址未配置']);
|
||||||
$hookUrl = $app_url . '/api/v1/guest/telegram/webhook?' . http_build_query([
|
}
|
||||||
|
$hookUrl .= '?' . http_build_query([
|
||||||
'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token')))
|
'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token')))
|
||||||
]);
|
]);
|
||||||
$telegramService = new TelegramService($request->input('telegram_bot_token'));
|
$telegramService = new TelegramService($request->input('telegram_bot_token'));
|
||||||
$telegramService->getMe();
|
$telegramService->getMe();
|
||||||
$telegramService->setWebhook($hookUrl);
|
$telegramService->setWebhook(url: $hookUrl);
|
||||||
$telegramService->registerBotCommands();
|
$telegramService->registerBotCommands();
|
||||||
return $this->success(true);
|
return $this->success([
|
||||||
|
'success' => true,
|
||||||
|
'webhook_url' => $hookUrl,
|
||||||
|
'webhook_base_url' => $this->getTelegramWebhookBaseUrl(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetch(Request $request)
|
public function fetch(Request $request)
|
||||||
@@ -131,6 +116,7 @@ class ConfigController extends Controller
|
|||||||
'tos_url' => admin_setting('tos_url'),
|
'tos_url' => admin_setting('tos_url'),
|
||||||
'currency' => admin_setting('currency', 'CNY'),
|
'currency' => admin_setting('currency', 'CNY'),
|
||||||
'currency_symbol' => admin_setting('currency_symbol', '¥'),
|
'currency_symbol' => admin_setting('currency_symbol', '¥'),
|
||||||
|
'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0),
|
||||||
],
|
],
|
||||||
'subscribe' => [
|
'subscribe' => [
|
||||||
'plan_change_enable' => (bool) admin_setting('plan_change_enable', 1),
|
'plan_change_enable' => (bool) admin_setting('plan_change_enable', 1),
|
||||||
@@ -157,6 +143,8 @@ class ConfigController extends Controller
|
|||||||
'server_pull_interval' => admin_setting('server_pull_interval', 60),
|
'server_pull_interval' => admin_setting('server_pull_interval', 60),
|
||||||
'server_push_interval' => admin_setting('server_push_interval', 60),
|
'server_push_interval' => admin_setting('server_push_interval', 60),
|
||||||
'device_limit_mode' => (int) admin_setting('device_limit_mode', 0),
|
'device_limit_mode' => (int) admin_setting('device_limit_mode', 0),
|
||||||
|
'server_ws_enable' => (bool) admin_setting('server_ws_enable', 1),
|
||||||
|
'server_ws_url' => admin_setting('server_ws_url', ''),
|
||||||
],
|
],
|
||||||
'email' => [
|
'email' => [
|
||||||
'email_template' => admin_setting('email_template', 'default'),
|
'email_template' => admin_setting('email_template', 'default'),
|
||||||
@@ -171,6 +159,7 @@ class ConfigController extends Controller
|
|||||||
'telegram' => [
|
'telegram' => [
|
||||||
'telegram_bot_enable' => (bool) admin_setting('telegram_bot_enable', 0),
|
'telegram_bot_enable' => (bool) admin_setting('telegram_bot_enable', 0),
|
||||||
'telegram_bot_token' => admin_setting('telegram_bot_token'),
|
'telegram_bot_token' => admin_setting('telegram_bot_token'),
|
||||||
|
'telegram_webhook_url' => admin_setting('telegram_webhook_url'),
|
||||||
'telegram_discuss_link' => admin_setting('telegram_discuss_link')
|
'telegram_discuss_link' => admin_setting('telegram_discuss_link')
|
||||||
],
|
],
|
||||||
'app' => [
|
'app' => [
|
||||||
@@ -208,14 +197,14 @@ class ConfigController extends Controller
|
|||||||
],
|
],
|
||||||
'subscribe_template' => [
|
'subscribe_template' => [
|
||||||
'subscribe_template_singbox' => $this->formatTemplateContent(
|
'subscribe_template_singbox' => $this->formatTemplateContent(
|
||||||
admin_setting('subscribe_template_singbox', $this->getDefaultTemplate('singbox')),
|
subscribe_template('singbox') ?? '',
|
||||||
'json'
|
'json'
|
||||||
),
|
),
|
||||||
'subscribe_template_clash' => admin_setting('subscribe_template_clash', $this->getDefaultTemplate('clash')),
|
'subscribe_template_clash' => subscribe_template('clash') ?? '',
|
||||||
'subscribe_template_clashmeta' => admin_setting('subscribe_template_clashmeta', $this->getDefaultTemplate('clashmeta')),
|
'subscribe_template_clashmeta' => subscribe_template('clashmeta') ?? '',
|
||||||
'subscribe_template_stash' => admin_setting('subscribe_template_stash', $this->getDefaultTemplate('stash')),
|
'subscribe_template_stash' => subscribe_template('stash') ?? '',
|
||||||
'subscribe_template_surge' => admin_setting('subscribe_template_surge', $this->getDefaultTemplate('surge')),
|
'subscribe_template_surge' => subscribe_template('surge') ?? '',
|
||||||
'subscribe_template_surfboard' => admin_setting('subscribe_template_surfboard', $this->getDefaultTemplate('surfboard'))
|
'subscribe_template_surfboard' => subscribe_template('surfboard') ?? ''
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -224,7 +213,20 @@ class ConfigController extends Controller
|
|||||||
{
|
{
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
|
|
||||||
|
$templateKeys = [
|
||||||
|
'subscribe_template_singbox' => 'singbox',
|
||||||
|
'subscribe_template_clash' => 'clash',
|
||||||
|
'subscribe_template_clashmeta' => 'clashmeta',
|
||||||
|
'subscribe_template_stash' => 'stash',
|
||||||
|
'subscribe_template_surge' => 'surge',
|
||||||
|
'subscribe_template_surfboard' => 'surfboard',
|
||||||
|
];
|
||||||
|
|
||||||
foreach ($data as $k => $v) {
|
foreach ($data as $k => $v) {
|
||||||
|
if (isset($templateKeys[$k])) {
|
||||||
|
SubscribeTemplate::setContent($templateKeys[$k], $v);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($k == 'frontend_theme') {
|
if ($k == 'frontend_theme') {
|
||||||
$themeService = app(ThemeService::class);
|
$themeService = app(ThemeService::class);
|
||||||
$themeService->switch($v);
|
$themeService->switch($v);
|
||||||
@@ -267,50 +269,32 @@ class ConfigController extends Controller
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getTelegramWebhookBaseUrl(): ?string
|
||||||
* 获取默认模板内容
|
|
||||||
*
|
|
||||||
* @param string $type 模板类型
|
|
||||||
* @return string 默认模板内容
|
|
||||||
*/
|
|
||||||
private function getDefaultTemplate(string $type): string
|
|
||||||
{
|
{
|
||||||
$fileMap = [
|
$customUrl = trim((string) admin_setting('telegram_webhook_url', ''));
|
||||||
'singbox' => [SingBox::CUSTOM_TEMPLATE_FILE, SingBox::DEFAULT_TEMPLATE_FILE],
|
if ($customUrl !== '') {
|
||||||
'clash' => [Clash::CUSTOM_TEMPLATE_FILE, Clash::DEFAULT_TEMPLATE_FILE],
|
return rtrim($customUrl, '/');
|
||||||
'clashmeta' => [
|
|
||||||
ClashMeta::CUSTOM_TEMPLATE_FILE,
|
|
||||||
ClashMeta::CUSTOM_CLASH_TEMPLATE_FILE,
|
|
||||||
ClashMeta::DEFAULT_TEMPLATE_FILE
|
|
||||||
],
|
|
||||||
'stash' => [
|
|
||||||
Stash::CUSTOM_TEMPLATE_FILE,
|
|
||||||
Stash::CUSTOM_CLASH_TEMPLATE_FILE,
|
|
||||||
Stash::DEFAULT_TEMPLATE_FILE
|
|
||||||
],
|
|
||||||
'surge' => [Surge::CUSTOM_TEMPLATE_FILE, Surge::DEFAULT_TEMPLATE_FILE],
|
|
||||||
'surfboard' => [Surfboard::CUSTOM_TEMPLATE_FILE, Surfboard::DEFAULT_TEMPLATE_FILE],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!isset($fileMap[$type])) {
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按优先级查找可用的模板文件
|
$appUrl = trim((string) admin_setting('app_url', ''));
|
||||||
foreach ($fileMap[$type] as $file) {
|
if ($appUrl !== '') {
|
||||||
$content = $this->getTemplateContent($file);
|
return rtrim($appUrl, '/');
|
||||||
if (!empty($content)) {
|
|
||||||
// 对于 SingBox,需要格式化 JSON
|
|
||||||
if ($type === 'singbox') {
|
|
||||||
$decoded = json_decode($content, true);
|
|
||||||
if (json_last_error() === JSON_ERROR_NONE) {
|
|
||||||
return json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTelegramWebhookUrl(): ?string
|
||||||
|
{
|
||||||
|
$baseUrl = $this->getTelegramWebhookBaseUrl();
|
||||||
|
if (!$baseUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($baseUrl, '/api/v1/guest/telegram/webhook')) {
|
||||||
|
return $baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $baseUrl . '/api/v1/guest/telegram/webhook';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,12 @@ class ManageController extends Controller
|
|||||||
'show' => 'integer',
|
'show' => 'integer',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!Server::where('id', $request->id)->update(['show' => $request->show])) {
|
$server = Server::find($request->id);
|
||||||
|
if (!$server) {
|
||||||
|
return $this->fail([400202, '服务器不存在']);
|
||||||
|
}
|
||||||
|
$server->show = (int) $request->show;
|
||||||
|
if (!$server->save()) {
|
||||||
return $this->fail([500, '保存失败']);
|
return $this->fail([500, '保存失败']);
|
||||||
}
|
}
|
||||||
return $this->success(true);
|
return $this->success(true);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class RouteController extends Controller
|
|||||||
$params = $request->validate([
|
$params = $request->validate([
|
||||||
'remarks' => 'required',
|
'remarks' => 'required',
|
||||||
'match' => 'required|array',
|
'match' => 'required|array',
|
||||||
'action' => 'required|in:block,dns',
|
'action' => 'required|in:block,direct,dns,proxy',
|
||||||
'action_value' => 'nullable'
|
'action_value' => 'nullable'
|
||||||
], [
|
], [
|
||||||
'remarks.required' => '备注不能为空',
|
'remarks.required' => '备注不能为空',
|
||||||
|
|||||||
@@ -536,19 +536,20 @@ class StatController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
|
$ids = $currentData->pluck('id');
|
||||||
|
$names = $type === 'node'
|
||||||
|
? Server::whereIn('id', $ids)->pluck('name', 'id')
|
||||||
|
: User::whereIn('id', $ids)->pluck('email', 'id');
|
||||||
|
|
||||||
foreach ($currentData as $data) {
|
foreach ($currentData as $data) {
|
||||||
$previousValue = isset($previousData[$data->id]) ? $previousData[$data->id]->value : 0;
|
$previousValue = isset($previousData[$data->id]) ? $previousData[$data->id]->value : 0;
|
||||||
$change = $previousValue > 0 ? round(($data->value - $previousValue) / $previousValue * 100, 1) : 0;
|
$change = $previousValue > 0 ? round(($data->value - $previousValue) / $previousValue * 100, 1) : 0;
|
||||||
|
|
||||||
$name = $type === 'node'
|
|
||||||
? optional(Server::find($data->id))->name ?? "Node {$data->id}"
|
|
||||||
: optional(User::find($data->id))->email ?? "User {$data->id}";
|
|
||||||
|
|
||||||
$result[] = [
|
$result[] = [
|
||||||
'id' => (string) $data->id,
|
'id' => (string) $data->id,
|
||||||
'name' => $name,
|
'name' => $names[$data->id] ?? ($type === 'node' ? "Node {$data->id}" : "User {$data->id}"),
|
||||||
'value' => $data->value, // Convert to GB
|
'value' => $data->value,
|
||||||
'previousValue' => $previousValue, // Convert to GB
|
'previousValue' => $previousValue,
|
||||||
'change' => $change,
|
'change' => $change,
|
||||||
'timestamp' => date('c', $endDate)
|
'timestamp' => date('c', $endDate)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\V2\Admin;
|
namespace App\Http\Controllers\V2\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Log as LogModel;
|
use App\Models\AdminAuditLog;
|
||||||
use App\Utils\CacheKey;
|
use App\Utils\CacheKey;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
@@ -23,37 +23,10 @@ class SystemController extends Controller
|
|||||||
'schedule' => $this->getScheduleStatus(),
|
'schedule' => $this->getScheduleStatus(),
|
||||||
'horizon' => $this->getHorizonStatus(),
|
'horizon' => $this->getHorizonStatus(),
|
||||||
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)),
|
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)),
|
||||||
'logs' => $this->getLogStatistics()
|
|
||||||
];
|
];
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志统计信息
|
|
||||||
*
|
|
||||||
* @return array 各级别日志的数量统计
|
|
||||||
*/
|
|
||||||
protected function getLogStatistics(): array
|
|
||||||
{
|
|
||||||
// 初始化日志统计数组
|
|
||||||
$statistics = [
|
|
||||||
'info' => 0,
|
|
||||||
'warning' => 0,
|
|
||||||
'error' => 0,
|
|
||||||
'total' => 0
|
|
||||||
];
|
|
||||||
|
|
||||||
if (class_exists(LogModel::class) && LogModel::count() > 0) {
|
|
||||||
$statistics['info'] = LogModel::where('level', 'INFO')->count();
|
|
||||||
$statistics['warning'] = LogModel::where('level', 'WARNING')->count();
|
|
||||||
$statistics['error'] = LogModel::where('level', 'ERROR')->count();
|
|
||||||
$statistics['total'] = LogModel::count();
|
|
||||||
|
|
||||||
return $statistics;
|
|
||||||
}
|
|
||||||
return $statistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getQueueWorkload(WorkloadRepository $workload)
|
public function getQueueWorkload(WorkloadRepository $workload)
|
||||||
{
|
{
|
||||||
return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
|
return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
|
||||||
@@ -125,34 +98,26 @@ class SystemController extends Controller
|
|||||||
})->count();
|
})->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSystemLog(Request $request)
|
public function getAuditLog(Request $request)
|
||||||
{
|
{
|
||||||
$current = $request->input('current') ? $request->input('current') : 1;
|
$current = max(1, (int) $request->input('current', 1));
|
||||||
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
|
$pageSize = max(10, (int) $request->input('page_size', 10));
|
||||||
$level = $request->input('level');
|
|
||||||
$keyword = $request->input('keyword');
|
|
||||||
|
|
||||||
$builder = LogModel::orderBy('created_at', 'DESC')
|
$builder = AdminAuditLog::with('admin:id,email')
|
||||||
->when($level, function ($query) use ($level) {
|
->orderBy('id', 'DESC')
|
||||||
return $query->where('level', strtoupper($level));
|
->when($request->input('action'), fn($q, $v) => $q->where('action', $v))
|
||||||
})
|
->when($request->input('admin_id'), fn($q, $v) => $q->where('admin_id', $v))
|
||||||
->when($keyword, function ($query) use ($keyword) {
|
->when($request->input('keyword'), function ($q, $keyword) {
|
||||||
return $query->where(function ($q) use ($keyword) {
|
$q->where(function ($q) use ($keyword) {
|
||||||
$q->where('data', 'like', '%' . $keyword . '%')
|
$q->where('uri', 'like', '%' . $keyword . '%')
|
||||||
->orWhere('context', 'like', '%' . $keyword . '%')
|
->orWhere('request_data', 'like', '%' . $keyword . '%');
|
||||||
->orWhere('title', 'like', '%' . $keyword . '%')
|
|
||||||
->orWhere('uri', 'like', '%' . $keyword . '%');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$total = $builder->count();
|
$total = $builder->count();
|
||||||
$res = $builder->forPage($current, $pageSize)
|
$res = $builder->forPage($current, $pageSize)->get();
|
||||||
->get();
|
|
||||||
|
|
||||||
return response([
|
return response(['data' => $res, 'total' => $total]);
|
||||||
'data' => $res,
|
|
||||||
'total' => $total
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository)
|
public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository)
|
||||||
@@ -176,125 +141,4 @@ class SystemController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除系统日志
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function clearSystemLog(Request $request)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'days' => 'integer|min:0|max:365',
|
|
||||||
'level' => 'string|in:info,warning,error,all',
|
|
||||||
'limit' => 'integer|min:100|max:10000'
|
|
||||||
], [
|
|
||||||
'days.required' => '请指定要清除多少天前的日志',
|
|
||||||
'days.integer' => '天数必须为整数',
|
|
||||||
'days.min' => '天数不能少于1天',
|
|
||||||
'days.max' => '天数不能超过365天',
|
|
||||||
'level.in' => '日志级别只能是:info、warning、error、all',
|
|
||||||
'limit.min' => '单次清除数量不能少于100条',
|
|
||||||
'limit.max' => '单次清除数量不能超过10000条'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$days = $request->input('days', 30); // 默认清除30天前的日志
|
|
||||||
$level = $request->input('level', 'all'); // 默认清除所有级别
|
|
||||||
$limit = $request->input('limit', 1000); // 默认单次清除1000条
|
|
||||||
|
|
||||||
try {
|
|
||||||
$cutoffDate = now()->subDays($days);
|
|
||||||
|
|
||||||
// 构建查询条件
|
|
||||||
$query = LogModel::where('created_at', '<', $cutoffDate->timestamp);
|
|
||||||
|
|
||||||
if ($level !== 'all') {
|
|
||||||
$query->where('level', strtoupper($level));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取要删除的记录数量
|
|
||||||
$totalCount = $query->count();
|
|
||||||
|
|
||||||
if ($totalCount === 0) {
|
|
||||||
return $this->success([
|
|
||||||
'message' => '没有找到符合条件的日志记录',
|
|
||||||
'deleted_count' => 0,
|
|
||||||
'total_count' => $totalCount
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分批删除,避免单次删除过多数据
|
|
||||||
$deletedCount = 0;
|
|
||||||
$batchSize = min($limit, 1000); // 每批最多1000条
|
|
||||||
|
|
||||||
while ($deletedCount < $limit && $deletedCount < $totalCount) {
|
|
||||||
$remainingLimit = min($batchSize, $limit - $deletedCount);
|
|
||||||
|
|
||||||
$batchQuery = LogModel::where('created_at', '<', $cutoffDate->timestamp);
|
|
||||||
if ($level !== 'all') {
|
|
||||||
$batchQuery->where('level', strtoupper($level));
|
|
||||||
}
|
|
||||||
|
|
||||||
$idsToDelete = $batchQuery->limit($remainingLimit)->pluck('id');
|
|
||||||
|
|
||||||
if ($idsToDelete->isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$batchDeleted = LogModel::whereIn('id', $idsToDelete)->delete();
|
|
||||||
$deletedCount += $batchDeleted;
|
|
||||||
|
|
||||||
// 避免长时间占用数据库连接
|
|
||||||
if ($deletedCount < $limit && $deletedCount < $totalCount) {
|
|
||||||
usleep(100000); // 暂停0.1秒
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'message' => '日志清除完成',
|
|
||||||
'deleted_count' => $deletedCount,
|
|
||||||
'total_count' => $totalCount,
|
|
||||||
'remaining_count' => max(0, $totalCount - $deletedCount)
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $this->fail(ResponseEnum::HTTP_ERROR, null, '清除日志失败:' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志清除统计信息
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function getLogClearStats(Request $request)
|
|
||||||
{
|
|
||||||
$days = $request->input('days', 30);
|
|
||||||
$level = $request->input('level', 'all');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$cutoffDate = now()->subDays($days);
|
|
||||||
|
|
||||||
$query = LogModel::where('created_at', '<', $cutoffDate->timestamp);
|
|
||||||
if ($level !== 'all') {
|
|
||||||
$query->where('level', strtoupper($level));
|
|
||||||
}
|
|
||||||
|
|
||||||
$stats = [
|
|
||||||
'days' => $days,
|
|
||||||
'level' => $level,
|
|
||||||
'cutoff_date' => $cutoffDate->format(format: 'Y-m-d H:i:s'),
|
|
||||||
'total_logs' => LogModel::count(),
|
|
||||||
'logs_to_clear' => $query->count(),
|
|
||||||
'oldest_log' => LogModel::orderBy('created_at', 'asc')->first(),
|
|
||||||
'newest_log' => LogModel::orderBy('created_at', 'desc')->first(),
|
|
||||||
];
|
|
||||||
|
|
||||||
return $this->success($stats);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $this->fail(ResponseEnum::HTTP_ERROR, null, '获取统计信息失败:' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ use App\Jobs\SendEmailJob;
|
|||||||
use App\Models\Plan;
|
use App\Models\Plan;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\AuthService;
|
use App\Services\AuthService;
|
||||||
|
use App\Services\NodeSyncService;
|
||||||
use App\Services\UserService;
|
use App\Services\UserService;
|
||||||
use App\Traits\QueryOperators;
|
use App\Traits\QueryOperators;
|
||||||
use App\Utils\Helper;
|
use App\Utils\Helper;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
@@ -34,27 +36,15 @@ class UserController extends Controller
|
|||||||
return $this->success($user->save());
|
return $this->success($user->save());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Apply filters and sorts to the query builder.
|
||||||
* Apply filters and sorts to the query builder
|
private function applyFiltersAndSorts(Request $request, Builder|QueryBuilder $builder): void
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @param Builder $builder
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function applyFiltersAndSorts(Request $request, Builder $builder): void
|
|
||||||
{
|
{
|
||||||
$this->applyFilters($request, $builder);
|
$this->applyFilters($request, $builder);
|
||||||
$this->applySorting($request, $builder);
|
$this->applySorting($request, $builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Apply filters to the query builder.
|
||||||
* Apply filters to the query builder
|
private function applyFilters(Request $request, Builder|QueryBuilder $builder): void
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @param Builder $builder
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function applyFilters(Request $request, Builder $builder): void
|
|
||||||
{
|
{
|
||||||
if (!$request->has('filter')) {
|
if (!$request->has('filter')) {
|
||||||
return;
|
return;
|
||||||
@@ -70,18 +60,14 @@ class UserController extends Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Build one filter query condition.
|
||||||
* Build the filter query based on field and value
|
private function buildFilterQuery(Builder|QueryBuilder $query, string $field, mixed $value): void
|
||||||
*
|
|
||||||
* @param Builder $query
|
|
||||||
* @param string $field
|
|
||||||
* @param mixed $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function buildFilterQuery(Builder $query, string $field, mixed $value): void
|
|
||||||
{
|
{
|
||||||
// 处理关联查询
|
// 处理关联查询
|
||||||
if (str_contains($field, '.')) {
|
if (str_contains($field, '.')) {
|
||||||
|
if (!method_exists($query, 'whereHas')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
[$relation, $relationField] = explode('.', $field);
|
[$relation, $relationField] = explode('.', $field);
|
||||||
$query->whereHas($relation, function ($q) use ($relationField, $value) {
|
$query->whereHas($relation, function ($q) use ($relationField, $value) {
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
@@ -126,14 +112,8 @@ class UserController extends Controller
|
|||||||
$this->applyQueryCondition($query, $queryField, $operator, $filterValue);
|
$this->applyQueryCondition($query, $queryField, $operator, $filterValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Apply sorting rules to the query builder.
|
||||||
* Apply sorting to the query builder
|
private function applySorting(Request $request, Builder|QueryBuilder $builder): void
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @param Builder $builder
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function applySorting(Request $request, Builder $builder): void
|
|
||||||
{
|
{
|
||||||
if (!$request->has('sort')) {
|
if (!$request->has('sort')) {
|
||||||
return;
|
return;
|
||||||
@@ -146,19 +126,50 @@ class UserController extends Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Resolve bulk operation scope and normalize user_ids.
|
||||||
* Fetch paginated user list with filters and sorting
|
private function resolveScope(Request $request): array
|
||||||
*
|
{
|
||||||
* @param Request $request
|
$scope = $request->input('scope');
|
||||||
* @return \Illuminate\Http\Response
|
$userIds = $request->input('user_ids');
|
||||||
*/
|
|
||||||
|
$hasSelection = is_array($userIds) && count(array_filter($userIds, static fn($v) => is_numeric($v))) > 0;
|
||||||
|
$hasFilter = $request->has('filter') && !empty($request->input('filter'));
|
||||||
|
|
||||||
|
if (!in_array($scope, ['selected', 'filtered', 'all'], true)) {
|
||||||
|
if ($hasSelection) {
|
||||||
|
$scope = 'selected';
|
||||||
|
} elseif ($hasFilter) {
|
||||||
|
$scope = 'filtered';
|
||||||
|
} else {
|
||||||
|
$scope = 'all';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedIds = [];
|
||||||
|
if ($scope === 'selected') {
|
||||||
|
$normalizedIds = is_array($userIds) ? $userIds : [];
|
||||||
|
$normalizedIds = array_values(array_unique(array_map(static function ($v) {
|
||||||
|
return is_numeric($v) ? (int) $v : null;
|
||||||
|
}, $normalizedIds)));
|
||||||
|
$normalizedIds = array_values(array_filter($normalizedIds, static fn($v) => is_int($v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scope' => $scope,
|
||||||
|
'user_ids' => $normalizedIds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch paginated user list (filters + sorting).
|
||||||
public function fetch(Request $request)
|
public function fetch(Request $request)
|
||||||
{
|
{
|
||||||
$current = $request->input('current', 1);
|
$current = $request->input('current', 1);
|
||||||
$pageSize = $request->input('pageSize', 10);
|
$pageSize = $request->input('pageSize', 10);
|
||||||
|
|
||||||
$userModel = User::with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
|
$userModel = User::query()
|
||||||
->select(DB::raw('*, (u+d) as total_used'));
|
->with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
|
||||||
|
->select((new User())->getTable() . '.*')
|
||||||
|
->selectRaw('(u + d) as total_used');
|
||||||
|
|
||||||
$this->applyFiltersAndSorts($request, $userModel);
|
$this->applyFiltersAndSorts($request, $userModel);
|
||||||
|
|
||||||
@@ -172,12 +183,7 @@ class UserController extends Controller
|
|||||||
return $this->paginate($users);
|
return $this->paginate($users);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Transform user fields for API response.
|
||||||
* Transform user data for response
|
|
||||||
*
|
|
||||||
* @param User $user
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public static function transformUserData(User $user): array
|
public static function transformUserData(User $user): array
|
||||||
{
|
{
|
||||||
$user = $user->toArray();
|
$user = $user->toArray();
|
||||||
@@ -253,19 +259,25 @@ class UserController extends Controller
|
|||||||
return $this->success(true);
|
return $this->success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Export users to CSV.
|
||||||
* 导出用户数据为CSV格式
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Symfony\Component\HttpFoundation\StreamedResponse
|
|
||||||
*/
|
|
||||||
public function dumpCSV(Request $request)
|
public function dumpCSV(Request $request)
|
||||||
{
|
{
|
||||||
ini_set('memory_limit', '-1');
|
ini_set('memory_limit', '-1');
|
||||||
gc_enable(); // 启用垃圾回收
|
gc_enable(); // 启用垃圾回收
|
||||||
|
|
||||||
|
$scopeInfo = $this->resolveScope($request);
|
||||||
|
$scope = $scopeInfo['scope'];
|
||||||
|
$userIds = $scopeInfo['user_ids'];
|
||||||
|
|
||||||
|
if ($scope === 'selected') {
|
||||||
|
if (empty($userIds)) {
|
||||||
|
return $this->fail([422, 'user_ids不能为空']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 优化查询:使用with预加载plan关系,避免N+1问题
|
// 优化查询:使用with预加载plan关系,避免N+1问题
|
||||||
$query = User::with('plan:id,name')
|
$query = User::query()
|
||||||
|
->with('plan:id,name')
|
||||||
->orderBy('id', 'asc')
|
->orderBy('id', 'asc')
|
||||||
->select([
|
->select([
|
||||||
'email',
|
'email',
|
||||||
@@ -279,7 +291,11 @@ class UserController extends Controller
|
|||||||
'plan_id'
|
'plan_id'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->applyFiltersAndSorts($request, $query);
|
if ($scope === 'selected') {
|
||||||
|
$query->whereIn('id', $userIds);
|
||||||
|
} elseif ($scope === 'filtered') {
|
||||||
|
$this->applyFiltersAndSorts($request, $query);
|
||||||
|
} // all: ignore filter/sort
|
||||||
|
|
||||||
$filename = 'users_' . date('Y-m-d_His') . '.csv';
|
$filename = 'users_' . date('Y-m-d_His') . '.csv';
|
||||||
|
|
||||||
@@ -439,26 +455,65 @@ class UserController extends Controller
|
|||||||
public function sendMail(UserSendMail $request)
|
public function sendMail(UserSendMail $request)
|
||||||
{
|
{
|
||||||
ini_set('memory_limit', '-1');
|
ini_set('memory_limit', '-1');
|
||||||
|
$scopeInfo = $this->resolveScope($request);
|
||||||
|
$scope = $scopeInfo['scope'];
|
||||||
|
$userIds = $scopeInfo['user_ids'];
|
||||||
|
|
||||||
|
if ($scope === 'selected') {
|
||||||
|
if (empty($userIds)) {
|
||||||
|
return $this->fail([422, 'user_ids不能为空']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
||||||
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
||||||
$hourlyLimit = (int) env('MASS_EMAIL_HOURLY_LIMIT', 500);
|
$hourlyLimit = (int) env('MASS_EMAIL_HOURLY_LIMIT', 500);
|
||||||
$hourlyLimit = $hourlyLimit > 0 ? $hourlyLimit : 500;
|
$hourlyLimit = $hourlyLimit > 0 ? $hourlyLimit : 500;
|
||||||
$builder = User::orderBy($sort, $sortType);
|
|
||||||
$this->applyFiltersAndSorts($request, $builder);
|
$builder = User::query()
|
||||||
|
->with('plan:id,name')
|
||||||
|
->orderBy('id', 'desc');
|
||||||
|
|
||||||
|
if ($scope === 'filtered') {
|
||||||
|
// filtered: apply filters/sort
|
||||||
|
$builder->orderBy($sort, $sortType);
|
||||||
|
$this->applyFiltersAndSorts($request, $builder);
|
||||||
|
} elseif ($scope === 'selected') {
|
||||||
|
$builder->whereIn('id', $userIds);
|
||||||
|
} // all: ignore filter/sort
|
||||||
|
|
||||||
$subject = $request->input('subject');
|
$subject = $request->input('subject');
|
||||||
$content = $request->input('content');
|
$content = $request->input('content');
|
||||||
$templateValue = [
|
$appName = admin_setting('app_name', 'Notification Service');
|
||||||
'name' => admin_setting('app_name', 'Notification Service'),
|
$appUrl = admin_setting('app_url');
|
||||||
'url' => admin_setting('app_url'),
|
|
||||||
'content' => $content
|
|
||||||
];
|
|
||||||
|
|
||||||
$chunkSize = 1000;
|
$chunkSize = 1000;
|
||||||
$processed = 0;
|
$processed = 0;
|
||||||
|
|
||||||
$builder->chunk($chunkSize, function ($users) use ($subject, $templateValue, $hourlyLimit, &$processed) {
|
$builder->chunk($chunkSize, function ($users) use ($subject, $content, $appName, $appUrl, $hourlyLimit, &$processed) {
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
|
$vars = [
|
||||||
|
'app.name' => $appName,
|
||||||
|
'app.url' => $appUrl,
|
||||||
|
'now' => now()->format('Y-m-d H:i:s'),
|
||||||
|
'user.id' => $user->id,
|
||||||
|
'user.email' => $user->email,
|
||||||
|
'user.uuid' => $user->uuid,
|
||||||
|
'user.plan_name' => $user->plan?->name ?? '',
|
||||||
|
'user.expired_at' => $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '',
|
||||||
|
'user.transfer_enable' => (int) ($user->transfer_enable ?? 0),
|
||||||
|
'user.transfer_used' => (int) (($user->u ?? 0) + ($user->d ?? 0)),
|
||||||
|
'user.transfer_left' => (int) (($user->transfer_enable ?? 0) - (($user->u ?? 0) + ($user->d ?? 0))),
|
||||||
|
];
|
||||||
|
|
||||||
|
$templateValue = [
|
||||||
|
'name' => $appName,
|
||||||
|
'url' => $appUrl,
|
||||||
|
'content' => $content,
|
||||||
|
'vars' => $vars,
|
||||||
|
'content_mode' => 'text',
|
||||||
|
];
|
||||||
|
|
||||||
$delaySeconds = intdiv($processed, $hourlyLimit) * 3600;
|
$delaySeconds = intdiv($processed, $hourlyLimit) * 3600;
|
||||||
dispatch(new SendEmailJob([
|
dispatch(new SendEmailJob([
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
@@ -475,10 +530,29 @@ class UserController extends Controller
|
|||||||
|
|
||||||
public function ban(Request $request)
|
public function ban(Request $request)
|
||||||
{
|
{
|
||||||
|
$scopeInfo = $this->resolveScope($request);
|
||||||
|
$scope = $scopeInfo['scope'];
|
||||||
|
$userIds = $scopeInfo['user_ids'];
|
||||||
|
|
||||||
|
if ($scope === 'selected') {
|
||||||
|
if (empty($userIds)) {
|
||||||
|
return $this->fail([422, 'user_ids不能为空']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
||||||
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
||||||
$builder = User::orderBy($sort, $sortType);
|
|
||||||
$this->applyFilters($request, $builder);
|
$builder = User::query()->orderBy('id', 'desc');
|
||||||
|
|
||||||
|
if ($scope === 'filtered') {
|
||||||
|
// filtered: keep current semantics
|
||||||
|
$builder->orderBy($sort, $sortType);
|
||||||
|
$this->applyFiltersAndSorts($request, $builder);
|
||||||
|
} elseif ($scope === 'selected') {
|
||||||
|
$builder->whereIn('id', $userIds);
|
||||||
|
} // all: ignore filter/sort
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$builder->update([
|
$builder->update([
|
||||||
'banned' => 1
|
'banned' => 1
|
||||||
@@ -487,16 +561,11 @@ class UserController extends Controller
|
|||||||
Log::error($e);
|
Log::error($e);
|
||||||
return $this->fail([500, '处理失败']);
|
return $this->fail([500, '处理失败']);
|
||||||
}
|
}
|
||||||
|
// Full refresh not implemented.
|
||||||
return $this->success(true);
|
return $this->success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Delete user and related data.
|
||||||
* 删除用户及其关联数据
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return JsonResponse
|
|
||||||
*/
|
|
||||||
public function destroy(Request $request)
|
public function destroy(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\V2\Server;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Jobs\UserAliveSyncJob;
|
||||||
|
use App\Services\ServerService;
|
||||||
|
use App\Services\UserService;
|
||||||
|
use App\Utils\CacheKey;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Log;
|
||||||
|
|
||||||
|
class ServerController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* server handshake api
|
||||||
|
*/
|
||||||
|
public function handshake(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$websocket = ['enabled' => false];
|
||||||
|
|
||||||
|
if ((bool) admin_setting('server_ws_enable', 1)) {
|
||||||
|
$customUrl = trim((string) admin_setting('server_ws_url', ''));
|
||||||
|
|
||||||
|
if ($customUrl !== '') {
|
||||||
|
$wsUrl = rtrim($customUrl, '/');
|
||||||
|
} else {
|
||||||
|
$wsScheme = $request->isSecure() ? 'wss' : 'ws';
|
||||||
|
$wsUrl = "{$wsScheme}://{$request->getHost()}:8076";
|
||||||
|
}
|
||||||
|
|
||||||
|
$websocket = [
|
||||||
|
'enabled' => true,
|
||||||
|
'ws_url' => $wsUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'websocket' => $websocket
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* node report api - merge traffic + alive + status
|
||||||
|
* POST /api/v2/server/node/report
|
||||||
|
*/
|
||||||
|
public function report(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$node = $request->attributes->get('node_info');
|
||||||
|
$nodeType = $node->type;
|
||||||
|
$nodeId = $node->id;
|
||||||
|
|
||||||
|
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600);
|
||||||
|
|
||||||
|
// hanle traffic data
|
||||||
|
$traffic = $request->input('traffic');
|
||||||
|
if (is_array($traffic) && !empty($traffic)) {
|
||||||
|
$data = array_filter($traffic, function ($item) {
|
||||||
|
return is_array($item)
|
||||||
|
&& count($item) === 2
|
||||||
|
&& is_numeric($item[0])
|
||||||
|
&& is_numeric($item[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
Cache::put(
|
||||||
|
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId),
|
||||||
|
count($data),
|
||||||
|
3600
|
||||||
|
);
|
||||||
|
Cache::put(
|
||||||
|
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId),
|
||||||
|
time(),
|
||||||
|
3600
|
||||||
|
);
|
||||||
|
$userService = new UserService();
|
||||||
|
$userService->trafficFetch($node, $nodeType, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle alive data
|
||||||
|
$alive = $request->input('alive');
|
||||||
|
if (is_array($alive) && !empty($alive)) {
|
||||||
|
UserAliveSyncJob::dispatch($alive, $nodeType, $nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle active connections
|
||||||
|
$online = $request->input('online');
|
||||||
|
if (is_array($online) && !empty($online)) {
|
||||||
|
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
|
||||||
|
foreach ($online as $uid => $conn) {
|
||||||
|
$cacheKey = CacheKey::get("USER_ONLINE_CONN_{$nodeType}_{$nodeId}", $uid);
|
||||||
|
Cache::put($cacheKey, (int) $conn, $cacheTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle node status
|
||||||
|
$status = $request->input('status');
|
||||||
|
if (is_array($status) && !empty($status)) {
|
||||||
|
$statusData = [
|
||||||
|
'cpu' => (float) ($status['cpu'] ?? 0),
|
||||||
|
'mem' => [
|
||||||
|
'total' => (int) ($status['mem']['total'] ?? 0),
|
||||||
|
'used' => (int) ($status['mem']['used'] ?? 0),
|
||||||
|
],
|
||||||
|
'swap' => [
|
||||||
|
'total' => (int) ($status['swap']['total'] ?? 0),
|
||||||
|
'used' => (int) ($status['swap']['used'] ?? 0),
|
||||||
|
],
|
||||||
|
'disk' => [
|
||||||
|
'total' => (int) ($status['disk']['total'] ?? 0),
|
||||||
|
'used' => (int) ($status['disk']['used'] ?? 0),
|
||||||
|
],
|
||||||
|
'updated_at' => now()->timestamp,
|
||||||
|
'kernel_status' => $status['kernel_status'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
|
||||||
|
cache([
|
||||||
|
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData,
|
||||||
|
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp,
|
||||||
|
], $cacheTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle node metrics (Metrics)
|
||||||
|
$metrics = $request->input('metrics');
|
||||||
|
if (is_array($metrics) && !empty($metrics)) {
|
||||||
|
ServerService::updateMetrics($node, $metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['data' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ class Kernel extends HttpKernel
|
|||||||
// \Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
// \Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
// \App\Http\Middleware\VerifyCsrfToken::class,
|
// \App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
// \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
// \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
\App\Http\Middleware\ApplyRuntimeSettings::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'api' => [
|
'api' => [
|
||||||
@@ -46,6 +47,7 @@ class Kernel extends HttpKernel
|
|||||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||||
// \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
|
// \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
|
||||||
// \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
// \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
\App\Http\Middleware\ApplyRuntimeSettings::class,
|
||||||
\App\Http\Middleware\ForceJson::class,
|
\App\Http\Middleware\ForceJson::class,
|
||||||
\App\Http\Middleware\Language::class,
|
\App\Http\Middleware\Language::class,
|
||||||
'bindings',
|
'bindings',
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
|
class ApplyRuntimeSettings
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next)
|
||||||
|
{
|
||||||
|
$appUrl = admin_setting('app_url');
|
||||||
|
if (is_string($appUrl) && $appUrl !== '') {
|
||||||
|
URL::forceRootUrl($appUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) admin_setting('force_https', false)) {
|
||||||
|
URL::forceScheme('https');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,23 +2,59 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Models\AdminAuditLog;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
|
||||||
class RequestLog
|
class RequestLog
|
||||||
{
|
{
|
||||||
/**
|
private const SENSITIVE_KEYS = ['password', 'token', 'secret', 'key', 'api_key'];
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
if ($request->method() === 'POST') {
|
if ($request->method() !== 'POST') {
|
||||||
$path = $request->path();
|
return $next($request);
|
||||||
info("POST {$path}");
|
}
|
||||||
};
|
|
||||||
return $next($request);
|
$response = $next($request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$admin = $request->user();
|
||||||
|
if (!$admin || !$admin->is_admin) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $this->resolveAction($request->path());
|
||||||
|
$data = collect($request->all())->except(self::SENSITIVE_KEYS)->toArray();
|
||||||
|
|
||||||
|
AdminAuditLog::insert([
|
||||||
|
'admin_id' => $admin->id,
|
||||||
|
'action' => $action,
|
||||||
|
'method' => $request->method(),
|
||||||
|
'uri' => $request->getRequestUri(),
|
||||||
|
'request_data' => json_encode($data, JSON_UNESCAPED_UNICODE),
|
||||||
|
'ip' => $request->getClientIp(),
|
||||||
|
'created_at' => time(),
|
||||||
|
'updated_at' => time(),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Log::warning('Audit log write failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveAction(string $path): string
|
||||||
|
{
|
||||||
|
// api/v2/{secure_path}/user/update → user.update
|
||||||
|
$path = preg_replace('#^api/v[12]/[^/]+/#', '', $path);
|
||||||
|
// gift-card/create-template → gift_card.create_template
|
||||||
|
$path = str_replace('-', '_', $path);
|
||||||
|
// user/update → user.update, server/manage/sort → server_manage.sort
|
||||||
|
$segments = explode('/', $path);
|
||||||
|
$method = array_pop($segments);
|
||||||
|
$resource = implode('_', $segments);
|
||||||
|
|
||||||
|
return $resource . '.' . $method;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class ConfigSave extends FormRequest
|
|||||||
'tos_url' => 'nullable|url',
|
'tos_url' => 'nullable|url',
|
||||||
'currency' => '',
|
'currency' => '',
|
||||||
'currency_symbol' => '',
|
'currency_symbol' => '',
|
||||||
|
'ticket_must_wait_reply' => '',
|
||||||
// subscribe
|
// subscribe
|
||||||
'plan_change_enable' => '',
|
'plan_change_enable' => '',
|
||||||
'reset_traffic_method' => 'in:0,1,2,3,4',
|
'reset_traffic_method' => 'in:0,1,2,3,4',
|
||||||
@@ -50,6 +51,8 @@ class ConfigSave extends FormRequest
|
|||||||
'server_pull_interval' => 'integer',
|
'server_pull_interval' => 'integer',
|
||||||
'server_push_interval' => 'integer',
|
'server_push_interval' => 'integer',
|
||||||
'device_limit_mode' => 'integer',
|
'device_limit_mode' => 'integer',
|
||||||
|
'server_ws_enable' => 'boolean',
|
||||||
|
'server_ws_url' => 'nullable|url',
|
||||||
// frontend
|
// frontend
|
||||||
'frontend_theme' => '',
|
'frontend_theme' => '',
|
||||||
'frontend_theme_sidebar' => 'nullable|in:dark,light',
|
'frontend_theme_sidebar' => 'nullable|in:dark,light',
|
||||||
@@ -68,6 +71,7 @@ class ConfigSave extends FormRequest
|
|||||||
// telegram
|
// telegram
|
||||||
'telegram_bot_enable' => '',
|
'telegram_bot_enable' => '',
|
||||||
'telegram_bot_token' => '',
|
'telegram_bot_token' => '',
|
||||||
|
'telegram_webhook_url' => 'nullable|url',
|
||||||
'telegram_discuss_id' => '',
|
'telegram_discuss_id' => '',
|
||||||
'telegram_channel_id' => '',
|
'telegram_channel_id' => '',
|
||||||
'telegram_discuss_link' => 'nullable|url',
|
'telegram_discuss_link' => 'nullable|url',
|
||||||
@@ -128,6 +132,7 @@ class ConfigSave extends FormRequest
|
|||||||
'subscribe_url.url' => '订阅URL格式不正确,必须携带http(s)://',
|
'subscribe_url.url' => '订阅URL格式不正确,必须携带http(s)://',
|
||||||
'server_token.min' => '通讯密钥长度必须大于16位',
|
'server_token.min' => '通讯密钥长度必须大于16位',
|
||||||
'tos_url.url' => '服务条款URL格式不正确,必须携带http(s)://',
|
'tos_url.url' => '服务条款URL格式不正确,必须携带http(s)://',
|
||||||
|
'telegram_webhook_url.url' => 'Telegram Webhook地址格式不正确,必须携带http(s)://',
|
||||||
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式,必须携带http(s)://',
|
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式,必须携带http(s)://',
|
||||||
'logo.url' => 'LOGO URL格式不正确,必须携带https(s)://',
|
'logo.url' => 'LOGO URL格式不正确,必须携带https(s)://',
|
||||||
'secure_path.min' => '后台路径长度最小为8位',
|
'secure_path.min' => '后台路径长度最小为8位',
|
||||||
|
|||||||
@@ -8,6 +8,23 @@ use Illuminate\Foundation\Http\FormRequest;
|
|||||||
|
|
||||||
class ServerSave extends FormRequest
|
class ServerSave extends FormRequest
|
||||||
{
|
{
|
||||||
|
private const UTLS_RULES = [
|
||||||
|
'utls.enabled' => 'nullable|boolean',
|
||||||
|
'utls.fingerprint' => 'nullable|string',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const MULTIPLEX_RULES = [
|
||||||
|
'multiplex.enabled' => 'nullable|boolean',
|
||||||
|
'multiplex.protocol' => 'nullable|string',
|
||||||
|
'multiplex.max_connections' => 'nullable|integer',
|
||||||
|
'multiplex.min_streams' => 'nullable|integer',
|
||||||
|
'multiplex.max_streams' => 'nullable|integer',
|
||||||
|
'multiplex.padding' => 'nullable|boolean',
|
||||||
|
'multiplex.brutal.enabled' => 'nullable|boolean',
|
||||||
|
'multiplex.brutal.up_mbps' => 'nullable|integer',
|
||||||
|
'multiplex.brutal.down_mbps' => 'nullable|integer',
|
||||||
|
];
|
||||||
|
|
||||||
private const PROTOCOL_RULES = [
|
private const PROTOCOL_RULES = [
|
||||||
'shadowsocks' => [
|
'shadowsocks' => [
|
||||||
'cipher' => 'required|string',
|
'cipher' => 'required|string',
|
||||||
@@ -67,8 +84,8 @@ class ServerSave extends FormRequest
|
|||||||
'tls_settings' => 'nullable|array',
|
'tls_settings' => 'nullable|array',
|
||||||
],
|
],
|
||||||
'mieru' => [
|
'mieru' => [
|
||||||
'transport' => 'required|string',
|
'transport' => 'required|string|in:TCP,UDP',
|
||||||
'multiplexing' => 'required|string',
|
'traffic_pattern' => 'string'
|
||||||
],
|
],
|
||||||
'anytls' => [
|
'anytls' => [
|
||||||
'tls' => 'nullable|array',
|
'tls' => 'nullable|array',
|
||||||
@@ -97,6 +114,9 @@ class ServerSave extends FormRequest
|
|||||||
'rate' => 'required|numeric',
|
'rate' => 'required|numeric',
|
||||||
'rate_time_enable' => 'nullable|boolean',
|
'rate_time_enable' => 'nullable|boolean',
|
||||||
'rate_time_ranges' => 'nullable|array',
|
'rate_time_ranges' => 'nullable|array',
|
||||||
|
'custom_outbounds' => 'nullable|array',
|
||||||
|
'custom_routes' => 'nullable|array',
|
||||||
|
'cert_config' => 'nullable|array',
|
||||||
'rate_time_ranges.*.start' => 'required_with:rate_time_ranges|string|date_format:H:i',
|
'rate_time_ranges.*.start' => 'required_with:rate_time_ranges|string|date_format:H:i',
|
||||||
'rate_time_ranges.*.end' => 'required_with:rate_time_ranges|string|date_format:H:i',
|
'rate_time_ranges.*.end' => 'required_with:rate_time_ranges|string|date_format:H:i',
|
||||||
'rate_time_ranges.*.rate' => 'required_with:rate_time_ranges|numeric|min:0',
|
'rate_time_ranges.*.rate' => 'required_with:rate_time_ranges|numeric|min:0',
|
||||||
@@ -109,13 +129,45 @@ class ServerSave extends FormRequest
|
|||||||
$type = $this->input('type');
|
$type = $this->input('type');
|
||||||
$rules = $this->getBaseRules();
|
$rules = $this->getBaseRules();
|
||||||
|
|
||||||
foreach (self::PROTOCOL_RULES[$type] ?? [] as $field => $rule) {
|
$protocolRules = self::PROTOCOL_RULES[$type] ?? [];
|
||||||
|
if (in_array($type, ['vmess', 'vless', 'trojan', 'mieru'])) {
|
||||||
|
$protocolRules = array_merge($protocolRules, self::MULTIPLEX_RULES, self::UTLS_RULES);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($protocolRules as $field => $rule) {
|
||||||
$rules['protocol_settings.' . $field] = $rule;
|
$rules['protocol_settings.' . $field] = $rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rules;
|
return $rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'protocol_settings.cipher' => '加密方式',
|
||||||
|
'protocol_settings.obfs' => '混淆类型',
|
||||||
|
'protocol_settings.network' => '传输协议',
|
||||||
|
'protocol_settings.port_range' => '端口范围',
|
||||||
|
'protocol_settings.traffic_pattern' => 'Traffic Pattern',
|
||||||
|
'protocol_settings.transport' => '传输方式',
|
||||||
|
'protocol_settings.version' => '协议版本',
|
||||||
|
'protocol_settings.password' => '密码',
|
||||||
|
'protocol_settings.handshake.server' => '握手服务器',
|
||||||
|
'protocol_settings.handshake.server_port' => '握手端口',
|
||||||
|
'protocol_settings.multiplex.enabled' => '多路复用',
|
||||||
|
'protocol_settings.multiplex.protocol' => '复用协议',
|
||||||
|
'protocol_settings.multiplex.max_connections' => '最大连接数',
|
||||||
|
'protocol_settings.multiplex.min_streams' => '最小流数',
|
||||||
|
'protocol_settings.multiplex.max_streams' => '最大流数',
|
||||||
|
'protocol_settings.multiplex.padding' => '复用填充',
|
||||||
|
'protocol_settings.multiplex.brutal.enabled' => 'Brutal加速',
|
||||||
|
'protocol_settings.multiplex.brutal.up_mbps' => 'Brutal上行速率',
|
||||||
|
'protocol_settings.multiplex.brutal.down_mbps' => 'Brutal下行速率',
|
||||||
|
'protocol_settings.utls.enabled' => 'uTLS',
|
||||||
|
'protocol_settings.utls.fingerprint' => 'uTLS指纹',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function messages()
|
public function messages()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -136,7 +188,11 @@ class ServerSave extends FormRequest
|
|||||||
'networkSettings.array' => '传输协议配置有误',
|
'networkSettings.array' => '传输协议配置有误',
|
||||||
'ruleSettings.array' => '规则配置有误',
|
'ruleSettings.array' => '规则配置有误',
|
||||||
'tlsSettings.array' => 'tls配置有误',
|
'tlsSettings.array' => 'tls配置有误',
|
||||||
'dnsSettings.array' => 'dns配置有误'
|
'dnsSettings.array' => 'dns配置有误',
|
||||||
|
'protocol_settings.*.required' => ':attribute 不能为空',
|
||||||
|
'protocol_settings.*.string' => ':attribute 必须是字符串',
|
||||||
|
'protocol_settings.*.integer' => ':attribute 必须是整数',
|
||||||
|
'protocol_settings.*.in' => ':attribute 的值不合法',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,10 +218,8 @@ class AdminRoute
|
|||||||
$router->get('/getQueueStats', [SystemController::class, 'getQueueStats']);
|
$router->get('/getQueueStats', [SystemController::class, 'getQueueStats']);
|
||||||
$router->get('/getQueueWorkload', [SystemController::class, 'getQueueWorkload']);
|
$router->get('/getQueueWorkload', [SystemController::class, 'getQueueWorkload']);
|
||||||
$router->get('/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
|
$router->get('/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
|
||||||
$router->get('/getSystemLog', [SystemController::class, 'getSystemLog']);
|
|
||||||
$router->get('/getHorizonFailedJobs', [SystemController::class, 'getHorizonFailedJobs']);
|
$router->get('/getHorizonFailedJobs', [SystemController::class, 'getHorizonFailedJobs']);
|
||||||
$router->post('/clearSystemLog', [SystemController::class, 'clearSystemLog']);
|
$router->any('/getAuditLog', [SystemController::class, 'getAuditLog']);
|
||||||
$router->get('/getLogClearStats', [SystemController::class, 'getLogClearStats']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Http\Routes\V2;
|
||||||
|
|
||||||
|
use App\Http\Controllers\V2\Client\AppController;
|
||||||
|
use Illuminate\Contracts\Routing\Registrar;
|
||||||
|
|
||||||
|
class ClientRoute
|
||||||
|
{
|
||||||
|
public function map(Registrar $router)
|
||||||
|
{
|
||||||
|
$router->group([
|
||||||
|
'prefix' => 'client',
|
||||||
|
'middleware' => 'client'
|
||||||
|
], function ($router) {
|
||||||
|
// App
|
||||||
|
$router->get('/app/getConfig', [AppController::class, 'getConfig']);
|
||||||
|
$router->get('/app/getVersion', [AppController::class, 'getVersion']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Http\Routes\V2;
|
|||||||
use App\Http\Controllers\V1\Server\ShadowsocksTidalabController;
|
use App\Http\Controllers\V1\Server\ShadowsocksTidalabController;
|
||||||
use App\Http\Controllers\V1\Server\TrojanTidalabController;
|
use App\Http\Controllers\V1\Server\TrojanTidalabController;
|
||||||
use App\Http\Controllers\V1\Server\UniProxyController;
|
use App\Http\Controllers\V1\Server\UniProxyController;
|
||||||
|
use App\Http\Controllers\V2\Server\ServerController;
|
||||||
use Illuminate\Contracts\Routing\Registrar;
|
use Illuminate\Contracts\Routing\Registrar;
|
||||||
|
|
||||||
class ServerRoute
|
class ServerRoute
|
||||||
@@ -15,6 +16,8 @@ class ServerRoute
|
|||||||
'prefix' => 'server',
|
'prefix' => 'server',
|
||||||
'middleware' => 'server'
|
'middleware' => 'server'
|
||||||
], function ($route) {
|
], function ($route) {
|
||||||
|
$route->post('handshake', [ServerController::class, 'handshake']);
|
||||||
|
$route->post('report', [ServerController::class, 'report']);
|
||||||
$route->get('config', [UniProxyController::class, 'config']);
|
$route->get('config', [UniProxyController::class, 'config']);
|
||||||
$route->get('user', [UniProxyController::class, 'user']);
|
$route->get('user', [UniProxyController::class, 'user']);
|
||||||
$route->post('push', [UniProxyController::class, 'push']);
|
$route->post('push', [UniProxyController::class, 'push']);
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\NodeSyncService;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class NodeUserSyncJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $tries = 2;
|
||||||
|
public $timeout = 10;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly int $userId,
|
||||||
|
private readonly string $action,
|
||||||
|
private readonly ?int $oldGroupId = null
|
||||||
|
) {
|
||||||
|
$this->onQueue('node_sync');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$user = User::find($this->userId);
|
||||||
|
|
||||||
|
if ($this->action === 'updated' || $this->action === 'created') {
|
||||||
|
if ($this->oldGroupId) {
|
||||||
|
NodeSyncService::notifyUserRemovedFromGroup($this->userId, $this->oldGroupId);
|
||||||
|
}
|
||||||
|
if ($user) {
|
||||||
|
NodeSyncService::notifyUserChanged($user);
|
||||||
|
}
|
||||||
|
} elseif ($this->action === 'deleted') {
|
||||||
|
if ($this->oldGroupId) {
|
||||||
|
NodeSyncService::notifyUserRemovedFromGroup($this->userId, $this->oldGroupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ use Illuminate\Support\Facades\Cache;
|
|||||||
use App\Services\UserOnlineService;
|
use App\Services\UserOnlineService;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class UpdateAliveDataJob implements ShouldQueue
|
class UserAliveSyncJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class UpdateAliveDataJob implements ShouldQueue
|
|||||||
private readonly string $nodeType,
|
private readonly string $nodeType,
|
||||||
private readonly int $nodeId
|
private readonly int $nodeId
|
||||||
) {
|
) {
|
||||||
$this->onQueue('online_sync');
|
$this->onQueue('user_alive_sync');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
@@ -97,7 +97,7 @@ class UpdateAliveDataJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::error('UpdateAliveDataJob failed', [
|
Log::error('UserAliveSyncJob failed', [
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
$this->fail($e);
|
$this->fail($e);
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace App\Logging;
|
|
||||||
|
|
||||||
class MysqlLogger
|
|
||||||
{
|
|
||||||
public function __invoke(array $config){
|
|
||||||
return tap(new \Monolog\Logger('mysql'), function ($logger) {
|
|
||||||
$logger->pushHandler(new MysqlLoggerHandler());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace App\Logging;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Monolog\Handler\AbstractProcessingHandler;
|
|
||||||
use Monolog\Logger;
|
|
||||||
use App\Models\Log as LogModel;
|
|
||||||
use Monolog\LogRecord;
|
|
||||||
|
|
||||||
class MysqlLoggerHandler extends AbstractProcessingHandler
|
|
||||||
{
|
|
||||||
public function __construct($level = Logger::DEBUG, bool $bubble = true)
|
|
||||||
{
|
|
||||||
parent::__construct($level, $bubble);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function write(LogRecord $record): void
|
|
||||||
{
|
|
||||||
$record = $record->toArray();
|
|
||||||
try {
|
|
||||||
if (isset($record['context']['exception']) && is_object($record['context']['exception'])) {
|
|
||||||
$record['context']['exception'] = (array)$record['context']['exception'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$record['request_data'] = request()->all();
|
|
||||||
|
|
||||||
$log = [
|
|
||||||
'title' => $record['message'],
|
|
||||||
'level' => $record['level_name'],
|
|
||||||
'host' => $record['extra']['request_host'] ?? request()->getSchemeAndHttpHost(),
|
|
||||||
'uri' => $record['extra']['request_uri'] ?? request()->getRequestUri(),
|
|
||||||
'method' => $record['extra']['request_method'] ?? request()->getMethod(),
|
|
||||||
'ip' => request()->getClientIp(),
|
|
||||||
'data' => json_encode($record['request_data']),
|
|
||||||
'context' => json_encode($record['context']),
|
|
||||||
'created_at' => $record['datetime']->getTimestamp(),
|
|
||||||
'updated_at' => $record['datetime']->getTimestamp(),
|
|
||||||
];
|
|
||||||
|
|
||||||
LogModel::insert($log);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AdminAuditLog extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'v2_admin_audit_log';
|
||||||
|
protected $dateFormat = 'U';
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
protected $casts = [
|
||||||
|
'created_at' => 'timestamp',
|
||||||
|
'updated_at' => 'timestamp',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function admin()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'admin_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class Log extends Model
|
|
||||||
{
|
|
||||||
use \App\Scope\FilterScope;
|
|
||||||
protected $table = 'v2_log';
|
|
||||||
protected $dateFormat = 'U';
|
|
||||||
protected $guarded = ['id'];
|
|
||||||
protected $casts = [
|
|
||||||
'created_at' => 'timestamp',
|
|
||||||
'updated_at' => 'timestamp'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
+79
-8
@@ -41,6 +41,8 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
|||||||
* @property-read int|null $last_check_at 最后检查时间(Unix时间戳)
|
* @property-read int|null $last_check_at 最后检查时间(Unix时间戳)
|
||||||
* @property-read int|null $last_push_at 最后推送时间(Unix时间戳)
|
* @property-read int|null $last_push_at 最后推送时间(Unix时间戳)
|
||||||
* @property-read int $online 在线用户数
|
* @property-read int $online 在线用户数
|
||||||
|
* @property-read int $online_conn 在线连接数
|
||||||
|
* @property-read array|null $metrics 节点指标指标
|
||||||
* @property-read int $is_online 是否在线(1在线 0离线)
|
* @property-read int $is_online 是否在线(1在线 0离线)
|
||||||
* @property-read string $available_status 可用状态描述
|
* @property-read string $available_status 可用状态描述
|
||||||
* @property-read string $cache_key 缓存键
|
* @property-read string $cache_key 缓存键
|
||||||
@@ -112,6 +114,9 @@ class Server extends Model
|
|||||||
'route_ids' => 'array',
|
'route_ids' => 'array',
|
||||||
'tags' => 'array',
|
'tags' => 'array',
|
||||||
'protocol_settings' => 'array',
|
'protocol_settings' => 'array',
|
||||||
|
'custom_outbounds' => 'array',
|
||||||
|
'custom_routes' => 'array',
|
||||||
|
'cert_config' => 'array',
|
||||||
'last_check_at' => 'integer',
|
'last_check_at' => 'integer',
|
||||||
'last_push_at' => 'integer',
|
'last_push_at' => 'integer',
|
||||||
'show' => 'boolean',
|
'show' => 'boolean',
|
||||||
@@ -121,19 +126,55 @@ class Server extends Model
|
|||||||
'rate_time_enable' => 'boolean',
|
'rate_time_enable' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private const MULTIPLEX_CONFIGURATION = [
|
||||||
|
'multiplex' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'fields' => [
|
||||||
|
'enabled' => ['type' => 'boolean', 'default' => false],
|
||||||
|
'protocol' => ['type' => 'string', 'default' => 'yamux'],
|
||||||
|
'max_connections' => ['type' => 'integer', 'default' => null],
|
||||||
|
// 'min_streams' => ['type' => 'integer', 'default' => null],
|
||||||
|
// 'max_streams' => ['type' => 'integer', 'default' => null],
|
||||||
|
'padding' => ['type' => 'boolean', 'default' => false],
|
||||||
|
'brutal' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'fields' => [
|
||||||
|
'enabled' => ['type' => 'boolean', 'default' => false],
|
||||||
|
'up_mbps' => ['type' => 'integer', 'default' => null],
|
||||||
|
'down_mbps' => ['type' => 'integer', 'default' => null],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
private const UTLS_CONFIGURATION = [
|
||||||
|
'utls' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'fields' => [
|
||||||
|
'enabled' => ['type' => 'boolean', 'default' => false],
|
||||||
|
'fingerprint' => ['type' => 'string', 'default' => 'chrome'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
private const PROTOCOL_CONFIGURATIONS = [
|
private const PROTOCOL_CONFIGURATIONS = [
|
||||||
self::TYPE_TROJAN => [
|
self::TYPE_TROJAN => [
|
||||||
'allow_insecure' => ['type' => 'boolean', 'default' => false],
|
|
||||||
'server_name' => ['type' => 'string', 'default' => null],
|
|
||||||
'network' => ['type' => 'string', 'default' => null],
|
'network' => ['type' => 'string', 'default' => null],
|
||||||
'network_settings' => ['type' => 'array', 'default' => null]
|
'network_settings' => ['type' => 'array', 'default' => null],
|
||||||
|
'server_name' => ['type' => 'string', 'default' => null],
|
||||||
|
'allow_insecure' => ['type' => 'boolean', 'default' => false],
|
||||||
|
...self::MULTIPLEX_CONFIGURATION,
|
||||||
|
...self::UTLS_CONFIGURATION
|
||||||
],
|
],
|
||||||
self::TYPE_VMESS => [
|
self::TYPE_VMESS => [
|
||||||
'tls' => ['type' => 'integer', 'default' => 0],
|
'tls' => ['type' => 'integer', 'default' => 0],
|
||||||
'network' => ['type' => 'string', 'default' => null],
|
'network' => ['type' => 'string', 'default' => null],
|
||||||
'rules' => ['type' => 'array', 'default' => null],
|
'rules' => ['type' => 'array', 'default' => null],
|
||||||
'network_settings' => ['type' => 'array', 'default' => null],
|
'network_settings' => ['type' => 'array', 'default' => null],
|
||||||
'tls_settings' => ['type' => 'array', 'default' => null]
|
'tls_settings' => ['type' => 'array', 'default' => null],
|
||||||
|
...self::MULTIPLEX_CONFIGURATION,
|
||||||
|
...self::UTLS_CONFIGURATION
|
||||||
],
|
],
|
||||||
self::TYPE_VLESS => [
|
self::TYPE_VLESS => [
|
||||||
'tls' => ['type' => 'integer', 'default' => 0],
|
'tls' => ['type' => 'integer', 'default' => 0],
|
||||||
@@ -151,7 +192,9 @@ class Server extends Model
|
|||||||
'private_key' => ['type' => 'string', 'default' => null],
|
'private_key' => ['type' => 'string', 'default' => null],
|
||||||
'short_id' => ['type' => 'string', 'default' => null]
|
'short_id' => ['type' => 'string', 'default' => null]
|
||||||
]
|
]
|
||||||
]
|
],
|
||||||
|
...self::MULTIPLEX_CONFIGURATION,
|
||||||
|
...self::UTLS_CONFIGURATION
|
||||||
],
|
],
|
||||||
self::TYPE_SHADOWSOCKS => [
|
self::TYPE_SHADOWSOCKS => [
|
||||||
'cipher' => ['type' => 'string', 'default' => null],
|
'cipher' => ['type' => 'string', 'default' => null],
|
||||||
@@ -240,13 +283,15 @@ class Server extends Model
|
|||||||
'tls_settings' => [
|
'tls_settings' => [
|
||||||
'type' => 'object',
|
'type' => 'object',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'allow_insecure' => ['type' => 'boolean', 'default' => false]
|
'allow_insecure' => ['type' => 'boolean', 'default' => false],
|
||||||
|
'server_name' => ['type' => 'string', 'default' => null]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
self::TYPE_MIERU => [
|
self::TYPE_MIERU => [
|
||||||
'transport' => ['type' => 'string', 'default' => 'tcp'],
|
'transport' => ['type' => 'string', 'default' => 'TCP'],
|
||||||
'multiplexing' => ['type' => 'string', 'default' => 'MULTIPLEXING_LOW']
|
'traffic_pattern' => ['type' => 'string', 'default' => ''],
|
||||||
|
...self::MULTIPLEX_CONFIGURATION,
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -440,6 +485,32 @@ class Server extends Model
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指标指标访问器
|
||||||
|
*/
|
||||||
|
protected function metrics(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: function () {
|
||||||
|
$type = strtoupper($this->type);
|
||||||
|
$serverId = $this->parent_id ?: $this->id;
|
||||||
|
return Cache::get(CacheKey::get("SERVER_{$type}_METRICS", $serverId));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线连接数访问器
|
||||||
|
*/
|
||||||
|
protected function onlineConn(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: function () {
|
||||||
|
return $this->metrics['active_connections'] ?? 0;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 负载状态访问器
|
* 负载状态访问器
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class SubscribeTemplate extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'v2_subscribe_templates';
|
||||||
|
protected $guarded = [];
|
||||||
|
protected $casts = [
|
||||||
|
'name' => 'string',
|
||||||
|
'content' => 'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static string $cachePrefix = 'subscribe_template:';
|
||||||
|
|
||||||
|
public static function getContent(string $name): ?string
|
||||||
|
{
|
||||||
|
$cacheKey = self::$cachePrefix . $name;
|
||||||
|
|
||||||
|
return Cache::store('redis')->remember($cacheKey, 3600, function () use ($name) {
|
||||||
|
return self::where('name', $name)->value('content');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setContent(string $name, ?string $content): void
|
||||||
|
{
|
||||||
|
self::updateOrCreate(
|
||||||
|
['name' => $name],
|
||||||
|
['content' => $content]
|
||||||
|
);
|
||||||
|
Cache::store('redis')->forget(self::$cachePrefix . $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAllContents(): array
|
||||||
|
{
|
||||||
|
return self::pluck('content', 'name')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function flushCache(string $name): void
|
||||||
|
{
|
||||||
|
Cache::store('redis')->forget(self::$cachePrefix . $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,6 +147,14 @@ class User extends Authenticatable
|
|||||||
$this->plan_id !== null;
|
$this->plan_id !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否可用节点流量且充足
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return $this->isActive() && $this->getRemainingTraffic() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否需要重置流量
|
* 检查是否需要重置流量
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Plan;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\TrafficResetService;
|
||||||
|
|
||||||
|
class PlanObserver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* reset user next_reset_at
|
||||||
|
*/
|
||||||
|
public function updated(Plan $plan): void
|
||||||
|
{
|
||||||
|
if (!$plan->isDirty('reset_traffic_method')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$trafficResetService = app(TrafficResetService::class);
|
||||||
|
User::where('plan_id', $plan->id)
|
||||||
|
->where('banned', 0)
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->where('expired_at', '>', time())
|
||||||
|
->orWhereNull('expired_at');
|
||||||
|
})
|
||||||
|
->lazyById(500)
|
||||||
|
->each(function (User $user) use ($trafficResetService) {
|
||||||
|
$nextResetTime = $trafficResetService->calculateNextResetTime($user);
|
||||||
|
$user->update([
|
||||||
|
'next_reset_at' => $nextResetTime?->timestamp,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Services\NodeSyncService;
|
||||||
|
|
||||||
|
class ServerObserver
|
||||||
|
{
|
||||||
|
public function updated(Server $server): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
$server->isDirty([
|
||||||
|
'group_ids',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
NodeSyncService::notifyUsersUpdatedByGroup($server->id);
|
||||||
|
} else if (
|
||||||
|
$server->isDirty([
|
||||||
|
'server_port',
|
||||||
|
'protocol_settings',
|
||||||
|
'type',
|
||||||
|
'route_ids',
|
||||||
|
'custom_outbounds',
|
||||||
|
'custom_routes',
|
||||||
|
'cert_config',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
NodeSyncService::notifyConfigUpdated($server->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(Server $server): void
|
||||||
|
{
|
||||||
|
NodeSyncService::notifyConfigUpdated($server->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\ServerRoute;
|
||||||
|
use App\Services\NodeSyncService;
|
||||||
|
|
||||||
|
class ServerRouteObserver
|
||||||
|
{
|
||||||
|
public function updated(ServerRoute $route): void
|
||||||
|
{
|
||||||
|
$this->notifyAffectedNodes($route->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(ServerRoute $route): void
|
||||||
|
{
|
||||||
|
$this->notifyAffectedNodes($route->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyAffectedNodes(int $routeId): void
|
||||||
|
{
|
||||||
|
$servers = Server::where('show', 1)->get()->filter(
|
||||||
|
fn ($s) => in_array($routeId, $s->route_ids ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
NodeSyncService::notifyConfigUpdated($server->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Jobs\NodeUserSyncJob;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\TrafficResetService;
|
use App\Services\TrafficResetService;
|
||||||
|
|
||||||
@@ -15,12 +16,38 @@ class UserObserver
|
|||||||
public function updated(User $user): void
|
public function updated(User $user): void
|
||||||
{
|
{
|
||||||
if ($user->isDirty(['plan_id', 'expired_at'])) {
|
if ($user->isDirty(['plan_id', 'expired_at'])) {
|
||||||
$user->refresh();
|
$this->recalculateNextResetAt($user);
|
||||||
User::withoutEvents(function () use ($user) {
|
}
|
||||||
$nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
|
|
||||||
$user->next_reset_at = $nextResetTime?->timestamp;
|
if ($user->isDirty(['group_id', 'uuid', 'speed_limit', 'device_limit', 'banned', 'expired_at', 'transfer_enable', 'u', 'd', 'plan_id'])) {
|
||||||
$user->save();
|
$oldGroupId = $user->isDirty('group_id') ? $user->getOriginal('group_id') : null;
|
||||||
});
|
NodeUserSyncJob::dispatch($user->id, 'updated', $oldGroupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function created(User $user): void
|
||||||
|
{
|
||||||
|
$this->recalculateNextResetAt($user);
|
||||||
|
NodeUserSyncJob::dispatch($user->id, 'created');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(User $user): void
|
||||||
|
{
|
||||||
|
if ($user->group_id) {
|
||||||
|
NodeUserSyncJob::dispatch($user->id, 'deleted', $user->group_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前用户状态重新计算 next_reset_at
|
||||||
|
*/
|
||||||
|
private function recalculateNextResetAt(User $user): void
|
||||||
|
{
|
||||||
|
$user->refresh();
|
||||||
|
User::withoutEvents(function () use ($user) {
|
||||||
|
$nextResetTime = $this->trafficResetService->calculateNextResetTime($user);
|
||||||
|
$user->next_reset_at = $nextResetTime?->timestamp;
|
||||||
|
$user->save();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,7 @@ class Clash extends AbstractProtocol
|
|||||||
$appName = admin_setting('app_name', 'XBoard');
|
$appName = admin_setting('app_name', 'XBoard');
|
||||||
|
|
||||||
// 优先从数据库配置中获取模板
|
// 优先从数据库配置中获取模板
|
||||||
$template = admin_setting('subscribe_template_clash', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
$template = subscribe_template('clash');
|
||||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
|
||||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
|
|
||||||
|
|
||||||
$config = Yaml::parse($template);
|
$config = Yaml::parse($template);
|
||||||
$proxy = [];
|
$proxy = [];
|
||||||
|
|||||||
+149
-31
@@ -35,6 +35,7 @@ class ClashMeta extends AbstractProtocol
|
|||||||
'grpc' => '0.0.0',
|
'grpc' => '0.0.0',
|
||||||
'http' => '0.0.0',
|
'http' => '0.0.0',
|
||||||
'h2' => '0.0.0',
|
'h2' => '0.0.0',
|
||||||
|
'httpupgrade' => '0.0.0',
|
||||||
],
|
],
|
||||||
'strict' => true,
|
'strict' => true,
|
||||||
],
|
],
|
||||||
@@ -65,13 +66,7 @@ class ClashMeta extends AbstractProtocol
|
|||||||
$user = $this->user;
|
$user = $this->user;
|
||||||
$appName = admin_setting('app_name', 'XBoard');
|
$appName = admin_setting('app_name', 'XBoard');
|
||||||
|
|
||||||
$template = admin_setting('subscribe_template_clashmeta', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
$template = subscribe_template('clashmeta');
|
||||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
|
||||||
: (
|
|
||||||
File::exists(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
|
|
||||||
? File::get(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
|
|
||||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE))
|
|
||||||
));
|
|
||||||
|
|
||||||
$config = Yaml::parse($template);
|
$config = Yaml::parse($template);
|
||||||
$proxy = [];
|
$proxy = [];
|
||||||
@@ -199,7 +194,7 @@ class ClashMeta extends AbstractProtocol
|
|||||||
->filter()
|
->filter()
|
||||||
->mapWithKeys(function ($pair) {
|
->mapWithKeys(function ($pair) {
|
||||||
if (!str_contains($pair, '=')) {
|
if (!str_contains($pair, '=')) {
|
||||||
return [];
|
return [trim($pair) => true];
|
||||||
}
|
}
|
||||||
[$key, $value] = explode('=', $pair, 2);
|
[$key, $value] = explode('=', $pair, 2);
|
||||||
return [trim($key) => trim($value)];
|
return [trim($key) => trim($value)];
|
||||||
@@ -209,28 +204,42 @@ class ClashMeta extends AbstractProtocol
|
|||||||
// 根据插件类型进行字段映射
|
// 根据插件类型进行字段映射
|
||||||
switch ($plugin) {
|
switch ($plugin) {
|
||||||
case 'obfs':
|
case 'obfs':
|
||||||
$array['plugin-opts'] = [
|
case 'obfs-local':
|
||||||
'mode' => $parsedOpts['obfs'],
|
$array['plugin'] = 'obfs';
|
||||||
'host' => $parsedOpts['obfs-host'],
|
$array['plugin-opts'] = array_filter([
|
||||||
];
|
'mode' => $parsedOpts['obfs'] ?? ($parsedOpts['mode'] ?? 'http'),
|
||||||
|
'host' => $parsedOpts['obfs-host'] ?? ($parsedOpts['host'] ?? 'www.bing.com'),
|
||||||
// 可选path参数
|
]);
|
||||||
if (isset($parsedOpts['path'])) {
|
|
||||||
$array['plugin-opts']['path'] = $parsedOpts['path'];
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'v2ray-plugin':
|
case 'v2ray-plugin':
|
||||||
$array['plugin-opts'] = [
|
$array['plugin-opts'] = array_filter([
|
||||||
'mode' => $parsedOpts['mode'] ?? 'websocket',
|
'mode' => $parsedOpts['mode'] ?? 'websocket',
|
||||||
'tls' => isset($parsedOpts['tls']) && $parsedOpts['tls'] == 'true',
|
'tls' => isset($parsedOpts['tls']) || isset($parsedOpts['server']),
|
||||||
'host' => $parsedOpts['host'] ?? '',
|
'host' => $parsedOpts['host'] ?? null,
|
||||||
'path' => $parsedOpts['path'] ?? '/',
|
'path' => $parsedOpts['path'] ?? '/',
|
||||||
];
|
'mux' => isset($parsedOpts['mux']) ? true : null,
|
||||||
|
'headers' => isset($parsedOpts['host']) ? ['Host' => $parsedOpts['host']] : null
|
||||||
|
], fn($v) => $v !== null);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'shadow-tls':
|
||||||
|
$array['plugin-opts'] = array_filter([
|
||||||
|
'host' => $parsedOpts['host'] ?? null,
|
||||||
|
'password' => $parsedOpts['password'] ?? null,
|
||||||
|
'version' => isset($parsedOpts['version']) ? (int) $parsedOpts['version'] : 2
|
||||||
|
], fn($v) => $v !== null);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'restls':
|
||||||
|
$array['plugin-opts'] = array_filter([
|
||||||
|
'host' => $parsedOpts['host'] ?? null,
|
||||||
|
'password' => $parsedOpts['password'] ?? null,
|
||||||
|
'restls-script' => $parsedOpts['restls-script'] ?? '123'
|
||||||
|
], fn($v) => $v !== null);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 对于其他插件,直接使用解析出的键值对
|
|
||||||
$array['plugin-opts'] = $parsedOpts;
|
$array['plugin-opts'] = $parsedOpts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,19 +261,24 @@ class ClashMeta extends AbstractProtocol
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (data_get($protocol_settings, 'tls')) {
|
if (data_get($protocol_settings, 'tls')) {
|
||||||
$array['tls'] = true;
|
$array['tls'] = (bool) data_get($protocol_settings, 'tls');
|
||||||
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
|
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
|
||||||
$array['servername'] = data_get($protocol_settings, 'tls_settings.server_name');
|
$array['servername'] = data_get($protocol_settings, 'tls_settings.server_name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self::appendUtls($array, $protocol_settings);
|
||||||
|
self::appendMultiplex($array, $protocol_settings);
|
||||||
|
|
||||||
switch (data_get($protocol_settings, 'network')) {
|
switch (data_get($protocol_settings, 'network')) {
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
$array['network'] = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
|
$array['network'] = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
|
||||||
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
|
||||||
if ($httpOpts = array_filter([
|
if (
|
||||||
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
|
$httpOpts = array_filter([
|
||||||
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
|
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
|
||||||
])) {
|
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
|
||||||
|
])
|
||||||
|
) {
|
||||||
$array['http-opts'] = $httpOpts;
|
$array['http-opts'] = $httpOpts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,6 +295,22 @@ class ClashMeta extends AbstractProtocol
|
|||||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||||
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
||||||
break;
|
break;
|
||||||
|
case 'h2':
|
||||||
|
$array['network'] = 'h2';
|
||||||
|
$array['h2-opts'] = [];
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['h2-opts']['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$array['h2-opts']['host'] = is_array($host) ? $host : [$host];
|
||||||
|
break;
|
||||||
|
case 'httpupgrade':
|
||||||
|
$array['network'] = 'ws';
|
||||||
|
$array['ws-opts'] = ['v2ray-http-upgrade' => true];
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['ws-opts']['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$array['ws-opts']['headers'] = ['Host' => $host];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -311,6 +341,7 @@ class ClashMeta extends AbstractProtocol
|
|||||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
$array['servername'] = $serverName;
|
$array['servername'] = $serverName;
|
||||||
}
|
}
|
||||||
|
self::appendUtls($array, $protocol_settings);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
$array['tls'] = true;
|
$array['tls'] = true;
|
||||||
@@ -320,13 +351,28 @@ class ClashMeta extends AbstractProtocol
|
|||||||
'public-key' => data_get($protocol_settings, 'reality_settings.public_key'),
|
'public-key' => data_get($protocol_settings, 'reality_settings.public_key'),
|
||||||
'short-id' => data_get($protocol_settings, 'reality_settings.short_id')
|
'short-id' => data_get($protocol_settings, 'reality_settings.short_id')
|
||||||
];
|
];
|
||||||
$array['client-fingerprint'] = Helper::getRandFingerprint();
|
self::appendUtls($array, $protocol_settings);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data_get($protocol_settings, 'network')) {
|
switch (data_get($protocol_settings, 'network')) {
|
||||||
|
case 'tcp':
|
||||||
|
$array['network'] = 'tcp';
|
||||||
|
$headerType = data_get($protocol_settings, 'network_settings.header.type', 'none');
|
||||||
|
if ($headerType === 'http') {
|
||||||
|
$array['network'] = 'http';
|
||||||
|
if (
|
||||||
|
$httpOpts = array_filter([
|
||||||
|
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
|
||||||
|
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
$array['http-opts'] = $httpOpts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'ws':
|
case 'ws':
|
||||||
$array['network'] = 'ws';
|
$array['network'] = 'ws';
|
||||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
@@ -339,10 +385,28 @@ class ClashMeta extends AbstractProtocol
|
|||||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||||
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
||||||
break;
|
break;
|
||||||
|
case 'h2':
|
||||||
|
$array['network'] = 'h2';
|
||||||
|
$array['h2-opts'] = [];
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['h2-opts']['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$array['h2-opts']['host'] = is_array($host) ? $host : [$host];
|
||||||
|
break;
|
||||||
|
case 'httpupgrade':
|
||||||
|
$array['network'] = 'ws';
|
||||||
|
$array['ws-opts'] = ['v2ray-http-upgrade' => true];
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['ws-opts']['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$array['ws-opts']['headers'] = ['Host' => $host];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self::appendMultiplex($array, $protocol_settings);
|
||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +426,9 @@ class ClashMeta extends AbstractProtocol
|
|||||||
$array['sni'] = $serverName;
|
$array['sni'] = $serverName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self::appendUtls($array, $protocol_settings);
|
||||||
|
self::appendMultiplex($array, $protocol_settings);
|
||||||
|
|
||||||
switch (data_get($protocol_settings, 'network')) {
|
switch (data_get($protocol_settings, 'network')) {
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
$array['network'] = 'tcp';
|
$array['network'] = 'tcp';
|
||||||
@@ -378,6 +445,22 @@ class ClashMeta extends AbstractProtocol
|
|||||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||||
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
||||||
break;
|
break;
|
||||||
|
case 'h2':
|
||||||
|
$array['network'] = 'h2';
|
||||||
|
$array['h2-opts'] = [];
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['h2-opts']['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$array['h2-opts']['host'] = is_array($host) ? $host : [$host];
|
||||||
|
break;
|
||||||
|
case 'httpupgrade':
|
||||||
|
$array['network'] = 'ws';
|
||||||
|
$array['ws-opts'] = ['v2ray-http-upgrade' => true];
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['ws-opts']['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$array['ws-opts']['headers'] = ['Host' => $host];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$array['network'] = 'tcp';
|
$array['network'] = 'tcp';
|
||||||
break;
|
break;
|
||||||
@@ -401,6 +484,9 @@ class ClashMeta extends AbstractProtocol
|
|||||||
if (isset($server['ports'])) {
|
if (isset($server['ports'])) {
|
||||||
$array['ports'] = $server['ports'];
|
$array['ports'] = $server['ports'];
|
||||||
}
|
}
|
||||||
|
if ($hopInterval = data_get($protocol_settings, 'hop_interval')) {
|
||||||
|
$array['hop-interval'] = (int) $hopInterval;
|
||||||
|
}
|
||||||
switch (data_get($protocol_settings, 'version')) {
|
switch (data_get($protocol_settings, 'version')) {
|
||||||
case 1:
|
case 1:
|
||||||
$array['type'] = 'hysteria';
|
$array['type'] = 'hysteria';
|
||||||
@@ -491,8 +577,7 @@ class ClashMeta extends AbstractProtocol
|
|||||||
'port' => $server['port'],
|
'port' => $server['port'],
|
||||||
'username' => $password,
|
'username' => $password,
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP')),
|
'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP'))
|
||||||
'multiplexing' => data_get($protocol_settings, 'multiplexing', 'MULTIPLEXING_LOW')
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 如果配置了端口范围
|
// 如果配置了端口范围
|
||||||
@@ -566,4 +651,37 @@ class ClashMeta extends AbstractProtocol
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
protected static function appendMultiplex(&$array, $protocol_settings)
|
||||||
|
{
|
||||||
|
if ($multiplex = data_get($protocol_settings, 'multiplex')) {
|
||||||
|
if (data_get($multiplex, 'enabled')) {
|
||||||
|
$array['smux'] = array_filter([
|
||||||
|
'enabled' => true,
|
||||||
|
'protocol' => data_get($multiplex, 'protocol', 'yamux'),
|
||||||
|
'max-connections' => data_get($multiplex, 'max_connections'),
|
||||||
|
// 'min-streams' => data_get($multiplex, 'min_streams'),
|
||||||
|
// 'max-streams' => data_get($multiplex, 'max_streams'),
|
||||||
|
'padding' => data_get($multiplex, 'padding') ? true : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (data_get($multiplex, 'brutal.enabled')) {
|
||||||
|
$array['smux']['brutal-opts'] = [
|
||||||
|
'enabled' => true,
|
||||||
|
'up' => data_get($multiplex, 'brutal.up_mbps'),
|
||||||
|
'down' => data_get($multiplex, 'brutal.down_mbps'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function appendUtls(&$array, $protocol_settings)
|
||||||
|
{
|
||||||
|
if ($utls = data_get($protocol_settings, 'utls')) {
|
||||||
|
if (data_get($utls, 'enabled')) {
|
||||||
|
$array['client-fingerprint'] = Helper::getTlsFingerprint($utls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+164
-18
@@ -17,7 +17,10 @@ class General extends AbstractProtocol
|
|||||||
Server::TYPE_SHADOWSOCKS,
|
Server::TYPE_SHADOWSOCKS,
|
||||||
Server::TYPE_TROJAN,
|
Server::TYPE_TROJAN,
|
||||||
Server::TYPE_HYSTERIA,
|
Server::TYPE_HYSTERIA,
|
||||||
|
Server::TYPE_ANYTLS,
|
||||||
Server::TYPE_SOCKS,
|
Server::TYPE_SOCKS,
|
||||||
|
Server::TYPE_TUIC,
|
||||||
|
Server::TYPE_HTTP,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $protocolRequirements = [
|
protected $protocolRequirements = [
|
||||||
@@ -38,7 +41,10 @@ class General extends AbstractProtocol
|
|||||||
Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item),
|
Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item),
|
||||||
Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item),
|
Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item),
|
||||||
Server::TYPE_HYSTERIA => self::buildHysteria($item['password'], $item),
|
Server::TYPE_HYSTERIA => self::buildHysteria($item['password'], $item),
|
||||||
|
Server::TYPE_ANYTLS => self::buildAnyTLS($item['password'], $item),
|
||||||
Server::TYPE_SOCKS => self::buildSocks($item['password'], $item),
|
Server::TYPE_SOCKS => self::buildSocks($item['password'], $item),
|
||||||
|
Server::TYPE_TUIC => self::buildTuic($item['password'], $item),
|
||||||
|
Server::TYPE_HTTP => self::buildHttp($item['password'], $item),
|
||||||
default => '',
|
default => '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -109,6 +115,21 @@ class General extends AbstractProtocol
|
|||||||
if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
|
if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||||
$config['path'] = $path;
|
$config['path'] = $path;
|
||||||
break;
|
break;
|
||||||
|
case 'h2':
|
||||||
|
$config['net'] = 'h2';
|
||||||
|
$config['type'] = 'h2';
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$config['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$config['host'] = is_array($host) ? implode(',', $host) : $host;
|
||||||
|
break;
|
||||||
|
case 'httpupgrade':
|
||||||
|
$config['net'] = 'httpupgrade';
|
||||||
|
$config['type'] = 'httpupgrade';
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$config['path'] = $path;
|
||||||
|
$config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -133,6 +154,9 @@ class General extends AbstractProtocol
|
|||||||
switch ($server['protocol_settings']['tls']) {
|
switch ($server['protocol_settings']['tls']) {
|
||||||
case 1:
|
case 1:
|
||||||
$config['security'] = "tls";
|
$config['security'] = "tls";
|
||||||
|
if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
|
||||||
|
$config['fp'] = $fp;
|
||||||
|
}
|
||||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
$config['sni'] = $serverName;
|
$config['sni'] = $serverName;
|
||||||
}
|
}
|
||||||
@@ -144,7 +168,9 @@ class General extends AbstractProtocol
|
|||||||
$config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
|
$config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
|
||||||
$config['servername'] = data_get($protocol_settings, 'reality_settings.server_name');
|
$config['servername'] = data_get($protocol_settings, 'reality_settings.server_name');
|
||||||
$config['spx'] = "/";
|
$config['spx'] = "/";
|
||||||
$config['fp'] = Helper::getRandFingerprint();
|
if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
|
||||||
|
$config['fp'] = $fp;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -161,6 +187,13 @@ class General extends AbstractProtocol
|
|||||||
if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
|
if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||||
$config['serviceName'] = $path;
|
$config['serviceName'] = $path;
|
||||||
break;
|
break;
|
||||||
|
case 'h2':
|
||||||
|
$config['type'] = 'http';
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$config['path'] = $path;
|
||||||
|
if ($h2Host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$config['host'] = is_array($h2Host) ? implode(',', $h2Host) : $h2Host;
|
||||||
|
break;
|
||||||
case 'kcp':
|
case 'kcp':
|
||||||
if ($path = data_get($protocol_settings, 'network_settings.seed'))
|
if ($path = data_get($protocol_settings, 'network_settings.seed'))
|
||||||
$config['path'] = $path;
|
$config['path'] = $path;
|
||||||
@@ -210,6 +243,19 @@ class General extends AbstractProtocol
|
|||||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||||
$array['serviceName'] = $serviceName;
|
$array['serviceName'] = $serviceName;
|
||||||
break;
|
break;
|
||||||
|
case 'h2':
|
||||||
|
$array['type'] = 'http';
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['path'] = $path;
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||||
|
$array['host'] = is_array($host) ? implode(',', $host) : $host;
|
||||||
|
break;
|
||||||
|
case 'httpupgrade':
|
||||||
|
$array['type'] = 'httpupgrade';
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||||
|
$array['path'] = $path;
|
||||||
|
$array['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -225,40 +271,140 @@ class General extends AbstractProtocol
|
|||||||
{
|
{
|
||||||
$protocol_settings = $server['protocol_settings'];
|
$protocol_settings = $server['protocol_settings'];
|
||||||
$params = [];
|
$params = [];
|
||||||
// Return empty if version is not 2
|
$version = data_get($protocol_settings, 'version', 2);
|
||||||
if ($server['protocol_settings']['version'] !== 2) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
|
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
|
||||||
$params['sni'] = $serverName;
|
$params['sni'] = $serverName;
|
||||||
$params['security'] = 'tls';
|
|
||||||
}
|
}
|
||||||
|
$params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure') ? '1' : '0';
|
||||||
|
|
||||||
if (data_get($protocol_settings, 'obfs.open')) {
|
|
||||||
$params['obfs'] = 'salamander';
|
|
||||||
$params['obfs-password'] = data_get($protocol_settings, 'obfs.password');
|
|
||||||
}
|
|
||||||
if (isset($server['ports'])) {
|
|
||||||
$params['mport'] = $server['ports'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure');
|
|
||||||
|
|
||||||
$query = http_build_query($params);
|
|
||||||
$name = rawurlencode($server['name']);
|
$name = rawurlencode($server['name']);
|
||||||
$addr = Helper::wrapIPv6($server['host']);
|
$addr = Helper::wrapIPv6($server['host']);
|
||||||
|
|
||||||
$uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
|
if ($version === 2) {
|
||||||
|
if (data_get($protocol_settings, 'obfs.open')) {
|
||||||
|
$params['obfs'] = 'salamander';
|
||||||
|
$params['obfs-password'] = data_get($protocol_settings, 'obfs.password');
|
||||||
|
}
|
||||||
|
if (isset($server['ports'])) {
|
||||||
|
$params['mport'] = $server['ports'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = http_build_query($params);
|
||||||
|
$uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
|
||||||
|
} else {
|
||||||
|
$params['protocol'] = 'udp';
|
||||||
|
$params['auth'] = $password;
|
||||||
|
if ($upMbps = data_get($protocol_settings, 'bandwidth.up'))
|
||||||
|
$params['upmbps'] = $upMbps;
|
||||||
|
if ($downMbps = data_get($protocol_settings, 'bandwidth.down'))
|
||||||
|
$params['downmbps'] = $downMbps;
|
||||||
|
if ($obfsPassword = data_get($protocol_settings, 'obfs.password'))
|
||||||
|
$params['obfsParam'] = $obfsPassword;
|
||||||
|
|
||||||
|
$query = http_build_query($params);
|
||||||
|
$uri = "hysteria://{$addr}:{$server['port']}?{$query}#{$name}";
|
||||||
|
}
|
||||||
$uri .= "\r\n";
|
$uri .= "\r\n";
|
||||||
|
|
||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function buildTuic($password, $server)
|
||||||
|
{
|
||||||
|
$protocol_settings = data_get($server, 'protocol_settings', []);
|
||||||
|
$name = rawurlencode($server['name']);
|
||||||
|
$addr = Helper::wrapIPv6($server['host']);
|
||||||
|
$port = $server['port'];
|
||||||
|
$uuid = $password; // v2rayN格式里,uuid和password都是密码部分
|
||||||
|
$pass = $password;
|
||||||
|
|
||||||
|
$queryParams = [];
|
||||||
|
|
||||||
|
// 填充sni参数
|
||||||
|
if ($sni = data_get($protocol_settings, 'tls.server_name')) {
|
||||||
|
$queryParams['sni'] = $sni;
|
||||||
|
}
|
||||||
|
|
||||||
|
// alpn参数,支持多值时用逗号连接
|
||||||
|
if ($alpn = data_get($protocol_settings, 'alpn')) {
|
||||||
|
if (is_array($alpn)) {
|
||||||
|
$queryParams['alpn'] = implode(',', $alpn);
|
||||||
|
} else {
|
||||||
|
$queryParams['alpn'] = $alpn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// congestion_controller参数,默认cubic
|
||||||
|
$congestion = data_get($protocol_settings, 'congestion_control', 'cubic');
|
||||||
|
$queryParams['congestion_control'] = $congestion;
|
||||||
|
|
||||||
|
// udp_relay_mode参数,默认native
|
||||||
|
$udpRelay = data_get($protocol_settings, 'udp_relay_mode', 'native');
|
||||||
|
$queryParams['udp-relay-mode'] = $udpRelay;
|
||||||
|
|
||||||
|
$query = http_build_query($queryParams);
|
||||||
|
|
||||||
|
// 构造完整URI,格式:
|
||||||
|
// Tuic://uuid:password@host:port?sni=xxx&alpn=xxx&congestion_controller=xxx&udp_relay_mode=xxx#别名
|
||||||
|
$uri = "tuic://{$uuid}:{$pass}@{$addr}:{$port}";
|
||||||
|
|
||||||
|
if (!empty($query)) {
|
||||||
|
$uri .= "?{$query}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri .= "#{$name}\r\n";
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static function buildAnyTLS($password, $server)
|
||||||
|
{
|
||||||
|
$protocol_settings = $server['protocol_settings'];
|
||||||
|
$name = rawurlencode($server['name']);
|
||||||
|
$params = [
|
||||||
|
'sni' => data_get($protocol_settings, 'tls.server_name'),
|
||||||
|
'insecure' => data_get($protocol_settings, 'tls.allow_insecure')
|
||||||
|
];
|
||||||
|
$query = http_build_query($params);
|
||||||
|
$uri = "anytls://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
|
||||||
|
$uri .= "\r\n";
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
public static function buildSocks($password, $server)
|
public static function buildSocks($password, $server)
|
||||||
{
|
{
|
||||||
$name = rawurlencode($server['name']);
|
$name = rawurlencode($server['name']);
|
||||||
$credentials = base64_encode("{$password}:{$password}");
|
$credentials = base64_encode("{$password}:{$password}");
|
||||||
return "socks://{$credentials}@{$server['host']}:{$server['port']}#{$name}\r\n";
|
return "socks://{$credentials}@{$server['host']}:{$server['port']}#{$name}\r\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function buildHttp($password, $server)
|
||||||
|
{
|
||||||
|
$protocol_settings = data_get($server, 'protocol_settings', []);
|
||||||
|
$name = rawurlencode($server['name']);
|
||||||
|
$addr = Helper::wrapIPv6($server['host']);
|
||||||
|
$credentials = base64_encode("{$password}:{$password}");
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if (data_get($protocol_settings, 'tls')) {
|
||||||
|
$params['security'] = 'tls';
|
||||||
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
|
$params['sni'] = $serverName;
|
||||||
|
}
|
||||||
|
$params['allowInsecure'] = data_get($protocol_settings, 'tls_settings.allow_insecure') ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = "http://{$credentials}@{$addr}:{$server['port']}";
|
||||||
|
if (!empty($params)) {
|
||||||
|
$uri .= '?' . http_build_query($params);
|
||||||
|
}
|
||||||
|
$uri .= "#{$name}\r\n";
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-51
@@ -14,6 +14,7 @@ class Loon extends AbstractProtocol
|
|||||||
Server::TYPE_VMESS,
|
Server::TYPE_VMESS,
|
||||||
Server::TYPE_TROJAN,
|
Server::TYPE_TROJAN,
|
||||||
Server::TYPE_HYSTERIA,
|
Server::TYPE_HYSTERIA,
|
||||||
|
Server::TYPE_VLESS,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $protocolRequirements = [
|
protected $protocolRequirements = [
|
||||||
@@ -42,6 +43,9 @@ class Loon extends AbstractProtocol
|
|||||||
if ($item['type'] === Server::TYPE_HYSTERIA) {
|
if ($item['type'] === Server::TYPE_HYSTERIA) {
|
||||||
$uri .= self::buildHysteria($item['password'], $item, $user);
|
$uri .= self::buildHysteria($item['password'], $item, $user);
|
||||||
}
|
}
|
||||||
|
if ($item['type'] === Server::TYPE_VLESS) {
|
||||||
|
$uri .= self::buildVless($item['password'], $item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return response($uri)
|
return response($uri)
|
||||||
->header('content-type', 'text/plain')
|
->header('content-type', 'text/plain')
|
||||||
@@ -176,58 +180,77 @@ class Loon extends AbstractProtocol
|
|||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function buildVless($uuid, $server)
|
public static function buildVless($password, $server)
|
||||||
{
|
{
|
||||||
$protocol_settings = $server['protocol_settings'];
|
$protocol_settings = data_get($server, 'protocol_settings', []);
|
||||||
$config = [
|
|
||||||
"{$server['name']}=vless",
|
|
||||||
$server['host'],
|
|
||||||
$server['port'],
|
|
||||||
$uuid,
|
|
||||||
'fast-open=false',
|
|
||||||
'udp=true',
|
|
||||||
'alterId=0'
|
|
||||||
];
|
|
||||||
switch ((int) data_get($protocol_settings, 'tls')) {
|
|
||||||
case 1:
|
|
||||||
$config[] = 'over-tls=true';
|
|
||||||
$tlsSettings = data_get($protocol_settings, 'tls_settings', []);
|
|
||||||
if ($tlsSettings) {
|
|
||||||
$config[] = 'skip-cert-verify=' . (data_get($tlsSettings, 'allow_insecure') ? 'true' : 'false');
|
|
||||||
if ($serverName = data_get($tlsSettings, 'server_name')) {
|
|
||||||
$config[] = "tls-name={$serverName}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
$network_settings = data_get($protocol_settings, 'network_settings', []);
|
|
||||||
switch ((string) data_get($network_settings, 'network')) {
|
|
||||||
case 'tcp':
|
|
||||||
$config[] = 'transport=tcp';
|
|
||||||
if ($headerType = data_get($network_settings, 'header.type')) {
|
|
||||||
$config = collect($config)->map(function ($item) use ($headerType) {
|
|
||||||
return $item === 'transport=tcp' ? "transport={$headerType}" : $item;
|
|
||||||
})->toArray();
|
|
||||||
}
|
|
||||||
if ($paths = data_get($network_settings, 'header.request.path')) {
|
|
||||||
$config[] = 'path=' . $paths[array_rand($paths)];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ws':
|
|
||||||
$config[] = 'transport=ws';
|
|
||||||
if ($path = data_get($network_settings, 'path')) {
|
|
||||||
$config[] = "path={$path}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($host = data_get($network_settings, 'headers.Host')) {
|
$config = [
|
||||||
$config[] = "host={$host}";
|
"{$server['name']}=VLESS",
|
||||||
}
|
"{$server['host']}",
|
||||||
break;
|
"{$server['port']}",
|
||||||
}
|
"{$password}",
|
||||||
return implode(',', $config) . "\r\n";
|
"alterId=0",
|
||||||
}
|
"udp=true"
|
||||||
|
];
|
||||||
|
|
||||||
|
// flow
|
||||||
|
if ($flow = data_get($protocol_settings, 'flow')) {
|
||||||
|
$config[] = "flow={$flow}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS/Reality
|
||||||
|
switch (data_get($protocol_settings, 'tls')) {
|
||||||
|
case 1:
|
||||||
|
$config[] = "over-tls=true";
|
||||||
|
$config[] = "skip-cert-verify=" . (data_get($protocol_settings, 'tls_settings.allow_insecure', false) ? "true" : "false");
|
||||||
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
|
$config[] = "sni={$serverName}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$config[] = "over-tls=true";
|
||||||
|
$config[] = "skip-cert-verify=" . (data_get($protocol_settings, 'reality_settings.allow_insecure', false) ? "true" : "false");
|
||||||
|
if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) {
|
||||||
|
$config[] = "sni={$serverName}";
|
||||||
|
}
|
||||||
|
if ($pubkey = data_get($protocol_settings, 'reality_settings.public_key')) {
|
||||||
|
$config[] = "public-key={$pubkey}";
|
||||||
|
}
|
||||||
|
if ($shortid = data_get($protocol_settings, 'reality_settings.short_id')) {
|
||||||
|
$config[] = "short-id={$shortid}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$config[] = "over-tls=false";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// network
|
||||||
|
switch (data_get($protocol_settings, 'network')) {
|
||||||
|
case 'ws':
|
||||||
|
$config[] = "transport=ws";
|
||||||
|
if ($path = data_get($protocol_settings, 'network_settings.path')) {
|
||||||
|
$config[] = "path={$path}";
|
||||||
|
}
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) {
|
||||||
|
$config[] = "host={$host}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'grpc':
|
||||||
|
$config[] = "transport=grpc";
|
||||||
|
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) {
|
||||||
|
$config[] = "grpc-service-name={$serviceName}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$config[] = "transport=tcp";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = array_filter($config);
|
||||||
|
$uri = implode(',', $config) . "\r\n";
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
public static function buildHysteria($password, $server, $user)
|
public static function buildHysteria($password, $server, $user)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -165,13 +165,18 @@ class Shadowrocket extends AbstractProtocol
|
|||||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
$config['peer'] = $serverName;
|
$config['peer'] = $serverName;
|
||||||
}
|
}
|
||||||
|
if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
|
||||||
|
$config['fp'] = $fp;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
$config['tls'] = 1;
|
$config['tls'] = 1;
|
||||||
$config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
|
$config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
|
||||||
$config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key');
|
$config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key');
|
||||||
$config['sid'] = data_get($protocol_settings, 'reality_settings.short_id');
|
$config['sid'] = data_get($protocol_settings, 'reality_settings.short_id');
|
||||||
$config['fp'] = Helper::getRandFingerprint();
|
if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
|
||||||
|
$config['fp'] = $fp;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -356,8 +361,10 @@ class Shadowrocket extends AbstractProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function buildSocks($password, $server)
|
public static function buildSocks($password, $server)
|
||||||
{
|
{
|
||||||
$uri = "socks://" . base64_encode("{$password}:{$password}@{$server['host']}:{$server['port']}") . "?method=auto";
|
$protocol_settings = $server['protocol_settings'];
|
||||||
|
$name = rawurlencode($server['name']);
|
||||||
|
$uri = "socks://" . base64_encode("{$password}:{$password}@{$server['host']}:{$server['port']}") . "?method=auto#{$name}";
|
||||||
$uri .= "\r\n";
|
$uri .= "\r\n";
|
||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|||||||
+173
-18
@@ -3,7 +3,6 @@ namespace App\Protocols;
|
|||||||
|
|
||||||
use App\Utils\Helper;
|
use App\Utils\Helper;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use App\Support\AbstractProtocol;
|
use App\Support\AbstractProtocol;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
|
||||||
@@ -54,14 +53,14 @@ class SingBox extends AbstractProtocol
|
|||||||
'juicity' => [
|
'juicity' => [
|
||||||
'base_version' => '1.7.0'
|
'base_version' => '1.7.0'
|
||||||
],
|
],
|
||||||
'shadowtls' => [
|
|
||||||
'base_version' => '1.6.0'
|
|
||||||
],
|
|
||||||
'wireguard' => [
|
'wireguard' => [
|
||||||
'base_version' => '1.5.0'
|
'base_version' => '1.5.0'
|
||||||
],
|
],
|
||||||
'anytls' => [
|
'anytls' => [
|
||||||
'base_version' => '1.12.0'
|
'base_version' => '1.12.0'
|
||||||
|
],
|
||||||
|
'mieru' => [
|
||||||
|
'base_version' => '1.12.0'
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
@@ -72,6 +71,7 @@ class SingBox extends AbstractProtocol
|
|||||||
$this->config = $this->loadConfig();
|
$this->config = $this->loadConfig();
|
||||||
$this->buildOutbounds();
|
$this->buildOutbounds();
|
||||||
$this->buildRule();
|
$this->buildRule();
|
||||||
|
$this->adaptConfigForVersion();
|
||||||
$user = $this->user;
|
$user = $this->user;
|
||||||
|
|
||||||
return response()
|
return response()
|
||||||
@@ -83,9 +83,7 @@ class SingBox extends AbstractProtocol
|
|||||||
|
|
||||||
protected function loadConfig()
|
protected function loadConfig()
|
||||||
{
|
{
|
||||||
$jsonData = admin_setting('subscribe_template_singbox', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
$jsonData = subscribe_template('singbox');
|
||||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
|
||||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
|
|
||||||
|
|
||||||
return is_array($jsonData) ? $jsonData : json_decode($jsonData, true);
|
return is_array($jsonData) ? $jsonData : json_decode($jsonData, true);
|
||||||
}
|
}
|
||||||
@@ -135,6 +133,10 @@ class SingBox extends AbstractProtocol
|
|||||||
$httpConfig = $this->buildHttp($this->user['uuid'], $item);
|
$httpConfig = $this->buildHttp($this->user['uuid'], $item);
|
||||||
$proxies[] = $httpConfig;
|
$proxies[] = $httpConfig;
|
||||||
}
|
}
|
||||||
|
if ($item['type'] === Server::TYPE_MIERU) {
|
||||||
|
$mieruConfig = $this->buildMieru($this->user['uuid'], $item);
|
||||||
|
$proxies[] = $mieruConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
foreach ($outbounds as &$outbound) {
|
foreach ($outbounds as &$outbound) {
|
||||||
if (in_array($outbound['type'], ['urltest', 'selector'])) {
|
if (in_array($outbound['type'], ['urltest', 'selector'])) {
|
||||||
@@ -163,6 +165,91 @@ class SingBox extends AbstractProtocol
|
|||||||
$this->config['route']['rules'] = $rules;
|
$this->config['route']['rules'] = $rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据客户端版本自适应配置格式
|
||||||
|
*
|
||||||
|
* sing-box 版本断点:
|
||||||
|
* - 1.8.0: rule_set 替代 geoip/geosite db, cache_file 替代 clash_api.cache_file
|
||||||
|
* - 1.10.0: address 数组替代 inet4_address/inet6_address
|
||||||
|
* - 1.11.0: 移除 endpoint_independent_nat, sniff_override_destination
|
||||||
|
*/
|
||||||
|
protected function adaptConfigForVersion(): void
|
||||||
|
{
|
||||||
|
$coreVersion = $this->getSingBoxCoreVersion();
|
||||||
|
if (empty($coreVersion)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >= 1.11.0: 移除已废弃字段,避免 "配置已过时" 警告
|
||||||
|
if (version_compare($coreVersion, '1.11.0', '>=')) {
|
||||||
|
$this->removeDeprecatedFieldsV111();
|
||||||
|
}
|
||||||
|
|
||||||
|
// < 1.10.0: address 数组 → inet4_address/inet6_address
|
||||||
|
if (version_compare($coreVersion, '1.10.0', '<')) {
|
||||||
|
$this->convertAddressToLegacy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实际 sing-box 核心版本
|
||||||
|
*
|
||||||
|
* sing-box 客户端直接报核心版本,hiddify/sfm 等 wrapper 客户端
|
||||||
|
* 报的是 app 版本,需要映射到对应的 sing-box 核心版本
|
||||||
|
*/
|
||||||
|
private function getSingBoxCoreVersion(): ?string
|
||||||
|
{
|
||||||
|
if (empty($this->clientVersion)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sing-box 原生客户端,版本即核心版本
|
||||||
|
if ($this->clientName === 'sing-box') {
|
||||||
|
return $this->clientVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hiddify/SFM 等 wrapper 默认内置较新的 sing-box 核心
|
||||||
|
// 保守策略: 直接按最新格式输出(移除废弃字段),因为这些客户端普遍内置 >= 1.11 的核心
|
||||||
|
return '1.11.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sing-box >= 1.11.0: 移除废弃字段
|
||||||
|
*/
|
||||||
|
private function removeDeprecatedFieldsV111(): void
|
||||||
|
{
|
||||||
|
if (!isset($this->config['inbounds'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($this->config['inbounds'] as &$inbound) {
|
||||||
|
unset($inbound['endpoint_independent_nat']);
|
||||||
|
unset($inbound['sniff_override_destination']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sing-box < 1.10.0: 将 tun address 数组转换为 inet4_address/inet6_address
|
||||||
|
*/
|
||||||
|
private function convertAddressToLegacy(): void
|
||||||
|
{
|
||||||
|
if (!isset($this->config['inbounds'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($this->config['inbounds'] as &$inbound) {
|
||||||
|
if ($inbound['type'] !== 'tun' || !isset($inbound['address'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($inbound['address'] as $addr) {
|
||||||
|
if (str_contains($addr, ':')) {
|
||||||
|
$inbound['inet6_address'] = $addr;
|
||||||
|
} else {
|
||||||
|
$inbound['inet4_address'] = $addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($inbound['address']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function buildShadowsocks($password, $server)
|
protected function buildShadowsocks($password, $server)
|
||||||
{
|
{
|
||||||
$protocol_settings = data_get($server, 'protocol_settings');
|
$protocol_settings = data_get($server, 'protocol_settings');
|
||||||
@@ -193,16 +280,23 @@ class SingBox extends AbstractProtocol
|
|||||||
'uuid' => $uuid,
|
'uuid' => $uuid,
|
||||||
'security' => 'auto',
|
'security' => 'auto',
|
||||||
'alter_id' => 0,
|
'alter_id' => 0,
|
||||||
'transport' => [],
|
];
|
||||||
'tls' => $protocol_settings['tls'] ? [
|
|
||||||
|
if ($protocol_settings['tls']) {
|
||||||
|
$array['tls'] = [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'),
|
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'),
|
||||||
] : null
|
];
|
||||||
];
|
|
||||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
$this->appendUtls($array['tls'], $protocol_settings);
|
||||||
$array['tls']['server_name'] = $serverName;
|
|
||||||
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
|
$array['tls']['server_name'] = $serverName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->appendMultiplex($array, $protocol_settings);
|
||||||
|
|
||||||
$transport = match ($protocol_settings['network']) {
|
$transport = match ($protocol_settings['network']) {
|
||||||
'tcp' => data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none' ? [
|
'tcp' => data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none' ? [
|
||||||
'type' => 'http',
|
'type' => 'http',
|
||||||
@@ -220,6 +314,20 @@ class SingBox extends AbstractProtocol
|
|||||||
'type' => 'grpc',
|
'type' => 'grpc',
|
||||||
'service_name' => data_get($protocol_settings, 'network_settings.serviceName')
|
'service_name' => data_get($protocol_settings, 'network_settings.serviceName')
|
||||||
],
|
],
|
||||||
|
'h2' => [
|
||||||
|
'type' => 'http',
|
||||||
|
'host' => data_get($protocol_settings, 'network_settings.host'),
|
||||||
|
'path' => data_get($protocol_settings, 'network_settings.path')
|
||||||
|
],
|
||||||
|
'httpupgrade' => [
|
||||||
|
'type' => 'httpupgrade',
|
||||||
|
'path' => data_get($protocol_settings, 'network_settings.path'),
|
||||||
|
'host' => data_get($protocol_settings, 'network_settings.host', $server['host']),
|
||||||
|
'headers' => data_get($protocol_settings, 'network_settings.headers')
|
||||||
|
],
|
||||||
|
'quic' => [
|
||||||
|
'type' => 'quic'
|
||||||
|
],
|
||||||
default => null
|
default => null
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,12 +354,10 @@ class SingBox extends AbstractProtocol
|
|||||||
$tlsConfig = [
|
$tlsConfig = [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'),
|
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'),
|
||||||
'utls' => [
|
|
||||||
'enabled' => true,
|
|
||||||
'fingerprint' => Helper::getRandFingerprint()
|
|
||||||
]
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$this->appendUtls($tlsConfig, $protocol_settings);
|
||||||
|
|
||||||
switch ($protocol_settings['tls']) {
|
switch ($protocol_settings['tls']) {
|
||||||
case 1:
|
case 1:
|
||||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
@@ -271,6 +377,8 @@ class SingBox extends AbstractProtocol
|
|||||||
$array['tls'] = $tlsConfig;
|
$array['tls'] = $tlsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->appendMultiplex($array, $protocol_settings);
|
||||||
|
|
||||||
$transport = match ($protocol_settings['network']) {
|
$transport = match ($protocol_settings['network']) {
|
||||||
'tcp' => data_get($protocol_settings, 'network_settings.header.type') == 'http' ? [
|
'tcp' => data_get($protocol_settings, 'network_settings.header.type') == 'http' ? [
|
||||||
'type' => 'http',
|
'type' => 'http',
|
||||||
@@ -298,6 +406,9 @@ class SingBox extends AbstractProtocol
|
|||||||
'host' => data_get($protocol_settings, 'network_settings.host', $server['host']),
|
'host' => data_get($protocol_settings, 'network_settings.host', $server['host']),
|
||||||
'headers' => data_get($protocol_settings, 'network_settings.headers')
|
'headers' => data_get($protocol_settings, 'network_settings.headers')
|
||||||
],
|
],
|
||||||
|
'quic' => [
|
||||||
|
'type' => 'quic'
|
||||||
|
],
|
||||||
default => null
|
default => null
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -322,9 +433,15 @@ class SingBox extends AbstractProtocol
|
|||||||
'insecure' => (bool) data_get($protocol_settings, 'allow_insecure', false),
|
'insecure' => (bool) data_get($protocol_settings, 'allow_insecure', false),
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$this->appendUtls($array['tls'], $protocol_settings);
|
||||||
|
|
||||||
if ($serverName = data_get($protocol_settings, 'server_name')) {
|
if ($serverName = data_get($protocol_settings, 'server_name')) {
|
||||||
$array['tls']['server_name'] = $serverName;
|
$array['tls']['server_name'] = $serverName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->appendMultiplex($array, $protocol_settings);
|
||||||
|
|
||||||
$transport = match (data_get($protocol_settings, 'network')) {
|
$transport = match (data_get($protocol_settings, 'network')) {
|
||||||
'grpc' => [
|
'grpc' => [
|
||||||
'type' => 'grpc',
|
'type' => 'grpc',
|
||||||
@@ -339,7 +456,9 @@ class SingBox extends AbstractProtocol
|
|||||||
]),
|
]),
|
||||||
default => null
|
default => null
|
||||||
};
|
};
|
||||||
$array['transport'] = $transport;
|
if ($transport) {
|
||||||
|
$array['transport'] = array_filter($transport, fn($value) => !is_null($value));
|
||||||
|
}
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,4 +624,40 @@ class SingBox extends AbstractProtocol
|
|||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function appendMultiplex(&$array, $protocol_settings)
|
||||||
|
{
|
||||||
|
if ($multiplex = data_get($protocol_settings, 'multiplex')) {
|
||||||
|
if (data_get($multiplex, 'enabled')) {
|
||||||
|
$array['multiplex'] = [
|
||||||
|
'enabled' => true,
|
||||||
|
'protocol' => data_get($multiplex, 'protocol', 'yamux'),
|
||||||
|
'max_connections' => data_get($multiplex, 'max_connections'),
|
||||||
|
'min_streams' => data_get($multiplex, 'min_streams'),
|
||||||
|
'max_streams' => data_get($multiplex, 'max_streams'),
|
||||||
|
'padding' => (bool) data_get($multiplex, 'padding', false),
|
||||||
|
];
|
||||||
|
if (data_get($multiplex, 'brutal.enabled')) {
|
||||||
|
$array['multiplex']['brutal'] = [
|
||||||
|
'enabled' => true,
|
||||||
|
'up_mbps' => data_get($multiplex, 'brutal.up_mbps'),
|
||||||
|
'down_mbps' => data_get($multiplex, 'brutal.down_mbps'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$array['multiplex'] = array_filter($array['multiplex'], fn($v) => !is_null($v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function appendUtls(&$tlsConfig, $protocol_settings)
|
||||||
|
{
|
||||||
|
if ($utls = data_get($protocol_settings, 'utls')) {
|
||||||
|
if (data_get($utls, 'enabled')) {
|
||||||
|
$tlsConfig['utls'] = [
|
||||||
|
'enabled' => true,
|
||||||
|
'fingerprint' => Helper::getTlsFingerprint($utls)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-27
@@ -18,14 +18,14 @@ class Stash extends AbstractProtocol
|
|||||||
Server::TYPE_HYSTERIA,
|
Server::TYPE_HYSTERIA,
|
||||||
Server::TYPE_TROJAN,
|
Server::TYPE_TROJAN,
|
||||||
Server::TYPE_TUIC,
|
Server::TYPE_TUIC,
|
||||||
// Server::TYPE_ANYTLS,
|
Server::TYPE_ANYTLS,
|
||||||
Server::TYPE_SOCKS,
|
Server::TYPE_SOCKS,
|
||||||
Server::TYPE_HTTP,
|
Server::TYPE_HTTP,
|
||||||
];
|
];
|
||||||
protected $protocolRequirements = [
|
protected $protocolRequirements = [
|
||||||
'stash' => [
|
'stash' => [
|
||||||
'anytls' => [
|
'anytls' => [
|
||||||
'base_version' => '9.9.9'
|
'base_version' => '3.3.0' // AnyTLS 协议在3.3.0版本中添加
|
||||||
],
|
],
|
||||||
'vless' => [
|
'vless' => [
|
||||||
'protocol_settings.tls' => [
|
'protocol_settings.tls' => [
|
||||||
@@ -79,13 +79,7 @@ class Stash extends AbstractProtocol
|
|||||||
$user = $this->user;
|
$user = $this->user;
|
||||||
$appName = admin_setting('app_name', 'XBoard');
|
$appName = admin_setting('app_name', 'XBoard');
|
||||||
|
|
||||||
$template = admin_setting('subscribe_template_stash', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
$template = subscribe_template('stash');
|
||||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
|
||||||
: (
|
|
||||||
File::exists(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
|
|
||||||
? File::get(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
|
|
||||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE))
|
|
||||||
));
|
|
||||||
|
|
||||||
$config = Yaml::parse($template);
|
$config = Yaml::parse($template);
|
||||||
$proxy = [];
|
$proxy = [];
|
||||||
@@ -251,10 +245,13 @@ class Stash extends AbstractProtocol
|
|||||||
|
|
||||||
switch (data_get($protocol_settings, 'network')) {
|
switch (data_get($protocol_settings, 'network')) {
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
$array['network'] = data_get($protocol_settings, 'network_settings.header.type', 'http');
|
$headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
|
||||||
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
|
$array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
|
||||||
if ($host = data_get($protocol_settings, 'network_settings.header.request.headers.Host')) {
|
if ($headerType === 'http') {
|
||||||
$array['http-opts']['headers']['Host'] = $host;
|
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
|
||||||
|
if ($host = data_get($protocol_settings, 'network_settings.header.request.headers.Host')) {
|
||||||
|
$array['http-opts']['headers']['Host'] = $host;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'ws':
|
case 'ws':
|
||||||
@@ -286,7 +283,9 @@ class Stash extends AbstractProtocol
|
|||||||
$array['uuid'] = $uuid;
|
$array['uuid'] = $uuid;
|
||||||
$array['udp'] = true;
|
$array['udp'] = true;
|
||||||
|
|
||||||
$array['client-fingerprint'] = Helper::getRandFingerprint();
|
if ($fingerprint = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
|
||||||
|
$array['client-fingerprint'] = $fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
switch (data_get($protocol_settings, 'tls')) {
|
switch (data_get($protocol_settings, 'tls')) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -312,12 +311,15 @@ class Stash extends AbstractProtocol
|
|||||||
|
|
||||||
switch (data_get($protocol_settings, 'network')) {
|
switch (data_get($protocol_settings, 'network')) {
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
if ($headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp') != 'tcp') {
|
$headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
|
||||||
$array['network'] = $headerType;
|
$array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
|
||||||
if ($httpOpts = array_filter([
|
if ($headerType === 'http') {
|
||||||
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
|
if (
|
||||||
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
|
$httpOpts = array_filter([
|
||||||
])) {
|
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
|
||||||
|
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
|
||||||
|
])
|
||||||
|
) {
|
||||||
$array['http-opts'] = $httpOpts;
|
$array['http-opts'] = $httpOpts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,11 +335,11 @@ class Stash extends AbstractProtocol
|
|||||||
$array['network'] = 'grpc';
|
$array['network'] = 'grpc';
|
||||||
$array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName');
|
$array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName');
|
||||||
break;
|
break;
|
||||||
// case 'h2':
|
// case 'h2':
|
||||||
// $array['network'] = 'h2';
|
// $array['network'] = 'h2';
|
||||||
// $array['h2-opts']['host'] = data_get($protocol_settings, 'network_settings.host');
|
// $array['h2-opts']['host'] = data_get($protocol_settings, 'network_settings.host');
|
||||||
// $array['h2-opts']['path'] = data_get($protocol_settings, 'network_settings.path');
|
// $array['h2-opts']['path'] = data_get($protocol_settings, 'network_settings.path');
|
||||||
// break;
|
// break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
@@ -355,8 +357,11 @@ class Stash extends AbstractProtocol
|
|||||||
$array['udp'] = true;
|
$array['udp'] = true;
|
||||||
switch (data_get($protocol_settings, 'network')) {
|
switch (data_get($protocol_settings, 'network')) {
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
$array['network'] = data_get($protocol_settings, 'network_settings.header.type');
|
$headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
|
||||||
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
|
$array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
|
||||||
|
if ($headerType === 'http') {
|
||||||
|
$array['http-opts']['path'] = data_get($protocol_settings, 'network_settings.header.request.path', ['/']);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'ws':
|
case 'ws':
|
||||||
$array['network'] = 'ws';
|
$array['network'] = 'ws';
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ class Surfboard extends AbstractProtocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = admin_setting('subscribe_template_surfboard', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
$config = subscribe_template('surfboard');
|
||||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
|
||||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
|
|
||||||
// Subscription link
|
// Subscription link
|
||||||
$subsURL = Helper::getSubscribeUrl($user['token']);
|
$subsURL = Helper::getSubscribeUrl($user['token']);
|
||||||
$subsDomain = request()->header('Host');
|
$subsDomain = request()->header('Host');
|
||||||
|
|||||||
+98
-4
@@ -18,6 +18,9 @@ class Surge extends AbstractProtocol
|
|||||||
Server::TYPE_VMESS,
|
Server::TYPE_VMESS,
|
||||||
Server::TYPE_TROJAN,
|
Server::TYPE_TROJAN,
|
||||||
Server::TYPE_HYSTERIA,
|
Server::TYPE_HYSTERIA,
|
||||||
|
Server::TYPE_ANYTLS,
|
||||||
|
Server::TYPE_SOCKS,
|
||||||
|
Server::TYPE_HTTP,
|
||||||
];
|
];
|
||||||
protected $protocolRequirements = [
|
protected $protocolRequirements = [
|
||||||
'surge.hysteria.protocol_settings.version' => [2 => '2398'],
|
'surge.hysteria.protocol_settings.version' => [2 => '2398'],
|
||||||
@@ -40,7 +43,9 @@ class Surge extends AbstractProtocol
|
|||||||
'aes-128-gcm',
|
'aes-128-gcm',
|
||||||
'aes-192-gcm',
|
'aes-192-gcm',
|
||||||
'aes-256-gcm',
|
'aes-256-gcm',
|
||||||
'chacha20-ietf-poly1305'
|
'chacha20-ietf-poly1305',
|
||||||
|
'2022-blake3-aes-128-gcm',
|
||||||
|
'2022-blake3-aes-256-gcm'
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
$proxies .= self::buildShadowsocks($item['password'], $item);
|
$proxies .= self::buildShadowsocks($item['password'], $item);
|
||||||
@@ -58,12 +63,22 @@ class Surge extends AbstractProtocol
|
|||||||
$proxies .= self::buildHysteria($item['password'], $item);
|
$proxies .= self::buildHysteria($item['password'], $item);
|
||||||
$proxyGroup .= $item['name'] . ', ';
|
$proxyGroup .= $item['name'] . ', ';
|
||||||
}
|
}
|
||||||
|
if ($item['type'] === Server::TYPE_ANYTLS) {
|
||||||
|
$proxies .= self::buildAnyTLS($item['password'], $item);
|
||||||
|
$proxyGroup .= $item['name'] . ', ';
|
||||||
|
}
|
||||||
|
if ($item['type'] === Server::TYPE_SOCKS) {
|
||||||
|
$proxies .= self::buildSocks($item['password'], $item);
|
||||||
|
$proxyGroup .= $item['name'] . ', ';
|
||||||
|
}
|
||||||
|
if ($item['type'] === Server::TYPE_HTTP) {
|
||||||
|
$proxies .= self::buildHttp($item['password'], $item);
|
||||||
|
$proxyGroup .= $item['name'] . ', ';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$config = admin_setting('subscribe_template_surge', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
|
$config = subscribe_template('surge');
|
||||||
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
|
|
||||||
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
|
|
||||||
|
|
||||||
// Subscription link
|
// Subscription link
|
||||||
$subsDomain = request()->header('Host');
|
$subsDomain = request()->header('Host');
|
||||||
@@ -193,6 +208,28 @@ class Surge extends AbstractProtocol
|
|||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//参考文档: https://manual.nssurge.com/policy/proxy.html
|
||||||
|
public static function buildAnyTLS($password, $server)
|
||||||
|
{
|
||||||
|
$protocol_settings = data_get($server, 'protocol_settings', []);
|
||||||
|
$config = [
|
||||||
|
"{$server['name']}=anytls",
|
||||||
|
"{$server['host']}",
|
||||||
|
"{$server['port']}",
|
||||||
|
"password={$password}",
|
||||||
|
];
|
||||||
|
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
|
||||||
|
$config[] = "sni={$serverName}";
|
||||||
|
}
|
||||||
|
if (data_get($protocol_settings, 'tls.allow_insecure')) {
|
||||||
|
$config[] = 'skip-cert-verify=true';
|
||||||
|
}
|
||||||
|
$config = array_filter($config);
|
||||||
|
$uri = implode(',', $config);
|
||||||
|
$uri .= "\r\n";
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
//参考文档: https://manual.nssurge.com/policy/proxy.html
|
//参考文档: https://manual.nssurge.com/policy/proxy.html
|
||||||
public static function buildHysteria($password, $server)
|
public static function buildHysteria($password, $server)
|
||||||
{
|
{
|
||||||
@@ -222,4 +259,61 @@ class Surge extends AbstractProtocol
|
|||||||
$uri .= "\r\n";
|
$uri .= "\r\n";
|
||||||
return $uri;
|
return $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//参考文档: https://manual.nssurge.com/policy/proxy.html
|
||||||
|
public static function buildSocks($password, $server)
|
||||||
|
{
|
||||||
|
$protocol_settings = data_get($server, 'protocol_settings', []);
|
||||||
|
$type = data_get($protocol_settings, 'tls') ? 'socks5-tls' : 'socks5';
|
||||||
|
$config = [
|
||||||
|
"{$server['name']}={$type}",
|
||||||
|
"{$server['host']}",
|
||||||
|
"{$server['port']}",
|
||||||
|
"{$password}",
|
||||||
|
"{$password}",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (data_get($protocol_settings, 'tls')) {
|
||||||
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
|
$config[] = "sni={$serverName}";
|
||||||
|
}
|
||||||
|
if (data_get($protocol_settings, 'tls_settings.allow_insecure')) {
|
||||||
|
$config[] = 'skip-cert-verify=true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$config[] = 'udp-relay=true';
|
||||||
|
|
||||||
|
$config = array_filter($config);
|
||||||
|
$uri = implode(',', $config);
|
||||||
|
$uri .= "\r\n";
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
//参考文档: https://manual.nssurge.com/policy/proxy.html
|
||||||
|
public static function buildHttp($password, $server)
|
||||||
|
{
|
||||||
|
$protocol_settings = data_get($server, 'protocol_settings', []);
|
||||||
|
$type = data_get($protocol_settings, 'tls') ? 'https' : 'http';
|
||||||
|
$config = [
|
||||||
|
"{$server['name']}={$type}",
|
||||||
|
"{$server['host']}",
|
||||||
|
"{$server['port']}",
|
||||||
|
"{$password}",
|
||||||
|
"{$password}",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (data_get($protocol_settings, 'tls')) {
|
||||||
|
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||||
|
$config[] = "sni={$serverName}";
|
||||||
|
}
|
||||||
|
if (data_get($protocol_settings, 'tls_settings.allow_insecure')) {
|
||||||
|
$config[] = 'skip-cert-verify=true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = array_filter($config);
|
||||||
|
$uri = implode(',', $config);
|
||||||
|
$uri .= "\r\n";
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\ServerRoute;
|
||||||
|
use App\Models\Plan;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Observers\PlanObserver;
|
||||||
|
use App\Observers\ServerObserver;
|
||||||
|
use App\Observers\ServerRouteObserver;
|
||||||
use App\Observers\UserObserver;
|
use App\Observers\UserObserver;
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
||||||
class EventServiceProvider extends ServiceProvider
|
class EventServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -24,5 +31,10 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
||||||
User::observe(UserObserver::class);
|
User::observe(UserObserver::class);
|
||||||
|
Plan::observe(PlanObserver::class);
|
||||||
|
Server::observe(ServerObserver::class);
|
||||||
|
ServerRoute::observe(ServerRouteObserver::class);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
//
|
// HTTPS scheme is forced per-request via middleware (Octane-safe).
|
||||||
if (admin_setting('force_https')) {
|
|
||||||
resolve(\Illuminate\Routing\UrlGenerator::class)->forceScheme('https');
|
|
||||||
}
|
|
||||||
|
|
||||||
parent::boot();
|
parent::boot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Providers;
|
|||||||
use App\Support\Setting;
|
use App\Support\Setting;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Contracts\Foundation\Application;
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class SettingServiceProvider extends ServiceProvider
|
class SettingServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -29,5 +28,6 @@ class SettingServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
|
// App URL is forced per-request via middleware (Octane-safe).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Auth;
|
namespace App\Services\Auth;
|
||||||
|
|
||||||
|
use App\Exceptions\ApiException;
|
||||||
use App\Models\InviteCode;
|
use App\Models\InviteCode;
|
||||||
use App\Models\Plan;
|
use App\Models\Plan;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -113,7 +114,7 @@ class RegisterService
|
|||||||
|
|
||||||
if (!$inviteCodeModel) {
|
if (!$inviteCodeModel) {
|
||||||
if ((int) admin_setting('invite_force', 0)) {
|
if ((int) admin_setting('invite_force', 0)) {
|
||||||
throw new \Exception(__('Invalid invitation code'));
|
throw new ApiException(__('Invalid invitation code'));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class GiftCardService
|
|||||||
$userService->assignPlan(
|
$userService->assignPlan(
|
||||||
$this->user,
|
$this->user,
|
||||||
$plan,
|
$plan,
|
||||||
$rewards['plan_validity_days'] ?? null
|
$rewards['plan_validity_days'] ?? 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,6 +13,33 @@ use Illuminate\Support\Facades\Mail;
|
|||||||
|
|
||||||
class MailService
|
class MailService
|
||||||
{
|
{
|
||||||
|
// Render {{key}} / {{key|default}} placeholders.
|
||||||
|
private static function renderPlaceholders(string $template, array $vars): string
|
||||||
|
{
|
||||||
|
if ($template === '' || empty($vars)) {
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) preg_replace_callback('/\{\{\s*([a-zA-Z0-9_.-]+)(?:\|([^}]*))?\s*\}\}/', function ($m) use ($vars) {
|
||||||
|
$key = $m[1] ?? '';
|
||||||
|
$default = array_key_exists(2, $m) ? trim((string) $m[2]) : null;
|
||||||
|
|
||||||
|
if (!array_key_exists($key, $vars) || $vars[$key] === null || $vars[$key] === '') {
|
||||||
|
return $default !== null ? $default : $m[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $vars[$key];
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return $value ? '1' : '0';
|
||||||
|
}
|
||||||
|
if (is_scalar($value)) {
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
|
||||||
|
}, $template);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取需要发送提醒的用户总数
|
* 获取需要发送提醒的用户总数
|
||||||
*/
|
*/
|
||||||
@@ -233,19 +260,44 @@ class MailService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('content', $params['template_value'])) {
|
if (array_key_exists('content', $params['template_value'])) {
|
||||||
$params['template_value']['content'] = self::sanitizeMailText((string) $params['template_value']['content']);
|
$params['template_value']['content'] = (string) $params['template_value']['content'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = (string) $params['email'];
|
$email = (string) $params['email'];
|
||||||
$subject = self::sanitizeMailText((string) $params['subject']);
|
$originTemplateName = (string) $params['template_name'];
|
||||||
|
$subject = (string) $params['subject'];
|
||||||
|
|
||||||
|
$vars = is_array($params['template_value']) ? ($params['template_value']['vars'] ?? []) : [];
|
||||||
|
$contentMode = is_array($params['template_value']) ? ($params['template_value']['content_mode'] ?? null) : null;
|
||||||
|
|
||||||
|
if (is_array($vars) && !empty($vars)) {
|
||||||
|
$subject = self::renderPlaceholders($subject, $vars);
|
||||||
|
|
||||||
|
if (isset($params['template_value']['content']) && is_string($params['template_value']['content'])) {
|
||||||
|
$params['template_value']['content'] = self::renderPlaceholders($params['template_value']['content'], $vars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = self::sanitizeMailText($subject);
|
||||||
if ($subject === '') {
|
if ($subject === '') {
|
||||||
$subject = 'Notification';
|
$subject = 'Notification';
|
||||||
}
|
}
|
||||||
|
|
||||||
$originTemplateName = (string) $params['template_name'];
|
if (array_key_exists('content', $params['template_value'])) {
|
||||||
|
$params['template_value']['content'] = self::sanitizeMailText((string) $params['template_value']['content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$contentMode === 'text'
|
||||||
|
&& $originTemplateName !== 'notify'
|
||||||
|
&& isset($params['template_value']['content'])
|
||||||
|
&& is_string($params['template_value']['content'])
|
||||||
|
) {
|
||||||
|
$params['template_value']['content'] = e($params['template_value']['content']);
|
||||||
|
}
|
||||||
|
|
||||||
$params['template_name'] = 'mail.' . admin_setting('email_template', 'default') . '.' . $originTemplateName;
|
$params['template_name'] = 'mail.' . admin_setting('email_template', 'default') . '.' . $originTemplateName;
|
||||||
$logTemplateName = $params['template_name'];
|
$logTemplateName = $params['template_name'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($originTemplateName === 'notify') {
|
if ($originTemplateName === 'notify') {
|
||||||
$html = self::buildModernNotifyHtml($params['template_value'], $subject, $appName);
|
$html = self::buildModernNotifyHtml($params['template_value'], $subject, $appName);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Workerman\Connection\TcpConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory registry for active WebSocket node connections.
|
||||||
|
* Runs inside the Workerman process.
|
||||||
|
*/
|
||||||
|
class NodeRegistry
|
||||||
|
{
|
||||||
|
/** @var array<int, TcpConnection> nodeId → connection */
|
||||||
|
private static array $connections = [];
|
||||||
|
|
||||||
|
public static function add(int $nodeId, TcpConnection $conn): void
|
||||||
|
{
|
||||||
|
// Close existing connection for this node (if reconnecting)
|
||||||
|
if (isset(self::$connections[$nodeId])) {
|
||||||
|
self::$connections[$nodeId]->close();
|
||||||
|
}
|
||||||
|
self::$connections[$nodeId] = $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function remove(int $nodeId): void
|
||||||
|
{
|
||||||
|
unset(self::$connections[$nodeId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get(int $nodeId): ?TcpConnection
|
||||||
|
{
|
||||||
|
return self::$connections[$nodeId] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a JSON message to a specific node.
|
||||||
|
*/
|
||||||
|
public static function send(int $nodeId, string $event, array $data): bool
|
||||||
|
{
|
||||||
|
$conn = self::get($nodeId);
|
||||||
|
if (!$conn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_encode([
|
||||||
|
'event' => $event,
|
||||||
|
'data' => $data,
|
||||||
|
'timestamp' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conn->send($payload);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the connection for a node by ID, checking if it's still alive.
|
||||||
|
*/
|
||||||
|
public static function isOnline(int $nodeId): bool
|
||||||
|
{
|
||||||
|
$conn = self::get($nodeId);
|
||||||
|
return $conn !== null && $conn->getStatus() === TcpConnection::STATUS_ESTABLISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all connected node IDs.
|
||||||
|
* @return int[]
|
||||||
|
*/
|
||||||
|
public static function getConnectedNodeIds(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::$connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function count(): int
|
||||||
|
{
|
||||||
|
return count(self::$connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
|
class NodeSyncService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if node has active WS connection
|
||||||
|
*/
|
||||||
|
private static function isNodeOnline(int $nodeId): bool
|
||||||
|
{
|
||||||
|
return (bool) Cache::get("node_ws_alive:{$nodeId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push node config update
|
||||||
|
*/
|
||||||
|
public static function notifyConfigUpdated(int $nodeId): void
|
||||||
|
{
|
||||||
|
if (!self::isNodeOnline($nodeId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
$node = Server::find($nodeId);
|
||||||
|
if (!$node)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
self::push($nodeId, 'sync.config', ['config' => ServerService::buildNodeConfig($node)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push all users to all nodes in the group
|
||||||
|
*/
|
||||||
|
public static function notifyUsersUpdatedByGroup(int $groupId): void
|
||||||
|
{
|
||||||
|
$servers = Server::whereJsonContains('group_ids', (string) $groupId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
if (!self::isNodeOnline($server->id))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$users = ServerService::getAvailableUsers($server)->toArray();
|
||||||
|
self::push($server->id, 'sync.users', ['users' => $users]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push user changes (add/remove) to affected nodes
|
||||||
|
*/
|
||||||
|
public static function notifyUserChanged(User $user): void
|
||||||
|
{
|
||||||
|
if (!$user->group_id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
$servers = Server::whereJsonContains('group_ids', (string) $user->group_id)->get();
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
if (!self::isNodeOnline($server->id))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if ($user->isAvailable()) {
|
||||||
|
self::push($server->id, 'sync.user.delta', [
|
||||||
|
'action' => 'add',
|
||||||
|
'users' => [
|
||||||
|
[
|
||||||
|
'id' => $user->id,
|
||||||
|
'uuid' => $user->uuid,
|
||||||
|
'speed_limit' => $user->speed_limit,
|
||||||
|
'device_limit' => $user->device_limit,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
self::push($server->id, 'sync.user.delta', [
|
||||||
|
'action' => 'remove',
|
||||||
|
'users' => [['id' => $user->id]],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push user removal from a specific group's nodes
|
||||||
|
*/
|
||||||
|
public static function notifyUserRemovedFromGroup(int $userId, int $groupId): void
|
||||||
|
{
|
||||||
|
$servers = Server::whereJsonContains('group_ids', (string) $groupId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
if (!self::isNodeOnline($server->id))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
self::push($server->id, 'sync.user.delta', [
|
||||||
|
'action' => 'remove',
|
||||||
|
'users' => [['id' => $userId]],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full sync: push config + users to a node
|
||||||
|
*/
|
||||||
|
public static function notifyFullSync(int $nodeId): void
|
||||||
|
{
|
||||||
|
if (!self::isNodeOnline($nodeId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
$node = Server::find($nodeId);
|
||||||
|
if (!$node)
|
||||||
|
return;
|
||||||
|
|
||||||
|
self::push($nodeId, 'sync.config', ['config' => ServerService::buildNodeConfig($node)]);
|
||||||
|
|
||||||
|
$users = ServerService::getAvailableUsers($node)->toArray();
|
||||||
|
self::push($nodeId, 'sync.users', ['users' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish a push command to Redis — picked up by the Workerman WS server
|
||||||
|
*/
|
||||||
|
private static function push(int $nodeId, string $event, array $data): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Redis::publish('node:push', json_encode([
|
||||||
|
'node_id' => $nodeId,
|
||||||
|
'event' => $event,
|
||||||
|
'data' => $data,
|
||||||
|
]));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning("[NodePush] Redis publish failed: {$e->getMessage()}", [
|
||||||
|
'node_id' => $nodeId,
|
||||||
|
'event' => $event,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,13 +95,14 @@ class OrderService
|
|||||||
public function open(): void
|
public function open(): void
|
||||||
{
|
{
|
||||||
$order = $this->order;
|
$order = $this->order;
|
||||||
$this->user = User::find($order->user_id);
|
|
||||||
$plan = Plan::find($order->plan_id);
|
$plan = Plan::find($order->plan_id);
|
||||||
|
|
||||||
HookManager::call('order.open.before', $order);
|
HookManager::call('order.open.before', $order);
|
||||||
|
|
||||||
|
|
||||||
DB::transaction(function () use ($order, $plan) {
|
DB::transaction(function () use ($order, $plan) {
|
||||||
|
$this->user = User::lockForUpdate()->find($order->user_id);
|
||||||
|
|
||||||
if ($order->refund_amount) {
|
if ($order->refund_amount) {
|
||||||
$this->user->balance += $order->refund_amount;
|
$this->user->balance += $order->refund_amount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ class ServerService
|
|||||||
'is_online',
|
'is_online',
|
||||||
'available_status',
|
'available_status',
|
||||||
'cache_key',
|
'cache_key',
|
||||||
'load_status'
|
'load_status',
|
||||||
|
'metrics',
|
||||||
|
'online_conn'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ class ServerService
|
|||||||
$server->port = (int) $server->port;
|
$server->port = (int) $server->port;
|
||||||
}
|
}
|
||||||
$server->password = $server->generateServerPassword($user);
|
$server->password = $server->generateServerPassword($user);
|
||||||
|
$server->rate = $server->getCurrentRate();
|
||||||
return $server;
|
return $server;
|
||||||
})->toArray();
|
})->toArray();
|
||||||
|
|
||||||
@@ -92,6 +95,174 @@ class ServerService
|
|||||||
return $routes;
|
return $routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update node metrics and load status
|
||||||
|
*/
|
||||||
|
public static function updateMetrics(Server $node, array $metrics): void
|
||||||
|
{
|
||||||
|
$nodeType = strtoupper($node->type);
|
||||||
|
$nodeId = $node->id;
|
||||||
|
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
|
||||||
|
|
||||||
|
$metricsData = [
|
||||||
|
'uptime' => (int) ($metrics['uptime'] ?? 0),
|
||||||
|
'goroutines' => (int) ($metrics['goroutines'] ?? 0),
|
||||||
|
'active_connections' => (int) ($metrics['active_connections'] ?? 0),
|
||||||
|
'total_connections' => (int) ($metrics['total_connections'] ?? 0),
|
||||||
|
'total_users' => (int) ($metrics['total_users'] ?? 0),
|
||||||
|
'active_users' => (int) ($metrics['active_users'] ?? 0),
|
||||||
|
'inbound_speed' => (int) ($metrics['inbound_speed'] ?? 0),
|
||||||
|
'outbound_speed' => (int) ($metrics['outbound_speed'] ?? 0),
|
||||||
|
'cpu_per_core' => $metrics['cpu_per_core'] ?? [],
|
||||||
|
'load' => $metrics['load'] ?? [],
|
||||||
|
'speed_limiter' => $metrics['speed_limiter'] ?? [],
|
||||||
|
'gc' => $metrics['gc'] ?? [],
|
||||||
|
'api' => $metrics['api'] ?? [],
|
||||||
|
'ws' => $metrics['ws'] ?? [],
|
||||||
|
'limits' => $metrics['limits'] ?? [],
|
||||||
|
'updated_at' => now()->timestamp,
|
||||||
|
'kernel_status' => (bool) ($metrics['kernel_status'] ?? false),
|
||||||
|
];
|
||||||
|
|
||||||
|
\Illuminate\Support\Facades\Cache::put(
|
||||||
|
\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_METRICS', $nodeId),
|
||||||
|
$metricsData,
|
||||||
|
$cacheTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildNodeConfig(Server $node): array
|
||||||
|
{
|
||||||
|
$nodeType = $node->type;
|
||||||
|
$protocolSettings = $node->protocol_settings;
|
||||||
|
$serverPort = $node->server_port;
|
||||||
|
$host = $node->host;
|
||||||
|
|
||||||
|
$baseConfig = [
|
||||||
|
'protocol' => $nodeType,
|
||||||
|
'listen_ip' => '0.0.0.0',
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
'network' => data_get($protocolSettings, 'network'),
|
||||||
|
'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = match ($nodeType) {
|
||||||
|
'shadowsocks' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'cipher' => $protocolSettings['cipher'],
|
||||||
|
'plugin' => $protocolSettings['plugin'],
|
||||||
|
'plugin_opts' => $protocolSettings['plugin_opts'],
|
||||||
|
'server_key' => match ($protocolSettings['cipher']) {
|
||||||
|
'2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
|
||||||
|
'2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
|
||||||
|
default => null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vmess' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'tls' => (int) $protocolSettings['tls'],
|
||||||
|
'multiplex' => data_get($protocolSettings, 'multiplex'),
|
||||||
|
],
|
||||||
|
'trojan' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'host' => $host,
|
||||||
|
'server_name' => $protocolSettings['server_name'],
|
||||||
|
'multiplex' => data_get($protocolSettings, 'multiplex'),
|
||||||
|
],
|
||||||
|
'vless' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'tls' => (int) $protocolSettings['tls'],
|
||||||
|
'flow' => $protocolSettings['flow'],
|
||||||
|
'tls_settings' => match ((int) $protocolSettings['tls']) {
|
||||||
|
2 => $protocolSettings['reality_settings'],
|
||||||
|
default => $protocolSettings['tls_settings'],
|
||||||
|
},
|
||||||
|
'multiplex' => data_get($protocolSettings, 'multiplex'),
|
||||||
|
],
|
||||||
|
'hysteria' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
'version' => (int) $protocolSettings['version'],
|
||||||
|
'host' => $host,
|
||||||
|
'server_name' => $protocolSettings['tls']['server_name'],
|
||||||
|
'up_mbps' => (int) $protocolSettings['bandwidth']['up'],
|
||||||
|
'down_mbps' => (int) $protocolSettings['bandwidth']['down'],
|
||||||
|
...match ((int) $protocolSettings['version']) {
|
||||||
|
1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null],
|
||||||
|
2 => [
|
||||||
|
'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null,
|
||||||
|
'obfs-password' => $protocolSettings['obfs']['password'] ?? null,
|
||||||
|
],
|
||||||
|
default => [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'tuic' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'version' => (int) $protocolSettings['version'],
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
'server_name' => $protocolSettings['tls']['server_name'],
|
||||||
|
'congestion_control' => $protocolSettings['congestion_control'],
|
||||||
|
'tls_settings' => data_get($protocolSettings, 'tls_settings'),
|
||||||
|
'auth_timeout' => '3s',
|
||||||
|
'zero_rtt_handshake' => false,
|
||||||
|
'heartbeat' => '3s',
|
||||||
|
],
|
||||||
|
'anytls' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
'server_name' => $protocolSettings['tls']['server_name'],
|
||||||
|
'padding_scheme' => $protocolSettings['padding_scheme'],
|
||||||
|
],
|
||||||
|
'socks' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
],
|
||||||
|
'naive' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
'tls' => (int) $protocolSettings['tls'],
|
||||||
|
'tls_settings' => $protocolSettings['tls_settings'],
|
||||||
|
],
|
||||||
|
'http' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
'tls' => (int) $protocolSettings['tls'],
|
||||||
|
'tls_settings' => $protocolSettings['tls_settings'],
|
||||||
|
],
|
||||||
|
'mieru' => [
|
||||||
|
...$baseConfig,
|
||||||
|
'server_port' => (int) $serverPort,
|
||||||
|
'transport' => data_get($protocolSettings, 'transport', 'TCP'),
|
||||||
|
'traffic_pattern' => $protocolSettings['traffic_pattern'],
|
||||||
|
// 'multiplex' => data_get($protocolSettings, 'multiplex'),
|
||||||
|
],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
|
||||||
|
$response = array_filter(
|
||||||
|
$response,
|
||||||
|
static fn ($value) => $value !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($node['route_ids'])) {
|
||||||
|
$response['routes'] = self::getRoutes($node['route_ids']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($node['custom_outbounds'])) {
|
||||||
|
$response['custom_outbounds'] = $node['custom_outbounds'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($node['custom_routes'])) {
|
||||||
|
$response['custom_routes'] = $node['custom_routes'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($node['cert_config'])) {
|
||||||
|
$response['cert_config'] = $node['cert_config'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据协议类型和标识获取服务器
|
* 根据协议类型和标识获取服务器
|
||||||
* @param int $serverId
|
* @param int $serverId
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class UserService
|
|||||||
// 默认设置
|
// 默认设置
|
||||||
$user->remind_expire = admin_setting('default_remind_expire', 1);
|
$user->remind_expire = admin_setting('default_remind_expire', 1);
|
||||||
$user->remind_traffic = admin_setting('default_remind_traffic', 1);
|
$user->remind_traffic = admin_setting('default_remind_traffic', 1);
|
||||||
$user->expired_at = 0;
|
$user->expired_at = null;
|
||||||
|
|
||||||
// 可选字段
|
// 可选字段
|
||||||
$this->setOptionalFields($user, $data);
|
$this->setOptionalFields($user, $data);
|
||||||
@@ -242,6 +242,7 @@ class UserService
|
|||||||
$user->group_id = $plan->group_id;
|
$user->group_id = $plan->group_id;
|
||||||
$user->transfer_enable = $plan->transfer_enable * 1073741824;
|
$user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||||
$user->speed_limit = $plan->speed_limit;
|
$user->speed_limit = $plan->speed_limit;
|
||||||
|
$user->device_limit = $plan->device_limit;
|
||||||
|
|
||||||
if ($validityDays > 0) {
|
if ($validityDays > 0) {
|
||||||
$user = $this->extendSubscription($user, $validityDays);
|
$user = $this->extendSubscription($user, $validityDays);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class CacheKey
|
|||||||
'SERVER_*_LAST_PUSH_AT', // 节点最后推送时间
|
'SERVER_*_LAST_PUSH_AT', // 节点最后推送时间
|
||||||
'SERVER_*_LOAD_STATUS', // 节点负载状态
|
'SERVER_*_LOAD_STATUS', // 节点负载状态
|
||||||
'SERVER_*_LAST_LOAD_AT', // 节点最后负载提交时间
|
'SERVER_*_LAST_LOAD_AT', // 节点最后负载提交时间
|
||||||
|
'SERVER_*_METRICS', // 节点指标数据
|
||||||
|
'USER_ONLINE_CONN_*_*', // 用户在线连接数 (特定节点类型_ID)
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +59,7 @@ class CacheKey
|
|||||||
private static function matchesPattern(string $key): bool
|
private static function matchesPattern(string $key): bool
|
||||||
{
|
{
|
||||||
foreach (self::ALLOWED_PATTERNS as $pattern) {
|
foreach (self::ALLOWED_PATTERNS as $pattern) {
|
||||||
$regex = '/^' . str_replace('*', '[A-Z_]+', $pattern) . '$/';
|
$regex = '/^' . str_replace('*', '[A-Za-z0-9_]+', $pattern) . '$/';
|
||||||
if (preg_match($regex, $key)) {
|
if (preg_match($regex, $key)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -142,8 +142,13 @@ class Helper
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function randomPort($range): int {
|
public static function randomPort($range): int {
|
||||||
$portRange = explode('-', $range);
|
$portRange = explode('-', (string) $range, 2);
|
||||||
return random_int((int)$portRange[0], (int)$portRange[1]);
|
$min = (int) ($portRange[0] ?? 0);
|
||||||
|
$max = (int) ($portRange[1] ?? $portRange[0] ?? 0);
|
||||||
|
if ($min > $max) {
|
||||||
|
[$min, $max] = [$max, $min];
|
||||||
|
}
|
||||||
|
return random_int($min, $max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function base64EncodeUrlSafe($data)
|
public static function base64EncodeUrlSafe($data)
|
||||||
@@ -183,8 +188,20 @@ class Helper
|
|||||||
public static function getIpByDomainName($domain) {
|
public static function getIpByDomainName($domain) {
|
||||||
return gethostbynamel($domain) ?: [];
|
return gethostbynamel($domain) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getTlsFingerprint($utls = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (is_array($utls) || is_object($utls)) {
|
||||||
|
if (!data_get($utls, 'enabled')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$fingerprint = data_get($utls, 'fingerprint', 'chrome');
|
||||||
|
if ($fingerprint !== 'random') {
|
||||||
|
return $fingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function getRandFingerprint() {
|
|
||||||
$fingerprints = ['chrome', 'firefox', 'safari', 'ios', 'edge', 'qq'];
|
$fingerprints = ['chrome', 'firefox', 'safari', 'ios', 'edge', 'qq'];
|
||||||
return Arr::random($fingerprints);
|
return Arr::random($fingerprints);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,24 @@ services:
|
|||||||
command: php artisan horizon
|
command: php artisan horizon
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
ws-server:
|
||||||
|
image: xboard-local:latest
|
||||||
|
volumes:
|
||||||
|
- ./.docker/.data/redis/:/data/
|
||||||
|
- ./.env:/www/.env
|
||||||
|
- ./.docker/.data/:/www/.docker/.data
|
||||||
|
- ./storage/logs:/www/storage/logs
|
||||||
|
- ./plugins:/www/plugins
|
||||||
|
restart: on-failure
|
||||||
|
# network_mode: host
|
||||||
|
command: php artisan ws-server start
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
command: redis-server --unixsocket /data/redis.sock --unixsocketperm 777 --save 900 1 --save 300 10 --save 60 10000
|
command: redis-server --unixsocket /data/redis.sock --unixsocketperm 777 --save 900 1 --save 300 10 --save 60 10000
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./.docker/.data/redis:/data
|
- ./.docker/.data/redis:/data
|
||||||
|
sysctls:
|
||||||
|
net.core.somaxconn: 1024
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
"symfony/http-client": "^7.0",
|
"symfony/http-client": "^7.0",
|
||||||
"symfony/mailgun-mailer": "^7.0",
|
"symfony/mailgun-mailer": "^7.0",
|
||||||
"symfony/yaml": "*",
|
"symfony/yaml": "*",
|
||||||
|
"webmozart/assert": "*",
|
||||||
|
"workerman/redis": "^2.0",
|
||||||
|
"workerman/workerman": "^5.1",
|
||||||
"zoujingli/ip2region": "^2.0"
|
"zoujingli/ip2region": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ return [
|
|||||||
'database' => env('DB_DATABASE') ? base_path(env('DB_DATABASE')) : database_path('database.sqlite'),
|
'database' => env('DB_DATABASE') ? base_path(env('DB_DATABASE')) : database_path('database.sqlite'),
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => env('DB_BUSY_TIMEOUT', 30000),
|
||||||
|
'journal_mode' => env('DB_JOURNAL_MODE', 'wal'),
|
||||||
|
'synchronous' => env('DB_SYNCHRONOUS', 'normal'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'mysql' => [
|
'mysql' => [
|
||||||
|
|||||||
+40
-4
@@ -60,7 +60,7 @@ return [
|
|||||||
|
|
||||||
'prefix' => env(
|
'prefix' => env(
|
||||||
'HORIZON_PREFIX',
|
'HORIZON_PREFIX',
|
||||||
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
|
Str::slug(env('APP_NAME', 'laravel'), '_') . '_horizon:'
|
||||||
),
|
),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -155,7 +155,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'memory_limit' => 64,
|
'memory_limit' => 256,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -169,22 +169,58 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'environments' => [
|
'environments' => [
|
||||||
|
'production' => [
|
||||||
|
'data-pipeline' => [
|
||||||
|
'connection' => 'redis',
|
||||||
|
'queue' => ['traffic_fetch', 'stat', 'user_alive_sync'],
|
||||||
|
'balance' => 'auto',
|
||||||
|
'autoScalingStrategy' => 'time',
|
||||||
|
'minProcesses' => 1,
|
||||||
|
'maxProcesses' => 8,
|
||||||
|
'balanceCooldown' => 1,
|
||||||
|
'tries' => 3,
|
||||||
|
'timeout' => 30,
|
||||||
|
],
|
||||||
|
'business' => [
|
||||||
|
'connection' => 'redis',
|
||||||
|
'queue' => ['default', 'order_handle'],
|
||||||
|
'balance' => 'simple',
|
||||||
|
'minProcesses' => 1,
|
||||||
|
'maxProcesses' => 3,
|
||||||
|
'tries' => 3,
|
||||||
|
'timeout' => 30,
|
||||||
|
],
|
||||||
|
'notification' => [
|
||||||
|
'connection' => 'redis',
|
||||||
|
'queue' => ['send_email', 'send_telegram', 'send_email_mass', 'node_sync'],
|
||||||
|
'balance' => 'auto',
|
||||||
|
'autoScalingStrategy' => 'size',
|
||||||
|
'minProcesses' => 1,
|
||||||
|
'maxProcesses' => 3,
|
||||||
|
'tries' => 3,
|
||||||
|
'timeout' => 60,
|
||||||
|
'backoff' => [3, 10, 30],
|
||||||
|
],
|
||||||
|
],
|
||||||
'local' => [
|
'local' => [
|
||||||
'Xboard' => [
|
'Xboard' => [
|
||||||
'connection' => 'redis',
|
'connection' => 'redis',
|
||||||
'queue' => [
|
'queue' => [
|
||||||
|
'default',
|
||||||
'order_handle',
|
'order_handle',
|
||||||
'traffic_fetch',
|
'traffic_fetch',
|
||||||
'stat',
|
'stat',
|
||||||
'send_email',
|
'send_email',
|
||||||
'send_email_mass',
|
'send_email_mass',
|
||||||
'send_telegram',
|
'send_telegram',
|
||||||
'online_sync'
|
'user_alive_sync',
|
||||||
|
'node_sync'
|
||||||
],
|
],
|
||||||
'balance' => 'auto',
|
'balance' => 'auto',
|
||||||
'minProcesses' => 1,
|
'minProcesses' => 1,
|
||||||
'maxProcesses' => 20,
|
'maxProcesses' => 5,
|
||||||
'tries' => 1,
|
'tries' => 1,
|
||||||
|
'timeout' => 60,
|
||||||
'balanceCooldown' => 3,
|
'balanceCooldown' => 3,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
+6
-54
@@ -5,40 +5,9 @@ use Monolog\Handler\SyslogUdpHandler;
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
'default' => env('LOG_CHANNEL', 'daily'),
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Log Channel
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option defines the default log channel that gets used when writing
|
|
||||||
| messages to the logs. The name specified in this option should match
|
|
||||||
| one of the channels defined in the "channels" configuration array.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => 'mysql',
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Log Channels
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may configure the log channels for your application. Out of
|
|
||||||
| the box, Laravel uses the Monolog PHP logging library. This gives
|
|
||||||
| you a variety of powerful log handlers / formatters to utilize.
|
|
||||||
|
|
|
||||||
| Available Drivers: "single", "daily", "slack", "syslog",
|
|
||||||
| "errorlog", "monolog",
|
|
||||||
| "custom", "stack"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'channels' => [
|
'channels' => [
|
||||||
'mysql' => [
|
|
||||||
'driver' => 'custom',
|
|
||||||
'via' => App\Logging\MysqlLogger::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
'stack' => [
|
'stack' => [
|
||||||
'driver' => 'stack',
|
'driver' => 'stack',
|
||||||
'channels' => ['daily'],
|
'channels' => ['daily'],
|
||||||
@@ -54,36 +23,19 @@ return [
|
|||||||
'single' => [
|
'single' => [
|
||||||
'driver' => 'single',
|
'driver' => 'single',
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
'level' => 'debug',
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'daily' => [
|
'daily' => [
|
||||||
'driver' => 'daily',
|
'driver' => 'daily',
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
'level' => 'debug',
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'days' => 14,
|
'days' => 14,
|
||||||
],
|
],
|
||||||
|
|
||||||
'slack' => [
|
|
||||||
'driver' => 'slack',
|
|
||||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
|
||||||
'username' => 'Laravel Log',
|
|
||||||
'emoji' => ':boom:',
|
|
||||||
'level' => 'critical',
|
|
||||||
],
|
|
||||||
|
|
||||||
'papertrail' => [
|
|
||||||
'driver' => 'monolog',
|
|
||||||
'level' => 'debug',
|
|
||||||
'handler' => SyslogUdpHandler::class,
|
|
||||||
'handler_with' => [
|
|
||||||
'host' => env('PAPERTRAIL_URL'),
|
|
||||||
'port' => env('PAPERTRAIL_PORT'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
'stderr' => [
|
'stderr' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
'handler' => StreamHandler::class,
|
'handler' => StreamHandler::class,
|
||||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
'with' => [
|
'with' => [
|
||||||
@@ -93,12 +45,12 @@ return [
|
|||||||
|
|
||||||
'syslog' => [
|
'syslog' => [
|
||||||
'driver' => 'syslog',
|
'driver' => 'syslog',
|
||||||
'level' => 'debug',
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'errorlog' => [
|
'errorlog' => [
|
||||||
'driver' => 'errorlog',
|
'driver' => 'errorlog',
|
||||||
'level' => 'debug',
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
],
|
],
|
||||||
|
|
||||||
'deprecations' => [
|
'deprecations' => [
|
||||||
|
|||||||
+8
-8
@@ -79,7 +79,7 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
RequestTerminated::class => [
|
RequestTerminated::class => [
|
||||||
// FlushUploadedFiles::class,
|
FlushUploadedFiles::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
TaskReceived::class => [
|
TaskReceived::class => [
|
||||||
@@ -102,8 +102,8 @@ return [
|
|||||||
|
|
||||||
OperationTerminated::class => [
|
OperationTerminated::class => [
|
||||||
FlushTemporaryContainerInstances::class,
|
FlushTemporaryContainerInstances::class,
|
||||||
// DisconnectFromDatabases::class,
|
DisconnectFromDatabases::class,
|
||||||
// CollectGarbage::class,
|
CollectGarbage::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
WorkerErrorOccurred::class => [
|
WorkerErrorOccurred::class => [
|
||||||
@@ -132,7 +132,7 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'flush' => [
|
'flush' => [
|
||||||
//
|
\App\Services\Plugin\HookManager::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -147,8 +147,8 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'cache' => [
|
'cache' => [
|
||||||
'rows' => 1000,
|
'rows' => 5000,
|
||||||
'bytes' => 10000,
|
'bytes' => 20000,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -203,7 +203,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'garbage' => 50,
|
'garbage' => 128,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -216,6 +216,6 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'max_execution_time' => 30,
|
'max_execution_time' => 60,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class CreateV2SettingsTable extends Migration
|
|||||||
$table->id();
|
$table->id();
|
||||||
$table->string('group')->comment('设置分组')->nullable();
|
$table->string('group')->comment('设置分组')->nullable();
|
||||||
$table->string('type')->comment('设置类型')->nullable();
|
$table->string('type')->comment('设置类型')->nullable();
|
||||||
$table->string('name')->comment('设置名称')->uniqid();
|
$table->string('name')->comment('设置名称')->unique();
|
||||||
$table->string('value')->comment('设置值')->nullable();
|
$table->string('value')->comment('设置值')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('v2_subscribe_templates', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique()->comment('Template key, e.g. singbox, clash');
|
||||||
|
$table->mediumText('content')->nullable()->comment('Template content');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->seedDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('v2_subscribe_templates');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedDefaults(): void
|
||||||
|
{
|
||||||
|
// Fallback order matches original protocol class behavior
|
||||||
|
$protocols = [
|
||||||
|
'singbox' => [
|
||||||
|
'resources/rules/custom.sing-box.json',
|
||||||
|
'resources/rules/default.sing-box.json',
|
||||||
|
],
|
||||||
|
'clash' => [
|
||||||
|
'resources/rules/custom.clash.yaml',
|
||||||
|
'resources/rules/default.clash.yaml',
|
||||||
|
],
|
||||||
|
'clashmeta' => [
|
||||||
|
'resources/rules/custom.clashmeta.yaml',
|
||||||
|
'resources/rules/custom.clash.yaml',
|
||||||
|
'resources/rules/default.clash.yaml',
|
||||||
|
],
|
||||||
|
'stash' => [
|
||||||
|
'resources/rules/custom.stash.yaml',
|
||||||
|
'resources/rules/custom.clash.yaml',
|
||||||
|
'resources/rules/default.clash.yaml',
|
||||||
|
],
|
||||||
|
'surge' => [
|
||||||
|
'resources/rules/custom.surge.conf',
|
||||||
|
'resources/rules/default.surge.conf',
|
||||||
|
],
|
||||||
|
'surfboard' => [
|
||||||
|
'resources/rules/custom.surfboard.conf',
|
||||||
|
'resources/rules/default.surfboard.conf',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($protocols as $name => $fileFallbacks) {
|
||||||
|
$existing = DB::table('v2_settings')
|
||||||
|
->where('name', "subscribe_template_{$name}")
|
||||||
|
->value('value');
|
||||||
|
|
||||||
|
if ($existing !== null && $existing !== '') {
|
||||||
|
$content = $existing;
|
||||||
|
} else {
|
||||||
|
$content = '';
|
||||||
|
foreach ($fileFallbacks as $file) {
|
||||||
|
$path = base_path($file);
|
||||||
|
if (File::exists($path)) {
|
||||||
|
$content = File::get($path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('v2_subscribe_templates')->insert([
|
||||||
|
'name' => $name,
|
||||||
|
'content' => $content,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old entries from v2_settings
|
||||||
|
DB::table('v2_settings')
|
||||||
|
->where('name', 'like', 'subscribe_template_%')
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('v2_admin_audit_log', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('admin_id')->index();
|
||||||
|
$table->string('action', 64)->index()->comment('Action identifier e.g. user.update');
|
||||||
|
$table->string('method', 10);
|
||||||
|
$table->string('uri', 512);
|
||||||
|
$table->text('request_data')->nullable();
|
||||||
|
$table->string('ip', 128)->nullable();
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
$table->unsignedInteger('updated_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('v2_admin_audit_log');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('v2_stat_user', function (Blueprint $table) {
|
||||||
|
$table->index(['record_at', 'user_id'], 'idx_stat_user_record_user');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('v2_stat_user', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_stat_user_record_user');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('v2_server', function (Blueprint $table) {
|
||||||
|
$table->json('custom_outbounds')->nullable()->after('protocol_settings');
|
||||||
|
$table->json('custom_routes')->nullable()->after('custom_outbounds');
|
||||||
|
$table->json('cert_config')->nullable()->after('custom_routes');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('v2_server', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['custom_outbounds', 'custom_routes', 'cert_config']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,38 +1,48 @@
|
|||||||
# 1Panel 快速部署指南
|
# Quick Deployment Guide for 1Panel
|
||||||
|
|
||||||
本文档介绍如何使用 1Panel 部署 Xboard。
|
This guide explains how to deploy Xboard using 1Panel.
|
||||||
|
|
||||||
## 1. 环境准备
|
## 1. Environment Preparation
|
||||||
|
|
||||||
安装 1Panel:
|
Install 1Panel:
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && \
|
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && \
|
||||||
sudo bash quick_start.sh
|
sudo bash quick_start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. 环境配置
|
## 2. Environment Configuration
|
||||||
|
|
||||||
1. 从应用商店安装:
|
1. Install from App Store:
|
||||||
- OpenResty(任意版本)
|
- OpenResty (any version)
|
||||||
- 勾选“外部端口访问”以放行防火墙
|
- ⚠️ Check "External Port Access" to open firewall
|
||||||
- MySQL 5.7(ARM 架构请使用 MariaDB)
|
- MySQL 5.7 (Use MariaDB for ARM architecture)
|
||||||
|
|
||||||
2. 创建数据库:
|
2. Create Database:
|
||||||
- 数据库名:`xboard`
|
- Database name: `xboard`
|
||||||
- 用户名:`xboard`
|
- Username: `xboard`
|
||||||
- 权限:所有主机(%)
|
- Access rights: All hosts (%)
|
||||||
- 请保存数据库密码,安装时需要使用
|
- Save the database password for installation
|
||||||
|
|
||||||
## 3. 部署步骤
|
## 3. Deployment Steps
|
||||||
|
|
||||||
1. 添加网站:
|
1. Add Website:
|
||||||
- 进入“网站” > “创建网站” > “反向代理”
|
- Go to "Website" > "Create Website" > "Reverse Proxy"
|
||||||
- 域名:填写你的域名
|
- Domain: Enter your domain
|
||||||
- 代号:`xboard`
|
- Code: `xboard`
|
||||||
- 代理地址:`127.0.0.1:7001`
|
- Proxy address: `127.0.0.1:7001`
|
||||||
|
|
||||||
2. 配置反向代理:
|
2. Configure Reverse Proxy:
|
||||||
```nginx
|
```nginx
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://127.0.0.1:8076;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
location ^~ / {
|
location ^~ / {
|
||||||
proxy_pass http://127.0.0.1:7001;
|
proxy_pass http://127.0.0.1:7001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -49,31 +59,32 @@ location ^~ / {
|
|||||||
proxy_cache off;
|
proxy_cache off;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
> The `/ws/` location enables WebSocket real-time node synchronization via `ws-server`. This service is enabled by default and can be toggled in Admin Panel > System Settings > Server.
|
||||||
|
|
||||||
3. 安装 Xboard:
|
3. Install Xboard:
|
||||||
```bash
|
```bash
|
||||||
# 进入网站目录
|
# Enter site directory
|
||||||
cd /opt/1panel/apps/openresty/openresty/www/sites/xboard/index
|
cd /opt/1panel/apps/openresty/openresty/www/sites/xboard/index
|
||||||
|
|
||||||
# 安装 Git(未安装时执行)
|
# Install Git (if not installed)
|
||||||
## Ubuntu/Debian
|
## Ubuntu/Debian
|
||||||
apt update && apt install -y git
|
apt update && apt install -y git
|
||||||
## CentOS/RHEL
|
## CentOS/RHEL
|
||||||
yum update && yum install -y git
|
yum update && yum install -y git
|
||||||
|
|
||||||
# 克隆仓库
|
# Clone repository
|
||||||
git clone -b compose --depth 1 https://github.com/Micah123321/Xboard ./
|
git clone -b compose --depth 1 https://github.com/cedar2025/Xboard ./
|
||||||
|
|
||||||
# 配置 Docker Compose
|
# Configure Docker Compose
|
||||||
```
|
```
|
||||||
|
|
||||||
4. 编辑 compose.yaml:
|
4. Edit compose.yaml:
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
image: ghcr.io/Micah123321/xboard:new
|
image: ghcr.io/cedar2025/xboard:new
|
||||||
volumes:
|
volumes:
|
||||||
- ./.docker/.data/redis/:/data/
|
- redis-data:/data
|
||||||
- ./.env:/www/.env
|
- ./.env:/www/.env
|
||||||
- ./.docker/.data/:/www/.docker/.data
|
- ./.docker/.data/:/www/.docker/.data
|
||||||
- ./storage/logs:/www/storage/logs
|
- ./storage/logs:/www/storage/logs
|
||||||
@@ -91,9 +102,9 @@ services:
|
|||||||
- 1panel-network
|
- 1panel-network
|
||||||
|
|
||||||
horizon:
|
horizon:
|
||||||
image: ghcr.io/Micah123321/xboard:new
|
image: ghcr.io/cedar2025/xboard:new
|
||||||
volumes:
|
volumes:
|
||||||
- ./.docker/.data/redis/:/data/
|
- redis-data:/data
|
||||||
- ./.env:/www/.env
|
- ./.env:/www/.env
|
||||||
- ./.docker/.data/:/www/.docker/.data
|
- ./.docker/.data/:/www/.docker/.data
|
||||||
- ./storage/logs:/www/storage/logs
|
- ./storage/logs:/www/storage/logs
|
||||||
@@ -104,6 +115,22 @@ services:
|
|||||||
- 1panel-network
|
- 1panel-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
ws-server:
|
||||||
|
image: ghcr.io/cedar2025/xboard:new
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
- ./.env:/www/.env
|
||||||
|
- ./.docker/.data/:/www/.docker/.data
|
||||||
|
- ./storage/logs:/www/storage/logs
|
||||||
|
- ./plugins:/www/plugins
|
||||||
|
restart: on-failure
|
||||||
|
ports:
|
||||||
|
- 8076:8076
|
||||||
|
networks:
|
||||||
|
- 1panel-network
|
||||||
|
command: php artisan ws-server start
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
@@ -112,67 +139,72 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- 1panel-network
|
- 1panel-network
|
||||||
volumes:
|
volumes:
|
||||||
- ./.docker/.data/redis:/data
|
- redis-data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
1panel-network:
|
1panel-network:
|
||||||
external: true
|
external: true
|
||||||
```
|
```
|
||||||
|
|
||||||
5. 初始化安装:
|
5. Initialize Installation:
|
||||||
```bash
|
```bash
|
||||||
# 安装依赖并初始化
|
# Install dependencies and initialize
|
||||||
docker compose run -it --rm web php artisan xboard:install
|
docker compose run -it --rm web php artisan xboard:install
|
||||||
```
|
```
|
||||||
|
|
||||||
重要配置说明:
|
⚠️ Important Configuration Notes:
|
||||||
1. 数据库配置
|
1. Database Configuration
|
||||||
- Database Host:按部署方式填写:
|
- Database Host: Choose based on your deployment:
|
||||||
1. 如果数据库与 Xboard 在同一网络,填写 `mysql`
|
1. If database and Xboard are in the same network, use `mysql`
|
||||||
2. 如果连接失败,进入:数据库 -> 选择数据库 -> 连接信息 -> 容器连接,使用其中的 Host 值
|
2. If connection fails, go to: Database -> Select Database -> Connection Info -> Container Connection, and use the "Host" value
|
||||||
3. 如果使用外部数据库,填写实际数据库地址
|
3. If using external database, enter your actual database host
|
||||||
- Database Port:`3306`(默认端口,除非你另有配置)
|
- Database Port: `3306` (default port unless configured otherwise)
|
||||||
- Database Name:`xboard`(前面创建的数据库)
|
- Database Name: `xboard` (the database created earlier)
|
||||||
- Database User:`xboard`(前面创建的用户)
|
- Database User: `xboard` (the user created earlier)
|
||||||
- Database Password:填写前面保存的密码
|
- Database Password: Enter the password saved earlier
|
||||||
|
|
||||||
2. Redis 配置
|
2. Redis Configuration
|
||||||
- 选择使用内置 Redis
|
- Choose to use built-in Redis
|
||||||
- 无需额外配置
|
- No additional configuration needed
|
||||||
|
|
||||||
3. 管理员信息
|
3. Administrator Information
|
||||||
- 保存安装完成后显示的管理员账号信息
|
- Save the admin credentials displayed after installation
|
||||||
- 记录管理后台访问地址
|
- Note down the admin panel access URL
|
||||||
|
|
||||||
配置完成后,启动服务:
|
After configuration, start the services:
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
6. 启动服务:
|
6. Start Services:
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 版本更新
|
## 4. Version Update
|
||||||
|
|
||||||
> 重要说明:更新命令会因安装版本不同而有所区别:
|
> 💡 Important Note: The update command varies depending on your installation version:
|
||||||
> - 如果是最近安装(新版本),使用以下命令:
|
> - If you installed recently (new version), use this command:
|
||||||
```bash
|
```bash
|
||||||
docker compose pull && \
|
docker compose pull && \
|
||||||
docker compose run -it --rm web php artisan xboard:update && \
|
docker compose run -it --rm web php artisan xboard:update && \
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
> - 如果是较早安装(旧版本),请把 `web` 替换为 `xboard`:
|
> - If you installed earlier (old version), replace `web` with `xboard`:
|
||||||
```bash
|
```bash
|
||||||
docker compose pull && \
|
docker compose pull && \
|
||||||
docker compose run -it --rm xboard php artisan xboard:update && \
|
docker compose run -it --rm xboard php artisan xboard:update && \
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
> 不确定该用哪个命令?先尝试新版本命令,失败后再使用旧版本命令。
|
> 🤔 Not sure which to use? Try the new version command first, if it fails, use the old version command.
|
||||||
|
|
||||||
## 重要提示
|
## Important Notes
|
||||||
|
|
||||||
- 请确保已开启防火墙,避免 7001 端口直接暴露到公网
|
- ⚠️ Ensure firewall is enabled to prevent port 7001 exposure to public
|
||||||
- 代码修改后需要重启服务才能生效
|
- Service restart is required after code modifications
|
||||||
- 建议配置 SSL 证书以保障访问安全
|
- SSL certificate configuration is recommended for secure access
|
||||||
|
|
||||||
|
> The node will automatically detect WebSocket availability during handshake. No extra configuration is needed on the node side.
|
||||||
|
|||||||
@@ -1,89 +1,99 @@
|
|||||||
# aaPanel + Docker 环境下的 Xboard 部署指南
|
# Xboard Deployment Guide for aaPanel + Docker Environment
|
||||||
|
|
||||||
## 目录
|
## Table of Contents
|
||||||
1. [环境要求](#环境要求)
|
1. [Requirements](#requirements)
|
||||||
2. [快速部署](#快速部署)
|
2. [Quick Deployment](#quick-deployment)
|
||||||
3. [详细配置](#详细配置)
|
3. [Detailed Configuration](#detailed-configuration)
|
||||||
4. [维护指南](#维护指南)
|
4. [Maintenance Guide](#maintenance-guide)
|
||||||
5. [故障排查](#故障排查)
|
5. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
## 环境要求
|
## Requirements
|
||||||
|
|
||||||
### 硬件要求
|
### Hardware Requirements
|
||||||
- CPU:1 核及以上
|
- CPU: 1 core or above
|
||||||
- 内存:2GB 及以上
|
- Memory: 2GB or above
|
||||||
- 存储:可用空间 10GB+
|
- Storage: 10GB+ available space
|
||||||
|
|
||||||
### 软件要求
|
### Software Requirements
|
||||||
- 操作系统:Ubuntu 20.04+ / CentOS 7+ / Debian 10+
|
- Operating System: Ubuntu 20.04+ / CentOS 7+ / Debian 10+
|
||||||
- aaPanel 最新版本
|
- Latest version of aaPanel
|
||||||
- Docker 和 Docker Compose
|
- Docker and Docker Compose
|
||||||
- Nginx(任意版本)
|
- Nginx (any version)
|
||||||
- MySQL 5.7+
|
- MySQL 5.7+
|
||||||
|
|
||||||
## 快速部署
|
## Quick Deployment
|
||||||
|
|
||||||
### 1. 安装 aaPanel
|
### 1. Install aaPanel
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://www.aapanel.com/script/install_6.0_en.sh -o install_6.0_en.sh && \
|
curl -sSL https://www.aapanel.com/script/install_6.0_en.sh -o install_6.0_en.sh && \
|
||||||
bash install_6.0_en.sh aapanel
|
bash install_6.0_en.sh aapanel
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 基础环境配置
|
### 2. Basic Environment Setup
|
||||||
|
|
||||||
#### 2.1 安装 Docker
|
#### 2.1 Install Docker
|
||||||
```bash
|
```bash
|
||||||
# 安装 Docker
|
# Install Docker
|
||||||
curl -sSL https://get.docker.com | bash
|
curl -sSL https://get.docker.com | bash
|
||||||
|
|
||||||
# CentOS 系统还需要执行:
|
# For CentOS systems, also run:
|
||||||
systemctl enable docker
|
systemctl enable docker
|
||||||
systemctl start docker
|
systemctl start docker
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2.2 安装必需组件
|
#### 2.2 Install Required Components
|
||||||
在 aaPanel 面板中安装:
|
In the aaPanel dashboard, install:
|
||||||
- Nginx(任意版本)
|
- Nginx (any version)
|
||||||
- MySQL 5.7
|
- MySQL 5.7
|
||||||
- PHP 和 Redis 不需要安装
|
- ⚠️ PHP and Redis are not required
|
||||||
|
|
||||||
### 3. 网站配置
|
### 3. Site Configuration
|
||||||
|
|
||||||
#### 3.1 创建网站
|
#### 3.1 Create Website
|
||||||
1. 进入:aaPanel > 网站 > 添加站点
|
1. Navigate to: aaPanel > Website > Add site
|
||||||
2. 填写信息:
|
2. Fill in the information:
|
||||||
- 域名:填写你的网站域名
|
- Domain: Enter your site domain
|
||||||
- 数据库:选择 MySQL
|
- Database: Select MySQL
|
||||||
- PHP 版本:选择纯静态
|
- PHP Version: Select Pure Static
|
||||||
|
|
||||||
#### 3.2 部署 Xboard
|
#### 3.2 Deploy Xboard
|
||||||
```bash
|
```bash
|
||||||
# 进入网站目录
|
# Enter site directory
|
||||||
cd /www/wwwroot/your-domain
|
cd /www/wwwroot/your-domain
|
||||||
|
|
||||||
# 清理目录
|
# Clean directory
|
||||||
chattr -i .user.ini
|
chattr -i .user.ini
|
||||||
rm -rf .htaccess 404.html 502.html index.html .user.ini
|
rm -rf .htaccess 404.html 502.html index.html .user.ini
|
||||||
|
|
||||||
# 克隆仓库
|
# Clone repository
|
||||||
git clone https://github.com/Micah123321/Xboard.git ./
|
git clone https://github.com/cedar2025/Xboard.git ./
|
||||||
|
|
||||||
# 准备配置文件
|
# Prepare configuration file
|
||||||
cp compose.sample.yaml compose.yaml
|
cp compose.sample.yaml compose.yaml
|
||||||
|
|
||||||
# 安装依赖并初始化
|
# Install dependencies and initialize
|
||||||
docker compose run -it --rm web sh init.sh
|
docker compose run -it --rm web sh init.sh
|
||||||
```
|
```
|
||||||
> 请保存安装完成后显示的管理后台地址、用户名和密码
|
> ⚠️ Please save the admin dashboard URL, username, and password shown after installation
|
||||||
|
|
||||||
#### 3.3 启动服务
|
#### 3.3 Start Services
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3.4 配置反向代理
|
#### 3.4 Configure Reverse Proxy
|
||||||
将以下内容添加到网站配置:
|
Add the following content to your site configuration:
|
||||||
```nginx
|
```nginx
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://127.0.0.1:8076;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
location ^~ / {
|
location ^~ / {
|
||||||
proxy_pass http://127.0.0.1:7001;
|
proxy_pass http://127.0.0.1:7001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -100,19 +110,20 @@ location ^~ / {
|
|||||||
proxy_cache off;
|
proxy_cache off;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
> The `/ws/` location enables real-time node synchronization via `ws-server`. This service is enabled by default and can be toggled in Admin Panel > System Settings > Server.
|
||||||
|
|
||||||
## 维护指南
|
## Maintenance Guide
|
||||||
|
|
||||||
### 版本更新
|
### Version Updates
|
||||||
|
|
||||||
> 重要说明:更新命令会因你安装的版本不同而有所区别:
|
> 💡 Important Note: Update commands may vary depending on your installed version:
|
||||||
> - 如果是最近安装(新版本),使用:
|
> - For recent installations (new version), use:
|
||||||
```bash
|
```bash
|
||||||
docker compose pull && \
|
docker compose pull && \
|
||||||
docker compose run -it --rm web sh update.sh && \
|
docker compose run -it --rm web sh update.sh && \
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
> - 如果是较早安装(旧版本),请把 `web` 替换为 `xboard`:
|
> - For older installations, replace `web` with `xboard`:
|
||||||
```bash
|
```bash
|
||||||
git config --global --add safe.directory $(pwd)
|
git config --global --add safe.directory $(pwd)
|
||||||
git fetch --all && git reset --hard origin/master && git pull origin master
|
git fetch --all && git reset --hard origin/master && git pull origin master
|
||||||
@@ -120,18 +131,21 @@ docker compose pull && \
|
|||||||
docker compose run -it --rm xboard sh update.sh && \
|
docker compose run -it --rm xboard sh update.sh && \
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
> 不确定该用哪个命令?先尝试新版本命令,失败后再使用旧版本命令。
|
> 🤔 Not sure which to use? Try the new version command first, if it fails, use the old version command.
|
||||||
|
|
||||||
### 日常维护
|
### Routine Maintenance
|
||||||
- 定期查看日志:`docker compose logs`
|
- Regular log checking: `docker compose logs`
|
||||||
- 监控系统资源使用情况
|
- Monitor system resource usage
|
||||||
- 定期备份数据库和配置文件
|
- Regular backup of database and configuration files
|
||||||
|
|
||||||
## 故障排查
|
## Troubleshooting
|
||||||
|
|
||||||
如果在安装或运行中遇到问题,请检查:
|
If you encounter any issues during installation or operation, please check:
|
||||||
1. 系统要求是否满足
|
1. **Empty Admin Dashboard**: If the admin panel is blank, run `git submodule update --init --recursive --force` to restore the theme files.
|
||||||
2. 所有必需端口是否可用
|
2. System requirements are met
|
||||||
3. Docker 服务是否正常运行
|
3. All required ports are available
|
||||||
4. Nginx 配置是否正确
|
3. Docker services are running properly
|
||||||
5. 查看日志以获取详细报错信息
|
4. Nginx configuration is correct
|
||||||
|
5. Check logs for detailed error messages
|
||||||
|
|
||||||
|
> The node will automatically detect WebSocket availability during handshake. No extra configuration is needed on the node side.
|
||||||
|
|||||||
+116
-81
@@ -1,46 +1,46 @@
|
|||||||
# aaPanel 环境下的 Xboard 部署指南
|
# Xboard Deployment Guide for aaPanel Environment
|
||||||
|
|
||||||
## 目录
|
## Table of Contents
|
||||||
1. [环境要求](#环境要求)
|
1. [Requirements](#requirements)
|
||||||
2. [快速部署](#快速部署)
|
2. [Quick Deployment](#quick-deployment)
|
||||||
3. [详细配置](#详细配置)
|
3. [Detailed Configuration](#detailed-configuration)
|
||||||
4. [维护指南](#维护指南)
|
4. [Maintenance Guide](#maintenance-guide)
|
||||||
5. [故障排查](#故障排查)
|
5. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
## 环境要求
|
## Requirements
|
||||||
|
|
||||||
### 硬件要求
|
### Hardware Requirements
|
||||||
- CPU:1 核及以上
|
- CPU: 1 core or above
|
||||||
- 内存:2GB 及以上
|
- Memory: 2GB or above
|
||||||
- 存储:可用空间 10GB+
|
- Storage: 10GB+ available space
|
||||||
|
|
||||||
### 软件要求
|
### Software Requirements
|
||||||
- 操作系统:Ubuntu 20.04+ / Debian 10+(CentOS 7 不推荐)
|
- Operating System: Ubuntu 20.04+ / Debian 10+ (⚠️ CentOS 7 is not recommended)
|
||||||
- aaPanel 最新版本
|
- Latest version of aaPanel
|
||||||
- PHP 8.2
|
- PHP 8.2
|
||||||
- MySQL 5.7+
|
- MySQL 5.7+
|
||||||
- Redis
|
- Redis
|
||||||
- Nginx(任意版本)
|
- Nginx (any version)
|
||||||
|
|
||||||
## 快速部署
|
## Quick Deployment
|
||||||
|
|
||||||
### 1. 安装 aaPanel
|
### 1. Install aaPanel
|
||||||
```bash
|
```bash
|
||||||
URL=https://www.aapanel.com/script/install_6.0_en.sh && \
|
URL=https://www.aapanel.com/script/install_6.0_en.sh && \
|
||||||
if [ -f /usr/bin/curl ];then curl -ksSO "$URL" ;else wget --no-check-certificate -O install_6.0_en.sh "$URL";fi && \
|
if [ -f /usr/bin/curl ];then curl -ksSO "$URL" ;else wget --no-check-certificate -O install_6.0_en.sh "$URL";fi && \
|
||||||
bash install_6.0_en.sh aapanel
|
bash install_6.0_en.sh aapanel
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 基础环境配置
|
### 2. Basic Environment Setup
|
||||||
|
|
||||||
#### 2.1 安装 LNMP 环境
|
#### 2.1 Install LNMP Environment
|
||||||
在 aaPanel 面板中安装:
|
In the aaPanel dashboard, install:
|
||||||
- Nginx(任意版本)
|
- Nginx (any version)
|
||||||
- MySQL 5.7
|
- MySQL 5.7
|
||||||
- PHP 8.2
|
- PHP 8.2
|
||||||
|
|
||||||
#### 2.2 安装 PHP 扩展
|
#### 2.2 Install PHP Extensions
|
||||||
必需的 PHP 扩展:
|
Required PHP extensions:
|
||||||
- redis
|
- redis
|
||||||
- fileinfo
|
- fileinfo
|
||||||
- swoole
|
- swoole
|
||||||
@@ -48,41 +48,41 @@ bash install_6.0_en.sh aapanel
|
|||||||
- event
|
- event
|
||||||
- mbstring
|
- mbstring
|
||||||
|
|
||||||
#### 2.3 启用所需 PHP 函数
|
#### 2.3 Enable Required PHP Functions
|
||||||
需要启用的函数:
|
Functions that need to be enabled:
|
||||||
- putenv
|
- putenv
|
||||||
- proc_open
|
- proc_open
|
||||||
- pcntl_alarm
|
- pcntl_alarm
|
||||||
- pcntl_signal
|
- pcntl_signal
|
||||||
|
|
||||||
### 3. 网站配置
|
### 3. Site Configuration
|
||||||
|
|
||||||
#### 3.1 创建网站
|
#### 3.1 Create Website
|
||||||
1. 进入:aaPanel > 网站 > 添加站点
|
1. Navigate to: aaPanel > Website > Add site
|
||||||
2. 填写信息:
|
2. Fill in the information:
|
||||||
- 域名:填写你的网站域名
|
- Domain: Enter your site domain
|
||||||
- 数据库:选择 MySQL
|
- Database: Select MySQL
|
||||||
- PHP 版本:选择 8.2
|
- PHP Version: Select 8.2
|
||||||
|
|
||||||
#### 3.2 部署 Xboard
|
#### 3.2 Deploy Xboard
|
||||||
```bash
|
```bash
|
||||||
# 进入网站目录
|
# Enter site directory
|
||||||
cd /www/wwwroot/your-domain
|
cd /www/wwwroot/your-domain
|
||||||
|
|
||||||
# 清理目录
|
# Clean directory
|
||||||
chattr -i .user.ini
|
chattr -i .user.ini
|
||||||
rm -rf .htaccess 404.html 502.html index.html .user.ini
|
rm -rf .htaccess 404.html 502.html index.html .user.ini
|
||||||
|
|
||||||
# 克隆仓库
|
# Clone repository
|
||||||
git clone https://github.com/Micah123321/Xboard.git ./
|
git clone https://github.com/cedar2025/Xboard.git ./
|
||||||
|
|
||||||
# 安装依赖
|
# Install dependencies
|
||||||
sh init.sh
|
sh init.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3.3 配置网站
|
#### 3.3 Configure Site
|
||||||
1. 运行目录设置为 `/public`
|
1. Set running directory to `/public`
|
||||||
2. 添加伪静态规则:
|
2. Add rewrite rules:
|
||||||
```nginx
|
```nginx
|
||||||
location /downloads {
|
location /downloads {
|
||||||
}
|
}
|
||||||
@@ -99,33 +99,33 @@ location ~ .*\.(js|css)?$
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 详细配置
|
## Detailed Configuration
|
||||||
|
|
||||||
### 1. 配置守护进程
|
### 1. Configure Daemon Process
|
||||||
1. 安装 Supervisor
|
1. Install Supervisor
|
||||||
2. 添加队列守护进程:
|
2. Add queue daemon process:
|
||||||
- 名称:`Xboard`
|
- Name: `Xboard`
|
||||||
- 运行用户:`www`
|
- Run User: `www`
|
||||||
- 运行目录:网站目录
|
- Running Directory: Site directory
|
||||||
- 启动命令:`php artisan horizon`
|
- Start Command: `php artisan horizon`
|
||||||
- 进程数量:1
|
- Process Count: 1
|
||||||
|
|
||||||
### 2. 配置计划任务
|
### 2. Configure Scheduled Tasks
|
||||||
- 类型:Shell 脚本
|
- Type: Shell Script
|
||||||
- 任务名称:v2board
|
- Task Name: v2board
|
||||||
- 执行用户:www
|
- Run User: www
|
||||||
- 执行周期:每 1 分钟
|
- Frequency: 1 minute
|
||||||
- 脚本内容:`php /www/wwwroot/site-directory/artisan schedule:run`
|
- Script Content: `php /www/wwwroot/site-directory/artisan schedule:run`
|
||||||
|
|
||||||
### 3. Octane 配置(可选)
|
### 3. Octane Configuration (Optional)
|
||||||
#### 3.1 添加 Octane 守护进程
|
#### 3.1 Add Octane Daemon Process
|
||||||
- 名称:Octane
|
- Name: Octane
|
||||||
- 运行用户:www
|
- Run User: www
|
||||||
- 运行目录:网站目录
|
- Running Directory: Site directory
|
||||||
- 启动命令:`/www/server/php/82/bin/php artisan octane:start --port 7010`
|
- Start Command: `/www/server/php/82/bin/php artisan octane:start --port 7010`
|
||||||
- 进程数量:1
|
- Process Count: 1
|
||||||
|
|
||||||
#### 3.2 Octane 专用伪静态规则
|
#### 3.2 Octane-specific Rewrite Rules
|
||||||
```nginx
|
```nginx
|
||||||
location ~* \.(jpg|jpeg|png|gif|js|css|svg|woff2|woff|ttf|eot|wasm|json|ico)$ {
|
location ~* \.(jpg|jpeg|png|gif|js|css|svg|woff2|woff|ttf|eot|wasm|json|ico)$ {
|
||||||
}
|
}
|
||||||
@@ -146,30 +146,65 @@ location ~ .* {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 维护指南
|
## Maintenance Guide
|
||||||
|
|
||||||
### 版本更新
|
### Version Updates
|
||||||
```bash
|
```bash
|
||||||
# 进入网站目录
|
# Enter site directory
|
||||||
cd /www/wwwroot/your-domain
|
cd /www/wwwroot/your-domain
|
||||||
|
|
||||||
# 执行更新脚本
|
# Execute update script
|
||||||
git fetch --all && git reset --hard origin/master && git pull origin master
|
git fetch --all && git reset --hard origin/master && git pull origin master
|
||||||
sh update.sh
|
sh update.sh
|
||||||
|
|
||||||
# 如果启用了 Octane,请重启守护进程
|
# If Octane is enabled, restart the daemon process
|
||||||
# aaPanel > 应用商店 > 工具 > Supervisor > 重启 Octane
|
# aaPanel > App Store > Tools > Supervisor > Restart Octane
|
||||||
```
|
```
|
||||||
|
|
||||||
### 日常维护
|
### Routine Maintenance
|
||||||
- 定期检查日志
|
- Regular log checking
|
||||||
- 监控系统资源使用情况
|
- Monitor system resource usage
|
||||||
- 定期备份数据库和配置文件
|
- Regular backup of database and configuration files
|
||||||
|
|
||||||
## 故障排查
|
## Troubleshooting
|
||||||
|
|
||||||
### 常见问题
|
### Common Issues
|
||||||
1. 修改后台路径后,需要重启服务才会生效
|
1. **Empty Admin Dashboard**: If the admin panel is blank, run `git submodule update --init --recursive --force` to restore the theme files.
|
||||||
2. 启用 Octane 后,任何代码变更都需要重启才会生效
|
2. Changes to admin path require service restart to take effect
|
||||||
3. PHP 扩展安装失败时,请检查 PHP 版本是否正确
|
3. Any code changes after enabling Octane require restart to take effect
|
||||||
4. 数据库连接失败时,请检查数据库配置和权限
|
3. When PHP extension installation fails, check if PHP version is correct
|
||||||
|
4. For database connection failures, check database configuration and permissions
|
||||||
|
|
||||||
|
## Enable WebSocket Real-time Sync (Optional)
|
||||||
|
|
||||||
|
WebSocket enables real-time synchronization of configurations and user changes to nodes.
|
||||||
|
|
||||||
|
### 1. Start WS Server
|
||||||
|
|
||||||
|
Add a WebSocket daemon process in aaPanel Supervisor:
|
||||||
|
- Name: `Xboard-WS`
|
||||||
|
- Run User: `www`
|
||||||
|
- Running Directory: Site directory
|
||||||
|
- Start Command: `php artisan ws-server start`
|
||||||
|
- Process Count: 1
|
||||||
|
|
||||||
|
### 2. Configure Nginx
|
||||||
|
|
||||||
|
Add the WebSocket location **before** the main `location ^~ /` block in your site's Nginx configuration:
|
||||||
|
```nginx
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://127.0.0.1:8076;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Restart Services
|
||||||
|
|
||||||
|
Restart the Octane and WS Server processes in Supervisor.
|
||||||
|
|
||||||
|
> The node will automatically detect WebSocket availability during handshake. No extra configuration is needed on the node side.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
rm -rf composer.phar
|
rm -rf composer.phar
|
||||||
wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar
|
wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar
|
||||||
php composer.phar install -vvv
|
php composer.phar install -vvv
|
||||||
|
git submodule update --init --recursive --force
|
||||||
php artisan xboard:install
|
php artisan xboard:install
|
||||||
|
|
||||||
if [ -f "/etc/init.d/bt" ] || [ -f "/.dockerenv" ]; then
|
if [ -f "/etc/init.d/bt" ] || [ -f "/.dockerenv" ]; then
|
||||||
|
|||||||
Submodule
+1
Submodule public/assets/admin added at 9d13978a61
Binary file not shown.
File diff suppressed because one or more lines are too long
-470
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-21
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-67731
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
-1947
File diff suppressed because one or more lines are too long
@@ -1,32 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
|
||||||
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Shadcn Admin</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
|
||||||
/>
|
|
||||||
<script>
|
|
||||||
window.settings = {
|
|
||||||
base_url: 'http://127.0.0.1:8000',
|
|
||||||
title: 'Xboard',
|
|
||||||
version: '1.0.0',
|
|
||||||
logo: 'https://xboard.io/i6mages/logo.png',
|
|
||||||
secure_path: '/afbced4e',
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script src="./locales/en-US.js"></script>
|
|
||||||
<script src="./locales/zh-CN.js"></script>
|
|
||||||
<script src="./locales/ko-KR.js"></script>
|
|
||||||
<script type="module" crossorigin src="./assets/index.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index.css">
|
|
||||||
<link rel="stylesheet" crossorigin href="./assets/vendor.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Vendored
-3029
File diff suppressed because it is too large
Load Diff
Vendored
-2151
File diff suppressed because it is too large
Load Diff
Vendored
-3046
File diff suppressed because it is too large
Load Diff
+226
-522
@@ -1,233 +1,94 @@
|
|||||||
# port: 7890
|
|
||||||
# socks-port: 7891
|
|
||||||
# redir-port: 7892
|
|
||||||
# tproxy-port: 7893
|
|
||||||
mixed-port: 7890
|
mixed-port: 7890
|
||||||
allow-lan: true
|
allow-lan: true
|
||||||
bind-address: "*"
|
bind-address: "*"
|
||||||
mode: rule
|
mode: rule
|
||||||
log-level: info
|
log-level: info
|
||||||
external-controller: 127.0.0.1:9090
|
external-controller: 127.0.0.1:9090
|
||||||
|
unified-delay: true
|
||||||
|
tcp-concurrent: true
|
||||||
|
|
||||||
dns:
|
dns:
|
||||||
enable: true
|
enable: true
|
||||||
# listen: 0.0.0.0:53
|
|
||||||
ipv6: false
|
ipv6: false
|
||||||
|
|
||||||
default-nameserver:
|
default-nameserver:
|
||||||
- 223.5.5.5
|
- 223.5.5.5
|
||||||
- 119.29.29.29
|
- 119.29.29.29
|
||||||
enhanced-mode: fake-ip
|
enhanced-mode: fake-ip
|
||||||
fake-ip-range: 198.18.0.1/16
|
fake-ip-range: 198.18.0.1/16
|
||||||
use-hosts: true
|
use-hosts: true
|
||||||
|
nameserver-policy:
|
||||||
|
"+.google.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.googleapis.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.googleapis.cn": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.googlevideo.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.gstatic.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.youtube.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.youtu.be": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.facebook.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.twitter.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.x.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.github.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.githubusercontent.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.openai.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.chatgpt.com": "https://dns.cloudflare.com/dns-query"
|
||||||
|
"+.anthropic.com": "https://dns.cloudflare.com/dns-query"
|
||||||
nameserver:
|
nameserver:
|
||||||
- https://doh.pub/dns-query
|
- https://doh.pub/dns-query
|
||||||
- https://dns.alidns.com/dns-query
|
- https://dns.alidns.com/dns-query
|
||||||
|
- tls://dot.pub:853
|
||||||
|
- tls://dns.alidns.com:853
|
||||||
fallback:
|
fallback:
|
||||||
- https://doh-pure.onedns.net/dns-query
|
- https://dns.cloudflare.com/dns-query
|
||||||
- https://ada.openbld.net/dns-query
|
- https://dns.google/dns-query
|
||||||
- https://223.5.5.5/dns-query
|
- tls://1.1.1.1:853
|
||||||
- https://223.6.6.6/dns-query
|
- tls://8.8.8.8:853
|
||||||
fallback-filter:
|
fallback-filter:
|
||||||
geoip: true
|
geoip: true
|
||||||
|
geoip-code: CN
|
||||||
ipcidr:
|
ipcidr:
|
||||||
|
- 0.0.0.0/8
|
||||||
|
- 10.0.0.0/8
|
||||||
|
- 100.64.0.0/10
|
||||||
|
- 127.0.0.0/8
|
||||||
|
- 169.254.0.0/16
|
||||||
|
- 172.16.0.0/12
|
||||||
|
- 192.168.0.0/16
|
||||||
|
- 224.0.0.0/4
|
||||||
- 240.0.0.0/4
|
- 240.0.0.0/4
|
||||||
- 0.0.0.0/32
|
domain:
|
||||||
|
- "+.google.com"
|
||||||
|
- "+.facebook.com"
|
||||||
|
- "+.youtube.com"
|
||||||
|
- "+.githubusercontent.com"
|
||||||
|
- "+.googlevideo.com"
|
||||||
|
- "+.googleapis.cn"
|
||||||
|
fake-ip-filter:
|
||||||
|
- "*.lan"
|
||||||
|
- "*.local"
|
||||||
|
- "*.localhost"
|
||||||
|
- "*.test"
|
||||||
|
- localhost.ptlogin2.qq.com
|
||||||
|
- "+.stun.*.*"
|
||||||
|
- "+.stun.*.*.*"
|
||||||
|
- "+.stun.*.*.*.*"
|
||||||
|
- "lens.l.google.com"
|
||||||
|
- "*.srv.nintendo.net"
|
||||||
|
- "+.stun.playstation.net"
|
||||||
|
- "xbox.*.*.microsoft.com"
|
||||||
|
- "*.*.xboxlive.com"
|
||||||
|
- "+.msftncsi.com"
|
||||||
|
- "+.msftconnecttest.com"
|
||||||
|
|
||||||
proxies:
|
proxies:
|
||||||
|
|
||||||
proxy-groups:
|
proxy-groups:
|
||||||
- { name: "$app_name", type: select, proxies: ["自动选择", "故障转移"] }
|
- { name: "$app_name", type: select, proxies: ["自动选择", "故障转移", "DIRECT"] }
|
||||||
- { name: "自动选择", type: url-test, proxies: [], url: "http://www.gstatic.com/generate_204", interval: 86400 }
|
- { name: "自动选择", type: url-test, proxies: [], url: "http://www.gstatic.com/generate_204", interval: 300, tolerance: 50 }
|
||||||
- { name: "故障转移", type: fallback, proxies: [], url: "http://www.gstatic.com/generate_204", interval: 7200 }
|
- { name: "故障转移", type: fallback, proxies: [], url: "http://www.gstatic.com/generate_204", interval: 300 }
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
# 自定义规则
|
# Custom
|
||||||
## 您可以在此处插入您补充的自定义规则(请注意保持缩进)
|
# Ad blocking
|
||||||
|
|
||||||
# Google 中国服务
|
|
||||||
- DOMAIN-SUFFIX,services.googleapis.cn,$app_name
|
|
||||||
- DOMAIN-SUFFIX,xn--ngstr-lra8j.com,$app_name
|
|
||||||
|
|
||||||
# Apple
|
|
||||||
- DOMAIN,safebrowsing.urlsec.qq.com,DIRECT # 如果您并不信任此服务提供商或防止其下载消耗过多带宽资源,可以进入 Safari 设置,关闭 Fraudulent Website Warning 功能,并使用 REJECT 策略。
|
|
||||||
- DOMAIN,safebrowsing.googleapis.com,DIRECT # 如果您并不信任此服务提供商或防止其下载消耗过多带宽资源,可以进入 Safari 设置,关闭 Fraudulent Website Warning 功能,并使用 REJECT 策略。
|
|
||||||
- DOMAIN,developer.apple.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,digicert.com,$app_name
|
|
||||||
- DOMAIN,ocsp.apple.com,$app_name
|
|
||||||
- DOMAIN,ocsp.comodoca.com,$app_name
|
|
||||||
- DOMAIN,ocsp.usertrust.com,$app_name
|
|
||||||
- DOMAIN,ocsp.sectigo.com,$app_name
|
|
||||||
- DOMAIN,ocsp.verisign.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,apple-dns.net,$app_name
|
|
||||||
- DOMAIN,testflight.apple.com,$app_name
|
|
||||||
- DOMAIN,sandbox.itunes.apple.com,$app_name
|
|
||||||
- DOMAIN,itunes.apple.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,apps.apple.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,blobstore.apple.com,$app_name
|
|
||||||
- DOMAIN,cvws.icloud-content.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,mzstatic.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,itunes.apple.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,icloud.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,icloud-content.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,me.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,aaplimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,cdn20.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,cdn-apple.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,akadns.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,akamaiedge.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,edgekey.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,mwcloudcdn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,mwcname.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,apple.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,apple-cloudkit.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,apple-mapkit.com,DIRECT
|
|
||||||
# - DOMAIN,e.crashlytics.com,REJECT //注释此选项有助于大多数App开发者分析崩溃信息;如果您拒绝一切崩溃数据统计、搜集,请取消 # 注释。
|
|
||||||
|
|
||||||
# 国内网站
|
|
||||||
- DOMAIN-SUFFIX,126.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,126.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,127.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,163.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,360buyimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,36kr.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,acfun.tv,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,air-matters.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,aixifan.com,DIRECT
|
|
||||||
- DOMAIN-KEYWORD,alicdn,DIRECT
|
|
||||||
- DOMAIN-KEYWORD,alipay,DIRECT
|
|
||||||
- DOMAIN-KEYWORD,taobao,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,amap.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,autonavi.com,DIRECT
|
|
||||||
- DOMAIN-KEYWORD,baidu,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,bdimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,bdstatic.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,bilibili.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,bilivideo.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,caiyunapp.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,clouddn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,cnbeta.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,cnbetacdn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,cootekservice.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,csdn.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ctrip.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,dgtle.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,dianping.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,douban.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,doubanio.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,duokan.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,easou.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ele.me,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,feng.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,fir.im,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,frdic.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,g-cores.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,godic.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,gtimg.com,DIRECT
|
|
||||||
- DOMAIN,cdn.hockeyapp.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,hongxiu.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,hxcdn.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,iciba.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ifeng.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ifengimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ipip.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,iqiyi.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,jd.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,jianshu.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,knewone.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,le.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,lecloud.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,lemicp.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,licdn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,luoo.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,meituan.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,meituan.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,mi.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,miaopai.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,microsoft.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,microsoftonline.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,miui.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,miwifi.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,mob.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,netease.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,office.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,office365.com,DIRECT
|
|
||||||
- DOMAIN-KEYWORD,officecdn,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,oschina.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ppsimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,pstatp.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qcloud.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qdaily.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qdmm.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qhimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qhres.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qidian.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qihucdn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qiniu.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qiniucdn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qiyipic.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qq.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,qqurl.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,rarbg.to,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ruguoapp.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,segmentfault.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,sinaapp.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,smzdm.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,snapdrop.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,sogou.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,sogoucdn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,sohu.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,soku.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,speedtest.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,sspai.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,suning.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,taobao.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,tencent.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,tenpay.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,tianyancha.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,tmall.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,tudou.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,umetrip.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,upaiyun.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,upyun.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,veryzhun.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,weather.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,weibo.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,xiami.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,xiami.net,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,xiaomicp.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ximalaya.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,xmcdn.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,xunlei.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,yhd.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,yihaodianimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,yinxiang.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,ykimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,youdao.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,youku.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,zealer.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,zhihu.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,zhimg.com,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,zimuzu.tv,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,zoho.com,DIRECT
|
|
||||||
|
|
||||||
# 抗 DNS 污染
|
|
||||||
- DOMAIN-KEYWORD,amazon,$app_name
|
|
||||||
- DOMAIN-KEYWORD,google,$app_name
|
|
||||||
- DOMAIN-KEYWORD,gmail,$app_name
|
|
||||||
- DOMAIN-KEYWORD,youtube,$app_name
|
|
||||||
- DOMAIN-KEYWORD,facebook,$app_name
|
|
||||||
- DOMAIN-SUFFIX,fb.me,$app_name
|
|
||||||
- DOMAIN-SUFFIX,fbcdn.net,$app_name
|
|
||||||
- DOMAIN-KEYWORD,twitter,$app_name
|
|
||||||
- DOMAIN-KEYWORD,instagram,$app_name
|
|
||||||
- DOMAIN-KEYWORD,dropbox,$app_name
|
|
||||||
- DOMAIN-SUFFIX,twimg.com,$app_name
|
|
||||||
- DOMAIN-KEYWORD,blogspot,$app_name
|
|
||||||
- DOMAIN-SUFFIX,youtu.be,$app_name
|
|
||||||
- DOMAIN-KEYWORD,whatsapp,$app_name
|
|
||||||
|
|
||||||
# 常见广告域名屏蔽
|
|
||||||
- DOMAIN-KEYWORD,admarvel,REJECT
|
- DOMAIN-KEYWORD,admarvel,REJECT
|
||||||
- DOMAIN-KEYWORD,admaster,REJECT
|
- DOMAIN-KEYWORD,admaster,REJECT
|
||||||
- DOMAIN-KEYWORD,adsage,REJECT
|
- DOMAIN-KEYWORD,adsage,REJECT
|
||||||
@@ -235,348 +96,191 @@ rules:
|
|||||||
- DOMAIN-KEYWORD,adsrvmedia,REJECT
|
- DOMAIN-KEYWORD,adsrvmedia,REJECT
|
||||||
- DOMAIN-KEYWORD,adwords,REJECT
|
- DOMAIN-KEYWORD,adwords,REJECT
|
||||||
- DOMAIN-KEYWORD,adservice,REJECT
|
- DOMAIN-KEYWORD,adservice,REJECT
|
||||||
- DOMAIN-SUFFIX,appsflyer.com,REJECT
|
|
||||||
- DOMAIN-KEYWORD,domob,REJECT
|
- DOMAIN-KEYWORD,domob,REJECT
|
||||||
- DOMAIN-SUFFIX,doubleclick.net,REJECT
|
|
||||||
- DOMAIN-KEYWORD,duomeng,REJECT
|
- DOMAIN-KEYWORD,duomeng,REJECT
|
||||||
- DOMAIN-KEYWORD,dwtrack,REJECT
|
- DOMAIN-KEYWORD,dwtrack,REJECT
|
||||||
- DOMAIN-KEYWORD,guanggao,REJECT
|
- DOMAIN-KEYWORD,guanggao,REJECT
|
||||||
- DOMAIN-KEYWORD,lianmeng,REJECT
|
- DOMAIN-KEYWORD,lianmeng,REJECT
|
||||||
- DOMAIN-SUFFIX,mmstat.com,REJECT
|
|
||||||
- DOMAIN-KEYWORD,mopub,REJECT
|
|
||||||
- DOMAIN-KEYWORD,omgmta,REJECT
|
- DOMAIN-KEYWORD,omgmta,REJECT
|
||||||
- DOMAIN-KEYWORD,openx,REJECT
|
- DOMAIN-KEYWORD,openx,REJECT
|
||||||
- DOMAIN-KEYWORD,partnerad,REJECT
|
- DOMAIN-KEYWORD,partnerad,REJECT
|
||||||
- DOMAIN-KEYWORD,pingfore,REJECT
|
|
||||||
- DOMAIN-KEYWORD,supersonicads,REJECT
|
- DOMAIN-KEYWORD,supersonicads,REJECT
|
||||||
- DOMAIN-KEYWORD,uedas,REJECT
|
|
||||||
- DOMAIN-KEYWORD,umeng,REJECT
|
- DOMAIN-KEYWORD,umeng,REJECT
|
||||||
- DOMAIN-KEYWORD,usage,REJECT
|
|
||||||
- DOMAIN-SUFFIX,vungle.com,REJECT
|
|
||||||
- DOMAIN-KEYWORD,wlmonitor,REJECT
|
|
||||||
- DOMAIN-KEYWORD,zjtoolbar,REJECT
|
- DOMAIN-KEYWORD,zjtoolbar,REJECT
|
||||||
|
- DOMAIN-SUFFIX,appsflyer.com,REJECT
|
||||||
# 国外网站
|
- DOMAIN-SUFFIX,doubleclick.net,REJECT
|
||||||
- DOMAIN-SUFFIX,9to5mac.com,$app_name
|
- DOMAIN-SUFFIX,mmstat.com,REJECT
|
||||||
- DOMAIN-SUFFIX,abpchina.org,$app_name
|
# LAN
|
||||||
- DOMAIN-SUFFIX,adblockplus.org,$app_name
|
- DOMAIN-SUFFIX,local,DIRECT
|
||||||
- DOMAIN-SUFFIX,adobe.com,$app_name
|
- DOMAIN-SUFFIX,localhost,DIRECT
|
||||||
- DOMAIN-SUFFIX,akamaized.net,$app_name
|
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,alfredapp.com,$app_name
|
- IP-CIDR,17.0.0.0/8,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,amplitude.com,$app_name
|
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,ampproject.org,$app_name
|
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,android.com,$app_name
|
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,angularjs.org,$app_name
|
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,aolcdn.com,$app_name
|
- IP-CIDR,198.18.0.0/16,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,apkpure.com,$app_name
|
- IP-CIDR,224.0.0.0/4,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,appledaily.com,$app_name
|
- IP-CIDR6,::1/128,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,appshopper.com,$app_name
|
- IP-CIDR6,fc00::/7,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,appspot.com,$app_name
|
- IP-CIDR6,fe80::/10,DIRECT,no-resolve
|
||||||
- DOMAIN-SUFFIX,arcgis.com,$app_name
|
# Apple (App Store via proxy for foreign regions)
|
||||||
- DOMAIN-SUFFIX,archive.org,$app_name
|
- DOMAIN-SUFFIX,apps.apple.com,$app_name
|
||||||
- DOMAIN-SUFFIX,armorgames.com,$app_name
|
- DOMAIN-SUFFIX,itunes.apple.com,$app_name
|
||||||
- DOMAIN-SUFFIX,aspnetcdn.com,$app_name
|
- DOMAIN-SUFFIX,blobstore.apple.com,$app_name
|
||||||
- DOMAIN-SUFFIX,att.com,$app_name
|
- DOMAIN,safebrowsing.urlsec.qq.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,awsstatic.com,$app_name
|
- DOMAIN-SUFFIX,apple.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,azureedge.net,$app_name
|
- DOMAIN-SUFFIX,apple-cloudkit.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,azurewebsites.net,$app_name
|
- DOMAIN-SUFFIX,icloud.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,bing.com,$app_name
|
- DOMAIN-SUFFIX,icloud-content.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,bintray.com,$app_name
|
- DOMAIN-SUFFIX,mzstatic.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,bit.com,$app_name
|
- DOMAIN-SUFFIX,aaplimg.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,bit.ly,$app_name
|
- DOMAIN-SUFFIX,cdn-apple.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,bitbucket.org,$app_name
|
- DOMAIN-SUFFIX,akadns.net,DIRECT
|
||||||
- DOMAIN-SUFFIX,bjango.com,$app_name
|
# China direct (KEYWORD)
|
||||||
- DOMAIN-SUFFIX,bkrtx.com,$app_name
|
- DOMAIN-KEYWORD,baidu,DIRECT
|
||||||
- DOMAIN-SUFFIX,blog.com,$app_name
|
- DOMAIN-KEYWORD,alibaba,DIRECT
|
||||||
- DOMAIN-SUFFIX,blogcdn.com,$app_name
|
- DOMAIN-KEYWORD,alicdn,DIRECT
|
||||||
- DOMAIN-SUFFIX,blogger.com,$app_name
|
- DOMAIN-KEYWORD,alipay,DIRECT
|
||||||
- DOMAIN-SUFFIX,blogsmithmedia.com,$app_name
|
- DOMAIN-KEYWORD,taobao,DIRECT
|
||||||
- DOMAIN-SUFFIX,blogspot.com,$app_name
|
- DOMAIN-KEYWORD,tencent,DIRECT
|
||||||
- DOMAIN-SUFFIX,blogspot.hk,$app_name
|
- DOMAIN-KEYWORD,bilibili,DIRECT
|
||||||
- DOMAIN-SUFFIX,bloomberg.com,$app_name
|
- DOMAIN-KEYWORD,weibo,DIRECT
|
||||||
- DOMAIN-SUFFIX,box.com,$app_name
|
- DOMAIN-KEYWORD,douyin,DIRECT
|
||||||
- DOMAIN-SUFFIX,box.net,$app_name
|
- DOMAIN-KEYWORD,bytedance,DIRECT
|
||||||
- DOMAIN-SUFFIX,cachefly.net,$app_name
|
- DOMAIN-KEYWORD,xiaomi,DIRECT
|
||||||
- DOMAIN-SUFFIX,chromium.org,$app_name
|
- DOMAIN-KEYWORD,huawei,DIRECT
|
||||||
- DOMAIN-SUFFIX,cl.ly,$app_name
|
- DOMAIN-KEYWORD,netease,DIRECT
|
||||||
- DOMAIN-SUFFIX,cloudflare.com,$app_name
|
- DOMAIN-KEYWORD,meituan,DIRECT
|
||||||
- DOMAIN-SUFFIX,cloudfront.net,$app_name
|
- DOMAIN-KEYWORD,pinduoduo,DIRECT
|
||||||
- DOMAIN-SUFFIX,cloudmagic.com,$app_name
|
- DOMAIN-KEYWORD,kuaishou,DIRECT
|
||||||
- DOMAIN-SUFFIX,cmail19.com,$app_name
|
- DOMAIN-KEYWORD,jingdong,DIRECT
|
||||||
- DOMAIN-SUFFIX,cnet.com,$app_name
|
- DOMAIN-KEYWORD,officecdn,DIRECT
|
||||||
- DOMAIN-SUFFIX,cocoapods.org,$app_name
|
# China direct (SUFFIX)
|
||||||
- DOMAIN-SUFFIX,comodoca.com,$app_name
|
- DOMAIN-SUFFIX,qq.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,crashlytics.com,$app_name
|
- DOMAIN-SUFFIX,weixin.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,culturedcode.com,$app_name
|
- DOMAIN-SUFFIX,wechat.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,d.pr,$app_name
|
- DOMAIN-SUFFIX,gtimg.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,danilo.to,$app_name
|
- DOMAIN-SUFFIX,qcloud.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,dayone.me,$app_name
|
- DOMAIN-SUFFIX,myqcloud.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,db.tt,$app_name
|
- DOMAIN-SUFFIX,qpic.cn,DIRECT
|
||||||
- DOMAIN-SUFFIX,deskconnect.com,$app_name
|
- DOMAIN-SUFFIX,tenpay.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,disq.us,$app_name
|
- DOMAIN-SUFFIX,tmall.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,disqus.com,$app_name
|
- DOMAIN-SUFFIX,jd.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,disquscdn.com,$app_name
|
- DOMAIN-SUFFIX,360buyimg.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,dnsimple.com,$app_name
|
- DOMAIN-SUFFIX,iqiyi.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,docker.com,$app_name
|
- DOMAIN-SUFFIX,youku.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,dribbble.com,$app_name
|
- DOMAIN-SUFFIX,ykimg.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,droplr.com,$app_name
|
- DOMAIN-SUFFIX,tudou.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,duckduckgo.com,$app_name
|
- DOMAIN-SUFFIX,acfun.tv,DIRECT
|
||||||
- DOMAIN-SUFFIX,dueapp.com,$app_name
|
- DOMAIN-SUFFIX,hdslb.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,dytt8.net,$app_name
|
- DOMAIN-SUFFIX,sohu.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,edgecastcdn.net,$app_name
|
- DOMAIN-SUFFIX,sogou.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,edgekey.net,$app_name
|
- DOMAIN-SUFFIX,zhihu.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,edgesuite.net,$app_name
|
- DOMAIN-SUFFIX,zhimg.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,engadget.com,$app_name
|
- DOMAIN-SUFFIX,douban.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,entrust.net,$app_name
|
- DOMAIN-SUFFIX,doubanio.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,eurekavpt.com,$app_name
|
- DOMAIN-SUFFIX,163.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,evernote.com,$app_name
|
- DOMAIN-SUFFIX,126.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,fabric.io,$app_name
|
- DOMAIN-SUFFIX,126.net,DIRECT
|
||||||
- DOMAIN-SUFFIX,fast.com,$app_name
|
- DOMAIN-SUFFIX,127.net,DIRECT
|
||||||
- DOMAIN-SUFFIX,fastly.net,$app_name
|
- DOMAIN-SUFFIX,yeah.net,DIRECT
|
||||||
- DOMAIN-SUFFIX,fc2.com,$app_name
|
- DOMAIN-SUFFIX,sina.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,feedburner.com,$app_name
|
- DOMAIN-SUFFIX,sinaimg.cn,DIRECT
|
||||||
- DOMAIN-SUFFIX,feedly.com,$app_name
|
- DOMAIN-SUFFIX,ximalaya.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,feedsportal.com,$app_name
|
- DOMAIN-SUFFIX,xmcdn.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,fiftythree.com,$app_name
|
- DOMAIN-SUFFIX,csdn.net,DIRECT
|
||||||
- DOMAIN-SUFFIX,firebaseio.com,$app_name
|
- DOMAIN-SUFFIX,gitee.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,flexibits.com,$app_name
|
- DOMAIN-SUFFIX,jianshu.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,flickr.com,$app_name
|
- DOMAIN-SUFFIX,cnblogs.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,flipboard.com,$app_name
|
- DOMAIN-SUFFIX,oschina.net,DIRECT
|
||||||
- DOMAIN-SUFFIX,g.co,$app_name
|
- DOMAIN-SUFFIX,ele.me,DIRECT
|
||||||
- DOMAIN-SUFFIX,gabia.net,$app_name
|
- DOMAIN-SUFFIX,ctrip.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,geni.us,$app_name
|
- DOMAIN-SUFFIX,suning.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,gfx.ms,$app_name
|
- DOMAIN-SUFFIX,dianping.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,ggpht.com,$app_name
|
- DOMAIN-SUFFIX,amap.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,ghostnoteapp.com,$app_name
|
- DOMAIN-SUFFIX,autonavi.com,DIRECT
|
||||||
- DOMAIN-SUFFIX,git.io,$app_name
|
- DOMAIN-SUFFIX,mi.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,miui.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,ifeng.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,youdao.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,iciba.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,xunlei.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,smzdm.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,sspai.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,36kr.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,speedtest.net,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,microsoft.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,microsoftonline.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,office.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,office365.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,windows.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,windowsupdate.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,live.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,msn.com,DIRECT
|
||||||
|
- DOMAIN-SUFFIX,cn,DIRECT
|
||||||
|
- DOMAIN-KEYWORD,-cn,DIRECT
|
||||||
|
# Blocked services (KEYWORD)
|
||||||
|
- DOMAIN-KEYWORD,google,$app_name
|
||||||
|
- DOMAIN-KEYWORD,gmail,$app_name
|
||||||
|
- DOMAIN-KEYWORD,youtube,$app_name
|
||||||
|
- DOMAIN-KEYWORD,facebook,$app_name
|
||||||
|
- DOMAIN-KEYWORD,twitter,$app_name
|
||||||
|
- DOMAIN-KEYWORD,instagram,$app_name
|
||||||
|
- DOMAIN-KEYWORD,whatsapp,$app_name
|
||||||
|
- DOMAIN-KEYWORD,telegram,$app_name
|
||||||
- DOMAIN-KEYWORD,github,$app_name
|
- DOMAIN-KEYWORD,github,$app_name
|
||||||
- DOMAIN-SUFFIX,globalsign.com,$app_name
|
- DOMAIN-KEYWORD,blogspot,$app_name
|
||||||
- DOMAIN-SUFFIX,gmodules.com,$app_name
|
- DOMAIN-KEYWORD,dropbox,$app_name
|
||||||
- DOMAIN-SUFFIX,godaddy.com,$app_name
|
- DOMAIN-KEYWORD,wikipedia,$app_name
|
||||||
- DOMAIN-SUFFIX,golang.org,$app_name
|
- DOMAIN-KEYWORD,pinterest,$app_name
|
||||||
- DOMAIN-SUFFIX,gongm.in,$app_name
|
- DOMAIN-KEYWORD,discord,$app_name
|
||||||
- DOMAIN-SUFFIX,goo.gl,$app_name
|
- DOMAIN-KEYWORD,openai,$app_name
|
||||||
- DOMAIN-SUFFIX,goodreaders.com,$app_name
|
- DOMAIN-KEYWORD,anthropic,$app_name
|
||||||
- DOMAIN-SUFFIX,goodreads.com,$app_name
|
- DOMAIN-KEYWORD,netflix,$app_name
|
||||||
- DOMAIN-SUFFIX,gravatar.com,$app_name
|
- DOMAIN-KEYWORD,spotify,$app_name
|
||||||
- DOMAIN-SUFFIX,gstatic.com,$app_name
|
- DOMAIN-KEYWORD,amazon,$app_name
|
||||||
- DOMAIN-SUFFIX,gvt0.com,$app_name
|
# Blocked services (SUFFIX)
|
||||||
- DOMAIN-SUFFIX,hockeyapp.net,$app_name
|
- DOMAIN-SUFFIX,t.co,$app_name
|
||||||
- DOMAIN-SUFFIX,hotmail.com,$app_name
|
- DOMAIN-SUFFIX,x.com,$app_name
|
||||||
- DOMAIN-SUFFIX,icons8.com,$app_name
|
- DOMAIN-SUFFIX,twimg.com,$app_name
|
||||||
- DOMAIN-SUFFIX,ifixit.com,$app_name
|
- DOMAIN-SUFFIX,fb.me,$app_name
|
||||||
- DOMAIN-SUFFIX,ift.tt,$app_name
|
- DOMAIN-SUFFIX,fbcdn.net,$app_name
|
||||||
- DOMAIN-SUFFIX,ifttt.com,$app_name
|
- DOMAIN-SUFFIX,youtu.be,$app_name
|
||||||
- DOMAIN-SUFFIX,iherb.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,imageshack.us,$app_name
|
|
||||||
- DOMAIN-SUFFIX,img.ly,$app_name
|
|
||||||
- DOMAIN-SUFFIX,imgur.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,imore.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,instapaper.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ipn.li,$app_name
|
|
||||||
- DOMAIN-SUFFIX,is.gd,$app_name
|
|
||||||
- DOMAIN-SUFFIX,issuu.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,itgonglun.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,itun.es,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ixquick.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,j.mp,$app_name
|
|
||||||
- DOMAIN-SUFFIX,js.revsci.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,jshint.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,jtvnw.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,justgetflux.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,kat.cr,$app_name
|
|
||||||
- DOMAIN-SUFFIX,klip.me,$app_name
|
|
||||||
- DOMAIN-SUFFIX,libsyn.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,linkedin.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,line-apps.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,linode.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,lithium.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,littlehj.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,live.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,live.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,livefilestore.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,llnwd.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,macid.co,$app_name
|
|
||||||
- DOMAIN-SUFFIX,macromedia.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,macrumors.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,mashable.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,mathjax.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,medium.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,mega.co.nz,$app_name
|
|
||||||
- DOMAIN-SUFFIX,mega.nz,$app_name
|
|
||||||
- DOMAIN-SUFFIX,megaupload.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,microsofttranslator.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,mindnode.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,mobile01.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,modmyi.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,msedge.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,myfontastic.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,name.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,nextmedia.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,nsstatic.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,nssurge.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,nyt.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,nytimes.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,omnigroup.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,onedrive.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,onenote.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ooyala.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,openvpn.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,openwrt.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,orkut.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,osxdaily.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,outlook.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ow.ly,$app_name
|
|
||||||
- DOMAIN-SUFFIX,paddleapi.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,parallels.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,parse.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,pdfexpert.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,periscope.tv,$app_name
|
|
||||||
- DOMAIN-SUFFIX,pinboard.in,$app_name
|
|
||||||
- DOMAIN-SUFFIX,pinterest.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,pixelmator.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,pixiv.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,playpcesor.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,playstation.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,playstation.com.hk,$app_name
|
|
||||||
- DOMAIN-SUFFIX,playstation.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,playstationnetwork.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,pushwoosh.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,rime.im,$app_name
|
|
||||||
- DOMAIN-SUFFIX,servebom.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,sfx.ms,$app_name
|
|
||||||
- DOMAIN-SUFFIX,shadowsocks.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,sharethis.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,shazam.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,skype.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,smartdns$app_name.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,smartmailcloud.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,sndcdn.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,sony.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,soundcloud.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,sourceforge.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,spotify.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,squarespace.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,sstatic.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,st.luluku.pw,$app_name
|
|
||||||
- DOMAIN-SUFFIX,stackoverflow.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,startpage.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,staticflickr.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,steamcommunity.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,symauth.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,symcb.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,symcd.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tapbots.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tapbots.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tdesktop.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,techcrunch.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,techsmith.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,thepiratebay.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,theverge.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,time.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,timeinc.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tiny.cc,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tinypic.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tmblr.co,$app_name
|
|
||||||
- DOMAIN-SUFFIX,todoist.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,trello.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,trustasiassl.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tumblr.co,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tumblr.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tweetdeck.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,tweetmarker.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,twitch.tv,$app_name
|
|
||||||
- DOMAIN-SUFFIX,txmblr.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,typekit.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ubertags.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ublock.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ubnt.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ulyssesapp.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,urchin.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,usertrust.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,v.gd,$app_name
|
|
||||||
- DOMAIN-SUFFIX,v2ex.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,vimeo.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,vimeocdn.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,vine.co,$app_name
|
|
||||||
- DOMAIN-SUFFIX,vivaldi.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,vox-cdn.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,vsco.co,$app_name
|
|
||||||
- DOMAIN-SUFFIX,vultr.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,w.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,w3schools.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,webtype.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wikiwand.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wikileaks.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wikimedia.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wikipedia.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wikipedia.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,windows.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,windows.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wire.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wordpress.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,workflowy.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wp.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wsj.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,wsj.net,$app_name
|
|
||||||
- DOMAIN-SUFFIX,xda-developers.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,xeeno.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,xiti.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,yahoo.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,yimg.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ying.com,$app_name
|
|
||||||
- DOMAIN-SUFFIX,yoyo.org,$app_name
|
|
||||||
- DOMAIN-SUFFIX,ytimg.com,$app_name
|
- DOMAIN-SUFFIX,ytimg.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,gstatic.com,$app_name
|
||||||
# Telegram
|
- DOMAIN-SUFFIX,ggpht.com,$app_name
|
||||||
- DOMAIN-SUFFIX,telegra.ph,$app_name
|
- DOMAIN-SUFFIX,googlevideo.com,$app_name
|
||||||
- DOMAIN-SUFFIX,telegram.org,$app_name
|
- DOMAIN-SUFFIX,v2ex.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,medium.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,reddit.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,redd.it,$app_name
|
||||||
|
- DOMAIN-SUFFIX,imgur.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,pixiv.net,$app_name
|
||||||
|
- DOMAIN-SUFFIX,nytimes.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,nyt.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,bbc.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,bbc.co.uk,$app_name
|
||||||
|
- DOMAIN-SUFFIX,steamcommunity.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,twitch.tv,$app_name
|
||||||
|
- DOMAIN-SUFFIX,vimeo.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,tumblr.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,linkedin.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,licdn.com,$app_name
|
||||||
|
- DOMAIN-SUFFIX,mega.nz,$app_name
|
||||||
|
- DOMAIN-SUFFIX,archive.org,$app_name
|
||||||
|
- DOMAIN-SUFFIX,wikimedia.org,$app_name
|
||||||
|
- DOMAIN-SUFFIX,soundcloud.com,$app_name
|
||||||
|
# Telegram IP
|
||||||
- IP-CIDR,91.108.4.0/22,$app_name,no-resolve
|
- IP-CIDR,91.108.4.0/22,$app_name,no-resolve
|
||||||
- IP-CIDR,91.108.8.0/21,$app_name,no-resolve
|
- IP-CIDR,91.108.8.0/21,$app_name,no-resolve
|
||||||
|
- IP-CIDR,91.108.12.0/22,$app_name,no-resolve
|
||||||
- IP-CIDR,91.108.16.0/22,$app_name,no-resolve
|
- IP-CIDR,91.108.16.0/22,$app_name,no-resolve
|
||||||
- IP-CIDR,91.108.56.0/22,$app_name,no-resolve
|
- IP-CIDR,91.108.56.0/22,$app_name,no-resolve
|
||||||
- IP-CIDR,149.154.160.0/20,$app_name,no-resolve
|
- IP-CIDR,149.154.160.0/20,$app_name,no-resolve
|
||||||
- IP-CIDR6,2001:67c:4e8::/48,$app_name,no-resolve
|
- IP-CIDR6,2001:67c:4e8::/48,$app_name,no-resolve
|
||||||
- IP-CIDR6,2001:b28:f23d::/48,$app_name,no-resolve
|
- IP-CIDR6,2001:b28:f23d::/48,$app_name,no-resolve
|
||||||
- IP-CIDR6,2001:b28:f23f::/48,$app_name,no-resolve
|
- IP-CIDR6,2001:b28:f23f::/48,$app_name,no-resolve
|
||||||
|
# Fallback
|
||||||
# Google 中国服务 services.googleapis.cn
|
|
||||||
- IP-CIDR,120.232.181.162/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,120.241.147.226/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,120.253.253.226/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,120.253.255.162/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,120.253.255.34/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,120.253.255.98/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,180.163.150.162/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,180.163.150.34/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,180.163.151.162/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,180.163.151.34/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,203.208.39.0/24,$app_name,no-resolve
|
|
||||||
- IP-CIDR,203.208.40.0/24,$app_name,no-resolve
|
|
||||||
- IP-CIDR,203.208.41.0/24,$app_name,no-resolve
|
|
||||||
- IP-CIDR,203.208.43.0/24,$app_name,no-resolve
|
|
||||||
- IP-CIDR,203.208.50.0/24,$app_name,no-resolve
|
|
||||||
- IP-CIDR,220.181.174.162/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,220.181.174.226/32,$app_name,no-resolve
|
|
||||||
- IP-CIDR,220.181.174.34/32,$app_name,no-resolve
|
|
||||||
|
|
||||||
# LAN
|
|
||||||
- DOMAIN,injections.adguard.org,DIRECT
|
|
||||||
- DOMAIN,local.adguard.org,DIRECT
|
|
||||||
- DOMAIN-SUFFIX,local,DIRECT
|
|
||||||
- IP-CIDR,127.0.0.0/8,DIRECT
|
|
||||||
- IP-CIDR,172.16.0.0/12,DIRECT
|
|
||||||
- IP-CIDR,192.168.0.0/16,DIRECT
|
|
||||||
- IP-CIDR,10.0.0.0/8,DIRECT
|
|
||||||
- IP-CIDR,17.0.0.0/8,DIRECT
|
|
||||||
- IP-CIDR,100.64.0.0/10,DIRECT
|
|
||||||
- IP-CIDR,224.0.0.0/4,DIRECT
|
|
||||||
- IP-CIDR6,fe80::/10,DIRECT
|
|
||||||
|
|
||||||
# 剩余未匹配的国内网站
|
|
||||||
- DOMAIN-SUFFIX,cn,DIRECT
|
|
||||||
- DOMAIN-KEYWORD,-cn,DIRECT
|
|
||||||
|
|
||||||
# 最终规则
|
|
||||||
- GEOIP,CN,DIRECT
|
- GEOIP,CN,DIRECT
|
||||||
- MATCH,$app_name
|
- MATCH,$app_name
|
||||||
|
|||||||
@@ -14,12 +14,69 @@
|
|||||||
secure_path: "{{ $secure_path }}",
|
secure_path: "{{ $secure_path }}",
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/admin/assets/index.js"></script>
|
@php
|
||||||
<link rel="stylesheet" crossorigin href="/assets/admin/assets/index.css" />
|
$manifestPath = public_path('assets/admin/manifest.json');
|
||||||
<link rel="stylesheet" crossorigin href="/assets/admin/assets/vendor.css">
|
$manifest = file_exists($manifestPath) ? json_decode(file_get_contents($manifestPath), true) : null;
|
||||||
<script src="/assets/admin/locales/en-US.js"></script>
|
$entry = is_array($manifest) ? ($manifest['index.html'] ?? null) : null;
|
||||||
<script src="/assets/admin/locales/zh-CN.js"></script>
|
$scripts = [];
|
||||||
<script src="/assets/admin/locales/ko-KR.js"></script>
|
$styles = [];
|
||||||
|
$locales = [];
|
||||||
|
|
||||||
|
if (is_array($entry)) {
|
||||||
|
$visited = [];
|
||||||
|
$collectAssets = function ($chunkName) use (&$collectAssets, &$manifest, &$visited, &$scripts, &$styles) {
|
||||||
|
if (isset($visited[$chunkName]) || !isset($manifest[$chunkName]) || !is_array($manifest[$chunkName])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$visited[$chunkName] = true;
|
||||||
|
$chunk = $manifest[$chunkName];
|
||||||
|
|
||||||
|
if (!empty($chunk['css']) && is_array($chunk['css'])) {
|
||||||
|
foreach ($chunk['css'] as $cssFile) {
|
||||||
|
$styles[$cssFile] = $cssFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($chunk['imports']) && is_array($chunk['imports'])) {
|
||||||
|
foreach ($chunk['imports'] as $import) {
|
||||||
|
$collectAssets($import);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($chunk['isEntry']) && !empty($chunk['file'])) {
|
||||||
|
$scripts[$chunk['file']] = $chunk['file'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$collectAssets('index.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob(public_path('assets/admin/locales/*.js')) ?: [] as $localeFile) {
|
||||||
|
$locales[] = 'locales/' . basename($localeFile);
|
||||||
|
}
|
||||||
|
sort($locales);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($entry && count($scripts) > 0)
|
||||||
|
@foreach($styles as $css)
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/admin/{{ $css }}" />
|
||||||
|
@endforeach
|
||||||
|
@foreach($locales as $locale)
|
||||||
|
<script src="/assets/admin/{{ $locale }}"></script>
|
||||||
|
@endforeach
|
||||||
|
@foreach($scripts as $js)
|
||||||
|
<script type="module" crossorigin src="/assets/admin/{{ $js }}"></script>
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
{{-- Fallback: hardcoded paths for backward compatibility --}}
|
||||||
|
<script type="module" crossorigin src="/assets/admin/assets/index.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/admin/assets/index.css" />
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/admin/assets/vendor.css">
|
||||||
|
<script src="/assets/admin/locales/en-US.js"></script>
|
||||||
|
<script src="/assets/admin/locales/zh-CN.js"></script>
|
||||||
|
<script src="/assets/admin/locales/ko-KR.js"></script>
|
||||||
|
@endif
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user