Browse Source

feat: cloudflare r2 client download

M1Screw 2 years ago
parent
commit
cb6a1b31f6

+ 2 - 0
app/routes.php

@@ -98,6 +98,8 @@ return static function (Slim\App $app): void {
         $group->post('/payment/purchase/{type}', App\Services\Payment::class . ':purchase');
         $group->get('/payment/purchase/{type}', App\Services\Payment::class . ':purchase');
         $group->get('/payment/return/{type}', App\Services\Payment::class . ':returnHTML');
+        // Get Clients
+        $group->get('/clients/{name}', App\Controllers\User\ClientController::class . ':getClients');
     })->add(new Auth());
 
     $app->group('/payment', static function (RouteCollectorProxy $group): void {

+ 8 - 0
config/.config.example.php

@@ -133,3 +133,11 @@ $_ENV['ai_model'] = 'gpt-3.5-turbo';
 
 // ClientDownload 命令解决 API 访问频率高而被限制使用的 Github access token
 $_ENV['github_access_token'] = '';
+
+// use Cloudflare R2 for clients download
+$_ENV['enable_r2_client_download'] = false;
+$_ENV['r2_bucket_name'] = '';
+$_ENV['r2_account_id'] = '';
+$_ENV['r2_access_key_id'] = '';
+$_ENV['r2_access_key_secret'] = '';
+$_ENV['r2_client_download_timeout'] = 10;

+ 2 - 2
config/clients.json

@@ -7,8 +7,8 @@
             "savePath": "public/clients/",
             "downloads": [
                 {
-                    "sourceName": "Clash.Verge_%tagName1%_x64_zh-CN.msi",
-                    "saveName": "Clash.Verge.msi"
+                    "sourceName": "Clash.Verge_%tagName1%_x64-setup.exe",
+                    "saveName": "Clash.Verge.exe"
                 },
                 {
                     "sourceName": "Clash.Verge_%tagName1%_aarch64.dmg",

+ 39 - 10
resources/views/tabler/user/index.tpl

@@ -233,11 +233,19 @@
                                                 {/if}
                                                 {if $public_setting['enable_trojan_sub']}<a data-clipboard-text="{$TraditionalSub}?trojan=1"class="copy btn btn-primary">复制传统订阅(Trojan)</a>
                                                 {/if}
-                                                <a href="/clients/v2rayN-Core.zip"
+                                                <a {if $config['enable_r2_client_download']}
+                                                        href="/user/clients/v2rayN-Core.zip"
+                                                    {else}
+                                                        href="/clients/v2rayN-Core.zip"
+                                                    {/if}
                                                    class="btn btn-primary">
                                                     下载 v2rayN(Windows)
                                                 </a>
-                                                <a href="/clients/v2rayNG.apk"
+                                                <a {if $config['enable_r2_client_download']}
+                                                        href="/user/clients/v2rayNG.apk"
+                                                    {else}
+                                                        href="/clients/v2rayNG.apk"
+                                                    {/if}
                                                    class="btn btn-primary">
                                                     下载 v2rayNG(Android)
                                                 </a>
@@ -255,7 +263,12 @@
                                                class="copy btn btn-primary">
                                                 复制 Clash 订阅链接
                                             </a>
-                                            <a href="/clients/Clash.Verge.msi"
+                                            <a
+                                               {if $config['enable_r2_client_download']}
+                                                    href="/user/clients/Clash.Verge.exe"
+                                               {else}
+                                                    href="/clients/Clash.Verge.exe"
+                                               {/if}
                                                class="btn btn-primary">
                                                 下载 Clash Verge
                                             </a>
@@ -275,11 +288,19 @@
                                            class="copy btn btn-primary">
                                             复制 Clash 订阅链接
                                         </a>
-                                        <a href="/clients/Clash.Verge_aarch64.dmg"
+                                        <a {if $config['enable_r2_client_download']}
+                                                href="/user/clients/Clash.Verge_aarch64.dmg"
+                                            {else}
+                                                href="/clients/Clash.Verge_aarch64.dmg"
+                                            {/if}
                                            class="btn btn-primary">
                                             下载 Clash Verge (aarch64)
                                         </a>
-                                        <a href="/clients/Clash.Verge_x64.dmg"
+                                        <a {if $config['enable_r2_client_download']}
+                                                href="/user/clients/Clash.Verge_x64.dmg"
+                                            {else}
+                                                href="/clients/Clash.Verge_x64.dmg"
+                                            {/if}
                                            class="btn btn-primary">
                                             下载 Clash Verge (x86_64)
                                         </a>
@@ -298,7 +319,11 @@
                                            class="copy btn btn-primary">
                                             复制 Clash 订阅链接
                                         </a>
-                                        <a href="/clients/Clash-Android.apk"
+                                        <a {if $config['enable_r2_client_download']}
+                                                href="/user/clients/Clash-Android.apk"
+                                            {else}
+                                                href="/clients/Clash-Android.apk"
+                                            {/if}
                                            class="btn btn-primary">
                                             下载 Clash for Android
                                         </a>
@@ -339,7 +364,11 @@
                                            class="copy btn btn-primary">
                                             复制 Clash 订阅链接
                                         </a>
-                                        <a href="/clients/Clash.Verge.AppImage.tar.gz"
+                                        <a {if $config['enable_r2_client_download']}
+                                                href="/user/clients/Clash.Verge.AppImage.tar.gz"
+                                            {else}
+                                                href="/clients/Clash.Verge.AppImage.tar.gz"
+                                            {/if}
                                            class="btn btn-primary">
                                             下载 Clash Verge
                                         </a>
@@ -428,7 +457,7 @@
                             <i class="ti ti-bell-ringing icon"></i>
                         </div>
                         <div class="card-body">
-                            <h3 class="card-title">最新公告 
+                            <h3 class="card-title">最新公告
                             {if $ann !== null}
                             <span class="card-subtitle">{$ann->date}</span>
                             {/if}
@@ -492,7 +521,7 @@
                         </div>
                     </div>
                 {/if}
-                
+
             </div>
         </div>
     </div>
@@ -508,7 +537,7 @@
             $.ajax({
                 type: "POST",
                 url: "/user/checkin",
-                dataType: "json",              
+                dataType: "json",
                 data: {
                     {if $public_setting['enable_checkin_captcha'] && $user->isAbleToCheckin()}
                         {if $public_setting['captcha_provider'] === 'turnstile'}

+ 15 - 0
src/Command/ClientDownload.php

@@ -4,14 +4,24 @@ declare(strict_types=1);
 
 namespace App\Command;
 
+use App\Services\Cloudflare;
 use Exception;
 use GuzzleHttp\Client;
+use function file_exists;
+use function file_get_contents;
+use function file_put_contents;
 use function get_current_user;
+use function is_file;
 use function json_decode;
 use function json_encode;
 use function posix_geteuid;
 use function posix_getpwuid;
+use function str_replace;
+use function substr;
 use function time;
+use function unlink;
+use const BASE_PATH;
+use const PHP_EOL;
 
 final class ClientDownload extends Command
 {
@@ -236,6 +246,11 @@ final class ClientDownload extends Command
             if ($this->getSourceFile($fileName, $savePath, $downloadUrl)) {
                 $this->setLocalVersions($this->version);
             }
+
+            if ($_ENV['enable_r2_client_download']) {
+                Cloudflare::uploadR2($fileName, file_get_contents($filePath));
+                unlink($filePath);
+            }
         }
 
         echo '====== ' . $task['name'] . ' 结束 ======' . PHP_EOL;

+ 44 - 0
src/Controllers/User/ClientController.php

@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers\User;
+
+use App\Controllers\BaseController;
+use App\Services\Cloudflare;
+use Psr\Http\Message\ResponseInterface;
+use Slim\Http\Response;
+use Slim\Http\ServerRequest;
+use voku\helper\AntiXSS;
+use function in_array;
+
+final class ClientController extends BaseController
+{
+    public function getClients(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
+    {
+        $antiXss = new AntiXSS();
+        $clientName = $antiXss->xss_clean($args['name']);
+
+        if (! $_ENV['enable_r2_client_download'] || $clientName === '' || $clientName === null) {
+            return $response->withStatus(404);
+        }
+
+        $clients = [
+            'Clash.Verge.exe',
+            'Clash.Verge_aarch64.dmg',
+            'Clash.Verge_x64.dmg',
+            'Clash.Verge.AppImage.tar.gz',
+            'Clash-Android.apk',
+            'v2rayN-Core.zip',
+            'v2rayNG.apk',
+        ];
+
+        if (! in_array($clientName, $clients)) {
+            return $response->withStatus(404);
+        }
+
+        $presignedUrl = Cloudflare::genR2PresignedUrl($clientName);
+
+        return $response->withHeader('Location', $presignedUrl)->withStatus(302);
+    }
+}

+ 47 - 0
src/Services/Cloudflare.php

@@ -4,11 +4,14 @@ declare(strict_types=1);
 
 namespace App\Services;
 
+use Aws\Credentials\Credentials;
+use Aws\S3\S3Client;
 use Cloudflare\API\Adapter\Guzzle;
 use Cloudflare\API\Auth\APIKey;
 use Cloudflare\API\Endpoints\DNS;
 use Cloudflare\API\Endpoints\EndpointException;
 use Cloudflare\API\Endpoints\Zones;
+use Exception;
 
 final class Cloudflare
 {
@@ -56,4 +59,48 @@ final class Cloudflare
             }
         }
     }
+
+    public static function initR2(): S3Client
+    {
+        $credentials = new Credentials($_ENV['r2_access_key_id'], $_ENV['r2_access_key_secret']);
+
+        $options = [
+            'region' => 'auto',
+            'endpoint' => 'https://' . $_ENV['r2_account_id'] . '.r2.cloudflarestorage.com',
+            'version' => 'latest',
+            'credentials' => $credentials,
+        ];
+
+        return new S3Client($options);
+    }
+
+    public static function uploadR2($name, $file): void
+    {
+        $s3 = self::initR2();
+
+        try {
+            $s3->upload(
+                $_ENV['r2_bucket_name'],
+                $name,
+                $file,
+            );
+        } catch (Exception $e) {
+            echo $e->getMessage() . PHP_EOL;
+        }
+    }
+
+    public static function genR2PresignedUrl($fileName): string
+    {
+        $s3 = self::initR2();
+
+        $cmd = $s3->getCommand('GetObject', [
+            'Bucket' => $_ENV['r2_bucket_name'],
+            'Key' => $fileName,
+        ]);
+
+        return (string) $s3->createPresignedRequest(
+            $cmd,
+            '+' . $_ENV['r2_client_download_timeout'] . ' minutes'
+        )->getUri();
+    }
 }

+ 2 - 0
src/Services/Config.php

@@ -32,6 +32,8 @@ final class Config
 
             'subscribeLog' => $_ENV['subscribeLog'],
             'subscribeLog_keep_days' => $_ENV['subscribeLog_keep_days'],
+
+            'enable_r2_client_download' => $_ENV['enable_r2_client_download'],
         ];
     }