Files
Xboard/app/Protocols/QuantumultX.php
Dlphine 5dd4cd4bc9 fix: replace raw array access with data_get() to prevent Undefined array key
- Migrate $protocol_settings['key'] to data_get($protocol_settings, 'key') across General, SingBox, Shadowrocket, Surfboard, QuantumultX
- Prevents PHP 8 Undefined array key fatal errors when optional protocol_settings fields are missing
- Same class of bug that caused #735
2026-03-27 13:51:28 +08:00

233 lines
8.5 KiB
PHP

<?php
namespace App\Protocols;
use App\Utils\Helper;
use App\Support\AbstractProtocol;
use App\Models\Server;
class QuantumultX extends AbstractProtocol
{
public $flags = ['quantumult%20x', 'quantumult-x'];
public $allowedProtocols = [
Server::TYPE_SHADOWSOCKS,
Server::TYPE_VMESS,
Server::TYPE_VLESS,
Server::TYPE_TROJAN,
Server::TYPE_SOCKS,
Server::TYPE_HTTP,
];
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
$uri .= match ($item['type']) {
Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item),
Server::TYPE_VMESS => self::buildVmess($item['password'], $item),
Server::TYPE_VLESS => self::buildVless($item['password'], $item),
Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item),
Server::TYPE_SOCKS => self::buildSocks5($item['password'], $item),
Server::TYPE_HTTP => self::buildHttp($item['password'], $item),
default => ''
};
}
return response(base64_encode($uri))
->header('content-type', 'text/plain')
->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
}
public static function buildShadowsocks($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$password = data_get($server, 'password', $password);
$addr = Helper::wrapIPv6($server['host']);
$config = [
"shadowsocks={$addr}:{$server['port']}",
"method=" . data_get($protocol_settings, 'cipher'),
"password={$password}",
];
if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) {
$plugin = data_get($protocol_settings, 'plugin');
$pluginOpts = data_get($protocol_settings, 'plugin_opts', '');
$parsedOpts = collect(explode(';', $pluginOpts))
->filter()
->mapWithKeys(function ($pair) {
if (!str_contains($pair, '=')) {
return [];
}
[$key, $value] = explode('=', $pair, 2);
return [trim($key) => trim($value)];
})
->all();
if ($plugin === 'obfs') {
if (isset($parsedOpts['obfs'])) {
$config[] = "obfs={$parsedOpts['obfs']}";
}
if (isset($parsedOpts['obfs-host'])) {
$config[] = "obfs-host={$parsedOpts['obfs-host']}";
}
if (isset($parsedOpts['path'])) {
$config[] = "obfs-uri={$parsedOpts['path']}";
}
}
}
self::applyCommonSettings($config, $server);
return implode(',', array_filter($config)) . "\r\n";
}
public static function buildVmess($uuid, $server)
{
$protocol_settings = $server['protocol_settings'];
$addr = Helper::wrapIPv6($server['host']);
$config = [
"vmess={$addr}:{$server['port']}",
"method=" . data_get($protocol_settings, 'cipher', 'auto'),
"password={$uuid}",
];
self::applyTransportSettings($config, $protocol_settings);
self::applyCommonSettings($config, $server);
return implode(',', array_filter($config)) . "\r\n";
}
public static function buildVless($uuid, $server)
{
$protocol_settings = $server['protocol_settings'];
$addr = Helper::wrapIPv6($server['host']);
$config = [
"vless={$addr}:{$server['port']}",
'method=none',
"password={$uuid}",
];
self::applyTransportSettings($config, $protocol_settings);
if ($flow = data_get($protocol_settings, 'flow')) {
$config[] = "vless-flow={$flow}";
}
self::applyCommonSettings($config, $server);
return implode(',', array_filter($config)) . "\r\n";
}
private static function applyTransportSettings(&$config, $settings, bool $nativeTls = false, ?array $tlsData = null)
{
$tlsMode = (int) data_get($settings, 'tls', 0);
$network = data_get($settings, 'network', 'tcp');
$host = null;
$isWs = $network === 'ws';
switch ($network) {
case 'ws':
$config[] = $tlsMode ? 'obfs=wss' : 'obfs=ws';
if ($path = data_get($settings, 'network_settings.path')) {
$config[] = "obfs-uri={$path}";
}
$host = data_get($settings, 'network_settings.headers.Host');
break;
case 'tcp':
$headerType = data_get($settings, 'network_settings.header.type', 'tcp');
if ($headerType === 'http') {
$config[] = 'obfs=http';
$paths = data_get($settings, 'network_settings.header.request.path', ['/']);
$config[] = 'obfs-uri=' . (is_array($paths) ? ($paths[0] ?? '/') : $paths);
$hostVal = data_get($settings, 'network_settings.header.request.headers.Host');
$host = is_array($hostVal) ? ($hostVal[0] ?? null) : $hostVal;
} elseif ($tlsMode) {
$config[] = $nativeTls ? 'over-tls=true' : 'obfs=over-tls';
}
break;
}
switch ($tlsMode) {
case 2: // Reality
$host = $host ?? data_get($settings, 'reality_settings.server_name');
if ($pubKey = data_get($settings, 'reality_settings.public_key')) {
$config[] = "reality-base64-pubkey={$pubKey}";
}
if ($shortId = data_get($settings, 'reality_settings.short_id')) {
$config[] = "reality-hex-shortid={$shortId}";
}
break;
case 1: // TLS
$resolved = $tlsData ?? (array) data_get($settings, 'tls_settings', []);
$allowInsecure = (bool) ($resolved['allow_insecure'] ?? false);
$config[] = 'tls-verification=' . ($allowInsecure ? 'false' : 'true');
$host = $host ?? ($resolved['server_name'] ?? null);
break;
}
if ($host) {
$config[] = ($nativeTls && !$isWs) ? "tls-host={$host}" : "obfs-host={$host}";
}
}
private static function applyCommonSettings(&$config, $server)
{
$config[] = 'fast-open=true';
if ($server['type'] !== Server::TYPE_HTTP) {
$config[] = 'udp-relay=true';
}
$config[] = "tag={$server['name']}";
}
public static function buildTrojan($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$addr = Helper::wrapIPv6($server['host']);
$config = [
"trojan={$addr}:{$server['port']}",
"password={$password}",
];
$tlsData = [
'allow_insecure' => data_get($protocol_settings, 'allow_insecure', false),
'server_name' => data_get($protocol_settings, 'server_name'),
];
self::applyTransportSettings($config, $protocol_settings, true, $tlsData);
self::applyCommonSettings($config, $server);
return implode(',', array_filter($config)) . "\r\n";
}
public static function buildSocks5($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$addr = Helper::wrapIPv6($server['host']);
$config = [
"socks5={$addr}:{$server['port']}",
"username={$password}",
"password={$password}",
];
self::applyTransportSettings($config, $protocol_settings, true);
self::applyCommonSettings($config, $server);
return implode(',', array_filter($config)) . "\r\n";
}
public static function buildHttp($password, $server)
{
$protocol_settings = $server['protocol_settings'];
$addr = Helper::wrapIPv6($server['host']);
$config = [
"http={$addr}:{$server['port']}",
"username={$password}",
"password={$password}",
];
self::applyTransportSettings($config, $protocol_settings, true);
self::applyCommonSettings($config, $server);
return implode(',', array_filter($config)) . "\r\n";
}
}