diff --git a/web/admin/site/ssl_acme.py b/web/admin/site/ssl_acme.py index 81938bde1..fea23b068 100644 --- a/web/admin/site/ssl_acme.py +++ b/web/admin/site/ssl_acme.py @@ -42,9 +42,10 @@ def create_acme(): force = request.form.get('force', '') renew = request.form.get('renew', '') email = request.form.get('email', '') + wildcard_domain = request.form.get('wildcard_domain','') apply_type = request.form.get('apply_type', 'file') dnspai = request.form.get('dnspai','') - return MwSites.instance().createAcme(site_name, domains,force,renew,apply_type,dnspai, email) + return MwSites.instance().createAcme(site_name, domains, force, renew, apply_type, dnspai, email, wildcard_domain) diff --git a/web/admin/site/ssl_let.py b/web/admin/site/ssl_let.py new file mode 100644 index 000000000..dda93e6fa --- /dev/null +++ b/web/admin/site/ssl_let.py @@ -0,0 +1,55 @@ +# coding:utf-8 + +# --------------------------------------------------------------------------------- +# MW-Linux面板 +# --------------------------------------------------------------------------------- +# copyright (c) 2018-∞(https://github.com/midoks/mdserver-web) All rights reserved. +# --------------------------------------------------------------------------------- +# Author: midoks +# --------------------------------------------------------------------------------- + +import os +import json + +from flask import Blueprint, render_template +from flask import request + +from admin.user_login_check import panel_login_required + +from utils.plugin import plugin as MwPlugin +from utils.site import sites as MwSites +import core.mw as mw +import thisdb + +from .site import blueprint + + +# 获取ACME日志 +@blueprint.route('/get_let_logs', endpoint='get_let_logs', methods=['POST']) +@panel_login_required +def get_let_logs(): + log_file = MwSites.instance().letLogFile() + if not os.path.exists(log_file): + mw.execShell('touch ' + log_file) + return mw.returnData(True, 'OK', log_file) + + +@blueprint.route('/create_let', endpoint='create_let', methods=['POST']) +@panel_login_required +def create_let(): + site_name = request.form.get('siteName', '') + domains = request.form.get('domains', '') + force = request.form.get('force', '') + renew = request.form.get('renew', '') + email = request.form.get('email', '') + wildcard_domain = request.form.get('wildcard_domain','') + apply_type = request.form.get('apply_type', 'file') + dnspai = request.form.get('dnspai','') + return MwSites.instance().createLet(site_name, domains, force, renew, apply_type, dnspai, email, wildcard_domain) + + + + + + + diff --git a/web/core/mw.py b/web/core/mw.py index 0d7a8f6a2..fc9efa08d 100644 --- a/web/core/mw.py +++ b/web/core/mw.py @@ -836,6 +836,166 @@ def deDoubleCrypt(key, strings): writeFileLog(getTracebackInfo()) return strings +def aesEncrypt(data, key='ABCDEFGHIJKLMNOP', vi='0102030405060708'): + # aes加密 + # @param data 被加密的数据 + # @param key 加解密密匙 16位 + # @param vi 16位 + + from cryptography.hazmat.primitives import padding + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + + if not isinstance(data, bytes): + data = data.encode() + + # AES_CBC_KEY = os.urandom(32) + # AES_CBC_IV = os.urandom(16) + + AES_CBC_KEY = key.encode() + AES_CBC_IV = vi.encode() + + # print("AES_CBC_KEY:", AES_CBC_KEY) + # print("AES_CBC_IV:", AES_CBC_IV) + + padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_data = padder.update(data) + padder.finalize() + + cipher = Cipher(algorithms.AES(AES_CBC_KEY), + modes.CBC(AES_CBC_IV), + backend=default_backend()) + encryptor = cipher.encryptor() + + edata = encryptor.update(padded_data) + + # print(edata) + # print(str(edata)) + # print(edata.decode()) + return edata + + +def aesDecrypt(data, key='ABCDEFGHIJKLMNOP', vi='0102030405060708'): + # aes加密 + # @param data 被解密的数据 + # @param key 加解密密匙 16位 + # @param vi 16位 + + from cryptography.hazmat.primitives import padding + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + + from cryptography.hazmat.primitives import padding + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend + + if not isinstance(data, bytes): + data = data.encode() + + AES_CBC_KEY = key.encode() + AES_CBC_IV = vi.encode() + + cipher = Cipher(algorithms.AES(AES_CBC_KEY), + modes.CBC(AES_CBC_IV), + backend=default_backend()) + decryptor = cipher.decryptor() + + ddata = decryptor.update(data) + + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + data = unpadder.update(ddata) + + try: + uppadded_data = data + unpadder.finalize() + except ValueError: + raise Exception('无效的加密信息!') + + return uppadded_data + +def aesEncrypt_Crypto(data, key, vi): + # 该方法保留,暂时不使用 + # aes加密 + # @param data 被加密的数据 + # @param key 加解密密匙 16位 + # @param vi 16位 + + from Crypto.Cipher import AES + cryptor = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8')) + # 判断是否含有中文 + zhmodel = re.compile(u'[\u4e00-\u9fff]') + match = zhmodel.search(data) + if match == None: + # 无中文时 + add = 16 - len(data) % 16 + pad = lambda s: s + add * chr(add) + data = pad(data) + enctext = cryptor.encrypt(data.encode('utf8')) + else: + # 含有中文时 + data = data.encode() + add = 16 - len(data) % 16 + data = data + add * (chr(add)).encode() + enctext = cryptor.encrypt(data) + encodestrs = base64.b64encode(enctext).decode('utf8') + return encodestrs + + +def aesDecrypt_Crypto(data, key, vi): + # 该方法保留,暂时不使用 + # aes加密 + # @param data 被加密的数据 + # @param key 加解密密匙 16位 + # @param vi 16位 + + from crypto.Cipher import AES + data = data.encode('utf8') + encodebytes = base64.urlsafe_b64decode(data) + cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8')) + text_decrypted = cipher.decrypt(encodebytes) + # 判断是否含有中文 + zhmodel = re.compile(u'[\u4e00-\u9fff]') + match = zhmodel.search(text_decrypted) + if match == False: + # 无中文时补位 + unpad = lambda s: s[0:-s[-1]] + text_decrypted = unpad(text_decrypted) + text_decrypted = text_decrypted.decode('utf8').rstrip() # 去掉补位的右侧空格 + return text_decrypted + +def getDefault(data,val,def_val=''): + if val in data: + return data[val] + return def_val + +def encodeImage(imgsrc, newsrc): + # 图片加密 + import struct + old_fp = open(imgsrc, 'rb') + imgFile = old_fp.read() + old_fp.close() + + new_fp = open(newsrc,"wb") + for x in imgFile: + value = x ^ 86 + value = hex(value) + s = struct.pack('B',int(value,16)) + new_fp.write(s) + new_fp.close() + return True + +def buildSoftLink(src, dst, force=False): + ''' + 建立软连接 + ''' + if not os.path.exists(src): + return False + + if os.path.exists(dst) and force: + os.remove(dst) + + if not os.path.exists(dst): + execShell('ln -sf "' + src + '" "' + dst + '"') + return True + return False # ------------------------------ network start ----------------------------- def HttpGet(url, timeout=10): @@ -1009,6 +1169,14 @@ def panelCmd(method): # ------------------------------ openresty start ----------------------------- +def getOpVer(): + version = '' + version_file_pl = getServerDir() + '/openresty/version.pl' + if os.path.exists(version_file_pl): + version = readFile(version_file_pl) + version = version.strip() + return version + def checkWebConfig(): op_dir = getServerDir() + '/openresty/nginx' # "ulimit -n 10240 && " + @@ -1179,6 +1347,10 @@ def getMyORM(): # --------------------------------------------------------------------------------- ##################### ssl start ######################################### + +def strfDate(sdate): + return time.strftime('%Y-%m-%d', time.strptime(sdate, '%Y%m%d%H%M%S')) + # 获取证书名称 def getCertName(certPath): if not os.path.exists(certPath): @@ -1205,11 +1377,9 @@ def getCertName(certPath): if hasattr(issuer, 'O'): result['issuer'] = issuer.O # 取到期时间 - result['notAfter'] = strfDate( - bytes.decode(x509.get_notAfter())[:-1]) + result['notAfter'] = strfDate(bytes.decode(x509.get_notAfter())[:-1]) # 取申请时间 - result['notBefore'] = strfDate( - bytes.decode(x509.get_notBefore())[:-1]) + result['notBefore'] = strfDate(bytes.decode(x509.get_notBefore())[:-1]) # 取可选名称 result['dns'] = [] for i in range(x509.get_extension_count()): diff --git a/web/static/app/site.js b/web/static/app/site.js index 50c8eeb13..30abea07c 100755 --- a/web/static/app/site.js +++ b/web/static/app/site.js @@ -2119,13 +2119,87 @@ function renderDnsapi(){ } }); - $('#dnsapi_set').on('click', function(){ + $('#dnsapi_set').unbind('click').on('click', function(){ var index = $('#dnsapi_option option:selected').attr('index'); renderDnsapiHtml(data[index]); }); },'json'); } +function opSSLNow(type, id, siteName, callback){ + var now = '
\ + \ +
\ +
密钥(KEY)
\ +
证书(PEM格式)
\ +
\ +
\ + \ +
\ +
\ + '; + + $(".tab-con").html(now); + var key = ''; + var csr = ''; + var loadT = layer.msg('正在提交任务...',{icon:16,time:0,shade: [0.3, '#000']}); + $.post('/site/get_ssl','site_name='+siteName,function(data){ + layer.close(loadT); + var rdata = data['data']; + + if (rdata['cert_data']){ + var issuer = rdata['cert_data']['issuer'].split(" "); + var domains = rdata['cert_data']['dns'].join("、"); + + var cert_data = "
\ +
证书品牌:"+issuer[0]+"
\ +
到期时间:剩余"+rdata['cert_data']['endtime']+"天到期
\ +
\ +
\ +
认证域名:"+domains+"
\ +
强制HTTPS:\ + \ +
\ +
"; + $(".ssl_state_info").html(cert_data); + $(".ssl_state_info").css('display','block'); + } + + if(rdata.key == false){ + rdata.key = ''; + } else { + $(".ssl-btn").append(''); + } + + if(rdata.csr == false){ + rdata.csr = ''; + } + $("#key").val(rdata.key); + $("#csr").val(rdata.csr); + + $("#toHttps").attr('checked',rdata.httpTohttps); + if(rdata.status){ + $('.warning_info').css('display','none'); + + $(".ssl-btn").append(""); + $('#now_ssl').html('当前证书 - [已部署SSL]'); + } else{ + $('.warning_info').css('display','block'); + $('#now_ssl').html('当前证书 - [未部署SSL]'); + } + + + if (typeof (callback) != 'undefined'){ + callback(rdata); + } + },'json'); +} + function opSSLAcme(type, id, siteName, callback){ var acme = '
\
\ @@ -2192,7 +2266,6 @@ function opSSLAcme(type, id, siteName, callback){ } }); - renderDnsapi(); $.post('/site/get_ssl','site_name='+siteName+'&ssl_type=acme', function(data){ @@ -2278,92 +2351,33 @@ function opSSLAcme(type, id, siteName, callback){ },'json'); } -function opSSLNow(type, id, siteName, callback){ - var now = '
\ - \ -
\ -
密钥(KEY)
\ -
证书(PEM格式)
\ -
\ -
\ - \ -
\ -
\ -
    \ -
  • 粘贴您的*.key以及*.pem内容,然后保存即可。
  • \ -
  • 如果浏览器提示证书链不完整,请检查是否正确拼接PEM证书
  • PEM格式证书 = 域名证书.crt + 根证书(root_bundle).crt
  • \ -
  • 在未指定SSL默认站点时,未开启SSL的站点使用HTTPS会直接访问到已开启SSL的站点
  • \ -
'; - - $(".tab-con").html(now); - var key = ''; - var csr = ''; - var loadT = layer.msg('正在提交任务...',{icon:16,time:0,shade: [0.3, '#000']}); - $.post('/site/get_ssl','site_name='+siteName,function(data){ - layer.close(loadT); - var rdata = data['data']; - - if (rdata['cert_data']){ - var issuer = rdata['cert_data']['issuer'].split(" "); - var domains = rdata['cert_data']['dns'].join("、"); - - var cert_data = "
\ -
证书品牌:"+issuer[0]+"
\ -
到期时间:剩余"+rdata['cert_data']['endtime']+"天到期
\ -
\ -
\ -
认证域名:"+domains+"
\ -
强制HTTPS:\ - \ -
\ -
"; - $(".ssl_state_info").html(cert_data); - $(".ssl_state_info").css('display','block'); - } - - if(rdata.key == false){ - rdata.key = ''; - } else { - $(".ssl-btn").append(''); - } - - if(rdata.csr == false){ - rdata.csr = ''; - } - $("#key").val(rdata.key); - $("#csr").val(rdata.csr); - - $("#toHttps").attr('checked',rdata.httpTohttps); - if(rdata.status){ - $('.warning_info').css('display','none'); - - $(".ssl-btn").append(""); - $('#now_ssl').html('当前证书 - [已部署SSL]'); - } else{ - $('.warning_info').css('display','block'); - $('#now_ssl').html('当前证书 - [未部署SSL]'); - } - - - if (typeof (callback) != 'undefined'){ - callback(rdata); - } - },'json'); -} - function opSSLLet(type, id, siteName, callback){ var lets = '
\
\
\ -
\ - 验证方式\ -
\ - \ - \ -
\ -
\ + 验证方式\ +
\ + \ + \ + \ + \ +
\
\ + \ + \
\
\ \ @@ -2391,6 +2405,20 @@ function opSSLLet(type, id, siteName, callback){
'; $(".tab-con").html(lets); + + $('input[name="apply_type"]').on('change', function(){ + var val = $(this).val(); + if (val == 'file'){ + $('#dnsapi_option').css('display','none'); + $('#wildcard_domain_block').css('display','none'); + } else { + $('#dnsapi_option').css('display','block'); + $('#wildcard_domain_block').css('display','block'); + } + }); + + renderDnsapi(); + $.post('/site/get_ssl', 'site_name='+siteName+'&ssl_type=lets', function(data){ var rdata = data['data']; if(rdata.csr == false){ @@ -2517,46 +2545,58 @@ function ocSSL(action,siteName){ //生成SSL function newSSL(siteName, id, domains){ showSpeedWindow('正在申请...', 'site.get_let_logs', function(layers,index){ - var force = ''; - if ($("#checkDomain").prop("checked")){ - force = '&force=true'; - } - var email = $("input[name='admin_email']").val(); - $.post('/site/create_let','siteName='+siteName+'&domains='+domains+'&email='+email + force,function(rdata){ - layer.close(index); - if(rdata.status){ - showMsg(rdata.msg, function(){ + var pdata = {}; + pdata['siteName'] = siteName; + pdata['domains'] = domains; + pdata['email'] = $("input[name='admin_email']").val(); + + if($("#checkDomain").prop("checked")){ + pdata['force'] = 'true'; + } + + if($("#wildcard_domain").prop("checked")){ + pdata['wildcard_domain'] = 'true'; + } + + var apply_type = $('input[name="apply_type"]:checked').val(); + pdata['apply_type'] = apply_type; + if (apply_type == 'dns'){ + pdata['dnspai'] = $('#dnsapi_option option:selected').val(); + } + + $.post('/site/create_let',pdata,function(rdata){ + showMsg(rdata.msg, function(){ + layer.close(index); + if(rdata.status){ $(".tab-nav span:first-child").click(); - },{icon:1}, 2000); - return; - } - layer.msg(rdata.msg,{icon:2,area:'500px',time:0,shade:0.3,shadeClose:true}); + } + },{icon:rdata.status?1:2}, 3000); },'json'); }); } function newAcmeSSL(siteName, id, domains){ showSpeedWindow('正在由ACME申请...', 'site.get_acme_logs', function(layers,index){ - var force = ''; - var email = $("input[name='admin_email']").val(); - var apply_type = $('input[name="apply_type"]:checked').val(); - var pdata = {}; pdata['siteName'] = siteName; pdata['domains'] = domains; - pdata['email'] = email; - pdata['apply_type'] = apply_type; + pdata['email'] = $("input[name='admin_email']").val(); + if($("#checkDomain").prop("checked")){ pdata['force'] = 'true'; } + if($("#wildcard_domain").prop("checked")){ + pdata['wildcard_domain'] = 'true'; + } + + var apply_type = $('input[name="apply_type"]:checked').val(); + pdata['apply_type'] = apply_type; if (apply_type == 'dns'){ pdata['dnspai'] = $('#dnsapi_option option:selected').val(); } - console.log(pdata); $.post('/site/create_acme',pdata,function(rdata){ - console.log(rdata); showMsg(rdata.msg, function(){ layer.close(index); if(rdata.status){ diff --git a/web/utils/site.py b/web/utils/site.py index 3cfa99822..cf88d95ef 100644 --- a/web/utils/site.py +++ b/web/utils/site.py @@ -419,6 +419,74 @@ class sites(object): mw.writeLog('网站管理', '设置成功,站点【{1}】到期【{2}】后将自动停止!', (info['name'], end_date,)) return mw.returnData(True, '设置成功,站点到期后将自动停止!') + # ssl相关方法 start + def setSslConf(self, site_name): + file = self.getHostConf(site_name) + conf = mw.readFile(file) + + version = mw.getOpVer() + + keyPath = self.sslDir + '/' + site_name + '/privkey.pem' + certPath = self.sslDir + '/' + site_name + '/fullchain.pem' + if conf: + if conf.find('ssl_certificate') == -1: + # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; + # add_header Alt-Svc 'h3=":443";ma=86400,h3-29=":443";ma=86400'; + http3Header = """ + add_header Strict-Transport-Security "max-age=63072000"; + add_header Alt-Svc 'h3=":443";ma=86400'; +""" + if not version.startswith('1.25'): + http3Header = ''; + + sslStr = """#error_page 404/404.html; + ssl_certificate %s; + ssl_certificate_key %s; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + %s + error_page 497 https://$host$request_uri;""" % (certPath, keyPath, http3Header) + if(conf.find('ssl_certificate') != -1): + return mw.returnData(True, 'SSL开启成功!') + + conf = conf.replace('#error_page 404/404.html;', sslStr) + + rep = r"listen\s+([0-9]+)\s*[default_server|reuseport]*;" + tmp = re.findall(rep, conf) + if not mw.inArray(tmp, '443'): + listen = re.search(rep, conf).group() + + if version.startswith('1.25'): + http_ssl = "\n\tlisten 443 ssl;" + http_ssl = http_ssl + "\n\tlisten [::]:443 ssl;" + http_ssl = http_ssl + "\n\tlisten 443 quic;" + http_ssl = http_ssl + "\n\tlisten [::]:443 quic;" + http_ssl = http_ssl + "\n\thttp2 on;" + else: + http_ssl = "\n\tlisten 443 ssl http2;" + http_ssl = http_ssl + "\n\tlisten [::]:443 ssl http2;" + + + conf = conf.replace(listen, listen + http_ssl) + + mw.backFile(file) + mw.writeFile(file, conf) + isError = mw.checkWebConfig() + if(isError != True): + mw.restoreFile(file) + return mw.returnData(False, '证书错误:
' + isError.replace("\n", '
') + '
') + + self.saveCert(keyPath, certPath) + + msg = mw.getInfo('网站[{1}]开启SSL成功!', (site_name,)) + mw.writeLog('网站管理', msg) + + mw.restartWeb() + return mw.returnData(True, 'SSL开启成功!') + # 设置网站备注 def setPs(self, site_id, ps): if thisdb.setSitesData(site_id, ps=ps): @@ -1466,6 +1534,7 @@ location ^~ {from} {\n\ csr = mw.readFile(csr_path) cert_data = mw.getCertName(csr_path) + # print(csr_path,cert_data) data = { 'status': status, 'domain': domains, @@ -1477,6 +1546,21 @@ location ^~ {from} {\n\ } return mw.returnData(True, 'OK', data) + def saveCert(self, keyPath, certPath): + try: + certInfo = mw.getCertName(certPath) + if not certInfo: + return mw.returnData(False, '证书解析失败!') + vpath = self.sslDir + '/' + certInfo['subject'].strip() + if not os.path.exists(vpath): + os.system('mkdir -p ' + vpath) + mw.writeFile(vpath + '/privkey.pem', mw.readFile(keyPath)) + mw.writeFile(vpath + '/fullchain.pem', mw.readFile(certPath)) + mw.writeFile(vpath + '/info.json', json.dumps(certInfo)) + return mw.returnData(True, '证书保存成功!') + except Exception as e: + return mw.returnData(False, '证书保存失败!') + def getCertList(self): try: vpath = self.sslDir @@ -1657,7 +1741,6 @@ location ^~ {from} {\n\ thisdb.setOption('dnsapi',json.dumps(dnsapi_data)) return mw.returnData(True, '设置成功!') - def acmeLogFile(self): return mw.getPanelDir() + '/logs/acme.log' @@ -1666,13 +1749,20 @@ location ^~ {from} {\n\ mw.writeFile(log_file, msg+"\n", "wb+") return True + def letLogFile(self): + return mw.getPanelDir() + '/logs/letsencrypt.log' + + def writeLetLog(self,msg): + log_file = self.letLogFile() + mw.writeFile(log_file, msg+"\n", "wb+") + return True + def createAcmeMultiDomin(self): pass - def createAcmeFile(self, site_name, domains,force,renew, apply_type, dnspai, email): - + def createAcmeFile(self, site_name, domains, email, force, renew): - print(site_name, domains,force,renew,apply_type, dnspai, email) + print(site_name, domains,force,renew, email) file = self.getHostConf(site_name) @@ -1702,20 +1792,7 @@ location ^~ {from} {\n\ srcPath = siteInfo['path'] - # 检测acme是否安装 - acme_dir = mw.getAcmeDir() - if not os.path.exists(acme_dir): - try: - mw.execShell("curl -sS curl https://get.acme.sh | sh") - except: - pass - if not os.path.exists(acme_dir): - return mw.returnData(False, '尝试自动安装ACME失败,请通过以下命令尝试手动安装

安装命令: curl https://get.acme.sh | sh

') - - # 避免频繁执行 - checkAcmeRun = mw.execShell('ps -ef|grep acme.sh |grep -v grep') - if checkAcmeRun[0] != '': - return mw.returnData(False, '正在申请或更新SSL中...') + if force == 'true': force_bool = True @@ -1813,7 +1890,7 @@ location ^~ {from} {\n\ top_domain = s[last_index-1]+'.'+s[last_index] return top_domain - def createAcmeDns(self, site_name, domains, dnspai, force, renew): + def createAcmeDns(self, site_name, domains, dnspai, wildcard_domain, force, renew): dnsapi_option = thisdb.getOptionByJson('dnsapi', default={}) if not dnspai in dnsapi_option: return mw.returnData(False, dnspai+'未设置') @@ -1823,29 +1900,187 @@ location ^~ {from} {\n\ if dnsapi_data[k] == '': return mw.returnData(False, k+'为空!') - cmd = self.getDnsapiExportVar(dnsapi_data) - + acme_dir = mw.getAcmeDir() for d in domains: - top_domain = self.getDomainRootName(d) - cmd += 'acme.sh --issue --dns '+str(dnspai)+' -d '+top_domain+' -d "*.'+top_domain+'" --force' + cmd = ''' +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin:/opt/homebrew/bin:%s +export PATH +''' % (acme_dir,) + + cmd += self.getDnsapiExportVar(dnsapi_data) + if wildcard_domain == 'true': + top_domain = self.getDomainRootName(d) + cmd += 'acme.sh --issue --dns '+str(dnspai)+' -d '+top_domain+' -d "*.'+top_domain+'"' + d = top_domain + else: + cmd += 'acme.sh --issue --dns '+str(dnspai)+' -d '+d + + log_file = self.acmeLogFile() + cmd += ' >> ' + log_file + print(cmd) + result = mw.execShell(cmd) + print(result) + + # acme源的ssl证书 + src_path = mw.getAcmeDomainDir(d) + src_cert = src_path + '/fullchain.cer' + src_key = src_path + '/' + d + '.key' + src_cert.replace("\\*", "*") + + msg = '签发失败,您尝试申请证书的失败次数已达上限!\ +

1、检查域名是否正确解析到本服务器,或解析还未完全生效

\ +

2、如果以上检查都确认没有问题,请尝试更换DNS服务商

' + if not os.path.exists(src_cert): + data = {} + data['err'] = result + data['out'] = result[0] + data['msg'] = msg + data['result'] = {} + if result[1].find('new-authz error:') != -1: + data['result'] = json.loads(re.search("{.+}", result[1]).group()) + if data['result']['status'] == 429: + data['msg'] = msg + data['status'] = False + return data + + # acme源建立软链接(目标) + dst_path = self.sslDir + '/' + site_name + dst_cert = dst_path + "/fullchain.pem" # 生成证书路径 + dst_key = dst_path + "/privkey.pem" # 密钥文件路径 + + if not os.path.exists(dst_path): + mw.execShell("mkdir -p " + dst_path) + + mw.buildSoftLink(src_cert, dst_cert, True) + mw.buildSoftLink(src_key, dst_key, True) + mw.execShell('echo "acme" > "' + dst_path + '/README"') + + # 写入配置文件 + result = self.setSslConf(site_name) + if not result['status']: + return result + result['csr'] = mw.readFile(src_cert) + result['key'] = mw.readFile(src_key) - print(dnsapi_data) - print(domains) - print(cmd) - return mw.returnData(False, '测试中!') + mw.restartWeb() + return mw.returnData(True, '证书已更新!', result) - def createAcme(self, site_name, domains,force,renew, apply_type, dnspai, email): + def createAcme(self, site_name, domains,force,renew, apply_type, dnspai, email, wildcard_domain): domains = json.loads(domains) if len(domains) < 1: return mw.returnData(False, '请选择域名') if email.strip() != '': thisdb.setOption('ssl_email', email) + # 检测acme是否安装 + acme_dir = mw.getAcmeDir() + if not os.path.exists(acme_dir): + try: + mw.execShell("curl -sS curl https://get.acme.sh | sh") + except: + pass + if not os.path.exists(acme_dir): + return mw.returnData(False, '尝试自动安装ACME失败,请通过以下命令尝试手动安装

安装命令: curl https://get.acme.sh | sh

') + + # 避免频繁执行 + checkAcmeRun = mw.execShell('ps -ef|grep acme.sh |grep -v grep') + if checkAcmeRun[0] != '': + return mw.returnData(False, '正在申请或更新SSL中...') + if apply_type == 'file': - return self.createAcmeFile(site_name, domains,force,renew, apply_type, dnspai, email) + return self.createAcmeFile(site_name, domains, email,force,renew) elif apply_type == 'dns': - return self.createAcmeDns(site_name, domains, dnspai,force, renew) - return mw.returnData(False, '异常请求') + return self.createAcmeDns(site_name, domains, dnspai, wildcard_domain,force, renew) + return mw.returnData(False, '异常请求') + + def createLet(self, site_name, domains, force, renew, apply_type, dnspai, email, wildcard_domain): + siteName = request.form.get('siteName', '') + domains = request.form.get('domains', '') + force = request.form.get('force', '') + renew = request.form.get('renew', '') + email_args = request.form.get('email', '') + + domains = json.loads(domains) + email = mw.M('users').getField('email') + if email_args.strip() != '': + mw.M('users').setField('email', email_args) + email = email_args + + if not len(domains): + return mw.returnJson(False, '请选择域名') + + host_conf_file = self.getHostConf(siteName) + if os.path.exists(host_conf_file): + siteConf = mw.readFile(host_conf_file) + if siteConf.find('301-END') != -1: + return mw.returnJson(False, '检测到您的站点做了301重定向设置,请先关闭重定向!') + + # 检测存在反向代理 + data_path = self.getProxyDataPath(siteName) + data_content = mw.readFile(data_path) + if data_content != False: + try: + data = json.loads(data_content) + except: + pass + for proxy in data: + proxy_dir = "{}/{}".format(self.proxyPath, siteName) + proxy_dir_file = proxy_dir + '/' + proxy['id'] + '.conf' + if os.path.exists(proxy_dir_file): + return mw.returnJson(False, '检测到您的站点做了反向代理设置,请先关闭反向代理!') + + # fix binddir domain ssl apply question + mw.backFile(host_conf_file) + auth_to = self.getSitePath(siteName) + rep = r"\s*root\s*(.+);" + replace_root = "\n\troot " + auth_to + ";" + siteConf = re.sub(rep, replace_root, siteConf) + mw.writeFile(host_conf_file, siteConf) + mw.restartWeb() + + to_args = { + 'domains': domains, + 'auth_type': 'http', + 'auth_to': auth_to, + } + + src_letpath = mw.getServerDir() + '/web_conf/letsencrypt/' + siteName + src_csrpath = src_letpath + "/fullchain.pem" # 生成证书路径 + src_keypath = src_letpath + "/privkey.pem" # 密钥文件路径 + + dst_letpath = self.sslDir + '/' + siteName + dst_csrpath = dst_letpath + '/fullchain.pem' + dst_keypath = dst_letpath + '/privkey.pem' + + if not os.path.exists(src_letpath): + import cert_api + data = cert_api.cert_api().applyCertApi(to_args) + mw.restoreFile(host_conf_file) + if not data['status']: + msg = data['msg'] + if type(data['msg']) != str: + msg = data['msg'][0] + emsg = data['msg'][1]['challenges'][0]['error'] + msg = msg + '

响应状态:' + str(emsg['status']) + '

错误类型:' + emsg[ + 'type'] + '

错误代码:' + emsg['detail'] + '

' + return mw.returnJson(data['status'], msg, data['msg']) + + mw.execShell('mkdir -p ' + dst_letpath) + mw.buildSoftLink(src_csrpath, dst_csrpath, True) + mw.buildSoftLink(src_keypath, dst_keypath, True) + mw.execShell('echo "lets" > "' + dst_letpath + '/README"') + + # 写入配置文件 + result = self.setSslConf(siteName) + if not result['status']: + return mw.getJson(result) + + result['csr'] = mw.readFile(src_csrpath) + result['key'] = mw.readFile(src_keypath) + + mw.restartWeb() + return mw.returnData(data['status'], data['msg'], result) def getPhpVersion(self):