Sfoglia il codice sorgente

Multi-Clients Makeover

BrettonYe 1 anno fa
parent
commit
7960ff285c

+ 0 - 265
app/Helpers/ClientConfig.php

@@ -1,265 +0,0 @@
-<?php
-
-namespace App\Helpers;
-
-use App\Utils\Clients\Clash;
-use App\Utils\Clients\QuantumultX;
-use App\Utils\Clients\Surge;
-use App\Utils\Clients\URLSchemes;
-use File;
-use Symfony\Component\Yaml\Yaml;
-
-trait ClientConfig
-{
-    private function clientConfig(string $target): string
-    {
-        if (str_contains($target, 'quantumult%20x')) {
-            return $this->quantumultX();
-        }
-        if (str_contains($target, 'quantumult')) {
-            return $this->quantumult();
-        }
-        if (str_contains($target, 'clash') || str_contains($target, 'stash')) {
-            return $this->clash();
-        }
-        if (str_contains($target, 'bob_vpn')) {
-            return $this->clash('bob');
-        }
-        if (str_contains($target, 'surfboard')) {
-            return $this->surfboard();
-        }
-        if (str_contains($target, 'surge')) {
-            return $this->surge($target);
-        }
-        if (str_contains($target, 'shadowrocket')) {
-            return $this->shadowrocket();
-        }
-        if (str_contains($target, 'v2rayn') || str_contains($target, 'v2rayng') || str_contains($target, 'v2rayu')) {
-            return $this->v2rayN();
-        }
-
-        //        if (str_contains($target, 'shadowsocks')) {
-        //            exit($this->shaodowsocksSIP008());
-        //        }
-        return $this->origin();
-    }
-
-    private function quantumultX(): string
-    {
-        $user = $this->getUser();
-        $uri = '';
-        if (sysConfig('is_custom_subscribe')) {
-            header("subscription-userinfo: upload=$user->u; download=$user->d; total=$user->transfer_enable; expire=".strtotime($user->expired_at));
-        }
-        foreach ($this->getServers() as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $uri .= QuantumultX::buildShadowsocks($server);
-            }
-            if ($server['type'] === 'shadowsocksr') {
-                $uri .= QuantumultX::buildShadowsocksr($server);
-            }
-            if ($server['type'] === 'v2ray') {
-                $uri .= QuantumultX::buildVmess($server);
-            }
-            if ($server['type'] === 'trojan') {
-                $uri .= QuantumultX::buildTrojan($server);
-            }
-        }
-
-        return base64_encode($uri);
-    }
-
-    private function quantumult(): string
-    {
-        $user = $this->getUser();
-        if (sysConfig('is_custom_subscribe')) {
-            header("subscription-userinfo: upload=$user->u; download=$user->d; total=$user->transfer_enable; expire=".strtotime($user->expired_at));
-        }
-
-        return $this->origin();
-    }
-
-    private function origin(bool $encode = true): string
-    {
-        $uri = '';
-        foreach ($this->getServers() as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $uri .= URLSchemes::buildShadowsocks($server);
-            }
-            if ($server['type'] === 'shadowsocksr') {
-                $uri .= URLSchemes::buildShadowsocksr($server);
-            }
-            if ($server['type'] === 'v2ray') {
-                $uri .= URLSchemes::buildVmess($server);
-            }
-            if ($server['type'] === 'trojan') {
-                $uri .= URLSchemes::buildTrojan($server);
-            }
-        }
-
-        return $encode ? base64_encode($uri) : $uri;
-    }
-
-    private function clash(?string $client = null): string
-    {
-        $user = $this->getUser();
-        $webName = sysConfig('website_name');
-        header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($webName).'.yaml');
-        header('profile-update-interval: 24');
-        header('profile-web-page-url:'.sysConfig('website_url'));
-        if (sysConfig('is_custom_subscribe')) {
-            header("subscription-userinfo: upload=$user->u; download=$user->d; total=$user->transfer_enable; expire=".strtotime($user->expired_at));
-        }
-        $custom_path = '/resources/rules/custom.clash.yaml';
-        if ($client === 'bob') {
-            $file_path = '/resources/rules/bob.clash.yaml';
-        } elseif (File::exists(base_path().$custom_path)) {
-            $file_path = $custom_path;
-        } else {
-            $file_path = '/resources/rules/default.clash.yaml';
-        }
-
-        $config = Yaml::parseFile(base_path().$file_path);
-
-        foreach ($this->getServers() as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $proxy[] = Clash::buildShadowsocks($server);
-                $proxies[] = $server['name'];
-            }
-            if ($server['type'] === 'shadowsocksr') {
-                $proxy[] = Clash::buildShadowsocksr($server);
-                $proxies[] = $server['name'];
-            }
-            if ($server['type'] === 'v2ray') {
-                $proxy[] = Clash::buildVmess($server);
-                $proxies[] = $server['name'];
-            }
-            if ($server['type'] === 'trojan') {
-                $proxy[] = Clash::buildTrojan($server);
-                $proxies[] = $server['name'];
-            }
-        }
-
-        $config['proxies'] = array_merge($config['proxies'] ?: [], $proxy ?? []);
-        foreach ($config['proxy-groups'] as $k => $v) {
-            if (! is_array($config['proxy-groups'][$k]['proxies'])) {
-                continue;
-            }
-            $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies ?? []);
-        }
-
-        array_unshift($config['rules'], 'DOMAIN,'.$_SERVER['HTTP_HOST'].',DIRECT'); // Set current sub-domain to be direct
-
-        return str_replace('$app_name', $webName, Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE));
-    }
-
-    private function surfboard(): string
-    {
-        $defaultConfig = base_path().'/resources/rules/default.surfboard.conf';
-        $customConfig = base_path().'/resources/rules/custom.surfboard.conf';
-
-        return $this->sugerLike($customConfig, $defaultConfig);
-    }
-
-    private function sugerLike(string $customConfig, string $defaultConfig, string $target = ''): string
-    {
-        if (File::exists($customConfig)) {
-            $config = file_get_contents($customConfig);
-        } else {
-            $config = file_get_contents($defaultConfig);
-        }
-
-        $proxies = '';
-        $proxyGroup = '';
-        $user = $this->getUser();
-        $webName = sysConfig('website_name');
-        header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($webName).'.conf');
-        foreach ($this->getServers() as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $proxies .= Surge::buildShadowsocks($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-            if ($server['type'] === 'v2ray') {
-                $proxies .= Surge::buildVmess($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-            if ($server['type'] === 'trojan') {
-                $proxies .= Surge::buildTrojan($server);
-                $proxyGroup .= $server['name'].', ';
-            }
-        }
-
-        if (str_contains($target, 'list')) {
-            return $proxies;
-        }
-
-        if (sysConfig('is_custom_subscribe')) {
-            $upload = formatBytes($user->u);
-            $download = formatBytes($user->d);
-            $totalTraffic = formatBytes($user->transfer_enable);
-            $subscribeInfo = "title=$webName".trans('user.subscribe.info.title').', content='.trans('user.subscribe.info.upload').": $upload\\n".trans('user.subscribe.info.download').": $download\\n".trans('user.subscribe.info.total').": $totalTraffic\\n".trans('model.user.expired_date').": $user->expired_at";
-            $config = str_replace('$subscribe_info', $subscribeInfo, $config);
-        }
-
-        return str_replace(['$subs_link', '$subs_domain', '$proxies', '$proxy_group'],
-            [route('sub', $user->subscribe->code), $_SERVER['HTTP_HOST'], $proxies, rtrim($proxyGroup, ', ')],
-            $config);
-    }
-
-    private function surge(string $target): string
-    {
-        $defaultConfig = base_path().'/resources/rules/default.surge.conf';
-        $customConfig = base_path().'/resources/rules/custom.surge.conf';
-
-        return $this->sugerLike($customConfig, $defaultConfig, $target);
-    }
-
-    private function shadowrocket(): string
-    {
-        //display remaining traffic and expire date
-        $uri = '';
-        $user = $this->getUser();
-        if (sysConfig('is_custom_subscribe')) {
-            $upload = formatBytes($user->u);
-            $download = formatBytes($user->d);
-            $totalTraffic = formatBytes($user->transfer_enable);
-            $uri = "STATUS=📤:{$upload}📥:{$download}⏳:{$totalTraffic}📅:$user->expiration_date\r\n";
-        }
-        $uri .= $this->origin(false);
-
-        return base64_encode($uri);
-    }
-
-    private function v2rayN(): string
-    {
-        $uri = '';
-        $user = $this->getUser();
-        if (sysConfig('is_custom_subscribe')) {
-            $text = '';
-            if ($user->expiration_date > date('Y-m-d')) {
-                if ($user->transfer_enable === 0) {
-                    $text .= trans('user.account.remain').': 0';
-                } else {
-                    $text .= trans('user.account.remain').': '.formatBytes($user->transfer_enable);
-                }
-                $text .= ', '.trans('model.user.expired_date').": $user->expiration_date";
-            } else {
-                $text .= trans('user.account.reason.expired');
-            }
-            $uri .= $this->failedProxyReturn($text, 2);
-        }
-
-        return base64_encode($uri.$this->origin(false));
-    }
-
-    private function shaodowsocksSIP008(): string
-    {
-        foreach ($this->getServers() as $server) {
-            if ($server['type'] === 'shadowsocks') {
-                $configs[] = URLSchemes::buildShadowsocksSIP008($server);
-            }
-        }
-
-        return json_encode(['version' => 1, 'remark' => sysConfig('website_name'), 'servers' => $configs ?? []], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
-    }
-}

+ 23 - 9
app/Services/ProxyService.php

@@ -2,17 +2,15 @@
 
 namespace App\Services;
 
-use App\Helpers\ClientConfig;
 use App\Models\Node;
 use App\Models\User;
-use App\Utils\Clients\Text;
-use App\Utils\Clients\URLSchemes;
+use App\Utils\Clients\Protocols\Text;
+use App\Utils\Clients\Protocols\URLSchemes;
 use Arr;
+use ReflectionClass;
 
 class ProxyService
 {
-    use ClientConfig;
-
     private static ?User $user;
 
     private static array $servers;
@@ -55,7 +53,7 @@ class ProxyService
 
         $this->setServers($servers);
 
-        return $this->clientConfig($target);
+        return $this->getClientConfig($target);
     }
 
     public function getNodeList(?int $type = null, bool $isConfig = true): array
@@ -118,7 +116,7 @@ class ProxyService
                     break;
                 case 2:
                     $config = array_merge($config, [
-                        'type' => 'v2ray',
+                        'type' => 'vmess',
                         'port' => $node->port,
                         'uuid' => $user->vmess_id,
                     ], $node->profile);
@@ -180,7 +178,7 @@ class ProxyService
 
         $this->setServers($servers);
 
-        return $this->clientConfig($target);
+        return $this->getClientConfig($target);
     }
 
     public function getUserProxyConfig(array $server, bool $is_url): string
@@ -190,8 +188,24 @@ class ProxyService
         return match ($server['type']) {
             'shadowsocks' => $type->buildShadowsocks($server),
             'shadowsocksr' => $type->buildShadowsocksr($server),
-            'v2ray' => $type->buildVmess($server),
+            'vmess' => $type->buildVmess($server),
             'trojan' => $type->buildTrojan($server),
         };
     }
+
+    private function getClientConfig(string $target): string
+    {
+        foreach (glob(app_path('Utils/Clients').'/*.php') as $file) {
+            $class = 'App\\Utils\\Clients\\'.basename($file, '.php');
+            $reflectionClass = new ReflectionClass($class);
+
+            foreach ($reflectionClass->getConstant('AGENT') as $agent) {
+                if (str_contains($target, $agent)) {
+                    return (new $class())->getConfig($this->getServers(), $this->getUser(), $target);
+                }
+            }
+        }
+
+        return URLSchemes::build($this->getServers()); // Origin
+    }
 }

+ 37 - 78
app/Utils/Clients/Clash.php

@@ -1,98 +1,57 @@
 <?php
-
-namespace App\Utils\Clients;
-
 /*
- * 本文件依据
- * https://github.com/Dreamacro/clash/tree/master/adapter/outbound
- * https://github.com/Dreamacro/clash/wiki/Configuration#all-configuration-options
- * https://lancellc.gitbook.io/clash/clash-config-file/proxies/config-a-shadowsocks-proxy
- *
+ * Developed based on
+ * https://clash.wiki/configuration/configuration-reference.html
+ * https://docs.gtk.pw/contents/urlscheme.html#%E4%B8%8B%E8%BD%BD%E9%85%8D%E7%BD%AE
+ * https://opensource.clash.wiki/Dreamacro/clash/
+ * https://stash.wiki/get-started
  */
 
+namespace App\Utils\Clients;
+
+use App\Models\User;
 use App\Utils\Library\Templates\Client;
+use File;
+use Symfony\Component\Yaml\Yaml;
 
 class Clash implements Client
 {
-    public static function buildShadowsocks(array $server): array
-    {
-        return [
-            'name' => $server['name'],
-            'type' => 'ss',
-            'server' => $server['host'],
-            'port' => $server['port'],
-            'password' => $server['passwd'],
-            'cipher' => $server['method'],
-            'udp' => $server['udp'],
-        ];
-    }
+    public const AGENT = ['clash', 'stash', 'bob_vpn'];
 
-    public static function buildShadowsocksr(array $server): array
+    public function getConfig(array $servers, User $user, string $target): array|string
     {
-        return [
-            'name' => $server['name'],
-            'type' => 'ssr',
-            'server' => $server['host'],
-            'port' => $server['port'],
-            'password' => $server['passwd'],
-            'cipher' => $server['method'],
-            'obfs' => $server['obfs'],
-            'obfs-param' => $server['obfs_param'],
-            'protocol' => $server['protocol'],
-            'protocol-param' => $server['protocol_param'],
-            'udp' => $server['udp'],
-        ];
-    }
-
-    public static function buildVmess(array $server): array
-    {
-        $array = [
-            'name' => $server['name'],
-            'type' => 'vmess',
-            'server' => $server['host'],
-            'port' => $server['port'],
-            'uuid' => $server['uuid'],
-            'alterId' => $server['v2_alter_id'],
-            'cipher' => $server['method'],
-            'udp' => $server['udp'],
-        ];
-
-        if ($server['v2_tls']) {
-            $array['tls'] = true;
-            $array['servername'] = $server['v2_host'];
+        $custom_path = '/resources/rules/custom.clash.yaml';
+        if (str_contains($target, 'bob_vpn')) {
+            $file_path = '/resources/rules/bob.clash.yaml';
+        } elseif (File::exists(base_path().$custom_path)) {
+            $file_path = $custom_path;
+        } else {
+            $file_path = '/resources/rules/default.clash.yaml';
         }
-        $array['network'] = $server['v2_net'];
 
-        if ($server['v2_net'] === 'ws') {
-            $array['ws-opts'] = [];
-            $array['ws-opts']['path'] = $server['v2_path'];
-            if ($server['v2_host']) {
-                $array['ws-opts']['headers'] = ['Host' => $server['v2_host']];
-            }
-            $array['ws-path'] = $server['v2_path'];
-            if ($server['v2_host']) {
-                $array['ws-headers'] = ['Host' => $server['v2_host']];
-            }
+        $appName = sysConfig('website_name');
+        header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($appName).'.yaml');
+        header('profile-update-interval: 24');
+        header('profile-web-page-url:'.sysConfig('website_url'));
+        if (sysConfig('is_custom_subscribe')) {
+            // display remaining traffic and expire date
+            header("subscription-userinfo: upload=$user->u; download=$user->d; total=$user->transfer_enable; expire=".strtotime($user->expired_at));
         }
 
-        return $array;
-    }
+        $config = Yaml::parseFile(base_path().$file_path);
 
-    public static function buildTrojan(array $server): array
-    {
-        $array = [
-            'name' => $server['name'],
-            'type' => 'trojan',
-            'server' => $server['host'],
-            'port' => $server['port'],
-            'password' => $server['passwd'],
-            'udp' => $server['udp'],
-        ];
+        $proxyProfiles = Protocols\Clash::build($servers);
 
-        if (! empty($server['sni'])) {
-            $array['sni'] = $server['sni'];
+        $config['proxies'] = array_merge($config['proxies'] ?: [], $proxyProfiles['proxies']);
+        foreach ($config['proxy-groups'] as $k => $v) {
+            if (! is_array($config['proxy-groups'][$k]['proxies'])) {
+                continue;
+            }
+            $config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxyProfiles['name']);
         }
 
-        return $array;
+        array_unshift($config['rules'], 'DOMAIN,'.$_SERVER['HTTP_HOST'].',DIRECT'); // Set current sub-domain to be direct
+
+        return str_replace('$app_name', $appName, Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE));
     }
 }

+ 107 - 0
app/Utils/Clients/Protocols/Clash.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace App\Utils\Clients\Protocols;
+
+use App\Utils\Library\Templates\Protocol;
+
+class Clash implements Protocol
+{
+    public static function build(array $servers): array
+    {
+        $validTypes = ['shadowsocks', 'shadowsocksr', 'vmess', 'trojan'];
+
+        $names = [];
+        $proxies = [];
+
+        foreach ($servers as $server) {
+            if (in_array($server['type'], $validTypes, true)) {
+                $names[] = $server['name'];
+                $proxies[] = call_user_func([self::class, 'build'.ucfirst($server['type'])], $server);
+            }
+        }
+
+        return ['name' => $names, 'proxies' => $proxies];
+    }
+
+    public static function buildShadowsocks(array $server): array
+    {
+        return [
+            'name' => $server['name'],
+            'type' => 'ss',
+            'server' => $server['host'],
+            'port' => $server['port'],
+            'cipher' => $server['method'],
+            'password' => $server['passwd'],
+            'udp' => $server['udp'],
+        ];
+    }
+
+    public static function buildShadowsocksr(array $server): array
+    {
+        return [
+            'name' => $server['name'],
+            'type' => 'ssr',
+            'server' => $server['host'],
+            'port' => $server['port'],
+            'password' => $server['passwd'],
+            'cipher' => $server['method'],
+            'obfs' => $server['obfs'],
+            'obfs-param' => $server['obfs_param'],
+            'protocol' => $server['protocol'],
+            'protocol-param' => $server['protocol_param'],
+            'udp' => $server['udp'],
+        ];
+    }
+
+    public static function buildVmess(array $server): array
+    {
+        $array = [
+            'name' => $server['name'],
+            'type' => 'vmess',
+            'server' => $server['host'],
+            'port' => $server['port'],
+            'uuid' => $server['uuid'],
+            'alterId' => $server['v2_alter_id'],
+            'cipher' => $server['method'],
+            'udp' => $server['udp'],
+        ];
+
+        if ($server['v2_tls']) {
+            $array['tls'] = true;
+            $array['servername'] = $server['v2_host'];
+        }
+        $array['network'] = $server['v2_net'];
+
+        if ($server['v2_net'] === 'ws') {
+            $array['ws-opts'] = [];
+            $array['ws-opts']['path'] = $server['v2_path'];
+            if ($server['v2_host']) {
+                $array['ws-opts']['headers'] = ['Host' => $server['v2_host']];
+            }
+            $array['ws-path'] = $server['v2_path'];
+            if ($server['v2_host']) {
+                $array['ws-headers'] = ['Host' => $server['v2_host']];
+            }
+        }
+
+        return $array;
+    }
+
+    public static function buildTrojan(array $server): array
+    {
+        $array = [
+            'name' => $server['name'],
+            'type' => 'trojan',
+            'server' => $server['host'],
+            'port' => $server['port'],
+            'password' => $server['passwd'],
+            'udp' => $server['udp'],
+        ];
+
+        if (! empty($server['sni'])) {
+            $array['sni'] = $server['sni'];
+        }
+
+        return $array;
+    }
+}

+ 17 - 9
app/Utils/Clients/QuantumultX.php → app/Utils/Clients/Protocols/QuantumultX.php

@@ -1,17 +1,25 @@
 <?php
 
-namespace App\Utils\Clients;
+namespace App\Utils\Clients\Protocols;
 
-/*
- * 本文件依据
- * https://github.com/crossutility/Quantumult-X/blob/master/server-complete.snippet
- *
- */
+use App\Utils\Library\Templates\Protocol;
 
-use App\Utils\Library\Templates\Client;
-
-class QuantumultX implements Client
+class QuantumultX implements Protocol
 {
+    public static function build(array $servers, bool $isEncode = false): string
+    {
+        $validTypes = ['shadowsocks', 'shadowsocksr', 'vmess', 'trojan'];
+        $url = '';
+
+        foreach ($servers as $server) {
+            if (in_array($server['type'], $validTypes, true)) {
+                $url .= call_user_func([self::class, 'build'.ucfirst($server['type'])], $server);
+            }
+        }
+
+        return $isEncode ? base64_encode($url) : $url;
+    }
+
     public static function buildShadowsocks(array $server): string
     {
         $config = array_filter([

+ 82 - 0
app/Utils/Clients/Protocols/Surge.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Utils\Clients\Protocols;
+
+use App\Utils\Library\Templates\Protocol;
+
+class Surge implements Protocol
+{
+    public static function build(array $servers): array
+    {
+        $validTypes = ['shadowsocks', 'vmess', 'trojan'];
+        $names = '';
+        $proxies = '';
+
+        foreach ($servers as $server) {
+            if (in_array($server['type'], $validTypes, true)) {
+                $names .= $server['name'].', ';
+                $proxies .= call_user_func([self::class, 'build'.ucfirst($server['type'])], $server);
+            }
+        }
+
+        return ['name' => rtrim($names, ', '), 'proxies' => $proxies];
+    }
+
+    public static function buildShadowsocks(array $server): string
+    {
+        $config = array_filter([
+            "{$server['name']}=ss",
+            $server['host'],
+            $server['port'],
+            "encrypt-method={$server['method']}",
+            "password={$server['passwd']}",
+            'tfo=true',
+            "udp-relay={$server['udp']}",
+        ]);
+
+        return implode(',', $config).PHP_EOL;
+    }
+
+    public static function buildVmess(array $server): string
+    {
+        $config = [
+            "{$server['name']}=vmess",
+            $server['host'],
+            $server['port'],
+            "username={$server['uuid']}",
+            'vmess-aead=true',
+            'tfo=true',
+            "udp-relay={$server['udp']}",
+        ];
+
+        if ($server['v2_tls']) {
+            array_push($config, 'tls=true', "sni={$server['v2_host']}");
+        }
+        if ($server['v2_net'] === 'ws') {
+            array_push($config, 'ws=true', "ws-path={$server['v2_path']}", "ws-headers=Host:{$server['v2_host']}");
+        }
+
+        return implode(',', $config).PHP_EOL;
+    }
+
+    public static function buildTrojan(array $server): string
+    {
+        $config = array_filter([
+            "{$server['name']}=trojan",
+            $server['host'],
+            $server['port'],
+            "password={$server['passwd']}",
+            $server['sni'] ? "sni={$server['sni']}" : '',
+            'tfo=true',
+            "udp-relay={$server['udp']}",
+            // "skip-cert-verify={$server['allow_insecure']}"
+        ]);
+
+        return implode(',', $config).PHP_EOL;
+    }
+
+    public static function buildShadowsocksr(array $server): array|string
+    {
+        return '';
+    }
+}

+ 3 - 3
app/Utils/Clients/Text.php → app/Utils/Clients/Protocols/Text.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Utils\Clients;
+namespace App\Utils\Clients\Protocols;
 
-use App\Utils\Library\Templates\Client;
+use App\Utils\Library\Templates\Protocol;
 
-class Text implements Client
+class Text implements Protocol
 {
     public static function buildShadowsocks(array $server): string
     {

+ 18 - 4
app/Utils/Clients/URLSchemes.php → app/Utils/Clients/Protocols/URLSchemes.php

@@ -1,13 +1,27 @@
 <?php
 
-namespace App\Utils\Clients;
+namespace App\Utils\Clients\Protocols;
 
-use App\Utils\Library\Templates\Client;
+use App\Utils\Library\Templates\Protocol;
 
-class URLSchemes implements Client
+class URLSchemes implements Protocol
 {
-    public static function buildShadowsocks(array $server): string
+    public static function build(array $servers, bool $isEncode = true): string
     {
+        $validTypes = ['shadowsocks', 'shadowsocksr', 'vmess', 'trojan'];
+        $url = '';
+
+        foreach ($servers as $server) {
+            if (in_array($server['type'], $validTypes, true)) {
+                $url .= call_user_func([self::class, 'build'.ucfirst($server['type'])], $server);
+            }
+        }
+
+        return $isEncode ? base64_encode($url) : $url;
+    }
+
+    public static function buildShadowsocks(array $server): string
+    { // https://shadowsocks.org/doc/sip002.html
         $name = rawurlencode($server['name']);
         $str = base64url_encode("{$server['method']}:{$server['passwd']}");
 

+ 32 - 0
app/Utils/Clients/Quantumult.php

@@ -0,0 +1,32 @@
+<?php
+/*
+ * Developed based on
+ * https://github.com/crossutility/Quantumult/blob/master/quantumult-uri-scheme.md
+ *
+ */
+
+namespace App\Utils\Clients;
+
+use App\Models\User;
+use App\Utils\Clients\Protocols\QuantumultX;
+use App\Utils\Clients\Protocols\URLSchemes;
+use App\Utils\Library\Templates\Client;
+
+class Quantumult implements Client
+{
+    public const AGENT = ['quantumult', 'quantumult%20x'];
+
+    public function getConfig(array $servers, User $user, string $target): string
+    {
+        //display remaining traffic and expire date
+        if (sysConfig('is_custom_subscribe')) {
+            header("Subscription-Userinfo: upload=$user->u; download=$user->d; total=$user->transfer_enable; expire=".strtotime($user->expired_at));
+        }
+
+        if (str_contains($target, 'quantumult%20x')) {
+            return QuantumultX::build($servers, true);
+        }
+
+        return URLSchemes::build($servers);
+    }
+}

+ 25 - 0
app/Utils/Clients/Shadowrocket.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Utils\Clients;
+
+use App\Models\User;
+use App\Utils\Clients\Protocols\URLSchemes;
+use App\Utils\Library\Templates\Client;
+
+class Shadowrocket implements Client
+{
+    public const AGENT = ['shadowrocket'];
+
+    public function getConfig(array $servers, User $user): string
+    {
+        $uri = '';
+        //display remaining traffic and expire date
+        if (sysConfig('is_custom_subscribe')) {
+            $usedTraffic = formatBytes($user->used_traffic);
+            $remainTraffic = formatBytes($user->transfer_enable - $user->used_traffic);
+            $uri = "STATUS=📊:{$usedTraffic}💾:{$remainTraffic}📅:$user->expiration_date\r\n";
+        }
+
+        return base64_encode($uri.URLSchemes::build($servers, false));
+    }
+}

+ 48 - 51
app/Utils/Clients/Surge.php

@@ -1,66 +1,63 @@
 <?php
+/*
+ * Developed based on
+ * https://wiki.surge.community/modules
+ * https://getsurfboard.com/docs/profile-format/overview/
+ */
 
 namespace App\Utils\Clients;
 
+use App\Models\User;
 use App\Utils\Library\Templates\Client;
+use File;
 
 class Surge implements Client
 {
-    public static function buildShadowsocks(array $server): string
-    {
-        $config = array_filter([
-            "{$server['name']}=ss",
-            $server['host'],
-            $server['port'],
-            "encrypt-method={$server['method']}",
-            "password={$server['passwd']}",
-            'tfo=true',
-            "udp-relay={$server['udp']}",
-        ]);
-
-        return implode(',', $config).PHP_EOL;
-    }
+    public const AGENT = ['surge', 'surfboard'];
 
-    public static function buildVmess(array $server): string
+    public function getConfig(array $servers, User $user, string $target): string
     {
-        $config = [
-            "{$server['name']}=vmess",
-            $server['host'],
-            $server['port'],
-            "username={$server['uuid']}",
-            'vmess-aead=true',
-            'tfo=true',
-            "udp-relay={$server['udp']}",
-        ];
-
-        if ($server['v2_tls']) {
-            array_push($config, 'tls=true', "sni={$server['v2_host']}");
-        }
-        if ($server['v2_net'] === 'ws') {
-            array_push($config, 'ws=true', "ws-path={$server['v2_path']}", "ws-headers=Host:{$server['v2_host']}");
+        if (str_contains($target, 'surge')) {
+            $customConfig = base_path().'/resources/rules/custom.surge.conf';
+
+            if (File::exists($customConfig)) {
+                $config = file_get_contents($customConfig);
+            } else {
+                $config = file_get_contents(base_path().'/resources/rules/default.surge.conf');
+            }
+        } else {
+            $customConfig = base_path().'/resources/rules/custom.surfboard.conf';
+
+            if (File::exists($customConfig)) {
+                $config = file_get_contents($customConfig);
+            } else {
+                $config = file_get_contents(base_path().'/resources/rules/default.surfboard.conf');
+            }
         }
 
-        return implode(',', $config).PHP_EOL;
-    }
-
-    public static function buildTrojan(array $server): string
-    {
-        $config = array_filter([
-            "{$server['name']}=trojan",
-            $server['host'],
-            $server['port'],
-            "password={$server['passwd']}",
-            $server['sni'] ? "sni={$server['sni']}" : '',
-            'tfo=true',
-            "udp-relay={$server['udp']}",
-            // "skip-cert-verify={$server['allow_insecure']}"
-        ]);
-
-        return implode(',', $config).PHP_EOL;
-    }
+        $webName = sysConfig('website_name');
+        header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($webName).'.conf');
+        $proxyProfiles = Protocols\Surge::build($servers);
+
+        if (sysConfig('is_custom_subscribe')) {
+            $upload = formatBytes($user->u);
+            $download = formatBytes($user->d);
+            $totalTraffic = formatBytes($user->transfer_enable);
+            $style = 'info';
+            $remainTraffic = $user->transfer_enable - $user->d - $user->u;
+            $remainDates = now()->diffInDays($user->expired_at, false);
+            if ($remainTraffic <= 0 || $remainDates <= 0) {
+                $style = 'error';
+            } elseif (($user->transfer_enable - $user->d - $user->u) / $user->transfer_enable <= 0.05 || $remainDates <= 7) {
+                $style = 'alert';
+            }
+
+            $subscribeInfo = "title=$webName".trans('user.subscribe.info.title').', content='.trans('user.subscribe.info.upload').": $upload\n".trans('user.subscribe.info.download').": $download\n".trans('user.subscribe.info.total').": $totalTraffic\n".trans('model.user.expired_date').": $user->expired_at, style=$style";
+        } else {
+            $subscribeInfo = "title=$webName, content=";
+        }
 
-    public static function buildShadowsocksr(array $server): array|string
-    {
-        return '';
+        return str_replace(['$subscribe_info', '$subs_link', '$subs_domain', '$proxies', '$proxy_group'], [$subscribeInfo, route('sub', $user->subscribe->code), $_SERVER['HTTP_HOST'], $proxyProfiles['proxies'], $proxyProfiles['names']],
+            $config);
     }
 }

+ 33 - 0
app/Utils/Clients/V2rayN.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Utils\Clients;
+
+use App\Models\User;
+use App\Utils\Clients\Protocols\URLSchemes;
+use App\Utils\Library\Templates\Client;
+
+class V2rayN implements Client
+{
+    public const AGENT = ['v2rayn', 'v2rayng', 'v2rayu'];
+
+    public function getConfig(array $servers, User $user): string
+    {
+        $uri = '';
+        if (sysConfig('is_custom_subscribe')) {
+            $text = '';
+            if ($user->expiration_date > date('Y-m-d')) {
+                if ($user->transfer_enable === 0) {
+                    $text .= trans('user.account.remain').': 0';
+                } else {
+                    $text .= trans('user.account.remain').': '.formatBytes($user->transfer_enable);
+                }
+                $text .= ', '.trans('model.user.expired_date').": $user->expiration_date";
+            } else {
+                $text .= trans('user.account.reason.expired');
+            }
+            $uri .= 'trojan://[email protected]:0?peer=0.0.0.0#'.rawurlencode($text);
+        }
+
+        return base64_encode($uri.URLSchemes::build($servers, false));
+    }
+}

+ 0 - 7
app/Utils/Library/Templates/Client.php

@@ -4,11 +4,4 @@ namespace App\Utils\Library\Templates;
 
 interface Client
 {
-    public static function buildShadowsocks(array $server): array|string;
-
-    public static function buildShadowsocksr(array $server): array|string;
-
-    public static function buildVmess(array $server): array|string;
-
-    public static function buildTrojan(array $server): array|string;
 }

+ 14 - 0
app/Utils/Library/Templates/Protocol.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Utils\Library\Templates;
+
+interface Protocol
+{
+    public static function buildShadowsocks(array $server): array|string|null;
+
+    public static function buildShadowsocksr(array $server): array|string|null;
+
+    public static function buildVmess(array $server): array|string|null;
+
+    public static function buildTrojan(array $server): array|string|null;
+}