diff --git a/changelogs/fragments/44988-acme-post-as-get.yaml b/changelogs/fragments/44988-acme-post-as-get.yaml new file mode 100644 index 00000000000..f2968fd6dfe --- /dev/null +++ b/changelogs/fragments/44988-acme-post-as-get.yaml @@ -0,0 +1,2 @@ +bugfixes: +- "ACME modules support `POST-as-GET `__ and will be able to access Let's Encrypt ACME v2 endpoint after November 1st, 2019." diff --git a/lib/ansible/module_utils/acme.py b/lib/ansible/module_utils/acme.py index 3f48bfe50ff..cb519322b44 100644 --- a/lib/ansible/module_utils/acme.py +++ b/lib/ansible/module_utils/acme.py @@ -140,7 +140,7 @@ class ACMEDirectory(object): self.directory_root = module.params['acme_directory'] self.version = module.params['acme_version'] - self.directory, dummy = account.get_request(self.directory_root) + self.directory, dummy = account.get_request(self.directory_root, get_only=True) # Check whether self.version matches what we expect if self.version == 1: @@ -310,7 +310,10 @@ class ACMEAccount(object): def sign_request(self, protected, payload, key_data, key): try: - payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8')) + if payload is None: + payload64 = '' + else: + payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8')) protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8')) except Exception as e: raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e)) @@ -346,6 +349,9 @@ class ACMEAccount(object): Sends a JWS signed HTTP POST request to the ACME server and returns the response as dictionary https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.2 + + If payload is None, a POST-as-GET is performed. + (https://tools.ietf.org/html/draft-ietf-acme-acme-15#section-6.3) ''' failed_tries = 0 while True: @@ -389,14 +395,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) + def get_request(self, uri, parse_json_result=True, headers=None, get_only=False): + ''' + Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback + to GET if server replies with a status code of 405. + ''' + if not get_only and self.version != 1: + # Try POST-as-GET + content, info = self.send_signed_request(uri, None, parse_json_result=False) + if info['status'] == 405: + # Instead, do unauthenticated GET + get_only = True + else: + # Do unauthenticated GET + get_only = True - try: - content = resp.read() - except AttributeError: - content = info.get('body') + if get_only: + # Perform unauthenticated GET + resp, info = fetch_url(self.module, uri, method='GET', headers=headers) + try: + content = resp.read() + except AttributeError: + content = info.get('body') + + # Process result if parse_json_result: result = {} if content: @@ -477,10 +500,19 @@ class ACMEAccount(object): ''' if self.uri is None: raise ModuleFailException("Account URI unknown") - data = {} if self.version == 1: + data = {} data['resource'] = 'reg' - result, info = self.send_signed_request(self.uri, data) + result, info = self.send_signed_request(self.uri, data) + else: + # try POST-as-GET first (draft-15 or newer) + data = None + result, info = self.send_signed_request(self.uri, data) + # check whether that failed with a malformed request error + if info['status'] >= 400 and result.get('type') == 'urn:ietf:params:acme:error:malformed': + # retry as a regular POST (with no changed data) for pre-draft-15 ACME servers + data = {} + result, info = self.send_signed_request(self.uri, data) if info['status'] == 403 and result.get('type') == 'urn:ietf:params:acme:error:unauthorized': return None if info['status'] < 200 or info['status'] >= 300: