From f91d07929680fa713406cb07204c55ea0c374499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sat, 10 Dec 2022 13:16:31 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=B9=E6=80=A7=EF=BC=88=E5=85=A8=E5=B1=80?= =?UTF-8?q?=EF=BC=89=EF=BC=9A=E5=A2=9E=E5=BC=BA=E5=AE=89=E5=85=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/Panel.php | 76 +++++++++- app/Console/Kernel.php | 16 ++- app/Http/Controllers/Api/CronsController.php | 13 +- .../Controllers/Api/PluginsController.php | 4 +- app/Http/Controllers/Api/SafesController.php | 41 +++++- .../Controllers/Api/SettingsController.php | 3 +- .../Controllers/Api/WebsitesController.php | 85 ++++++++---- config/panel.php | 2 +- .../Controllers/OpenrestyController.php | 130 +++++++++++++----- plugins/Openresty/plugin.json | 2 +- plugins/Openresty/plugin.php | 6 +- plugins/Openresty/views/index.blade.php | 41 ++---- resources/views/monitor.blade.php | 2 +- resources/views/safe.blade.php | 2 +- resources/views/website/list.blade.php | 2 +- 15 files changed, 306 insertions(+), 119 deletions(-) diff --git a/app/Console/Commands/Panel.php b/app/Console/Commands/Panel.php index 5e0a3887..86a4bf68 100644 --- a/app/Console/Commands/Panel.php +++ b/app/Console/Commands/Panel.php @@ -34,6 +34,11 @@ class Panel extends Command */ public function handle() { + // 检测是否以root用户运行 + if (shell_exec('whoami') != 'root') { + $this->error('耗子Linux面板:请以root用户运行'); + return 1; + } $action = $this->argument('action'); switch ($action) { case 'init': @@ -69,6 +74,12 @@ class Panel extends Command case 'deleteSite': $this->deleteSite(); break; + case 'writeSetting': + $this->writeSetting(); + break; + case 'deleteSetting': + $this->deleteSetting(); + break; default: $this->info('耗子Linux面板命令行工具'); $this->info('请使用以下命令:'); @@ -84,6 +95,8 @@ class Panel extends Command $this->info('panel writeMysqlPassword {password} 写入MySQL root密码'); $this->info('panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板'); $this->info('panel deleteSite {name} 删除面板网站数据'); + $this->info('panel writeSetting {name} {value} 写入/更新面板设置数据'); + $this->info('panel deleteSetting {name} 删除面板设置数据'); break; } return Command::SUCCESS; @@ -161,6 +174,14 @@ class Panel extends Command $this->info(shell_exec('\cp -r /tmp/plugins/* /www/panel/plugins')); $this->info('正在更新面板数据库...'); $this->info(shell_exec('cd /www/panel && php-panel artisan migrate')); + $this->info('正在设置面板权限...'); + $this->info(shell_exec('chown -R root:root /www/panel')); + $this->info(shell_exec('chmod -R 755 /www/panel')); + $this->info(shell_exec('chown -R root:root /www/server/cron')); + $this->info(shell_exec('chmod -R 700 /www/server/cron')); + $this->info(shell_exec('chmod -R 600 /www/server/cron/logs')); + $this->info(shell_exec('chown -R root:root /www/server/vhost')); + $this->info(shell_exec('chmod -R 644 /www/server/vhost')); $this->info('正在重启面板服务...'); $this->info(shell_exec('systemctl restart panel.service')); // 检查重启是否成功 @@ -266,6 +287,7 @@ class Panel extends Command /** * 写入MySQL密码 + * @return void */ private function writeMysqlPassword(): void { @@ -287,6 +309,7 @@ class Panel extends Command /** * 清理所有运行中和等待中的任务 + * @return void */ private function cleanRunningTask(): void { @@ -299,6 +322,7 @@ class Panel extends Command /** * 备份网站/MySQL数据库/PostgreSQL数据库到指定目录 + * @return void */ private function backup(): void { @@ -339,13 +363,14 @@ class Panel extends Command return; } $backupFile = $path.'/'.$name.'_'.date('YmdHis').'.zip'; - shell_exec('zip -r '.$backupFile.' '.$sitePath.' 2>&1'); + shell_exec('zip -r '.$backupFile.' '.escapeshellarg($sitePath).' 2>&1'); $this->info('成功'); } elseif ($type == 'mysql') { // 备份MySQL数据库 $password = Setting::query()->where('name', 'mysql_root_password')->value('value'); $backupFile = $path.'/'.$name.'_'.date('YmdHis').'.sql'; // 判断数据库是否存在 + $name = escapeshellarg($name); $check = shell_exec("mysql -u root -p".$password." -e 'use ".$name."' 2>&1"); if (str_contains($check, 'ERROR')) { $this->error('数据库不存在'); @@ -353,7 +378,7 @@ class Panel extends Command } shell_exec("mysqldump -u root -p".$password." ".$name." > ".$backupFile." 2>&1"); // zip压缩 - shell_exec('zip -r '.$backupFile.'.zip '.$backupFile.' 2>&1'); + shell_exec('zip -r '.$backupFile.'.zip '.escapeshellarg($backupFile).' 2>&1'); // 删除sql文件 unlink($backupFile); $this->info('成功'); @@ -366,9 +391,10 @@ class Panel extends Command $this->error('数据库不存在'); return; } + $name = escapeshellarg($name); shell_exec('su - postgres -c "pg_dump '.$name.'" > '.$backupFile.' 2>&1'); // zip压缩 - shell_exec('zip -r '.$backupFile.'.zip '.$backupFile.' 2>&1'); + shell_exec('zip -r '.$backupFile.'.zip '.escapeshellarg($backupFile).' 2>&1'); // 删除sql文件 unlink($backupFile); $this->info('成功'); @@ -379,10 +405,10 @@ class Panel extends Command /** * 写入网站数据到面板 + * @return void */ private function writeSite(): void { - //{name} {status} {path} {php} {ssl} $name = $this->argument('a1'); $status = $this->argument('a2'); $path = $this->argument('a3'); @@ -419,6 +445,7 @@ class Panel extends Command /** * 删除面板网站数据 + * @return void */ private function deleteSite(): void { @@ -439,4 +466,45 @@ class Panel extends Command // 删除网站 Website::query()->where('name', $name)->delete(); } + + /** + * 写入/更新面板设置数据 + * @return void + */ + private function writeSetting(): void + { + $name = $this->argument('a1'); + $value = $this->argument('a2'); + + // 判空 + if (empty($name) || empty($value)) { + $this->error('参数错误'); + return; + } + Setting::query()->updateOrCreate(['name' => $name], ['value' => $value]); + } + + /** + * 删除面板设置数据 + * @return void + */ + private function deleteSetting(): void + { + $name = $this->argument('a1'); + + // 判空 + if (empty($name)) { + $this->error('参数错误'); + return; + } + + // 判断设置是否存在 + if (!Setting::query()->where('name', $name)->exists()) { + $this->error('设置不存在'); + return; + } + + // 删除设置 + Setting::query()->where('name', $name)->delete(); + } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index a7ff71f0..f01f7bc5 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -21,16 +21,26 @@ class Kernel extends ConsoleKernel // 查询所有计划任务 $crons = Cron::all(); foreach ($crons as $cron) { - $schedule->exec('bash /www/server/cron/'.$cron->shell)->withoutOverlapping()->cron($cron->time)->appendOutputTo('/www/server/cron/logs/'.$cron->id.'.log')->when(function ( + $file = '/www/server/cron/'.$cron->shell; + // 检查文件是否存在,及所有者是否为root + if (!file_exists($file) || fileowner($file) != 0) { + file_put_contents('/www/server/cron/logs/'.$cron->id.'.log', + '耗子Linux面板:检测到脚本文件异常,为确保安全已终止运行,如果你不知道发生了什么,这通常意味着服务器已被入侵。', + FILE_APPEND); + continue; + } + $schedule->exec('bash '.escapeshellarg($file))->withoutOverlapping()->cron($cron->time)->appendOutputTo('/www/server/cron/logs/'.$cron->id.'.log')->when(function ( ) use ($cron) { return (boolean) $cron->status; })->after(function () use ($cron) { $cron->updated_at = now(); $cron->save(); })->onSuccess(function () use ($cron) { - shell_exec('echo "'.Carbon::now()->toDateTimeString().' 任务执行成功" >> /www/server/cron/logs/'.$cron->id.'.log'); + file_put_contents('/www/server/cron/logs/'.$cron->id.'.log', + Carbon::now()->toDateTimeString().' 任务执行成功', FILE_APPEND); })->onFailure(function () use ($cron) { - shell_exec('echo "'.Carbon::now()->toDateTimeString().' 任务执行失败" >> /www/server/cron/logs/'.$cron->id.'.log'); + file_put_contents('/www/server/cron/logs/'.$cron->id.'.log', + Carbon::now()->toDateTimeString().' 任务执行失败', FILE_APPEND); }); } } diff --git a/app/Http/Controllers/Api/CronsController.php b/app/Http/Controllers/Api/CronsController.php index d560ed71..ce96f540 100644 --- a/app/Http/Controllers/Api/CronsController.php +++ b/app/Http/Controllers/Api/CronsController.php @@ -54,7 +54,10 @@ class CronsController extends Controller try { $credentials = $this->validate($request, [ 'name' => 'required|max:255', - 'time' => ['required', 'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+))*\s?){5}$/'], + 'time' => [ + 'required', + 'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+))*\s?){5}$/' + ], 'script' => 'required', ]); } catch (ValidationException $e) { @@ -65,10 +68,10 @@ class CronsController extends Controller $shellDir = '/www/server/cron/'; $shellLogDir = '/www/server/cron/logs/'; if (!is_dir($shellDir)) { - mkdir($shellDir, 0755, true); + mkdir($shellDir, 700, true); } if (!is_dir($shellLogDir)) { - mkdir($shellLogDir, 0755, true); + mkdir($shellLogDir, 600, true); } $shellFile = uniqid().'.sh'; file_put_contents($shellDir.$shellFile, $credentials['script']); @@ -110,10 +113,10 @@ class CronsController extends Controller $shellDir = '/www/server/cron/'; $shellLogDir = '/www/server/cron/logs/'; if (!is_dir($shellDir)) { - mkdir($shellDir, 0755, true); + mkdir($shellDir, 700, true); } if (!is_dir($shellLogDir)) { - mkdir($shellLogDir, 0755, true); + mkdir($shellLogDir, 600, true); } $shellFile = $cron->shell; file_put_contents($shellDir.$shellFile, $credentials['script']); diff --git a/app/Http/Controllers/Api/PluginsController.php b/app/Http/Controllers/Api/PluginsController.php index ad2cbff7..57d3da2d 100644 --- a/app/Http/Controllers/Api/PluginsController.php +++ b/app/Http/Controllers/Api/PluginsController.php @@ -34,9 +34,9 @@ class PluginsController extends Controller $shows = Plugin::query()->pluck('show', 'slug'); foreach ($data['data'] as $k => $v) { // 如果本地已安装,则显示本地名称 - $data['data'][$k]['name'] = PLUGINS[$v['slug']]['name'] ?? $data['data'][$k]['name']; + $data['data'][$k]['name'] = PLUGINS[$v['slug']]['name'] ?? $data['data'][$k]['name'] ?? '无名'; // 作者名称 - $data['data'][$k]['author'] = PLUGINS[$v['slug']]['author'] ?? '耗子'; + $data['data'][$k]['author'] = PLUGINS[$v['slug']]['author'] ?? $data['data'][$k]['author'] ?? '耗子'; // 已装版本 $data['data'][$k]['install_version'] = PLUGINS[$v['slug']]['version'] ?? ''; // 首页显示 diff --git a/app/Http/Controllers/Api/SafesController.php b/app/Http/Controllers/Api/SafesController.php index 4951dccb..ca09f40d 100644 --- a/app/Http/Controllers/Api/SafesController.php +++ b/app/Http/Controllers/Api/SafesController.php @@ -9,6 +9,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; class SafesController extends Controller { @@ -109,6 +110,12 @@ class SafesController extends Controller public function setSshPort(Request $request): JsonResponse { $port = $request->input('port'); + // 只能是数字 + if (!is_numeric($port)) { + $res['code'] = 1; + $res['msg'] = '端口只能是数字'; + return response()->json($res); + } $oldPort = trim(shell_exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")); shell_exec("sed -i 's/#Port ".$oldPort."/Port ".$port."/g' /etc/ssh/sshd_config"); shell_exec("sed -i 's/Port ".$oldPort."/Port ".$port."/g' /etc/ssh/sshd_config"); @@ -200,8 +207,21 @@ class SafesController extends Controller */ public function addFirewallRule(Request $request): JsonResponse { - $port = $request->input('port'); - $protocol = $request->input('protocol'); + // 消毒 + try { + $input = $this->validate($request, [ + 'port' => ['required','regex:/^([0-9]+)(-([0-9]+))?$/'], + 'protocol' => 'required|in:tcp,udp', + ]); + $port = $input['port']; + $protocol = $input['protocol']; + } catch (ValidationException $e) { + return response()->json([ + 'code' => 1, + 'msg' => '参数错误:'.$e->getMessage(), + 'errors' => $e->errors() + ], 200); + } // 判断是否开启 $firewallStatus = trim(shell_exec("firewall-cmd --state 2>&1")); if ($firewallStatus != 'running') { @@ -227,8 +247,21 @@ class SafesController extends Controller */ public function deleteFirewallRule(Request $request): JsonResponse { - $port = $request->input('port'); - $protocol = $request->input('protocol'); + // 消毒 + try { + $input = $this->validate($request, [ + 'port' => ['required','regex:/^([0-9]+)(-([0-9]+))?$/'], + 'protocol' => 'required|in:tcp,udp', + ]); + $port = $input['port']; + $protocol = $input['protocol']; + } catch (ValidationException $e) { + return response()->json([ + 'code' => 1, + 'msg' => '参数错误:'.$e->getMessage(), + 'errors' => $e->errors() + ], 200); + } // 判断是否开启 $firewallStatus = trim(shell_exec("firewall-cmd --state 2>&1")); if ($firewallStatus != 'running') { diff --git a/app/Http/Controllers/Api/SettingsController.php b/app/Http/Controllers/Api/SettingsController.php index 168e6995..dd19222b 100644 --- a/app/Http/Controllers/Api/SettingsController.php +++ b/app/Http/Controllers/Api/SettingsController.php @@ -107,11 +107,12 @@ class SettingsController extends Controller return response()->json($res); } if ($port != $matches[1]) { - $nginxConf = preg_replace('/listen\s+(\d+)/', 'listen '.$port, $nginxConf); + $nginxConf = preg_replace('/listen\s+'.$matches[1].'/', 'listen '.$port, $nginxConf, 1); file_put_contents('/www/server/nginx/conf/nginx.conf', $nginxConf); // 重载nginx shell_exec('systemctl reload nginx'); // 防火墙放行端口 + $port = escapeshellarg($port); shell_exec('firewall-cmd --permanent --zone=public --add-port='.$port.'/tcp >/dev/null 2>&1'); shell_exec('firewall-cmd --reload'); } diff --git a/app/Http/Controllers/Api/WebsitesController.php b/app/Http/Controllers/Api/WebsitesController.php index 68aec671..af53b567 100644 --- a/app/Http/Controllers/Api/WebsitesController.php +++ b/app/Http/Controllers/Api/WebsitesController.php @@ -91,6 +91,35 @@ class WebsitesController extends Controller } } + // 对db_name单独验证 + if ($credentials['db']) { + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]+$/', + $credentials['db_name'])) { + return response()->json([ + 'code' => 1, + 'msg' => '数据库名必须以字母开头,只能包含字母、数字、下划线' + ], 200); + } + } + + // 对数据库用户名和库名单独验证 + if ($credentials['db']) { + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]+$/', + $credentials['db_username'])) { + return response()->json([ + 'code' => 1, + 'msg' => '数据库用户名必须以字母开头,只能包含字母、数字、下划线' + ], 200); + } + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]+$/', + $credentials['db_name'])) { + return response()->json([ + 'code' => 1, + 'msg' => '数据库名必须以字母开头,只能包含字母、数字、下划线' + ], 200); + } + } + // 禁止添加重复网站 $website = Website::query()->where('name', $credentials['name'])->first(); if ($website) { @@ -99,8 +128,8 @@ class WebsitesController extends Controller 'msg' => '网站已存在' ]); } - // 禁止phpmyadmin作为名称 - if ($credentials['name'] == 'phpmyadmin') { + // 禁止部分保留名称 + if ($credentials['name'] == 'phpmyadmin' || $credentials['name'] == 'mysql' || $credentials['name'] == 'ssh' || $credentials['name'] == 'pure-ftpd' || $credentials['name'] == 'panel') { return response()->json([ 'code' => 1, 'msg' => '该名称为保留名称,请更换' @@ -125,9 +154,9 @@ class WebsitesController extends Controller // 入库 Website::query()->create($credentials); // 创建网站目录 - shell_exec("mkdir -p ".$credentials['path']); + shell_exec("mkdir -p ".escapeshellarg($credentials['path'])); // 创建index.html - shell_exec("touch ".$credentials['path']."/index.html"); + shell_exec("touch ".escapeshellarg($credentials['path']."/index.html")); // 写入到index.html $index_html = << @@ -226,9 +255,11 @@ $port_list EOF; // 写入nginx配置 file_put_contents('/www/server/vhost/'.$credentials['name'].'.conf', $nginx_config); - shell_exec('echo "" > /www/server/vhost/rewrite/'.$credentials['name'].'.conf'); - shell_exec('echo "" > /www/server/vhost/ssl/'.$credentials['name'].'.pem'); - shell_exec('echo "" > /www/server/vhost/ssl/'.$credentials['name'].'.key'); + shell_exec('echo "" > '.escapeshellarg('/www/server/vhost/rewrite/'.$credentials['name'].'.conf')); + shell_exec('echo "" > '.escapeshellarg('/www/server/vhost/ssl/'.$credentials['name'].'.pem')); + shell_exec('echo "" > '.escapeshellarg('/www/server/vhost/ssl/'.$credentials['name'].'.key')); + shell_exec('chown -R root:root /www/server/vhost'); + shell_exec('chmod -R 644 /www/server/vhost'); shell_exec("systemctl reload nginx"); // 创建数据库 @@ -236,15 +267,15 @@ EOF; if ($credentials['db_type'] == 'mysql') { $password = Setting::query()->where('name', 'mysql_root_password')->value('value'); shell_exec("mysql -u root -p".$password." -e \"CREATE DATABASE IF NOT EXISTS ".$credentials['db_name']." DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;\" 2>&1"); - shell_exec("mysql -u root -p".$password." -e \"CREATE USER '".$credentials['db_username']."'@'localhost' IDENTIFIED BY '".$credentials['db_password']."';\""); - shell_exec("mysql -u root -p".$password." -e \"GRANT ALL PRIVILEGES ON ".$credentials['db_name'].".* TO '".$credentials['db_username']."'@'localhost';\""); + shell_exec("mysql -u root -p".$password." -e \"CREATE USER ".escapeshellarg($credentials['db_username'])."@'localhost' IDENTIFIED BY ".escapeshellarg($credentials['db_password']).";\""); + shell_exec("mysql -u root -p".$password." -e \"GRANT ALL PRIVILEGES ON ".$credentials['db_name'].".* TO ".escapeshellarg($credentials['db_username'])."@'localhost';\""); shell_exec("mysql -u root -p".$password." -e \"flush privileges;\""); } elseif ($credentials['db_type'] == 'postgresql') { shell_exec('echo "CREATE DATABASE '.$credentials['db_name'].';"|su - postgres -c "psql"'); - shell_exec('echo "CREATE USER '.$credentials['db_username'].' WITH PASSWORD \''.$credentials['db_password'].'\';"|su - postgres -c "psql"'); + shell_exec('echo "CREATE USER '.$credentials['db_username'].' WITH PASSWORD '.escapeshellarg($credentials['db_password']).';"|su - postgres -c "psql"'); shell_exec('echo "GRANT ALL PRIVILEGES ON DATABASE '.$credentials['db_name'].' TO '.$credentials['db_username'].';"|su - postgres -c "psql"'); // 写入用户配置 - shell_exec('echo "host '.$credentials['db_name'].' '.$credentials['db_username'].' 127.0.0.1/32 scram-sha-256" >> /www/server/postgresql/15/pg_hba.conf'); + shell_exec('echo '.escapeshellarg('host '.$credentials['db_name'].' '.$credentials['db_username'].' 127.0.0.1/32 scram-sha-256').' >> /www/server/postgresql/15/pg_hba.conf'); // 重载 shell_exec('systemctl reload postgresql-15'); } @@ -266,14 +297,14 @@ EOF; // 从数据库删除 Website::query()->where('name', $name)->delete(); // 删除站点目录 - shell_exec("rm -rf /www/wwwroot/$name"); + shell_exec("rm -rf ".escapeshellarg("/www/wwwroot/$name")); // 删除nginx配置 - shell_exec("rm -rf /www/server/vhost/$name.conf"); + shell_exec("rm -rf ".escapeshellarg("/www/server/vhost/$name.conf")); // 删除rewrite配置 - shell_exec("rm -rf /www/server/vhost/rewrite/$name.conf"); + shell_exec("rm -rf ".escapeshellarg("/www/server/vhost/rewrite/$name.conf")); // 删除ssl配置 - shell_exec("rm -rf /www/server/vhost/ssl/$name.pem"); - shell_exec("rm -rf /www/server/vhost/ssl/$name.key"); + shell_exec("rm -rf ".escapeshellarg("/www/server/vhost/ssl/$name.pem")); + shell_exec("rm -rf ".escapeshellarg("/www/server/vhost/ssl/$name.key")); $res['code'] = 0; $res['msg'] = 'success'; @@ -410,7 +441,7 @@ EOF; $website['config_raw'] = file_get_contents('/www/server/vhost/'.$name.'.conf'); // 读取访问日志 - $website['log'] = shell_exec('tail -n 100 /www/wwwlogs/'.$name.'.log'); + $website['log'] = shell_exec('tail -n 100 '.escapeshellarg('/www/wwwlogs/'.$name.'.log')); // log需要转义实体 $website['log'] = htmlspecialchars($website['log']); @@ -454,7 +485,7 @@ EOF; $res['msg'] = 'success'; // 如果config_raw与本地配置文件不一致,则更新配置文件,然后直接返回 - $configRaw = shell_exec('cat /www/server/vhost/'.$name.'.conf'); + $configRaw = shell_exec('cat '.escapeshellarg('/www/server/vhost/'.$name.'.conf')); if (trim($configRaw) != trim($config['config_raw'])) { file_put_contents('/www/server/vhost/'.$name.'.conf', $config['config_raw']); return response()->json($res); @@ -533,12 +564,8 @@ EOF; // 写入open_basedir配置到.user.ini文件 if (is_dir($config['path'])) { file_put_contents($config['path'].'/.user.ini', $open_basedir); - // 为.user.ini文件添加i权限 - // shell_exec('chattr +i '.$config['path'].'/.user.ini'); } } else { - // 移除.user.ini文件的i权限 - shell_exec('chattr -i '.$config['path'].'/.user.ini'); // 删除.user.ini文件 if (file_exists($config['path'].'/.user.ini')) { unlink($config['path'].'/.user.ini'); @@ -655,7 +682,7 @@ EOL; public function clearSiteLog(Request $request): JsonResponse { $name = $request->input('name'); - shell_exec('echo "" > /www/wwwlogs/'.$name.'.log'); + unlink('/www/wwwlogs/'.$name.'.log'); $res['code'] = 0; $res['msg'] = 'success'; return response()->json($res); @@ -728,7 +755,7 @@ EOL; // 从数据库中获取网站目录 $sitePath = Website::query()->where('name', $credentials['name'])->value('path'); $backupFile = $backupPath.'/'.$credentials['name'].'_'.date('YmdHis').'.zip'; - shell_exec('zip -r '.$backupFile.' '.$sitePath.' 2>&1'); + shell_exec('zip -r '.$backupFile.' '.escapeshellarg($sitePath).' 2>&1'); $res['code'] = 0; $res['msg'] = 'success'; @@ -803,11 +830,11 @@ EOL; ], 200); } - shell_exec('rm -rf /www/wwwroot/'.$credentials['name'].'/*'); - shell_exec('unzip -o '.$backupFile.' -d /www/wwwroot/'.$credentials['name'].' 2>&1'); + shell_exec('rm -rf '.escapeshellarg('/www/wwwroot/'.$credentials['name'].'/*')); + shell_exec('unzip -o '.escapeshellarg($backupFile).' -d '.escapeshellarg('/www/wwwroot/'.$credentials['name']).' 2>&1'); // 设置权限 - shell_exec('chown -R www:www /www/wwwroot/'.$credentials['name']); - shell_exec('chmod -R 755 /www/wwwroot/'.$credentials['name']); + shell_exec('chown -R www:www '.escapeshellarg('/www/wwwroot/'.$credentials['name'])); + shell_exec('chmod -R 755 '.escapeshellarg('/www/wwwroot/'.$credentials['name'])); $res['code'] = 0; $res['msg'] = 'success'; @@ -946,7 +973,7 @@ server EOF; file_put_contents('/www/server/vhost/'.$website['name'].'.conf', $nginxConfig); // 重置伪静态规则 - shell_exec('echo "" > /www/server/vhost/rewrite/'.$website['name'].'.conf'); + shell_exec('echo "" > '.escapeshellarg('/www/server/vhost/rewrite/'.$website['name'].'.conf')); // 重载nginx shell_exec('systemctl reload nginx'); diff --git a/config/panel.php b/config/panel.php index 297014a8..429dba09 100644 --- a/config/panel.php +++ b/config/panel.php @@ -1,6 +1,6 @@ '耗子Linux面板', - 'version' => '20221209', + 'version' => '20221210', 'plugin_dir' => '/www/panel/plugins', ]; \ No newline at end of file diff --git a/plugins/Openresty/Controllers/OpenrestyController.php b/plugins/Openresty/Controllers/OpenrestyController.php index a5a9ff07..3e06f2a5 100755 --- a/plugins/Openresty/Controllers/OpenrestyController.php +++ b/plugins/Openresty/Controllers/OpenrestyController.php @@ -2,7 +2,7 @@ /** * Name: OpenResty插件控制器 * Author:耗子 - * Date: 2022-11-30 + * Date: 2022-12-10 */ namespace Plugins\Openresty\Controllers; @@ -10,32 +10,42 @@ namespace Plugins\Openresty\Controllers; use App\Http\Controllers\Controller; -// HTTP use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; -// Filesystem -use Illuminate\Filesystem\Filesystem; class OpenrestyController extends Controller { + /** + * 获取运行状态 + * @return JsonResponse + */ public function status(): JsonResponse { - $command = 'systemctl status nginx'; - $result = shell_exec($command); - - $res['code'] = 0; - $res['msg'] = 'success'; - if (str_contains($result, 'inactive')) { - $res['data'] = 'stopped'; + $status = shell_exec('systemctl status nginx | grep Active | grep -v grep | awk \'{print $2}\''); + // 格式化掉换行符 + $status = trim($status); + if (empty($status)) { + return response()->json(['code' => 1, 'msg' => '获取服务运行状态失败']); + } + if ($status == 'active') { + $status = 1; } else { - $res['data'] = 'running'; + $status = 0; } + // 返回结果 + $res['code'] = 0; + $res['msg'] = 'success'; + $res['data'] = $status; return response()->json($res); } + /** + * 重启 + * @return JsonResponse + */ public function restart(): JsonResponse { $command = 'nginx -t 2>&1'; @@ -50,12 +60,27 @@ class OpenrestyController extends Controller return response()->json($res); } - $command2 = 'systemctl restart nginx'; - shell_exec($command2); - $res['data'] = 'OpenResty已重启'; + shell_exec('systemctl restart nginx'); + $status = shell_exec('systemctl status nginx | grep Active | grep -v grep | awk \'{print $2}\''); + // 格式化掉换行符 + $status = trim($status); + if (empty($status)) { + return response()->json(['code' => 1, 'msg' => '获取服务运行状态失败']); + } + if ($status != 'active') { + return response()->json(['code' => 1, 'msg' => '重启服务失败']); + } + + // 返回结果 + $res['code'] = 0; + $res['msg'] = 'success'; return response()->json($res); } + /** + * 重载 + * @return JsonResponse + */ public function reload(): JsonResponse { $command = 'nginx -t 2>&1'; @@ -69,51 +94,74 @@ class OpenrestyController extends Controller return response()->json($res); } - $command2 = 'systemctl reload nginx'; - shell_exec($command2); - $res['data'] = 'OpenResty已重载'; + shell_exec('systemctl reload nginx'); + $status = shell_exec('systemctl status nginx | grep Active | grep -v grep | awk \'{print $2}\''); + // 格式化掉换行符 + $status = trim($status); + if (empty($status)) { + return response()->json(['code' => 1, 'msg' => '获取服务运行状态失败']); + } + if ($status != 'active') { + return response()->json(['code' => 1, 'msg' => '重载服务失败']); + } + + // 返回结果 + $res['code'] = 0; + $res['msg'] = 'success'; return response()->json($res); } + /** + * 获取配置文件 + * @return JsonResponse + */ public function getConfig(): JsonResponse { $res['code'] = 0; $res['msg'] = 'success'; - $res['data'] = file_get_contents('/www/server/nginx/conf/nginx.conf'); + $res['data'] = @file_get_contents('/www/server/nginx/conf/nginx.conf'); return response()->json($res); } + /** + * 保存配置文件 + * @param Request $request + * @return JsonResponse + */ public function saveConfig(Request $request): JsonResponse { - $res['code'] = 0; - $res['msg'] = 'success'; // 获取配置内容 $config = $request->input('config'); // 备份一份旧配置 - $old_config = file_get_contents('/www/server/nginx/conf/nginx.conf'); + $old_config = @file_get_contents('/www/server/nginx/conf/nginx.conf'); // 写入配置 - $result = file_put_contents('/www/server/nginx/conf/nginx.conf', $config); + @file_put_contents('/www/server/nginx/conf/nginx.conf', $config); // 测试配置是否正确 $test = shell_exec('nginx -t 2>&1'); // 判断结果 if (!str_contains($test, 'test is successful')) { // 测试失败,则不允许保存 - $res['msg'] = 'error'; - $res['data'] = 'OpenResty配置有误,请修正后再保存:'.$test; - // 恢复旧配置 - file_put_contents('/www/server/nginx/conf/nginx.conf', $old_config); - return response()->json($res); + @file_put_contents('/www/server/nginx/conf/nginx.conf', $old_config); + return response()->json(['code' => 1, 'msg' => 'OpenResty配置有误,请修正后再保存:'.$test]); } else { // 测试成功,则重载OpenResty shell_exec('systemctl reload nginx'); - $res['data'] = 'OpenResty主配置已保存'; - return response()->json($res); + return response()->json(['code' => 0, 'msg' => 'success']); } } + /** + * 获取负载状态 + * @return JsonResponse + */ public function load(): JsonResponse { - $raw_status = HTTP::get('http://127.0.0.1/nginx_status')->body(); + $status = HTTP::get('http://127.0.0.1/nginx_status'); + // 判断状态码 + if ($status->status() != 200) { + return response()->json(['code' => 1, 'msg' => '获取状态失败']); + } + $statusRaw = $status->body(); $res['code'] = 0; $res['msg'] = 'success'; @@ -124,11 +172,11 @@ class OpenrestyController extends Controller 2).'MB'; // 使用正则匹配Active connections: 的值 - preg_match('/Active connections:\s+(\d+)/', $raw_status, $matches); + preg_match('/Active connections:\s+(\d+)/', $statusRaw, $matches); $res['data'][2]['name'] = '活跃连接数'; $res['data'][2]['value'] = $matches[1] ?? 0; // 使用正则分别匹配server accepts handled requests的三个值 - preg_match('/server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)/', $raw_status, $matches2); + preg_match('/server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)/', $statusRaw, $matches2); $res['data'][3]['name'] = '总连接次数'; $res['data'][3]['value'] = $matches2[1] ?? 0; $res['data'][4]['name'] = '总握手次数'; @@ -136,26 +184,30 @@ class OpenrestyController extends Controller $res['data'][5]['name'] = '总请求次数'; $res['data'][5]['value'] = $matches2[3] ?? 0; // 使用正则匹配Reading: 的值 - preg_match('/Reading:\s+(\d+)/', $raw_status, $matches3); + preg_match('/Reading:\s+(\d+)/', $statusRaw, $matches3); $res['data'][6]['name'] = '请求数'; $res['data'][6]['value'] = $matches3[1] ?? 0; // 使用正则匹配Writing: 的值 - preg_match('/Writing:\s+(\d+)/', $raw_status, $matches4); + preg_match('/Writing:\s+(\d+)/', $statusRaw, $matches4); $res['data'][7]['name'] = '响应数'; $res['data'][7]['value'] = $matches4[1] ?? 0; // 使用正则匹配Waiting: 的值 - preg_match('/Waiting:\s+(\d+)/', $raw_status, $matches5); + preg_match('/Waiting:\s+(\d+)/', $statusRaw, $matches5); $res['data'][8]['name'] = '驻留进程'; $res['data'][8]['value'] = $matches5[1] ?? 0; return response()->json($res); } + /** + * 获取错误日志 + * @return JsonResponse + */ public function errorLog(): JsonResponse { $res['code'] = 0; $res['msg'] = 'success'; - $res['data'] = file_get_contents('/www/wwwlogs/nginx_error.log'); + $res['data'] = @file_get_contents('/www/wwwlogs/nginx_error.log'); //如果data为换行符,则令返回空 if ($res['data'] == "\n") { $res['data'] = ''; @@ -163,6 +215,10 @@ class OpenrestyController extends Controller return response()->json($res); } + /** + * 清空错误日志 + * @return JsonResponse + */ public function cleanErrorLog(): JsonResponse { $res['code'] = 0; diff --git a/plugins/Openresty/plugin.json b/plugins/Openresty/plugin.json index e4479a8a..69f691ae 100755 --- a/plugins/Openresty/plugin.json +++ b/plugins/Openresty/plugin.json @@ -1,7 +1,7 @@ { "name": "OpenResty", "author": "耗子", - "describe": "", + "describe": "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。", "slug": "openresty", "version": "1.21.4.1" } \ No newline at end of file diff --git a/plugins/Openresty/plugin.php b/plugins/Openresty/plugin.php index 044e7d0d..d0b1a1db 100755 --- a/plugins/Openresty/plugin.php +++ b/plugins/Openresty/plugin.php @@ -21,12 +21,12 @@ app('router')->group([ 'middleware' => ['auth:sanctum'], ], function () { Route::get('status', [OpenrestyController::class, 'status']); + Route::post('restart', [OpenrestyController::class, 'restart']); + Route::post('reload', [OpenrestyController::class, 'reload']); Route::get('load', [OpenrestyController::class, 'load']); Route::get('errorLog', [OpenrestyController::class, 'errorLog']); Route::get('config', [OpenrestyController::class, 'getConfig']); Route::post('config', [OpenrestyController::class, 'saveConfig']); - Route::get('cleanErrorLog', [OpenrestyController::class, 'cleanErrorLog']); - Route::get('restart', [OpenrestyController::class, 'restart']); - Route::get('reload', [OpenrestyController::class, 'reload']); + Route::post('cleanErrorLog', [OpenrestyController::class, 'cleanErrorLog']); }); diff --git a/plugins/Openresty/views/index.blade.php b/plugins/Openresty/views/index.blade.php index 5e6b1483..d981e195 100755 --- a/plugins/Openresty/views/index.blade.php +++ b/plugins/Openresty/views/index.blade.php @@ -1,7 +1,7 @@ OpenResty
@@ -20,7 +20,7 @@ Date: 2022-11-30
当前状态:获取中
+ class="layui-badge layui-bg-black">获取中
@@ -75,7 +75,7 @@ Date: 2022-11-30 console.log('耗子Linux面板:OpenResty运行状态获取失败,接口返回' + result); return false; } - if (result.data === "running") { + if (result.data) { $('#openresty-status').html('当前状态:运行中'); } else { $('#openresty-status').html('当前状态:已停止'); @@ -188,21 +188,19 @@ Date: 2022-11-30 layer.confirm('重启OpenResty有可能导致面板短时间无法访问,是否继续重启?', { btn: ['重启', '取消'] }, function () { + index = layer.msg('正在重启OpenResty...', {icon: 16, time: 0}); admin.req({ url: "/api/plugin/openresty/restart" - , method: 'get' + , method: 'post' , beforeSend: function () { layer.msg('已发送重启请求,请稍后刷新确认重启状态。'); } , success: function (result) { + layer.close(index); if (result.code !== 0) { console.log('耗子Linux面板:OpenResty重启失败,接口返回' + result); return false; } - if (result.msg === 'error') { - layer.alert(result.data); - return false; - } admin.events.refresh(); layer.alert('OpenResty重启成功!'); } @@ -210,24 +208,19 @@ Date: 2022-11-30 console.log('耗子Linux面板:ajax请求出错,错误' + error) } }); - }, function () { - layer.msg('取消重启'); }); }); $('#openresty-reload').click(function () { - layer.msg('OpenResty重载中...'); + index = layer.msg('正在重载OpenResty...', {icon: 16, time: 0}); admin.req({ url: "/api/plugin/openresty/reload" - , method: 'get' + , method: 'post' , success: function (result) { + layer.close(index); if (result.code !== 0) { console.log('耗子Linux面板:OpenResty重载失败,接口返回' + result); return false; } - if (result.msg === 'error') { - layer.alert(result.data); - return false; - } layer.alert('OpenResty重载成功!'); } , error: function (xhr, status, error) { @@ -236,7 +229,7 @@ Date: 2022-11-30 }); }); $('#openresty-config-save').click(function () { - layer.msg('OpenResty配置保存中...'); + index = layer.msg('正在保存OpenResty主配置...', {icon: 16, time: 0}); admin.req({ url: "/api/plugin/openresty/config" , method: 'post' @@ -244,14 +237,11 @@ Date: 2022-11-30 config: openresty_config_editor.getValue() } , success: function (result) { + layer.close(index); if (result.code !== 0) { console.log('耗子Linux面板:OpenResty配置保存失败,接口返回' + result); return false; } - if (result.msg === 'error') { - layer.alert(result.data); - return false; - } layer.alert('OpenResty配置保存成功!'); } , error: function (xhr, status, error) { @@ -260,19 +250,18 @@ Date: 2022-11-30 }); }); $('#openresty-clean-error-log').click(function () { - layer.msg('错误日志清空中...'); + index = layer.msg('正在清空OpenResty错误日志...', {icon: 16, time: 0}); admin.req({ url: "/api/plugin/openresty/cleanErrorLog" - , method: 'get' + , method: 'post' , success: function (result) { + layer.close(index); if (result.code !== 0) { console.log('耗子Linux面板:OpenResty错误日志清空失败,接口返回' + result); return false; } + admin.events.refresh(); layer.msg('OpenResty错误日志已清空!'); - setTimeout(function () { - admin.events.refresh(); - }, 1000); } , error: function (xhr, status, error) { console.log('耗子Linux面板:ajax请求出错,错误' + error) diff --git a/resources/views/monitor.blade.php b/resources/views/monitor.blade.php index c6ff688d..1bd400c6 100644 --- a/resources/views/monitor.blade.php +++ b/resources/views/monitor.blade.php @@ -2,7 +2,7 @@
-
+
开启监控