Commit 161499b9 authored by jose's avatar jose

Refactored Let'sEncrypt one-click deployment module

parent 93cc3323
......@@ -19,6 +19,7 @@ from flask_session import Session
from werkzeug.contrib.cache import SimpleCache
from werkzeug.wrappers import Response
from flask_socketio import SocketIO,emit,send
dns_client = None
#设置BasicAuth
basic_auth_conf = 'config/basic_auth.json'
......@@ -178,9 +179,9 @@ def is_login(result):
if 'login' in session:
if session['login'] == True:
result = make_response(result)
request_token = public.md5(app.secret_key + str(time.time()))
request_token = public.GetRandomString(48)
session['request_token'] = request_token
result.set_cookie('request_token',request_token,httponly=True,max_age=86400*30)
result.set_cookie('request_token',request_token,max_age=86400*30)
return result
@app.route('/site',methods=method_all)
......@@ -403,7 +404,9 @@ def config(pdata = None):
workers_p = 'data/workers.pl'
if not os.path.exists(workers_p): public.writeFile(workers_p,'1')
data['workers'] = int(public.readFile(workers_p))
data['session_timeout'] = int(public.readFile(sess_out_path))
s_time_tmp = public.readFile(sess_out_path)
if not s_time_tmp: s_time_tmp = '0'
data['session_timeout'] = int(s_time_tmp)
if c_obj.get_ipv6_listen(None): data['ipv6'] = 'checked'
if c_obj.get_token(None)['open']: data['api'] = 'checked'
data['basic_auth'] = c_obj.get_basic_auth_stat(None)
......@@ -420,7 +423,7 @@ def ajax(pdata = None):
if comReturn: return comReturn
import ajax
ajaxObject = ajax.ajax()
defs = ('check_user_auth','to_not_beta','get_beta_logs','apple_beta','GetApacheStatus','GetCloudHtml','get_load_average','GetOpeLogs','GetFpmLogs','GetFpmSlowLogs','SetMemcachedCache','GetMemcachedStatus','GetRedisStatus','GetWarning','SetWarning','CheckLogin','GetSpeed','GetAd','phpSort','ToPunycode','GetBetaStatus','SetBeta','setPHPMyAdmin','delClose','KillProcess','GetPHPInfo','GetQiniuFileList','UninstallLib','InstallLib','SetQiniuAS','GetQiniuAS','GetLibList','GetProcessList','GetNetWorkList','GetNginxStatus','GetPHPStatus','GetTaskCount','GetSoftList','GetNetWorkIo','GetDiskIo','GetCpuIo','CheckInstalled','UpdatePanel','GetInstalled','GetPHPConfig','SetPHPConfig')
defs = ('set_phpmyadmin_ssl','get_phpmyadmin_ssl','check_user_auth','to_not_beta','get_beta_logs','apple_beta','GetApacheStatus','GetCloudHtml','get_load_average','GetOpeLogs','GetFpmLogs','GetFpmSlowLogs','SetMemcachedCache','GetMemcachedStatus','GetRedisStatus','GetWarning','SetWarning','CheckLogin','GetSpeed','GetAd','phpSort','ToPunycode','GetBetaStatus','SetBeta','setPHPMyAdmin','delClose','KillProcess','GetPHPInfo','GetQiniuFileList','UninstallLib','InstallLib','SetQiniuAS','GetQiniuAS','GetLibList','GetProcessList','GetNetWorkList','GetNginxStatus','GetPHPStatus','GetTaskCount','GetSoftList','GetNetWorkIo','GetDiskIo','GetCpuIo','CheckInstalled','UpdatePanel','GetInstalled','GetPHPConfig','SetPHPConfig')
return publicObject(ajaxObject,defs,None,pdata);
@app.route('/system',methods=method_all)
......@@ -481,7 +484,7 @@ def ssl(pdata = None):
if comReturn: return comReturn
import panelSSL
toObject = panelSSL.panelSSL()
defs = ('RemoveCert','SetCertToSite','GetCertList','SaveCert','GetCert','GetCertName','DelToken','GetToken','GetUserInfo','GetOrderList','GetDVSSL','Completed','SyncOrder','GetSSLInfo','downloadCRT','GetSSLProduct','Renew_SSL','Get_Renew_SSL')
defs = ('RemoveCert','renew_lets_ssl','SetCertToSite','GetCertList','SaveCert','GetCert','GetCertName','DelToken','GetToken','GetUserInfo','GetOrderList','GetDVSSL','Completed','SyncOrder','GetSSLInfo','downloadCRT','GetSSLProduct','Renew_SSL','Get_Renew_SSL')
result = publicObject(toObject,defs,None,pdata);
return result;
......@@ -873,14 +876,15 @@ except:
@socketio.on('webssh')
def webssh(msg):
if not check_login():
if not check_login(msg['x_http_token']):
emit('server_response',{'data':public.getMsg('INIT_WEBSSH_LOGOUT')})
return None
global shell,ssh
ssh_success = True
if type(msg) == dict:
if 'ssh_user' in msg:
connect_ssh(msg['ssh_user'].strip(),msg['ssh_passwd'].strip())
if type(msg['data']) == dict:
if 'ssh_user' in msg['data']:
connect_ssh(msg['data']['ssh_user'].strip(),msg['data']['ssh_passwd'].strip())
if not shell: ssh_success = connect_ssh()
if not shell:
emit('server_response',{'data':public.getMsg('INIT_WEBSSH_CONN_ERR')})
......@@ -889,13 +893,10 @@ def webssh(msg):
if not ssh_success:
emit('server_response',{'data':public.getMsg('INIT_WEBSSH_CONN_ERR')})
return;
shell.send(msg)
try:
time.sleep(0.005)
recv = shell.recv(4096)
emit('server_response',{'data':recv.decode("utf-8")})
except Exception as ex:
pass
shell.send(msg['data'])
time.sleep(0.005)
recv = shell.recv(4096)
emit('server_response',{'data':recv.decode("utf-8")})
def connect_ssh(user=None,passwd=None):
global shell,ssh
......@@ -964,35 +965,25 @@ def connected_msg(msg):
if not shell: connect_ssh()
if shell:
try:
#shell.send(msg)
recv = shell.recv(8192)
emit('server_response',{'data':recv.decode("utf-8")})
except:
pass
@socketio.on('panel')
def websocket_test(data):
pdata = data
if not check_login():
emit(pdata.s_response,{'data':public.returnMsg(-1,public.getMsg('INIT_WEBSSH_LOGOUT'))})
return None
mods = ['site','ftp','database','ajax','system','crontab','files','config','panel_data','plugin','ssl','auth','firewall','panel_wxapp']
if not pdata['s_module'] in mods:
result = public.returnMsg(False,"INIT_WEBSOCKET_ERR")
else:
result = eval("%s(pdata)" % pdata['s_module'])
if not hasattr(pdata,'s_response'): pdata.s_response = 'response'
emit(pdata.s_response,{'data':result})
def check_csrf():
request_token = request.cookies.get('request_token')
if session['request_token'] != request_token: return False
http_token = request.headers.get('x-http-token')
if not http_token: return False
if http_token != session['request_token_head']: return False
cookie_token = request.headers.get('x-cookie-token')
if cookie_token != session['request_token']: return False
return True
def publicObject(toObject,defs,action=None,get = None):
if 'request_token' in session and 'login' in session:
request_token = request.cookies.get('request_token')
if session['request_token'] != request_token:
if session['login'] != False:
session['login'] = False;
cache.set('dologin',True)
return redirect('/login')
if not check_csrf(): return public.ReturnJson(False,'Csrf-Token error.'),json_header
if not get: get = get_input()
if action: get.action = action
......@@ -1015,10 +1006,12 @@ def publicObject(toObject,defs,action=None,get = None):
return public.ReturnJson(False,'ARGS_ERR'),json_header
def check_login():
def check_login(http_token=None):
if cache.get('dologin'): return False
if 'login' in session:
loginStatus = session['login']
if loginStatus and http_token:
if session['request_token_head'] != http_token: return False
return loginStatus
return False
......
......@@ -4,8 +4,7 @@ var site = {
get_list: function (page, search, type) {
if (page == undefined) page = 1;
if (type == '-1' || type == undefined) {
console.log($('.site_type select').val())
type = $('.site_type select').val();
type = $('.site_type select').val();
}
bt.site.get_list(page, search, type, function (rdata) {
$('.dataTables_paginate').html(rdata.page);
......@@ -347,7 +346,6 @@ var site = {
}
layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 });
});
console.log(php_version)
},
del_site: function (wid, wname) {
var thtml = "<div class='options' style='width: 320px'><label><input type='checkbox' id='delftp' name='ftp'><span>FTP</span></label><label><input type='checkbox' id='deldata' name='data'><span>" + lan.site.database + "</span></label><label><input type='checkbox' id='delpath' name='path'><span>" + lan.site.root_dir + "</span></label></div>";
......@@ -528,13 +526,60 @@ var site = {
},
ssl: {
my_ssl_msg : null,
renew_ssl: function () {
$.post('/ssl?action=Renew_SSL', {}, function (rdata) {
layer.msg(rdata.msg, { icon: rdata.status ? 1 : 2 });
renew_ssl: function (siteName) {
data = {}
if (siteName != undefined) data = { siteName: siteName }
var loadT = bt.load("Renewing certificate at one click.")
bt.send("renew_lets_ssl", 'ssl/renew_lets_ssl', data, function (rdata) {
loadT.close();
if (rdata.status) {
setTimeout(function () { site.ssl.get_renew_stat(); }, 500);
if (siteName != undefined) {
if (rdata.err_list.length > 0) {
bt.msg({ status: false, msg: rdata.err_list[0].msg })
}
else {
site.reload();
bt.msg({ status: true, time: 6, msg: 'The website [' + siteName + '] renewed the certificate successfully.' })
}
}
else {
var ehtml = '', shtml = ''
if (rdata.sucess_list.length > 0) {
var sucess = {};
sucess.title = "Successful renewal " + rdata.sucess_list.length + " certificate";
sucess.list = [{ title: "Domain name list", val: rdata.sucess_list.join() }];
shtml = bt.render_ps(sucess);
}
if (rdata.err_list.length > 0) {
var error = {};
error.title = "Renewal failed " + rdata.err_list.length + " certificate";
error.list = []
for (var i = 0; i < rdata.err_list.length; i++) {
error.list.push({ title: rdata.err_list[i]['siteName'], val: rdata.err_list[i]['msg'] })
}
ehtml = bt.render_ps(error);
}
bt.open({
type: 1,
area: '600px',
title: "Successful visa renewal",
closeBtn: 2,
shadeClose: false,
content: "<div class='success-msg'><div class='pic'><img src='/static/img/success-pic.png'></div><div class='suc-con'>" + shtml + ehtml + "</div></div>",
});
if ($(".success-msg").height() < 150) {
$(".success-msg").find("img").css({ "width": "150px", "margin-top": "30px" });
}
}
}
});
else {
bt.msg(rdata)
}
})
},
get_renew_stat: function () {
$.post('/ssl?action=Get_Renew_SSL', {}, function (task_list) {
......@@ -1307,7 +1352,7 @@ var site = {
arrs_list.push({ title: api[x].title, value: api[x].name });
arr_obj[api[x].name] = api[x];
}
var data = {
var data = [{
title: lan.site.choose_dns, class: 'checks_line', items: [
{
name: 'dns_select', width: '120px', type: 'select', items: arrs_list, callback: function (obj) {
......@@ -1355,17 +1400,20 @@ var site = {
}
}
},
{
title: lan.site.wait+' ', name: 'dnssleep', width: '60px', type: 'number', value: 10, unit: lan.site.second, callback: function (obj) {
if (obj.val() < 10) obj.val(10);
if (obj.val() > 120) obj.val(120);
}
}
]
}
var _form_data = bt.render_form_line(data);
$(obj).parents('.line').append(_form_data.html);
bt.render_clicks(_form_data.clicks);
, {
title: ' ', class: 'checks_line label-input-group', items:
[
{ css: 'label-input-group ptb10', text: 'Automatically combine pan-domain names', name: 'app_root', type: 'checkbox' }
]
}
]
for (var i = 0; i < data.length; i++) {
var _form_data = bt.render_form_line(data[i]);
$(obj).parents('.line').append(_form_data.html)
bt.render_clicks(_form_data.clicks);
}
})
}
},
......@@ -1373,6 +1421,7 @@ var site = {
},
{ title: lan.site.admin_email, name: 'admin_email', value: rdata.email, width: '260px' }
]
for (var i = 0; i < datas.length; i++) {
var _form_data = bt.render_form_line(datas[i]);
robj.append(_form_data.html);
......@@ -1413,17 +1462,28 @@ var site = {
updateOf: 1,
domains: JSON.stringify(ldata['domains'])
}
if (ddata.email.indexOf('@') === -1) {
layer.msg('Admin email cannot be empty!', {icon:2});
return;
}
if (ldata.check_file) {
ddata['force'] = ldata.force;
site.create_let(ddata);
site.create_let(ddata, function (res) {
if (res.status === true) {
site.reload();
}
});
}
else {
ddata['dnsapi'] = ldata.dns_select;
ddata['dnssleep'] = ldata.dnssleep;
ddata['app_root'] = ldata.app_root ? 1 : 0;
site.create_let(ddata, function (ret) {
if (ldata.dns_select == 'dns') {
if (ret.key) {
site.ssl.reload(1);
site.reload();
bt.msg(ret);
return;
}
......@@ -1438,7 +1498,7 @@ var site = {
});
setTimeout(function () {
var data = [];
for (var j = 0; j < ret.fullDomain.length; j++) data.push({ name: ret.fullDomain[j], txt: ret.txtValue[j] });
for (var j = 0; j < ret.dns_names.length; j++) data.push({ name: ret.dns_names[j].acme_name, txt: ret.dns_names[j].domain_dns_value });
bt.render({
table: '#dns_txt_jx',
columns: [
......@@ -1447,7 +1507,7 @@ var site = {
],
data: data
})
if (ret.fullDomain.length == 0) ret.fullDomain.append('_acme-challenge.bt.cn')
if (ret.dns_names.length == 0) ret.dns_names.append('_acme-challenge.bt.cn')
$('.div_txt_jx').append(bt.render_help([lan.site.dns_resolve_tips4, lan.site.dns_resolve_tips2 + ret.fullDomain[0], lan.site.dns_resolve_tips3]));
$('.btn_check_txt').click(function () {
......@@ -1456,7 +1516,8 @@ var site = {
domains: ddata.domains,
updateOf: 1,
email: ldata.email,
renew: 'True'
renew: 'True',
dnsapi:'dns'
}
site.create_let(new_data, function (ldata) {
if (ldata.status) {
......@@ -1468,7 +1529,7 @@ var site = {
}, 100)
}
else {
site.ssl.reload(1);
site.reload();
bt.msg(ret);
}
})
......@@ -1559,7 +1620,7 @@ var site = {
},
{
title: lan.site.ssl_dir, callback: function (robj) {
robj.html("<div class='divtable'><table id='cer_list_table' class='table table-hover'></table></div>");
robj.html("<div class='divtable' style='height:510px;'><table id='cer_list_table' class='table table-hover'></table></div>");
bt.site.get_cer_list(function (rdata) {
bt.render({
table: '#cer_list_table',
......@@ -1591,12 +1652,25 @@ var site = {
$('#toHttps').click(function (sdata) {
var isHttps = $("#toHttps").attr('checked');
if (isHttps) {
layer.confirm(lan.site.close_force_https_tips, { icon: 3, title: lan.site.close_force_https }, function () {
bt.site.close_http_to_https(web.name, function () { site.reload(7); })
layer.confirm('After closing HTTPS, you need to clear your browser cache to see the effect. Continue?', { icon: 3, title: "Turn off forced HTTPS\"" }, function () {
bt.site.close_http_to_https(web.name, function (rdata) {
if (rdata.status) {
setTimeout(function () {
site.reload(7);
}, 3000);
}
})
});
}
else {
bt.site.set_http_to_https(web.name, function () { site.reload(7); })
bt.site.set_http_to_https(web.name, function (rdata) {
if (!rdata.status) {
setTimeout(function () {
site.reload(7);
}, 3000);
}
})
}
})
switch (rdata.type) {
......@@ -2163,7 +2237,6 @@ var site = {
},
{
field: 'dname', title: lan.site.operate, align: 'right', templet: function (item) {
console.log(item)
var proxyname = item.proxyname;
var sitename = item.sitename;
item = JSON.stringify(item).myReplace('"', '\'');
......
......@@ -27,7 +27,7 @@ class panelSetup:
if ua:
ua = ua.lower();
if ua.find('spider') != -1 or ua.find('bot') != -1: return redirect('https://www.baidu.com');
g.version = '6.1.1'
g.version = '6.1.2'
g.title = public.GetConfigValue('title')
g.uri = request.path
session['version'] = g.version;
......@@ -142,6 +142,13 @@ class panelAdmin(panelSetup):
return redirect('/login')
public.writeFile(sess_input_path,str(int(time.time())))
except:pass
filename = '/www/server/panel/data/login_token.pl'
if os.path.exists(filename):
token = public.readFile(filename).strip()
if 'login_token' in session:
if session['login_token'] != token:
return redirect('/login?dologin=True')
except:
return redirect('/login')
......
......@@ -15,11 +15,18 @@ class config:
def getPanelState(self,get):
return os.path.exists('/www/server/panel/data/close.pl');
def reload_session(self):
userInfo = public.M('users').where("id=?",(1,)).field('username,password').find()
token = public.Md5(userInfo['username'] + '/' + userInfo['password'])
public.writeFile('/www/server/panel/data/login_token.pl',token)
session['login_token'] = token
def setPassword(self,get):
if get.password1 != get.password2: return public.returnMsg(False,'USER_PASSWORD_CHECK')
if len(get.password1) < 5: return public.returnMsg(False,'USER_PASSWORD_LEN')
public.M('users').where("username=?",(session['username'],)).setField('password',public.md5(get.password1.strip()))
public.WriteLog('TYPE_PANEL','USER_PASSWORD_SUCCESS',(session['username'],))
self.reload_session()
return public.returnMsg(True,'USER_PASSWORD_SUCCESS')
def setUsername(self,get):
......@@ -28,6 +35,7 @@ class config:
public.M('users').where("username=?",(session['username'],)).setField('username',get.username1.strip())
public.WriteLog('TYPE_PANEL','USER_USERNAME_SUCCESS',(session['username'],get.username2))
session['username'] = get.username1
self.reload_session()
return public.returnMsg(True,'USER_USERNAME_SUCCESS')
def setPanel(self,get):
......@@ -36,8 +44,10 @@ class config:
sess_out_path = 'data/session_timeout.pl'
if 'session_timeout' in get:
session_timeout = int(get.session_timeout)
if int(public.readFile(sess_out_path)) != session_timeout:
if session_timeout < 300: return public.returnMsg(False,public.GetMsg("NOT_LESS_THAN_TIMEOUT"))
s_time_tmp = public.readFile(sess_out_path)
if not s_time_tmp: s_time_tmp = '0'
if int(s_time_tmp) != session_timeout:
if session_timeout < 300: return public.returnMsg(False,'NOT_LESS_THAN_TIMEOUT')
public.writeFile(sess_out_path,str(session_timeout))
isReWeb = True
......@@ -411,9 +421,6 @@ class config:
os.system('rm -f ' + sslConf);
return public.returnMsg(True,'PANEL_SSL_CLOSE');
else:
os.system('pip insatll cffi==1.10');
os.system('pip install cryptography==2.1');
os.system('pip install pyOpenSSL==16.2');
try:
if not self.CreateSSL(): return public.returnMsg(False,'PANEL_SSL_ERR');
public.writeFile(sslConf,'True')
......
#coding: utf-8
# +-------------------------------------------------------------------
# | 宝塔Linux面板
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved.
# +-------------------------------------------------------------------
# | Author: 曹觉心 <314866873@qq.com>
# +-------------------------------------------------------------------
import public,os,sys,json,time,random
import requests
from OpenSSL import crypto
import sys, os
import time
import copy
import json
import base64
import hashlib
import binascii
import urllib
if sys.version_info[0] == 2: # python2
import urlparse
import urllib2
import cryptography.hazmat
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.serialization
else: # python3
from urllib.parse import urlparse
import cryptography
import platform
import hmac
try:
import requests
except:
os.system('pip install requests')
import requests
try:
import OpenSSL
except:
os.system('pip install pyopenssl')
import OpenSSL
import random
import datetime
import logging
from hashlib import sha1
os.chdir("/www/server/panel")
sys.path.append("class/")
import public
def extract_zone(domain_name):
domain_name = domain_name.lstrip("*.")
if domain_name.count(".") > 1:
zone, middle, last = str(domain_name).rsplit(".", 2)
root = ".".join([middle, last])
acme_txt = "_acme-challenge.%s" % zone
else:
zone = ""
root = domain_name
acme_txt = "_acme-challenge"
return root, zone, acme_txt
class AliyunDns(object):
def __init__(self, key, secret, ):
self.key = str(key).strip()
self.secret = str(secret).strip()
self.url = "http://alidns.aliyuncs.com"
def sign(self, accessKeySecret, parameters): # '''签名方法
def percent_encode(encodeStr):
encodeStr = str(encodeStr)
if sys.version_info[0] == 3:
res = urllib.parse.quote(encodeStr, '')
else:
res = urllib2.quote(encodeStr, '')
res = res.replace('+', '%20')
res = res.replace('*', '%2A')
res = res.replace('%7E', '~')
return res
sortedParameters = sorted(parameters.items(), key=lambda parameters: parameters[0])
canonicalizedQueryString = ''
for (k, v) in sortedParameters:
canonicalizedQueryString += '&' + percent_encode(k) + '=' + percent_encode(v)
stringToSign = 'GET&%2F&' + percent_encode(canonicalizedQueryString[1:])
if sys.version_info[0] == 2:
h = hmac.new(accessKeySecret + "&", stringToSign, sha1)
else:
h = hmac.new(bytes(accessKeySecret + "&", encoding="utf8"), stringToSign.encode('utf8'), sha1)
signature = base64.encodestring(h.digest()).strip()
return signature
def create_dns_record(self, domain_name, domain_dns_value):
root, _, acme_txt = extract_zone(domain_name)
print("create_dns_record start: ", acme_txt, domain_dns_value)
randomint = random.randint(11111111111111, 99999999999999)
now = datetime.datetime.utcnow()
otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
paramsdata = {
"Action": "AddDomainRecord", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime,
"SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key,
"DomainName": root,
"RR": acme_txt,
"Type": "TXT",
"Value": domain_dns_value,
}
Signature = self.sign(self.secret, paramsdata)
paramsdata['Signature'] = Signature
req = requests.get(url=self.url, params=paramsdata)
if req.status_code != 200:
if req.json()['Code'] == 'IncorrectDomainUser' or req.json()['Code'] == 'InvalidDomainName.NoExist':
raise ValueError(json.dumps({"data": "这个阿里云账户下面不存在这个域名,添加解析失败", "msg": req.json()}))
elif req.json()['Code'] == 'InvalidAccessKeyId.NotFound' or req.json()['Code'] == 'SignatureDoesNotMatch':
raise ValueError(json.dumps({"data": "API密钥错误,添加解析失败", "msg": req.json()}))
else:
raise ValueError(json.dumps({"data": req.json()['Message'], "msg": req.json()}))
print("create_dns_record end")
def query_recored_items(self, host, zone=None, tipe=None, page=1, psize=200):
randomint = random.randint(11111111111111, 99999999999999)
now = datetime.datetime.utcnow()
otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
paramsdata = {
"Action": "DescribeDomainRecords", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime,
"SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key,
"DomainName": host,
}
if zone:
paramsdata['RRKeyWord'] = zone
if tipe:
paramsdata['TypeKeyWord'] = tipe
Signature = self.sign(self.secret, paramsdata)
paramsdata['Signature'] = Signature
req = requests.get(url=self.url, params=paramsdata)
return req.json()
def query_recored_id(self, root, zone, tipe="TXT"):
record_id = None
recoreds = self.query_recored_items(root, zone, tipe=tipe)
recored_list = recoreds.get("DomainRecords", {}).get("Record", [])
recored_item_list = [i for i in recored_list if i["RR"] == zone]
if len(recored_item_list):
record_id = recored_item_list[0]["RecordId"]
return record_id
def delete_dns_record(self, domain_name, domain_dns_value):
root, _, acme_txt = extract_zone(domain_name)
print("delete_dns_record start: ", acme_txt, domain_dns_value)
record_id = self.query_recored_id(root, acme_txt)
if not record_id:
msg = "找不到域名的record_id: ", domain_name
print(msg)
return
print("start to delete dns record, id: ", record_id)
randomint = random.randint(11111111111111, 99999999999999)
now = datetime.datetime.utcnow()
otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
paramsdata = {
"Action": "DeleteDomainRecord", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime,
"SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key,
"RecordId": record_id,
}
Signature = self.sign(self.secret, paramsdata)
paramsdata['Signature'] = Signature
req = requests.get(url=self.url, params=paramsdata)
if req.status_code != 200:
raise ValueError(json.dumps({"data": "删除解析记录失败", "msg": req.json()}))
print("delete_dns_record end: ", acme_txt)
class CloudxnsDns(object):
def __init__(self, key, secret, ):
self.key = key
self.secret = secret
self.APIREQUESTDATE = time.ctime()
def extract_zone(self,domain_name):
domain_name = domain_name.lstrip("*.")
if domain_name.count(".") > 1:
zone, middle, last = str(domain_name).rsplit(".", 2)
root = ".".join([middle, last])
acme_txt = "_acme-challenge.%s" % zone
else:
zone = ""
root = domain_name
acme_txt = "_acme-challenge"
return root, zone, acme_txt
def get_headers(self, url, parameter=''):
APIREQUESTDATE = self.APIREQUESTDATE
APIHMAC = public.Md5(self.key + url + parameter + APIREQUESTDATE + self.secret)
headers = {
"API-KEY": self.key,
"API-REQUEST-DATE": APIREQUESTDATE,
"API-HMAC": APIHMAC,
"API-FORMAT": "json"
}
return headers
def get_domain_list(self):
url = "https://www.cloudxns.net/api2/domain"
headers = self.get_headers(url)
req = requests.get(url=url, headers=headers,verify=False)
req = req.json()
return req
def get_domain_id(self, domain_name):
req = self.get_domain_list()
for i in req["data"]:
if domain_name.strip() == i['domain'][:-1]:
return i['id']
return False
def create_dns_record(self, domain_name, domain_dns_value):
root, _, acme_txt = self.extract_zone(domain_name)
domain = self.get_domain_id(root)
if not domain:
raise ValueError('域名不存在这个cloudxns用户下面,添加解析失败。')
print("create_dns_record,", acme_txt, domain_dns_value)
url = "https://www.cloudxns.net/api2/record"
data = {
"domain_id": int(domain),
"host": acme_txt,
"value": domain_dns_value,
"type": "TXT",
"line_id": 1,
}
parameter = json.dumps(data)
headers = self.get_headers(url, parameter)
req = requests.post(url=url, headers=headers, data=parameter,verify=False)
req = req.json()
print("create_dns_record_end")
return req
def delete_dns_record(self, domain_name, domain_dns_value):
root, _, acme_txt = self.extract_zone(domain_name)
print("delete_dns_record start: ", acme_txt, domain_dns_value)
url = "https://www.cloudxns.net/api2/record/{}/{}".format(self.get_record_id(root), self.get_domain_id(root))
headers = self.get_headers(url, )
req = requests.delete(url=url, headers=headers, verify=False)
req = req.json()
print("delete_dns_record_success")
return req
def get_record_id(self, domain_name):
url = "http://www.cloudxns.net/api2/record/{}?host_id=0&offset=0&row_num=2000".format(self.get_domain_id(domain_name))
headers = self.get_headers(url, )
req = requests.get(url=url, headers=headers,verify=False)
req = req.json()
for i in req['data']:
if i['type'] == "TXT":
return i['record_id']
return False
class Dns_com(object):
def extract_zone(self,domain_name):
domain_name = domain_name.lstrip("*.")
if domain_name.count(".") > 1:
zone, middle, last = str(domain_name).rsplit(".", 2)
root = ".".join([middle, last])
acme_txt = "_acme-challenge.%s" % zone
else:
zone = ""
root = domain_name
acme_txt = "_acme-challenge"
return root, zone, acme_txt
def get_dns_obj(self):
p_path = '/www/server/panel/plugin/dns'
if not os.path.exists(p_path +'/dns_main.py'): return None
sys.path.insert(0,p_path)
import dns_main
return dns_main.dns_main()
def create_dns_record(self, domain_name, domain_dns_value):
root, _, acme_txt = self.extract_zone(domain_name)
print("[DNS]创建TXT记录,", acme_txt, domain_dns_value)
result = self.get_dns_obj().add_txt(acme_txt + '.' + root,domain_dns_value)
if result == "False":
raise ValueError('[DNS]当前绑定的宝塔DNS云解析账户里面不存在这个域名,添加解析失败!')
print("[DNS]TXT记录创建成功")
def delete_dns_record(self, domain_name, domain_dns_value):
root, _, acme_txt = self.extract_zone(domain_name)
print("[DNS]准备删除TXT记录: ", acme_txt, domain_dns_value)
result = self.get_dns_obj().remove_txt(acme_txt + '.' + root)
print("[DNS]TXT记录删除成功")
#coding: utf-8
# +-------------------------------------------------------------------
# | 宝塔Linux面板
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved.
# +-------------------------------------------------------------------
# | Author: 曹觉心 <314866873@qq.com>
# +-------------------------------------------------------------------
import os,sys,json,time
setup_path = '/www/server/panel'
os.chdir(setup_path)
sys.path.append("class/")
import requests,sewer,public
from OpenSSL import crypto
requests.packages.urllib3.disable_warnings()
import BTPanel
class panelLets:
let_url = "https://acme-v02.api.letsencrypt.org/directory"
#let_url_test = "https://acme-staging-v02.api.letsencrypt.org/directory"
setupPath = None #安装路径
server_type = None
#构造方法
def __init__(self):
self.setupPath = public.GetConfigValue('setup_path')
self.server_type = public.get_webserver()
#拆分根证书
def split_ca_data(self,cert):
datas = cert.split('-----END CERTIFICATE-----')
return {"cert":datas[0] + "-----END CERTIFICATE-----\n","ca_data":datas[1] + '-----END CERTIFICATE-----\n' }
#证书转为pkcs12
def dump_pkcs12(self,key_pem=None,cert_pem = None, ca_pem=None, friendly_name=None):
p12 = crypto.PKCS12()
if cert_pem:
ret = p12.set_certificate(crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem.encode()))
assert ret is None
if key_pem:
ret = p12.set_privatekey(crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem.encode()))
assert ret is None
if ca_pem:
ret = p12.set_ca_certificates((crypto.load_certificate(crypto.FILETYPE_PEM, ca_pem.encode()),) )
if friendly_name:
ret = p12.set_friendlyname(friendly_name.encode())
return p12
#获取根域名
def get_root_domain(self,domain_name):
if domain_name.count(".") != 1:
pos = domain_name.rfind(".", 0, domain_name.rfind("."))
subd = domain_name[:pos]
domain_name = domain_name[pos + 1 :]
return domain_name
#获取acmename
def get_acme_name(self,domain_name):
domain_name = domain_name.lstrip("*.")
if domain_name.count(".") > 1:
zone, middle, last = str(domain_name).rsplit(".", 2)
root = ".".join([middle, last])
acme_name = "_acme-challenge.%s.%s" % (zone,root)
else:
root = domain_name
acme_name = "_acme-challenge.%s" % root
return acme_name
#格式化错误输出
def get_error(self,error):
if error.find("Max checks allowed") >= 0 :
return "CA服务器验证超时,请等待5-10分钟后重试."
elif error.find("Max retries exceeded with") >= 0:
return "CA服务器连接超时,请确保服务器网络通畅."
elif error.find("The domain name belongs") >= 0:
return "域名不属于此DNS服务商,请确保域名填写正确."
elif error.find('login token ID is invalid') >=0:
return 'DNS服务器连接失败,请检查密钥是否正确.'
elif "too many certificates already issued for exact set of domains" in error or "Error creating new account :: too many registrations for this IP" in error:
return '<h2>签发失败,您今天尝试申请证书的次数已达上限!</h2>'
elif "DNS problem: NXDOMAIN looking up A for" in error or "No valid IP addresses found for" in error or "Invalid response from" in error:
return '<h2>签发失败,域名解析错误,或解析未生效,或域名未备案!</h2>'
else:
return error;
#获取DNS服务器
def get_dns_class(self,data):
if data['dnsapi'] == 'dns_ali':
import panelDnsapi
dns_class = panelDnsapi.AliyunDns(key = data['dns_param'][0], secret = data['dns_param'][1])
return dns_class
elif data['dnsapi'] == 'dns_dp':
dns_class = sewer.DNSPodDns(DNSPOD_ID = data['dns_param'][0] ,DNSPOD_API_KEY = data['dns_param'][1])
return dns_class
elif data['dnsapi'] == 'dns_cx':
import panelDnsapi
dns_class = panelDnsapi.CloudxnsDns(key = data['dns_param'][0] ,secret =data['dns_param'][1])
result = dns_class.get_domain_list()
if result['code'] == 1:
return dns_class
elif data['dnsapi'] == 'dns_bt':
import panelDnsapi
dns_class = panelDnsapi.Dns_com()
return dns_class
return False
#续签证书
def renew_lest_cert(self,data):
#续签网站
path = self.setupPath + '/panel/vhost/cert/'+ data['siteName'];
if not os.path.exists(path): return public.returnMsg(False, '续签失败,证书目录不存在.')
account_path = path + "/account_key.key"
if not os.path.exists(account_path): return public.returnMsg(False, '续签失败,缺少account_key.')
#续签
data['account_key'] = public.readFile(account_path)
if not 'first_domain' in data: data['first_domain'] = data['domains'][0]
if 'dnsapi' in data:
certificate = self.crate_let_by_dns(data)
else:
certificate = self.crate_let_by_file(data)
if not certificate['status']: return public.returnMsg(False, certificate['msg'])
#存储证书
public.writeFile(path + "/privkey.pem",certificate['key'])
public.writeFile(path + "/fullchain.pem",certificate['cert'] + certificate['ca_data'])
public.writeFile(path + "/account_key.key", certificate['account_key']) #续签KEY
#转为IIS证书
p12 = self.dump_pkcs12(certificate['key'], certificate['cert'] + certificate['ca_data'],certificate['ca_data'],data['first_domain'])
pfx_buffer = p12.export()
public.writeFile(path + "/fullchain.pfx",pfx_buffer,'wb+')
return public.returnMsg(True, '[%s]证书续签成功.' % data['siteName'])
#申请证书
def apple_lest_cert(self,get):
data = {}
data['siteName'] = get.siteName
data['domains'] = json.loads(get.domains)
data['email'] = get.email
data['dnssleep'] = get.dnssleep
if len(data['domains']) <=0 : return public.returnMsg(False, '申请域名列表不能为空.')
data['first_domain'] = data['domains'][0]
path = self.setupPath + '/panel/vhost/cert/'+ data['siteName'];
if not os.path.exists(path): os.makedirs(path)
# 检查是否自定义证书
partnerOrderId = path + '/partnerOrderId';
if os.path.exists(partnerOrderId): os.remove(partnerOrderId)
#清理续签key
re_key = path + '/account_key.key';
if os.path.exists(re_key): os.remove(re_key)
re_password = path + '/password';
if os.path.exists(re_password): os.remove(re_password)
data['account_key'] = None
if hasattr(get, 'dnsapi'):
if not 'app_root' in get: get.app_root = '0'
data['app_root'] = get.app_root
domain_list = data['domains']
if data['app_root'] == '1':
domain_list = []
data['first_domain'] = self.get_root_domain(data['first_domain'])
for domain in data['domains']:
rootDoamin = self.get_root_domain(domain)
if not rootDoamin in domain_list: domain_list.append(rootDoamin)
if not "*." + rootDoamin in domain_list: domain_list.append("*." + rootDoamin)
data['domains'] = domain_list
if get.dnsapi == 'dns':
domain_path = path + '/domain_txt_dns_value.json'
if hasattr(get, 'renew'): #验证
data['renew'] = True
dns = json.loads(public.readFile(domain_path))
data['dns'] = dns
certificate = self.crate_let_by_oper(data)
else:
#手动解析提前返回
result = self.crate_let_by_oper(data)
public.writeFile(domain_path, json.dumps(result))
result['code'] = 2
result['status'] = True
result['msg'] = '获取成功,请手动解析域名'
return result
elif get.dnsapi == 'dns_bt':
data['dnsapi'] = get.dnsapi
certificate = self.crate_let_by_dns(data)
else:
data['dnsapi'] = get.dnsapi
data['dns_param'] = get.dns_param.split('|')
certificate = self.crate_let_by_dns(data)
else:
#文件验证
data['site_dir'] = get.site_dir;
certificate = self.crate_let_by_file(data)
if not certificate['status']: return public.returnMsg(False, certificate['msg'])
#保存续签
cpath = self.setupPath + '/panel/vhost/cert/crontab.json'
config = {}
if os.path.exists(cpath):
config = json.loads(public.readFile(cpath))
config[data['siteName']] = data
public.writeFile(cpath,json.dumps(config))
public.set_mode(cpath,600)
#存储证书
public.writeFile(path + "/privkey.pem",certificate['key'])
public.writeFile(path + "/fullchain.pem",certificate['cert'] + certificate['ca_data'])
public.writeFile(path + "/account_key.key",certificate['account_key']) #续签KEY
#转为IIS证书
p12 = self.dump_pkcs12(certificate['key'], certificate['cert'] + certificate['ca_data'],certificate['ca_data'],data['first_domain'])
pfx_buffer = p12.export()
public.writeFile(path + "/fullchain.pfx",pfx_buffer,'wb+')
public.writeFile(path + "/README","let")
#计划任务续签
echo = public.md5(public.md5('renew_lets_ssl_bt'))
crontab = public.M('crontab').where('echo=?',(echo,)).find()
if not crontab:
cronPath = public.GetConfigValue('setup_path') + '/cron/' + echo
shell = 'python %s/panel/class/panelLets.py renew_lets_ssl ' % (self.setupPath)
public.writeFile(cronPath,shell)
public.M('crontab').add('name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress',("续签Let's Encrypt证书",'day','','0','10',echo,time.strftime('%Y-%m-%d %X',time.localtime()),1,'','localhost','toShell','',shell,''))
return public.returnMsg(True, '申请成功.')
#手动解析
def crate_let_by_oper(self,data):
result = {}
result['status'] = False
try:
if not data['email']: data['email'] = public.M('users').getField('email')
#手动解析记录值
if not 'renew' in data:
BTPanel.dns_client = sewer.Client(domain_name = data['first_domain'],dns_class = None,account_key = data['account_key'],domain_alt_names = data['domains'],contact_email = str(data['email']) ,ACME_AUTH_STATUS_WAIT_PERIOD = 15,ACME_AUTH_STATUS_MAX_CHECKS = 5,ACME_REQUEST_TIMEOUT = 20,ACME_DIRECTORY_URL = self.let_url)
domain_dns_value = "placeholder"
dns_names_to_delete = []
BTPanel.dns_client.acme_register()
authorizations, finalize_url = BTPanel.dns_client.apply_for_cert_issuance()
responders = []
for url in authorizations:
identifier_auth = BTPanel.dns_client.get_identifier_authorization(url)
authorization_url = identifier_auth["url"]
dns_name = identifier_auth["domain"]
dns_token = identifier_auth["dns_token"]
dns_challenge_url = identifier_auth["dns_challenge_url"]
acme_keyauthorization, domain_dns_value = BTPanel.dns_client.get_keyauthorization(dns_token)
acme_name = self.get_acme_name(dns_name)
dns_names_to_delete.append({"dns_name": dns_name,"acme_name":acme_name, "domain_dns_value": domain_dns_value})
responders.append(
{
"authorization_url": authorization_url,
"acme_keyauthorization": acme_keyauthorization,
"dns_challenge_url": dns_challenge_url,
}
)
dns = {}
dns['dns_names'] = dns_names_to_delete
dns['responders'] = responders
dns['finalize_url'] = finalize_url
return dns
else:
responders = data['dns']['responders']
dns_names_to_delete = data['dns']['dns_names']
finalize_url = data['dns']['finalize_url']
for i in responders:
auth_status_response = BTPanel.dns_client.check_authorization_status(i["authorization_url"])
if auth_status_response.json()["status"] == "pending":
BTPanel.dns_client.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"])
for i in responders:
BTPanel.dns_client.check_authorization_status(i["authorization_url"], ["valid"])
certificate_url = BTPanel.dns_client.send_csr(finalize_url)
certificate = BTPanel.dns_client.download_certificate(certificate_url)
if certificate:
certificate = self.split_ca_data(certificate)
result['cert'] = certificate['cert']
result['ca_data'] = certificate['ca_data']
result['key'] = BTPanel.dns_client.certificate_key
result['account_key'] = BTPanel.dns_client.account_key
result['status'] = True
BTPanel.dns_client = None
else:
result['msg'] = '证书获取失败,请稍后重试.'
except Exception as e:
print(public.get_error_info())
result['msg'] = self.get_error(str(e))
return result
#dns验证
def crate_let_by_dns(self,data):
dns_class = self.get_dns_class(data)
if not dns_class:
return public.returnMsg(False, 'DNS连接失败,请检查密钥是否正确.')
result = {}
result['status'] = False
try:
log_level = "INFO"
if data['account_key']: log_level = 'ERROR'
if not data['email']: data['email'] = public.M('users').getField('email')
client = sewer.Client(domain_name = data['first_domain'],domain_alt_names = data['domains'],account_key = data['account_key'],contact_email = str(data['email']),LOG_LEVEL = log_level,ACME_AUTH_STATUS_WAIT_PERIOD = 15,ACME_AUTH_STATUS_MAX_CHECKS = 5,ACME_REQUEST_TIMEOUT = 20, dns_class = dns_class,ACME_DIRECTORY_URL = self.let_url)
domain_dns_value = "placeholder"
dns_names_to_delete = []
try:
client.acme_register()
authorizations, finalize_url = client.apply_for_cert_issuance()
responders = []
for url in authorizations:
identifier_auth = client.get_identifier_authorization(url)
authorization_url = identifier_auth["url"]
dns_name = identifier_auth["domain"]
dns_token = identifier_auth["dns_token"]
dns_challenge_url = identifier_auth["dns_challenge_url"]
acme_keyauthorization, domain_dns_value = client.get_keyauthorization(dns_token)
dns_class.create_dns_record(dns_name, domain_dns_value)
dns_names_to_delete.append({"dns_name": dns_name, "domain_dns_value": domain_dns_value})
responders.append({"authorization_url": authorization_url, "acme_keyauthorization": acme_keyauthorization,"dns_challenge_url": dns_challenge_url} )
for i in responders:
auth_status_response = client.check_authorization_status(i["authorization_url"])
r_data = auth_status_response.json()
if r_data["status"] == "pending":
client.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"])
for i in responders: client.check_authorization_status(i["authorization_url"], ["valid"])
certificate_url = client.send_csr(finalize_url)
certificate = client.download_certificate(certificate_url)
if certificate:
certificate = self.split_ca_data(certificate)
result['cert'] = certificate['cert']
result['ca_data'] = certificate['ca_data']
result['key'] = client.certificate_key
result['account_key'] = client.account_key
result['status'] = True
except Exception as e:
print(public.get_error_info())
raise e
finally:
try:
for i in dns_names_to_delete: dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"])
except :
pass
except Exception as err:
print(public.get_error_info())
result['msg'] = self.get_error(str(err))
return result
#文件验证
def crate_let_by_file(self,data):
result = {}
result['status'] = False
try:
log_level = "INFO"
if data['account_key']: log_level = 'ERROR'
if not data['email']: data['email'] = public.M('users').getField('email')
client = sewer.Client(domain_name = data['first_domain'],dns_class = None,account_key = data['account_key'],domain_alt_names = data['domains'],contact_email = str(data['email']),LOG_LEVEL = log_level,ACME_AUTH_STATUS_WAIT_PERIOD = 15,ACME_AUTH_STATUS_MAX_CHECKS = 5,ACME_REQUEST_TIMEOUT = 20,ACME_DIRECTORY_URL = self.let_url)
client.acme_register()
authorizations, finalize_url = client.apply_for_cert_issuance()
responders = []
sucess_domains = []
for url in authorizations:
identifier_auth = self.get_identifier_authorization(client,url)
authorization_url = identifier_auth["url"]
http_name = identifier_auth["domain"]
http_token = identifier_auth["http_token"]
http_challenge_url = identifier_auth["http_challenge_url"]
acme_keyauthorization, domain_http_value = client.get_keyauthorization(http_token)
acme_dir = '%s/.well-known/acme-challenge' % (data['site_dir']);
if not os.path.exists(acme_dir): os.makedirs(acme_dir)
#写入token
wellknown_path = acme_dir + '/' + http_token
public.writeFile(wellknown_path,acme_keyauthorization)
wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(http_name, http_token)
retkey = public.httpGet(wellknown_url)
if retkey == acme_keyauthorization:
sucess_domains.append(http_name)
responders.append({"authorization_url": authorization_url, "acme_keyauthorization": acme_keyauthorization,"http_challenge_url": http_challenge_url})
if len(sucess_domains) > 0:
#验证
for i in responders:
auth_status_response = client.check_authorization_status(i["authorization_url"])
if auth_status_response.json()["status"] == "pending":
client.respond_to_challenge(i["acme_keyauthorization"], i["http_challenge_url"])
for i in responders:
client.check_authorization_status(i["authorization_url"], ["valid"])
certificate_url = client.send_csr(finalize_url)
certificate = client.download_certificate(certificate_url)
if certificate:
certificate = self.split_ca_data(certificate)
result['cert'] = certificate['cert']
result['ca_data'] = certificate['ca_data']
result['key'] = client.certificate_key
result['account_key'] = client.account_key
result['status'] = True
else:
result['msg'] = '证书获取失败,请稍后重试.'
else:
result['msg'] = "签发失败,我们无法验证您的域名:<p>1、检查域名是否绑定到对应站点</p><p>2、检查域名是否正确解析到本服务器,或解析还未完全生效</p><p>3、如果您的站点设置了反向代理,或使用了CDN,请先将其关闭</p><p>4、如果您的站点设置了301重定向,请先将其关闭</p><p>5、如果以上检查都确认没有问题,请尝试更换DNS服务商</p>'"
except Exception as e:
result['msg'] = self.get_error(str(e))
return result
def get_identifier_authorization(self,client, url):
headers = {"User-Agent": client.User_Agent}
get_identifier_authorization_response = requests.get(url, timeout = client.ACME_REQUEST_TIMEOUT, headers=headers,verify=False)
if get_identifier_authorization_response.status_code not in [200, 201]:
raise ValueError("Error getting identifier authorization: status_code={status_code}".format(status_code=get_identifier_authorization_response.status_code ) )
res = get_identifier_authorization_response.json()
domain = res["identifier"]["value"]
wildcard = res.get("wildcard")
if wildcard:
domain = "*." + domain
for i in res["challenges"]:
if i["type"] == "http-01":
http_challenge = i
http_token = http_challenge["token"]
http_challenge_url = http_challenge["url"]
identifier_auth = {
"domain": domain,
"url": url,
"wildcard": wildcard,
"http_token": http_token,
"http_challenge_url": http_challenge_url,
}
return identifier_auth
#获取证书哈希
def get_cert_data(self,path):
try:
if path[-4:] == '.pfx':
f = open(path,'rb')
pfx_buffer = f.read()
p12 = crypto.load_pkcs12(pfx_buffer,'')
x509 = p12.get_certificate()
else:
cret_data = public.readFile(path)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cret_data)
buffs = x509.digest('sha1')
hash = bytes.decode(buffs).replace(':','')
data = {}
data['hash'] = hash
data['timeout'] = bytes.decode(x509.get_notAfter())[:-1]
return data
except :
return False
#获取快过期的证书
def get_renew_lets_bytimeout(self,cron_list):
tday = 30
path = self.setupPath + '/panel/vhost/cert'
nlist = {}
new_list = {}
for siteName in cron_list:
spath = path + '/' + siteName
#验证是否存在续签KEY
if os.path.exists(spath + '/account_key.key'):
if public.M('sites').where("name=?",(siteName,)).count():
new_list[siteName] = cron_list[siteName]
data = self.get_cert_data(self.setupPath + '/panel/vhost/cert/' + siteName + '/fullchain.pem')
timeout = int(time.mktime(time.strptime(data['timeout'],'%Y%m%d%H%M%S')))
eday = (timeout - int(time.time())) / 86400
if eday < 30:
nlist[siteName] = cron_list[siteName]
#清理过期配置
public.writeFile(self.setupPath + '/panel/vhost/cert/crontab.json',json.dumps(new_list))
return nlist
#===================================== 计划任务续订证书 =====================================#
#续订
def renew_lets_ssl(self):
cpath = self.setupPath + '/panel/vhost/cert/crontab.json'
if not os.path.exists(cpath):
print("|-当前没有可以续订的证书. " );
else:
old_list = json.loads(public.ReadFile(cpath))
print('=======================================================================')
print('|-%s 共计[%s]续签证书任务.' % (time.strftime('%Y-%m-%d %X',time.localtime()),len(old_list)))
cron_list = self.get_renew_lets_bytimeout(old_list)
tlist = []
for siteName in old_list:
if not siteName in cron_list: tlist.append(siteName)
print('|-[%s]未到期或网站未使用Let\'s Encrypt证书.' % (','.join(tlist)))
print('|-%s 等待续签[%s].' % (time.strftime('%Y-%m-%d %X',time.localtime()),len(cron_list)))
sucess_list = []
err_list = []
for siteName in cron_list:
data = cron_list[siteName]
ret = self.renew_lest_cert(data)
if ret['status']:
sucess_list.append(siteName)
else:
err_list.append({"siteName":siteName,"msg":ret['msg']})
print("|-任务执行完毕,共需续订[%s],续订成功[%s],续订失败[%s]. " % (len(cron_list),len(sucess_list),len(err_list)));
if len(sucess_list) > 0:
print("|-续订成功:%s" % (','.join(sucess_list)))
if len(err_list) > 0:
print("|-续订失败:")
for x in err_list:
print(" %s ->> %s" % (x['siteName'],x['msg']))
print('=======================================================================')
print(" ");
if __name__ == "__main__":
if len(sys.argv) > 1:
type = sys.argv[1]
if type == 'renew_lets_ssl':
panelLets().renew_lets_ssl()
......@@ -357,6 +357,7 @@ class panelSSL:
data['dns'] = result[0].replace('DNS:','').replace(' ','').strip().split(',');
return data;
except:
print(public.get_error_info())
return None;
#转换时间
......@@ -393,90 +394,32 @@ class panelSSL:
if type(result) != str: result = result.decode('utf-8')
return json.loads(result);
# 手动一键续签
def Renew_SSL(self, get):
if not os.path.isfile("/www/server/panel/vhost/crontab.json"):
return {"status": False, "msg": public.GetMsg("NOT_RENEW_CERT")}
cmd_list = json.loads(public.ReadFile("/www/server/panel/vhost/crontab.json"))
import panelTask
task = panelTask.bt_task()
Renew = True
for xt in task.get_task_list():
if xt['status'] != 1: Renew = False
if not Renew:
return {"status": False, "msg": public.GetMsg("EXIST_RENEW_TASK")}
for j in cmd_list:
siteName = j['siteName']
home_path = os.path.join("/www/server/panel/vhost/cert/", siteName)
public.ExecShell("mkdir -p {}".format(home_path))
public.ExecShell('''cd {} && rm -rf check_authorization_status_response Confirmation_verification domain_txt_dns_value.json apply_for_cert_issuance_response timeout_info'''.format(home_path))
cmd = j['cmd']
for x in task.get_task_list():
if x['name'] == siteName:
get.id = x['id']
task.remove_task(get) # 删除旧的任务
task.create_task(siteName, 0, cmd)
return {"status": True, "msg": public.GetMsg("ADD_RENEW_TO_TASK")}
# 手动一键续签
def renew_lets_ssl(self, get):
if not os.path.exists('vhost/cert/crontab.json'):
return public.returnMsg(False,'There are currently no certificates to renew!')
# 获取一键续订结果
def Get_Renew_SSL(self, get):
if not os.path.isfile("/www/server/panel/vhost/crontab.json"):
return {"status": False, "msg": public.GetMsg("GET_FAIL_NOT_RESULT"), "data": []}
cmd_list = json.loads(public.ReadFile("/www/server/panel/vhost/crontab.json"))
import panelTask
CertList = self.GetCertList(get)
data = []
for j in cmd_list:
siteName = j['siteName']
cmd = j['cmd']
home_path = os.path.join("/www/server/panel/vhost/cert/", siteName)
home_csr = os.path.join(home_path, "fullchain.pem")
home_key = os.path.join(home_path, "privkey.pem")
old_list = json.loads(public.ReadFile("vhost/cert/crontab.json"))
cron_list = old_list
if hasattr(get, 'siteName'):
if not get.siteName in old_list:
return public.returnMsg(False,'There is no certificate that can be renewed on the current website..')
cron_list = {}
cron_list[get.siteName] = old_list[get.siteName]
task = panelTask.bt_task()
for i in task.get_task_list():
if i['name'] == siteName:
siteName_task = {'status': i['status']}
siteName_task['subject'] = siteName
siteName_task['dns'] = [siteName, ]
for item in CertList:
if siteName == item['subject']:
siteName_task['dns'] = item['dns']
siteName_task['notAfter'] = item['notAfter']
siteName_task['issuer'] = item['issuer']
timeArray = time.localtime(i['addtime'])
siteName_task['addtime'] = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)
if i['endtime']:
timeArray = time.localtime(i['endtime'])
siteName_task['endtime'] = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)
else:
siteName_task['endtime'] = i['endtime']
if i['status'] == -1:
siteName_task['msg'] = public.GetMsg("RENEW_NOW")
if i['status'] == 0:
siteName_task['msg'] = public.GetMsg("WAIT_RENEW")
if i['status'] == 1:
get.keyPath =home_key
get.certPath = home_csr
self.SaveCert(get);
siteName_task['msg'] = public.GetMsg("RENEW_SUCCESS")
siteName_task['status'] = True
if not os.path.isfile(home_key) and not os.path.isfile(home_csr):
siteName_task['msg'] = public.GetMsg("RENEW_FAIL")
siteName_task['status'] = False
if os.path.isfile(os.path.join(home_path, "check_authorization_status_response")):
siteName_task['msg'] = public.GetMsg("RENEW_FAIL1")
siteName_task['status'] = False
if os.path.isfile(os.path.join(home_path, "apply_for_cert_issuance_response")):
siteName_task['msg'] = public.GetMsg("RENEW_FAIL2")
siteName_task['status'] = False
import panelLets
lets = panelLets.panelLets()
data.append(siteName_task)
break
if data:
return {"status": True, "msg": public.GetMsg("SSL_GET_SUCCESS"), "data": data}
else:
return {"status": False, "msg": public.GetMsg("GET_FAIL_NOT_RESULT"), "data": []}
\ No newline at end of file
result = {}
result['status'] = True
result['sucess_list'] = []
result['err_list'] = []
for siteName in cron_list:
data = cron_list[siteName]
ret = lets.renew_lest_cert(data)
if ret['status']:
result['sucess_list'].append(siteName)
else:
result['err_list'].append({"siteName":siteName,"msg":ret['msg']})
return result;
......@@ -93,7 +93,7 @@ class panelSite(panelRedirect):
listen = "\nListen "+tmp[0]
listen_ipv6 = ''
if self.is_ipv6: listen_ipv6 = "\nListen [::]:" + port
#if self.is_ipv6: listen_ipv6 = "\nListen [::]:" + port
allConf = allConf.replace(listen,listen + "\nListen " + port + listen_ipv6)
public.writeFile(filename, allConf)
return True
......@@ -129,7 +129,7 @@ class panelSite(panelRedirect):
DocumentRoot "%s"
ServerName %s.%s
ServerAlias %s
errorDocument 404 /404.html
#errorDocument 404 /404.html
ErrorLog "%s-error_log"
CustomLog "%s-access_log" combined
......@@ -775,37 +775,25 @@ class panelSite(panelRedirect):
except:
return False
#保存第三方证书
def SetSSL(self,get):
#type = get.type;
# 保存第三方证书
def SetSSL(self, get):
siteName = get.siteName;
path = '/www/server/panel/vhost/cert/' + siteName;
if not os.path.exists(path):
public.ExecShell('mkdir -p ' + path)
csrpath = path+"/fullchain.pem"; #生成证书路径
keypath = path+"/privkey.pem"; #密钥文件路径
if(get.key.find('KEY') == -1): return public.returnMsg(False, 'SITE_SSL_ERR_PRIVATE');
if(get.csr.find('CERTIFICATE') == -1): return public.returnMsg(False, 'SITE_SSL_ERR_CERT');
public.writeFile('/tmp/cert.pl',get.csr);
if not public.CheckCert('/tmp/cert.pl'): return public.returnMsg(False,public.GetMsg("PEM_FORMAT_ERR"));
public.ExecShell('\\cp -a '+keypath+' /tmp/backup1.conf');
public.ExecShell('\\cp -a '+csrpath+' /tmp/backup2.conf');
#清理旧的证书链
if os.path.exists(path+'/README'):
public.ExecShell('rm -rf ' + path);
public.ExecShell('rm -rf ' + path + '-00*');
public.ExecShell('rm -rf /etc/letsencrypt/archive/' + get.siteName);
public.ExecShell('rm -rf /etc/letsencrypt/archive/' + get.siteName + '-00*');
public.ExecShell('rm -f /etc/letsencrypt/renewal/' + get.siteName + '.conf');
public.ExecShell('rm -f /etc/letsencrypt/renewal/' + get.siteName + '-00*.conf');
public.ExecShell('rm -f /etc/letsencrypt/live/' + get.siteName + '/README')
public.ExecShell('rm -f ' + path + '/README');
public.ExecShell('mkdir -p ' + path);
csrpath = path + "/fullchain.pem"
keypath = path + "/privkey.pem"
if (get.key.find('KEY') == -1): return public.returnMsg(False, 'SITE_SSL_ERR_PRIVATE');
if (get.csr.find('CERTIFICATE') == -1): return public.returnMsg(False, 'SITE_SSL_ERR_CERT');
public.writeFile('/tmp/cert.pl', get.csr);
if not public.CheckCert('/tmp/cert.pl'): return public.returnMsg(False, 'PEM_FORMAT_ERR');
backup_cert = '/tmp/backup_cert_' + siteName
import shutil
if os.path.exists(backup_cert): shutil.rmtree(backup_cert)
if os.path.exists(path): shutil.move(path,backup_cert)
if os.path.exists(path): shutil.rmtree(path)
public.ExecShell('mkdir -p ' + path)
public.writeFile(keypath, get.key);
public.writeFile(csrpath, get.csr);
......@@ -815,16 +803,19 @@ class panelSite(panelRedirect):
isError = public.checkWebConfig();
if (type(isError) == str):
public.ExecShell('\\cp -a /tmp/backup1.conf ' + keypath);
public.ExecShell('\\cp -a /tmp/backup2.conf ' + csrpath);
if os.path.exists(path): shutil.rmtree(backup_cert)
shutil.move(backup_cert,path)
return public.returnMsg(False, 'ERROR: <br><a style="color:red;">' + isError.replace("\n", '<br>') + '</a>');
public.serviceReload();
if os.path.exists(path + '/partnerOrderId'): os.system('rm -f ' + path + '/partnerOrderId');
p_file = '/etc/letsencrypt/live/' + get.siteName + '/partnerOrderId'
if os.path.exists(p_file): os.system('rm -f ' + p_file);
public.WriteLog('TYPE_SITE', 'SITE_SSL_SAVE_SUCCESS');
return public.returnMsg(True, 'SITE_SSL_SUCCESS');
if os.path.exists(path + '/partnerOrderId'): os.remove(path + '/partnerOrderId')
p_file = '/etc/letsencrypt/live/' + get.siteName
if os.path.exists(p_file): shutil.rmtree(p_file)
public.WriteLog('TYPE_SITE', 'SITE_SSL_SAVE_SUCCESS')
#清理备份证书
if os.path.exists(backup_cert): shutil.rmtree(backup_cert)
return public.returnMsg(True, 'SITE_SSL_SUCCESS')
#获取运行目录
def GetRunPath(self,get):
......@@ -837,329 +828,73 @@ class panelSite(panelRedirect):
if type(get.id) == list: get.id = get.id[0]['id'];
result = self.GetSiteRunPath(get);
return result['runPath'];
# 创建Let's Encrypt免费证书
def CreateLet(self, get):
import time
def CreateLet(self,get):
# 确定主域名顺序
domains = json.loads(get.domains)
domainsTmp = []
if get.siteName in domains: domainsTmp.append(get.siteName);
Wildcard_domain = ''
for domainTmp in domains:
if domainTmp.startswith("*."):
Wildcard_domain = domainTmp
if domainTmp == get.siteName: continue;
domainsTmp.append(domainTmp);
domains = domainsTmp;
if not len(domains): return public.returnMsg(False, public.GetMsg("CHOOSE_DOMAIN"));
get.first_domain = domains[0].replace("*.", '')
if len(domains) > 1 and Wildcard_domain:
for dom in domains:
if "*." in dom: continue
if len(dom.split(".")) == 2: continue
if Wildcard_domain.replace("*.", "") == dom.split(".")[-2] + "." + dom.split(".")[-1]:
return public.returnMsg(False, public.GetMsg("ACCEPT_SSL_ERR"));
# 定义证书目录
path = os.path.join('/www/server/panel/vhost/cert/', get.first_domain);
path = path.replace("*.", '')
if os.path.isdir(path):
public.ExecShell("rm -rf {}".format(path))
public.ExecShell("mkdir -p {}".format(path))
csrpath = os.path.join(path, "fullchain.pem"); # 生成证书路径
keypath = os.path.join(path, "privkey.pem"); # 密钥文件路径
# 准备基础信息
actionstr = get.updateOf
siteInfo = public.M('sites').where('name=?', (get.siteName,)).field('id,name,path').find();
runPath = self.GetRunPath(get);
srcPath = siteInfo['path'];
if runPath != False and runPath != '/': siteInfo['path'] += runPath;
get.path = siteInfo['path'];
if not len(domains):
return public.returnMsg(False, 'CHOOSE_DOMAIN')
file_auth = True
if hasattr(get, 'dnsapi'):
file_auth = False
if not hasattr(get, 'dnssleep'):
get.dnssleep = 10
email = public.M('users').getField('email');
if hasattr(get, 'email'):
if get.email.strip() != '':
public.M('users').setField('email', get.email);
email = get.email;
if "@" not in email: email = ''
force = False;
dns = False
dns_plu = False
crontab = ''
renew = False
file_auth = False
if not hasattr(get, "dnsapi"):
file_auth = True
if hasattr(get, 'force'):
if get.force == 'true':
force = True;
if hasattr(get, 'renew'): # 验证手动的dns txt解析
file_auth = False
renew = True
public.ExecShell('''cd {} && rm -rf account_key.key fullchain.pem privkey.pem Confirmation_verification domain_txt_dns_value.json '''.format(path))
result = [" ", " "]
public.WriteFile(os.path.join(path, "Confirmation_verification"), "ok", mode="w")
num = 0
while True:
num += 1
if os.path.isfile(csrpath) and os.path.isfile(keypath):
break
else:
data = {};
data['err'] = result;
data['out'] = result
if os.path.isfile(os.path.join(path, "check_authorization_status_response")):
result[1] = public.ReadFile(os.path.join(path, "check_authorization_status_response"))
public.ExecShell("cd {} && rm -rf check_authorization_status_response".format(path))
public.WriteFile(os.path.join(path, "timeout_info"), "", mode="w")
data['msg'] = public.GetMsg("CHECK_TXT_ERR1");
data['status'] = False;
return data
elif os.path.isfile(os.path.join(path, "timeout_info")) or num > 60:
result[1] = public.GetMsg("CHECK_TXT_ERR2")
data['msg'] = public.GetMsg("CHECK_TXT_ERR3")
data['status'] = False;
return data
elif not public.ExecShell('''ps aux|grep -v grep|grep sewer_Usage''')[0]:
return public.returnMsg(False, public.GetMsg("CHECK_TXT_ERR4"))
time.sleep(5)
else:
if not file_auth:
dnssleep = get.dnssleep
dnsapi = get.dnsapi
if get.email.find('@') == -1:
get.email = email
else:
dnssleep = 10
dnsapi = ''
public.ExecShell(
'''cd {} && rm -rf account_key.key fullchain.pem privkey.pem check_authorization_status_response Confirmation_verification domain_txt_dns_value.json apply_for_cert_issuance_response timeout_info'''.format(
path))
json_parem = {"dnsapi": dnsapi, "domain_alt_names": "", "contact_email": email, "dnssleep": dnssleep, "key": "", "secret": "", "path": path}
if hasattr(get, 'dnsapi'):
if get.dnsapi == 'dns':
json_parem['dnssleep'] = get.dnssleep
dns = True
else:
if not self.Check_DnsApi(get.dnsapi): return public.returnMsg(False, public.GetMsg("SET_API_FIRST"));
if get.dnsapi == 'dns_bt':
c_file = '/www/server/panel/plugin/dns/dns_main.py';
if not os.path.exists(c_file): return public.returnMsg(False,'INSTALL_CLOUDDNS_FIRST');
c_conf = public.readFile(c_file)
if c_conf.find('add_txt') == -1:
os.system('wget -O ' + c_file + ' http://download.bt.cn/install/plugin/dns/dns_main.py -T 5')
sys.path.append('/www/server/panel/plugin/dns')
import dns_main
dns_plu = dns_main.dns_main()
else:
dns_api_list = self.GetDnsApi(get)
for i in dns_api_list:
if i['name'] == get.dnsapi:
json_parem['key'] = i['data'][0]['value']
json_parem['secret'] = i['data'][1]['value']
# 构造参数
domainCount = 0
errorDomain = "";
errorDns = "";
done = '';
dns_type = hasattr(get, 'dnsapi')
get.email = get.email.strip()
public.M('users').where('id=?',(1,)).setField('email',get.email);
else:
get.email = email
for domain in domains:
if public.checkIp(domain): continue;
if not dns_type:
if domain.find('*.') != -1:
if not renew:
return public.returnMsg(False, public.GetMsg("CHECK_SSL_ERR"));
get.domain = domain;
if public.M('domain').where('name=?', (domain,)).count():
p = siteInfo['path'];
else:
p = public.M('binding').where('domain=?', (domain,)).getField('path');
get.path = p;
if force:
if not self.CheckDomainPing(get): errorDomain += '<li>' + domain + '</li>';
if dns_plu:
domainId, key = dns_plu.get_domainid_byfull('test.' + domain)
if not domainId: errorDns += '<li>' + domain + '</li>';
if p != done:
done = p;
domainCount += 1
if errorDomain: return public.returnMsg(False, 'SITE_SSL_ERR_DNS', ('<span style="color:red;"><br>' + errorDomain + '</span>',));
# 获取域名数据
if domainCount == 0: return public.returnMsg(False, 'SITE_SSL_ERR_EMPTY')
# 检查是否自定义证书
partnerOrderId = path + '/partnerOrderId';
if os.path.exists(partnerOrderId):
os.remove(partnerOrderId)
#检查依赖
self.check_ssl_pack()
if domain.find('*.') >=0 and not file_auth:
return public.returnMsg(False, 'A generic domain name cannot be used to apply for a certificate using [File Validation]!');
# dns调用脚本获取ssl证书
if dns_type:
json_parem['domain_alt_names'] = ",".join(domains)
if dns:
result = public.ExecShell('''nohup python /www/server/panel/class/sewer_Usage.py '{}' > sewer_Usage.log 2>&1 & '''.format(json.dumps(json_parem)))
else:
shell_str = '''python /www/server/panel/class/sewer_Usage.py '{}' '''.format(json.dumps(json_parem))
result = public.ExecShell(shell_str);
crontab = shell_str
if dns: # 返回txt手动解析信息
while True:
if os.path.isfile(os.path.join(path, "domain_txt_dns_value.json")):
txt_domain_value_li = json.loads(public.ReadFile(os.path.join(path, "domain_txt_dns_value.json")))
break
elif os.path.isfile(os.path.join(path, "apply_for_cert_issuance_response")):
data = {};
data['err'] = ["", public.ReadFile(os.path.join(path, "apply_for_cert_issuance_response"))]
data['out'] = data['err'][1]
data['result'] = json.loads(data['err'][1]);
if data['result']['status'] == 429:
data['msg'] = public.GetMsg("ACCEPT_SSL_ERR1")
if data['result']['status'] == 400:
data['msg'] = public.GetMsg("ACCEPT_SSL_ERR2")
data['status'] = False;
return data
time.sleep(5)
try:
data = {}
data['err'] = result;
data['out'] = result[0];
data['status'] = True
data['fullDomain'] = []
data['txtValue'] = []
data['msg'] = public.GetMsg("RESOLVE_DOMAIN_BYF")
for i in txt_domain_value_li:
data['fullDomain'].append('_acme-challenge.' + i['dns_name'].replace('*.',''))
data['txtValue'].append(i['acme_txt_value'])
return data
except:
data = {};
data['err'] = result;
data['out'] = result[0];
data['msg'] = public.GetMsg("GET_FAIL")
data['result'] = {};
return data
# 判断是否获取成功
if not os.path.exists(csrpath) and not os.path.exists(keypath) and dns_type:
data = {};
data['err'] = result;
data['out'] = result[0];
try:
msg = json.loads(re.search("{.+}", result[1]).group())['data']
data['msg'] = "<h2>签发失败," + msg + "</h2>"
except Exception:
data['msg'] = public.GetMsg("ISSUANCE_FAIL")
data['result'] = {};
if os.path.isfile(os.path.join(path, "check_authorization_status_response")):
data['result'] = json.loads(public.ReadFile(os.path.join(path, "check_authorization_status_response")));
if data['result']['status'] == "invalid":
data['msg'] = public.GetMsg("ACCEPT_SSL_ERR3")
if os.path.isfile("timeout_info"):
data['msg'] = public.GetMsg("ACCEPT_SSL_ERR4")
if os.path.isfile(os.path.join(path, "apply_for_cert_issuance_response")):
data['result'] = json.loads(public.ReadFile(os.path.join(path, "apply_for_cert_issuance_response")));
if data['result']['status'] == 429:
data['msg'] = '<h2>签发失败,您尝试申请证书的失败次数已达上限!</h2>';
if data['result']['status'] == 400:
data['msg'] = '<h2>签发失败,::通配符域名不能和子域名一起申请证书!</h2>';
data['status'] = False;
return data
if file_auth: # 文件验证调用脚本
# 检查是否设置301和反向代理
if file_auth:
get.sitename = get.siteName
if self.GetRedirectList(get): return public.returnMsg(False, 'SITE_SSL_ERR_301');
if self.GetProxyList(get): return public.returnMsg(False,'已开启反向代理的站点无法申请SSL!');
DOMAINS = ''
for dom in domains:
DOMAINS += 'DNS:{},'.format(dom)
json_parem = {"path": path, "siteName": get.siteName, "DOMAINS": DOMAINS[:-1],"sitePath":siteInfo['path']}
result = public.ExecShell('''python /www/server/panel/class/letsencrypt.py '{}' '''.format(json.dumps(json_parem)))
crontab = '''python /www/server/panel/class/letsencrypt.py '{}' '''.format(json.dumps(json_parem))
if os.path.exists(csrpath) and os.path.exists(keypath):
pass
else:
if result[1]:
data = {};
data['err'] = result;
data['out'] = result[0];
data['msg'] = public.GetMsg("ISSUANCE_FAIL")
data['result'] = result;
if "too many certificates already issued for exact set of domains" in result[1] or "Error creating new account :: too many registrations for this IP" in result[1]:
data['msg'] = public.GetMsg("ACCEPT_SSL_ERR1")
elif "DNS problem: NXDOMAIN looking up A for" in result[1] or "No valid IP addresses found for" in result[1] or "Invalid response from" in result[1] \
or "Policy forbids issuing for name" in result[1] or "\'status\'\:\ 4" in result[1] or '''"status": 4''' in result[1]:
data['msg'] = public.GetMsg("ACCEPT_SSL_ERR5")
elif public.GetMsg("ACCEPT_SSL_ERR6") in result[1]:
data['msg'] = public.GetMsg("ACCEPT_SSL_ERR7")
data['status'] = False;
public.ExecShell("rm -rf {}/*".format(path))
return data
public.ExecShell('echo "let" > "' + path + '/README"');
if (actionstr == '2'): return public.returnMsg(True, 'SITE_SSL_UPDATE_SUCCESS');
# 定时任务
if crontab:
if "0 0 1 * * python /www/server/panel/class/crontab_ssl.py" not in public.ExecShell("crontab -l")[0]:
with open("/var/spool/cron/root", "a") as f:
f.write("\n0 0 1 * * python /www/server/panel/class/crontab_ssl.py")
crontab_path = "/www/server/panel/vhost/crontab.json"
crontab_list = []
if os.path.isfile(crontab_path):
crontab_list = json.loads(public.ReadFile(crontab_path))
if crontab_list :
modify = False
for i in crontab_list:
if get.siteName == i["siteName"]:
i["cmd"] = crontab
modify = True
if not modify:
crontab_list.append({"siteName": get.siteName, "cmd": crontab})
else:
crontab_list.append({"siteName": get.siteName, "cmd": crontab})
public.WriteFile(crontab_path, json.dumps(crontab_list), mode="w")
sitekey = os.path.join("/www/server/panel/vhost/cert", get.siteName, 'privkey.pem')
sitecsr = os.path.join("/www/server/panel/vhost/cert", get.siteName, 'fullchain.pem')
public.ExecShell("mkdir -p {}".format(os.path.join("/www/server/panel/vhost/cert", get.siteName)))
public.ExecShell('''echo "let" > {}'''.format(os.path.join("/www/server/panel/vhost/cert", get.siteName, "README")));
if not os.path.isfile(sitecsr) and not os.path.isfile(sitekey):
public.ExecShell("/bin/cp {} {}".format(csrpath, sitecsr))
public.ExecShell("/bin/cp {} {}".format(keypath, sitekey))
# 写入配置文件
result = self.SetSSLConf(get);
result['csr'] = public.readFile(csrpath);
result['key'] = public.readFile(keypath);
public.serviceReload();
return result;
if self.GetProxyList(get): return public.returnMsg(False,'Sites that have reverse proxy turned on cannot request SSL!');
data = self.get_site_info(get.siteName)
get.site_dir = data['path']
else:
dns_api_list = self.GetDnsApi(get)
get.dns_param = None
for dns in dns_api_list:
if dns['name'] == get.dnsapi:
param = [];
if not dns['data']: continue
for val in dns['data']:
param.append(val['value'])
get.dns_param = '|'.join(param)
n_list = ['dns' , 'dns_bt']
if not get.dnsapi in n_list:
if len(get.dns_param) < 16: return public.returnMsg(False, 'Please set the API interface parameters of [%s] first.' % get.dnsapi);
if get.dnsapi == 'dns_bt':
if not os.path.exists('plugin/dns/dns_main.py'):
return public.returnMsg(False, 'Please go to the software store to install [Cloud Resolution] and complete the domain name NS binding.');
self.check_ssl_pack()
import panelLets
lets = panelLets.panelLets()
result = lets.apple_lest_cert(get)
if result['status'] and not 'code' in result:
get.onkey = 1;
result = self.SetSSLConf(get)
return result
#处理acme.sh安装位置问题
def CheckAcme(self):
p1 = '/root/.acme.sh/'
p2 = '/.acme.sh/'
def get_site_info(self,siteName):
data = public.M("sites").where('name=?',siteName).field('path,name').find()
return data
r_name = 'account.conf'
check_names = ['account.conf','acme.sh','dnsapi','deploy']
for r_name in check_names:
if os.path.exists(p1 +r_name):
if not os.path.exists(p2 + r_name): public.ExecShell("ln -sf " + p1 +r_name + ' ' + p2 +r_name)
else:
if os.path.exists(p2 + r_name): public.ExecShell("ln -sf " + p2 +r_name + ' ' + p1 +r_name)
return True
#检测依赖库
def check_ssl_pack(self):
......@@ -1188,15 +923,10 @@ class panelSite(panelRedirect):
apis = json.loads(public.ReadFile('./config/dns_api.json'))
path = '/root/.acme.sh'
if not os.path.exists(path + '/account.conf'): path = "/.acme.sh"
#if not os.path.exists(path + '/dnsapi'): os.makedirs(path + '/dnsapi')
account = public.readFile(path + '/account.conf')
if not account: account = ''
is_write = False
for i in range(len(apis)):
#filename = path + '/dnsapi/' + apis[i]['name'] + '.sh'
#if not os.path.exists(filename) and apis[i]['name'] != 'dns':
# public.downloadFile('http://download.bt.cn/install/dnsapi/' + apis[i]['name'] + '.sh',filename)
# public.ExecShell("chmod +x " + filename)
if not apis[i]['data']: continue
for j in range(len(apis[i]['data'])):
if apis[i]['data'][j]['value']: continue
......@@ -1208,9 +938,6 @@ class panelSite(panelRedirect):
#设置DNS-API
def SetDnsApi(self,get):
#path = '/root/.acme.sh'
#if not os.path.exists(path + '/account.conf'): path = "/.acme.sh"
#filename = path + '/account.conf'
pdata = json.loads(get.pdata)
apis = json.loads(public.ReadFile('./config/dns_api.json'))
is_write = False
......@@ -1221,9 +948,6 @@ class panelSite(panelRedirect):
if apis[i]['data'][j]['key'] != key: continue
apis[i]['data'][j]['value'] = pdata[key]
is_write = True
#kvalue = key + "='" + pdata[key] + "'"
#public.ExecShell("sed -i '/%s/d' %s" % (key,filename))
#public.ExecShell("echo \"%s\" >> %s" % (kvalue,filename))
if is_write: public.writeFile('./config/dns_api.json',json.dumps(apis))
return public.returnMsg(True,"设置成功!")
......@@ -1242,7 +966,7 @@ class panelSite(panelRedirect):
tmp['binding'] = True
domains.append(tmp)
data['domains'] = domains
data['email'] = public.M('users').getField('email')
data['email'] = public.M('users').where('id=?',(1,)).getField('email')
if data['email'] == '287962566@qq.com': data['email'] = ''
return data
......@@ -1276,6 +1000,14 @@ class panelSite(panelRedirect):
return ' TLSv1.3'
return ''
# 获取apache反向代理
def get_apache_proxy(self,conf):
rep = "\n*#Referenced reverse proxy rule, if commented, the configured reverse proxy will be invalid\n+\s+IncludeOptiona[\s\w\/\.\*]+"
proxy = re.search(rep,conf)
if proxy:
return proxy.group()
return ""
# 添加SSL配置
def SetSSLConf(self, get):
siteName = get.siteName
......@@ -1329,6 +1061,7 @@ class panelSite(panelRedirect):
file = self.setupPath + '/panel/vhost/apache/' + siteName + '.conf';
conf = public.readFile(file);
if conf:
ap_proxy = self.get_apache_proxy(conf)
if conf.find('SSLCertificateFile') == -1:
find = public.M('sites').where("name=?", (siteName,)).field('id,path').find()
tmp = public.M('domain').where('pid=?', (find['id'],)).field('name').select()
......@@ -1367,7 +1100,7 @@ class panelSite(panelRedirect):
#errorDocument 404 /404.html
ErrorLog "%s-error_log"
CustomLog "%s-access_log" combined
%s
#SSL
SSLEngine On
SSLCertificateFile /www/server/panel/vhost/cert/%s/fullchain.pem
......@@ -1391,7 +1124,7 @@ class panelSite(panelRedirect):
%s
DirectoryIndex %s
</Directory>
</VirtualHost>''' % (vName, path, siteName, domains, public.GetConfigValue('logs_path') + '/' + siteName, public.GetConfigValue('logs_path') + '/' + siteName, get.first_domain, get.first_domain, phpConfig, path, apaOpt, index)
</VirtualHost>''' % (vName, path, siteName, domains, public.GetConfigValue('logs_path') + '/' + siteName, public.GetConfigValue('logs_path') + '/' + siteName,ap_proxy, get.first_domain, get.first_domain, phpConfig, path, apaOpt, index)
conf = conf + "\n" + sslStr;
self.apacheAddPort('443');
......@@ -1413,7 +1146,10 @@ class panelSite(panelRedirect):
public.serviceReload();
self.save_cert(get);
public.WriteLog('TYPE_SITE', 'SITE_SSL_OPEN_SUCCESS', (siteName,));
return public.returnMsg(True, 'SITE_SSL_OPEN_SUCCESS');
result = public.returnMsg(True, 'SITE_SSL_OPEN_SUCCESS');
result['csr'] = public.readFile('/www/server/panel/vhost/cert/' + get.siteName + '/fullchain.pem');
result['key'] = public.readFile( '/www/server/panel/vhost/cert/' + get.siteName + '/privkey.pem');
return result
def save_cert(self, get):
# try:
......@@ -1584,7 +1320,10 @@ class panelSite(panelRedirect):
get.certPath = csrpath
import panelSSL
cert_data = panelSSL.panelSSL().GetCertName(get)
return {'status': status, 'domain': domains, 'key': key, 'csr': csr, 'type': type, 'httpTohttps': toHttps,'cert_data':cert_data}
email = public.M('users').where('id=?',(1,)).getField('email')
if email == '287962566@qq.com': email = ''
return {'status': status, 'domain': domains, 'key': key, 'csr': csr, 'type': type, 'httpTohttps': toHttps,'cert_data':cert_data,'email':email}
#启动站点
......@@ -2906,9 +2645,6 @@ location %s
return public.returnMsg(False, 'ERROR: %s<br><a style="color:red;">' % public.GetMsg("CONFIG_ERROR") + isError.replace("\n",
'<br>') + '</a>')
return public.returnMsg(True, 'SUCCESS')
#开启缓存
......
......@@ -708,6 +708,12 @@ def inArray(arrays,searchStr):
return False
#格式化指定时间戳
def format_date(format="%Y-%m-%d %H:%M:%S",times = None):
if not times: times = int(time.time())
time_local = time.localtime(times)
return time.strftime(format, time_local)
#检查Web服务器配置文件是否有错误
def checkWebConfig():
......@@ -1252,4 +1258,4 @@ def set_own(filename,user,group=None):
user = user_info.pw_uid
group = user_info.pw_gid
os.chown(filename,user,group)
return True
\ No newline at end of file
return True
from .client import Client # noqa: F401
from .dns_providers import BaseDns # noqa: F401
from .dns_providers import AuroraDns # noqa: F401
from .dns_providers import CloudFlareDns # noqa: F401
from .dns_providers import AcmeDnsDns # noqa: F401
from .dns_providers import AliyunDns # noqa:F401
from .dns_providers import HurricaneDns # noqa:F401
from .dns_providers import RackspaceDns # noqa:F401
from .dns_providers import DNSPodDns
from .dns_providers import DuckDNSDns
__title__ = "sewer"
__description__ = "Sewer is a programmatic Lets Encrypt(ACME) client"
__url__ = "https://github.com/komuw/sewer"
__version__ = "0.7.2"
__author__ = "komuW"
__author_email__ = "komuw05@gmail.com"
__license__ = "MIT"
import os
import logging
import argparse
from . import Client
from . import __version__ as sewer_version
from .config import ACME_DIRECTORY_URL_STAGING, ACME_DIRECTORY_URL_PRODUCTION
def main():
"""
Usage:
1. To get a new certificate:
CLOUDFLARE_EMAIL=example@example.com \
CLOUDFLARE_API_KEY=api-key \
sewer \
--dns cloudflare \
--domain example.com \
--action run
2. To renew a certificate:
CLOUDFLARE_EMAIL=example@example.com \
CLOUDFLARE_API_KEY=api-key \
sewer \
--account_key /path/to/your/account.key \
--dns cloudflare \
--domain example.com \
--action renew
"""
parser = argparse.ArgumentParser(
prog="sewer",
description="""Sewer is a Let's Encrypt(ACME) client.
Example usage::
CLOUDFLARE_EMAIL=example@example.com \
CLOUDFLARE_API_KEY=api-key \
sewer \
--dns cloudflare \
--domain example.com \
--action run""",
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s {version}".format(version=sewer_version.__version__),
help="The currently installed sewer version.",
)
parser.add_argument(
"--account_key",
type=argparse.FileType("r"),
required=False,
help="The path to your letsencrypt/acme account key. \
eg: --account_key /home/myaccount.key",
)
parser.add_argument(
"--certificate_key",
type=argparse.FileType("r"),
required=False,
help="The path to your certificate key. \
eg: --certificate_key /home/mycertificate.key",
)
parser.add_argument(
"--dns",
type=str,
required=True,
choices=[
"cloudflare",
"aurora",
"acmedns",
"aliyun",
"hurricane",
"rackspace",
"dnspod",
"duckdns",
],
help="The name of the dns provider that you want to use.",
)
parser.add_argument(
"--domain",
type=str,
required=True,
help="The domain/subdomain name for which \
you want to get/renew certificate for. \
wildcards are also supported \
eg: --domain example.com",
)
parser.add_argument(
"--alt_domains",
type=str,
required=False,
default=[],
nargs="*",
help="A list of alternative domain/subdomain name/s(if any) for which \
you want to get/renew certificate for. \
eg: --alt_domains www.example.com blog.example.com",
)
parser.add_argument(
"--bundle_name",
type=str,
required=False,
help="The name to use for certificate \
certificate key and account key. Default is name of domain.",
)
parser.add_argument(
"--endpoint",
type=str,
required=False,
default="production",
choices=["production", "staging"],
help="Whether to use letsencrypt/acme production/live endpoints \
or staging endpoints. production endpoints are used by default. \
eg: --endpoint staging",
)
parser.add_argument(
"--email",
type=str,
required=False,
help="Email to be used for registration and recovery. \
eg: --email me@example.com",
)
parser.add_argument(
"--action",
type=str,
required=True,
choices=["run", "renew"],
help="The action that you want to perform. \
Either run (get a new certificate) or renew (renew a certificate). \
eg: --action run",
)
parser.add_argument(
"--out_dir",
type=str,
required=False,
default=os.getcwd(),
help="""The dir where the certificate and keys file will be stored.
default: The directory you run sewer command.
eg: --out_dir /data/ssl/
""",
)
parser.add_argument(
"--loglevel",
type=str,
required=False,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="The log level to output log messages at. \
eg: --loglevel DEBUG",
)
args = parser.parse_args()
dns_provider = args.dns
domain = args.domain
alt_domains = args.alt_domains
action = args.action
account_key = args.account_key
certificate_key = args.certificate_key
bundle_name = args.bundle_name
endpoint = args.endpoint
email = args.email
loglevel = args.loglevel
out_dir = args.out_dir
# Make sure the output dir user specified is writable
if not os.access(out_dir, os.W_OK):
raise OSError("The dir '{0}' is not writable".format(out_dir))
logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
if not logger.handlers:
logger.addHandler(handler)
logger.setLevel(loglevel)
if account_key:
account_key = account_key.read()
if certificate_key:
certificate_key = certificate_key.read()
if bundle_name:
file_name = bundle_name
else:
file_name = "{0}".format(domain)
if endpoint == "staging":
ACME_DIRECTORY_URL = ACME_DIRECTORY_URL_STAGING
else:
ACME_DIRECTORY_URL = ACME_DIRECTORY_URL_PRODUCTION
if dns_provider == "cloudflare":
from . import CloudFlareDns
try:
CLOUDFLARE_EMAIL = os.environ["CLOUDFLARE_EMAIL"]
CLOUDFLARE_API_KEY = os.environ["CLOUDFLARE_API_KEY"]
dns_class = CloudFlareDns(
CLOUDFLARE_EMAIL=CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY=CLOUDFLARE_API_KEY
)
logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
elif dns_provider == "aurora":
from . import AuroraDns
try:
AURORA_API_KEY = os.environ["AURORA_API_KEY"]
AURORA_SECRET_KEY = os.environ["AURORA_SECRET_KEY"]
dns_class = AuroraDns(
AURORA_API_KEY=AURORA_API_KEY, AURORA_SECRET_KEY=AURORA_SECRET_KEY
)
logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
elif dns_provider == "acmedns":
from . import AcmeDnsDns
try:
ACME_DNS_API_USER = os.environ["ACME_DNS_API_USER"]
ACME_DNS_API_KEY = os.environ["ACME_DNS_API_KEY"]
ACME_DNS_API_BASE_URL = os.environ["ACME_DNS_API_BASE_URL"]
dns_class = AcmeDnsDns(
ACME_DNS_API_USER=ACME_DNS_API_USER,
ACME_DNS_API_KEY=ACME_DNS_API_KEY,
ACME_DNS_API_BASE_URL=ACME_DNS_API_BASE_URL,
)
logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
elif dns_provider == "aliyun":
from . import AliyunDns
try:
aliyun_ak = os.environ["ALIYUN_AK_ID"]
aliyun_secret = os.environ["ALIYUN_AK_SECRET"]
aliyun_endpoint = os.environ.get("ALIYUN_ENDPOINT", "cn-beijing")
dns_class = AliyunDns(aliyun_ak, aliyun_secret, aliyun_endpoint)
logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
elif dns_provider == "hurricane":
from . import HurricaneDns
try:
he_username = os.environ["HURRICANE_USERNAME"]
he_password = os.environ["HURRICANE_PASSWORD"]
dns_class = HurricaneDns(he_username, he_password)
logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
elif dns_provider == "rackspace":
from . import RackspaceDns
try:
RACKSPACE_USERNAME = os.environ["RACKSPACE_USERNAME"]
RACKSPACE_API_KEY = os.environ["RACKSPACE_API_KEY"]
dns_class = RackspaceDns(RACKSPACE_USERNAME, RACKSPACE_API_KEY)
logger.info("chosen_dns_prover. Using {0} as dns provider. ".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
elif dns_provider == "dnspod":
from . import DNSPodDns
try:
DNSPOD_ID = os.environ["DNSPOD_ID"]
DNSPOD_API_KEY = os.environ["DNSPOD_API_KEY"]
dns_class = DNSPodDns(DNSPOD_ID, DNSPOD_API_KEY)
logger.info("chosen_dns_prover. Using {0} as dns provider. ".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
elif dns_provider == "duckdns":
from . import DuckDNSDns
try:
duckdns_token = os.environ["DUCKDNS_TOKEN"]
dns_class = DuckDNSDns(duckdns_token=duckdns_token)
logger.info("chosen_dns_provider. Using {0} as dns provider.".format(dns_provider))
except KeyError as e:
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
raise
else:
raise ValueError("The dns provider {0} is not recognised.".format(dns_provider))
client = Client(
domain_name=domain,
dns_class=dns_class,
domain_alt_names=alt_domains,
contact_email=email,
account_key=account_key,
certificate_key=certificate_key,
ACME_DIRECTORY_URL=ACME_DIRECTORY_URL,
LOG_LEVEL=loglevel,
)
certificate_key = client.certificate_key
account_key = client.account_key
# prepare file path
account_key_file_path = os.path.join(out_dir, "{0}.account.key".format(file_name))
crt_file_path = os.path.join(out_dir, "{0}.crt".format(file_name))
crt_key_file_path = os.path.join(out_dir, "{0}.key".format(file_name))
# write out account_key in out_dir directory
with open(account_key_file_path, "w") as account_file:
account_file.write(account_key)
logger.info("account key succesfully written to {0}.".format(account_key_file_path))
if action == "renew":
message = "Certificate Succesfully renewed. The certificate, certificate key and account key have been saved in the current directory"
certificate = client.renew()
else:
message = "Certificate Succesfully issued. The certificate, certificate key and account key have been saved in the current directory"
certificate = client.cert()
# write out certificate and certificate key in out_dir directory
with open(crt_file_path, "w") as certificate_file:
certificate_file.write(certificate)
with open(crt_key_file_path, "w") as certificate_key_file:
certificate_key_file.write(certificate_key)
logger.info("certificate succesfully written to {0}.".format(crt_file_path))
logger.info("certificate key succesfully written to {0}.".format(crt_key_file_path))
logger.info("the_end. {0}".format(message))
import time
import copy
import json
import base64
import hashlib
import logging
import binascii
import platform
import sys
import requests
import OpenSSL
import cryptography
from . import __version__ as sewer_version
from .config import ACME_DIRECTORY_URL_PRODUCTION
requests.packages.urllib3.disable_warnings()
class Client(object):
"""
todo: improve documentation.
usage:
import sewer
dns_class = sewer.CloudFlareDns(CLOUDFLARE_EMAIL='example@example.com',
CLOUDFLARE_API_KEY='nsa-grade-api-key')
1. to create a new certificate.
client = sewer.Client(domain_name='example.com',
dns_class=dns_class)
certificate = client.cert()
certificate_key = client.certificate_key
account_key = client.account_key
with open('certificate.crt', 'w') as certificate_file:
certificate_file.write(certificate)
with open('certificate.key', 'w') as certificate_key_file:
certificate_key_file.write(certificate_key)
2. to renew a certificate:
with open('account_key.key', 'r') as account_key_file:
account_key = account_key_file.read()
client = sewer.Client(domain_name='example.com',
dns_class=dns_class,
account_key=account_key)
certificate = client.renew()
certificate_key = client.certificate_key
todo:
- handle more exceptions
"""
def __init__(
self,
domain_name,
dns_class,
domain_alt_names=None,
contact_email=None,
account_key=None,
certificate_key=None,
bits=2048,
digest="sha256",
ACME_REQUEST_TIMEOUT=7,
ACME_AUTH_STATUS_WAIT_PERIOD=8,
ACME_AUTH_STATUS_MAX_CHECKS=3,
ACME_DIRECTORY_URL=ACME_DIRECTORY_URL_PRODUCTION,
LOG_LEVEL="INFO",
):
self.domain_name = domain_name
self.dns_class = dns_class
if not domain_alt_names:
domain_alt_names = []
self.domain_alt_names = domain_alt_names
self.domain_alt_names = list(set(self.domain_alt_names))
self.contact_email = contact_email
self.bits = bits
self.digest = digest
self.ACME_REQUEST_TIMEOUT = ACME_REQUEST_TIMEOUT
self.ACME_AUTH_STATUS_WAIT_PERIOD = ACME_AUTH_STATUS_WAIT_PERIOD
self.ACME_AUTH_STATUS_MAX_CHECKS = ACME_AUTH_STATUS_MAX_CHECKS
self.ACME_DIRECTORY_URL = ACME_DIRECTORY_URL
self.LOG_LEVEL = LOG_LEVEL.upper()
self.logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
if not self.logger.handlers:
self.logger.addHandler(handler)
self.logger.setLevel(self.LOG_LEVEL)
try:
self.all_domain_names = copy.copy(self.domain_alt_names)
self.all_domain_names.insert(0, self.domain_name)
self.domain_alt_names = list(set(self.domain_alt_names))
self.User_Agent = self.get_user_agent()
acme_endpoints = self.get_acme_endpoints().json()
self.ACME_GET_NONCE_URL = acme_endpoints["newNonce"]
self.ACME_TOS_URL = acme_endpoints["meta"]["termsOfService"]
self.ACME_KEY_CHANGE_URL = acme_endpoints["keyChange"]
self.ACME_NEW_ACCOUNT_URL = acme_endpoints["newAccount"]
self.ACME_NEW_ORDER_URL = acme_endpoints["newOrder"]
self.ACME_REVOKE_CERT_URL = acme_endpoints["revokeCert"]
# unique account identifier
# https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.2
self.kid = None
self.certificate_key = certificate_key or self.create_certificate_key()
self.csr = self.create_csr()
if not account_key:
self.account_key = self.create_account_key()
self.PRIOR_REGISTERED = False
else:
self.account_key = account_key
self.PRIOR_REGISTERED = True
self.logger.info(
"intialise_success, sewer_version={0}, domain_names={1}, acme_server={2}".format(
sewer_version.__version__,
self.all_domain_names,
self.ACME_DIRECTORY_URL[:20] + "...",
)
)
except Exception as e:
self.logger.error("Unable to intialise client. error={0}".format(str(e)))
raise e
@staticmethod
def log_response(response):
"""
renders response as json or as a string
"""
# TODO: use this to handle all response logs.
try:
log_body = response.json()
except ValueError:
log_body = response.content[:30]
return log_body
@staticmethod
def get_user_agent():
return "python-requests/{requests_version} ({system}: {machine}) sewer {sewer_version} ({sewer_url})".format(
requests_version=requests.__version__,
system=platform.system(),
machine=platform.machine(),
sewer_version=sewer_version.__version__,
sewer_url=sewer_version.__url__,
)
def get_acme_endpoints(self):
self.logger.debug("get_acme_endpoints")
headers = {"User-Agent": self.User_Agent}
get_acme_endpoints = requests.get(
self.ACME_DIRECTORY_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
)
self.logger.debug(
"get_acme_endpoints_response. status_code={0}".format(get_acme_endpoints.status_code)
)
if get_acme_endpoints.status_code not in [200, 201]:
raise ValueError(
"Error while getting Acme endpoints: status_code={status_code} response={response}".format(
status_code=get_acme_endpoints.status_code,
response=self.log_response(get_acme_endpoints),
)
)
return get_acme_endpoints
def create_certificate_key(self):
self.logger.debug("create_certificate_key")
return self.create_key().decode()
def create_account_key(self):
self.logger.debug("create_account_key")
return self.create_key().decode()
def create_key(self, key_type=OpenSSL.crypto.TYPE_RSA):
key = OpenSSL.crypto.PKey()
key.generate_key(key_type, self.bits)
private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
return private_key
def create_csr(self):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
The CSR is sent in the base64url-encoded version of the DER format. (NB: this
field uses base64url, and does not include headers, it is different from PEM.)
"""
self.logger.debug("create_csr")
X509Req = OpenSSL.crypto.X509Req()
X509Req.get_subject().CN = self.domain_name
if self.domain_alt_names:
SAN = "DNS:{0}, ".format(self.domain_name).encode("utf8") + ", ".join(
"DNS:" + i for i in self.domain_alt_names
).encode("utf8")
else:
SAN = "DNS:{0}".format(self.domain_name).encode("utf8")
X509Req.add_extensions(
[
OpenSSL.crypto.X509Extension(
"subjectAltName".encode("utf8"), critical=False, value=SAN
)
]
)
pk = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, self.certificate_key.encode()
)
X509Req.set_pubkey(pk)
X509Req.set_version(2)
X509Req.sign(pk, self.digest)
return OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_ASN1, X509Req)
def acme_register(self):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.3
The server creates an account and stores the public key used to
verify the JWS (i.e., the "jwk" element of the JWS header) to
authenticate future requests from the account.
The server returns this account object in a 201 (Created) response, with the account URL
in a Location header field.
This account URL will be used in subsequest requests to ACME, as the "kid" value in the acme header.
If the server already has an account registered with the provided
account key, then it MUST return a response with a 200 (OK) status
code and provide the URL of that account in the Location header field.
If there is an existing account with the new key
provided, then the server SHOULD use status code 409 (Conflict) and
provide the URL of that account in the Location header field
"""
self.logger.info("acme_register")
if self.PRIOR_REGISTERED:
payload = {"onlyReturnExisting": True}
elif self.contact_email:
payload = {
"termsOfServiceAgreed": True,
"contact": ["mailto:{0}".format(self.contact_email)],
}
else:
payload = {"termsOfServiceAgreed": True}
url = self.ACME_NEW_ACCOUNT_URL
acme_register_response = self.make_signed_acme_request(url=url, payload=payload)
self.logger.debug(
"acme_register_response. status_code={0}. response={1}".format(
acme_register_response.status_code, self.log_response(acme_register_response)
)
)
if acme_register_response.status_code not in [201, 200, 409]:
raise ValueError(
"Error while registering: status_code={status_code} response={response}".format(
status_code=acme_register_response.status_code,
response=self.log_response(acme_register_response),
)
)
kid = acme_register_response.headers["Location"]
setattr(self, "kid", kid)
self.logger.info("acme_register_success")
return acme_register_response
def apply_for_cert_issuance(self):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
The order object returned by the server represents a promise that if
the client fulfills the server's requirements before the "expires"
time, then the server will be willing to finalize the order upon
request and issue the requested certificate. In the order object,
any authorization referenced in the "authorizations" array whose
status is "pending" represents an authorization transaction that the
client must complete before the server will issue the certificate.
Once the client believes it has fulfilled the server's requirements,
it should send a POST request to the order resource's finalize URL.
The POST body MUST include a CSR:
The date values seem to be ignored by LetsEncrypt although they are
in the ACME draft spec; https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
"""
self.logger.info("apply_for_cert_issuance")
identifiers = []
for domain_name in self.all_domain_names:
identifiers.append({"type": "dns", "value": domain_name})
payload = {"identifiers": identifiers}
url = self.ACME_NEW_ORDER_URL
apply_for_cert_issuance_response = self.make_signed_acme_request(url=url, payload=payload)
self.logger.debug(
"apply_for_cert_issuance_response. status_code={0}. response={1}".format(
apply_for_cert_issuance_response.status_code,
self.log_response(apply_for_cert_issuance_response),
)
)
if apply_for_cert_issuance_response.status_code != 201:
raise ValueError(
"Error applying for certificate issuance: status_code={status_code} response={response}".format(
status_code=apply_for_cert_issuance_response.status_code,
response=self.log_response(apply_for_cert_issuance_response),
)
)
apply_for_cert_issuance_response_json = apply_for_cert_issuance_response.json()
finalize_url = apply_for_cert_issuance_response_json["finalize"]
authorizations = apply_for_cert_issuance_response_json["authorizations"]
self.logger.info("apply_for_cert_issuance_success")
return authorizations, finalize_url
def get_identifier_authorization(self, url):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5
When a client receives an order from the server it downloads the
authorization resources by sending GET requests to the indicated
URLs. If the client initiates authorization using a request to the
new authorization resource, it will have already received the pending
authorization object in the response to that request.
This is also where we get the challenges/tokens.
"""
self.logger.info("get_identifier_authorization")
headers = {"User-Agent": self.User_Agent}
get_identifier_authorization_response = requests.get(
url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
)
self.logger.debug(
"get_identifier_authorization_response. status_code={0}. response={1}".format(
get_identifier_authorization_response.status_code,
self.log_response(get_identifier_authorization_response),
)
)
if get_identifier_authorization_response.status_code not in [200, 201]:
raise ValueError(
"Error getting identifier authorization: status_code={status_code} response={response}".format(
status_code=get_identifier_authorization_response.status_code,
response=self.log_response(get_identifier_authorization_response),
)
)
res = get_identifier_authorization_response.json()
domain = res["identifier"]["value"]
wildcard = res.get("wildcard")
if wildcard:
domain = "*." + domain
for i in res["challenges"]:
if i["type"] == "dns-01":
dns_challenge = i
dns_token = dns_challenge["token"]
dns_challenge_url = dns_challenge["url"]
identifier_auth = {
"domain": domain,
"url": url,
"wildcard": wildcard,
"dns_token": dns_token,
"dns_challenge_url": dns_challenge_url,
}
self.logger.debug(
"get_identifier_authorization_success. identifier_auth={0}".format(identifier_auth)
)
self.logger.info("get_identifier_authorization_success")
return identifier_auth
def get_keyauthorization(self, dns_token):
self.logger.debug("get_keyauthorization")
acme_header_jwk_json = json.dumps(
self.get_acme_header("GET_THUMBPRINT")["jwk"], sort_keys=True, separators=(",", ":")
)
acme_thumbprint = self.calculate_safe_base64(
hashlib.sha256(acme_header_jwk_json.encode("utf8")).digest()
)
acme_keyauthorization = "{0}.{1}".format(dns_token, acme_thumbprint)
base64_of_acme_keyauthorization = self.calculate_safe_base64(
hashlib.sha256(acme_keyauthorization.encode("utf8")).digest()
)
return acme_keyauthorization, base64_of_acme_keyauthorization
def check_authorization_status(self, authorization_url, desired_status=None):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5.1
To check on the status of an authorization, the client sends a GET(polling)
request to the authorization URL, and the server responds with the
current authorization object.
https://tools.ietf.org/html/draft-ietf-acme-acme#section-8.2
Clients SHOULD NOT respond to challenges until they believe that the
server's queries will succeed. If a server's initial validation
query fails, the server SHOULD retry[intended to address things like propagation delays in
HTTP/DNS provisioning] the query after some time.
The server MUST provide information about its retry state to the
client via the "errors" field in the challenge and the Retry-After
"""
self.logger.info("check_authorization_status")
desired_status = desired_status or ["pending", "valid"]
number_of_checks = 0
while True:
headers = {"User-Agent": self.User_Agent}
check_authorization_status_response = requests.get(
authorization_url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
)
a_auth = check_authorization_status_response.json()
print(a_auth)
authorization_status = a_auth["status"]
number_of_checks = number_of_checks + 1
self.logger.debug(
"check_authorization_status_response. status_code={0}. response={1}".format(
check_authorization_status_response.status_code,
self.log_response(check_authorization_status_response),
)
)
if number_of_checks == self.ACME_AUTH_STATUS_MAX_CHECKS:
raise StopIteration(
"Checks done={0}. Max checks allowed={1}. Interval between checks={2}seconds.".format(
number_of_checks,
self.ACME_AUTH_STATUS_MAX_CHECKS,
self.ACME_AUTH_STATUS_WAIT_PERIOD,
)
)
if authorization_status in desired_status:
break
else:
# for any other status, sleep then retry
time.sleep(self.ACME_AUTH_STATUS_WAIT_PERIOD)
self.logger.info("check_authorization_status_success")
return check_authorization_status_response
def respond_to_challenge(self, acme_keyauthorization, dns_challenge_url):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5.1
To prove control of the identifier and receive authorization, the
client needs to respond with information to complete the challenges.
The server is said to "finalize" the authorization when it has
completed one of the validations, by assigning the authorization a
status of "valid" or "invalid".
Usually, the validation process will take some time, so the client
will need to poll the authorization resource to see when it is finalized.
To check on the status of an authorization, the client sends a GET(polling)
request to the authorization URL, and the server responds with the
current authorization object.
"""
self.logger.info("respond_to_challenge")
payload = {"keyAuthorization": "{0}".format(acme_keyauthorization)}
respond_to_challenge_response = self.make_signed_acme_request(dns_challenge_url, payload)
self.logger.debug(
"respond_to_challenge_response. status_code={0}. response={1}".format(
respond_to_challenge_response.status_code,
self.log_response(respond_to_challenge_response),
)
)
self.logger.info("respond_to_challenge_success")
return respond_to_challenge_response
def send_csr(self, finalize_url):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
Once the client believes it has fulfilled the server's requirements,
it should send a POST request(include a CSR) to the order resource's finalize URL.
A request to finalize an order will result in error if the order indicated does not have status "pending",
if the CSR and order identifiers differ, or if the account is not authorized for the identifiers indicated in the CSR.
The CSR is sent in the base64url-encoded version of the DER format(OpenSSL.crypto.FILETYPE_ASN1)
A valid request to finalize an order will return the order to be finalized.
The client should begin polling the order by sending a
GET request to the order resource to obtain its current state.
"""
self.logger.info("send_csr")
payload = {"csr": self.calculate_safe_base64(self.csr)}
send_csr_response = self.make_signed_acme_request(url=finalize_url, payload=payload)
self.logger.debug(
"send_csr_response. status_code={0}. response={1}".format(
send_csr_response.status_code, self.log_response(send_csr_response)
)
)
if send_csr_response.status_code not in [200, 201]:
raise ValueError(
"Error sending csr: status_code={status_code} response={response}".format(
status_code=send_csr_response.status_code,
response=self.log_response(send_csr_response),
)
)
send_csr_response_json = send_csr_response.json()
certificate_url = send_csr_response_json["certificate"]
self.logger.info("send_csr_success")
return certificate_url
def download_certificate(self, certificate_url):
self.logger.info("download_certificate")
download_certificate_response = self.make_signed_acme_request(
certificate_url, payload="DOWNLOAD_Z_CERTIFICATE"
)
self.logger.debug(
"download_certificate_response. status_code={0}. response={1}".format(
download_certificate_response.status_code,
self.log_response(download_certificate_response),
)
)
if download_certificate_response.status_code not in [200, 201]:
raise ValueError(
"Error fetching signed certificate: status_code={status_code} response={response}".format(
status_code=download_certificate_response.status_code,
response=self.log_response(download_certificate_response),
)
)
pem_certificate = download_certificate_response.content.decode("utf-8")
self.logger.info("download_certificate_success")
return pem_certificate
def sign_message(self, message):
self.logger.debug("sign_message")
pk = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, self.account_key.encode())
return OpenSSL.crypto.sign(pk, message.encode("utf8"), self.digest)
def get_nonce(self):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.4
Each request to an ACME server must include a fresh unused nonce
in order to protect against replay attacks.
"""
self.logger.debug("get_nonce")
headers = {"User-Agent": self.User_Agent}
response = requests.get(
self.ACME_GET_NONCE_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
)
nonce = response.headers["Replay-Nonce"]
return nonce
@staticmethod
def stringfy_items(payload):
"""
method that takes a dictionary and then converts any keys or values
in that are of type bytes into unicode strings.
This is necessary esp if you want to then turn that dict into a json string.
"""
if isinstance(payload, str):
return payload
for k, v in payload.items():
if isinstance(k, bytes):
k = k.decode("utf-8")
if isinstance(v, bytes):
v = v.decode("utf-8")
payload[k] = v
return payload
@staticmethod
def calculate_safe_base64(un_encoded_data):
"""
takes in a string or bytes
returns a string
"""
if sys.version_info[0] == 3:
if isinstance(un_encoded_data, str):
un_encoded_data = un_encoded_data.encode("utf8")
r = base64.urlsafe_b64encode(un_encoded_data).rstrip(b"=")
return r.decode("utf8")
def get_acme_header(self, url):
"""
https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.2
The JWS Protected Header MUST include the following fields:
- "alg" (Algorithm)
- "jwk" (JSON Web Key, only for requests to new-account and revoke-cert resources)
- "kid" (Key ID, for all other requests). gotten from self.ACME_NEW_ACCOUNT_URL
- "nonce". gotten from self.ACME_GET_NONCE_URL
- "url"
"""
self.logger.debug("get_acme_header")
header = {"alg": "RS256", "nonce": self.get_nonce(), "url": url}
if url in [self.ACME_NEW_ACCOUNT_URL, self.ACME_REVOKE_CERT_URL, "GET_THUMBPRINT"]:
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
self.account_key.encode(),
password=None,
backend=cryptography.hazmat.backends.default_backend(),
)
public_key_public_numbers = private_key.public_key().public_numbers()
# private key public exponent in hex format
exponent = "{0:x}".format(public_key_public_numbers.e)
exponent = "0{0}".format(exponent) if len(exponent) % 2 else exponent
# private key modulus in hex format
modulus = "{0:x}".format(public_key_public_numbers.n)
jwk = {
"kty": "RSA",
"e": self.calculate_safe_base64(binascii.unhexlify(exponent)),
"n": self.calculate_safe_base64(binascii.unhexlify(modulus)),
}
header["jwk"] = jwk
else:
header["kid"] = self.kid
return header
def make_signed_acme_request(self, url, payload):
self.logger.debug("make_signed_acme_request")
headers = {"User-Agent": self.User_Agent}
payload = self.stringfy_items(payload)
if payload in ["GET_Z_CHALLENGE", "DOWNLOAD_Z_CERTIFICATE"]:
response = requests.get(url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False)
else:
payload64 = self.calculate_safe_base64(json.dumps(payload))
protected = self.get_acme_header(url)
protected64 = self.calculate_safe_base64(json.dumps(protected))
signature = self.sign_message(message="{0}.{1}".format(protected64, payload64)) # bytes
signature64 = self.calculate_safe_base64(signature) # str
data = json.dumps(
{"protected": protected64, "payload": payload64, "signature": signature64}
)
headers.update({"Content-Type": "application/jose+json"})
response = requests.post(
url, data=data.encode("utf8"), timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ,verify=False
)
return response
def get_certificate(self):
self.logger.debug("get_certificate")
domain_dns_value = "placeholder"
dns_names_to_delete = []
try:
self.acme_register()
authorizations, finalize_url = self.apply_for_cert_issuance()
responders = []
for url in authorizations:
identifier_auth = self.get_identifier_authorization(url)
authorization_url = identifier_auth["url"]
dns_name = identifier_auth["domain"]
dns_token = identifier_auth["dns_token"]
dns_challenge_url = identifier_auth["dns_challenge_url"]
acme_keyauthorization, domain_dns_value = self.get_keyauthorization(dns_token)
self.dns_class.create_dns_record(dns_name, domain_dns_value)
dns_names_to_delete.append(
{"dns_name": dns_name, "domain_dns_value": domain_dns_value}
)
responders.append(
{
"authorization_url": authorization_url,
"acme_keyauthorization": acme_keyauthorization,
"dns_challenge_url": dns_challenge_url,
}
)
# for a case where you want certificates for *.example.com and example.com
# you have to create both dns records AND then respond to the challenge.
# see issues/83
for i in responders:
# Make sure the authorization is in a status where we can submit a challenge
# response. The authorization can be in the "valid" state before submitting
# a challenge response if there was a previous authorization for these hosts
# that was successfully validated, still cached by the server.
auth_status_response = self.check_authorization_status(i["authorization_url"])
if auth_status_response.json()["status"] == "pending":
self.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"])
for i in responders:
# Before sending a CSR, we need to make sure the server has completed the
# validation for all the authorizations
self.check_authorization_status(i["authorization_url"], ["valid"])
certificate_url = self.send_csr(finalize_url)
certificate = self.download_certificate(certificate_url)
except Exception as e:
self.logger.error("Error: Unable to issue certificate. error={0}".format(str(e)))
raise e
finally:
for i in dns_names_to_delete:
self.dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"])
return certificate
def cert(self):
"""
convenience method to get a certificate without much hassle
"""
return self.get_certificate()
def renew(self):
"""
renews a certificate.
A renewal is actually just getting a new certificate.
An issuance request counts as a renewal if it contains the exact same set of hostnames as a previously issued certificate.
https://letsencrypt.org/docs/rate-limits/
"""
return self.cert()
ACME_DIRECTORY_URL_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory"
ACME_DIRECTORY_URL_PRODUCTION = "https://acme-v02.api.letsencrypt.org/directory"
from .common import BaseDns # noqa: F401
from .auroradns import AuroraDns # noqa: F401
from .cloudflare import CloudFlareDns # noqa: F401
from .acmedns import AcmeDnsDns # noqa: F401
from .aliyundns import AliyunDns # noqa: F401
from .hurricane import HurricaneDns # noqa: F401
from .rackspace import RackspaceDns # noqa: F401
from .dnspod import DNSPodDns
from .duckdns import DuckDNSDns
try:
import urllib.parse as urlparse
except:
import urlparse
try:
acmedns_dependencies = True
from dns.resolver import Resolver
except ImportError:
acmedns_dependencies = False
import requests
from . import common
class AcmeDnsDns(common.BaseDns):
"""
"""
dns_provider_name = "acmedns"
def __init__(self, ACME_DNS_API_USER, ACME_DNS_API_KEY, ACME_DNS_API_BASE_URL):
if not acmedns_dependencies:
raise ImportError(
"""You need to install AcmeDnsDns dependencies. run; pip3 install sewer[acmedns]"""
)
self.ACME_DNS_API_USER = ACME_DNS_API_USER
self.ACME_DNS_API_KEY = ACME_DNS_API_KEY
self.HTTP_TIMEOUT = 65 # seconds
if ACME_DNS_API_BASE_URL[-1] != "/":
self.ACME_DNS_API_BASE_URL = ACME_DNS_API_BASE_URL + "/"
else:
self.ACME_DNS_API_BASE_URL = ACME_DNS_API_BASE_URL
super(AcmeDnsDns, self).__init__()
def create_dns_record(self, domain_name, domain_dns_value):
self.logger.info("create_dns_record")
# if we have been given a wildcard name, strip wildcard
domain_name = domain_name.lstrip("*.")
resolver = Resolver(configure=False)
resolver.nameservers = ["8.8.8.8"]
answer = resolver.query("_acme-challenge.{0}.".format(domain_name), "TXT")
subdomain, _ = str(answer.canonical_name).split(".", 1)
url = urlparse.urljoin(self.ACME_DNS_API_BASE_URL, "update")
headers = {"X-Api-User": self.ACME_DNS_API_USER, "X-Api-Key": self.ACME_DNS_API_KEY}
body = {"subdomain": subdomain, "txt": domain_dns_value}
update_acmedns_dns_record_response = requests.post(
url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT
)
self.logger.debug(
"update_acmedns_dns_record_response. status_code={0}. response={1}".format(
update_acmedns_dns_record_response.status_code,
self.log_response(update_acmedns_dns_record_response),
)
)
if update_acmedns_dns_record_response.status_code != 200:
# raise error so that we do not continue to make calls to ACME
# server
raise ValueError(
"Error creating acme-dns dns record: status_code={status_code} response={response}".format(
status_code=update_acmedns_dns_record_response.status_code,
response=self.log_response(update_acmedns_dns_record_response),
)
)
self.logger.info("create_dns_record_end")
def delete_dns_record(self, domain_name, domain_dns_value):
self.logger.info("delete_dns_record")
# acme-dns doesn't support this
self.logger.info("delete_dns_record_success")
import json
try:
aliyun_dependencies = True
from aliyunsdkcore import client
from aliyunsdkalidns.request.v20150109 import DescribeDomainRecordsRequest
from aliyunsdkalidns.request.v20150109 import AddDomainRecordRequest
from aliyunsdkalidns.request.v20150109 import DeleteDomainRecordRequest
except ImportError:
aliyun_dependencies = False
from . import common
class _ResponseForAliyun(object):
"""
wrapper aliyun resp to the format sewer wanted.
"""
def __init__(self, status_code=200, content=None, headers=None):
self.status_code = status_code
self.headers = headers or {}
self.content = content or {}
self.content = json.dumps(content)
super(_ResponseForAliyun, self).__init__()
def json(self):
return json.loads(self.content)
class AliyunDns(common.BaseDns):
def __init__(self, key, secret, endpoint="cn-beijing", debug=False):
"""
aliyun dns client
:param str key: access key
:param str secret: access sceret
:param str endpoint: endpoint
:param bool debug: if debug?
"""
super(AliyunDns, self).__init__()
if not aliyun_dependencies:
raise ImportError(
"""You need to install aliyunDns dependencies. run; pip3 install sewer[aliyun]"""
)
self._key = key
self._secret = secret
self._endpoint = endpoint
self._debug = debug
self.clt = client.AcsClient(self._key, self._secret, self._endpoint, debug=self._debug)
def _send_reqeust(self, request):
"""
send request to aliyun
"""
request.set_accept_format("json")
try:
status, headers, result = self.clt.implementation_of_do_action(request)
result = json.loads(result)
if "Message" in result or "Code" in result:
result["Success"] = False
self.logger.warning("aliyundns resp error: %s", result)
except Exception as exc:
self.logger.warning("aliyundns failed to send request: %s, %s", str(exc), request)
status, headers, result = 502, {}, '{"Success": false}'
result = json.loads(result)
if self._debug:
self.logger.info("aliyundns request name: %s", request.__class__.__name__)
self.logger.info("aliyundns request query: %s", request.get_query_params())
return _ResponseForAliyun(status, result, headers)
def query_recored_items(self, host, zone=None, tipe=None, page=1, psize=200):
"""
query recored items.
:param str host: like example.com
:param str zone: like menduo.example.com
:param str tipe: TXT, CNAME, IP or other
:param int page:
:param int psize:
:return dict: res = {
'DomainRecords':
{'Record': [
{
'DomainName': 'menduo.net',
'Line': 'default',
'Locked': False,
'RR': 'zb',
'RecordId': '3989515483698964',
'Status': 'ENABLE',
'TTL': 600,
'Type': 'A',
'Value': '127.0.0.1',
'Weight': 1
},
{
'DomainName': 'menduo.net',
'Line': 'default',
'Locked': False,
'RR': 'a.sub',
'RecordId': '3989515480778964',
'Status': 'ENABLE',
'TTL': 600,
'Type': 'CNAME',
'Value': 'h.p.menduo.net',
'Weight': 1
}
]
},
'PageNumber': 1,
'PageSize': 20,
'RequestId': 'FC4D02CD-EDCC-4EE8-942F-1497CCC3B10E',
'TotalCount': 95
}
"""
request = DescribeDomainRecordsRequest.DescribeDomainRecordsRequest()
request.get_action_name()
request.set_DomainName(host)
request.set_PageNumber(page)
request.set_PageSize(psize)
if zone:
request.set_RRKeyWord(zone)
if tipe:
request.set_TypeKeyWord(tipe)
resp = self._send_reqeust(request)
body = resp.json()
return body
def query_recored_id(self, root, zone, tipe="TXT"):
"""
find recored
:param str root: root host, like example.com
:param str zone: sub zone, like menduo.example.com
:param str tipe: record tipe, TXT, CNAME, IP. we use TXT
:return str:
"""
record_id = None
recoreds = self.query_recored_items(root, zone, tipe=tipe)
recored_list = recoreds.get("DomainRecords", {}).get("Record", [])
recored_item_list = [i for i in recored_list if i["RR"] == zone]
if len(recored_item_list):
record_id = recored_item_list[0]["RecordId"]
return record_id
@staticmethod
def extract_zone(domain_name):
"""
extract domain to root, sub, acme_txt
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
:return tuple: root, zone, acme_txt
"""
# if we have been given a wildcard name, strip wildcard
domain_name = domain_name.lstrip("*.")
if domain_name.count(".") > 1:
zone, middle, last = str(domain_name).rsplit(".", 2)
root = ".".join([middle, last])
acme_txt = "_acme-challenge.%s" % zone
else:
zone = ""
root = domain_name
acme_txt = "_acme-challenge"
return root, zone, acme_txt
def create_dns_record(self, domain_name, domain_dns_value):
"""
create a dns record
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
:param str domain_dns_value: the value sewer client passed in.
:return _ResponseForAliyun:
"""
self.logger.info("create_dns_record start: %s", (domain_name, domain_dns_value))
root, _, acme_txt = self.extract_zone(domain_name)
request = AddDomainRecordRequest.AddDomainRecordRequest()
request.set_DomainName(root)
request.set_TTL(600)
request.set_RR(acme_txt)
request.set_Type("TXT")
request.set_Value(domain_dns_value)
resp = self._send_reqeust(request)
self.logger.info("create_dns_record end: %s", (domain_name, domain_dns_value, resp.json()))
return resp
def delete_dns_record(self, domain_name, domain_dns_value):
"""
delete a txt record we created just now.
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
:param str domain_dns_value: the value sewer client passed in. we do not use this.
:return _ResponseForAliyun:
:return:
"""
self.logger.info("delete_dns_record start: %s", (domain_name, domain_dns_value))
root, _, acme_txt = self.extract_zone(domain_name)
record_id = self.query_recored_id(root, acme_txt)
if not record_id:
msg = "failed to find record_id of domain: %s, value: %s", domain_name, domain_dns_value
self.logger.warning(msg)
return
self.logger.info("start to delete dns record, id: %s", record_id)
request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
request.set_RecordId(record_id)
resp = self._send_reqeust(request)
self.logger.info("delete_dns_record end: %s", (domain_name, domain_dns_value, resp.json()))
return resp
# DNS Provider for AuroRa DNS from the dutch hosting provider pcextreme
# https://www.pcextreme.nl/aurora/dns
# Aurora uses libcloud from apache
# https://libcloud.apache.org/
try:
aurora_dependencies = True
from libcloud.dns.providers import get_driver
from libcloud.dns.types import Provider, RecordType
import tldextract
except ImportError:
aurora_dependencies = False
from . import common
class AuroraDns(common.BaseDns):
"""
Todo: re-organize this class so that we make it easier to mock things out to
facilitate better tests.
"""
dns_provider_name = "aurora"
def __init__(self, AURORA_API_KEY, AURORA_SECRET_KEY):
if not aurora_dependencies:
raise ImportError(
"""You need to install AuroraDns dependencies. run; pip3 install sewer[aurora]"""
)
self.AURORA_API_KEY = AURORA_API_KEY
self.AURORA_SECRET_KEY = AURORA_SECRET_KEY
super(AuroraDns, self).__init__()
def create_dns_record(self, domain_name, domain_dns_value):
self.logger.info("create_dns_record")
# if we have been given a wildcard name, strip wildcard
domain_name = domain_name.lstrip("*.")
extractedDomain = tldextract.extract(domain_name)
domainSuffix = extractedDomain.domain + "." + extractedDomain.suffix
if extractedDomain.subdomain is "":
subDomain = "_acme-challenge"
else:
subDomain = "_acme-challenge." + extractedDomain.subdomain
cls = get_driver(Provider.AURORADNS)
driver = cls(key=self.AURORA_API_KEY, secret=self.AURORA_SECRET_KEY)
zone = driver.get_zone(domainSuffix)
zone.create_record(name=subDomain, type=RecordType.TXT, data=domain_dns_value)
self.logger.info("create_dns_record_success")
return
def delete_dns_record(self, domain_name, domain_dns_value):
self.logger.info("delete_dns_record")
extractedDomain = tldextract.extract(domain_name)
domainSuffix = extractedDomain.domain + "." + extractedDomain.suffix
if extractedDomain.subdomain is "":
subDomain = "_acme-challenge"
else:
subDomain = "_acme-challenge." + extractedDomain.subdomain
cls = get_driver(Provider.AURORADNS)
driver = cls(key=self.AURORA_API_KEY, secret=self.AURORA_SECRET_KEY)
zone = driver.get_zone(domainSuffix)
records = driver.list_records(zone)
for x in records:
if x.name == subDomain and x.type == "TXT":
record_id = x.id
self.logger.info(
"Found record "
+ subDomain
+ "."
+ domainSuffix
+ " with id : "
+ record_id
+ "."
)
record = driver.get_record(zone_id=zone.id, record_id=record_id)
driver.delete_record(record)
self.logger.info(
"Deleted record "
+ subDomain
+ "."
+ domainSuffix
+ " with id : "
+ record_id
+ "."
)
else:
self.logger.info(
"Record " + subDomain + "." + domainSuffix + " not found. No record to delete."
)
self.logger.info("delete_dns_record_success")
return
try:
import urllib.parse as urlparse
except:
import urlparse
import requests
from . import common
class CloudFlareDns(common.BaseDns):
"""
"""
dns_provider_name = "cloudflare"
def __init__(
self,
CLOUDFLARE_EMAIL,
CLOUDFLARE_API_KEY,
CLOUDFLARE_API_BASE_URL="https://api.cloudflare.com/client/v4/",
):
self.CLOUDFLARE_DNS_ZONE_ID = None
self.CLOUDFLARE_EMAIL = CLOUDFLARE_EMAIL
self.CLOUDFLARE_API_KEY = CLOUDFLARE_API_KEY
self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL
self.HTTP_TIMEOUT = 65 # seconds
if CLOUDFLARE_API_BASE_URL[-1] != "/":
self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + "/"
else:
self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL
super(CloudFlareDns, self).__init__()
def find_dns_zone(self, domain_name):
self.logger.debug("find_dns_zone")
url = urlparse.urljoin(self.CLOUDFLARE_API_BASE_URL, "zones?status=active")
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
find_dns_zone_response = requests.get(url, headers=headers, timeout=self.HTTP_TIMEOUT)
self.logger.debug(
"find_dns_zone_response. status_code={0}".format(find_dns_zone_response.status_code)
)
if find_dns_zone_response.status_code != 200:
raise ValueError(
"Error creating cloudflare dns record: status_code={status_code} response={response}".format(
status_code=find_dns_zone_response.status_code,
response=self.log_response(find_dns_zone_response),
)
)
result = find_dns_zone_response.json()["result"]
for i in result:
if i["name"] in domain_name:
setattr(self, "CLOUDFLARE_DNS_ZONE_ID", i["id"])
if isinstance(self.CLOUDFLARE_DNS_ZONE_ID, type(None)):
raise ValueError(
"Error unable to get DNS zone for domain_name={domain_name}: status_code={status_code} response={response}".format(
domain_name=domain_name,
status_code=find_dns_zone_response.status_code,
response=self.log_response(find_dns_zone_response),
)
)
self.logger.debug("find_dns_zone_success")
def create_dns_record(self, domain_name, domain_dns_value):
self.logger.info("create_dns_record")
# if we have been given a wildcard name, strip wildcard
domain_name = domain_name.lstrip("*.")
self.find_dns_zone(domain_name)
url = urllib.parse.urljoin(
self.CLOUDFLARE_API_BASE_URL,
"zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID),
)
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
body = {
"type": "TXT",
"name": "_acme-challenge" + "." + domain_name + ".",
"content": "{0}".format(domain_dns_value),
}
create_cloudflare_dns_record_response = requests.post(
url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT
)
self.logger.debug(
"create_cloudflare_dns_record_response. status_code={0}. response={1}".format(
create_cloudflare_dns_record_response.status_code,
self.log_response(create_cloudflare_dns_record_response),
)
)
if create_cloudflare_dns_record_response.status_code != 200:
# raise error so that we do not continue to make calls to ACME
# server
raise ValueError(
"Error creating cloudflare dns record: status_code={status_code} response={response}".format(
status_code=create_cloudflare_dns_record_response.status_code,
response=self.log_response(create_cloudflare_dns_record_response),
)
)
self.logger.info("create_dns_record_end")
def delete_dns_record(self, domain_name, domain_dns_value):
self.logger.info("delete_dns_record")
class MockResponse(object):
def __init__(self, status_code=200, content="mock-response"):
self.status_code = status_code
self.content = content
super(MockResponse, self).__init__()
def json(self):
return {}
delete_dns_record_response = MockResponse()
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
dns_name = "_acme-challenge" + "." + domain_name
list_dns_payload = {"type": "TXT", "name": dns_name}
list_dns_url = urllib.parse.urljoin(
self.CLOUDFLARE_API_BASE_URL,
"zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID),
)
list_dns_response = requests.get(
list_dns_url, params=list_dns_payload, headers=headers, timeout=self.HTTP_TIMEOUT
)
for i in range(0, len(list_dns_response.json()["result"])):
dns_record_id = list_dns_response.json()["result"][i]["id"]
url = urllib.parse.urljoin(
self.CLOUDFLARE_API_BASE_URL,
"zones/{0}/dns_records/{1}".format(self.CLOUDFLARE_DNS_ZONE_ID, dns_record_id),
)
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
delete_dns_record_response = requests.delete(
url, headers=headers, timeout=self.HTTP_TIMEOUT
)
self.logger.debug(
"delete_dns_record_response. status_code={0}. response={1}".format(
delete_dns_record_response.status_code,
self.log_response(delete_dns_record_response),
)
)
if delete_dns_record_response.status_code != 200:
# extended logging for debugging
# we do not need to raise exception
self.logger.error(
"delete_dns_record_response. status_code={0}. response={1}".format(
delete_dns_record_response.status_code,
self.log_response(delete_dns_record_response),
)
)
self.logger.info("delete_dns_record_success")
import logging
class BaseDns(object):
"""
"""
def __init__(self, LOG_LEVEL="INFO"):
self.LOG_LEVEL = LOG_LEVEL
self.dns_provider_name = self.__class__.__name__
self.logger = logging.getLogger("sewer")
handler = logging.StreamHandler()
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
if not self.logger.handlers:
self.logger.addHandler(handler)
self.logger.setLevel(self.LOG_LEVEL)
def log_response(self, response):
"""
renders a python-requests response as json or as a string
"""
try:
log_body = response.json()
except ValueError:
log_body = response.content
return log_body
def create_dns_record(self, domain_name, domain_dns_value):
"""
Method that creates/adds a dns TXT record for a domain/subdomain name on
a chosen DNS provider.
:param domain_name: :string: The domain/subdomain name whose dns record ought to be
created/added on a chosen DNS provider.
:param domain_dns_value: :string: The value/content of the TXT record that will be
created/added for the given domain/subdomain
This method should return None
Basic Usage:
If the value of the `domain_name` variable is example.com and the value of
`domain_dns_value` is HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld
Then, your implementation of this method ought to create a DNS TXT record
whose name is '_acme-challenge' + '.' + domain_name + '.' (ie: _acme-challenge.example.com. )
and whose value/content is HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld
Using a dns client like dig(https://linux.die.net/man/1/dig) to do a dns lookup should result
in something like:
dig TXT _acme-challenge.example.com
...
;; ANSWER SECTION:
_acme-challenge.example.com. 120 IN TXT "HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld"
_acme-challenge.singularity.brandur.org. 120 IN TXT "9C0DqKC_4MkowIFByHhFaP8u0Zv4z7Wz2IHM91lTKec"
Optionally, you may also use an online dns client like: https://toolbox.googleapps.com/apps/dig/#TXT/
Please consult your dns provider on how/format of their DNS TXT records.
You may also want to consult the cloudflare DNS implementation that is found in this repository.
"""
self.logger.info("create_dns_record")
raise NotImplementedError("create_dns_record method must be implemented.")
def delete_dns_record(self, domain_name, domain_dns_value):
"""
Method that deletes/removes a dns TXT record for a domain/subdomain name on
a chosen DNS provider.
:param domain_name: :string: The domain/subdomain name whose dns record ought to be
deleted/removed on a chosen DNS provider.
:param domain_dns_value: :string: The value/content of the TXT record that will be
deleted/removed for the given domain/subdomain
This method should return None
"""
self.logger.info("delete_dns_record")
raise NotImplementedError("delete_dns_record method must be implemented.")
try:
import urllib.parse as urlparse
except:
import urlparse
import requests
from . import common
class DNSPodDns(common.BaseDns):
"""
"""
dns_provider_name = "dnspod"
def __init__(self, DNSPOD_ID, DNSPOD_API_KEY, DNSPOD_API_BASE_URL="https://dnsapi.cn/"):
self.DNSPOD_ID = DNSPOD_ID
self.DNSPOD_API_KEY = DNSPOD_API_KEY
self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL
self.HTTP_TIMEOUT = 65 # seconds
self.DNSPOD_LOGIN = "{0},{1}".format(self.DNSPOD_ID, self.DNSPOD_API_KEY)
if DNSPOD_API_BASE_URL[-1] != "/":
self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + "/"
else:
self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL
super(DNSPodDns, self).__init__()
def create_dns_record(self, domain_name, domain_dns_value):
self.logger.info("create_dns_record")
# if we have been given a wildcard name, strip wildcard
domain_name = domain_name.lstrip("*.")
subd = ""
if domain_name.count(".") != 1: # not top level domain
pos = domain_name.rfind(".", 0, domain_name.rfind("."))
subd = domain_name[:pos]
domain_name = domain_name[pos + 1 :]
if subd != "":
subd = "." + subd
url = urlparse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Create")
body = {
"record_type": "TXT",
"domain": domain_name,
"sub_domain": "_acme-challenge" + subd,
"value": domain_dns_value,
"record_line_id": "0",
"format": "json",
"login_token": self.DNSPOD_LOGIN,
}
create_dnspod_dns_record_response = requests.post(
url, data=body, timeout=self.HTTP_TIMEOUT
).json()
self.logger.debug(
"create_dnspod_dns_record_response. status_code={0}. response={1}".format(
create_dnspod_dns_record_response["status"]["code"],
create_dnspod_dns_record_response["status"]["message"],
)
)
if create_dnspod_dns_record_response["status"]["code"] != "1":
# raise error so that we do not continue to make calls to ACME
# server
raise ValueError(
"Error creating dnspod dns record: status_code={status_code} response={response}".format(
status_code=create_dnspod_dns_record_response["status"]["code"],
response=create_dnspod_dns_record_response["status"]["message"],
)
)
self.logger.info("create_dns_record_end")
def delete_dns_record(self, domain_name, domain_dns_value):
self.logger.info("delete_dns_record")
domain_name = domain_name.lstrip("*.")
subd = ""
if domain_name.count(".") != 1: # not top level domain
pos = domain_name.rfind(".", 0, domain_name.rfind("."))
subd = domain_name[:pos]
domain_name = domain_name[pos + 1 :]
if subd != "":
subd = "." + subd
url = urllib.parse.urljoin(self.DNSPOD_API_BASE_URL, "Record.List")
# pos = domain_name.rfind(".",0, domain_name.rfind("."))
subdomain = "_acme-challenge." + subd
rootdomain = domain_name
body = {
"login_token": self.DNSPOD_LOGIN,
"format": "json",
"domain": rootdomain,
"subdomain": subdomain,
"record_type": "TXT",
}
list_dns_response = requests.post(url, data=body, timeout=self.HTTP_TIMEOUT).json()
if list_dns_response["status"]["code"] != "1":
self.logger.error(
"list_dns_record_response. status_code={0}. message={1}".format(
list_dns_response["status"]["code"], list_dns_response["status"]["message"]
)
)
for i in range(0, len(list_dns_response["records"])):
rid = list_dns_response["records"][i]["id"]
urlr = urllib.parse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Remove")
bodyr = {
"login_token": self.DNSPOD_LOGIN,
"format": "json",
"domain": rootdomain,
"record_id": rid,
}
delete_dns_record_response = requests.post(
urlr, data=bodyr, timeout=self.HTTP_TIMEOUT
).json()
if delete_dns_record_response["status"]["code"] != "1":
self.logger.error(
"delete_dns_record_response. status_code={0}. message={1}".format(
delete_dns_record_response["status"]["code"],
delete_dns_record_response["status"]["message"],
)
)
self.logger.info("delete_dns_record_success")
try:
import urllib.parse as urlparse
except:
import urlparse
import requests
from . import common
class DuckDNSDns(common.BaseDns):
dns_provider_name = "duckdns"
def __init__(self, duckdns_token, DUCKDNS_API_BASE_URL="https://www.duckdns.org"):
self.duckdns_token = duckdns_token
self.HTTP_TIMEOUT = 65 # seconds
if DUCKDNS_API_BASE_URL[-1] != "/":
self.DUCKDNS_API_BASE_URL = DUCKDNS_API_BASE_URL + "/"
else:
self.DUCKDNS_API_BASE_URL = DUCKDNS_API_BASE_URL
super(DuckDNSDns, self).__init__()
def _common_dns_record(self, logger_info, domain_name, payload_end_arg):
self.logger.info("{0}".format(logger_info))
# if we have been given a wildcard name, strip wildcard
domain_name = domain_name.lstrip("*.")
# add provider domain to the domain name if not present
provider_domain = ".duckdns.org"
if domain_name.rfind(provider_domain) == -1:
"".join((domain_name, provider_domain))
url = urlparse.urljoin(self.DUCKDNS_API_BASE_URL, "update")
payload = dict([("domains", domain_name), ("token", self.duckdns_token), payload_end_arg])
update_duckdns_dns_record_response = requests.get(
url, params=payload, timeout=self.HTTP_TIMEOUT
)
normalized_response = update_duckdns_dns_record_response.text
self.logger.debug(
"update_duckdns_dns_record_response. status_code={0}. response={1}".format(
update_duckdns_dns_record_response.status_code, normalized_response
)
)
if update_duckdns_dns_record_response.status_code != 200 or normalized_response != "OK":
# raise error so that we do not continue to make calls to DuckDNS
# server
raise ValueError(
"Error creating DuckDNS dns record: status_code={status_code} response={response}".format(
status_code=update_duckdns_dns_record_response.status_code,
response=normalized_response,
)
)
self.logger.info("{0}_success".format(logger_info))
def create_dns_record(self, domain_name, domain_dns_value):
self._common_dns_record("create_dns_record", domain_name, ("txt", domain_dns_value))
def delete_dns_record(self, domain_name, domain_dns_value):
self._common_dns_record("delete_dns_record", domain_name, ("clear", "true"))
"""
Hurricane Electric DNS Support
"""
import json
try:
hedns_dependencies = True
import HurricaneDNS as _hurricanedns
except ImportError:
hedns_dependencies = False
from . import common
class _Response(object):
"""
wrapper aliyun resp to the format sewer wanted.
"""
def __init__(self, status_code=200, content=None, headers=None):
self.status_code = status_code
self.headers = headers or {}
self.content = content or {}
self.content = json.dumps(content)
super(_Response, self).__init__()
def json(self):
return json.loads(self.content)
class HurricaneDns(common.BaseDns):
def __init__(self, username, password):
super(HurricaneDns, self).__init__()
if not hedns_dependencies:
raise ImportError(
"""You need to install HurricaneDns dependencies. run: pip3 install sewer[hurricane]"""
)
self.clt = _hurricanedns.HurricaneDNS(username, password)
@staticmethod
def extract_zone(domain_name):
"""
extract domain to root, sub, acme_txt
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
:return tuple: root, zone, acme_txt
"""
# if we have been given a wildcard name, strip wildcard
domain_name = domain_name.lstrip("*.")
if domain_name.count(".") > 1:
zone, middle, last = str(domain_name).rsplit(".", 2)
root = ".".join([middle, last])
acme_txt = "_acme-challenge.%s" % zone
else:
zone = ""
root = domain_name
acme_txt = "_acme-challenge"
return root, zone, acme_txt
def create_dns_record(self, domain_name, domain_dns_value):
self.logger.info("create_dns_record start: %s", (domain_name, domain_dns_value))
root, _, acme_txt = self.extract_zone(domain_name)
self.clt.add_record(root, acme_txt, "TXT", domain_dns_value, ttl=300)
self.logger.info("create_dns_record end: %s", (domain_name, domain_dns_value))
def delete_dns_record(self, domain_name, domain_dns_value):
self.logger.info("delete_dns_record start: %s", (domain_name, domain_dns_value))
root, _, acme_txt = self.extract_zone(domain_name)
host = "%s.%s" % (acme_txt, root)
recored_list = self.clt.get_records(root, host, "TXT")
for i in recored_list:
self.clt.del_record(root, i["id"])
self.logger.info("delete_dns_record end: %s", (domain_name, domain_dns_value))
try:
import urllib.parse as urlparse
except:
import urlparse
import requests
from . import common
try:
rackspace_dependencies = True
import tldextract
except ImportError:
rackspace_dependencies = False
import time
class RackspaceDns(common.BaseDns):
"""
"""
dns_providername = "rackspace"
def get_rackspace_credentials(self):
self.logger.debug("get_rackspace_credentials")
RACKSPACE_IDENTITY_URL = "https://identity.api.rackspacecloud.com/v2.0/tokens"
payload = {
"auth": {
"RAX-KSKEY:apiKeyCredentials": {
"username": self.RACKSPACE_USERNAME,
"apiKey": self.RACKSPACE_API_KEY,
}
}
}
find_rackspace_api_details_response = requests.post(RACKSPACE_IDENTITY_URL, json=payload)
self.logger.debug(
"find_rackspace_api_details_response. status_code={0}".format(
find_rackspace_api_details_response.status_code
)
)
if find_rackspace_api_details_response.status_code != 200:
raise ValueError(
"Error getting token and URL details from rackspace identity server: status_code={status_code} response={response}".format(
status_code=find_rackspace_api_details_response.status_code,
response=self.log_response(find_rackspace_api_details_response),
)
)
data = find_rackspace_api_details_response.json()
api_token = data["access"]["token"]["id"]
url_data = next(
(item for item in data["access"]["serviceCatalog"] if item["type"] == "rax:dns"), None
)
if url_data is None:
raise ValueError(
"Error finding url data for the rackspace dns api in the response from the identity server"
)
else:
api_base_url = url_data["endpoints"][0]["publicURL"] + "/"
return (api_token, api_base_url)
def __init__(self, RACKSPACE_USERNAME, RACKSPACE_API_KEY):
if not rackspace_dependencies:
raise ImportError(
"""You need to install RackspaceDns dependencies. run; pip3 install sewer[rackspace]"""
)
self.RACKSPACE_DNS_ZONE_ID = None
self.RACKSPACE_USERNAME = RACKSPACE_USERNAME
self.RACKSPACE_API_KEY = RACKSPACE_API_KEY
self.HTTP_TIMEOUT = 65 # seconds
super(RackspaceDns, self).__init__()
self.RACKSPACE_API_TOKEN, self.RACKSPACE_API_BASE_URL = self.get_rackspace_credentials()
self.RACKSPACE_HEADERS = {
"X-Auth-Token": self.RACKSPACE_API_TOKEN,
"Content-Type": "application/json",
}
def get_dns_zone(self, domain_name):
self.logger.debug("get_dns_zone")
extracted_domain = tldextract.extract(domain_name)
self.RACKSPACE_DNS_ZONE = ".".join([extracted_domain.domain, extracted_domain.suffix])
def find_dns_zone_id(self, domain_name):
self.logger.debug("find_dns_zone_id")
self.get_dns_zone(domain_name)
url = self.RACKSPACE_API_BASE_URL + "domains"
find_dns_zone_id_response = requests.get(url, headers=self.RACKSPACE_HEADERS)
self.logger.debug(
"find_dns_zone_id_response. status_code={0}".format(
find_dns_zone_id_response.status_code
)
)
if find_dns_zone_id_response.status_code != 200:
raise ValueError(
"Error getting rackspace dns domain info: status_code={status_code} response={response}".format(
status_code=find_dns_zone_id_response.status_code,
response=self.log_response(find_dns_zone_id_response),
)
)
result = find_dns_zone_id_response.json()
domain_data = next(
(item for item in result["domains"] if item["name"] == self.RACKSPACE_DNS_ZONE), None
)
if domain_data is None:
raise ValueError(
"Error finding information for {dns_zone} in dns response data:\n{response_data})".format(
dns_zone=self.RACKSPACE_DNS_ZONE,
response_data=self.log_response(find_dns_zone_id_response),
)
)
dns_zone_id = domain_data["id"]
self.logger.debug("find_dns_zone_id_success")
return dns_zone_id
def find_dns_record_id(self, domain_name, domain_dns_value):
self.logger.debug("find_dns_record_id")
self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name)
url = self.RACKSPACE_API_BASE_URL + "domains/{0}/records".format(self.RACKSPACE_DNS_ZONE_ID)
find_dns_record_id_response = requests.get(url, headers=self.RACKSPACE_HEADERS)
self.logger.debug(
"find_dns_record_id_response. status_code={0}".format(
find_dns_record_id_response.status_code
)
)
self.logger.debug(url)
if find_dns_record_id_response.status_code != 200:
raise ValueError(
"Error finding dns records for {dns_zone}: status_code={status_code} response={response}".format(
dns_zone=self.RACKSPACE_DNS_ZONE,
status_code=find_dns_record_id_response.status_code,
response=self.log_response(find_dns_record_id_response),
)
)
records = find_dns_record_id_response.json()["records"]
RACKSPACE_RECORD_DATA = next(
(item for item in records if item["data"] == domain_dns_value), None
)
if RACKSPACE_RECORD_DATA is None:
raise ValueError(
"Couldn't find record with name {domain_name}\ncontaining data: {domain_dns_value}\nin the response data:{response_data}".format(
domain_name=domain_name,
domain_dns_value=domain_dns_value,
response_data=self.log_response(find_dns_record_id_response),
)
)
record_id = RACKSPACE_RECORD_DATA["id"]
self.logger.debug("find_dns_record_id success")
return record_id
def poll_callback_url(self, callback_url):
start_time = time.time()
while True:
callback_url_response = requests.get(callback_url, headers=self.RACKSPACE_HEADERS)
if time.time() > start_time + self.HTTP_TIMEOUT:
raise ValueError(
"Timed out polling callbackurl for dns record status. Last status_code={status_code} last response={response}".format(
status_code=callback_url_response.status_code,
response=self.log_response(callback_url_response),
)
)
if callback_url_response.status_code != 200:
raise Exception(
"Could not get dns record status from callback url. Status code ={status_code}. response={response}".format(
status_code=callback_url_response.status_code,
response=self.log_response(callback_url_response),
)
)
if callback_url_response.json()["status"] == "ERROR":
raise Exception(
"Error in creating/deleting dns record: status_Code={status_code}. response={response}".format(
status_code=callback_url_response.status_code,
response=self.log_response(callback_url_response),
)
)
if callback_url_response.json()["status"] == "COMPLETED":
break
def create_dns_record(self, domain_name, domain_dns_value):
self.logger.info("create_dns_record")
# strip wildcard if present
domain_name = domain_name.lstrip("*.")
self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name)
record_name = "_acme-challenge." + domain_name
url = urlparse.urljoin(
self.RACKSPACE_API_BASE_URL, "domains/{0}/records".format(self.RACKSPACE_DNS_ZONE_ID)
)
body = {
"records": [{"name": record_name, "type": "TXT", "data": domain_dns_value, "ttl": 3600}]
}
create_rackspace_dns_record_response = requests.post(
url, headers=self.RACKSPACE_HEADERS, json=body, timeout=self.HTTP_TIMEOUT
)
self.logger.debug(
"create_rackspace_dns_record_response. status_code={status_code}".format(
status_code=create_rackspace_dns_record_response.status_code
)
)
if create_rackspace_dns_record_response.status_code != 202:
raise ValueError(
"Error creating rackspace dns record: status_code={status_code} response={response}".format(
status_code=create_rackspace_dns_record_response.status_code,
response=create_rackspace_dns_record_response.text,
)
)
# response=self.log_response(create_rackspace_dns_record_response)))
# After posting the dns record we want created, the response gives us a url to check that will
# update when the job is done
callback_url = create_rackspace_dns_record_response.json()["callbackUrl"]
self.poll_callback_url(callback_url)
self.logger.info(
"create_dns_record_success. Name: {record_name} Data: {data}".format(
record_name=record_name, data=domain_dns_value
)
)
def delete_dns_record(self, domain_name, domain_dns_value):
self.logger.info("delete_dns_record")
record_name = "_acme-challenge." + domain_name
self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name)
self.RACKSPACE_RECORD_ID = self.find_dns_record_id(domain_name, domain_dns_value)
url = self.RACKSPACE_API_BASE_URL + "domains/{domain_id}/records/?id={record_id}".format(
domain_id=self.RACKSPACE_DNS_ZONE_ID, record_id=self.RACKSPACE_RECORD_ID
)
delete_dns_record_response = requests.delete(url, headers=self.RACKSPACE_HEADERS)
# After sending a delete request, if all goes well, we get a 202 from the server and a URL that we can poll
# to see when the job is done
self.logger.debug(
"delete_dns_record_response={0}".format(delete_dns_record_response.status_code)
)
if delete_dns_record_response.status_code != 202:
raise ValueError(
"Error deleting rackspace dns record: status_code={status_code} response={response}".format(
status_code=delete_dns_record_response.status_code,
response=self.log_response(delete_dns_record_response),
)
)
callback_url = delete_dns_record_response.json()["callbackUrl"]
self.poll_callback_url(callback_url)
self.logger.info(
"delete_dns_record_success. Name: {record_name} Data: {data}".format(
record_name=record_name, data=domain_dns_value
)
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment