diff --git a/lib/ansible/module_utils/acme.py b/lib/ansible/module_utils/acme.py index fcf91513bf..8ecc5b0b5a 100644 --- a/lib/ansible/module_utils/acme.py +++ b/lib/ansible/module_utils/acme.py @@ -81,29 +81,6 @@ 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 = {} - 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 - - def read_file(fn, mode='b'): try: with open(fn, 'r' + mode) as f: @@ -469,12 +446,12 @@ class ACMEDirectory(object): https://tools.ietf.org/html/draft-ietf-acme-acme-14#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: @@ -512,7 +489,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) # Grab account URI from module parameters. # Make sure empty string is treated as None. @@ -533,6 +509,8 @@ class ACMEAccount(object): # Make sure self.jws_header is updated self.set_account_uri(self.uri) + self.directory = ACMEDirectory(module, self) + def get_keyauthorization(self, token): ''' Returns the key authorization for the given token @@ -566,7 +544,7 @@ class ACMEAccount(object): else: return _sign_request_openssl(self._openssl_bin, self.module, payload64, protected64, key_data) - def send_signed_request(self, url, payload, key_data=None, jws_header=None): + def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True): ''' Sends a JWS signed HTTP POST request to the ACME server and returns the response as dictionary @@ -596,17 +574,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-14#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: @@ -614,6 +594,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 diff --git a/lib/ansible/modules/crypto/acme/acme_certificate.py b/lib/ansible/modules/crypto/acme/acme_certificate.py index 47a453b953..ff36082c10 100644 --- a/lib/ansible/modules/crypto/acme/acme_certificate.py +++ b/lib/ansible/modules/crypto/acme/acme_certificate.py @@ -330,7 +330,7 @@ account_uri: ''' from ansible.module_utils.acme import ( - ModuleFailException, fetch_url, write_file, nopad_b64, simple_get, pem_to_der, ACMEAccount, + ModuleFailException, write_file, nopad_b64, pem_to_der, ACMEAccount, HAS_CURRENT_CRYPTOGRAPHY, cryptography_get_csr_domains, cryptography_get_cert_days, set_crypto_backend, ) @@ -553,7 +553,7 @@ class ACMEClient(object): status = '' while status not in ['valid', 'invalid', 'revoked']: - result = simple_get(self.module, auth['uri']) + result, dummy = self.account.get_request(auth['uri']) result['uri'] = auth['uri'] if self._add_or_update_auth(domain, result): self.changed = True @@ -590,7 +590,7 @@ class ACMEClient(object): status = result['status'] while status not in ['valid', 'invalid']: time.sleep(2) - result = simple_get(self.module, order) + result, dummy = self.account.get_request(order) status = result['status'] if status != 'valid': @@ -611,11 +611,7 @@ class ACMEClient(object): Download and parse the certificate chain. https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.4.2 ''' - resp, info = fetch_url(self.module, url, headers={'Accept': 'application/pem-certificate-chain'}) - try: - content = resp.read() - except AttributeError: - content = info.get('body') + content, info = self.account.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'}) if not content or not info['content-type'].startswith('application/pem-certificate-chain'): raise ModuleFailException("Cannot download certificate chain from {0}: {1} (headers: {2})".format(url, content, info)) @@ -642,9 +638,9 @@ class ACMEClient(object): parsed_link = re.match(r'<(.+)>;rel="(\w+)"', link) if parsed_link and parsed_link.group(2) == "up": chain_link = parsed_link.group(1) - chain_result, chain_info = fetch_url(self.module, chain_link, method='GET') + chain_result, chain_info = self.account.get_request(chain_link, parse_json_result=False) if chain_info['status'] in [200, 201]: - chain.append(self._der_to_pem(chain_result.read())) + chain.append(self._der_to_pem(chain_result)) if cert is None or current: raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info)) @@ -669,9 +665,9 @@ class ACMEClient(object): parsed_link = re.match(r'<(.+)>;rel="(\w+)"', link) if parsed_link and parsed_link.group(2) == "up": chain_link = parsed_link.group(1) - chain_result, chain_info = fetch_url(self.module, chain_link, method='GET') + chain_result, chain_info = self.account.get_request(chain_link, parse_json_result=False) if chain_info['status'] in [200, 201]: - chain = [self._der_to_pem(chain_result.read())] + chain = [self._der_to_pem(chain_result)] if info['status'] not in [200, 201]: raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result)) @@ -698,7 +694,7 @@ class ACMEClient(object): raise ModuleFailException("Error new order: CODE: {0} RESULT: {1}".format(info['status'], result)) for auth_uri in result['authorizations']: - auth_data = simple_get(self.module, auth_uri) + auth_data, dummy = self.account.get_request(auth_uri) auth_data['uri'] = auth_uri domain = auth_data['identifier']['value'] if auth_data.get('wildcard', False): @@ -774,11 +770,7 @@ class ACMEClient(object): else: # For ACME v2, we obtain the order object by fetching the # order URI, and extract the information from there. - resp, info = fetch_url(self.module, self.order_uri) - try: - result = resp.read() - except AttributeError: - result = info.get('body') + result, info = self.account.get_request(self.order_uri) if not result: raise ModuleFailException("Cannot download order from {0}: {1} (headers: {2})".format(self.order_uri, result, info)) @@ -786,9 +778,8 @@ class ACMEClient(object): if info['status'] not in [200]: raise ModuleFailException("Error on downloading order: CODE: {0} RESULT: {1}".format(info['status'], result)) - result = self.module.from_json(result.decode('utf8')) for auth_uri in result['authorizations']: - auth_data = simple_get(self.module, auth_uri) + auth_data, dummy = self.account.get_request(auth_uri) auth_data['uri'] = auth_uri domain = auth_data['identifier']['value'] if auth_data.get('wildcard', False):