' + v.name + '
' + + 'diff --git a/app/Http/Controllers/Api/SettingsController.php b/app/Http/Controllers/Api/SettingsController.php index fd10b6d8..168e6995 100644 --- a/app/Http/Controllers/Api/SettingsController.php +++ b/app/Http/Controllers/Api/SettingsController.php @@ -57,6 +57,7 @@ class SettingsController extends Controller $data = [ 'name' => $settingArr['name'], 'username' => $request->user()->username, + 'email' => $request->user()->email, 'password' => '', 'port' => $matches[1], 'api' => $api, @@ -80,19 +81,22 @@ class SettingsController extends Controller $settings = $request->all(); // 将数据入库 foreach ($settings as $key => $value) { - if ($key == 'access_token' || $key == 'username' || $key == 'password' || $key == 'api_token' || $key == 'api' || $key == 'port') { + if ($key == 'access_token' || $key == 'username' || $key == 'email' || $key == 'password' || $key == 'api_token' || $key == 'api' || $key == 'port') { continue; } // 创建或更新 Setting::query()->updateOrCreate(['name' => $key], ['value' => $value]); } - // 单独处理用户名和密码 + // 单独处理用户名、密码、邮箱 if ($request->input('username') != $request->user()->username) { $request->user()->update(['username' => $request->input('username')]); } if ($request->input('password') != '') { $request->user()->update(['password' => Hash::make($request->input('password'))]); } + if ($request->input('email') != $request->user()->email) { + $request->user()->update(['email' => $request->input('email')]); + } // 处理面板端口 $port = $request->input('port'); $nginxConf = file_get_contents('/www/server/nginx/conf/nginx.conf'); diff --git a/app/Http/Controllers/Api/WebsitesController.php b/app/Http/Controllers/Api/WebsitesController.php index 8d05dd55..58e585ee 100644 --- a/app/Http/Controllers/Api/WebsitesController.php +++ b/app/Http/Controllers/Api/WebsitesController.php @@ -9,9 +9,12 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Website; use App\Models\Setting; +use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; +use skoerfgen\ACMECert\ACME_Exception; +use skoerfgen\ACMECert\ACMECert; class WebsitesController extends Controller { @@ -372,6 +375,13 @@ EOF; $website['ssl_certificate_key'] = file_get_contents($matches5[1][0]); $website['http_redirect'] = str_contains($nginx_config, '# http重定向标记位'); $website['hsts'] = str_contains($nginx_config, '# hsts标记位'); + try { + $sslDate = (new ACMECert())->getRemainingDays($website['ssl_certificate']); + $sslDate = round($sslDate, 2); + } catch (Exception $e) { + $sslDate = '未知'; + } + $website['ssl_date'] = $sslDate; } else { $website['ssl_certificate'] = @file_get_contents('/www/server/vhost/ssl/'.$name.'.pem'); $website['ssl_certificate_key'] = @file_get_contents('/www/server/vhost/ssl/'.$name.'.key'); @@ -1027,6 +1037,152 @@ EOF; return response()->json($res); } + /** + * 签发SSL证书 + * @param Request $request + * @return JsonResponse + * @throws ACME_Exception|Exception + */ + public function issueSsl(Request $request): JsonResponse + { + try { + $input = $this->validate($request, [ + 'type' => 'required|in:lets,buypass,google,sslcom,zerossl', + 'name' => 'required', + ]); + } catch (ValidationException $e) { + return response()->json([ + 'code' => 1, + 'msg' => '参数错误:'.$e->getMessage(), + 'errors' => $e->errors() + ], 200); + } + + $user = $request->user(); + + // 检查网站是否存在 + $website = Website::query()->where('name', $input['name'])->first(); + if (!$website) { + return response()->json([ + 'code' => 1, + 'msg' => '网站不存在', + ], 200); + } + // 从配置文件中获取网站域名 + $nginxConfig = file_get_contents('/www/server/vhost/'.$website['name'].'.conf'); + $domainConfig = $this->cut('# server_name标记位开始', '# server_name标记位结束', $nginxConfig); + preg_match_all('/server_name\s+(.+);/', $domainConfig, $matches1); + $domains = explode(" ", $matches1[1][0]); + // 从配置文件中获取网站目录 + $pathConfig = $this->cut('# root标记位开始', '# root标记位结束', $nginxConfig); + preg_match_all('/root\s+(.+);/', $pathConfig, $matches2); + $path = $matches2[1][0]; + + /** + * 对域名需要进行一下处理,如果域名是泛域名,返回暂不支持泛域名 + */ + foreach ($domains as $domain) { + if (str_contains($domain, '*')) { + return response()->json([ + 'code' => 1, + 'msg' => '暂不支持泛域名', + ], 200); + } + } + + switch ($input['type']) { + case 'lets': + $ac = new ACMECert('https://acme-v02.api.letsencrypt.org/directory'); + break; + case 'buypass': + $ac = new ACMECert('https://api.buypass.com/acme/directory'); + break; + case 'google': + $ac = new ACMECert('https://dv.acme-v02.api.pki.goog/directory'); + break; + case 'sslcom': + $ac = new ACMECert('https://acme.ssl.com/sslcom-dv-rsa'); + break; + case 'zerossl': + $ac = new ACMECert('https://acme.zerossl.com/v2/DV90'); + break; + default: + $res = [ + 'code' => 1, + 'msg' => '参数错误:type', + ]; + return response()->json($res); + break; + } + + try { + $accountKey = $ac->generateECKey('P-384'); + $certKey = $ac->generateECKey('P-384'); + } catch (Exception $e) { + return response()->json([ + 'code' => 1, + 'msg' => '生成密钥失败:'.$e->getMessage(), + ], 200); + } + try { + $ac->loadAccountKey($accountKey); + } catch (Exception $e) { + return response()->json([ + 'code' => 1, + 'msg' => '加载密钥失败:'.$e->getMessage(), + ], 200); + } + try { + $ac->register(true, $user->email); + } catch (Exception $e) { + return response()->json([ + 'code' => 1, + 'msg' => '注册CA账户失败:'.$e->getMessage(), + ], 200); + } + + // 初始化域名数组 + $domainConfig = []; + foreach ($domains as $domain) { + $domainConfig[$domain] = [ + 'challenge' => 'http-01', + 'docroot' => $path + ]; + } + + $handler = function ($opts) { + $fn = $opts['config']['docroot'].$opts['key']; + @mkdir(dirname($fn), 0777, true); + file_put_contents($fn, $opts['value']); + return function ($opts) { + unlink($opts['config']['docroot'].$opts['key']); + }; + }; + + // 申请证书 + try { + $fullchain = $ac->getCertificateChain($certKey, $domainConfig, $handler); + } catch (ACME_Exception $e) { + return response()->json([ + 'code' => 1, + 'msg' => '申请证书失败:'.$e->getMessage(), + ], 200); + } + + // 写入证书 + $sslDir = '/www/server/vhost/ssl/'; + file_put_contents($sslDir.$website['name'].'.key', $certKey); + file_put_contents($sslDir.$website['name'].'.pem', $fullchain); + + // 返回 + $res = [ + 'code' => 0, + 'msg' => 'success', + ]; + return response()->json($res); + } + + /** * 裁剪字符串 * @param $begin diff --git a/composer.json b/composer.json index 00024ac0..2f6a5fef 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "laravel/framework": "^9.19", "laravel/sanctum": "^3.0", "laravel/tinker": "^2.7", - "overtrue/laravel-lang": "^6.0" + "overtrue/laravel-lang": "^6.0", + "skoerfgen/acmecert": "^3.2" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index cdaf39d0..4b52f804 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "efb63260d6e5ee652c84bfee6f458ada", + "content-hash": "199471d49176a915109df8e3774d6e7e", "packages": [ { "name": "brick/math", @@ -2980,6 +2980,50 @@ ], "time": "2022-09-16T03:22:46+00:00" }, + { + "name": "skoerfgen/acmecert", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/skoerfgen/ACMECert.git", + "reference": "5cf724cebf4bcab13ec609308d6a0cbd136ccdfd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/skoerfgen/ACMECert/zipball/5cf724cebf4bcab13ec609308d6a0cbd136ccdfd", + "reference": "5cf724cebf4bcab13ec609308d6a0cbd136ccdfd", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=5.6.0" + }, + "suggest": { + "ext-curl": "Optional for better http performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "skoerfgen\\ACMECert\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stefan Körfgen", + "homepage": "https://github.com/skoerfgen" + } + ], + "description": "PHP client library for Let's Encrypt and other ACME v2 - RFC 8555 compatible Certificate Authorities", + "support": { + "issues": "https://github.com/skoerfgen/ACMECert/issues", + "source": "https://github.com/skoerfgen/ACMECert" + }, + "time": "2022-08-03T21:36:49+00:00" + }, { "name": "symfony/console", "version": "v6.1.7", diff --git a/config/panel.php b/config/panel.php index 27284c5f..18637972 100644 --- a/config/panel.php +++ b/config/panel.php @@ -1,6 +1,6 @@ '耗子Linux面板', - 'version' => '20221205', + 'version' => '20221208', 'plugin_dir' => '/www/panel/plugins', ]; \ No newline at end of file diff --git a/public/panel/modules/file.js b/public/panel/modules/file.js new file mode 100644 index 00000000..7fbb0de9 --- /dev/null +++ b/public/panel/modules/file.js @@ -0,0 +1,401 @@ +layui.define(['jquery', 'layer', 'laypage'], function(exports) { //提示:模块也可以依赖其它模块,如:layui.define('layer', callback); + var $ = layui.jquery, + layer = layui.layer, + laypage = layui.laypage; + //外部接口 + var fm = { + config:{'test':'test','thumb':{'nopic':'',width:100,height:100},icon_url:'ico/',btn_upload:true,btn_create:true} + ,cache: {} //数据缓存 + ,index: layui.fm ? (layui.fm.index + 10000) : 0 + //设置全局项 + ,set: function(options){ + var that = this; + that.config = $.extend({}, that.config, options); + return that; + } + //事件监听 + ,on: function(events, callback){ + return layui.onevent.call(this, 'file', events, callback); + } + ,dirRoot:[{'path':'','name': '根目录'}] + ,v:'1.0.1.2019.12.26' + } + //操作当前实例 + , thisFm = function() { + var that = this, + options = that.config, + id = options.id || options.index; + + // console.log(id) + if (id) { + thisFm.that[id] = that; //记录当前实例对象 + thisFm.config[id] = options; //记录当前实例配置项 + } + return { + config: options, + reload: function(options) { + that.reload.call(that, options); + } + } + } + //获取当前实例配置项 + ,getThisFmConfig = function(id){ + var config = thisFm.config[id]; + if(!config) hint.error('The ID option was not found in the fm instance'); + return config || null; + } + //构造器 + ,Class = function(options){ + var that = this; + that.config = $.extend({}, that.config, fm.config, options); + //记录所有实例 + thisFm.that = {}; //记录所有实例对象 + thisFm.config = {}; //记录所有实例配置项 + // console.log(that.config) + that.render(); + }; + //渲染 + Class.prototype.render = function(){ + var that = this + ,options = that.config; + + options.elem = $(options.elem); + options.where = options.where || {}; + options.id = options.id || options.elem.attr('id') || that.index; + + //请求参数的自定义格式 + options.request = $.extend({ + pageName: 'page' + ,limitName: 'limit' + }, options.request) + + //响应数据的自定义格式 + options.response = $.extend({ + statusName: 'code' + ,statusCode: 0 + ,msgName: 'msg' + ,dataName: 'data' + ,countName: 'count' + }, options.response); + + //如果 page 传入 laypage 对象 + if(typeof options.page === 'object'){ + options.limit = options.page.limit || options.limit; + options.limits = options.page.limits || options.limits; + that.page = options.page.curr = options.page.curr || 1; + delete options.page.elem; + delete options.page.jump; + } + + if(!options.elem[0]) return that; + //渲染主体 + var _btn = '' + if(options.btn_create){ + _btn +=''; + } + if(options.btn_upload){ + _btn +=''; + } + var _html = '

' + v.name + '
' + + '