|
|
|
@ -60,27 +60,12 @@ def nopad_b64(data):
|
|
|
|
|
return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def simple_get(module, url):
|
|
|
|
|
resp, info = fetch_url(module, url, method='GET')
|
|
|
|
|
|
|
|
|
|
result = {}
|
|
|
|
|
def read_file(fn, mode='b'):
|
|
|
|
|
try:
|
|
|
|
|
content = resp.read()
|
|
|
|
|
except AttributeError:
|
|
|
|
|
content = info.get('body')
|
|
|
|
|
|
|
|
|
|
if content:
|
|
|
|
|
if info['content-type'].startswith('application/json'):
|
|
|
|
|
try:
|
|
|
|
|
result = module.from_json(content.decode('utf8'))
|
|
|
|
|
except ValueError:
|
|
|
|
|
raise ModuleFailException("Failed to parse the ACME response: {0} {1}".format(url, content))
|
|
|
|
|
else:
|
|
|
|
|
result = content
|
|
|
|
|
|
|
|
|
|
if info['status'] >= 400:
|
|
|
|
|
raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], result))
|
|
|
|
|
return result
|
|
|
|
|
with open(fn, 'r' + mode) as f:
|
|
|
|
|
return f.read()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# function source: network/basics/uri.py
|
|
|
|
@ -150,12 +135,12 @@ class ACMEDirectory(object):
|
|
|
|
|
https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-7.1.1
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
def __init__(self, module):
|
|
|
|
|
def __init__(self, module, account):
|
|
|
|
|
self.module = module
|
|
|
|
|
self.directory_root = module.params['acme_directory']
|
|
|
|
|
self.version = module.params['acme_version']
|
|
|
|
|
|
|
|
|
|
self.directory = simple_get(self.module, self.directory_root)
|
|
|
|
|
self.directory, dummy = account.get_request(self.directory_root)
|
|
|
|
|
|
|
|
|
|
# Check whether self.version matches what we expect
|
|
|
|
|
if self.version == 1:
|
|
|
|
@ -193,7 +178,6 @@ class ACMEAccount(object):
|
|
|
|
|
# account_key path and content are mutually exclusive
|
|
|
|
|
self.key = module.params['account_key_src']
|
|
|
|
|
self.key_content = module.params['account_key_content']
|
|
|
|
|
self.directory = ACMEDirectory(module)
|
|
|
|
|
|
|
|
|
|
self.uri = None
|
|
|
|
|
|
|
|
|
@ -224,6 +208,8 @@ class ACMEAccount(object):
|
|
|
|
|
"jwk": self.jwk,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.directory = ACMEDirectory(module, self)
|
|
|
|
|
|
|
|
|
|
def get_keyauthorization(self, token):
|
|
|
|
|
'''
|
|
|
|
|
Returns the key authorization for the given token
|
|
|
|
@ -355,7 +341,7 @@ class ACMEAccount(object):
|
|
|
|
|
"signature": nopad_b64(to_bytes(out)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def send_signed_request(self, url, payload):
|
|
|
|
|
def send_signed_request(self, url, payload, parse_json_result=True):
|
|
|
|
|
'''
|
|
|
|
|
Sends a JWS signed HTTP POST request to the ACME server and returns
|
|
|
|
|
the response as dictionary
|
|
|
|
@ -383,17 +369,19 @@ class ACMEAccount(object):
|
|
|
|
|
except AttributeError:
|
|
|
|
|
content = info.get('body')
|
|
|
|
|
|
|
|
|
|
if content:
|
|
|
|
|
if info['content-type'].startswith('application/json') or 400 <= info['status'] < 600:
|
|
|
|
|
if content or not parse_json_result:
|
|
|
|
|
if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600:
|
|
|
|
|
try:
|
|
|
|
|
result = self.module.from_json(content.decode('utf8'))
|
|
|
|
|
decoded_result = self.module.from_json(content.decode('utf8'))
|
|
|
|
|
# In case of badNonce error, try again (up to 5 times)
|
|
|
|
|
# (https://tools.ietf.org/html/draft-ietf-acme-acme-09#section-6.6)
|
|
|
|
|
if (400 <= info['status'] < 600 and
|
|
|
|
|
result.get('type') == 'urn:ietf:params:acme:error:badNonce' and
|
|
|
|
|
decoded_result.get('type') == 'urn:ietf:params:acme:error:badNonce' and
|
|
|
|
|
failed_tries <= 5):
|
|
|
|
|
failed_tries += 1
|
|
|
|
|
continue
|
|
|
|
|
if parse_json_result:
|
|
|
|
|
result = decoded_result
|
|
|
|
|
except ValueError:
|
|
|
|
|
raise ModuleFailException("Failed to parse the ACME response: {0} {1}".format(url, content))
|
|
|
|
|
else:
|
|
|
|
@ -401,6 +389,31 @@ class ACMEAccount(object):
|
|
|
|
|
|
|
|
|
|
return result, info
|
|
|
|
|
|
|
|
|
|
def get_request(self, uri, parse_json_result=True, headers=None):
|
|
|
|
|
resp, info = fetch_url(self.module, uri, method='GET', headers=headers)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
content = resp.read()
|
|
|
|
|
except AttributeError:
|
|
|
|
|
content = info.get('body')
|
|
|
|
|
|
|
|
|
|
if parse_json_result:
|
|
|
|
|
result = {}
|
|
|
|
|
if content:
|
|
|
|
|
if info['content-type'].startswith('application/json'):
|
|
|
|
|
try:
|
|
|
|
|
result = self.module.from_json(content.decode('utf8'))
|
|
|
|
|
except ValueError:
|
|
|
|
|
raise ModuleFailException("Failed to parse the ACME response: {0} {1}".format(uri, content))
|
|
|
|
|
else:
|
|
|
|
|
result = content
|
|
|
|
|
else:
|
|
|
|
|
result = content
|
|
|
|
|
|
|
|
|
|
if info['status'] >= 400:
|
|
|
|
|
raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], result))
|
|
|
|
|
return result, info
|
|
|
|
|
|
|
|
|
|
def set_account_uri(self, uri):
|
|
|
|
|
'''
|
|
|
|
|
Set account URI. For ACME v2, it needs to be used to sending signed
|
|
|
|
|