def _check_response(cls, response, content_type=None): """Check response content and its type. .. note:: Checking is not strict: wrong server response ``Content-Type`` HTTP header is ignored if response is an expected JSON object (c.f. Boulder #56). :param str content_type: Expected Content-Type response header. If JSON is expected and not present in server response, this function will raise an error. Otherwise, wrong Content-Type is ignored, but logged. :raises letsencrypt.messages2.Error: If server response body carries HTTP Problem (draft-ietf-appsawg-http-problem-00). :raises letsencrypt.errors.NetworkError: In case of other networking errors. """ response_ct = response.headers.get('Content-Type') try: # TODO: response.json() is called twice, once here, and # once in _get and _post clients jobj = response.json() except ValueError as error: jobj = None if not response.ok: if jobj is not None: if response_ct != cls.JSON_ERROR_CONTENT_TYPE: logging.debug( 'Ignoring wrong Content-Type (%r) for JSON Error', response_ct) try: logging.error("Error: %s", jobj) logging.error("Response from server: %s", response.content) raise messages2.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object raise errors.NetworkError((response, error)) else: # response is not JSON object raise errors.NetworkError(response) else: if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: logging.debug( 'Ignoring wrong Content-Type (%r) for JSON decodable ' 'response', response_ct) if content_type == cls.JSON_CONTENT_TYPE and jobj is None: raise errors.NetworkError( 'Unexpected response Content-Type: {0}'.format(response_ct))
def answer_challenge(self, challb, response): """Answer challenge. :param challb: Challenge Resource body. :type challb: `.ChallengeBody` :param response: Corresponding Challenge response :type response: `.challenges.ChallengeResponse` :returns: Challenge Resource with updated body. :rtype: `.ChallengeResource` :raises errors.UnexpectedUpdate: """ response = self._post(challb.uri, self._wrap_in_jws(response)) try: authzr_uri = response.links['up']['url'] except KeyError: raise errors.NetworkError('"up" Link header missing') challr = messages2.ChallengeResource( authzr_uri=authzr_uri, body=messages2.ChallengeBody.from_json(response.json())) # TODO: check that challr.uri == response.headers['Location']? if challr.uri != challb.uri: raise errors.UnexpectedUpdate(challr.uri) return challr
def revoke(self, certr, when=messages2.Revocation.NOW): """Revoke certificate. :param when: When should the revocation take place? Takes the same values as `.messages2.Revocation.revoke`. """ rev = messages2.Revocation(revoke=when, authorizations=tuple( authzr.uri for authzr in certr.authzrs)) response = self._post(certr.uri, self._wrap_in_jws(rev)) if response.status_code != httplib.OK: raise errors.NetworkError( 'Successful revocation must return HTTP OK status')
def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): """Send GET request. :raises letsencrypt.client.errors.NetworkError: :returns: HTTP Response :rtype: `requests.Response` """ try: response = requests.get(uri, **kwargs) except requests.exceptions.RequestException as error: raise errors.NetworkError(error) self._check_response(response, content_type=content_type) return response
def _authzr_from_response(self, response, identifier, uri=None, new_cert_uri=None): if new_cert_uri is None: try: new_cert_uri = response.links['next']['url'] except KeyError: raise errors.NetworkError('"next" link missing') authzr = messages2.AuthorizationResource( body=messages2.Authorization.from_json(response.json()), uri=response.headers.get('Location', uri), new_cert_uri=new_cert_uri) if (authzr.body.key != self.key.public() or authzr.body.identifier != identifier): raise errors.UnexpectedUpdate(authzr) return authzr
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, terms_of_service=None): terms_of_service = ( response.links['terms-of-service']['url'] if 'terms-of-service' in response.links else terms_of_service) if new_authzr_uri is None: try: new_authzr_uri = response.links['next']['url'] except KeyError: raise errors.NetworkError('"next" link missing') return messages2.RegistrationResource( body=messages2.Registration.from_json(response.json()), uri=response.headers.get('Location', uri), new_authzr_uri=new_authzr_uri, terms_of_service=terms_of_service)
def check_cert(self, certr): """Check for new cert. :param certr: Certificate Resource :type certr: `.CertificateResource` :returns: Updated Certificate Resource. :rtype: `.CertificateResource` """ # TODO: acme-spec 5.1 table action should be renamed to # "refresh cert", and this method integrated with self.refresh response, cert = self._get_cert(certr.uri) if 'Location' not in response.headers: raise errors.NetworkError('Location header missing') if response.headers['Location'] != certr.uri: raise errors.UnexpectedUpdate(response.text) return certr.update(body=cert)
def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs): """Send POST data. :param str content_type: Expected ``Content-Type``, fails if not set. :raises letsencrypt.acme.messages2.NetworkError: :returns: HTTP Response :rtype: `requests.Response` """ logging.debug('Sending POST data: %s', data) try: response = requests.post(uri, data=data, **kwargs) except requests.exceptions.RequestException as error: raise errors.NetworkError(error) logging.debug('Received response %s: %s', response, response.text) self._check_response(response, content_type=content_type) return response
def request_issuance(self, csr, authzrs): """Request issuance. :param csr: CSR :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` :param authzrs: `list` of `.AuthorizationResource` :returns: Issued certificate :rtype: `.messages2.CertificateResource` """ assert authzrs, "Authorizations list is empty" logging.debug("Requesting issuance...") # TODO: assert len(authzrs) == number of SANs req = messages2.CertificateRequest( csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self._post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 self._wrap_in_jws(req), content_type=content_type, headers={'Accept': content_type}) cert_chain_uri = response.links.get('up', {}).get('url') try: uri = response.headers['Location'] except KeyError: raise errors.NetworkError('"Location" Header missing') return messages2.CertificateResource( uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, body=jose.ComparableX509( M2Crypto.X509.load_cert_der_string(response.content)))