2023-11-17 14:44:01 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\V1\Client;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
2025-05-22 17:58:22 +08:00
|
|
|
|
use App\Models\Server;
|
2023-11-17 14:44:01 +08:00
|
|
|
|
use App\Protocols\General;
|
2025-01-26 02:31:57 +08:00
|
|
|
|
use App\Services\Plugin\HookManager;
|
2023-11-17 14:44:01 +08:00
|
|
|
|
use App\Services\ServerService;
|
|
|
|
|
|
use App\Services\UserService;
|
|
|
|
|
|
use App\Utils\Helper;
|
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
|
|
|
|
|
|
|
class ClientController extends Controller
|
|
|
|
|
|
{
|
2025-01-21 14:57:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Protocol prefix mapping for server names
|
|
|
|
|
|
*/
|
|
|
|
|
|
private const PROTOCOL_PREFIXES = [
|
|
|
|
|
|
'hysteria' => [
|
|
|
|
|
|
1 => '[Hy]',
|
|
|
|
|
|
2 => '[Hy2]'
|
|
|
|
|
|
],
|
|
|
|
|
|
'vless' => '[vless]',
|
|
|
|
|
|
'shadowsocks' => '[ss]',
|
|
|
|
|
|
'vmess' => '[vmess]',
|
|
|
|
|
|
'trojan' => '[trojan]',
|
2025-02-23 00:13:04 +08:00
|
|
|
|
'tuic' => '[tuic]',
|
2025-02-25 19:14:19 +08:00
|
|
|
|
'socks' => '[socks]',
|
2025-05-19 09:25:52 +08:00
|
|
|
|
'anytls' => '[anytls]'
|
2025-01-21 14:57:54 +08:00
|
|
|
|
];
|
2023-11-17 14:44:01 +08:00
|
|
|
|
|
2025-01-21 14:57:54 +08:00
|
|
|
|
|
|
|
|
|
|
public function subscribe(Request $request)
|
|
|
|
|
|
{
|
2025-01-26 02:31:57 +08:00
|
|
|
|
HookManager::call('client.subscribe.before');
|
2025-01-21 14:57:54 +08:00
|
|
|
|
$request->validate([
|
|
|
|
|
|
'types' => ['nullable', 'string'],
|
|
|
|
|
|
'filter' => ['nullable', 'string'],
|
|
|
|
|
|
'flag' => ['nullable', 'string'],
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$user = $request->user();
|
|
|
|
|
|
$userService = new UserService();
|
|
|
|
|
|
|
|
|
|
|
|
if (!$userService->isAvailable($user)) {
|
2025-07-16 00:08:30 +08:00
|
|
|
|
HookManager::call('client.subscribe.unavailable');
|
2025-07-22 01:33:59 +08:00
|
|
|
|
return response('', 403, ['Content-Type' => 'text/plain']);
|
2025-07-16 00:08:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $this->doSubscribe($request, $user);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function doSubscribe(Request $request, $user, $servers = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($servers === null) {
|
|
|
|
|
|
$servers = ServerService::getAvailableServers($user);
|
|
|
|
|
|
$servers = HookManager::filter('client.subscribe.servers', $servers, $user, $request);
|
2025-01-21 14:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$clientInfo = $this->getClientInfo($request);
|
2025-05-22 17:58:22 +08:00
|
|
|
|
|
|
|
|
|
|
$requestedTypes = $this->parseRequestedTypes($request->input('types'));
|
|
|
|
|
|
$filterKeywords = $this->parseFilterKeywords($request->input('filter'));
|
|
|
|
|
|
|
|
|
|
|
|
$protocolClassName = app('protocols.manager')->matchProtocolClassName($clientInfo['flag'])
|
|
|
|
|
|
?? General::class;
|
|
|
|
|
|
|
2025-01-21 14:57:54 +08:00
|
|
|
|
$serversFiltered = $this->filterServers(
|
|
|
|
|
|
servers: $servers,
|
2025-05-22 17:58:22 +08:00
|
|
|
|
allowedTypes: $requestedTypes,
|
|
|
|
|
|
filterKeywords: $filterKeywords
|
2025-01-21 14:57:54 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
|
|
|
|
|
|
$serversFiltered = $this->addPrefixToServerName($serversFiltered);
|
|
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
// Instantiate the protocol class with filtered servers and client info
|
|
|
|
|
|
$protocolInstance = app()->make($protocolClassName, [
|
|
|
|
|
|
'user' => $user,
|
|
|
|
|
|
'servers' => $serversFiltered,
|
|
|
|
|
|
'clientName' => $clientInfo['name'] ?? null,
|
2026-03-23 14:56:41 +08:00
|
|
|
|
'clientVersion' => $clientInfo['version'] ?? null,
|
|
|
|
|
|
'userAgent' => $clientInfo['flag'] ?? null
|
2025-05-22 17:58:22 +08:00
|
|
|
|
]);
|
2024-07-19 06:29:35 +08:00
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
return $protocolInstance->handle();
|
2025-01-21 14:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Parses the input string for requested server types.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function parseRequestedTypes(?string $typeInputString): array
|
2025-01-21 14:57:54 +08:00
|
|
|
|
{
|
2025-07-07 03:14:21 +08:00
|
|
|
|
if (blank($typeInputString) || $typeInputString === 'all') {
|
2025-05-22 17:58:22 +08:00
|
|
|
|
return Server::VALID_TYPES;
|
2025-01-21 14:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
$requested = collect(preg_split('/[|,|]+/', $typeInputString))
|
|
|
|
|
|
->map(fn($type) => trim($type))
|
|
|
|
|
|
->filter() // Remove empty strings that might result from multiple delimiters
|
|
|
|
|
|
->all();
|
2025-01-21 14:57:54 +08:00
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
return array_values(array_intersect($requested, Server::VALID_TYPES));
|
2025-01-21 14:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Parses the input string for filter keywords.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function parseFilterKeywords(?string $filterInputString): ?array
|
2025-01-21 14:57:54 +08:00
|
|
|
|
{
|
2025-05-22 17:58:22 +08:00
|
|
|
|
if (blank($filterInputString) || mb_strlen($filterInputString) > 20) {
|
2025-01-21 14:57:54 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2025-05-22 17:58:22 +08:00
|
|
|
|
|
|
|
|
|
|
return collect(preg_split('/[|,|]+/', $filterInputString))
|
|
|
|
|
|
->map(fn($keyword) => trim($keyword))
|
|
|
|
|
|
->filter() // Remove empty strings
|
|
|
|
|
|
->all();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Filters servers based on allowed types and keywords.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function filterServers(array $servers, array $allowedTypes, ?array $filterKeywords): array
|
|
|
|
|
|
{
|
|
|
|
|
|
return collect($servers)->filter(function ($server) use ($allowedTypes, $filterKeywords) {
|
|
|
|
|
|
// Condition 1: Server type must be in the list of allowed types
|
2025-07-22 01:33:59 +08:00
|
|
|
|
if ($allowedTypes && !in_array($server['type'], $allowedTypes)) {
|
2025-05-22 17:58:22 +08:00
|
|
|
|
return false; // Filter out (don't keep)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Condition 2: If filterKeywords are provided, at least one keyword must match
|
|
|
|
|
|
if (!empty($filterKeywords)) { // Check if $filterKeywords is not empty
|
|
|
|
|
|
$keywordMatch = collect($filterKeywords)->contains(function ($keyword) use ($server) {
|
|
|
|
|
|
return stripos($server['name'], $keyword) !== false
|
|
|
|
|
|
|| in_array($keyword, $server['tags'] ?? []);
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!$keywordMatch) {
|
|
|
|
|
|
return false; // Filter out if no keywords match
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Keep the server if its type is allowed AND (no filter keywords OR at least one keyword matched)
|
|
|
|
|
|
return true;
|
|
|
|
|
|
})->values()->all();
|
2025-01-21 14:57:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function getClientInfo(Request $request): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
|
|
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
$clientName = null;
|
|
|
|
|
|
$clientVersion = null;
|
2025-01-21 14:57:54 +08:00
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
if (preg_match('/([a-zA-Z0-9\-_]+)[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/', $flag, $matches)) {
|
|
|
|
|
|
$potentialName = strtolower($matches[1]);
|
|
|
|
|
|
$clientVersion = preg_replace('/^v/', '', $matches[2]);
|
2025-01-21 14:57:54 +08:00
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
if (in_array($potentialName, app('protocols.flags'))) {
|
|
|
|
|
|
$clientName = $potentialName;
|
2024-07-19 06:29:35 +08:00
|
|
|
|
}
|
2025-01-21 14:57:54 +08:00
|
|
|
|
}
|
2024-07-19 06:29:35 +08:00
|
|
|
|
|
2025-05-22 17:58:22 +08:00
|
|
|
|
if (!$clientName) {
|
|
|
|
|
|
$flags = collect(app('protocols.flags'))->sortByDesc(fn($f) => strlen($f))->values()->all();
|
|
|
|
|
|
foreach ($flags as $name) {
|
|
|
|
|
|
if (stripos($flag, $name) !== false) {
|
|
|
|
|
|
$clientName = $name;
|
|
|
|
|
|
if (!$clientVersion) {
|
|
|
|
|
|
$pattern = '/' . preg_quote($name, '/') . '[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/i';
|
|
|
|
|
|
if (preg_match($pattern, $flag, $vMatches)) {
|
|
|
|
|
|
$clientVersion = preg_replace('/^v/', '', $vMatches[1]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2024-07-19 06:29:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-22 17:58:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!$clientVersion) {
|
|
|
|
|
|
if (preg_match('/\/v?(\d+(?:\.\d+){0,2})/', $flag, $matches)) {
|
|
|
|
|
|
$clientVersion = $matches[1];
|
2025-01-21 14:57:54 +08:00
|
|
|
|
}
|
2025-05-22 17:58:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'flag' => $flag,
|
|
|
|
|
|
'name' => $clientName,
|
|
|
|
|
|
'version' => $clientVersion
|
|
|
|
|
|
];
|
2024-07-19 06:29:35 +08:00
|
|
|
|
}
|
2023-11-17 14:44:01 +08:00
|
|
|
|
|
|
|
|
|
|
private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0)
|
|
|
|
|
|
{
|
2024-07-19 06:29:35 +08:00
|
|
|
|
if (!isset($servers[0]))
|
|
|
|
|
|
return;
|
|
|
|
|
|
if ($rejectServerCount > 0) {
|
2023-11-17 14:44:01 +08:00
|
|
|
|
array_unshift($servers, array_merge($servers[0], [
|
2024-07-19 06:29:35 +08:00
|
|
|
|
'name' => "过滤掉{$rejectServerCount}条线路",
|
2023-11-17 14:44:01 +08:00
|
|
|
|
]));
|
|
|
|
|
|
}
|
2024-07-19 06:29:35 +08:00
|
|
|
|
if (!(int) admin_setting('show_info_to_server_enable', 0))
|
|
|
|
|
|
return;
|
2023-11-17 14:44:01 +08:00
|
|
|
|
$useTraffic = $user['u'] + $user['d'];
|
|
|
|
|
|
$totalTraffic = $user['transfer_enable'];
|
|
|
|
|
|
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
|
2025-01-21 14:57:54 +08:00
|
|
|
|
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('长期有效');
|
2023-11-17 14:44:01 +08:00
|
|
|
|
$userService = new UserService();
|
|
|
|
|
|
$resetDay = $userService->getResetDay($user);
|
|
|
|
|
|
array_unshift($servers, array_merge($servers[0], [
|
|
|
|
|
|
'name' => "套餐到期:{$expiredDate}",
|
|
|
|
|
|
]));
|
|
|
|
|
|
if ($resetDay) {
|
|
|
|
|
|
array_unshift($servers, array_merge($servers[0], [
|
|
|
|
|
|
'name' => "距离下次重置剩余:{$resetDay} 天",
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
array_unshift($servers, array_merge($servers[0], [
|
|
|
|
|
|
'name' => "剩余流量:{$remainingTraffic}",
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-01-21 14:57:54 +08:00
|
|
|
|
private function addPrefixToServerName(array $servers): array
|
2024-07-19 06:29:35 +08:00
|
|
|
|
{
|
2025-01-21 14:57:54 +08:00
|
|
|
|
if (!admin_setting('show_protocol_to_server_enable', false)) {
|
|
|
|
|
|
return $servers;
|
2023-11-17 14:44:01 +08:00
|
|
|
|
}
|
2025-01-21 14:57:54 +08:00
|
|
|
|
return collect($servers)
|
|
|
|
|
|
->map(function (array $server): array {
|
|
|
|
|
|
$server['name'] = $this->getPrefixedServerName($server);
|
|
|
|
|
|
return $server;
|
|
|
|
|
|
})
|
|
|
|
|
|
->all();
|
|
|
|
|
|
}
|
2025-05-22 17:58:22 +08:00
|
|
|
|
|
2025-01-21 14:57:54 +08:00
|
|
|
|
private function getPrefixedServerName(array $server): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$type = $server['type'] ?? '';
|
|
|
|
|
|
if (!isset(self::PROTOCOL_PREFIXES[$type])) {
|
|
|
|
|
|
return $server['name'] ?? '';
|
2023-11-17 14:44:01 +08:00
|
|
|
|
}
|
2025-01-21 14:57:54 +08:00
|
|
|
|
$prefix = is_array(self::PROTOCOL_PREFIXES[$type])
|
|
|
|
|
|
? self::PROTOCOL_PREFIXES[$type][$server['protocol_settings']['version'] ?? 1] ?? ''
|
|
|
|
|
|
: self::PROTOCOL_PREFIXES[$type];
|
|
|
|
|
|
return $prefix . ($server['name'] ?? '');
|
2023-11-17 14:44:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|