改进
This commit is contained in:
parent
cd9d716892
commit
7f5bd004a2
48
app/Console/Commands/ChangePassword.php
Normal file
48
app/Console/Commands/ChangePassword.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ChangePassword extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'pwd';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '修改指定管理员用户的密码';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->ask('请输入邮箱');
|
||||
|
||||
$admin = Admin::where('email', $email)->first();
|
||||
if (! $admin) {
|
||||
$this->error('用户不存在');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$password = $this->secret('请输入新密码');
|
||||
$admin->password = bcrypt($password);
|
||||
$admin->save();
|
||||
|
||||
$this->info('密码修改成功');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
62
app/Console/Commands/CreateAdmin.php
Normal file
62
app/Console/Commands/CreateAdmin.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CreateAdmin extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'admin:create';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Create a new admin user';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// if is local
|
||||
if (env('APP_ENV') == 'local') {
|
||||
$this->info('由于是 local 环境,将会自动创建 Admin 用户。');
|
||||
Admin::create([
|
||||
'name' => 'Test',
|
||||
'email' => 'im@ivampiresp.com',
|
||||
'password' => bcrypt('123456'),
|
||||
]);
|
||||
$this->info('邮箱: im@ivampiresp.com, 密码: 123456');
|
||||
|
||||
return 0;
|
||||
}
|
||||
// ask for the name of the admin to create
|
||||
$name = $this->ask('请输入用户名');
|
||||
// ask for the email of the admin to create
|
||||
$email = $this->ask('请输入邮箱');
|
||||
|
||||
// enter password
|
||||
$password = $this->secret('请输入密码(密码不会显示在终端)');
|
||||
|
||||
// create the admin
|
||||
Admin::create([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => bcrypt($password),
|
||||
]);
|
||||
|
||||
$this->info('管理员创建成功!');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MakeAdmin extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'make-admin {email}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '将一个用户升级为管理员,需要提供用户的邮箱。';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
|
||||
$user = (new User)->where('email', $email)->first();
|
||||
|
||||
if ($user) {
|
||||
|
||||
if (!$user->is_admin) {
|
||||
$user->is_admin = true;
|
||||
$user->save();
|
||||
$this->info('用户已经升级为管理员。需要取消管理员身份请使用再次执行此命令。');
|
||||
return;
|
||||
}
|
||||
|
||||
$cancel = $this->confirm('用户已经升级为管理员,要取消管理员身份吗?', true);
|
||||
|
||||
if ($cancel) {
|
||||
$user->is_admin = false;
|
||||
$user->save();
|
||||
$this->info('用户已经取消管理员身份。');
|
||||
} else {
|
||||
$this->info('用户仍然是管理员。');
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->error('用户不存在。');
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use App\Http\Controllers\Admin\ServerController;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
@ -12,7 +13,15 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
$schedule->call(function () {
|
||||
(new ServerController())->checkServer();
|
||||
})->everyMinute()->name('FrpServer')->withoutOverlapping()->onOneServer();
|
||||
|
||||
// $schedule->job(new Cost())->hourly()->name('FrpServerCost')->withoutOverlapping()->onOneServer();
|
||||
|
||||
// every three days
|
||||
// $schedule->job(new ReviewWebsiteJob())->daily()->name('reviewWebsiteJob')->withoutOverlapping()->onOneServer();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
58
app/Helpers.php
Normal file
58
app/Helpers.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('success')) {
|
||||
function success($data = null, $to = false)
|
||||
{
|
||||
if ($to) {
|
||||
return redirect()->to($to)->with('status', $data ?? 'Success!');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('status', $data ?? 'Success!');
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('failed')) {
|
||||
function failed($data = null)
|
||||
{
|
||||
return redirect()->back()->with('error', $data ?? 'Success!');
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('isUser')) {
|
||||
function isUser($user_id)
|
||||
{
|
||||
return auth()->id() === $user_id ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('unitConversion')) {
|
||||
function unitConversion($num)
|
||||
{
|
||||
$p = 0;
|
||||
$format = 'Bytes';
|
||||
if ($num > 0 && $num < 1024) {
|
||||
$p = 0;
|
||||
|
||||
return number_format($num).' '.$format;
|
||||
}
|
||||
if ($num >= 1024 && $num < pow(1024, 2)) {
|
||||
$p = 1;
|
||||
$format = 'KB';
|
||||
}
|
||||
if ($num >= pow(1024, 2) && $num < pow(1024, 3)) {
|
||||
$p = 2;
|
||||
$format = 'MB';
|
||||
}
|
||||
if ($num >= pow(1024, 3) && $num < pow(1024, 4)) {
|
||||
$p = 3;
|
||||
$format = 'GB';
|
||||
}
|
||||
if ($num >= pow(1024, 4) && $num < pow(1024, 5)) {
|
||||
$p = 3;
|
||||
$format = 'TB';
|
||||
}
|
||||
$num /= pow(1024, $p);
|
||||
|
||||
return number_format($num, 3).' '.$format;
|
||||
}
|
||||
}
|
44
app/Http/Controllers/Admin/IndexController.php
Normal file
44
app/Http/Controllers/Admin/IndexController.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class IndexController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// if not login, redirect to login
|
||||
if (! Auth::guard('admin')->check()) {
|
||||
return view('admin.login');
|
||||
} else {
|
||||
$servers = Server::where('status', '!=', 'up')->get();
|
||||
|
||||
return view('admin.index', compact('servers'));
|
||||
}
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
// attempt to login
|
||||
if (Auth::guard('admin')->attempt($request->only(['email', 'password']), $request->has('remember'))) {
|
||||
// if success, redirect to home
|
||||
return redirect()->intended('/');
|
||||
} else {
|
||||
// if fail, redirect to login with error message
|
||||
return redirect()->back()->withErrors(['message' => '用户名或密码错误'])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
// logout
|
||||
Auth::guard('admin')->logout();
|
||||
|
||||
return redirect()->route('admin.login');
|
||||
}
|
||||
}
|
247
app/Http/Controllers/Admin/ServerController.php
Normal file
247
app/Http/Controllers/Admin/ServerController.php
Normal file
@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Support\Frp;
|
||||
use App\Models\Server;
|
||||
use Illuminate\View\View;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
class ServerController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
//
|
||||
$servers = Server::get();
|
||||
// $servers = Server::simplePaginate(10);
|
||||
|
||||
return view('admin.servers.index', compact('servers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('admin.servers.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate($this->rules());
|
||||
|
||||
$request_data = $request->toArray();
|
||||
// $request_data['user_id'] = auth()->id();
|
||||
|
||||
$request_data['allow_http'] = $request->allow_http ?? 0;
|
||||
$request_data['allow_https'] = $request->allow_https ?? 0;
|
||||
$request_data['allow_tcp'] = $request->allow_tcp ?? 0;
|
||||
$request_data['allow_udp'] = $request->allow_udp ?? 0;
|
||||
$request_data['allow_stcp'] = $request->allow_stcp ?? 0;
|
||||
$request_data['allow_xtcp'] = $request->allow_xtcp ?? 0;
|
||||
$request_data['allow_sudp'] = $request->allow_sudp ?? 0;
|
||||
$request_data['is_china_mainland'] = $request->is_china_mainland ?? 0;
|
||||
|
||||
$server = Server::create($request_data);
|
||||
|
||||
return redirect()->route('admin.servers.edit', $server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param Server $server
|
||||
* @return RedirectResponse|View
|
||||
*/
|
||||
public function show(Server $server)
|
||||
{
|
||||
try {
|
||||
$serverInfo = (object) (new Frp($server))->serverInfo();
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
return redirect()->route('admin.servers.index')->with('error', '服务器连接失败。');
|
||||
}
|
||||
|
||||
return view('admin.servers.show', compact('server'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param Server $server
|
||||
* @return View
|
||||
*/
|
||||
public function edit(Server $server)
|
||||
{
|
||||
$serverInfo = (object) (new Frp($server))->serverInfo();
|
||||
|
||||
return view('admin.servers.edit', compact('server', 'serverInfo'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param Server $server
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function update(Request $request, Server $server)
|
||||
{
|
||||
if (! $request->has('status')) {
|
||||
$request->merge(['allow_http' => $request->has('allow_http') ? true : false]);
|
||||
$request->merge(['allow_https' => $request->has('allow_https') ? true : false]);
|
||||
$request->merge(['allow_tcp' => $request->has('allow_tcp') ? true : false]);
|
||||
$request->merge(['allow_udp' => $request->has('allow_udp') ? true : false]);
|
||||
$request->merge(['allow_stcp' => $request->has('allow_stcp') ? true : false]);
|
||||
$request->merge(['allow_xtcp' => $request->has('allow_xtcp') ? true : false]);
|
||||
$request->merge(['allow_sudp' => $request->has('allow_sudp') ? true : false]);
|
||||
$request->merge(['is_china_mainland' => $request->has('is_china_mainland') ? true : false]);
|
||||
}
|
||||
|
||||
$data = $request->all();
|
||||
|
||||
$server->update($data);
|
||||
|
||||
return redirect()->route('admin.servers.index')->with('success', '服务器成功更新。');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param Server $server
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function destroy(Server $server)
|
||||
{
|
||||
$server->delete();
|
||||
|
||||
return redirect()->route('admin.servers.index')->with('success', '服务器成功删除。');
|
||||
}
|
||||
|
||||
public function rules($id = null)
|
||||
{
|
||||
return [
|
||||
'name' => 'required|max:20',
|
||||
'server_address' => [
|
||||
'required',
|
||||
Rule::unique('servers')->ignore($id),
|
||||
],
|
||||
'server_port' => 'required|integer|max:65535|min:1',
|
||||
'token' => 'required|max:50',
|
||||
'dashboard_port' => 'required|integer|max:65535|min:1',
|
||||
'dashboard_user' => 'required|max:20',
|
||||
'dashboard_password' => 'required|max:32',
|
||||
'allow_http' => 'boolean',
|
||||
'allow_https' => 'boolean',
|
||||
'allow_tcp' => 'boolean',
|
||||
'allow_udp' => 'boolean',
|
||||
'allow_stcp' => 'boolean',
|
||||
'allow_xtcp' => 'boolean',
|
||||
'allow_sudp' => 'boolean',
|
||||
'min_port' => 'required|integer|max:65535|min:1',
|
||||
'max_port' => 'required|integer|max:65535|min:1',
|
||||
'max_tunnels' => 'required|integer|max:65535|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
public function checkServer($id = null)
|
||||
{
|
||||
if (is_null($id)) {
|
||||
// refresh all
|
||||
Server::chunk(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
dispatch(new ServerCheckJob($server->id));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (Server::where('id', $id)->exists()) {
|
||||
dispatch(new ServerCheckJob($id));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function scanTunnel($server_id)
|
||||
{
|
||||
$server = Server::find($server_id);
|
||||
if (is_null($server)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$frp = new Frp($server);
|
||||
|
||||
if ($server->allow_http) {
|
||||
$proxies = $frp->httpTunnels()['proxies'] ?? ['proxies' => []];
|
||||
$this->cacheProxies($proxies);
|
||||
}
|
||||
|
||||
if ($server->allow_https) {
|
||||
$proxies = $frp->httpsTunnels()['proxies'] ?? ['proxies' => []];
|
||||
$this->cacheProxies($proxies);
|
||||
}
|
||||
|
||||
if ($server->allow_tcp) {
|
||||
$proxies = $frp->tcpTunnels()['proxies'] ?? ['proxies' => []];
|
||||
$this->cacheProxies($proxies);
|
||||
}
|
||||
|
||||
if ($server->allow_udp) {
|
||||
$proxies = $frp->udpTunnels()['proxies'] ?? ['proxies' => []];
|
||||
$this->cacheProxies($proxies);
|
||||
}
|
||||
|
||||
if ($server->allow_stcp) {
|
||||
$proxies = $frp->stcpTunnels()['proxies'] ?? ['proxies' => []];
|
||||
$this->cacheProxies($proxies);
|
||||
}
|
||||
|
||||
if ($server->allow_xtcp) {
|
||||
$proxies = $frp->xtcpTunnels()['proxies'] ?? ['proxies' => []];
|
||||
$this->cacheProxies($proxies);
|
||||
}
|
||||
}
|
||||
|
||||
private function cacheProxies($proxies)
|
||||
{
|
||||
foreach ($proxies as $proxy) {
|
||||
if (! isset($proxy['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cache_key = 'frpTunnel_data_'.$proxy['name'];
|
||||
|
||||
Cache::put($cache_key, $proxy, 86400);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTunnel($name)
|
||||
{
|
||||
$cache_key = 'frpTunnel_data_'.$name;
|
||||
|
||||
return Cache::get($cache_key);
|
||||
}
|
||||
}
|
146
app/Http/Controllers/Admin/TunnelController.php
Normal file
146
app/Http/Controllers/Admin/TunnelController.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Tunnel;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class TunnelController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return RedirectResponse|View
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$hosts = Tunnel::with('user');
|
||||
|
||||
// if has has_free_traffic
|
||||
if ($request->has_free_traffic == 1) {
|
||||
$hosts = $hosts->where('free_traffic', '>', 0);
|
||||
}
|
||||
|
||||
foreach ($request->except(['has_free_traffic', 'page']) as $key => $value) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($request->{$key}) {
|
||||
$hosts = $hosts->where($key, 'LIKE', '%'.$value.'%');
|
||||
}
|
||||
}
|
||||
|
||||
$count = $hosts->count();
|
||||
|
||||
$hosts = $hosts->simplePaginate(100);
|
||||
|
||||
return view('admin.tunnels.index', ['hosts' => $hosts, 'count' => $count]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param Tunnel $tunnel
|
||||
* @return View
|
||||
*/
|
||||
public function show(Tunnel $tunnel)
|
||||
{
|
||||
$tunnel->load('server');
|
||||
|
||||
return view('admin.tunnels.show', compact('tunnel'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param Tunnel $tunnel
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function update(Request $request, Tunnel $tunnel)
|
||||
{
|
||||
$request->validate([
|
||||
'locked_reason' => 'nullable|string'
|
||||
]);
|
||||
|
||||
$tunnel->update($request->all());
|
||||
|
||||
return back()->with('success', '完成。');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param Tunnel $host
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function destroy(Tunnel $host)
|
||||
{
|
||||
$host->delete();
|
||||
|
||||
return back()->with('success', '已开始销毁。');
|
||||
}
|
||||
|
||||
public function generateConfig(Tunnel $tunnel)
|
||||
{
|
||||
$tunnel->load('server');
|
||||
|
||||
// 配置文件
|
||||
$config = [];
|
||||
|
||||
$config['server'] = <<<EOF
|
||||
[common]
|
||||
server_addr = {$tunnel->server->server_address}
|
||||
server_port = {$tunnel->server->server_port}
|
||||
token = {$tunnel->server->token}
|
||||
EOF;
|
||||
|
||||
$local_addr = explode(':', $tunnel->local_address);
|
||||
$config['client'] = <<<EOF
|
||||
[{$tunnel->client_token}]
|
||||
type = {$tunnel->protocol}
|
||||
local_ip = {$local_addr[0]}
|
||||
local_port = {$local_addr[1]}
|
||||
EOF;
|
||||
|
||||
if ($tunnel->protocol == 'tcp' || $tunnel->protocol == 'udp') {
|
||||
$config['client'] .= PHP_EOL . 'remote_port = ' . $tunnel->remote_port;
|
||||
} elseif ($tunnel->protocol == 'http' || $tunnel->protocol == 'https') {
|
||||
$config['client'] .= PHP_EOL . 'custom_domains = ' . $tunnel->custom_domain . PHP_EOL;
|
||||
} elseif ($tunnel->server->allow_stcp || $tunnel->server->allow_xtcp || $tunnel->server->allow_sudp) {
|
||||
$uuid = Str::uuid();
|
||||
$config['client'] .= <<<EOF
|
||||
|
||||
sk = {$tunnel->sk}
|
||||
|
||||
# 以下的是对端配置文件,请不要复制或者使用!
|
||||
# 如果你想让别人通过 XTCP|STCP|SUDP 连接到你的主机,请将以下配置文件发给你信任的人。如果你不信任他人, 请勿发送, 这样会导致不信任的人也能通过 XTCP 连接到你的主机。
|
||||
# XTCP 连接不能保证稳定性, 并且也不会100%成功。
|
||||
|
||||
|
||||
#------ 对端复制开始 --------
|
||||
[common]
|
||||
server_addr = {$tunnel->server->server_address}
|
||||
server_port = {$tunnel->server->server_port}
|
||||
user = visitor_{$uuid}
|
||||
token = {$tunnel->server->token}
|
||||
[lae_visitor_{$uuid}]
|
||||
type = xtcp
|
||||
role = visitor
|
||||
server_name = lae_visitor_{$uuid}
|
||||
sk = {$tunnel->sk}
|
||||
bind_addr = {$local_addr[0]}
|
||||
bind_port = {$local_addr[1]}
|
||||
#------ 对端复制结束 --------
|
||||
# 非常感谢您的支持。如果您觉得这个项目不错,请将我们的网站分享给您的朋友。谢谢。
|
||||
EOF;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
66
app/Http/Controllers/Admin/UserController.php
Normal file
66
app/Http/Controllers/Admin/UserController.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$users = new User();
|
||||
$count = User::count();
|
||||
|
||||
foreach ($request->except(['page']) as $key => $value) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
if ($request->{$key}) {
|
||||
$users = $users->where($key, 'LIKE', '%'.$value.'%');
|
||||
}
|
||||
}
|
||||
|
||||
$count = $users->count();
|
||||
|
||||
$users = $users->simplePaginate(100);
|
||||
|
||||
return view('admin.users.index', ['users' => $users, 'count' => $count]);
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user)
|
||||
{
|
||||
$user->update($request->all());
|
||||
|
||||
// if not ajax
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => '更新成功',
|
||||
]);
|
||||
} else {
|
||||
return redirect()->route('users.index');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param User $user
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
{
|
||||
$user->delete();
|
||||
|
||||
return back()->with('success', '删除成功');
|
||||
}
|
||||
}
|
@ -40,15 +40,19 @@ public function handler(Request $request, $key)
|
||||
return $this->failed('找不到隧道。');
|
||||
}
|
||||
|
||||
switch ($host->status) {
|
||||
case 'stopped':
|
||||
return $this->failed('隧道已停止。');
|
||||
case 'error':
|
||||
return $this->failed('隧道出错。');
|
||||
case 'suspended':
|
||||
return $this->failed('隧道已暂停。');
|
||||
if ($host->locked_reason) {
|
||||
return $this->failed('隧道被锁定,原因是' . $host->locked_reason . '。');
|
||||
}
|
||||
|
||||
// switch ($host->status) {
|
||||
// case 'stopped':
|
||||
// return $this->failed('隧道已停止。');
|
||||
// case 'error':
|
||||
// return $this->failed('隧道出错。');
|
||||
// case 'suspended':
|
||||
// return $this->failed('隧道已暂停。');
|
||||
// }
|
||||
|
||||
if ($request->input('content')['proxy_type'] !== $host->protocol) {
|
||||
return $this->failed('不允许的隧道协议。');
|
||||
}
|
||||
@ -69,14 +73,11 @@ public function handler(Request $request, $key)
|
||||
}
|
||||
|
||||
// cache
|
||||
// $cache_key = 'frp_user_' . $request->content['proxy_name'];
|
||||
// Cache::put($cache_key, $host->user_id);
|
||||
|
||||
$cache_key = 'frpTunnel_data_' . $host->client_token;
|
||||
Cache::put($cache_key, ['status' => 'online']);
|
||||
|
||||
// $host->run_id = $request->input('content')['user']['run_id'];
|
||||
// $host->saveQuietly();
|
||||
$host->run_id = $request->input('content')['user']['run_id'];
|
||||
$host->saveQuietly();
|
||||
|
||||
// $data = [
|
||||
// 'message' => '隧道 ' . $host->name . ' 已启动。',
|
||||
|
@ -147,12 +147,19 @@ public function store(Request $request)
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(TunnelRequest $request, Tunnel $tunnel)
|
||||
public function show(TunnelRequest $tunnelRequest, Tunnel $tunnel)
|
||||
{
|
||||
unset($request);
|
||||
unset($tunnelRequest);
|
||||
$tunnel['config'] = $tunnel->getConfig();
|
||||
return $this->success($tunnel);
|
||||
}
|
||||
|
||||
public function close(TunnelRequest $tunnelRequest, Tunnel $tunnel) {
|
||||
unset($tunnelRequest);
|
||||
$tunnel->close();
|
||||
return $this->noContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
|
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Admin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure(Request): (Response) $next
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!$request->user('sanctum')?->isAdmin()) {
|
||||
return response()->json([
|
||||
'message' => 'You are not authorized to access this resource.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
35
app/Jobs/CheckServer.php
Normal file
35
app/Jobs/CheckServer.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CheckServer implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
151
app/Jobs/Cost.php
Normal file
151
app/Jobs/Cost.php
Normal file
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Host;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Cost implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private $http;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_old()
|
||||
{
|
||||
$this->http = Http::remote('remote')->asForm();
|
||||
|
||||
Server::with('hosts')->where('status', 'up')->whereNot('price_per_gb', 0)->chunk(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
// $ServerCheckJob = new ServerCheckJob($server->id);
|
||||
// $ServerCheckJob->handle();
|
||||
|
||||
foreach ($server->hosts as $host) {
|
||||
$host->load('user');
|
||||
|
||||
Log::debug('------------');
|
||||
Log::debug('主机: ' . $host->name);
|
||||
Log::debug('属于用户: ' . $host->user->name);
|
||||
|
||||
$cache_key = 'frpTunnel_data_' . $host->client_token;
|
||||
// $tunnel = 'frp_user_' . $host->client_token;
|
||||
// $tunnel_user_id = Cache::get($tunnel);
|
||||
$tunnel_data = Cache::get($cache_key, null);
|
||||
|
||||
if (!is_null($tunnel_data)) {
|
||||
$traffic = ($tunnel_data['today_traffic_in'] ?? 0) + ($tunnel_data['today_traffic_out'] ?? 0);
|
||||
|
||||
// $traffic = 1073741824 * 10;
|
||||
|
||||
Log::debug('本次使用的流量: ' . round($traffic / 1024 / 1024 / 1024, 2) ?? 0);
|
||||
|
||||
$day = date('d');
|
||||
|
||||
$traffic_key = 'traffic_day_' . $day . '_used_' . $host->id;
|
||||
|
||||
$used_traffic = Cache::get($traffic_key, 0);
|
||||
if ($used_traffic !== $traffic) {
|
||||
// 保存 2 天
|
||||
Cache::put($traffic_key, $traffic, 86400);
|
||||
|
||||
$used_traffic_gb = round($used_traffic / 1024 / 1024 / 1024, 2);
|
||||
|
||||
// Log::debug('上次使用的流量: ' . $used_traffic);
|
||||
Log::debug('上次使用的流量 GB: ' . $used_traffic_gb);
|
||||
|
||||
$used_traffic = $traffic - $used_traffic;
|
||||
|
||||
Log::debug('流量差值: ' . round($used_traffic / 1024 / 1024 / 1024, 2) . ' GB');
|
||||
}
|
||||
|
||||
$left_traffic = 0;
|
||||
|
||||
if ($host->user->free_traffic > 0) {
|
||||
Log::debug('开始扣除免费流量时的 used_traffic: ' . round($used_traffic / 1024 / 1024 / 1024, 2));
|
||||
|
||||
$user_free_traffic = round($host->user->free_traffic * 1024 * 1024 * 1024, 2);
|
||||
|
||||
Log::debug('用户免费流量: ' . round($user_free_traffic / 1024 / 1024 / 1024, 2));
|
||||
|
||||
// $used_traffic -= $user_free_traffic;
|
||||
// $used_traffic = abs($used_traffic);
|
||||
|
||||
Log::debug('扣除免费流量时的 used_traffic: ' . $used_traffic / 1024 / 1024 / 1024);
|
||||
|
||||
// 获取剩余
|
||||
$left_traffic = $user_free_traffic - $used_traffic;
|
||||
|
||||
Log::debug('计算后剩余的免费流量: ' . $left_traffic / 1024 / 1024 / 1024);
|
||||
|
||||
// 保存
|
||||
|
||||
if ($left_traffic < 0) {
|
||||
$left_traffic = 0;
|
||||
}
|
||||
|
||||
// 保留两位小数
|
||||
$left_traffic = round($left_traffic / 1024 / 1024 / 1024, 2);
|
||||
|
||||
$host->user->free_traffic = $left_traffic;
|
||||
$host->user->save();
|
||||
}
|
||||
|
||||
$used_traffic = abs($used_traffic);
|
||||
|
||||
Log::debug('实际用量:' . $used_traffic / 1024 / 1024 / 1024);
|
||||
|
||||
// $used_traffic -= $server->free_traffic * 1024 * 1024 * 1024;
|
||||
// // $used_traffic = abs($used_traffic);
|
||||
|
||||
// Log::debug('服务器免费流量: ' . $server->free_traffic * 1024 * 1024 * 1024);
|
||||
|
||||
// Log::debug('使用的流量(减去服务器免费流量): ' . $used_traffic);
|
||||
|
||||
if ($used_traffic > 0 && $left_traffic == 0) {
|
||||
Log::debug('此时 used_traffic: ' . $used_traffic);
|
||||
|
||||
// 要计费的流量
|
||||
$traffic = round($used_traffic / (1024 * 1024 * 1024), 2) ?? 0;
|
||||
|
||||
$traffic = abs($traffic);
|
||||
|
||||
$gb = round($traffic, 2);
|
||||
|
||||
// 计算价格
|
||||
$cost = $traffic * $host->server->price_per_gb;
|
||||
$cost = abs($cost);
|
||||
|
||||
// 记录到日志
|
||||
// if local
|
||||
// if (config('app.env') == 'local') {
|
||||
Log::debug('计费:' . $host->server->name . ' ' . $host->name . ' ' . $gb . 'GB ' . $cost . ' 的 CNY 消耗');
|
||||
// }
|
||||
|
||||
// 如果计费金额大于 0,则扣费
|
||||
if ($cost > 0) {
|
||||
// 发送扣费请求
|
||||
$this->http->post('hosts/' . $host->host_id . '/cost', [
|
||||
'amount' => $cost,
|
||||
'description' => $host->name . ' 的 ' . $gb . ' GB 流量费用。',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
61
app/Jobs/ServerCheckJob.php
Normal file
61
app/Jobs/ServerCheckJob.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Http\Controllers\Admin\ServerController;
|
||||
use App\Models\Server;
|
||||
use App\Support\Frp;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ServerCheckJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$frpServer = Server::find($this->id);
|
||||
if (!is_null($frpServer)) {
|
||||
// $frp = new FrpController($this->id);
|
||||
$s = new ServerController();
|
||||
$s->scanTunnel($frpServer->id);
|
||||
$frpController = new Frp($frpServer);
|
||||
$meta = $frpController->serverInfo();
|
||||
|
||||
if (!$meta) {
|
||||
$meta = [
|
||||
'status' => 'failed',
|
||||
];
|
||||
echo '服务器不可用: ' . $frpServer->name . ' failed' . PHP_EOL;
|
||||
} else {
|
||||
echo 'ServerCheckJob: ' . $frpServer->name . PHP_EOL;
|
||||
}
|
||||
|
||||
$data = $frpServer->toArray();
|
||||
$data['meta'] = $meta;
|
||||
|
||||
Cache::put('serverinfo_' . $frpServer->id, $data, 300);
|
||||
}
|
||||
}
|
||||
}
|
40
app/Jobs/StatusJob.php
Normal file
40
app/Jobs/StatusJob.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class StatusJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $host_id;
|
||||
|
||||
public array $requests;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($host_id, $requests)
|
||||
{
|
||||
$this->host_id = $host_id;
|
||||
$this->requests = $requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Http::remote()->asForm()->patch('hosts/'.$this->host_id, $this->requests);
|
||||
}
|
||||
}
|
46
app/Jobs/StopAllHostJob.php
Normal file
46
app/Jobs/StopAllHostJob.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Host;
|
||||
use App\Models\Tunnel;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class StopAllHostJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(int $user_id)
|
||||
{
|
||||
//
|
||||
$this->user_id = $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$hosts = Tunnel::where('user_id', $this->user_id);
|
||||
|
||||
$hosts->chunk(100, function () use ($hosts) {
|
||||
foreach ($hosts as $host) {
|
||||
$host->status = 'stopped';
|
||||
$host->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ActiveTunnel extends Model
|
||||
{
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
@ -7,13 +7,16 @@
|
||||
|
||||
class Server extends Model
|
||||
{
|
||||
public $hidden = [
|
||||
protected $hidden = [
|
||||
'dashboard_password',
|
||||
'dashboard_user',
|
||||
'dashboard_port'
|
||||
'dashboard_port',
|
||||
'key'
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'key',
|
||||
'server_address',
|
||||
'server_port',
|
||||
'token',
|
||||
@ -31,6 +34,7 @@ class Server extends Model
|
||||
'max_port',
|
||||
'max_tunnels',
|
||||
'is_china_mainland',
|
||||
'status'
|
||||
];
|
||||
|
||||
// tunnels
|
||||
|
@ -2,7 +2,12 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Frp;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Http\Controllers\Admin\ServerController;
|
||||
use App\Http\Controllers\Admin\TunnelController;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Tunnel extends Model
|
||||
@ -18,6 +23,8 @@ class Tunnel extends Model
|
||||
'status',
|
||||
'server_id',
|
||||
'user_id',
|
||||
'locked_reason',
|
||||
'run_id'
|
||||
];
|
||||
|
||||
protected $with = [
|
||||
@ -27,6 +34,55 @@ class Tunnel extends Model
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function getConfig()
|
||||
{
|
||||
return (new TunnelController)->generateConfig($this);
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->run_id) {
|
||||
$frp = new Frp($this->server);
|
||||
$closed = $frp->close($this->run_id);
|
||||
|
||||
if ($closed) {
|
||||
$cache_key = 'frpTunnel_data_' . $this->client_token;
|
||||
Cache::forget($cache_key);
|
||||
|
||||
$this->run_id = null;
|
||||
$this->saveQuietly();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function (self $tunnel) {
|
||||
$tunnel->client_token = Str::random(18);
|
||||
});
|
||||
|
||||
static::updated(function (self $tunnel) {
|
||||
if ($tunnel->locked_reason) {
|
||||
$tunnel->close();
|
||||
}
|
||||
});
|
||||
|
||||
static::deleted(function (self $tunnel) {
|
||||
$tunnel->close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
require_once app_path() . '/Helpers.php';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,8 +31,14 @@ public function boot(): void
|
||||
->prefix('api')
|
||||
->group(base_path('routes/api.php'));
|
||||
|
||||
Route::middleware(['web', 'auth:admin'])
|
||||
->as('admin.')
|
||||
->prefix('admin')
|
||||
->group(base_path('routes/admin.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->group(base_path('routes/web.php'));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -11,12 +11,12 @@ class Frp
|
||||
{
|
||||
public string|int $id;
|
||||
|
||||
protected Server $frpServer;
|
||||
protected Server $server;
|
||||
|
||||
public function __construct($id)
|
||||
public function __construct(Server $server)
|
||||
{
|
||||
$this->frpServer = (new Server)->find($id);
|
||||
$this->id = $id;
|
||||
$this->server = $server;
|
||||
$this->id = $server->id;
|
||||
}
|
||||
|
||||
public function serverInfo()
|
||||
@ -48,22 +48,22 @@ protected function cache($key, $path = null)
|
||||
|
||||
protected function get($url)
|
||||
{
|
||||
$addr = 'http://' . $this->frpServer->server_address . ':' . $this->frpServer->dashboard_port . '/api' . $url;
|
||||
$addr = 'http://' . $this->server->server_address . ':' . $this->server->dashboard_port . '/api' . $url;
|
||||
try {
|
||||
$resp = Http::timeout(3)->withBasicAuth($this->frpServer->dashboard_user, $this->frpServer->dashboard_password)->get($addr)->json() ?? [];
|
||||
$resp = Http::timeout(3)->withBasicAuth($this->server->dashboard_user, $this->server->dashboard_password)->get($addr)->json() ?? [];
|
||||
|
||||
// if under maintenance
|
||||
|
||||
if ($this->frpServer->status !== 'maintenance') {
|
||||
if ($this->frpServer->status !== 'up') {
|
||||
$this->frpServer->status = 'up';
|
||||
if ($this->server->status !== 'maintenance') {
|
||||
if ($this->server->status !== 'up') {
|
||||
$this->server->status = 'up';
|
||||
}
|
||||
}
|
||||
} catch (Exception) {
|
||||
$this->frpServer->status = 'down';
|
||||
$this->server->status = 'down';
|
||||
$resp = false;
|
||||
} finally {
|
||||
$this->frpServer->save();
|
||||
$this->server->save();
|
||||
}
|
||||
|
||||
return $resp;
|
||||
|
18
app/View/Components/AppLayout.php
Normal file
18
app/View/Components/AppLayout.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('layouts.app');
|
||||
}
|
||||
}
|
18
app/View/Components/GuestLayout.php
Normal file
18
app/View/Components/GuestLayout.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class GuestLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('layouts.guest');
|
||||
}
|
||||
}
|
28
app/View/Components/Menu.php
Normal file
28
app/View/Components/Menu.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Menu extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View|\Closure|string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('components.menu');
|
||||
}
|
||||
}
|
33
app/View/Components/ServerView.php
Normal file
33
app/View/Components/ServerView.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ServerView extends Component
|
||||
{
|
||||
public $server;
|
||||
|
||||
public $url;
|
||||
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($server, $url)
|
||||
{
|
||||
$this->server = $server;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View|\Closure|string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('components.server-view');
|
||||
}
|
||||
}
|
@ -40,6 +40,11 @@
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'admin' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'admins',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
@ -65,6 +70,11 @@
|
||||
'model' => App\Models\User::class,
|
||||
],
|
||||
|
||||
'admins' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => App\Models\Admin::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
|
@ -17,7 +17,6 @@ public function up(): void
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->boolean('is_admin')->default(false)->index();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('admins', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('admins');
|
||||
}
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->string('key')->nullable()->index()->after('id');
|
||||
});
|
||||
|
||||
Server::all()->each(function (Server $server) {
|
||||
$server->key = $server->id;
|
||||
$server->save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('key');
|
||||
});
|
||||
}
|
||||
};
|
@ -16,6 +16,8 @@ public function up(): void
|
||||
|
||||
$table->string('name')->index();
|
||||
|
||||
$table->string('client_token')->index();
|
||||
|
||||
$table->char('protocol', 5)->index()->default('tcp');
|
||||
|
||||
$table->string('custom_domain')->nullable()->index();
|
||||
@ -38,6 +40,9 @@ public function up(): void
|
||||
// use_compression
|
||||
$table->boolean('use_compression')->default(false)->index();
|
||||
|
||||
$table->string('run_id')->nullable()->index();
|
||||
|
||||
$table->string('locked_reason')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
|
@ -14,6 +14,8 @@
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"bootstrap": "5.3.0-alpha1",
|
||||
"bootstrap-icons": "^1.10.3",
|
||||
"echarts": "^5.4.2",
|
||||
"humanize-plus": "^1.8.2",
|
||||
"vue": "^3.2.36",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-router": "^4.0.13"
|
||||
|
@ -8,9 +8,6 @@
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb-8 {
|
||||
|
@ -25,10 +25,10 @@ const items = ref([
|
||||
name: "隧道",
|
||||
route: "tunnels",
|
||||
},
|
||||
{
|
||||
name: "服务器列表",
|
||||
route: "servers",
|
||||
},
|
||||
// {
|
||||
// name: "服务器列表",
|
||||
// route: "servers",
|
||||
// },
|
||||
{
|
||||
name: "创建隧道",
|
||||
route: "tunnels.create",
|
||||
@ -36,14 +36,14 @@ const items = ref([
|
||||
]);
|
||||
|
||||
// 如果是管理员
|
||||
if (window.Base.User.is_admin) {
|
||||
// items.value.push({
|
||||
// name: "服务器",
|
||||
// route: "servers",
|
||||
// });
|
||||
items.value.push({
|
||||
name: "创建服务器",
|
||||
route: "servers.create",
|
||||
});
|
||||
}
|
||||
// if (window.Base.User.is_admin) {
|
||||
// // items.value.push({
|
||||
// // name: "服务器",
|
||||
// // route: "servers",
|
||||
// // });
|
||||
// items.value.push({
|
||||
// name: "创建服务器",
|
||||
// route: "servers.create",
|
||||
// });
|
||||
// }
|
||||
</script>
|
||||
|
@ -20,10 +20,14 @@
|
||||
<tr v-for="tunnel in tunnels">
|
||||
<th>1</th>
|
||||
<td>
|
||||
<router-link :to="{name: 'tunnels.show', params: {id: tunnel.id}}">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'tunnels.show',
|
||||
params: { id: tunnel.id },
|
||||
}"
|
||||
>
|
||||
{{ tunnel.name }}
|
||||
</router-link>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{{ tunnel.protocol.toString().toUpperCase() }}
|
||||
@ -33,7 +37,19 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ tunnel.server.server_address }}{{ tunnel.remote_port }}
|
||||
<span
|
||||
v-if="
|
||||
tunnel.protocol === 'http' ||
|
||||
tunnel.protocol === 'https'
|
||||
"
|
||||
>
|
||||
{{ tunnel.custom_domain }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ tunnel.server.server_address }}:{{
|
||||
tunnel.remote_port
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>0</td>
|
||||
@ -43,7 +59,8 @@
|
||||
<td>{{ tunnel.server.name }}</td>
|
||||
|
||||
<td>
|
||||
<span class="text-danger">离线</span>
|
||||
<span class="text-success" v-if="tunnel.run_id">在线</span>
|
||||
<span class="text-danger" v-else="tunnel.run_id">离线</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -51,26 +68,25 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import { ref } from "vue";
|
||||
import http from "../../plugins/http";
|
||||
|
||||
|
||||
const tunnels = ref([
|
||||
{
|
||||
id: '0',
|
||||
protocol: '',
|
||||
id: "0",
|
||||
protocol: "",
|
||||
server: {
|
||||
server_address: '',
|
||||
server_port: '',
|
||||
name: '',
|
||||
}
|
||||
}
|
||||
])
|
||||
server_address: "",
|
||||
server_port: "",
|
||||
name: "",
|
||||
},
|
||||
run_id: ""
|
||||
},
|
||||
]);
|
||||
|
||||
http.get('tunnels').then((res) => {
|
||||
tunnels.value = res.data
|
||||
|
||||
console.log(tunnels.value)
|
||||
})
|
||||
http.get("tunnels").then((res) => {
|
||||
tunnels.value = res.data;
|
||||
|
||||
console.log(tunnels.value);
|
||||
});
|
||||
</script>
|
||||
|
@ -2,38 +2,243 @@
|
||||
<div>
|
||||
<h2>隧道: {{ tunnel.name }}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-show="chart" id="chart" style="height: 400px"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>配置文件</h2>
|
||||
<pre
|
||||
>{{ tunnel.config.server }}
|
||||
|
||||
{{ tunnel.config.client }}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="tunnel.run_id" class="mb-3">
|
||||
<h2>强制下线</h2>
|
||||
<button class="btn btn-primary" @click="kickTunnel()">强制下线</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>删除</h2>
|
||||
<button class="btn btn-primary" @click="deleteTunnel()">
|
||||
删除隧道
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import http from "../../plugins/http";
|
||||
import router from "../../plugins/router";
|
||||
import * as echarts from "echarts";
|
||||
const showChart = ref(false);
|
||||
let chart = undefined;
|
||||
|
||||
let tunnel_id = router.currentRoute.value.params.id
|
||||
let tunnel_id = router.currentRoute.value.params.id;
|
||||
|
||||
const tunnel = ref({
|
||||
name: '',
|
||||
protocol: '',
|
||||
local_address: '',
|
||||
remote_port: '',
|
||||
})
|
||||
|
||||
name: "",
|
||||
protocol: "",
|
||||
local_address: "",
|
||||
remote_port: "",
|
||||
config: {
|
||||
server: "",
|
||||
client: "",
|
||||
},
|
||||
run_id: "",
|
||||
});
|
||||
|
||||
function getTunnel() {
|
||||
http.get(`/tunnels/${tunnel_id}`).then(res => {
|
||||
tunnel.value = res.data
|
||||
})
|
||||
http.get(`/tunnels/${tunnel_id}`).then((res) => {
|
||||
tunnel.value = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
function updateTunnel() {
|
||||
http.put(`/tunnels/${tunnel_id}`, tunnel.value).then(res => {
|
||||
tunnel.value = res.data
|
||||
})
|
||||
http.put(`/tunnels/${tunnel_id}`, tunnel.value).then((res) => {
|
||||
tunnel.value = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(getTunnel)
|
||||
let chartOptions = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
formatter: function (data) {
|
||||
let html = "";
|
||||
if (data.length > 0) {
|
||||
html += data[0].name + "<br/>";
|
||||
}
|
||||
for (let v of data) {
|
||||
let colorEl =
|
||||
'<span style="display:inline-block;margin-right:5px;' +
|
||||
"border-radius:10px;width:9px;height:9px;background-color:" +
|
||||
v.color +
|
||||
'"></span>';
|
||||
html += `${colorEl + v.seriesName}: ${Humanize.fileSize(
|
||||
v.value
|
||||
)}<br/>`;
|
||||
}
|
||||
return html;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ["入站流量", "出站流量"],
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: "category",
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
return Humanize.fileSize(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "入站流量",
|
||||
type: "bar",
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
name: "出站流量",
|
||||
type: "bar",
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function initChart() {
|
||||
let chartDom = document.getElementById("chart");
|
||||
chart = echarts.init(chartDom, {
|
||||
backgroundColor: "transparent",
|
||||
renderer: "svg",
|
||||
});
|
||||
|
||||
chartOptions && chart.setOption(chartOptions);
|
||||
}
|
||||
|
||||
function deleteTunnel() {
|
||||
if (confirm("确定删除隧道吗?")) {
|
||||
http.delete(`/tunnels/${tunnel_id}`).then(() => {
|
||||
alert("删除成功");
|
||||
|
||||
router.push({ name: "tunnels" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function kickTunnel() {
|
||||
http.post(`/tunnels/${tunnel_id}/close`);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
http.get("/tunnels/" + tunnel_id).then((res) => {
|
||||
tunnel.value = res.data;
|
||||
// console.log(res.data)
|
||||
// tunnel.value.tunnel = res.data.tunnel
|
||||
// console.log(tunnel.value.tunnel.conf)
|
||||
// console.log(tunnel,tunnel.value,res.data);
|
||||
// console.log(res.data);
|
||||
|
||||
if (res.data.traffic) {
|
||||
if (!showChart.value) {
|
||||
// initChart()
|
||||
showChart.value = true;
|
||||
}
|
||||
|
||||
let now = new Date();
|
||||
now = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate() - 6
|
||||
);
|
||||
let dates = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
dates.push(
|
||||
now.getFullYear() +
|
||||
"-" +
|
||||
(now.getMonth() + 1) +
|
||||
"-" +
|
||||
now.getDate()
|
||||
);
|
||||
now = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate() + 1
|
||||
);
|
||||
}
|
||||
|
||||
chartOptions.xAxis[0].data = dates;
|
||||
|
||||
let trafficInArr = res.data.traffic.traffic_in;
|
||||
let trafficOutArr = res.data.traffic.traffic_out;
|
||||
|
||||
if (!trafficInArr || !trafficOutArr) {
|
||||
return;
|
||||
}
|
||||
|
||||
trafficInArr = trafficInArr.reverse();
|
||||
trafficOutArr = trafficOutArr.reverse();
|
||||
|
||||
if (chart) {
|
||||
chartOptions.series[0].data = trafficInArr;
|
||||
chartOptions.series[1].data = trafficOutArr;
|
||||
|
||||
chart.setOption(chartOptions);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
let resizeInterval = setInterval(() => {
|
||||
chart && chart.resize();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getTunnel();
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
chart && chart.resize();
|
||||
});
|
||||
});
|
||||
|
||||
const timer = setInterval(refresh, 10000);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer);
|
||||
|
||||
if (resizeInterval) {
|
||||
clearInterval(resizeInterval);
|
||||
}
|
||||
|
||||
// remove listener
|
||||
window.removeEventListener("resize", () => {
|
||||
resizeInterval = setInterval(() => {
|
||||
chart && chart.resize();
|
||||
}, 1000);
|
||||
initChart();
|
||||
chart && chart.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
10
resources/views/admin/index.blade.php
Normal file
10
resources/views/admin/index.blade.php
Normal file
@ -0,0 +1,10 @@
|
||||
<x-app-layout>
|
||||
@if (count($servers) > 0)
|
||||
<h1>不在线或维护中的服务器</h1>
|
||||
|
||||
@foreach ($servers as $server)
|
||||
<x-Server-View :server="$server" :url="route('admin.servers.edit', $server->id)" />
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
</x-app-layout>
|
19
resources/views/admin/login.blade.php
Normal file
19
resources/views/admin/login.blade.php
Normal file
@ -0,0 +1,19 @@
|
||||
<x-app-layout>
|
||||
<div>
|
||||
<h1>登录</h1>
|
||||
|
||||
<form action="{{ route('admin.login') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
{{-- email --}}
|
||||
<input type="text" name="email" placeholder="邮箱">
|
||||
{{-- password --}}
|
||||
<input type="password" name="password" placeholder="密码">
|
||||
{{-- remember --}}
|
||||
<input type="checkbox" name="remember" id="remember">
|
||||
<label for="remember">记住我</label>
|
||||
{{-- submit --}}
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
134
resources/views/admin/servers/create.blade.php
Normal file
134
resources/views/admin/servers/create.blade.php
Normal file
@ -0,0 +1,134 @@
|
||||
<x-app-layout>
|
||||
|
||||
|
||||
<form action="{{ route('admin.servers.store') }}" method="post">
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>服务器</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serverName" class="form-label">服务器名称</label>
|
||||
<input type="text" required class="form-control" id="serverName" placeholder="输入服务器名称,它将会被搜索到"
|
||||
name="name">
|
||||
</div>
|
||||
|
||||
<h3>Frps 信息</h3>
|
||||
<div class="mb-3">
|
||||
<label for="serverAddr" class="form-label">Frps 地址</label>
|
||||
<input type="text" required class="form-control" id="serverAddr" name="server_address">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serverPort" class="form-label">Frps 端口</label>
|
||||
<input type="text" required class="form-control" id="serverPort" name="server_port">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serverToken" class="form-label">Frps 令牌</label>
|
||||
<input type="text" required class="form-control" id="serverToken" name="token">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
|
||||
<h3>Frps Dashboard 配置</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dashboardPort" class="form-label">端口</label>
|
||||
<input type="text" required class="form-control" id="dashboardPort" name="dashboard_port"
|
||||
value="7500">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dashboardUser" class="form-label">登录用户名</label>
|
||||
<input type="text" required class="form-control" id="dashboardUser" name="dashboard_user"
|
||||
value="admin">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dashboardPwd" class="form-label">密码</label>
|
||||
<input type="text" required class="form-control" id="dashboardPwd" name="dashboard_password"
|
||||
value="admin">
|
||||
</div>
|
||||
|
||||
<h3>端口范围限制</h3>
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
<input type="text" required class="form-control" placeholder="最小端口,比如: 10000" name="min_port"
|
||||
value="10000">
|
||||
<input type="text" required class="form-control" placeholder="最大端口,比如: 65535" name="max_port"
|
||||
value="65535">
|
||||
</div>
|
||||
|
||||
<h3>最多隧道数量</h3>
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
<input type="text" required class="form-control" placeholder="最多隧道数量,比如:1024个隧道"
|
||||
name="max_tunnels">
|
||||
</div>
|
||||
|
||||
<h3>隧道协议限制</h3>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_http" value="1" id="allow_http">
|
||||
<label class="form-check-label" for="allow_http">
|
||||
允许 HTTP
|
||||
</label>
|
||||
<br />
|
||||
超文本传输协议
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_https" value="1" id="allow_https">
|
||||
<label class="form-check-label" for="allow_https">
|
||||
允许 HTTPS
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_tcp" value="1" id="allow_tcp">
|
||||
<label class="form-check-label" for="allow_tcp">
|
||||
允许 TCP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_udp" value="1" id="allow_udp">
|
||||
<label class="form-check-label" for="allow_udp">
|
||||
允许 UDP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_stcp" value="1"
|
||||
id="allow_stcp">
|
||||
<label class="form-check-label" for="allow_stcp">
|
||||
允许 STCP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_xtcp" value="1"
|
||||
id="allow_xtcp">
|
||||
<label class="form-check-label" for="allow_xtcp">
|
||||
允许 XTCP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p>服务器是否位于中国大陆</p>
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
{{-- checkbox --}}
|
||||
<input type="checkbox" name="is_china_mainland" value="1" id="is_china_mainland">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary mb-3">新建服务器</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</x-app-layout>
|
336
resources/views/admin/servers/edit.blade.php
Normal file
336
resources/views/admin/servers/edit.blade.php
Normal file
@ -0,0 +1,336 @@
|
||||
<x-app-layout>
|
||||
|
||||
{{-- <nav>
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<button class="nav-link active" aria-selected="true" data-bs-toggle="tab" data-bs-target="#nav-info"
|
||||
type="button" role="tab">基础信息</button>
|
||||
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-settings" type="button"
|
||||
role="tab">服务器设置</button>
|
||||
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-frps" type="button" role="tab">Frps
|
||||
配置文件</button>
|
||||
</div>
|
||||
</nav> --}}
|
||||
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<div class="tab-pane fade" id="nav-settings" role="tabpanel">
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<form action="{{ route('admin.servers.update', $server->id) }}" method="post">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<h3>服务器</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serverName" class="form-label">服务器名称</label>
|
||||
<input type="text" required value="{{ $server->name }}" class="form-control"
|
||||
id="serverName" placeholder="输入服务器名称,它将会被搜索到" name="name">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serverKey" class="form-label">鉴权 Key</label>
|
||||
<input type="text" required value="{{ $server->key }}" class="form-control"
|
||||
id="serverKey" name="key">
|
||||
</div>
|
||||
|
||||
<h3>Frps 信息</h3>
|
||||
<div class="mb-3">
|
||||
<label for="serverAddr" class="form-label">Frps 地址</label>
|
||||
<input type="text" required value="{{ $server->server_address }}" class="form-control"
|
||||
id="serverAddr" name="server_address">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serverPort" class="form-label">Frps 端口</label>
|
||||
<input type="text" required value="{{ $server->server_port }}" class="form-control"
|
||||
id="serverPort" name="server_port">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serverToken" class="form-label">Frps 令牌</label>
|
||||
<input type="text" required value="{{ $server->token }}" class="form-control"
|
||||
id="serverToken" name="token">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
|
||||
<h3>Frps Dashboard 配置</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dashboardPort" class="form-label">端口</label>
|
||||
<input type="text" required value="{{ $server->dashboard_port }}" class="form-control"
|
||||
id="dashboardPort" name="dashboard_port">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dashboardUser" class="form-label">登录用户名</label>
|
||||
<input type="text" required value="{{ $server->dashboard_user }}" class="form-control"
|
||||
id="dashboardUser" name="dashboard_user">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dashboardPwd" class="form-label">密码</label>
|
||||
<input type="text" required value="{{ $server->dashboard_password }}" class="form-control"
|
||||
id="dashboardPwd" name="dashboard_password">
|
||||
</div>
|
||||
|
||||
<h3>端口范围限制</h3>
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
<input type="text" value="{{ $server->min_port }}" required class="form-control"
|
||||
placeholder="最小端口,比如:1024" name="min_port">
|
||||
<input type="text" value="{{ $server->max_port }}" required class="form-control"
|
||||
placeholder="最大端口,比如:65535" name="max_port">
|
||||
</div>
|
||||
|
||||
<h3>最多隧道数量</h3>
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
<input type="text" value="{{ $server->max_tunnels }}" required class="form-control"
|
||||
placeholder="最多隧道数量,比如:1024个隧道" name="max_tunnels">
|
||||
</div>
|
||||
|
||||
<h3>隧道协议限制</h3>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_http" value="1" id="allow_http"
|
||||
@if ($server->allow_http) checked @endif>
|
||||
<label class="form-check-label" for="allow_http">
|
||||
允许 HTTP
|
||||
</label>
|
||||
<br />
|
||||
超文本传输协议
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_https" value="1"
|
||||
id="allow_https" @if ($server->allow_https) checked @endif>
|
||||
<label class="form-check-label" for="allow_https">
|
||||
允许 HTTPS
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_tcp" value="1"
|
||||
id="allow_tcp" @if ($server->allow_tcp) checked @endif>
|
||||
<label class="form-check-label" for="allow_tcp">
|
||||
允许 TCP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_udp" value="1"
|
||||
id="allow_udp" @if ($server->allow_udp) checked @endif>
|
||||
<label class="form-check-label" for="allow_udp">
|
||||
允许 UDP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_stcp" value="1"
|
||||
id="allow_stcp" @if ($server->allow_stcp) checked @endif>
|
||||
<label class="form-check-label" for="allow_stcp">
|
||||
允许 STCP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_xtcp" value="1"
|
||||
id="allow_xtcp" @if ($server->allow_xtcp) checked @endif>
|
||||
<label class="form-check-label" for="allow_xtcp">
|
||||
允许 XTCP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="allow_sudp" value="1"
|
||||
id="allow_sudp" @if ($server->allow_sudp) checked @endif>
|
||||
<label class="form-check-label" for="allow_sudp">
|
||||
允许 SUDP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
{{-- checkbox --}}
|
||||
<input type="checkbox" value="1" name="is_china_mainland"
|
||||
id="is_china_mainland" @if ($server->is_china_mainland) checked @endif>
|
||||
<span>服务器是否位于中国大陆</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary mb-3">保存更改</button>
|
||||
</form>
|
||||
|
||||
<form class="d-inline" action="{{ route('admin.servers.destroy', $server->id) }}" method="post">
|
||||
@method('DELETE')
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-danger mb-3"
|
||||
onclick="confirm('确定删除这个服务器吗?删除后将无法恢复,与此关联的隧道将一并删除。') ? true : event.preventDefault()">删除服务器</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane fade show active" id="nav-info" role="tabpanel">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Frps 版本</td>
|
||||
<td>{{ $serverInfo->version ?? 0 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>绑定端口</td>
|
||||
<td>{{ $serverInfo->bind_port ?? 0 }}</td>
|
||||
</tr>
|
||||
@if ($serverInfo->bind_udp_port ?? 0)
|
||||
<tr>
|
||||
<td>UDP 端口</td>
|
||||
<td>{{ $serverInfo->bind_udp_port ?? 0 }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
<tr>
|
||||
<td>HTTP 端口</td>
|
||||
<td>{{ $serverInfo->vhost_http_port ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>HTTPS 端口</td>
|
||||
<td>{{ $serverInfo->vhost_https_port ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>KCP 端口</td>
|
||||
<td>{{ $serverInfo->kcp_bind_port ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
@if (!empty($serverInfo->subdomain_host))
|
||||
<tr>
|
||||
<td>子域名</td>
|
||||
<td>{{ $serverInfo->subdomain_host ?? 0 }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
<tr>
|
||||
<td>Max PoolCount</td>
|
||||
<td>{{ $serverInfo->max_pool_count ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Max Ports Peer Client</td>
|
||||
<td>{{ $serverInfo->max_ports_per_client ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Heartbeat timeout</td>
|
||||
<td>{{ $serverInfo->heart_beat_timeout ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>自启动以来总入流量</td>
|
||||
<td>{{ unitConversion($serverInfo->total_traffic_in ?? 0) }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>自启动以来总出流量</td>
|
||||
<td>{{ unitConversion($serverInfo->total_traffic_out ?? 0) }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>客户端数量</td>
|
||||
<td>{{ $serverInfo->client_counts ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>当前连接数量</td>
|
||||
<td>{{ $serverInfo->cur_conns ?? 0 }}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($server->status == 'down')
|
||||
<span style="color: red">无法连接到服务器。</span>
|
||||
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" name="status" value="up" />
|
||||
<button type="submit">强制标记为在线</button>
|
||||
</form>
|
||||
@else
|
||||
<span style="color: green">正常</span>
|
||||
|
||||
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" name="status" value="down" />
|
||||
<button type="submit">标记为离线</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($server->status == 'maintenance')
|
||||
<span style="color: red">维护中</span>
|
||||
|
||||
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" name="status" value="down" />
|
||||
<button type="submit">取消维护</button>
|
||||
</form>
|
||||
@else
|
||||
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" name="status" value="maintenance" />
|
||||
<button type="submit">开始维护</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.servers.destroy', $server->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit">删除</button>
|
||||
</form>
|
||||
|
||||
<div class="tab-pane fade" id="nav-frps" role="tabpanel">
|
||||
<textarea readonly class="form-control" rows="20" cols="80">[common]
|
||||
bind_port = {{ $server->server_port }}
|
||||
bind_udp_port = {{ $server->server_port }}
|
||||
@if ($server->server_port + 1 > 65535)
|
||||
kcp_bind_port = {{ $server->server_port - 1 }}
|
||||
@else
|
||||
kcp_bind_port = {{ $server->server_port + 1 }}
|
||||
@endif
|
||||
token = {{ $server->token }}
|
||||
@if ($server->allow_http)
|
||||
vhost_http_port = 80
|
||||
@endif
|
||||
@if ($server->allow_https)
|
||||
vhost_https_port = 443
|
||||
@endif
|
||||
|
||||
dashboard_port = {{ $server->dashboard_port }}
|
||||
dashboard_user = {{ $server->dashboard_user }}
|
||||
dashboard_pwd = {{ $server->dashboard_password }}
|
||||
|
||||
[plugin.port-manager]
|
||||
addr = {{ route('api.tunnel.handler', '') }}/
|
||||
path = {{ $server->key }}
|
||||
ops = NewProxy
|
||||
|
||||
</textarea>
|
||||
将这些文件放入: frps.ini
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-app-layout>
|
10
resources/views/admin/servers/index.blade.php
Normal file
10
resources/views/admin/servers/index.blade.php
Normal file
@ -0,0 +1,10 @@
|
||||
<x-app-layout>
|
||||
|
||||
<a href="{{ route('admin.servers.create') }}">添加 Frps 服务器</a>
|
||||
<div class="list-group mt-3">
|
||||
@foreach ($servers as $server)
|
||||
<x-Server-View :server="$server" :url="route('admin.servers.edit', $server->id)" />
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</x-app-layout>
|
100
resources/views/admin/servers/show.blade.php
Normal file
100
resources/views/admin/servers/show.blade.php
Normal file
@ -0,0 +1,100 @@
|
||||
<x-app-layout>
|
||||
|
||||
@php($user = auth()->user())
|
||||
|
||||
<h3>{{ $server->name }}</h3>
|
||||
|
||||
<a href="{{ route('servers.edit', $server->id) }}">编辑服务器</a>
|
||||
|
||||
<p>
|
||||
服务器地址: {{ $server->server_address }} <br />
|
||||
允许的协议列表: <br />
|
||||
{{ $server->allow_http ? 'HTTP' : ' ' }}
|
||||
{{ $server->allow_https ? 'HTTPS' : ' ' }}
|
||||
{{ $server->allow_tcp ? 'TCP' : ' ' }}
|
||||
{{ $server->allow_udp ? 'UDP' : ' ' }}
|
||||
{{ $server->allow_stcp ? 'STCP' : ' ' }}
|
||||
{{ $server->allow_xtcp ? 'XTCP' : ' ' }}
|
||||
</p>
|
||||
|
||||
<p>端口号范围: {{ $server->min_port }} ~ {{ $server->max_port }} </p>
|
||||
<p>隧道数量: {{ $server->tunnels }} / {{ $server->max_tunnels }} </p>
|
||||
|
||||
<p>客户端数量:{{ $serverInfo->client_counts ?? 0 }},连接数:{{ $serverInfo->cur_conns ?? 0 }},进站流量:{{ unitConversion($serverInfo->total_traffic_in ?? 0) }},出站流量:{{ unitConversion($serverInfo->total_traffic_out ?? 0) }},
|
||||
{{ $serverInfo->version ?? '离线' }}。
|
||||
</p>
|
||||
|
||||
{{--
|
||||
<h3>使用这个服务器创建隧道</h3>
|
||||
|
||||
<form action="{{ route('tunnels.store') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="server_id" value="{{ $server->id }}" />
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" name="name">
|
||||
<label>隧道名称</label>
|
||||
</div>
|
||||
|
||||
<select class="form-select" name="protocol" id="protocol">
|
||||
<option selected>选择协议</option>
|
||||
<option value="http" @if (!$user->verified_at) disabled @endif>HTTP</option>
|
||||
<option value="https" @if (!$user->verified_at) disabled @endif>HTTPS</option>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="stcp">STCP</option>
|
||||
</select>
|
||||
|
||||
<div class="form-floating mb-3 mt-3">
|
||||
<input type="text" class="form-control" name="local_address">
|
||||
<label>本地地址</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3 hidden" id="remote">
|
||||
<input type="text" class="form-control" name="remote_port">
|
||||
<label>远程端口</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3 hidden" id="domain">
|
||||
<input type="text" class="form-control" name="custom_domain">
|
||||
<label>域名</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3 hidden" id="sk">
|
||||
<input type="text" class="form-control" name="sk">
|
||||
<label>STCP 密钥</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">创建</button>
|
||||
|
||||
</form> --}}
|
||||
{{--
|
||||
<script>
|
||||
const protocol = document.getElementById('protocol');
|
||||
protocol.addEventListener('change', () => {
|
||||
let val = protocol.value;
|
||||
|
||||
function hide(id) {
|
||||
document.getElementById(id).style.display = 'none';
|
||||
}
|
||||
|
||||
function show(id) {
|
||||
document.getElementById(id).style.display = 'block';
|
||||
}
|
||||
|
||||
if (val == 'http' || val == 'https') {
|
||||
hide('sk')
|
||||
hide('remote')
|
||||
show('domain')
|
||||
} else if (val == 'tcp' || val == 'udp') {
|
||||
hide('sk')
|
||||
hide('domain')
|
||||
show('remote')
|
||||
} else if (val == 'stcp') {
|
||||
hide('sk')
|
||||
hide('domain')
|
||||
show('sk')
|
||||
}
|
||||
})
|
||||
</script> --}}
|
||||
|
||||
</x-app-layout>
|
103
resources/views/admin/tunnels/index.blade.php
Normal file
103
resources/views/admin/tunnels/index.blade.php
Normal file
@ -0,0 +1,103 @@
|
||||
<x-app-layout>
|
||||
<h1>隧道</h1>
|
||||
|
||||
<form name="filter">
|
||||
Host ID: <input type="text" name="host_id" value="{{ Request::get('host_id') }}" />
|
||||
名称: <input type="text" name="name" value="{{ Request::get('name') }}" />
|
||||
|
||||
<select name="protocol">
|
||||
<option value="">协议</option>
|
||||
<option value="http" @if (Request::get('protocol') == 'http') selected @endif>HTTP</option>
|
||||
<option value="https" @if (Request::get('protocol') == 'https') selected @endif>HTTPS</option>
|
||||
<option value="tcp" @if (Request::get('protocol') == 'tcp') selected @endif>TCP</option>
|
||||
<option value="udp" @if (Request::get('protocol') == 'udp') selected @endif>UDP</option>
|
||||
<option value="xtcp" @if (Request::get('protocol') == 'xtcp') selected @endif>XTCP</option>
|
||||
<option value="stcp" @if (Request::get('protocol') == 'stcp') selected @endif>STCP</option>
|
||||
<option value="sudp" @if (Request::get('protocol') == 'sudp') selected @endif>SUDP</option>
|
||||
</select>
|
||||
|
||||
|
||||
|
||||
<button type="submit">筛选</button>
|
||||
</form>
|
||||
|
||||
<p>总计: {{ $count }}</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>客户</th>
|
||||
<th>状态</th>
|
||||
<th scope="col">协议</th>
|
||||
<th scope="col">本地地址</th>
|
||||
<th scope="col">远程端口/域名</th>
|
||||
<th scope="col">连接数</th>
|
||||
<th scope="col">下载流量</th>
|
||||
<th scope="col">上载流量</th>
|
||||
<th scope="col">服务器</th>
|
||||
<th scope="col">隧道状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
||||
<tbody>
|
||||
@foreach ($hosts as $host)
|
||||
<tr>
|
||||
<td>{{ $host->host_id }}</td>
|
||||
<td>{{ $host->name }}</td>
|
||||
<td>{{ $host->user->name }}</td>
|
||||
<td>{{ $host->status }}</td>
|
||||
@php($cache = Cache::get('frpTunnel_data_' . $host->client_token, ['status' => 'offline']))
|
||||
<td>{{ strtoupper($host->protocol) }}</td>
|
||||
<td>{{ $host->local_address }}</td>
|
||||
|
||||
@if ($host->protocol == 'http' || $host->protocol == 'https')
|
||||
<td>{{ $host->custom_domain }}</td>
|
||||
@else
|
||||
<td>{{ $host->server->server_address . ':' . $host->remote_port }}</td>
|
||||
@endif
|
||||
|
||||
<td>{{ $cache['cur_conns'] ?? 0 }}</td>
|
||||
<td>{{ unitConversion($cache['today_traffic_in'] ?? 0) }}</td>
|
||||
<td>{{ unitConversion($cache['today_traffic_out'] ?? 0) }}</td>
|
||||
|
||||
<td><a href="{{ route('admin.servers.show', $host->server->id) }}">{{ $host->server->name }}</a></td>
|
||||
|
||||
<td>
|
||||
@if($host->locked_reason)
|
||||
<span class="text-danger">被锁定,因为 {{ $host->locked_reason }}</span>
|
||||
<br />
|
||||
@endif
|
||||
@if ($cache['status'] === 'online')
|
||||
<span class="text-success">在线</span>
|
||||
@else
|
||||
<span class="text-danger">离线</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td>{{ $host->created_at }}</td>
|
||||
<td>{{ $host->updated_at }}</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.tunnels.show', ['tunnel' => $host]) }}">编辑</a>
|
||||
|
||||
<form action="{{ route('admin.tunnels.destroy', ['tunnel' => $host]) }}" method="POST"
|
||||
onsubmit="return confirm('真的要删除吗?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{{ $hosts->links() }}
|
||||
</x-app-layout>
|
88
resources/views/admin/tunnels/show.blade.php
Normal file
88
resources/views/admin/tunnels/show.blade.php
Normal file
@ -0,0 +1,88 @@
|
||||
<x-app-layout>
|
||||
|
||||
@php($cache = Cache::get('frpTunnel_data_' . $tunnel->client_token, []))
|
||||
|
||||
<p> 隧道状态:
|
||||
@if ($cache['status'] ?? false === 'online')
|
||||
<span style="color: green">在线</span>
|
||||
@else
|
||||
<span style="color: red">离线</span>
|
||||
@endif
|
||||
</p>
|
||||
|
||||
<p> 连接数:{{ $cache['cur_conns'] ?? 0 }}</p>
|
||||
<p> 下载流量:{{ unitConversion($cache['today_traffic_in'] ?? 0) }}</p>
|
||||
<p> 上载流量:{{ unitConversion($cache['today_traffic_out'] ?? 0) }}</p>
|
||||
|
||||
<hr />
|
||||
<p>如果填写锁定原因,隧道将会立即下线,并且客户端无法登录。</p>
|
||||
<form action="{{ route('admin.tunnels.update', $tunnel) }}" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="text" name="locked_reason" @if($tunnel->locked_reason) value="{{$tunnel->locked_reason}}" @endif placeholder="留空解除" />
|
||||
<button type="submit">确定</button>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
{{-- @if ($host->protocol == 'http' || $host->protocol == 'https')
|
||||
<h3>网页截图</h3>
|
||||
<img src="" />
|
||||
@endif --}}
|
||||
|
||||
<h3>配置文件</h3>
|
||||
<textarea id="config" cols="80" rows="20" readonly="readonly"></textarea>
|
||||
<script>
|
||||
let tunnel_config = {!! $tunnel !!}
|
||||
// let put_config()
|
||||
function put_config() {
|
||||
let local_addr = tunnel_config.local_address.split(':')
|
||||
let config = `[common]
|
||||
server_addr = ${tunnel_config.server.server_address}
|
||||
server_port = ${tunnel_config.server.server_port}
|
||||
token = ${tunnel_config.server.token}
|
||||
|
||||
# ${tunnel_config.name} 于服务器 ${tunnel_config.server.name}
|
||||
[${tunnel_config.client_token}]
|
||||
type = ${tunnel_config.protocol}
|
||||
local_ip = ${local_addr[0]}
|
||||
local_port = ${local_addr[1]}
|
||||
`;
|
||||
|
||||
if (tunnel_config.protocol == 'tcp' || tunnel_config.protocol == 'udp') {
|
||||
config += `remote_port = ${tunnel_config.remote_port}
|
||||
|
||||
`;
|
||||
} else if (tunnel_config.protocol == 'http' || tunnel_config.protocol == 'https') {
|
||||
config += `custom_domains = ${tunnel_config.custom_domain}
|
||||
`;
|
||||
} else if (tunnel_config.protocol == 'stcp') {
|
||||
let random = Math.floor(Math.random() * 50);
|
||||
config += `sk = ${tunnel_config.sk}
|
||||
|
||||
#------ Visitor config file --------
|
||||
[common]
|
||||
server_addr = ${tunnel_config.server.server_address}
|
||||
server_port = ${tunnel_config.server.server_port}
|
||||
user = client
|
||||
token = ${tunnel_config.server.token}
|
||||
|
||||
[client_visitor_${random}]
|
||||
type = stcp
|
||||
role = visitor
|
||||
server_name = ${tunnel_config.client_token}
|
||||
sk = ${tunnel_config.sk}
|
||||
bind_addr = 127.0.0.1
|
||||
bind_port = ${local_addr[1]}
|
||||
|
||||
#------ Visitor config file --------
|
||||
`
|
||||
}
|
||||
document.getElementById('config').value = config;
|
||||
};
|
||||
|
||||
put_config();
|
||||
</script>
|
||||
|
||||
</x-app-layout>
|
88
resources/views/admin/users/index.blade.php
Normal file
88
resources/views/admin/users/index.blade.php
Normal file
@ -0,0 +1,88 @@
|
||||
<x-app-layout>
|
||||
<h1>已经发现的客户</h1>
|
||||
|
||||
<p>总计: {{ $count }}</p>
|
||||
|
||||
|
||||
<form name="filter">
|
||||
用户 ID: <input type="text" name="id" value="{{ Request::get('id') }}" />
|
||||
名称: <input type="text" name="name" value="{{ Request::get('name') }}" />
|
||||
邮箱: <input type="text" name="email" value="{{ Request::get('email') }}" />
|
||||
|
||||
<button type="submit">筛选</button>
|
||||
</form>
|
||||
|
||||
|
||||
<table>
|
||||
{{-- 表头 --}}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>邮箱</th>
|
||||
<th>剩余流量</th>
|
||||
{{-- <th>发现时间</th>
|
||||
<th>更新时间</th> --}}
|
||||
{{-- <th>操作</th> --}}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{{-- 表内容 --}}
|
||||
<tbody>
|
||||
@foreach ($users as $user)
|
||||
<tr>
|
||||
<td>{{ $user->id }}</td>
|
||||
<td>{{ $user->name }}</td>
|
||||
<td>{{ $user->email }}</td>
|
||||
<td><input type="text" value="{{ $user->traffic ?? 0 }}"
|
||||
onchange="updateTraffic({{ $user->id }}, this)" /> GB</td>
|
||||
{{-- <td>{{ $user->created_at }}</td>
|
||||
<td>{{ $user->updated_at }}</td> --}}
|
||||
{{-- <td>
|
||||
<a href="{{ route('user.show', $user) }}">查看</a>
|
||||
<a href="{{ route('user.edit', $user) }}">编辑</a>
|
||||
<form action="{{ route('user.destroy', $user) }}" method="POST">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit">删除</button>
|
||||
</form>
|
||||
</td> --}}
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{{ $users->links() }}
|
||||
|
||||
<script>
|
||||
function updateTraffic(userId, input) {
|
||||
const url = '/users/' + userId
|
||||
// xml http request
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('PATCH', url);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
// csrf
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
|
||||
|
||||
// not follow redirect
|
||||
xhr.responseType = 'json';
|
||||
|
||||
// add ajax header
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
|
||||
|
||||
xhr.send(JSON.stringify({
|
||||
traffic: input.value
|
||||
}));
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status != 200) {
|
||||
alert(`Error ${xhr.status}: ${xhr.statusText}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</x-app-layout>
|
67
resources/views/components/menu.blade.php
Normal file
67
resources/views/components/menu.blade.php
Normal file
@ -0,0 +1,67 @@
|
||||
<div>
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
Edge.st {{ config('app.name') }}
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu"
|
||||
aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
@php($verified = auth()->user()->verified_at ?? null)
|
||||
|
||||
<div class="collapse navbar-collapse" id="navMenu">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="{{ route('servers.index') }}">服务器列表</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="{{ route('tunnels.index') }}">隧道列表</a>
|
||||
</li>
|
||||
|
||||
@if (!$verified)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="{{ route('verify') }}">实名认证</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@auth
|
||||
@if (auth()->user()->is_admin)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page"
|
||||
href="{{ route('servers.create') }}">创建服务器</a>
|
||||
</li>
|
||||
@endif
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="{{ route('tunnels.create') }}">创建隧道</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
{{ auth()->user()->name }}
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="{{ route('servers.index') }}">服务器列表</a>
|
||||
<a class="dropdown-item" href="{{ route('tunnels.index') }}">隧道列表</a>
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="axios.post(route('login.logout')).then(() => {window.location.reload()})">
|
||||
注销 <span x-html="config.app_html"></span>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
@else
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="{{ route('login.redirect') }}">登录</a>
|
||||
</li>
|
||||
@endauth
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
29
resources/views/components/server-view.blade.php
Normal file
29
resources/views/components/server-view.blade.php
Normal file
@ -0,0 +1,29 @@
|
||||
<hr />
|
||||
<div>
|
||||
<a href="{{ $url }}" class="list-group-item list-group-item-action">
|
||||
{{-- <div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1 text-success">{{ $server->name }}</h5>
|
||||
<small class="text-muted">{{ $server->updated_at->diffForHumans() }}</small>
|
||||
</div> --}}
|
||||
{{-- <p class="mb-1"></p> --}}
|
||||
{{ $server->name }}
|
||||
{{ $server->updated_at->diffForHumans() }}
|
||||
</a>
|
||||
|
||||
<small class="text-muted">
|
||||
<p>状态: {{ $server->status }}</p>
|
||||
服务器地址: {{ $server->server_address }}, 支持的协议:
|
||||
{{ $server->allow_http ? 'HTTP' : ' ' }}
|
||||
{{ $server->allow_https ? 'HTTPS' : ' ' }}
|
||||
{{ $server->allow_tcp ? 'TCP' : ' ' }}
|
||||
{{ $server->allow_udp ? 'UDP' : ' ' }}
|
||||
{{ $server->allow_STCP ? 'STCP' : ' ' }}。
|
||||
服务器位于@if ($server->is_china_mainland)
|
||||
<span style="color: green">中国大陆</span>
|
||||
@else
|
||||
<span style="color: red">境外</span>
|
||||
@endif
|
||||
<p>每 GB 需要消耗的 CNY: {{ $server->price_per_gb }}</p>
|
||||
</small>
|
||||
</div>
|
||||
<hr />
|
72
resources/views/layouts/app.blade.php
Normal file
72
resources/views/layouts/app.blade.php
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
|
||||
|
||||
<title>{{ config('app.name', 'LAE') }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="top">
|
||||
@auth
|
||||
<h3 style="text-align: center">{{ config('app.display_name') }} 后台</h3>
|
||||
{{-- 顶部横向菜单 --}}
|
||||
<div class="top-menu">
|
||||
<ul>
|
||||
<li><a href="/">首页</a></li>
|
||||
<li><a href="{{ route('admin.users.index') }}">用户</a></li>
|
||||
<li><a href="{{ route('admin.tunnels.index') }}">隧道</a></li>
|
||||
<li><a href="{{ route('admin.servers.index') }}">服务器</a></li>
|
||||
<li><a href="{{ route('logout') }}">退出</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
{{-- display error --}}
|
||||
|
||||
{{-- if has success --}}
|
||||
@if (session('success'))
|
||||
<p style="color: green">
|
||||
{{ session('success') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<p style="color: red">
|
||||
{{ session('error') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if ($errors->any())
|
||||
<div>
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li style="color: red;">{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<button onclick="location.reload()">重新加载</button>
|
||||
<button onclick="location.reload()">重新加载</button>
|
||||
<button onclick="location.reload()">重新加载</button>
|
||||
<button onclick="location.reload()">重新加载</button>
|
||||
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
{{-- 摆烂 --}}
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
27
routes/admin.php
Normal file
27
routes/admin.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\TunnelController;
|
||||
use App\Http\Controllers\Admin\IndexController;
|
||||
use App\Http\Controllers\Admin\ReplyController;
|
||||
use App\Http\Controllers\Admin\ReviewController;
|
||||
use App\Http\Controllers\Admin\ServerController;
|
||||
use App\Http\Controllers\Admin\UserController;
|
||||
use App\Http\Controllers\Admin\WorkOrderController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::withoutMiddleware('auth:admin')->group(function() {
|
||||
Route::get('/login', [IndexController::class, 'index'])->name('login');
|
||||
Route::post('/login', [IndexController::class, 'login']);
|
||||
});
|
||||
|
||||
// Auth group
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/', [IndexController::class, 'index'])->name('index');
|
||||
|
||||
Route::resource('users', UserController::class);
|
||||
Route::resource('servers', ServerController::class);
|
||||
Route::resource('tunnels', TunnelController::class);
|
||||
|
||||
|
||||
Route::get('/logout', [IndexController::class, 'logout'])->name('logout');
|
||||
});
|
@ -5,11 +5,17 @@
|
||||
use App\Http\Controllers\Api\UserController;
|
||||
use App\Http\Controllers\Api\ServerController;
|
||||
use App\Http\Controllers\Api\TunnelController;
|
||||
use App\Http\Controllers\Api\PortManagerController;
|
||||
|
||||
|
||||
Route::prefix('tunnel')->name('api.tunnel.')->group(function () {
|
||||
Route::post('/handler/{key}', [PortManagerController::class, 'handler'])->name('handler');
|
||||
});
|
||||
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::get('user', UserController::class);
|
||||
Route::apiResource('tunnels', TunnelController::class);
|
||||
Route::post('tunnels/{tunnel}/close', [TunnelController::class, 'close']);
|
||||
Route::apiResource('servers', ServerController::class);
|
||||
});
|
||||
|
25
yarn.lock
25
yarn.lock
@ -263,6 +263,14 @@ delayed-stream@~1.0.0:
|
||||
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
echarts@^5.4.2:
|
||||
version "5.4.2"
|
||||
resolved "https://registry.npmmirror.com/echarts/-/echarts-5.4.2.tgz#9f38781c9c6ae323e896956178f6956952c77a48"
|
||||
integrity sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==
|
||||
dependencies:
|
||||
tslib "2.3.0"
|
||||
zrender "5.4.3"
|
||||
|
||||
esbuild@^0.16.14:
|
||||
version "0.16.17"
|
||||
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259"
|
||||
@ -327,6 +335,11 @@ has@^1.0.3:
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
humanize-plus@^1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.npmmirror.com/humanize-plus/-/humanize-plus-1.8.2.tgz#a65b34459ad6367adbb3707a82a3c9f916167030"
|
||||
integrity sha512-jaLeQyyzjjINGv7O9JJegjsaUcWjSj/1dcXvLEgU3pGdqCdP1PiC/uwr+saJXhTNBHZtmKnmpXyazgh+eceRxA==
|
||||
|
||||
is-core-module@^2.9.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
|
||||
@ -431,6 +444,11 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
tslib@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
vite-plugin-full-reload@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmmirror.com/vite-plugin-full-reload/-/vite-plugin-full-reload-1.0.5.tgz#6cddfa94e51909843bc7156ab728dbac972b8560"
|
||||
@ -473,3 +491,10 @@ vue@^3.2.36:
|
||||
"@vue/runtime-dom" "3.2.36"
|
||||
"@vue/server-renderer" "3.2.36"
|
||||
"@vue/shared" "3.2.36"
|
||||
|
||||
zrender@5.4.3:
|
||||
version "5.4.3"
|
||||
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.4.3.tgz#41ffaf835f3a3210224abd9d6964b48ff01e79f5"
|
||||
integrity sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==
|
||||
dependencies:
|
||||
tslib "2.3.0"
|
||||
|
Loading…
Reference in New Issue
Block a user