2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 14:57:16 +08:00

特性(全局):增强安全性

This commit is contained in:
耗子
2022-12-10 13:16:31 +08:00
parent d4de5c6697
commit f91d079296
15 changed files with 306 additions and 119 deletions

View File

@@ -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();
}
}

View File

@@ -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);
});
}
}

View File

@@ -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']);

View File

@@ -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'] ?? '';
// 首页显示

View File

@@ -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') {

View File

@@ -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');
}

View File

@@ -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 = <<<EOF
<!DOCTYPE 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');

View File

@@ -1,6 +1,6 @@
<?php
return [
'name' => '耗子Linux面板',
'version' => '20221209',
'version' => '20221210',
'plugin_dir' => '/www/panel/plugins',
];

View File

@@ -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;

View File

@@ -1,7 +1,7 @@
{
"name": "OpenResty",
"author": "耗子",
"describe": "",
"describe": "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。",
"slug": "openresty",
"version": "1.21.4.1"
}

View File

@@ -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']);
});

View File

@@ -1,7 +1,7 @@
<!--
Name: Openresty管理器
Author: 耗子
Date: 2022-11-30
Date: 2022-12-10
-->
<title>OpenResty</title>
<div class="layui-fluid" id="component-tabs">
@@ -20,7 +20,7 @@ Date: 2022-11-30
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<blockquote id="openresty-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
class="layui-badge layui-bg-black">获取中</span></blockquote>
class="layui-badge layui-bg-black">获取中</span></blockquote>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="openresty-start" class="layui-btn">启动</button>
<button id="openresty-stop" class="layui-btn layui-btn-danger">停止</button>
@@ -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('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#openresty-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
@@ -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)

View File

@@ -2,7 +2,7 @@
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-form layui-card-body">
<div class="layui-form layui-card-body" style="overflow: hidden;">
<div class="layui-inline">
<span style="margin-right: 10px;">开启监控</span><input type="checkbox" id="monitor-switch"
lay-filter="monitor" lay-skin="switch"

View File

@@ -268,7 +268,7 @@
// 监听添加端口保存
$('#safe_add_firewall_rule').click(function () {
var port = Number($('#safe_add_firewall_rule_port').val());
var port = $('#safe_add_firewall_rule_port').val();
var protocol = $('#safe_add_firewall_rule_protocol').val();
var index = layer.load();
admin.req({

View File

@@ -85,7 +85,7 @@ Date: 2022-11-28
, {field: 'php', title: 'PHP', width: 60}
, {field: 'ssl', title: 'SSL', width: 110, templet: '#website-ssl'}
, {field: 'note', title: '备注', edit: 'textarea'}
, {fixed: 'right', title: '操作', toolbar: '#website-control', width: 160}
, {fixed: 'right', title: '操作', unresize: true, toolbar: '#website-control', width: 160}
]]
/**
* TODO: 分页