feat: add python version of huaweicloud dns api

This commit is contained in:
jimcheung 2020-02-08 00:02:32 +08:00
parent c96b4bc5c0
commit 379ba69430
4 changed files with 288 additions and 38 deletions

View File

@ -7,10 +7,10 @@
不管是申请还是续期,只要是通配符证书,只能采用 dns-01 的方式校验申请者的域名,也就是说 certbot 操作者必须手动添加 DNS TXT 记录。
如果你编写一个 Cron (比如 1 1 */1 * * root certbot-auto renew),自动 renew 通配符证书,此时 Cron 无法自动添加 TXT 记录,这样 renew 操作就会失败,如何解决?
certbot 提供了一个 hook可以编写一个 Shell 脚本,让脚本调用 DNS 服务商的 API 接口,动态添加 TXT 记录,这样就无需人工干预了。
在 certbot 官方提供的插件和 hook 例子中,都没有针对国内 DNS 服务器的样例,所以我编写了这样一个工具,目前支持阿里云 DNS、腾讯云 DNS、GoDaddycertbot 官方没有对应的插件)。
在 certbot 官方提供的插件和 hook 例子中,都没有针对国内 DNS 服务器的样例,所以我编写了这样一个工具,目前支持阿里云 DNS、腾讯云 DNS、GoDaddycertbot 官方没有对应的插件)。
### 自动申请通配符证书
@ -21,7 +21,7 @@ $ git clone https://github.com/ywdblog/certbot-letencrypt-wildcardcertificates-a
$ cd certbot-letencrypt-wildcardcertificates-alydns-au
$ chmod 0777 au.sh
$ chmod 0777 au.sh
```
2配置
@ -32,10 +32,11 @@ $ chmod 0777 au.sh
2DNS API 密钥:
这个 API 密钥什么意思呢?由于需要通过 API 操作阿里云 DNS 腾讯云 DNS 的记录,所以需要去域名服务商哪儿获取 API 密钥,然后配置在 au.sh 文件中:
这个 API 密钥什么意思呢?由于需要通过 API 操作阿里云 DNS, 腾讯云 DNS 的记录,所以需要去域名服务商哪儿获取 API 密钥,然后配置在 au.sh 文件中:
- ALY_KEY 和 ALY_TOKEN阿里云 [API key 和 Secrec 官方申请文档](https://help.aliyun.com/knowledge_detail/38738.html)。
- TXY_KEY 和 TXY_TOKEN腾讯云 [API 密钥官方申请文档](https://console.cloud.tencent.com/cam/capi)。
- HWY_KEY 和 HWY_TOKEN: 华为云 [API 密钥官方申请文档](https://support.huaweicloud.com/devg-apisign/api-sign-provide.html)
- GODADDY_KEY 和 GODADDY_TOKENGoDaddy [API 密钥官方申请文档](https://developer.godaddy.com/getstarted)。
3选择运行环境
@ -49,16 +50,17 @@ $ chmod 0777 au.sh
- Python(支持2.7和3.7,无需任何第三方库)
- au.sh python aly add/cleanPython操作阿里云DNS增加/清空DNS。
- au.sh python txy add/cleanPython操作腾讯云DNS增加/清空DNS。
- au.sh python godaddy add/cleanPython操作GoDaddy DNS增加/清空DNS。
- au.sh python hwy add/cleanPython操作华为云DNS增加/清空DNS。
- au.sh python godaddy add/cleanPython操作GoDaddy DNS增加/清空DNS。
根据自己服务器环境和域名服务商选择任意一个 hook shell包含相应参数具体使用见下面。
3申请证书
测试是否有错误:
```
$ ./certbot-auto certonly -d *.example.com --manual --preferred-challenges dns --dry-run --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
$ ./certbot-auto certonly -d *.example.com --manual --preferred-challenges dns --dry-run --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
```
**Debug** 操作 DNS API 可能会遇到一系列问题,比如 API token 权限不足,遇到相关问题,可以查看 /var/log/certd.log。
@ -70,12 +72,12 @@ $ ./certbot-auto certonly -d *.example.com --manual --preferred-challenges dns
- 第三个参数是固定的(--manual-auth-hook中用add--manual-clean-hook中用clean)
比如你要选择Python环境可以将 --manual-auth-hook 输入修改为 "/脚本目录/au.sh python aly add"--manual-cleanup-hook 输入修改为 "/脚本目录/au.sh python aly clean"
确认无误后,实际运行(去除 --dry-run 参数):
```
```
# 实际申请
$ ./certbot-auto certonly -d *.example.com --manual --preferred-challenges dns --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
$ ./certbot-auto certonly -d *.example.com --manual --preferred-challenges dns --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
```
参数解释(可以不用关心):
@ -91,7 +93,7 @@ $ ./certbot-auto certonly -d *.example.com --manual --preferred-challenges dns
如果你想为多个域名申请通配符证书(合并在一张证书中,也叫做 **SAN 通配符证书**),直接输入多个 -d 参数即可,比如:
```
$ ./certbot-auto certonly -d *.example.com -d *.example.org -d www.example.cn --manual --preferred-challenges dns --dry-run --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
$ ./certbot-auto certonly -d *.example.com -d *.example.org -d www.example.cn --manual --preferred-challenges dns --dry-run --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
```
### 续期证书
@ -99,7 +101,7 @@ $ ./certbot-auto certonly -d *.example.com -d *.example.org -d www.example.cn
1对机器上所有证书 renew
```
$ ./certbot-auto renew --manual --preferred-challenges dns --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
$ ./certbot-auto renew --manual --preferred-challenges dns --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
```
2对某一张证书进行续期
@ -117,23 +119,23 @@ $ ./certbot-auto certificates
记住证书名,比如 simplehttps.com然后运行下列命令 renew
```
$ ./certbot-auto renew --cert-name simplehttps.com --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
$ ./certbot-auto renew --cert-name simplehttps.com --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
```
### 加入 crontab
### 加入 crontab
编辑文件 /etc/crontab :
```
#证书有效期<30天才会renew所以crontab可以配置为1天或1周
1 1 */1 * * root certbot-auto renew --manual --preferred-challenges dns --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
1 1 */1 * * root certbot-auto renew --manual --preferred-challenges dns --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
```
如果是certbot 机器和运行web服务比如 nginxapache的机器是同一台那么成功renew证书后可以启动对应的web 服务器运行下列crontab :
```
# 注意只有成功renew证书才会重新启动nginx
1 1 */1 * * root certbot-auto renew --manual --preferred-challenges dns --deploy-hook "service nginx restart" --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
1 1 */1 * * root certbot-auto renew --manual --preferred-challenges dns --deploy-hook "service nginx restart" --manual-auth-hook "/脚本目录/au.sh php aly add" --manual-cleanup-hook "/脚本目录/au.sh php aly clean"
```
@ -142,18 +144,19 @@ $ ./certbot-auto renew --cert-name simplehttps.com --manual-auth-hook "/脚本
### 贡献
- 阿里云 python 版 @Duke-Wu
- 腾讯云 python 版 @akgnah
- 腾讯云 python 版 @akgnah
- 华为云 python 版 @jinhucheung
- GoDaddy PHP 版 wlx_1990 2019-01-11
### 其他
- 可以关注公众号虞大胆的叽叽喳喳yudadanwx了解更多密码学&HTTPS协议知识。
- 我写了一本书[《深入浅出HTTPS从原理到实战》](https://mp.weixin.qq.com/s/80oQhzmP9BTimoReo1oMeQ)了解更多关于HTTPS方面的知识。**如果你觉得本书还可以,希望能在豆瓣做个点评,以便让更多人了解,非常感谢。豆瓣评论地址:[https://book.douban.com/subject/30250772/](https://book.douban.com/subject/30250772/)**
公众号二维码:
![公众号虞大胆的叽叽喳喳yudadanwx](https://notes.newyingyong.cn/static/image/wxgzh/qrcode_258.jpg)
《深入浅出HTTPS从原理到实战》二维码
![深入浅出HTTPS从原理到实战](https://notes.newyingyong.cn/static/image/httpsbook/httpsbook-small-jd.jpg)
![深入浅出HTTPS从原理到实战](https://notes.newyingyong.cn/static/image/httpsbook/httpsbook-small-jd.jpg)

47
au.sh
View File

@ -4,9 +4,9 @@
###### 根据自己的情况修改 Begin ##############
#PHP 命令行路径,如果有需要可以修改
#PHP 命令行路径,如果有需要可以修改
phpcmd="/usr/bin/php"
#Python 命令行路径,如果有需要可以修改
#Python 命令行路径,如果有需要可以修改
pythoncmd="/usr/bin/python"
#填写阿里云的AccessKey ID及AccessKey Secret
@ -19,6 +19,11 @@ ALY_TOKEN=""
TXY_KEY=""
TXY_TOKEN=""
#填写华为云的 Access Key Id 及 Secret Access Key
#如何申请见https://support.huaweicloud.com/devg-apisign/api-sign-provide.html
HWY_KEY=""
HWY_TOKEN=""
#GoDaddy的SecretId及SecretKey
#如何申请见https://developer.godaddy.com/getstarted
GODADDY_KEY=""
@ -32,8 +37,8 @@ PATH=$(cd `dirname $0`; pwd)
# 第一个参数:使用什么语言环境
# 第二个参数:使用那个 DNS 的 API
# 第三个参数add or clean
plang=$1 #python or php
pdns=$2 #aly or txy
plang=$1 #python or php
pdns=$2 #aly, txy, hwy, godaddy
paction=$3 #add or clean
#内部变量
@ -45,43 +50,53 @@ if [[ "$paction" != "clean" ]]; then
paction="add"
fi
case $plang in
"php")
case $plang in
"php")
cmd=$phpcmd
if [[ "$pdns" == "aly" ]]; then
dnsapi=$PATH"/php-version/alydns.php"
key=$ALY_KEY
if [[ "$pdns" == "aly" ]]; then
dnsapi=$PATH"/php-version/alydns.php"
key=$ALY_KEY
token=$ALY_TOKEN
elif [[ "$pdns" == "txy" ]] ;then
elif [[ "$pdns" == "txy" ]]; then
dnsapi="$PATH/php-version/txydns.php"
key=$TXY_KEY
token=$TXY_TOKEN
elif [[ "$pdns" == "hwy" ]]; then
# TODO
dnsapi=""
key=$HWY_KEY
token=$HWY_TOKEN
exit
else
dnsapi="$PATH/php-version/godaddydns.php"
key=$GODADDY_KEY
token=$GODADDY_TOKEN
fi
;;
"python")
cmd=$pythoncmd
if [[ "$pdns" == "aly" ]]; then
if [[ "$pdns" == "aly" ]]; then
dnsapi=$PATH"/python-version/alydns.py"
key=$ALY_KEY
token=$ALY_TOKEN
elif [[ "$pdns" == "txy" ]] ;then
elif [[ "$pdns" == "txy" ]]; then
dnsapi=$PATH"/python-version/txydns.py"
key=$TXY_KEY
token=$TXY_TOKEN
elif [[ "$pdns" == "hwy" ]]; then
dnsapi="$PATH/python-version/hwydns.py"
key=$HWY_KEY
token=$HWY_TOKEN
else
dnsapi=$PATH"/python-version/godaddydns.py"
key=$GODADDY_KEY
token=$GODADDY_TOKEN
exit
fi
;;
fi
;;
esac
$cmd $dnsapi $paction $CERTBOT_DOMAIN "_acme-challenge" $CERTBOT_VALIDATION $key $token >>"/var/log/certd.log"

View File

@ -8,7 +8,7 @@ import random
import string
import json
import sys
import os
import os
pv = "python2"
#python2
@ -32,8 +32,8 @@ class AliDns:
@staticmethod
def getDomain(domain):
domain_parts = domain.split('.')
if len(domain_parts) > 2:
dirpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
domainfile = dirpath + "/domain.ini"

232
python-version/hwydns.py Executable file
View File

@ -0,0 +1,232 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import time
import urllib
import hashlib
import hmac
import binascii
import json
if sys.version_info < (3, 0):
import urllib2
import urllib
import urlparse
else:
import urllib.request as urllib2
import urllib.parse as urllib
class HwyDns:
__endpoint = 'dns.myhuaweicloud.com'
def __init__(self, access_key_id, secret_access_key):
self.access_key_id = access_key_id
self.secret_access_key = secret_access_key
@staticmethod
def getDomain(domain):
domain_parts = domain.split('.')
if len(domain_parts) > 2:
dirpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
domainfile = dirpath + '/domain.ini'
domainarr = []
with open(domainfile) as f:
for line in f:
val = line.strip()
domainarr.append(val)
index = -3 if '.'.join(domain_parts[-2:]).lower() in domainarr else -2
return ('.'.join(domain_parts[:index]), '.'.join(domain_parts[index:]))
return ('', domain)
# @example hwydns.add_domain_record("example.com", "_acme-challenge", "123456", "TXT")
def add_domain_record(self, domain, rr, value, _type = 'TXT'):
zone_id = self.get_domain_zone_id(domain)
if not zone_id:
return
self.__request('POST', '/v2/zones/%s/recordsets' % (zone_id), {
'name' : '%s.%s.' % (rr, domain),
'type' : _type,
'records' : [ "\"%s\"" % (value) ]
})
# @example hwydns.delete_domain_record("example.com", "_acme-challenge", "TXT")
def delete_domain_record(self, domain, rr, _type = 'TXT'):
zone_id = self.get_domain_zone_id(domain)
recordset_id = self.get_domain_recordset_id(domain, rr, _type)
if not (zone_id and recordset_id):
return
self.__request('DELETE', '/v2/zones/%s/recordsets/%s' % (zone_id, recordset_id))
# @example hwydns.get_domain_record("example.com", "_acme-challenge", "TXT")
def get_domain_record(self, domain, rr, _type = 'TXT'):
try:
full_domain = '.'.join([rr, domain])
response = self.__request('GET', '/v2/recordsets?type=%s&name=%s' % (_type, full_domain))
content = json.loads(response)
return list(filter(lambda record: record['name'][:-1] == full_domain and record['type'] == _type, content['recordsets']))[0]
except Exception as e:
print('hwydns#get_domain_record raise: ' + str(e))
return None
# @example hwydns.get_domain("example.com")
def get_domain(self, domain):
try:
response = self.__request('GET', '/v2/zones?type=public&name=%s' % (domain))
content = json.loads(response)
return list(filter(lambda item: item['name'][:-1] == domain, content['zones']))[0]
except Exception as e:
print('hwydns#get_domain raise: ' + str(e))
return None
def get_domain_recordset_id(self, domain, rr, _type = 'TXT'):
try:
record = self.get_domain_record(domain, rr, _type)
return record['id'] if record else None
except Exception as e:
print('hwydns#get_domain_recordset_id raise: ' + str(e))
return None
def get_domain_zone_id(self, domain):
try:
record = self.get_domain(domain)
return record['id'] if record else None
except Exception as e:
print('hwydns#get_domain_zone_id raise: ' + str(e))
return None
def __request(self, method, path, payload={}):
url = 'https://%s%s?%s' % (self.__endpoint, self.__parse_path(path)[:-1], self.__parse_query_string(path))
data = json.dumps(payload).encode('utf8')
sdk_date = self.__build_sdk_date()
print('Request URL: ' + url)
print('Request Data: ' + str(data))
request = urllib2.Request(url=url, data=data)
request.get_method = lambda: method
request.add_header('Content-Type', 'application/json')
request.add_header('Host', self.__endpoint)
request.add_header('X-sdk-date', sdk_date)
request.add_header('Authorization', self.__build_authorization(request))
print('Request headers: ' + str(request.headers))
try:
f = urllib2.urlopen(request, timeout=45)
response = f.read().decode('utf-8')
print(response)
return response
except urllib2.HTTPError as e:
print('hwydns#__request raise urllib2.HTTPError: ' + str(e))
raise SystemExit(e)
def __build_sdk_date(self):
return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
def __build_authorization(self, request):
algorithm = 'SDK-HMAC-SHA256'
canonical_request = self.__build_canonical_request(request)
canonical_request_hexencode = self.__hexencode_sha256_hash(canonical_request.encode('utf-8'))
string2sign = "%s\n%s\n%s" % (algorithm, request.get_header('X-sdk-date'), canonical_request_hexencode)
sign = self.__build_sign(string2sign)
return "%s Access=%s, SignedHeaders=%s, Signature=%s" % (algorithm, self.access_key_id, self.__parse_header_keys(request.headers), sign)
def __build_canonical_request(self, request):
return "%(method)s\n%(path)s\n%(query_string)s\n%(headers)s\n%(header_keys)s\n%(data_hexencode)s" % {
'method': request.get_method().upper(),
'path': self.__parse_path(request.get_full_url()),
'query_string': self.__parse_query_string(request.get_full_url()),
'headers': self.__parse_headers(request.headers),
'header_keys': self.__parse_header_keys(request.headers),
'data_hexencode': self.__hexencode_sha256_hash(request.data)
}
def __parse_path(self, url):
if sys.version_info < (3,0):
path = urlparse.urlsplit(url).path
else:
path = urllib.urlsplit(url).path
path = path if path else '/'
pattens = urllib.unquote(path).split('/')
tmp_paths = []
for v in pattens:
tmp_paths.append(self.__urlencode(v))
urlpath = '/'.join(tmp_paths)
if urlpath[-1] != '/':
urlpath = urlpath + '/'
return urlpath
def __parse_query_string(self, url):
if sys.version_info < (3,0):
query = urlparse.parse_qs(urlparse.urlsplit(url).query)
else:
query = urllib.parse_qs(urllib.urlsplit(url).query)
sorted_query = sorted(query.items(), key=lambda item: item[0])
sorted_query_string = ''
for (k, v) in sorted_query:
if type(v) is list:
v.sort()
for item in v:
sorted_query_string += '&' + self.__urlencode(k) + '=' + self.__urlencode(item)
else:
sorted_query_string += '&' + self.__urlencode(k) + '=' + self.__urlencode(v)
return sorted_query_string[1:]
def __parse_headers(self, headers):
format_headers = dict(((k.lower(), v.strip())) for (k, v) in headers.items())
header_string = ''
for (k, v) in sorted(format_headers.items(), key=lambda item: item[0]):
header_string += "%s:%s\n" % (k, v)
return header_string
def __parse_header_keys(self, headers):
return ';'.join(sorted(map(lambda key: key.lower(), headers.keys())))
def __build_sign(self, string2sign):
if sys.version_info < (3,0):
hm = hmac.new(self.secret_access_key, string2sign, digestmod=hashlib.sha256).digest()
else:
hm = hmac.new(self.secret_access_key.encode('utf-8'), string2sign.encode('utf-8'), digestmod=hashlib.sha256).digest()
return binascii.hexlify(hm).decode()
def __urlencode(self, string):
return urllib.quote(str(string), safe='~')
def __hexencode_sha256_hash(self, data):
sha256 = hashlib.sha256()
sha256.update(data)
return sha256.hexdigest()
if __name__ == '__main__':
print('开始调用华为云 DNS API')
print('-'.join(sys.argv))
_, action, certbot_domain, acme_challenge, certbot_validation, api_key, api_secret = sys.argv
subdomain, main_domain = HwyDns.getDomain(certbot_domain)
if subdomain:
subdomain = acme_challenge + '.' + subdomain
else:
subdomain = acme_challenge
hwydns = HwyDns(api_key, api_secret)
if 'add' == action:
hwydns.add_domain_record(main_domain, subdomain, certbot_validation)
elif 'clean' == action:
hwydns.delete_domain_record(main_domain, subdomain)
print('结束调用华为云 DNS API')