diff --git a/.coveragerc b/.coveragerc index 087900105..1a87ab2da 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,2 @@ [report] -# show lines missing coverage in output -show_missing = True +omit = */setup.py diff --git a/.gitignore b/.gitignore index b63e40d1c..4dff20caf 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,9 @@ tests/letstest/*.pem tests/letstest/venv/ .venv + +# pytest cache +.cache + +# docker files +.docker diff --git a/.pylintrc b/.pylintrc index d510fddfd..36d8c286f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -41,7 +41,7 @@ load-plugins=linter_plugin # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import +disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code # abstract-class-not-used cannot be disabled locally (at least in # pylint 1.4.1), same for abstract-class-little-used diff --git a/.travis.yml b/.travis.yml index b27081c24..35666d8e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,53 +5,44 @@ cache: - $HOME/.cache/pip before_install: - - '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas python3)' + - '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas python3 && brew upgrade python && brew link python)' before_script: - 'if [ $TRAVIS_OS_NAME = osx ] ; then ulimit -n 1024 ; fi' -# using separate envs with different TOXENVs creates 4x1 Travis build -# matrix, which allows us to clearly distinguish which component under -# test has failed matrix: include: - - python: "2.7" - env: TOXENV=cover - - python: "2.7" - env: TOXENV=lint - - python: "2.7" - env: TOXENV=py27-oldest BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker - - python: "2.6" - env: TOXENV=py26 BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker - python: "2.7" env: TOXENV=py27_install BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - - sudo: required - env: TOXENV=apache_compat - services: docker - before_install: - addons: + - python: "2.7" + env: TOXENV=cover FYI="this also tests py27" - sudo: required env: TOXENV=nginx_compat services: docker before_install: addons: + - python: "2.7" + env: TOXENV=lint + - python: "2.6" + env: TOXENV=py26 + sudo: required + services: docker + - python: "2.7" + env: TOXENV=py27-oldest + sudo: required + services: docker + - python: "3.3" + env: TOXENV=py33 + sudo: required + services: docker + - python: "3.6" + env: TOXENV=py36 + sudo: required + services: docker - sudo: required - env: TOXENV=le_auto_precise + env: TOXENV=apache_compat services: docker before_install: addons: @@ -60,60 +51,11 @@ matrix: services: docker before_install: addons: - - sudo: required - env: TOXENV=le_auto_wheezy - services: docker - before_install: - addons: - - sudo: required - env: TOXENV=le_auto_centos6 - services: docker - before_install: - addons: - - sudo: required - env: TOXENV=docker_dev - services: docker - before_install: - addons: - python: "2.7" env: TOXENV=apacheconftest sudo: required - - python: "3.3" - env: TOXENV=py33 BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker - - python: "3.4" - env: TOXENV=py34 BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker - - python: "3.5" - env: TOXENV=py35 BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker - - python: "3.6" - env: TOXENV=py36 BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker - python: "2.7" env: TOXENV=nginxroundtrip - - language: generic - env: TOXENV=py27 - os: osx - - language: generic - env: TOXENV=py36 - os: osx # Only build pushes to the master branch, PRs, and branches beginning with @@ -130,17 +72,6 @@ branches: sudo: false addons: - # Custom /etc/hosts required for simple verification of http-01 - # and tls-sni-01, and for certbot_test_nginx - hosts: - - le.wtf - - le1.wtf - - le2.wtf - - le3.wtf - - nginx.wtf - - boulder - - boulder-mysql - - boulder-rabbitmq apt: sources: - augeas @@ -160,8 +91,10 @@ addons: - libapache2-mod-wsgi - libapache2-mod-macro -install: "travis_retry pip install tox coveralls" -script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)' +install: "travis_retry $(command -v pip || command -v pip3) install tox coveralls" +script: + - travis_retry tox + - '[ -z "${BOULDER_INTEGRATION+x}" ] || (travis_retry tests/boulder-fetch.sh && tests/tox-boulder-integration.sh)' after_success: '[ "$TOXENV" == "cover" ] && coveralls' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aaef4af1..1369b0907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,105 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 0.21.1 - 2018-01-25 + +### Fixed + +* When creating an HTTP to HTTPS redirect in Nginx, we now ensure the Host + header of the request is set to an expected value before redirecting users to + the domain found in the header. The previous way Certbot configured Nginx + redirects was a potential security issue which you can read more about at + https://community.letsencrypt.org/t/security-issue-with-redirects-added-by-certbots-nginx-plugin/51493. +* Fixed a problem where Certbot's Apache plugin could fail HTTP-01 challenges + if basic authentication is configured for the domain you request a + certificate for. +* certbot-auto --no-bootstrap now properly tries to use Python 3.4 on RHEL 6 + based systems rather than Python 2.6. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/49?closed=1 + +## 0.21.0 - 2018-01-17 + +### Added + +* Support for the HTTP-01 challenge type was added to our Apache and Nginx + plugins. For those not aware, Let's Encrypt disabled the TLS-SNI-01 challenge + type which was what was previously being used by our Apache and Nginx plugins + last week due to a security issue. For more information about Let's Encrypt's + change, click + [here](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188). + Our Apache and Nginx plugins will automatically switch to use HTTP-01 so no + changes need to be made to your Certbot configuration, however, you should + make sure your server is accessible on port 80 and isn't behind an external + proxy doing things like redirecting all traffic from HTTP to HTTPS. HTTP to + HTTPS redirects inside Apache and Nginx are fine. +* IPv6 support was added to the Nginx plugin. +* Support for automatically creating server blocks based on the default server + block was added to the Nginx plugin. +* The flags --delete-after-revoke and --no-delete-after-revoke were added + allowing users to control whether the revoke subcommand also deletes the + certificates it is revoking. + +### Changed + +* We deprecated support for Python 2.6 and Python 3.3 in Certbot and its ACME + library. Support for these versions of Python will be removed in the next + major release of Certbot. If you are using certbot-auto on a RHEL 6 based + system, it will guide you through the process of installing Python 3. +* We split our implementation of JOSE (Javascript Object Signing and + Encryption) out of our ACME library and into a separate package named josepy. + This package is available on [PyPI](https://pypi.python.org/pypi/josepy) and + on [GitHub](https://github.com/certbot/josepy). +* We updated the ciphersuites used in Apache to the new [values recommended by + Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29). + The major change here is adding ChaCha20 to the list of supported + ciphersuites. + +### Fixed + +* An issue with our Apache plugin on Gentoo due to differences in their + apache2ctl command have been resolved. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/47?closed=1 + +## 0.20.0 - 2017-12-06 + +### Added + +* Certbot's ACME library now recognizes URL fields in challenge objects in + preparation for Let's Encrypt's new ACME endpoint. The value is still + accessible in our ACME library through the name "uri". + +### Changed + +* The Apache plugin now parses some distro specific Apache configuration files + on non-Debian systems allowing it to get a clearer picture on the running + configuration. Internally, these changes were structured so that external + contributors can easily write patches to make the plugin work in new Apache + configurations. +* Certbot better reports network failures by removing information about + connection retries from the error output. +* An unnecessary question when using Certbot's webroot plugin interactively has + been removed. + +### Fixed + +* Certbot's NGINX plugin no longer sometimes incorrectly reports that it was + unable to deploy a HTTP->HTTPS redirect when requesting Certbot to enable a + redirect for multiple domains. +* Problems where the Apache plugin was failing to find directives and + duplicating existing directives on openSUSE have been resolved. +* An issue running the test shipped with Certbot and some our DNS plugins with + older versions of mock have been resolved. +* On some systems, users reported strangely interleaved output depending on + when stdout and stderr were flushed. This problem was resolved by having + Certbot regularly flush these streams. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/44?closed=1 + ## 0.19.0 - 2017-10-04 ### Added diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 55780c0ac..03917f8ca 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,3 +1,9 @@ +If you're having trouble using Certbot and aren't sure you've found a bug or +request for a new feature, please first try asking for help at +https://community.letsencrypt.org/. There is a much larger community there of +people familiar with the project who will be able to more quickly answer your +questions. + ## My operating system is (include version): diff --git a/acme/acme/__init__.py b/acme/acme/__init__.py index e8a0b16a8..5850fa955 100644 --- a/acme/acme/__init__.py +++ b/acme/acme/__init__.py @@ -10,3 +10,13 @@ supported version: `draft-ietf-acme-01`_. https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01 """ +import sys +import warnings + +for (major, minor) in [(2, 6), (3, 3)]: + if sys.version_info[:2] == (major, minor): + warnings.warn( + "Python {0}.{1} support will be dropped in the next release of " + "acme. Please upgrade your Python version.".format(major, minor), + DeprecationWarning, + ) #pragma: no cover diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 14641af10..96997297b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -6,13 +6,13 @@ import logging import socket from cryptography.hazmat.primitives import hashes # type: ignore +import josepy as jose import OpenSSL import requests from acme import errors from acme import crypto_util from acme import fields -from acme import jose logger = logging.getLogger(__name__) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 49e790102..834d569aa 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -1,6 +1,7 @@ """Tests for acme.challenges.""" import unittest +import josepy as jose import mock import OpenSSL import requests @@ -8,7 +9,6 @@ import requests from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error from acme import errors -from acme import jose from acme import test_util CERT = test_util.load_comparable_cert('cert.pem') diff --git a/acme/acme/client.py b/acme/acme/client.py index fa903f0e6..77e89e535 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -10,12 +10,13 @@ import time import six from six.moves import http_client # pylint: disable=import-error +import josepy as jose import OpenSSL +import re import requests import sys from acme import errors -from acme import jose from acme import jws from acme import messages @@ -38,39 +39,24 @@ DEFAULT_NETWORK_TIMEOUT = 45 DER_CONTENT_TYPE = 'application/pkix-cert' -class Client(object): # pylint: disable=too-many-instance-attributes - """ACME client. - - .. todo:: - Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()`. +class ClientBase(object): # pylint: disable=too-many-instance-attributes + """ACME client base object. :ivar messages.Directory directory: - :ivar key: `.JWK` (private) - :ivar alg: `.JWASignature` - :ivar bool verify_ssl: Verify SSL certificates? - :ivar .ClientNetwork net: Client network. Useful for testing. If not - supplied, it will be initialized using `key`, `alg` and - `verify_ssl`. - + :ivar .ClientNetwork net: Client network. + :ivar int acme_version: ACME protocol version. 1 or 2. """ - def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, - net=None): + def __init__(self, directory, net, acme_version): """Initialize. - :param directory: Directory Resource (`.messages.Directory`) or - URI from which the resource will be downloaded. - + :param .messages.Directory directory: Directory Resource + :param .ClientNetwork net: Client network. + :param int acme_version: ACME protocol version. 1 or 2. """ - self.key = key - self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net - - if isinstance(directory, six.string_types): - self.directory = messages.Directory.from_json( - self.net.get(directory).json()) - else: - self.directory = directory + self.directory = directory + self.net = net + self.acme_version = acme_version @classmethod def _regr_from_response(cls, response, uri=None, terms_of_service=None): @@ -82,28 +68,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes uri=response.headers.get('Location', uri), terms_of_service=terms_of_service) - def register(self, new_reg=None): - """Register. - - :param .NewRegistration new_reg: - - :returns: Registration Resource. - :rtype: `.RegistrationResource` - - """ - new_reg = messages.NewRegistration() if new_reg is None else new_reg - assert isinstance(new_reg, messages.NewRegistration) - - response = self.net.post(self.directory[new_reg], new_reg) - # TODO: handle errors - assert response.status_code == http_client.CREATED - - # "Instance of 'Field' has no key/contact member" bug: - # pylint: disable=no-member - return self._regr_from_response(response) - def _send_recv_regr(self, regr, body): - response = self.net.post(regr.uri, body) + response = self.net.post(regr.uri, body, acme_version=self.acme_version) # TODO: Boulder returns httplib.ACCEPTED #assert response.status_code == httplib.OK @@ -129,6 +95,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes update = regr.body if update is None else update body = messages.UpdateRegistration(**dict(update)) updated_regr = self._send_recv_regr(regr, body=body) + self.net.account = updated_regr return updated_regr def deactivate_registration(self, regr): @@ -152,21 +119,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes """ return self._send_recv_regr(regr, messages.UpdateRegistration()) - def agree_to_tos(self, regr): - """Agree to the terms-of-service. - - Agree to the terms-of-service in a Registration Resource. - - :param regr: Registration Resource. - :type regr: `.RegistrationResource` - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - return self.update_registration( - regr.update(body=regr.body.update(agreement=regr.terms_of_service))) - def _authzr_from_response(self, response, identifier, uri=None): authzr = messages.AuthorizationResource( body=messages.Authorization.from_json(response.json()), @@ -175,42 +127,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes raise errors.UnexpectedUpdate(authzr) return authzr - def request_challenges(self, identifier, new_authzr_uri=None): - """Request challenges. - - :param .messages.Identifier identifier: Identifier to be challenged. - :param str new_authzr_uri: Deprecated. Do not use. - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - """ - if new_authzr_uri is not None: - logger.debug("request_challenges with new_authzr_uri deprecated.") - new_authz = messages.NewAuthorization(identifier=identifier) - response = self.net.post(self.directory.new_authz, new_authz) - # TODO: handle errors - assert response.status_code == http_client.CREATED - return self._authzr_from_response(response, identifier) - - def request_domain_challenges(self, domain, new_authzr_uri=None): - """Request challenges for domain names. - - This is simply a convenience function that wraps around - `request_challenges`, but works with domain names instead of - generic identifiers. See ``request_challenges`` for more - documentation. - - :param str domain: Domain name to be challenged. - :param str new_authzr_uri: Deprecated. Do not use. - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - """ - return self.request_challenges(messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri) - def answer_challenge(self, challb, response): """Answer challenge. @@ -226,7 +142,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes :raises .UnexpectedUpdate: """ - response = self.net.post(challb.uri, response) + response = self.net.post(challb.uri, response, + acme_version=self.acme_version) try: authzr_uri = response.links['up']['url'] except KeyError: @@ -287,6 +204,133 @@ class Client(object): # pylint: disable=too-many-instance-attributes response, authzr.body.identifier, authzr.uri) return updated_authzr, response + def revoke(self, cert, rsn): + """Revoke certificate. + + :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in + `.ComparableX509` + + :param int rsn: Reason code for certificate revocation. + + :raises .ClientError: If revocation is unsuccessful. + + """ + response = self.net.post(self.directory[messages.Revocation], + messages.Revocation( + certificate=cert, + reason=rsn), + content_type=None, + acme_version=self.acme_version) + if response.status_code != http_client.OK: + raise errors.ClientError( + 'Successful revocation must return HTTP OK status') + +class Client(ClientBase): + """ACME client for a v1 API. + + .. todo:: + Clean up raised error types hierarchy, document, and handle (wrap) + instances of `.DeserializationError` raised in `from_json()`. + + :ivar messages.Directory directory: + :ivar key: `.JWK` (private) + :ivar alg: `.JWASignature` + :ivar bool verify_ssl: Verify SSL certificates? + :ivar .ClientNetwork net: Client network. Useful for testing. If not + supplied, it will be initialized using `key`, `alg` and + `verify_ssl`. + + """ + + def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, + net=None): + """Initialize. + + :param directory: Directory Resource (`.messages.Directory`) or + URI from which the resource will be downloaded. + + """ + # pylint: disable=too-many-arguments + self.key = key + self.net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl) if net is None else net + + if isinstance(directory, six.string_types): + directory = messages.Directory.from_json( + self.net.get(directory).json()) + super(Client, self).__init__(directory=directory, + net=net, acme_version=1) + + def register(self, new_reg=None): + """Register. + + :param .NewRegistration new_reg: + + :returns: Registration Resource. + :rtype: `.RegistrationResource` + + """ + new_reg = messages.NewRegistration() if new_reg is None else new_reg + response = self.net.post(self.directory[new_reg], new_reg, + acme_version=1) + # TODO: handle errors + assert response.status_code == http_client.CREATED + + # "Instance of 'Field' has no key/contact member" bug: + # pylint: disable=no-member + return self._regr_from_response(response) + + def agree_to_tos(self, regr): + """Agree to the terms-of-service. + + Agree to the terms-of-service in a Registration Resource. + + :param regr: Registration Resource. + :type regr: `.RegistrationResource` + + :returns: Updated Registration Resource. + :rtype: `.RegistrationResource` + + """ + return self.update_registration( + regr.update(body=regr.body.update(agreement=regr.terms_of_service))) + + def request_challenges(self, identifier, new_authzr_uri=None): + """Request challenges. + + :param .messages.Identifier identifier: Identifier to be challenged. + :param str new_authzr_uri: Deprecated. Do not use. + + :returns: Authorization Resource. + :rtype: `.AuthorizationResource` + + """ + if new_authzr_uri is not None: + logger.debug("request_challenges with new_authzr_uri deprecated.") + new_authz = messages.NewAuthorization(identifier=identifier) + response = self.net.post(self.directory.new_authz, new_authz, + acme_version=1) + # TODO: handle errors + assert response.status_code == http_client.CREATED + return self._authzr_from_response(response, identifier) + + def request_domain_challenges(self, domain, new_authzr_uri=None): + """Request challenges for domain names. + + This is simply a convenience function that wraps around + `request_challenges`, but works with domain names instead of + generic identifiers. See ``request_challenges`` for more + documentation. + + :param str domain: Domain name to be challenged. + :param str new_authzr_uri: Deprecated. Do not use. + + :returns: Authorization Resource. + :rtype: `.AuthorizationResource` + + """ + return self.request_challenges(messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri) + def request_issuance(self, csr, authzrs): """Request issuance. @@ -310,7 +354,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes self.directory.new_cert, req, content_type=content_type, - headers={'Accept': content_type}) + headers={'Accept': content_type}, + acme_version=1) cert_chain_uri = response.links.get('up', {}).get('url') @@ -407,7 +452,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes :param str uri: URI of certificate :returns: tuple of the form - (response, :class:`acme.jose.ComparableX509`) + (response, :class:`josepy.util.ComparableX509`) :rtype: tuple """ @@ -480,37 +525,67 @@ class Client(object): # pylint: disable=too-many-instance-attributes "Recursion limit reached. Didn't get {0}".format(uri)) return chain - def revoke(self, cert, rsn): - """Revoke certificate. - :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` - :param int rsn: Reason code for certificate revocation. +class ClientV2(ClientBase): + """ACME client for a v2 API. - :raises .ClientError: If revocation is unsuccessful. + :ivar messages.Directory directory: + :ivar .ClientNetwork net: Client network. + """ + + def __init__(self, directory, net): + """Initialize. + + :param .messages.Directory directory: Directory Resource + :param .ClientNetwork net: Client network. + """ + super(ClientV2, self).__init__(directory=directory, + net=net, acme_version=2) + + def new_account(self, new_account): + """Register. + + :param .NewRegistration new_account: + + :returns: Registration Resource. + :rtype: `.RegistrationResource` """ - response = self.net.post(self.directory[messages.Revocation], - messages.Revocation( - certificate=cert, - reason=rsn), - content_type=None) - if response.status_code != http_client.OK: - raise errors.ClientError( - 'Successful revocation must return HTTP OK status') - + response = self.net.post(self.directory['newAccount'], new_account, + acme_version=2) + # "Instance of 'Field' has no key/contact member" bug: + # pylint: disable=no-member + regr = self._regr_from_response(response) + self.net.account = regr + return regr class ClientNetwork(object): # pylint: disable=too-many-instance-attributes - """Client network.""" + """Wrapper around requests that signs POSTs for authentication. + + Also adds user agent, and handles Content-Type. + """ JSON_CONTENT_TYPE = 'application/json' JOSE_CONTENT_TYPE = 'application/jose+json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' REPLAY_NONCE_HEADER = 'Replay-Nonce' - def __init__(self, key, alg=jose.RS256, verify_ssl=True, + """Initialize. + + :param key: Account private key + :param messages.RegistrationResource account: Account object. Required if you are + planning to use .post() with acme_version=2 for anything other than creating a new + account; may be set later after registering. + :param josepy.JWASignature alg: Algoritm to use in signing JWS. + :param bool verify_ssl: Whether to verify certificates on SSL connections. + :param str user_agent: String to send as User-Agent header. + :param float timeout: Timeout for requests. + """ + def __init__(self, key, account=None, alg=jose.RS256, verify_ssl=True, user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT): + # pylint: disable=too-many-arguments self.key = key + self.account = account self.alg = alg self.verify_ssl = verify_ssl self._nonces = set() @@ -526,21 +601,29 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes except Exception: # pylint: disable=broad-except pass - def _wrap_in_jws(self, obj, nonce): + def _wrap_in_jws(self, obj, nonce, url, acme_version): """Wrap `JSONDeSerializable` object in JWS. .. todo:: Implement ``acmePath``. :param .JSONDeSerializable obj: + :param str url: The URL to which this object will be POSTed :param bytes nonce: :rtype: `.JWS` """ jobj = obj.json_dumps(indent=2).encode() logger.debug('JWS payload:\n%s', jobj) - return jws.JWS.sign( - payload=jobj, key=self.key, alg=self.alg, - nonce=nonce).json_dumps(indent=2) + kwargs = { + "alg": self.alg, + "nonce": nonce + } + if acme_version == 2: + kwargs["url"] = url + kwargs["kid"] = self.account["uri"] + kwargs["key"] = self.key + # pylint: disable=star-args + return jws.JWS.sign(jobj, **kwargs).json_dumps(indent=2) @classmethod def _check_response(cls, response, content_type=None): @@ -599,6 +682,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes return response def _send_request(self, method, url, *args, **kwargs): + # pylint: disable=too-many-locals """Send HTTP request. Makes sure that `verify_ssl` is respected. Logs request and @@ -624,7 +708,32 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('User-Agent', self.user_agent) kwargs.setdefault('timeout', self._default_timeout) - response = self.session.request(method, url, *args, **kwargs) + try: + response = self.session.request(method, url, *args, **kwargs) + except requests.exceptions.RequestException as e: + # pylint: disable=pointless-string-statement + """Requests response parsing + + The requests library emits exceptions with a lot of extra text. + We parse them with a regexp to raise a more readable exceptions. + + Example: + HTTPSConnectionPool(host='acme-v01.api.letsencrypt.org', + port=443): Max retries exceeded with url: /directory + (Caused by NewConnectionError(' + : Failed to establish a new connection: + [Errno 65] No route to host',))""" + + # pylint: disable=line-too-long + err_regex = r".*host='(\S*)'.*Max retries exceeded with url\: (\/\w*).*(\[Errno \d+\])([A-Za-z ]*)" + m = re.match(err_regex, str(e)) + if m is None: + raise # pragma: no cover + else: + host, path, _err_no, err_msg = m.groups() + raise ValueError("Requesting {0}{1}:{2}".format(host, path, err_msg)) + # If content is DER, log the base64 of it instead of raw bytes, to keep # binary data out of the logs. if response.headers.get("Content-Type") == DER_CONTENT_TYPE: @@ -687,8 +796,9 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes else: raise - def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs): - data = self._wrap_in_jws(obj, self._get_nonce(url)) + def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, + acme_version=1, **kwargs): + data = self._wrap_in_jws(obj, self._get_nonce(url), url, acme_version) kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) self._add_nonce(response) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 54652b46c..662c32942 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -5,12 +5,12 @@ import unittest from six.moves import http_client # pylint: disable=import-error +import josepy as jose import mock import requests from acme import challenges from acme import errors -from acme import jose from acme import jws as acme_jws from acme import messages from acme import messages_test @@ -104,6 +104,23 @@ class ClientTest(unittest.TestCase): self.assertEqual(self.regr, self.client.register(self.new_reg)) # TODO: test POST call arguments + def test_new_account_v2(self): + directory = messages.Directory({ + "newAccount": 'https://www.letsencrypt-demo.org/acme/new-account', + }) + from acme.client import ClientV2 + client = ClientV2(directory, self.net) + self.response.status_code = http_client.CREATED + self.response.json.return_value = self.regr.body.to_json() + self.response.headers['Location'] = self.regr.uri + + self.regr = messages.RegistrationResource( + body=messages.Registration( + contact=self.contact, key=KEY.public_key()), + uri='https://www.letsencrypt-demo.org/acme/reg/1') + + self.assertEqual(self.regr, client.new_account(self.regr)) + def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member @@ -142,20 +159,23 @@ class ClientTest(unittest.TestCase): self.client.request_challenges(self.identifier) self.net.post.assert_called_once_with( self.directory.new_authz, - messages.NewAuthorization(identifier=self.identifier)) + messages.NewAuthorization(identifier=self.identifier), + acme_version=1) def test_request_challenges_deprecated_arg(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier, new_authzr_uri="hi") self.net.post.assert_called_once_with( self.directory.new_authz, - messages.NewAuthorization(identifier=self.identifier)) + messages.NewAuthorization(identifier=self.identifier), + acme_version=1) def test_request_challenges_custom_uri(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier) self.net.post.assert_called_once_with( - 'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY) + 'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY, + acme_version=1) def test_request_challenges_unexpected_update(self): self._prepare_response_for_request_challenges() @@ -417,7 +437,8 @@ class ClientTest(unittest.TestCase): def test_revoke(self): self.client.revoke(self.certr.body, self.rsn) self.net.post.assert_called_once_with( - self.directory[messages.Revocation], mock.ANY, content_type=None) + self.directory[messages.Revocation], mock.ANY, content_type=None, + acme_version=1) def test_revocation_payload(self): obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn) @@ -432,9 +453,22 @@ class ClientTest(unittest.TestCase): self.certr, self.rsn) +class MockJSONDeSerializable(jose.JSONDeSerializable): + # pylint: disable=missing-docstring + def __init__(self, value): + self.value = value + + def to_partial_json(self): + return {'foo': self.value} + + @classmethod + def from_json(cls, value): + pass # pragma: no cover + class ClientNetworkTest(unittest.TestCase): """Tests for acme.client.ClientNetwork.""" + # pylint: disable=too-many-public-methods def setUp(self): self.verify_ssl = mock.MagicMock() @@ -453,25 +487,27 @@ class ClientNetworkTest(unittest.TestCase): self.assertTrue(self.net.verify_ssl is self.verify_ssl) def test_wrap_in_jws(self): - class MockJSONDeSerializable(jose.JSONDeSerializable): - # pylint: disable=missing-docstring - def __init__(self, value): - self.value = value - - def to_partial_json(self): - return {'foo': self.value} - - @classmethod - def from_json(cls, value): - pass # pragma: no cover - # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce=b'Tg') + MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", + acme_version=1) jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') + def test_wrap_in_jws_v2(self): + self.net.account = {'uri': 'acct-uri'} + # pylint: disable=protected-access + jws_dump = self.net._wrap_in_jws( + MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", + acme_version=2) + jws = acme_jws.JWS.json_loads(jws_dump) + self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) + self.assertEqual(jws.signature.combined.nonce, b'Tg') + self.assertEqual(jws.signature.combined.kid, u'acct-uri') + self.assertEqual(jws.signature.combined.url, u'url') + + def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False self.response.json.return_value = {} @@ -621,6 +657,21 @@ class ClientNetworkTest(unittest.TestCase): self.assertRaises(requests.exceptions.RequestException, self.net._send_request, 'GET', 'uri') + def test_urllib_error(self): + # Using a connection error to test a properly formatted error message + try: + # pylint: disable=protected-access + self.net._send_request('GET', "http://localhost:19123/nonexistent.txt") + + # Value Error Generated Exceptions + except ValueError as y: + self.assertEqual("Requesting localhost/nonexistent: " + "Connection refused", str(y)) + + # Requests Library Exceptions + except requests.exceptions.ConnectionError as z: #pragma: no cover + self.assertEqual("('Connection aborted.', " + "error(111, 'Connection refused'))", str(z)) class ClientNetworkWithMockedResponseTest(unittest.TestCase): """Tests for acme.client.ClientNetwork which mock out response.""" @@ -686,13 +737,13 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.assertEqual(self.checked_response, self.net.post( 'uri', self.obj, content_type=self.content_type)) self.net._wrap_in_jws.assert_called_once_with( - self.obj, jose.b64decode(self.all_nonces.pop())) + self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) self.available_nonces = [] self.assertRaises(errors.MissingNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) self.net._wrap_in_jws.assert_called_with( - self.obj, jose.b64decode(self.all_nonces.pop())) + self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) def test_post_wrong_initial_nonce(self): # HEAD self.available_nonces = [b'f', jose.b64encode(b'good')] diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 4046aa197..1d7f83ccf 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -8,17 +8,16 @@ import unittest import six from six.moves import socketserver #type: ignore # pylint: disable=import-error +import josepy as jose import OpenSSL from acme import errors -from acme import jose from acme import test_util class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" - _multiprocess_can_split_ = True def setUp(self): self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') @@ -69,7 +68,6 @@ class SSLSocketAndProbeSNITest(unittest.TestCase): class PyOpenSSLCertOrReqSANTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" - _multiprocess_can_split_ = True @classmethod def _call(cls, loader, name): @@ -140,7 +138,6 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase): class RandomSnTest(unittest.TestCase): """Test for random certificate serial numbers.""" - _multiprocess_can_split_ = True def setUp(self): self.cert_count = 5 diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 9d991fd75..de5f9d1f4 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -1,5 +1,5 @@ """ACME errors.""" -from acme.jose import errors as jose_errors +from josepy import errors as jose_errors class Error(Exception): diff --git a/acme/acme/fields.py b/acme/acme/fields.py index 12d09acf4..d7ec78403 100644 --- a/acme/acme/fields.py +++ b/acme/acme/fields.py @@ -1,10 +1,9 @@ """ACME JSON fields.""" import logging +import josepy as jose import pyrfc3339 -from acme import jose - logger = logging.getLogger(__name__) diff --git a/acme/acme/fields_test.py b/acme/acme/fields_test.py index de852b6fa..69dde8b89 100644 --- a/acme/acme/fields_test.py +++ b/acme/acme/fields_test.py @@ -2,10 +2,9 @@ import datetime import unittest +import josepy as jose import pytz -from acme import jose - class FixedTest(unittest.TestCase): """Tests for acme.fields.Fixed.""" diff --git a/acme/acme/jose/__init__.py b/acme/acme/jose/__init__.py deleted file mode 100644 index 9116bc433..000000000 --- a/acme/acme/jose/__init__.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Javascript Object Signing and Encryption (jose). - -This package is a Python implementation of the standards developed by -IETF `Javascript Object Signing and Encryption (Active WG)`_, in -particular the following RFCs: - - - `JSON Web Algorithms (JWA)`_ - - `JSON Web Key (JWK)`_ - - `JSON Web Signature (JWS)`_ - - -.. _`Javascript Object Signing and Encryption (Active WG)`: - https://tools.ietf.org/wg/jose/ - -.. _`JSON Web Algorithms (JWA)`: - https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-algorithms/ - -.. _`JSON Web Key (JWK)`: - https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-key/ - -.. _`JSON Web Signature (JWS)`: - https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-signature/ - -""" -from acme.jose.b64 import ( - b64decode, - b64encode, -) - -from acme.jose.errors import ( - DeserializationError, - SerializationError, - Error, - UnrecognizedTypeError, -) - -from acme.jose.interfaces import JSONDeSerializable - -from acme.jose.json_util import ( - Field, - JSONObjectWithFields, - TypedJSONObjectWithFields, - decode_b64jose, - decode_cert, - decode_csr, - decode_hex16, - encode_b64jose, - encode_cert, - encode_csr, - encode_hex16, -) - -from acme.jose.jwa import ( - HS256, - HS384, - HS512, - JWASignature, - PS256, - PS384, - PS512, - RS256, - RS384, - RS512, -) - -from acme.jose.jwk import ( - JWK, - JWKRSA, -) - -from acme.jose.jws import ( - Header, - JWS, - Signature, -) - -from acme.jose.util import ( - ComparableX509, - ComparableKey, - ComparableRSAKey, - ImmutableMap, -) diff --git a/acme/acme/jose/b64.py b/acme/acme/jose/b64.py deleted file mode 100644 index cf79aa820..000000000 --- a/acme/acme/jose/b64.py +++ /dev/null @@ -1,61 +0,0 @@ -"""JOSE Base64. - -`JOSE Base64`_ is defined as: - - - URL-safe Base64 - - padding stripped - - -.. _`JOSE Base64`: - https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-37#appendix-C - -.. Do NOT try to call this module "base64", as it will "shadow" the - standard library. - -""" -import base64 - -import six - - -def b64encode(data): - """JOSE Base64 encode. - - :param data: Data to be encoded. - :type data: `bytes` - - :returns: JOSE Base64 string. - :rtype: bytes - - :raises TypeError: if `data` is of incorrect type - - """ - if not isinstance(data, six.binary_type): - raise TypeError('argument should be {0}'.format(six.binary_type)) - return base64.urlsafe_b64encode(data).rstrip(b'=') - - -def b64decode(data): - """JOSE Base64 decode. - - :param data: Base64 string to be decoded. If it's unicode, then - only ASCII characters are allowed. - :type data: `bytes` or `unicode` - - :returns: Decoded data. - :rtype: bytes - - :raises TypeError: if input is of incorrect type - :raises ValueError: if input is unicode with non-ASCII characters - - """ - if isinstance(data, six.string_types): - try: - data = data.encode('ascii') - except UnicodeEncodeError: - raise ValueError( - 'unicode argument should contain only ASCII characters') - elif not isinstance(data, six.binary_type): - raise TypeError('argument should be a str or unicode') - - return base64.urlsafe_b64decode(data + b'=' * (4 - (len(data) % 4))) diff --git a/acme/acme/jose/b64_test.py b/acme/acme/jose/b64_test.py deleted file mode 100644 index cbabe2251..000000000 --- a/acme/acme/jose/b64_test.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Tests for acme.jose.b64.""" -import unittest - -import six - - -# https://en.wikipedia.org/wiki/Base64#Examples -B64_PADDING_EXAMPLES = { - b'any carnal pleasure.': (b'YW55IGNhcm5hbCBwbGVhc3VyZS4', b'='), - b'any carnal pleasure': (b'YW55IGNhcm5hbCBwbGVhc3VyZQ', b'=='), - b'any carnal pleasur': (b'YW55IGNhcm5hbCBwbGVhc3Vy', b''), - b'any carnal pleasu': (b'YW55IGNhcm5hbCBwbGVhc3U', b'='), - b'any carnal pleas': (b'YW55IGNhcm5hbCBwbGVhcw', b'=='), -} - - -B64_URL_UNSAFE_EXAMPLES = { - six.int2byte(251) + six.int2byte(239): b'--8', - six.int2byte(255) * 2: b'__8', -} - - -class B64EncodeTest(unittest.TestCase): - """Tests for acme.jose.b64.b64encode.""" - - @classmethod - def _call(cls, data): - from acme.jose.b64 import b64encode - return b64encode(data) - - def test_empty(self): - self.assertEqual(self._call(b''), b'') - - def test_unsafe_url(self): - for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES): - self.assertEqual(self._call(text), b64) - - def test_different_paddings(self): - for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES): - self.assertEqual(self._call(text), b64) - - def test_unicode_fails_with_type_error(self): - self.assertRaises(TypeError, self._call, u'some unicode') - - -class B64DecodeTest(unittest.TestCase): - """Tests for acme.jose.b64.b64decode.""" - - @classmethod - def _call(cls, data): - from acme.jose.b64 import b64decode - return b64decode(data) - - def test_unsafe_url(self): - for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES): - self.assertEqual(self._call(b64), text) - - def test_input_without_padding(self): - for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES): - self.assertEqual(self._call(b64), text) - - def test_input_with_padding(self): - for text, (b64, pad) in six.iteritems(B64_PADDING_EXAMPLES): - self.assertEqual(self._call(b64 + pad), text) - - def test_unicode_with_ascii(self): - self.assertEqual(self._call(u'YQ'), b'a') - - def test_non_ascii_unicode_fails(self): - self.assertRaises(ValueError, self._call, u'\u0105') - - def test_type_error_no_unicode_or_bytes(self): - self.assertRaises(TypeError, self._call, object()) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/errors.py b/acme/acme/jose/errors.py deleted file mode 100644 index 74c9443e1..000000000 --- a/acme/acme/jose/errors.py +++ /dev/null @@ -1,35 +0,0 @@ -"""JOSE errors.""" - - -class Error(Exception): - """Generic JOSE Error.""" - - -class DeserializationError(Error): - """JSON deserialization error.""" - - def __str__(self): - return "Deserialization error: {0}".format( - super(DeserializationError, self).__str__()) - - -class SerializationError(Error): - """JSON serialization error.""" - - -class UnrecognizedTypeError(DeserializationError): - """Unrecognized type error. - - :ivar str typ: The unrecognized type of the JSON object. - :ivar jobj: Full JSON object. - - """ - - def __init__(self, typ, jobj): - self.typ = typ - self.jobj = jobj - super(UnrecognizedTypeError, self).__init__(str(self)) - - def __str__(self): - return '{0} was not recognized, full message: {1}'.format( - self.typ, self.jobj) diff --git a/acme/acme/jose/errors_test.py b/acme/acme/jose/errors_test.py deleted file mode 100644 index 919980920..000000000 --- a/acme/acme/jose/errors_test.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Tests for acme.jose.errors.""" -import unittest - - -class UnrecognizedTypeErrorTest(unittest.TestCase): - def setUp(self): - from acme.jose.errors import UnrecognizedTypeError - self.error = UnrecognizedTypeError('foo', {'type': 'foo'}) - - def test_str(self): - self.assertEqual( - "foo was not recognized, full message: {'type': 'foo'}", - str(self.error)) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/interfaces.py b/acme/acme/jose/interfaces.py deleted file mode 100644 index f841848b3..000000000 --- a/acme/acme/jose/interfaces.py +++ /dev/null @@ -1,216 +0,0 @@ -"""JOSE interfaces.""" -import abc -import collections -import json - -import six - -from acme.jose import errors -from acme.jose import util - -# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class -# pylint: disable=too-few-public-methods - - -@six.add_metaclass(abc.ABCMeta) -class JSONDeSerializable(object): - # pylint: disable=too-few-public-methods - """Interface for (de)serializable JSON objects. - - Please recall, that standard Python library implements - :class:`json.JSONEncoder` and :class:`json.JSONDecoder` that perform - translations based on respective :ref:`conversion tables - ` that look pretty much like the one below (for - complete tables see relevant Python documentation): - - .. _conversion-table: - - ====== ====== - JSON Python - ====== ====== - object dict - ... ... - ====== ====== - - While the above **conversion table** is about translation of JSON - documents to/from the basic Python types only, - :class:`JSONDeSerializable` introduces the following two concepts: - - serialization - Turning an arbitrary Python object into Python object that can - be encoded into a JSON document. **Full serialization** produces - a Python object composed of only basic types as required by the - :ref:`conversion table `. **Partial - serialization** (accomplished by :meth:`to_partial_json`) - produces a Python object that might also be built from other - :class:`JSONDeSerializable` objects. - - deserialization - Turning a decoded Python object (necessarily one of the basic - types as required by the :ref:`conversion table - `) into an arbitrary Python object. - - Serialization produces **serialized object** ("partially serialized - object" or "fully serialized object" for partial and full - serialization respectively) and deserialization produces - **deserialized object**, both usually denoted in the source code as - ``jobj``. - - Wording in the official Python documentation might be confusing - after reading the above, but in the light of those definitions, one - can view :meth:`json.JSONDecoder.decode` as decoder and - deserializer of basic types, :meth:`json.JSONEncoder.default` as - serializer of basic types, :meth:`json.JSONEncoder.encode` as - serializer and encoder of basic types. - - One could extend :mod:`json` to support arbitrary object - (de)serialization either by: - - - overriding :meth:`json.JSONDecoder.decode` and - :meth:`json.JSONEncoder.default` in subclasses - - - or passing ``object_hook`` argument (or ``object_hook_pairs``) - to :func:`json.load`/:func:`json.loads` or ``default`` argument - for :func:`json.dump`/:func:`json.dumps`. - - Interestingly, ``default`` is required to perform only partial - serialization, as :func:`json.dumps` applies ``default`` - recursively. This is the idea behind making :meth:`to_partial_json` - produce only partial serialization, while providing custom - :meth:`json_dumps` that dumps with ``default`` set to - :meth:`json_dump_default`. - - To make further documentation a bit more concrete, please, consider - the following imaginatory implementation example:: - - class Foo(JSONDeSerializable): - def to_partial_json(self): - return 'foo' - - @classmethod - def from_json(cls, jobj): - return Foo() - - class Bar(JSONDeSerializable): - def to_partial_json(self): - return [Foo(), Foo()] - - @classmethod - def from_json(cls, jobj): - return Bar() - - """ - - @abc.abstractmethod - def to_partial_json(self): # pragma: no cover - """Partially serialize. - - Following the example, **partial serialization** means the following:: - - assert isinstance(Bar().to_partial_json()[0], Foo) - assert isinstance(Bar().to_partial_json()[1], Foo) - - # in particular... - assert Bar().to_partial_json() != ['foo', 'foo'] - - :raises acme.jose.errors.SerializationError: - in case of any serialization error. - :returns: Partially serializable object. - - """ - raise NotImplementedError() - - def to_json(self): - """Fully serialize. - - Again, following the example from before, **full serialization** - means the following:: - - assert Bar().to_json() == ['foo', 'foo'] - - :raises acme.jose.errors.SerializationError: - in case of any serialization error. - :returns: Fully serialized object. - - """ - def _serialize(obj): - if isinstance(obj, JSONDeSerializable): - return _serialize(obj.to_partial_json()) - if isinstance(obj, six.string_types): # strings are Sequence - return obj - elif isinstance(obj, list): - return [_serialize(subobj) for subobj in obj] - elif isinstance(obj, collections.Sequence): - # default to tuple, otherwise Mapping could get - # unhashable list - return tuple(_serialize(subobj) for subobj in obj) - elif isinstance(obj, collections.Mapping): - return dict((_serialize(key), _serialize(value)) - for key, value in six.iteritems(obj)) - else: - return obj - - return _serialize(self) - - @util.abstractclassmethod - def from_json(cls, jobj): # pylint: disable=unused-argument - """Deserialize a decoded JSON document. - - :param jobj: Python object, composed of only other basic data - types, as decoded from JSON document. Not necessarily - :class:`dict` (as decoded from "JSON object" document). - - :raises acme.jose.errors.DeserializationError: - if decoding was unsuccessful, e.g. in case of unparseable - X509 certificate, or wrong padding in JOSE base64 encoded - string, etc. - - """ - # TypeError: Can't instantiate abstract class with - # abstract methods from_json, to_partial_json - return cls() # pylint: disable=abstract-class-instantiated - - @classmethod - def json_loads(cls, json_string): - """Deserialize from JSON document string.""" - try: - loads = json.loads(json_string) - except ValueError as error: - raise errors.DeserializationError(error) - return cls.from_json(loads) - - def json_dumps(self, **kwargs): - """Dump to JSON string using proper serializer. - - :returns: JSON document string. - :rtype: str - - """ - return json.dumps(self, default=self.json_dump_default, **kwargs) - - def json_dumps_pretty(self): - """Dump the object to pretty JSON document string. - - :rtype: str - - """ - return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': ')) - - @classmethod - def json_dump_default(cls, python_object): - """Serialize Python object. - - This function is meant to be passed as ``default`` to - :func:`json.dump` or :func:`json.dumps`. They call - ``default(python_object)`` only for non-basic Python types, so - this function necessarily raises :class:`TypeError` if - ``python_object`` is not an instance of - :class:`IJSONSerializable`. - - Please read the class docstring for more information. - - """ - if isinstance(python_object, JSONDeSerializable): - return python_object.to_partial_json() - else: # this branch is necessary, cannot just "return" - raise TypeError(repr(python_object) + ' is not JSON serializable') diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py deleted file mode 100644 index cf98ff371..000000000 --- a/acme/acme/jose/interfaces_test.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Tests for acme.jose.interfaces.""" -import unittest - - -class JSONDeSerializableTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.jose.interfaces import JSONDeSerializable - - # pylint: disable=missing-docstring,invalid-name - - class Basic(JSONDeSerializable): - def __init__(self, v): - self.v = v - - def to_partial_json(self): - return self.v - - @classmethod - def from_json(cls, jobj): - return cls(jobj) - - class Sequence(JSONDeSerializable): - def __init__(self, x, y): - self.x = x - self.y = y - - def to_partial_json(self): - return [self.x, self.y] - - @classmethod - def from_json(cls, jobj): - return cls( - Basic.from_json(jobj[0]), Basic.from_json(jobj[1])) - - class Mapping(JSONDeSerializable): - def __init__(self, x, y): - self.x = x - self.y = y - - def to_partial_json(self): - return {self.x: self.y} - - @classmethod - def from_json(cls, jobj): - pass # pragma: no cover - - self.basic1 = Basic('foo1') - self.basic2 = Basic('foo2') - self.seq = Sequence(self.basic1, self.basic2) - self.mapping = Mapping(self.basic1, self.basic2) - self.nested = Basic([[self.basic1]]) - self.tuple = Basic(('foo',)) - - # pylint: disable=invalid-name - self.Basic = Basic - self.Sequence = Sequence - self.Mapping = Mapping - - def test_to_json_sequence(self): - self.assertEqual(self.seq.to_json(), ['foo1', 'foo2']) - - def test_to_json_mapping(self): - self.assertEqual(self.mapping.to_json(), {'foo1': 'foo2'}) - - def test_to_json_other(self): - mock_value = object() - self.assertTrue(self.Basic(mock_value).to_json() is mock_value) - - def test_to_json_nested(self): - self.assertEqual(self.nested.to_json(), [['foo1']]) - - def test_to_json(self): - self.assertEqual(self.tuple.to_json(), (('foo', ))) - - def test_from_json_not_implemented(self): - from acme.jose.interfaces import JSONDeSerializable - self.assertRaises(TypeError, JSONDeSerializable.from_json, 'xxx') - - def test_json_loads(self): - seq = self.Sequence.json_loads('["foo1", "foo2"]') - self.assertTrue(isinstance(seq, self.Sequence)) - self.assertTrue(isinstance(seq.x, self.Basic)) - self.assertTrue(isinstance(seq.y, self.Basic)) - self.assertEqual(seq.x.v, 'foo1') - self.assertEqual(seq.y.v, 'foo2') - - def test_json_dumps(self): - self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps()) - - def test_json_dumps_pretty(self): - self.assertEqual(self.seq.json_dumps_pretty(), - '[\n "foo1",\n "foo2"\n]') - - def test_json_dump_default(self): - from acme.jose.interfaces import JSONDeSerializable - - self.assertEqual( - 'foo1', JSONDeSerializable.json_dump_default(self.basic1)) - - jobj = JSONDeSerializable.json_dump_default(self.seq) - self.assertEqual(len(jobj), 2) - self.assertTrue(jobj[0] is self.basic1) - self.assertTrue(jobj[1] is self.basic2) - - def test_json_dump_default_type_error(self): - from acme.jose.interfaces import JSONDeSerializable - self.assertRaises( - TypeError, JSONDeSerializable.json_dump_default, object()) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/json_util.py b/acme/acme/jose/json_util.py deleted file mode 100644 index 4baadda5e..000000000 --- a/acme/acme/jose/json_util.py +++ /dev/null @@ -1,485 +0,0 @@ -"""JSON (de)serialization framework. - -The framework presented here is somewhat based on `Go's "json" package`_ -(especially the ``omitempty`` functionality). - -.. _`Go's "json" package`: http://golang.org/pkg/encoding/json/ - -""" -import abc -import binascii -import logging - -import OpenSSL -import six - -from acme.jose import b64 -from acme.jose import errors -from acme.jose import interfaces -from acme.jose import util - - -logger = logging.getLogger(__name__) - - -class Field(object): - """JSON object field. - - :class:`Field` is meant to be used together with - :class:`JSONObjectWithFields`. - - ``encoder`` (``decoder``) is a callable that accepts a single - parameter, i.e. a value to be encoded (decoded), and returns the - serialized (deserialized) value. In case of errors it should raise - :class:`~acme.jose.errors.SerializationError` - (:class:`~acme.jose.errors.DeserializationError`). - - Note, that ``decoder`` should perform partial serialization only. - - :ivar str json_name: Name of the field when encoded to JSON. - :ivar default: Default value (used when not present in JSON object). - :ivar bool omitempty: If ``True`` and the field value is empty, then - it will not be included in the serialized JSON object, and - ``default`` will be used for deserialization. Otherwise, if ``False``, - field is considered as required, value will always be included in the - serialized JSON objected, and it must also be present when - deserializing. - - """ - __slots__ = ('json_name', 'default', 'omitempty', 'fdec', 'fenc') - - def __init__(self, json_name, default=None, omitempty=False, - decoder=None, encoder=None): - # pylint: disable=too-many-arguments - self.json_name = json_name - self.default = default - self.omitempty = omitempty - - self.fdec = self.default_decoder if decoder is None else decoder - self.fenc = self.default_encoder if encoder is None else encoder - - @classmethod - def _empty(cls, value): - """Is the provided value considered "empty" for this field? - - This is useful for subclasses that might want to override the - definition of being empty, e.g. for some more exotic data types. - - """ - return not isinstance(value, bool) and not value - - def omit(self, value): - """Omit the value in output?""" - return self._empty(value) and self.omitempty - - def _update_params(self, **kwargs): - current = dict(json_name=self.json_name, default=self.default, - omitempty=self.omitempty, - decoder=self.fdec, encoder=self.fenc) - current.update(kwargs) - return type(self)(**current) # pylint: disable=star-args - - def decoder(self, fdec): - """Descriptor to change the decoder on JSON object field.""" - return self._update_params(decoder=fdec) - - def encoder(self, fenc): - """Descriptor to change the encoder on JSON object field.""" - return self._update_params(encoder=fenc) - - def decode(self, value): - """Decode a value, optionally with context JSON object.""" - return self.fdec(value) - - def encode(self, value): - """Encode a value, optionally with context JSON object.""" - return self.fenc(value) - - @classmethod - def default_decoder(cls, value): - """Default decoder. - - Recursively deserialize into immutable types ( - :class:`acme.jose.util.frozendict` instead of - :func:`dict`, :func:`tuple` instead of :func:`list`). - - """ - # bases cases for different types returned by json.loads - if isinstance(value, list): - return tuple(cls.default_decoder(subvalue) for subvalue in value) - elif isinstance(value, dict): - return util.frozendict( - dict((cls.default_decoder(key), cls.default_decoder(value)) - for key, value in six.iteritems(value))) - else: # integer or string - return value - - @classmethod - def default_encoder(cls, value): - """Default (passthrough) encoder.""" - # field.to_partial_json() is no good as encoder has to do partial - # serialization only - return value - - -class JSONObjectWithFieldsMeta(abc.ABCMeta): - """Metaclass for :class:`JSONObjectWithFields` and its subclasses. - - It makes sure that, for any class ``cls`` with ``__metaclass__`` - set to ``JSONObjectWithFieldsMeta``: - - 1. All fields (attributes of type :class:`Field`) in the class - definition are moved to the ``cls._fields`` dictionary, where - keys are field attribute names and values are fields themselves. - - 2. ``cls.__slots__`` is extended by all field attribute names - (i.e. not :attr:`Field.json_name`). Original ``cls.__slots__`` - are stored in ``cls._orig_slots``. - - In a consequence, for a field attribute name ``some_field``, - ``cls.some_field`` will be a slot descriptor and not an instance - of :class:`Field`. For example:: - - some_field = Field('someField', default=()) - - class Foo(object): - __metaclass__ = JSONObjectWithFieldsMeta - __slots__ = ('baz',) - some_field = some_field - - assert Foo.__slots__ == ('some_field', 'baz') - assert Foo._orig_slots == () - assert Foo.some_field is not Field - - assert Foo._fields.keys() == ['some_field'] - assert Foo._fields['some_field'] is some_field - - As an implementation note, this metaclass inherits from - :class:`abc.ABCMeta` (and not the usual :class:`type`) to mitigate - the metaclass conflict (:class:`ImmutableMap` and - :class:`JSONDeSerializable`, parents of :class:`JSONObjectWithFields`, - use :class:`abc.ABCMeta` as its metaclass). - - """ - - def __new__(mcs, name, bases, dikt): - fields = {} - - for base in bases: - fields.update(getattr(base, '_fields', {})) - # Do not reorder, this class might override fields from base classes! - for key, value in tuple(six.iteritems(dikt)): - # not six.iterkeys() (in-place edit!) - if isinstance(value, Field): - fields[key] = dikt.pop(key) - - dikt['_orig_slots'] = dikt.get('__slots__', ()) - dikt['__slots__'] = tuple( - list(dikt['_orig_slots']) + list(six.iterkeys(fields))) - dikt['_fields'] = fields - - return abc.ABCMeta.__new__(mcs, name, bases, dikt) - - -@six.add_metaclass(JSONObjectWithFieldsMeta) -class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): - # pylint: disable=too-few-public-methods - """JSON object with fields. - - Example:: - - class Foo(JSONObjectWithFields): - bar = Field('Bar') - empty = Field('Empty', omitempty=True) - - @bar.encoder - def bar(value): - return value + 'bar' - - @bar.decoder - def bar(value): - if not value.endswith('bar'): - raise errors.DeserializationError('No bar suffix!') - return value[:-3] - - assert Foo(bar='baz').to_partial_json() == {'Bar': 'bazbar'} - assert Foo.from_json({'Bar': 'bazbar'}) == Foo(bar='baz') - assert (Foo.from_json({'Bar': 'bazbar', 'Empty': '!'}) - == Foo(bar='baz', empty='!')) - assert Foo(bar='baz').bar == 'baz' - - """ - - @classmethod - def _defaults(cls): - """Get default fields values.""" - return dict([(slot, field.default) for slot, field - in six.iteritems(cls._fields)]) - - def __init__(self, **kwargs): - # pylint: disable=star-args - super(JSONObjectWithFields, self).__init__( - **(dict(self._defaults(), **kwargs))) - - def encode(self, name): - """Encode a single field. - - :param str name: Name of the field to be encoded. - - :raises errors.SerializationError: if field cannot be serialized - :raises errors.Error: if field could not be found - - """ - try: - field = self._fields[name] - except KeyError: - raise errors.Error("Field not found: {0}".format(name)) - - return field.encode(getattr(self, name)) - - def fields_to_partial_json(self): - """Serialize fields to JSON.""" - jobj = {} - omitted = set() - for slot, field in six.iteritems(self._fields): - value = getattr(self, slot) - - if field.omit(value): - omitted.add((slot, value)) - else: - try: - jobj[field.json_name] = field.encode(value) - except errors.SerializationError as error: - raise errors.SerializationError( - 'Could not encode {0} ({1}): {2}'.format( - slot, value, error)) - return jobj - - def to_partial_json(self): - return self.fields_to_partial_json() - - @classmethod - def _check_required(cls, jobj): - missing = set() - for _, field in six.iteritems(cls._fields): - if not field.omitempty and field.json_name not in jobj: - missing.add(field.json_name) - - if missing: - raise errors.DeserializationError( - 'The following fields are required: {0}'.format( - ','.join(missing))) - - @classmethod - def fields_from_json(cls, jobj): - """Deserialize fields from JSON.""" - cls._check_required(jobj) - fields = {} - for slot, field in six.iteritems(cls._fields): - if field.json_name not in jobj and field.omitempty: - fields[slot] = field.default - else: - value = jobj[field.json_name] - try: - fields[slot] = field.decode(value) - except errors.DeserializationError as error: - raise errors.DeserializationError( - 'Could not decode {0!r} ({1!r}): {2}'.format( - slot, value, error)) - return fields - - @classmethod - def from_json(cls, jobj): - return cls(**cls.fields_from_json(jobj)) - - -def encode_b64jose(data): - """Encode JOSE Base-64 field. - - :param bytes data: - :rtype: `unicode` - - """ - # b64encode produces ASCII characters only - return b64.b64encode(data).decode('ascii') - - -def decode_b64jose(data, size=None, minimum=False): - """Decode JOSE Base-64 field. - - :param unicode data: - :param int size: Required length (after decoding). - :param bool minimum: If ``True``, then `size` will be treated as - minimum required length, as opposed to exact equality. - - :rtype: bytes - - """ - error_cls = TypeError if six.PY2 else binascii.Error - try: - decoded = b64.b64decode(data.encode()) - except error_cls as error: - raise errors.DeserializationError(error) - - if size is not None and ((not minimum and len(decoded) != size) or - (minimum and len(decoded) < size)): - raise errors.DeserializationError( - "Expected at least or exactly {0} bytes".format(size)) - - return decoded - - -def encode_hex16(value): - """Hexlify. - - :param bytes value: - :rtype: unicode - - """ - return binascii.hexlify(value).decode() - - -def decode_hex16(value, size=None, minimum=False): - """Decode hexlified field. - - :param unicode value: - :param int size: Required length (after decoding). - :param bool minimum: If ``True``, then `size` will be treated as - minimum required length, as opposed to exact equality. - - :rtype: bytes - - """ - value = value.encode() - if size is not None and ((not minimum and len(value) != size * 2) or - (minimum and len(value) < size * 2)): - raise errors.DeserializationError() - error_cls = TypeError if six.PY2 else binascii.Error - try: - return binascii.unhexlify(value) - except error_cls as error: - raise errors.DeserializationError(error) - - -def encode_cert(cert): - """Encode certificate as JOSE Base-64 DER. - - :type cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` - :rtype: unicode - - """ - return encode_b64jose(OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) - - -def decode_cert(b64der): - """Decode JOSE Base-64 DER-encoded certificate. - - :param unicode b64der: - :rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` - - """ - try: - return util.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der))) - except OpenSSL.crypto.Error as error: - raise errors.DeserializationError(error) - - -def encode_csr(csr): - """Encode CSR as JOSE Base-64 DER. - - :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - :rtype: unicode - - """ - return encode_b64jose(OpenSSL.crypto.dump_certificate_request( - OpenSSL.crypto.FILETYPE_ASN1, csr.wrapped)) - - -def decode_csr(b64der): - """Decode JOSE Base-64 DER-encoded CSR. - - :param unicode b64der: - :rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - - """ - try: - return util.ComparableX509(OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der))) - except OpenSSL.crypto.Error as error: - raise errors.DeserializationError(error) - - -class TypedJSONObjectWithFields(JSONObjectWithFields): - """JSON object with type.""" - - typ = NotImplemented - """Type of the object. Subclasses must override.""" - - type_field_name = "type" - """Field name used to distinguish different object types. - - Subclasses will probably have to override this. - - """ - - TYPES = NotImplemented - """Types registered for JSON deserialization""" - - @classmethod - def register(cls, type_cls, typ=None): - """Register class for JSON deserialization.""" - typ = type_cls.typ if typ is None else typ - cls.TYPES[typ] = type_cls - return type_cls - - @classmethod - def get_type_cls(cls, jobj): - """Get the registered class for ``jobj``.""" - if cls in six.itervalues(cls.TYPES): - if cls.type_field_name not in jobj: - raise errors.DeserializationError( - "Missing type field ({0})".format(cls.type_field_name)) - # cls is already registered type_cls, force to use it - # so that, e.g Revocation.from_json(jobj) fails if - # jobj["type"] != "revocation". - return cls - - if not isinstance(jobj, dict): - raise errors.DeserializationError( - "{0} is not a dictionary object".format(jobj)) - try: - typ = jobj[cls.type_field_name] - except KeyError: - raise errors.DeserializationError("missing type field") - - try: - return cls.TYPES[typ] - except KeyError: - raise errors.UnrecognizedTypeError(typ, jobj) - - def to_partial_json(self): - """Get JSON serializable object. - - :returns: Serializable JSON object representing ACME typed object. - :meth:`validate` will almost certainly not work, due to reasons - explained in :class:`acme.interfaces.IJSONSerializable`. - :rtype: dict - - """ - jobj = self.fields_to_partial_json() - jobj[self.type_field_name] = self.typ - return jobj - - @classmethod - def from_json(cls, jobj): - """Deserialize ACME object from valid JSON object. - - :raises acme.errors.UnrecognizedTypeError: if type - of the ACME object has not been registered. - - """ - # make sure subclasses don't cause infinite recursive from_json calls - type_cls = cls.get_type_cls(jobj) - return type_cls(**type_cls.fields_from_json(jobj)) diff --git a/acme/acme/jose/json_util_test.py b/acme/acme/jose/json_util_test.py deleted file mode 100644 index 25e36211e..000000000 --- a/acme/acme/jose/json_util_test.py +++ /dev/null @@ -1,381 +0,0 @@ -"""Tests for acme.jose.json_util.""" -import itertools -import unittest - -import mock -import six - -from acme import test_util - -from acme.jose import errors -from acme.jose import interfaces -from acme.jose import util - - -CERT = test_util.load_comparable_cert('cert.pem') -CSR = test_util.load_comparable_csr('csr.pem') - - -class FieldTest(unittest.TestCase): - """Tests for acme.jose.json_util.Field.""" - - def test_no_omit_boolean(self): - from acme.jose.json_util import Field - for default, omitempty, value in itertools.product( - [True, False], [True, False], [True, False]): - self.assertFalse( - Field("foo", default=default, omitempty=omitempty).omit(value)) - - def test_descriptors(self): - mock_value = mock.MagicMock() - - # pylint: disable=missing-docstring - - def decoder(unused_value): - return 'd' - - def encoder(unused_value): - return 'e' - - from acme.jose.json_util import Field - field = Field('foo') - - field = field.encoder(encoder) - self.assertEqual('e', field.encode(mock_value)) - - field = field.decoder(decoder) - self.assertEqual('e', field.encode(mock_value)) - self.assertEqual('d', field.decode(mock_value)) - - def test_default_encoder_is_partial(self): - class MockField(interfaces.JSONDeSerializable): - # pylint: disable=missing-docstring - def to_partial_json(self): - return 'foo' # pragma: no cover - - @classmethod - def from_json(cls, jobj): - pass # pragma: no cover - mock_field = MockField() - - from acme.jose.json_util import Field - self.assertTrue(Field.default_encoder(mock_field) is mock_field) - # in particular... - self.assertNotEqual('foo', Field.default_encoder(mock_field)) - - def test_default_encoder_passthrough(self): - mock_value = mock.MagicMock() - from acme.jose.json_util import Field - self.assertTrue(Field.default_encoder(mock_value) is mock_value) - - def test_default_decoder_list_to_tuple(self): - from acme.jose.json_util import Field - self.assertEqual((1, 2, 3), Field.default_decoder([1, 2, 3])) - - def test_default_decoder_dict_to_frozendict(self): - from acme.jose.json_util import Field - obj = Field.default_decoder({'x': 2}) - self.assertTrue(isinstance(obj, util.frozendict)) - self.assertEqual(obj, util.frozendict(x=2)) - - def test_default_decoder_passthrough(self): - mock_value = mock.MagicMock() - from acme.jose.json_util import Field - self.assertTrue(Field.default_decoder(mock_value) is mock_value) - - -class JSONObjectWithFieldsMetaTest(unittest.TestCase): - """Tests for acme.jose.json_util.JSONObjectWithFieldsMeta.""" - - def setUp(self): - from acme.jose.json_util import Field - from acme.jose.json_util import JSONObjectWithFieldsMeta - self.field = Field('Baz') - self.field2 = Field('Baz2') - # pylint: disable=invalid-name,missing-docstring,too-few-public-methods - # pylint: disable=blacklisted-name - - @six.add_metaclass(JSONObjectWithFieldsMeta) - class A(object): - __slots__ = ('bar',) - baz = self.field - - class B(A): - pass - - class C(A): - baz = self.field2 - - self.a_cls = A - self.b_cls = B - self.c_cls = C - - def test_fields(self): - # pylint: disable=protected-access,no-member - self.assertEqual({'baz': self.field}, self.a_cls._fields) - self.assertEqual({'baz': self.field}, self.b_cls._fields) - - def test_fields_inheritance(self): - # pylint: disable=protected-access,no-member - self.assertEqual({'baz': self.field2}, self.c_cls._fields) - - def test_slots(self): - self.assertEqual(('bar', 'baz'), self.a_cls.__slots__) - self.assertEqual(('baz',), self.b_cls.__slots__) - - def test_orig_slots(self): - # pylint: disable=protected-access,no-member - self.assertEqual(('bar',), self.a_cls._orig_slots) - self.assertEqual((), self.b_cls._orig_slots) - - -class JSONObjectWithFieldsTest(unittest.TestCase): - """Tests for acme.jose.json_util.JSONObjectWithFields.""" - # pylint: disable=protected-access - - def setUp(self): - from acme.jose.json_util import JSONObjectWithFields - from acme.jose.json_util import Field - - class MockJSONObjectWithFields(JSONObjectWithFields): - # pylint: disable=invalid-name,missing-docstring,no-self-argument - # pylint: disable=too-few-public-methods - x = Field('x', omitempty=True, - encoder=(lambda x: x * 2), - decoder=(lambda x: x / 2)) - y = Field('y') - z = Field('Z') # on purpose uppercase - - @y.encoder - def y(value): - if value == 500: - raise errors.SerializationError() - return value - - @y.decoder - def y(value): - if value == 500: - raise errors.DeserializationError() - return value - - # pylint: disable=invalid-name - self.MockJSONObjectWithFields = MockJSONObjectWithFields - self.mock = MockJSONObjectWithFields(x=None, y=2, z=3) - - def test_init_defaults(self): - self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3)) - - def test_encode(self): - self.assertEqual(10, self.MockJSONObjectWithFields( - x=5, y=0, z=0).encode("x")) - - def test_encode_wrong_field(self): - self.assertRaises(errors.Error, self.mock.encode, 'foo') - - def test_encode_serialization_error_passthrough(self): - self.assertRaises( - errors.SerializationError, - self.MockJSONObjectWithFields(y=500, z=None).encode, "y") - - def test_fields_to_partial_json_omits_empty(self): - self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3}) - - def test_fields_from_json_fills_default_for_empty(self): - self.assertEqual( - {'x': None, 'y': 2, 'z': 3}, - self.MockJSONObjectWithFields.fields_from_json({'y': 2, 'Z': 3})) - - def test_fields_from_json_fails_on_missing(self): - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'y': 0}) - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'Z': 0}) - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'y': 0}) - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'Z': 0}) - - def test_fields_to_partial_json_encoder(self): - self.assertEqual( - self.MockJSONObjectWithFields(x=1, y=2, z=3).to_partial_json(), - {'x': 2, 'y': 2, 'Z': 3}) - - def test_fields_from_json_decoder(self): - self.assertEqual( - {'x': 2, 'y': 2, 'z': 3}, - self.MockJSONObjectWithFields.fields_from_json( - {'x': 4, 'y': 2, 'Z': 3})) - - def test_fields_to_partial_json_error_passthrough(self): - self.assertRaises( - errors.SerializationError, self.MockJSONObjectWithFields( - x=1, y=500, z=3).to_partial_json) - - def test_fields_from_json_error_passthrough(self): - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.from_json, - {'x': 4, 'y': 500, 'Z': 3}) - - -class DeEncodersTest(unittest.TestCase): - def setUp(self): - self.b64_cert = ( - u'MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhM' - u'CVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKz' - u'ApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxF' - u'DASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIx' - u'ODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRI' - u'wEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTW' - u'ljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwD' - u'QYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1' - u'AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwE' - u'AATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMnd' - u'fk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o' - ) - self.b64_csr = ( - u'MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2F' - u'uMRIwEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECw' - u'wWVW5pdmVyc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb' - u'20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD_N_HW9As' - u'dRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3' - u'C5QIDAQABoCkwJwYJKoZIhvcNAQkOMRowGDAWBgNVHREEDzANggtleGFtcG' - u'xlLmNvbTANBgkqhkiG9w0BAQsFAANBAHJH_O6BtC9aGzEVCMGOZ7z9iIRHW' - u'Szr9x_bOzn7hLwsbXPAgO1QxEwL-X-4g20Gn9XBE1N9W6HCIEut2d8wACg' - ) - - def test_encode_b64jose(self): - from acme.jose.json_util import encode_b64jose - encoded = encode_b64jose(b'x') - self.assertTrue(isinstance(encoded, six.string_types)) - self.assertEqual(u'eA', encoded) - - def test_decode_b64jose(self): - from acme.jose.json_util import decode_b64jose - decoded = decode_b64jose(u'eA') - self.assertTrue(isinstance(decoded, six.binary_type)) - self.assertEqual(b'x', decoded) - - def test_decode_b64jose_padding_error(self): - from acme.jose.json_util import decode_b64jose - self.assertRaises(errors.DeserializationError, decode_b64jose, u'x') - - def test_decode_b64jose_size(self): - from acme.jose.json_util import decode_b64jose - self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3)) - self.assertRaises( - errors.DeserializationError, decode_b64jose, u'Zm9v', size=2) - self.assertRaises( - errors.DeserializationError, decode_b64jose, u'Zm9v', size=4) - - def test_decode_b64jose_minimum_size(self): - from acme.jose.json_util import decode_b64jose - self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3, minimum=True)) - self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=2, minimum=True)) - self.assertRaises(errors.DeserializationError, decode_b64jose, - u'Zm9v', size=4, minimum=True) - - def test_encode_hex16(self): - from acme.jose.json_util import encode_hex16 - encoded = encode_hex16(b'foo') - self.assertEqual(u'666f6f', encoded) - self.assertTrue(isinstance(encoded, six.string_types)) - - def test_decode_hex16(self): - from acme.jose.json_util import decode_hex16 - decoded = decode_hex16(u'666f6f') - self.assertEqual(b'foo', decoded) - self.assertTrue(isinstance(decoded, six.binary_type)) - - def test_decode_hex16_minimum_size(self): - from acme.jose.json_util import decode_hex16 - self.assertEqual(b'foo', decode_hex16(u'666f6f', size=3, minimum=True)) - self.assertEqual(b'foo', decode_hex16(u'666f6f', size=2, minimum=True)) - self.assertRaises(errors.DeserializationError, decode_hex16, - u'666f6f', size=4, minimum=True) - - def test_decode_hex16_odd_length(self): - from acme.jose.json_util import decode_hex16 - self.assertRaises(errors.DeserializationError, decode_hex16, u'x') - - def test_encode_cert(self): - from acme.jose.json_util import encode_cert - self.assertEqual(self.b64_cert, encode_cert(CERT)) - - def test_decode_cert(self): - from acme.jose.json_util import decode_cert - cert = decode_cert(self.b64_cert) - self.assertTrue(isinstance(cert, util.ComparableX509)) - self.assertEqual(cert, CERT) - self.assertRaises(errors.DeserializationError, decode_cert, u'') - - def test_encode_csr(self): - from acme.jose.json_util import encode_csr - self.assertEqual(self.b64_csr, encode_csr(CSR)) - - def test_decode_csr(self): - from acme.jose.json_util import decode_csr - csr = decode_csr(self.b64_csr) - self.assertTrue(isinstance(csr, util.ComparableX509)) - self.assertEqual(csr, CSR) - self.assertRaises(errors.DeserializationError, decode_csr, u'') - - -class TypedJSONObjectWithFieldsTest(unittest.TestCase): - - def setUp(self): - from acme.jose.json_util import TypedJSONObjectWithFields - - # pylint: disable=missing-docstring,abstract-method - # pylint: disable=too-few-public-methods - - class MockParentTypedJSONObjectWithFields(TypedJSONObjectWithFields): - TYPES = {} - type_field_name = 'type' - - @MockParentTypedJSONObjectWithFields.register - class MockTypedJSONObjectWithFields( - MockParentTypedJSONObjectWithFields): - typ = 'test' - __slots__ = ('foo',) - - @classmethod - def fields_from_json(cls, jobj): - return {'foo': jobj['foo']} - - def fields_to_partial_json(self): - return {'foo': self.foo} - - self.parent_cls = MockParentTypedJSONObjectWithFields - self.msg = MockTypedJSONObjectWithFields(foo='bar') - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), { - 'type': 'test', - 'foo': 'bar', - }) - - def test_from_json_non_dict_fails(self): - for value in [[], (), 5, "asd"]: # all possible input types - self.assertRaises( - errors.DeserializationError, self.parent_cls.from_json, value) - - def test_from_json_dict_no_type_fails(self): - self.assertRaises( - errors.DeserializationError, self.parent_cls.from_json, {}) - - def test_from_json_unknown_type_fails(self): - self.assertRaises(errors.UnrecognizedTypeError, - self.parent_cls.from_json, {'type': 'bar'}) - - def test_from_json_returns_obj(self): - self.assertEqual({'foo': 'bar'}, self.parent_cls.from_json( - {'type': 'test', 'foo': 'bar'})) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/jwa.py b/acme/acme/jose/jwa.py deleted file mode 100644 index 9b682ecab..000000000 --- a/acme/acme/jose/jwa.py +++ /dev/null @@ -1,180 +0,0 @@ -"""JSON Web Algorithm. - -https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 - -""" -import abc -import collections -import logging - -import cryptography.exceptions -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes # type: ignore -from cryptography.hazmat.primitives import hmac # type: ignore -from cryptography.hazmat.primitives.asymmetric import padding # type: ignore - -from acme.jose import errors -from acme.jose import interfaces -from acme.jose import jwk - - -logger = logging.getLogger(__name__) - - -class JWA(interfaces.JSONDeSerializable): # pylint: disable=abstract-method - # pylint: disable=too-few-public-methods - # for some reason disable=abstract-method has to be on the line - # above... - """JSON Web Algorithm.""" - - -class JWASignature(JWA, collections.Hashable): # type: ignore - """JSON Web Signature Algorithm.""" - SIGNATURES = {} # type: dict - - def __init__(self, name): - self.name = name - - def __eq__(self, other): - if not isinstance(other, JWASignature): - return NotImplemented - return self.name == other.name - - def __hash__(self): - return hash((self.__class__, self.name)) - - def __ne__(self, other): - return not self == other - - @classmethod - def register(cls, signature_cls): - """Register class for JSON deserialization.""" - cls.SIGNATURES[signature_cls.name] = signature_cls - return signature_cls - - def to_partial_json(self): - return self.name - - @classmethod - def from_json(cls, jobj): - return cls.SIGNATURES[jobj] - - @abc.abstractmethod - def sign(self, key, msg): # pragma: no cover - """Sign the ``msg`` using ``key``.""" - raise NotImplementedError() - - @abc.abstractmethod - def verify(self, key, msg, sig): # pragma: no cover - """Verify the ``msg` and ``sig`` using ``key``.""" - raise NotImplementedError() - - def __repr__(self): - return self.name - - -class _JWAHS(JWASignature): - - kty = jwk.JWKOct - - def __init__(self, name, hash_): - super(_JWAHS, self).__init__(name) - self.hash = hash_() - - def sign(self, key, msg): - signer = hmac.HMAC(key, self.hash, backend=default_backend()) - signer.update(msg) - return signer.finalize() - - def verify(self, key, msg, sig): - verifier = hmac.HMAC(key, self.hash, backend=default_backend()) - verifier.update(msg) - try: - verifier.verify(sig) - except cryptography.exceptions.InvalidSignature as error: - logger.debug(error, exc_info=True) - return False - else: - return True - - -class _JWARSA(object): - - kty = jwk.JWKRSA - padding = NotImplemented - hash = NotImplemented - - def sign(self, key, msg): - """Sign the ``msg`` using ``key``.""" - try: - signer = key.signer(self.padding, self.hash) - except AttributeError as error: - logger.debug(error, exc_info=True) - raise errors.Error("Public key cannot be used for signing") - except ValueError as error: # digest too large - logger.debug(error, exc_info=True) - raise errors.Error(str(error)) - signer.update(msg) - try: - return signer.finalize() - except ValueError as error: - logger.debug(error, exc_info=True) - raise errors.Error(str(error)) - - def verify(self, key, msg, sig): - """Verify the ``msg` and ``sig`` using ``key``.""" - verifier = key.verifier(sig, self.padding, self.hash) - verifier.update(msg) - try: - verifier.verify() - except cryptography.exceptions.InvalidSignature as error: - logger.debug(error, exc_info=True) - return False - else: - return True - - -class _JWARS(_JWARSA, JWASignature): - - def __init__(self, name, hash_): - super(_JWARS, self).__init__(name) - self.padding = padding.PKCS1v15() - self.hash = hash_() - - -class _JWAPS(_JWARSA, JWASignature): - - def __init__(self, name, hash_): - super(_JWAPS, self).__init__(name) - self.padding = padding.PSS( - mgf=padding.MGF1(hash_()), - salt_length=padding.PSS.MAX_LENGTH) - self.hash = hash_() - - -class _JWAES(JWASignature): # pylint: disable=abstract-class-not-used - - # TODO: implement ES signatures - - def sign(self, key, msg): # pragma: no cover - raise NotImplementedError() - - def verify(self, key, msg, sig): # pragma: no cover - raise NotImplementedError() - - -HS256 = JWASignature.register(_JWAHS('HS256', hashes.SHA256)) -HS384 = JWASignature.register(_JWAHS('HS384', hashes.SHA384)) -HS512 = JWASignature.register(_JWAHS('HS512', hashes.SHA512)) - -RS256 = JWASignature.register(_JWARS('RS256', hashes.SHA256)) -RS384 = JWASignature.register(_JWARS('RS384', hashes.SHA384)) -RS512 = JWASignature.register(_JWARS('RS512', hashes.SHA512)) - -PS256 = JWASignature.register(_JWAPS('PS256', hashes.SHA256)) -PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384)) -PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512)) - -ES256 = JWASignature.register(_JWAES('ES256')) -ES384 = JWASignature.register(_JWAES('ES384')) -ES512 = JWASignature.register(_JWAES('ES512')) diff --git a/acme/acme/jose/jwa_test.py b/acme/acme/jose/jwa_test.py deleted file mode 100644 index 3328d083a..000000000 --- a/acme/acme/jose/jwa_test.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Tests for acme.jose.jwa.""" -import unittest - -from acme import test_util - -from acme.jose import errors - - -RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') -RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') -RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem') - - -class JWASignatureTest(unittest.TestCase): - """Tests for acme.jose.jwa.JWASignature.""" - - def setUp(self): - from acme.jose.jwa import JWASignature - - class MockSig(JWASignature): - # pylint: disable=missing-docstring,too-few-public-methods - # pylint: disable=abstract-class-not-used - def sign(self, key, msg): - raise NotImplementedError() # pragma: no cover - - def verify(self, key, msg, sig): - raise NotImplementedError() # pragma: no cover - - # pylint: disable=invalid-name - self.Sig1 = MockSig('Sig1') - self.Sig2 = MockSig('Sig2') - - def test_eq(self): - self.assertEqual(self.Sig1, self.Sig1) - - def test_ne(self): - self.assertNotEqual(self.Sig1, self.Sig2) - - def test_ne_other_type(self): - self.assertNotEqual(self.Sig1, 5) - - def test_repr(self): - self.assertEqual('Sig1', repr(self.Sig1)) - self.assertEqual('Sig2', repr(self.Sig2)) - - def test_to_partial_json(self): - self.assertEqual(self.Sig1.to_partial_json(), 'Sig1') - self.assertEqual(self.Sig2.to_partial_json(), 'Sig2') - - def test_from_json(self): - from acme.jose.jwa import JWASignature - from acme.jose.jwa import RS256 - self.assertTrue(JWASignature.from_json('RS256') is RS256) - - -class JWAHSTest(unittest.TestCase): # pylint: disable=too-few-public-methods - - def test_it(self): - from acme.jose.jwa import HS256 - sig = ( - b"\xceR\xea\xcd\x94\xab\xcf\xfb\xe0\xacA.:\x1a'\x08i\xe2\xc4" - b"\r\x85+\x0e\x85\xaeUZ\xd4\xb3\x97zO" - ) - self.assertEqual(HS256.sign(b'some key', b'foo'), sig) - self.assertTrue(HS256.verify(b'some key', b'foo', sig) is True) - self.assertTrue(HS256.verify(b'some key', b'foo', sig + b'!') is False) - - -class JWARSTest(unittest.TestCase): - - def test_sign_no_private_part(self): - from acme.jose.jwa import RS256 - self.assertRaises( - errors.Error, RS256.sign, RSA512_KEY.public_key(), b'foo') - - def test_sign_key_too_small(self): - from acme.jose.jwa import RS256 - from acme.jose.jwa import PS256 - self.assertRaises(errors.Error, RS256.sign, RSA256_KEY, b'foo') - self.assertRaises(errors.Error, PS256.sign, RSA256_KEY, b'foo') - - def test_rs(self): - from acme.jose.jwa import RS256 - sig = ( - b'|\xc6\xb2\xa4\xab(\x87\x99\xfa*:\xea\xf8\xa0N&}\x9f\x0f\xc0O' - b'\xc6t\xa3\xe6\xfa\xbb"\x15Y\x80Y\xe0\x81\xb8\x88)\xba\x0c\x9c' - b'\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99' - b'\xd2\xb9.>}\xfd' - ) - self.assertEqual(RS256.sign(RSA512_KEY, b'foo'), sig) - self.assertTrue(RS256.verify(RSA512_KEY.public_key(), b'foo', sig)) - self.assertFalse(RS256.verify( - RSA512_KEY.public_key(), b'foo', sig + b'!')) - - def test_ps(self): - from acme.jose.jwa import PS256 - sig = PS256.sign(RSA1024_KEY, b'foo') - self.assertTrue(PS256.verify(RSA1024_KEY.public_key(), b'foo', sig)) - self.assertFalse(PS256.verify( - RSA1024_KEY.public_key(), b'foo', sig + b'!')) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py deleted file mode 100644 index 54423f670..000000000 --- a/acme/acme/jose/jwk.py +++ /dev/null @@ -1,281 +0,0 @@ -"""JSON Web Key.""" -import abc -import binascii -import json -import logging - -import cryptography.exceptions -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes # type: ignore -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec # type: ignore -from cryptography.hazmat.primitives.asymmetric import rsa - -import six - -from acme.jose import errors -from acme.jose import json_util -from acme.jose import util - - -logger = logging.getLogger(__name__) - - -class JWK(json_util.TypedJSONObjectWithFields): - # pylint: disable=too-few-public-methods - """JSON Web Key.""" - type_field_name = 'kty' - TYPES = {} # type: dict - cryptography_key_types = () # type: tuple - """Subclasses should override.""" - - required = NotImplemented - """Required members of public key's representation as defined by JWK/JWA.""" - - _thumbprint_json_dumps_params = { - # "no whitespace or line breaks before or after any syntactic - # elements" - 'indent': None, - 'separators': (',', ':'), - # "members ordered lexicographically by the Unicode [UNICODE] - # code points of the member names" - 'sort_keys': True, - } - - def thumbprint(self, hash_function=hashes.SHA256): - """Compute JWK Thumbprint. - - https://tools.ietf.org/html/rfc7638 - - :returns bytes: - - """ - digest = hashes.Hash(hash_function(), backend=default_backend()) - digest.update(json.dumps( - dict((k, v) for k, v in six.iteritems(self.to_json()) - if k in self.required), - **self._thumbprint_json_dumps_params).encode()) - return digest.finalize() - - @abc.abstractmethod - def public_key(self): # pragma: no cover - """Generate JWK with public key. - - For symmetric cryptosystems, this would return ``self``. - - """ - raise NotImplementedError() - - @classmethod - def _load_cryptography_key(cls, data, password=None, backend=None): - backend = default_backend() if backend is None else backend - exceptions = {} - - # private key? - for loader in (serialization.load_pem_private_key, - serialization.load_der_private_key): - try: - return loader(data, password, backend) - except (ValueError, TypeError, - cryptography.exceptions.UnsupportedAlgorithm) as error: - exceptions[loader] = error - - # public key? - for loader in (serialization.load_pem_public_key, - serialization.load_der_public_key): - try: - return loader(data, backend) - except (ValueError, - cryptography.exceptions.UnsupportedAlgorithm) as error: - exceptions[loader] = error - - # no luck - raise errors.Error('Unable to deserialize key: {0}'.format(exceptions)) - - @classmethod - def load(cls, data, password=None, backend=None): - """Load serialized key as JWK. - - :param str data: Public or private key serialized as PEM or DER. - :param str password: Optional password. - :param backend: A `.PEMSerializationBackend` and - `.DERSerializationBackend` provider. - - :raises errors.Error: if unable to deserialize, or unsupported - JWK algorithm - - :returns: JWK of an appropriate type. - :rtype: `JWK` - - """ - try: - key = cls._load_cryptography_key(data, password, backend) - except errors.Error as error: - logger.debug('Loading symmetric key, asymmetric failed: %s', error) - return JWKOct(key=data) - - if cls.typ is not NotImplemented and not isinstance( - key, cls.cryptography_key_types): - raise errors.Error('Unable to deserialize {0} into {1}'.format( - key.__class__, cls.__class__)) - for jwk_cls in six.itervalues(cls.TYPES): - if isinstance(key, jwk_cls.cryptography_key_types): - return jwk_cls(key=key) - raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__)) - - -@JWK.register -class JWKES(JWK): # pragma: no cover - # pylint: disable=abstract-class-not-used - """ES JWK. - - .. warning:: This is not yet implemented! - - """ - typ = 'ES' - cryptography_key_types = ( - ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) - required = ('crv', JWK.type_field_name, 'x', 'y') - - def fields_to_partial_json(self): - raise NotImplementedError() - - @classmethod - def fields_from_json(cls, jobj): - raise NotImplementedError() - - def public_key(self): - raise NotImplementedError() - - -@JWK.register -class JWKOct(JWK): - """Symmetric JWK.""" - typ = 'oct' - __slots__ = ('key',) - required = ('k', JWK.type_field_name) - - def fields_to_partial_json(self): - # TODO: An "alg" member SHOULD also be present to identify the - # algorithm intended to be used with the key, unless the - # application uses another means or convention to determine - # the algorithm used. - return {'k': json_util.encode_b64jose(self.key)} - - @classmethod - def fields_from_json(cls, jobj): - return cls(key=json_util.decode_b64jose(jobj['k'])) - - def public_key(self): - return self - - -@JWK.register -class JWKRSA(JWK): - """RSA JWK. - - :ivar key: `cryptography.hazmat.primitives.rsa.RSAPrivateKey` - or `cryptography.hazmat.primitives.rsa.RSAPublicKey` wrapped - in `.ComparableRSAKey` - - """ - typ = 'RSA' - cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) - __slots__ = ('key',) - required = ('e', JWK.type_field_name, 'n') - - def __init__(self, *args, **kwargs): - if 'key' in kwargs and not isinstance( - kwargs['key'], util.ComparableRSAKey): - kwargs['key'] = util.ComparableRSAKey(kwargs['key']) - super(JWKRSA, self).__init__(*args, **kwargs) - - @classmethod - def _encode_param(cls, data): - """Encode Base64urlUInt. - - :type data: long - :rtype: unicode - - """ - def _leading_zeros(arg): - if len(arg) % 2: - return '0' + arg - return arg - - return json_util.encode_b64jose(binascii.unhexlify( - _leading_zeros(hex(data)[2:].rstrip('L')))) - - @classmethod - def _decode_param(cls, data): - """Decode Base64urlUInt.""" - try: - return int(binascii.hexlify(json_util.decode_b64jose(data)), 16) - except ValueError: # invalid literal for long() with base 16 - raise errors.DeserializationError() - - def public_key(self): - return type(self)(key=self.key.public_key()) - - @classmethod - def fields_from_json(cls, jobj): - # pylint: disable=invalid-name - n, e = (cls._decode_param(jobj[x]) for x in ('n', 'e')) - public_numbers = rsa.RSAPublicNumbers(e=e, n=n) - if 'd' not in jobj: # public key - key = public_numbers.public_key(default_backend()) - else: # private key - d = cls._decode_param(jobj['d']) - if ('p' in jobj or 'q' in jobj or 'dp' in jobj or - 'dq' in jobj or 'qi' in jobj or 'oth' in jobj): - # "If the producer includes any of the other private - # key parameters, then all of the others MUST be - # present, with the exception of "oth", which MUST - # only be present when more than two prime factors - # were used." - p, q, dp, dq, qi, = all_params = tuple( - jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi')) - if tuple(param for param in all_params if param is None): - raise errors.Error( - 'Some private parameters are missing: {0}'.format( - all_params)) - p, q, dp, dq, qi = tuple( - cls._decode_param(x) for x in all_params) - - # TODO: check for oth - else: - # cryptography>=0.8 - p, q = rsa.rsa_recover_prime_factors(n, e, d) - dp = rsa.rsa_crt_dmp1(d, p) - dq = rsa.rsa_crt_dmq1(d, q) - qi = rsa.rsa_crt_iqmp(p, q) - - key = rsa.RSAPrivateNumbers( - p, q, d, dp, dq, qi, public_numbers).private_key( - default_backend()) - - return cls(key=key) - - def fields_to_partial_json(self): - # pylint: disable=protected-access - if isinstance(self.key._wrapped, rsa.RSAPublicKey): - numbers = self.key.public_numbers() - params = { - 'n': numbers.n, - 'e': numbers.e, - } - else: # rsa.RSAPrivateKey - private = self.key.private_numbers() - public = self.key.public_key().public_numbers() - params = { - 'n': public.n, - 'e': public.e, - 'd': private.d, - 'p': private.p, - 'q': private.q, - 'dp': private.dmp1, - 'dq': private.dmq1, - 'qi': private.iqmp, - } - return dict((key, self._encode_param(value)) - for key, value in six.iteritems(params)) diff --git a/acme/acme/jose/jwk_test.py b/acme/acme/jose/jwk_test.py deleted file mode 100644 index eea5793bf..000000000 --- a/acme/acme/jose/jwk_test.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Tests for acme.jose.jwk.""" -import binascii -import unittest - -from acme import test_util - -from acme.jose import errors -from acme.jose import json_util -from acme.jose import util - - -DSA_PEM = test_util.load_vector('dsa512_key.pem') -RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') -RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') - - -class JWKTest(unittest.TestCase): - """Tests for acme.jose.jwk.JWK.""" - - def test_load(self): - from acme.jose.jwk import JWK - self.assertRaises(errors.Error, JWK.load, DSA_PEM) - - def test_load_subclass_wrong_type(self): - from acme.jose.jwk import JWKRSA - self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM) - - -class JWKTestBaseMixin(object): - """Mixin test for JWK subclass tests.""" - - thumbprint = NotImplemented - - def test_thumbprint_private(self): - self.assertEqual(self.thumbprint, self.jwk.thumbprint()) - - def test_thumbprint_public(self): - self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint()) - - -class JWKOctTest(unittest.TestCase, JWKTestBaseMixin): - """Tests for acme.jose.jwk.JWKOct.""" - - thumbprint = (b"\xf3\xe7\xbe\xa8`\xd2\xdap\xe9}\x9c\xce>" - b"\xd0\xfcI\xbe\xcd\x92'\xd4o\x0e\xf41\xea" - b"\x8e(\x8a\xb2i\x1c") - - def setUp(self): - from acme.jose.jwk import JWKOct - self.jwk = JWKOct(key=b'foo') - self.jobj = {'kty': 'oct', 'k': json_util.encode_b64jose(b'foo')} - - def test_to_partial_json(self): - self.assertEqual(self.jwk.to_partial_json(), self.jobj) - - def test_from_json(self): - from acme.jose.jwk import JWKOct - self.assertEqual(self.jwk, JWKOct.from_json(self.jobj)) - - def test_from_json_hashable(self): - from acme.jose.jwk import JWKOct - hash(JWKOct.from_json(self.jobj)) - - def test_load(self): - from acme.jose.jwk import JWKOct - self.assertEqual(self.jwk, JWKOct.load(b'foo')) - - def test_public_key(self): - self.assertTrue(self.jwk.public_key() is self.jwk) - - -class JWKRSATest(unittest.TestCase, JWKTestBaseMixin): - """Tests for acme.jose.jwk.JWKRSA.""" - # pylint: disable=too-many-instance-attributes - - thumbprint = (b'\x83K\xdc#3\x98\xca\x98\xed\xcb\x80\x80<\x0c' - b'\xf0\x95\xb9H\xb2*l\xbd$\xe5&|O\x91\xd4 \xb0Y') - - def setUp(self): - from acme.jose.jwk import JWKRSA - self.jwk256 = JWKRSA(key=RSA256_KEY.public_key()) - self.jwk256json = { - 'kty': 'RSA', - 'e': 'AQAB', - 'n': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk', - } - # pylint: disable=protected-access - self.jwk256_not_comparable = JWKRSA( - key=RSA256_KEY.public_key()._wrapped) - self.jwk512 = JWKRSA(key=RSA512_KEY.public_key()) - self.jwk512json = { - 'kty': 'RSA', - 'e': 'AQAB', - 'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5' - '80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q', - } - self.private = JWKRSA(key=RSA256_KEY) - self.private_json_small = self.jwk256json.copy() - self.private_json_small['d'] = ( - 'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE') - self.private_json = self.jwk256json.copy() - self.private_json.update({ - 'd': 'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE', - 'p': 'zUVNZn4lLLBD1R6NE8TKNQ', - 'q': 'wcfKfc7kl5jfqXArCRSURQ', - 'dp': 'CWJFq43QvT5Bm5iN8n1okQ', - 'dq': 'bHh2u7etM8LKKCF2pY2UdQ', - 'qi': 'oi45cEkbVoJjAbnQpFY87Q', - }) - self.jwk = self.private - - def test_init_auto_comparable(self): - self.assertTrue(isinstance( - self.jwk256_not_comparable.key, util.ComparableRSAKey)) - self.assertEqual(self.jwk256, self.jwk256_not_comparable) - - def test_encode_param_zero(self): - from acme.jose.jwk import JWKRSA - # pylint: disable=protected-access - # TODO: move encode/decode _param to separate class - self.assertEqual('AA', JWKRSA._encode_param(0)) - - def test_equals(self): - self.assertEqual(self.jwk256, self.jwk256) - self.assertEqual(self.jwk512, self.jwk512) - - def test_not_equals(self): - self.assertNotEqual(self.jwk256, self.jwk512) - self.assertNotEqual(self.jwk512, self.jwk256) - - def test_load(self): - from acme.jose.jwk import JWKRSA - self.assertEqual(self.private, JWKRSA.load( - test_util.load_vector('rsa256_key.pem'))) - - def test_public_key(self): - self.assertEqual(self.jwk256, self.private.public_key()) - - def test_to_partial_json(self): - self.assertEqual(self.jwk256.to_partial_json(), self.jwk256json) - self.assertEqual(self.jwk512.to_partial_json(), self.jwk512json) - self.assertEqual(self.private.to_partial_json(), self.private_json) - - def test_from_json(self): - from acme.jose.jwk import JWK - self.assertEqual( - self.jwk256, JWK.from_json(self.jwk256json)) - self.assertEqual( - self.jwk512, JWK.from_json(self.jwk512json)) - self.assertEqual(self.private, JWK.from_json(self.private_json)) - - def test_from_json_private_small(self): - from acme.jose.jwk import JWK - self.assertEqual(self.private, JWK.from_json(self.private_json_small)) - - def test_from_json_missing_one_additional(self): - from acme.jose.jwk import JWK - del self.private_json['q'] - self.assertRaises(errors.Error, JWK.from_json, self.private_json) - - def test_from_json_hashable(self): - from acme.jose.jwk import JWK - hash(JWK.from_json(self.jwk256json)) - - def test_from_json_non_schema_errors(self): - # valid against schema, but still failing - from acme.jose.jwk import JWK - self.assertRaises(errors.DeserializationError, JWK.from_json, - {'kty': 'RSA', 'e': 'AQAB', 'n': ''}) - self.assertRaises(errors.DeserializationError, JWK.from_json, - {'kty': 'RSA', 'e': 'AQAB', 'n': '1'}) - - def test_thumbprint_go_jose(self): - # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk.go#L155 - # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L331-L344 - # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L384 - from acme.jose.jwk import JWKRSA - key = JWKRSA.json_loads("""{ - "kty": "RSA", - "kid": "bilbo.baggins@hobbiton.example", - "use": "sig", - "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", - "e": "AQAB" -}""") - self.assertEqual( - binascii.hexlify(key.thumbprint()), - b"f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932") - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/jws.py b/acme/acme/jose/jws.py deleted file mode 100644 index 5f446e4b1..000000000 --- a/acme/acme/jose/jws.py +++ /dev/null @@ -1,433 +0,0 @@ -"""JOSE Web Signature.""" -import argparse -import base64 -import sys - -import OpenSSL -import six - -from acme.jose import b64 -from acme.jose import errors -from acme.jose import json_util -from acme.jose import jwa -from acme.jose import jwk -from acme.jose import util - - -class MediaType(object): - """MediaType field encoder/decoder.""" - - PREFIX = 'application/' - """MIME Media Type and Content Type prefix.""" - - @classmethod - def decode(cls, value): - """Decoder.""" - # 4.1.10 - if '/' not in value: - if ';' in value: - raise errors.DeserializationError('Unexpected semi-colon') - return cls.PREFIX + value - return value - - @classmethod - def encode(cls, value): - """Encoder.""" - # 4.1.10 - if ';' not in value: - assert value.startswith(cls.PREFIX) - return value[len(cls.PREFIX):] - return value - - -class Header(json_util.JSONObjectWithFields): - """JOSE Header. - - .. warning:: This class supports **only** Registered Header - Parameter Names (as defined in section 4.1 of the - protocol). If you need Public Header Parameter Names (4.2) - or Private Header Parameter Names (4.3), you must subclass - and override :meth:`from_json` and :meth:`to_partial_json` - appropriately. - - .. warning:: This class does not support any extensions through - the "crit" (Critical) Header Parameter (4.1.11) and as a - conforming implementation, :meth:`from_json` treats its - occurrence as an error. Please subclass if you seek for - a different behaviour. - - :ivar x5tS256: "x5t#S256" - :ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`. - :ivar str cty: Content-Type, inc. :const:`MediaType.PREFIX`. - - """ - alg = json_util.Field( - 'alg', decoder=jwa.JWASignature.from_json, omitempty=True) - jku = json_util.Field('jku', omitempty=True) - jwk = json_util.Field('jwk', decoder=jwk.JWK.from_json, omitempty=True) - kid = json_util.Field('kid', omitempty=True) - x5u = json_util.Field('x5u', omitempty=True) - x5c = json_util.Field('x5c', omitempty=True, default=()) - x5t = json_util.Field( - 'x5t', decoder=json_util.decode_b64jose, omitempty=True) - x5tS256 = json_util.Field( - 'x5t#S256', decoder=json_util.decode_b64jose, omitempty=True) - typ = json_util.Field('typ', encoder=MediaType.encode, - decoder=MediaType.decode, omitempty=True) - cty = json_util.Field('cty', encoder=MediaType.encode, - decoder=MediaType.decode, omitempty=True) - crit = json_util.Field('crit', omitempty=True, default=()) - - def not_omitted(self): - """Fields that would not be omitted in the JSON object.""" - return dict((name, getattr(self, name)) - for name, field in six.iteritems(self._fields) - if not field.omit(getattr(self, name))) - - def __add__(self, other): - if not isinstance(other, type(self)): - raise TypeError('Header cannot be added to: {0}'.format( - type(other))) - - not_omitted_self = self.not_omitted() - not_omitted_other = other.not_omitted() - - if set(not_omitted_self).intersection(not_omitted_other): - raise TypeError('Addition of overlapping headers not defined') - - not_omitted_self.update(not_omitted_other) - return type(self)(**not_omitted_self) # pylint: disable=star-args - - def find_key(self): - """Find key based on header. - - .. todo:: Supports only "jwk" header parameter lookup. - - :returns: (Public) key found in the header. - :rtype: .JWK - - :raises acme.jose.errors.Error: if key could not be found - - """ - if self.jwk is None: - raise errors.Error('No key found') - return self.jwk - - @crit.decoder - def crit(unused_value): - # pylint: disable=missing-docstring,no-self-argument,no-self-use - raise errors.DeserializationError( - '"crit" is not supported, please subclass') - - # x5c does NOT use JOSE Base64 (4.1.6) - - @x5c.encoder # type: ignore - def x5c(value): # pylint: disable=missing-docstring,no-self-argument - return [base64.b64encode(OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) for cert in value] - - @x5c.decoder # type: ignore - def x5c(value): # pylint: disable=missing-docstring,no-self-argument - try: - return tuple(util.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, - base64.b64decode(cert))) for cert in value) - except OpenSSL.crypto.Error as error: - raise errors.DeserializationError(error) - - -class Signature(json_util.JSONObjectWithFields): - """JWS Signature. - - :ivar combined: Combined Header (protected and unprotected, - :class:`Header`). - :ivar unicode protected: JWS protected header (Jose Base-64 decoded). - :ivar header: JWS Unprotected Header (:class:`Header`). - :ivar str signature: The signature. - - """ - header_cls = Header - - __slots__ = ('combined',) - protected = json_util.Field('protected', omitempty=True, default='') - header = json_util.Field( - 'header', omitempty=True, default=header_cls(), - decoder=header_cls.from_json) - signature = json_util.Field( - 'signature', decoder=json_util.decode_b64jose, - encoder=json_util.encode_b64jose) - - @protected.encoder # type: ignore - def protected(value): # pylint: disable=missing-docstring,no-self-argument - # wrong type guess (Signature, not bytes) | pylint: disable=no-member - return json_util.encode_b64jose(value.encode('utf-8')) - - @protected.decoder # type: ignore - def protected(value): # pylint: disable=missing-docstring,no-self-argument - return json_util.decode_b64jose(value).decode('utf-8') - - def __init__(self, **kwargs): - if 'combined' not in kwargs: - kwargs = self._with_combined(kwargs) - super(Signature, self).__init__(**kwargs) - assert self.combined.alg is not None - - @classmethod - def _with_combined(cls, kwargs): - assert 'combined' not in kwargs - header = kwargs.get('header', cls._fields['header'].default) - protected = kwargs.get('protected', cls._fields['protected'].default) - - if protected: - combined = header + cls.header_cls.json_loads(protected) - else: - combined = header - - kwargs['combined'] = combined - return kwargs - - @classmethod - def _msg(cls, protected, payload): - return (b64.b64encode(protected.encode('utf-8')) + b'.' + - b64.b64encode(payload)) - - def verify(self, payload, key=None): - """Verify. - - :param JWK key: Key used for verification. - - """ - key = self.combined.find_key() if key is None else key - return self.combined.alg.verify( - key=key.key, sig=self.signature, - msg=self._msg(self.protected, payload)) - - @classmethod - def sign(cls, payload, key, alg, include_jwk=True, - protect=frozenset(), **kwargs): - """Sign. - - :param JWK key: Key for signature. - - """ - assert isinstance(key, alg.kty) - - header_params = kwargs - header_params['alg'] = alg - if include_jwk: - header_params['jwk'] = key.public_key() - - assert set(header_params).issubset(cls.header_cls._fields) - assert protect.issubset(cls.header_cls._fields) - - protected_params = {} - for header in protect: - if header in header_params: - protected_params[header] = header_params.pop(header) - if protected_params: - # pylint: disable=star-args - protected = cls.header_cls(**protected_params).json_dumps() - else: - protected = '' - - header = cls.header_cls(**header_params) # pylint: disable=star-args - signature = alg.sign(key.key, cls._msg(protected, payload)) - - return cls(protected=protected, header=header, signature=signature) - - def fields_to_partial_json(self): - fields = super(Signature, self).fields_to_partial_json() - if not fields['header'].not_omitted(): - del fields['header'] - return fields - - @classmethod - def fields_from_json(cls, jobj): - fields = super(Signature, cls).fields_from_json(jobj) - fields_with_combined = cls._with_combined(fields) - if 'alg' not in fields_with_combined['combined'].not_omitted(): - raise errors.DeserializationError('alg not present') - return fields_with_combined - - -class JWS(json_util.JSONObjectWithFields): - """JSON Web Signature. - - :ivar str payload: JWS Payload. - :ivar str signature: JWS Signatures. - - """ - __slots__ = ('payload', 'signatures') - - signature_cls = Signature - - def verify(self, key=None): - """Verify.""" - return all(sig.verify(self.payload, key) for sig in self.signatures) - - @classmethod - def sign(cls, payload, **kwargs): - """Sign.""" - return cls(payload=payload, signatures=( - cls.signature_cls.sign(payload=payload, **kwargs),)) - - @property - def signature(self): - """Get a singleton signature. - - :rtype: `signature_cls` - - """ - assert len(self.signatures) == 1 - return self.signatures[0] - - def to_compact(self): - """Compact serialization. - - :rtype: bytes - - """ - assert len(self.signatures) == 1 - - assert 'alg' not in self.signature.header.not_omitted() - # ... it must be in protected - - return ( - b64.b64encode(self.signature.protected.encode('utf-8')) + - b'.' + - b64.b64encode(self.payload) + - b'.' + - b64.b64encode(self.signature.signature)) - - @classmethod - def from_compact(cls, compact): - """Compact deserialization. - - :param bytes compact: - - """ - try: - protected, payload, signature = compact.split(b'.') - except ValueError: - raise errors.DeserializationError( - 'Compact JWS serialization should comprise of exactly' - ' 3 dot-separated components') - - sig = cls.signature_cls( - protected=b64.b64decode(protected).decode('utf-8'), - signature=b64.b64decode(signature)) - return cls(payload=b64.b64decode(payload), signatures=(sig,)) - - def to_partial_json(self, flat=True): # pylint: disable=arguments-differ - assert self.signatures - payload = json_util.encode_b64jose(self.payload) - - if flat and len(self.signatures) == 1: - ret = self.signatures[0].to_partial_json() - ret['payload'] = payload - return ret - else: - return { - 'payload': payload, - 'signatures': self.signatures, - } - - @classmethod - def from_json(cls, jobj): - if 'signature' in jobj and 'signatures' in jobj: - raise errors.DeserializationError('Flat mixed with non-flat') - elif 'signature' in jobj: # flat - return cls(payload=json_util.decode_b64jose(jobj.pop('payload')), - signatures=(cls.signature_cls.from_json(jobj),)) - else: - return cls(payload=json_util.decode_b64jose(jobj['payload']), - signatures=tuple(cls.signature_cls.from_json(sig) - for sig in jobj['signatures'])) - - -class CLI(object): - """JWS CLI.""" - - @classmethod - def sign(cls, args): - """Sign.""" - key = args.alg.kty.load(args.key.read()) - args.key.close() - if args.protect is None: - args.protect = [] - if args.compact: - args.protect.append('alg') - - sig = JWS.sign(payload=sys.stdin.read().encode(), key=key, alg=args.alg, - protect=set(args.protect)) - - if args.compact: - six.print_(sig.to_compact().decode('utf-8')) - else: # JSON - six.print_(sig.json_dumps_pretty()) - - @classmethod - def verify(cls, args): - """Verify.""" - if args.compact: - sig = JWS.from_compact(sys.stdin.read().encode()) - else: # JSON - try: - sig = JWS.json_loads(sys.stdin.read()) - except errors.Error as error: - six.print_(error) - return -1 - - if args.key is not None: - assert args.kty is not None - key = args.kty.load(args.key.read()).public_key() - args.key.close() - else: - key = None - - sys.stdout.write(sig.payload) - return not sig.verify(key=key) - - @classmethod - def _alg_type(cls, arg): - return jwa.JWASignature.from_json(arg) - - @classmethod - def _header_type(cls, arg): - assert arg in Signature.header_cls._fields - return arg - - @classmethod - def _kty_type(cls, arg): - assert arg in jwk.JWK.TYPES - return jwk.JWK.TYPES[arg] - - @classmethod - def run(cls, args=sys.argv[1:]): - """Parse arguments and sign/verify.""" - parser = argparse.ArgumentParser() - parser.add_argument('--compact', action='store_true') - - subparsers = parser.add_subparsers() - parser_sign = subparsers.add_parser('sign') - parser_sign.set_defaults(func=cls.sign) - parser_sign.add_argument( - '-k', '--key', type=argparse.FileType('rb'), required=True) - parser_sign.add_argument( - '-a', '--alg', type=cls._alg_type, default=jwa.RS256) - parser_sign.add_argument( - '-p', '--protect', action='append', type=cls._header_type) - - parser_verify = subparsers.add_parser('verify') - parser_verify.set_defaults(func=cls.verify) - parser_verify.add_argument( - '-k', '--key', type=argparse.FileType('rb'), required=False) - parser_verify.add_argument( - '--kty', type=cls._kty_type, required=False) - - parsed = parser.parse_args(args) - return parsed.func(parsed) - - -if __name__ == '__main__': - exit(CLI.run()) # pragma: no cover diff --git a/acme/acme/jose/jws_test.py b/acme/acme/jose/jws_test.py deleted file mode 100644 index ec91f6a1b..000000000 --- a/acme/acme/jose/jws_test.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Tests for acme.jose.jws.""" -import base64 -import unittest - -import mock -import OpenSSL - -from acme import test_util - -from acme.jose import errors -from acme.jose import json_util -from acme.jose import jwa -from acme.jose import jwk - - -CERT = test_util.load_comparable_cert('cert.pem') -KEY = jwk.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - - -class MediaTypeTest(unittest.TestCase): - """Tests for acme.jose.jws.MediaType.""" - - def test_decode(self): - from acme.jose.jws import MediaType - self.assertEqual('application/app', MediaType.decode('application/app')) - self.assertEqual('application/app', MediaType.decode('app')) - self.assertRaises( - errors.DeserializationError, MediaType.decode, 'app;foo') - - def test_encode(self): - from acme.jose.jws import MediaType - self.assertEqual('app', MediaType.encode('application/app')) - self.assertEqual('application/app;foo', - MediaType.encode('application/app;foo')) - - -class HeaderTest(unittest.TestCase): - """Tests for acme.jose.jws.Header.""" - - def setUp(self): - from acme.jose.jws import Header - self.header1 = Header(jwk='foo') - self.header2 = Header(jwk='bar') - self.crit = Header(crit=('a', 'b')) - self.empty = Header() - - def test_add_non_empty(self): - from acme.jose.jws import Header - self.assertEqual(Header(jwk='foo', crit=('a', 'b')), - self.header1 + self.crit) - - def test_add_empty(self): - self.assertEqual(self.header1, self.header1 + self.empty) - self.assertEqual(self.header1, self.empty + self.header1) - - def test_add_overlapping_error(self): - self.assertRaises(TypeError, self.header1.__add__, self.header2) - - def test_add_wrong_type_error(self): - self.assertRaises(TypeError, self.header1.__add__, 'xxx') - - def test_crit_decode_always_errors(self): - from acme.jose.jws import Header - self.assertRaises(errors.DeserializationError, Header.from_json, - {'crit': ['a', 'b']}) - - def test_x5c_decoding(self): - from acme.jose.jws import Header - header = Header(x5c=(CERT, CERT)) - jobj = header.to_partial_json() - cert_asn1 = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped) - cert_b64 = base64.b64encode(cert_asn1) - self.assertEqual(jobj, {'x5c': [cert_b64, cert_b64]}) - self.assertEqual(header, Header.from_json(jobj)) - jobj['x5c'][0] = base64.b64encode(b'xxx' + cert_asn1) - self.assertRaises(errors.DeserializationError, Header.from_json, jobj) - - def test_find_key(self): - self.assertEqual('foo', self.header1.find_key()) - self.assertEqual('bar', self.header2.find_key()) - self.assertRaises(errors.Error, self.crit.find_key) - - -class SignatureTest(unittest.TestCase): - """Tests for acme.jose.jws.Signature.""" - - def test_from_json(self): - from acme.jose.jws import Header - from acme.jose.jws import Signature - self.assertEqual( - Signature(signature=b'foo', header=Header(alg=jwa.RS256)), - Signature.from_json( - {'signature': 'Zm9v', 'header': {'alg': 'RS256'}})) - - def test_from_json_no_alg_error(self): - from acme.jose.jws import Signature - self.assertRaises(errors.DeserializationError, - Signature.from_json, {'signature': 'foo'}) - - -class JWSTest(unittest.TestCase): - """Tests for acme.jose.jws.JWS.""" - - def setUp(self): - self.privkey = KEY - self.pubkey = self.privkey.public_key() - - from acme.jose.jws import JWS - self.unprotected = JWS.sign( - payload=b'foo', key=self.privkey, alg=jwa.RS256) - self.protected = JWS.sign( - payload=b'foo', key=self.privkey, alg=jwa.RS256, - protect=frozenset(['jwk', 'alg'])) - self.mixed = JWS.sign( - payload=b'foo', key=self.privkey, alg=jwa.RS256, - protect=frozenset(['alg'])) - - def test_pubkey_jwk(self): - self.assertEqual(self.unprotected.signature.combined.jwk, self.pubkey) - self.assertEqual(self.protected.signature.combined.jwk, self.pubkey) - self.assertEqual(self.mixed.signature.combined.jwk, self.pubkey) - - def test_sign_unprotected(self): - self.assertTrue(self.unprotected.verify()) - - def test_sign_protected(self): - self.assertTrue(self.protected.verify()) - - def test_sign_mixed(self): - self.assertTrue(self.mixed.verify()) - - def test_compact_lost_unprotected(self): - compact = self.mixed.to_compact() - self.assertEqual( - b'eyJhbGciOiAiUlMyNTYifQ.Zm9v.OHdxFVj73l5LpxbFp1AmYX4yJM0Pyb' - b'_893n1zQjpim_eLS5J1F61lkvrCrCDErTEJnBGOGesJ72M7b6Ve1cAJA', - compact) - - from acme.jose.jws import JWS - mixed = JWS.from_compact(compact) - - self.assertNotEqual(self.mixed, mixed) - self.assertEqual( - set(['alg']), set(mixed.signature.combined.not_omitted())) - - def test_from_compact_missing_components(self): - from acme.jose.jws import JWS - self.assertRaises(errors.DeserializationError, JWS.from_compact, b'.') - - def test_json_omitempty(self): - protected_jobj = self.protected.to_partial_json(flat=True) - unprotected_jobj = self.unprotected.to_partial_json(flat=True) - - self.assertTrue('protected' not in unprotected_jobj) - self.assertTrue('header' not in protected_jobj) - - unprotected_jobj['header'] = unprotected_jobj['header'].to_json() - - from acme.jose.jws import JWS - self.assertEqual(JWS.from_json(protected_jobj), self.protected) - self.assertEqual(JWS.from_json(unprotected_jobj), self.unprotected) - - def test_json_flat(self): - jobj_to = { - 'signature': json_util.encode_b64jose( - self.mixed.signature.signature), - 'payload': json_util.encode_b64jose(b'foo'), - 'header': self.mixed.signature.header, - 'protected': json_util.encode_b64jose( - self.mixed.signature.protected.encode('utf-8')), - } - jobj_from = jobj_to.copy() - jobj_from['header'] = jobj_from['header'].to_json() - - self.assertEqual(self.mixed.to_partial_json(flat=True), jobj_to) - from acme.jose.jws import JWS - self.assertEqual(self.mixed, JWS.from_json(jobj_from)) - - def test_json_not_flat(self): - jobj_to = { - 'signatures': (self.mixed.signature,), - 'payload': json_util.encode_b64jose(b'foo'), - } - jobj_from = jobj_to.copy() - jobj_from['signatures'] = [jobj_to['signatures'][0].to_json()] - - self.assertEqual(self.mixed.to_partial_json(flat=False), jobj_to) - from acme.jose.jws import JWS - self.assertEqual(self.mixed, JWS.from_json(jobj_from)) - - def test_from_json_mixed_flat(self): - from acme.jose.jws import JWS - self.assertRaises(errors.DeserializationError, JWS.from_json, - {'signatures': (), 'signature': 'foo'}) - - def test_from_json_hashable(self): - from acme.jose.jws import JWS - hash(JWS.from_json(self.mixed.to_json())) - - -class CLITest(unittest.TestCase): - - def setUp(self): - self.key_path = test_util.vector_path('rsa512_key.pem') - - def test_unverified(self): - from acme.jose.jws import CLI - with mock.patch('sys.stdin') as sin: - sin.read.return_value = '{"payload": "foo", "signature": "xxx"}' - with mock.patch('sys.stdout'): - self.assertEqual(-1, CLI.run(['verify'])) - - def test_json(self): - from acme.jose.jws import CLI - - with mock.patch('sys.stdin') as sin: - sin.read.return_value = 'foo' - with mock.patch('sys.stdout') as sout: - CLI.run(['sign', '-k', self.key_path, '-a', 'RS256', - '-p', 'jwk']) - sin.read.return_value = sout.write.mock_calls[0][1][0] - self.assertEqual(0, CLI.run(['verify'])) - - def test_compact(self): - from acme.jose.jws import CLI - - with mock.patch('sys.stdin') as sin: - sin.read.return_value = 'foo' - with mock.patch('sys.stdout') as sout: - CLI.run(['--compact', 'sign', '-k', self.key_path]) - sin.read.return_value = sout.write.mock_calls[0][1][0] - self.assertEqual(0, CLI.run([ - '--compact', 'verify', '--kty', 'RSA', - '-k', self.key_path])) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/util.py b/acme/acme/jose/util.py deleted file mode 100644 index 26b7e0c5a..000000000 --- a/acme/acme/jose/util.py +++ /dev/null @@ -1,226 +0,0 @@ -"""JOSE utilities.""" -import collections - -from cryptography.hazmat.primitives.asymmetric import rsa -import OpenSSL -import six - - -class abstractclassmethod(classmethod): - # pylint: disable=invalid-name,too-few-public-methods - """Descriptor for an abstract classmethod. - - It augments the :mod:`abc` framework with an abstract - classmethod. This is implemented as :class:`abc.abstractclassmethod` - in the standard Python library starting with version 3.2. - - This particular implementation, allegedly based on Python 3.3 source - code, is stolen from - http://stackoverflow.com/questions/11217878/python-2-7-combine-abc-abstractmethod-and-classmethod. - - """ - __isabstractmethod__ = True - - def __init__(self, target): - target.__isabstractmethod__ = True - super(abstractclassmethod, self).__init__(target) - - -class ComparableX509(object): # pylint: disable=too-few-public-methods - """Wrapper for OpenSSL.crypto.X509** objects that supports __eq__. - - :ivar wrapped: Wrapped certificate or certificate request. - :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. - - """ - def __init__(self, wrapped): - assert isinstance(wrapped, OpenSSL.crypto.X509) or isinstance( - wrapped, OpenSSL.crypto.X509Req) - self.wrapped = wrapped - - def __getattr__(self, name): - return getattr(self.wrapped, name) - - def _dump(self, filetype=OpenSSL.crypto.FILETYPE_ASN1): - """Dumps the object into a buffer with the specified encoding. - - :param int filetype: The desired encoding. Should be one of - `OpenSSL.crypto.FILETYPE_ASN1`, - `OpenSSL.crypto.FILETYPE_PEM`, or - `OpenSSL.crypto.FILETYPE_TEXT`. - - :returns: Encoded X509 object. - :rtype: str - - """ - if isinstance(self.wrapped, OpenSSL.crypto.X509): - func = OpenSSL.crypto.dump_certificate - else: # assert in __init__ makes sure this is X509Req - func = OpenSSL.crypto.dump_certificate_request - return func(filetype, self.wrapped) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return NotImplemented - # pylint: disable=protected-access - return self._dump() == other._dump() - - def __hash__(self): - return hash((self.__class__, self._dump())) - - def __ne__(self, other): - return not self == other - - def __repr__(self): - return '<{0}({1!r})>'.format(self.__class__.__name__, self.wrapped) - - -class ComparableKey(object): # pylint: disable=too-few-public-methods - """Comparable wrapper for `cryptography` keys. - - See https://github.com/pyca/cryptography/issues/2122. - - """ - __hash__ = NotImplemented - - def __init__(self, wrapped): - self._wrapped = wrapped - - def __getattr__(self, name): - return getattr(self._wrapped, name) - - def __eq__(self, other): - # pylint: disable=protected-access - if (not isinstance(other, self.__class__) or - self._wrapped.__class__ is not other._wrapped.__class__): - return NotImplemented - elif hasattr(self._wrapped, 'private_numbers'): - return self.private_numbers() == other.private_numbers() - elif hasattr(self._wrapped, 'public_numbers'): - return self.public_numbers() == other.public_numbers() - else: - return NotImplemented - - def __ne__(self, other): - return not self == other - - def __repr__(self): - return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped) - - def public_key(self): - """Get wrapped public key.""" - return self.__class__(self._wrapped.public_key()) - - -class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods - """Wrapper for `cryptography` RSA keys. - - Wraps around: - - `cryptography.hazmat.primitives.asymmetric.RSAPrivateKey` - - `cryptography.hazmat.primitives.asymmetric.RSAPublicKey` - - """ - - def __hash__(self): - # public_numbers() hasn't got stable hash! - # https://github.com/pyca/cryptography/issues/2143 - if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization): - priv = self.private_numbers() - pub = priv.public_numbers - return hash((self.__class__, priv.p, priv.q, priv.dmp1, - priv.dmq1, priv.iqmp, pub.n, pub.e)) - elif isinstance(self._wrapped, rsa.RSAPublicKeyWithSerialization): - pub = self.public_numbers() - return hash((self.__class__, pub.n, pub.e)) - - -class ImmutableMap(collections.Mapping, collections.Hashable): # type: ignore - # pylint: disable=too-few-public-methods - """Immutable key to value mapping with attribute access.""" - - __slots__ = () - """Must be overridden in subclasses.""" - - def __init__(self, **kwargs): - if set(kwargs) != set(self.__slots__): - raise TypeError( - '__init__() takes exactly the following arguments: {0} ' - '({1} given)'.format(', '.join(self.__slots__), - ', '.join(kwargs) if kwargs else 'none')) - for slot in self.__slots__: - object.__setattr__(self, slot, kwargs.pop(slot)) - - def update(self, **kwargs): - """Return updated map.""" - items = dict(self) - items.update(kwargs) - return type(self)(**items) # pylint: disable=star-args - - def __getitem__(self, key): - try: - return getattr(self, key) - except AttributeError: - raise KeyError(key) - - def __iter__(self): - return iter(self.__slots__) - - def __len__(self): - return len(self.__slots__) - - def __hash__(self): - return hash(tuple(getattr(self, slot) for slot in self.__slots__)) - - def __setattr__(self, name, value): - raise AttributeError("can't set attribute") - - def __repr__(self): - return '{0}({1})'.format(self.__class__.__name__, ', '.join( - '{0}={1!r}'.format(key, value) - for key, value in six.iteritems(self))) - - -class frozendict(collections.Mapping, collections.Hashable): # type: ignore - # pylint: disable=invalid-name,too-few-public-methods - """Frozen dictionary.""" - __slots__ = ('_items', '_keys') - - def __init__(self, *args, **kwargs): - if kwargs and not args: - items = dict(kwargs) - elif len(args) == 1 and isinstance(args[0], collections.Mapping): - items = args[0] - else: - raise TypeError() - # TODO: support generators/iterators - - object.__setattr__(self, '_items', items) - object.__setattr__(self, '_keys', tuple(sorted(six.iterkeys(items)))) - - def __getitem__(self, key): - return self._items[key] - - def __iter__(self): - return iter(self._keys) - - def __len__(self): - return len(self._items) - - def _sorted_items(self): - return tuple((key, self[key]) for key in self._keys) - - def __hash__(self): - return hash(self._sorted_items()) - - def __getattr__(self, name): - try: - return self._items[name] - except KeyError: - raise AttributeError(name) - - def __setattr__(self, name, value): - raise AttributeError("can't set attribute") - - def __repr__(self): - return 'frozendict({0})'.format(', '.join('{0}={1!r}'.format( - key, value) for key, value in self._sorted_items())) diff --git a/acme/acme/jose/util_test.py b/acme/acme/jose/util_test.py deleted file mode 100644 index 0038a6cc1..000000000 --- a/acme/acme/jose/util_test.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Tests for acme.jose.util.""" -import functools -import unittest - -import six - -from acme import test_util - - -class ComparableX509Test(unittest.TestCase): - """Tests for acme.jose.util.ComparableX509.""" - - def setUp(self): - # test_util.load_comparable_{csr,cert} return ComparableX509 - self.req1 = test_util.load_comparable_csr('csr.pem') - self.req2 = test_util.load_comparable_csr('csr.pem') - self.req_other = test_util.load_comparable_csr('csr-san.pem') - - self.cert1 = test_util.load_comparable_cert('cert.pem') - self.cert2 = test_util.load_comparable_cert('cert.pem') - self.cert_other = test_util.load_comparable_cert('cert-san.pem') - - def test_getattr_proxy(self): - self.assertTrue(self.cert1.has_expired()) - - def test_eq(self): - self.assertEqual(self.req1, self.req2) - self.assertEqual(self.cert1, self.cert2) - - def test_ne(self): - self.assertNotEqual(self.req1, self.req_other) - self.assertNotEqual(self.cert1, self.cert_other) - - def test_ne_wrong_types(self): - self.assertNotEqual(self.req1, 5) - self.assertNotEqual(self.cert1, 5) - - def test_hash(self): - self.assertEqual(hash(self.req1), hash(self.req2)) - self.assertNotEqual(hash(self.req1), hash(self.req_other)) - - self.assertEqual(hash(self.cert1), hash(self.cert2)) - self.assertNotEqual(hash(self.cert1), hash(self.cert_other)) - - def test_repr(self): - for x509 in self.req1, self.cert1: - self.assertEqual(repr(x509), - ''.format(x509.wrapped)) - - -class ComparableRSAKeyTest(unittest.TestCase): - """Tests for acme.jose.util.ComparableRSAKey.""" - - def setUp(self): - # test_utl.load_rsa_private_key return ComparableRSAKey - self.key = test_util.load_rsa_private_key('rsa256_key.pem') - self.key_same = test_util.load_rsa_private_key('rsa256_key.pem') - self.key2 = test_util.load_rsa_private_key('rsa512_key.pem') - - def test_getattr_proxy(self): - self.assertEqual(256, self.key.key_size) - - def test_eq(self): - self.assertEqual(self.key, self.key_same) - - def test_ne(self): - self.assertNotEqual(self.key, self.key2) - - def test_ne_different_types(self): - self.assertNotEqual(self.key, 5) - - def test_ne_not_wrapped(self): - # pylint: disable=protected-access - self.assertNotEqual(self.key, self.key_same._wrapped) - - def test_ne_no_serialization(self): - from acme.jose.util import ComparableRSAKey - self.assertNotEqual(ComparableRSAKey(5), ComparableRSAKey(5)) - - def test_hash(self): - self.assertTrue(isinstance(hash(self.key), int)) - self.assertEqual(hash(self.key), hash(self.key_same)) - self.assertNotEqual(hash(self.key), hash(self.key2)) - - def test_repr(self): - self.assertTrue(repr(self.key).startswith( - '=0.6) # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', + # formerly known as acme.jose: + 'josepy>=1.0.0', # Connection.set_tlsext_host_name (>=0.13) 'mock', 'PyOpenSSL>=0.13', @@ -31,7 +33,8 @@ if sys.version_info < (2, 7): ]) dev_extras = [ - 'nose', + 'pytest', + 'pytest-xdist', 'tox', ] @@ -73,10 +76,5 @@ setup( 'dev': dev_extras, 'docs': docs_extras, }, - entry_points={ - 'console_scripts': [ - 'jws = acme.jose.jws:CLI.run', - ], - }, test_suite='acme', ) diff --git a/certbot-apache/certbot_apache/apache_util.py b/certbot-apache/certbot_apache/apache_util.py new file mode 100644 index 000000000..f03c9da87 --- /dev/null +++ b/certbot-apache/certbot_apache/apache_util.py @@ -0,0 +1,100 @@ +""" Utility functions for certbot-apache plugin """ +import os + +from certbot import util + +def get_mod_deps(mod_name): + """Get known module dependencies. + + .. note:: This does not need to be accurate in order for the client to + run. This simply keeps things clean if the user decides to revert + changes. + .. warning:: If all deps are not included, it may cause incorrect parsing + behavior, due to enable_mod's shortcut for updating the parser's + currently defined modules (`.ApacheParser.add_mod`) + This would only present a major problem in extremely atypical + configs that use ifmod for the missing deps. + + """ + deps = { + "ssl": ["setenvif", "mime"] + } + return deps.get(mod_name, []) + + +def get_file_path(vhost_path): + """Get file path from augeas_vhost_path. + + Takes in Augeas path and returns the file name + + :param str vhost_path: Augeas virtual host path + + :returns: filename of vhost + :rtype: str + + """ + if not vhost_path or not vhost_path.startswith("/files/"): + return None + + return _split_aug_path(vhost_path)[0] + + +def get_internal_aug_path(vhost_path): + """Get the Augeas path for a vhost with the file path removed. + + :param str vhost_path: Augeas virtual host path + + :returns: Augeas path to vhost relative to the containing file + :rtype: str + + """ + return _split_aug_path(vhost_path)[1] + + +def _split_aug_path(vhost_path): + """Splits an Augeas path into a file path and an internal path. + + After removing "/files", this function splits vhost_path into the + file path and the remaining Augeas path. + + :param str vhost_path: Augeas virtual host path + + :returns: file path and internal Augeas path + :rtype: `tuple` of `str` + + """ + # Strip off /files + file_path = vhost_path[6:] + internal_path = [] + + # Remove components from the end of file_path until it becomes valid + while not os.path.exists(file_path): + file_path, _, internal_path_part = file_path.rpartition("/") + internal_path.append(internal_path_part) + + return file_path, "/".join(reversed(internal_path)) + + +def parse_define_file(filepath, varname): + """ Parses Defines from a variable in configuration file + + :param str filepath: Path of file to parse + :param str varname: Name of the variable + + :returns: Dict of Define:Value pairs + :rtype: `dict` + + """ + return_vars = {} + # Get list of words in the variable + a_opts = util.get_var_from_file(varname, filepath).split() + for i, v in enumerate(a_opts): + # Handle Define statements and make sure it has an argument + if v == "-D" and len(a_opts) >= i+2: + var_parts = a_opts[i+1].partition("=") + return_vars[var_parts[0]] = var_parts[2] + elif len(v) > 2 and v.startswith("-D"): + # Found var with no whitespace separator + var_parts = v[2:].partition("=") + return_vars[var_parts[0]] = var_parts[2] + return return_vars diff --git a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf index 17ae1be76..56c946a4e 100644 --- a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf @@ -8,7 +8,7 @@ SSLEngine on # Intermediate configuration, tweak to your needs SSLProtocol all -SSLv2 -SSLv3 -SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA +SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS SSLHonorCipherOrder on SSLOptions +StrictRequire diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 8b6c578d6..4bb2cbebd 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -3,6 +3,7 @@ import fnmatch import logging import os +import pkg_resources import re import socket import time @@ -19,12 +20,14 @@ from certbot import util from certbot.plugins import common from certbot.plugins.util import path_surgery +from certbot_apache import apache_util from certbot_apache import augeas_configurator from certbot_apache import constants from certbot_apache import display_ops -from certbot_apache import tls_sni_01 +from certbot_apache import http_01 from certbot_apache import obj from certbot_apache import parser +from certbot_apache import tls_sni_01 from collections import defaultdict @@ -85,27 +88,50 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): description = "Apache Web Server plugin - Beta" + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/sites-available", + vhost_files="*", + logs_root="/var/log/apache2", + version_cmd=['apache2ctl', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) + + def constant(self, key): + """Get constant for OS_DEFAULTS""" + return self.OS_DEFAULTS.get(key) + @classmethod def add_parser_arguments(cls, add): - add("enmod", default=constants.os_constant("enmod"), + add("enmod", default=cls.OS_DEFAULTS["enmod"], help="Path to the Apache 'a2enmod' binary.") - add("dismod", default=constants.os_constant("dismod"), + add("dismod", default=cls.OS_DEFAULTS["dismod"], help="Path to the Apache 'a2dismod' binary.") - add("le-vhost-ext", default=constants.os_constant("le_vhost_ext"), + add("le-vhost-ext", default=cls.OS_DEFAULTS["le_vhost_ext"], help="SSL vhost configuration extension.") - add("server-root", default=constants.os_constant("server_root"), + add("server-root", default=cls.OS_DEFAULTS["server_root"], help="Apache server root directory.") add("vhost-root", default=None, help="Apache server VirtualHost configuration root") - add("logs-root", default=constants.os_constant("logs_root"), + add("logs-root", default=cls.OS_DEFAULTS["logs_root"], help="Apache server logs directory") add("challenge-location", - default=constants.os_constant("challenge_location"), + default=cls.OS_DEFAULTS["challenge_location"], help="Directory path for challenge configuration.") - add("handle-modules", default=constants.os_constant("handle_mods"), + add("handle-modules", default=cls.OS_DEFAULTS["handle_mods"], help="Let installer handle enabling required modules for you." + "(Only Ubuntu/Debian currently)") - add("handle-sites", default=constants.os_constant("handle_sites"), + add("handle-sites", default=cls.OS_DEFAULTS["handle_sites"], help="Let installer handle enabling sites for you." + "(Only Ubuntu/Debian currently)") util.add_deprecated_argument(add, argument_name="ctl", nargs=1) @@ -166,7 +192,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): raise errors.NoInstallationError("Problem in Augeas installation") # Verify Apache is installed - restart_cmd = constants.os_constant("restart_cmd")[0] + restart_cmd = self.constant("restart_cmd")[0] if not util.exe_exists(restart_cmd): if not path_surgery(restart_cmd): raise errors.NoInstallationError( @@ -192,20 +218,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Parse vhost-root if defined on cli if not self.conf("vhost-root"): - self.vhostroot = constants.os_constant("vhost_root") + self.vhostroot = self.constant("vhost_root") else: self.vhostroot = os.path.abspath(self.conf("vhost-root")) - self.parser = parser.ApacheParser( - self.aug, self.conf("server-root"), self.conf("vhost-root"), - self.version, configurator=self) + self.parser = self.get_parser() + # Check for errors in parsing files with Augeas self.check_parsing_errors("httpd.aug") # Get all of the available vhosts self.vhosts = self.get_virtual_hosts() - install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest) + self.install_ssl_options_conf(self.mod_ssl_conf, + self.updated_mod_ssl_conf_digest) # Prevent two Apache plugins from modifying a config at once try: @@ -230,6 +256,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.aug.remove("/test/path") return matches + def get_parser(self): + """Initializes the ApacheParser""" + return parser.ApacheParser( + self.aug, self.conf("server-root"), self.conf("vhost-root"), + self.version, configurator=self) + def deploy_cert(self, domain, cert_path, key_path, chain_path=None, fullchain_path=None): """Deploys certificate to specified virtual host. @@ -404,12 +436,35 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return True return False - def _find_best_vhost(self, target_name): + def find_best_http_vhost(self, target, filter_defaults, port="80"): + """Returns non-HTTPS vhost objects found from the Apache config + + :param str target: Domain name of the desired VirtualHost + :param bool filter_defaults: whether _default_ vhosts should be + included if it is the best match + :param str port: port number the vhost should be listening on + + :returns: VirtualHost object that's the best match for target name + :rtype: `obj.VirtualHost` or None + """ + filtered_vhosts = [] + for vhost in self.vhosts: + if any(a.is_wildcard() or a.get_port() == port for a in vhost.addrs) and not vhost.ssl: + filtered_vhosts.append(vhost) + return self._find_best_vhost(target, filtered_vhosts, filter_defaults) + + def _find_best_vhost(self, target_name, vhosts=None, filter_defaults=True): """Finds the best vhost for a target_name. This does not upgrade a vhost to HTTPS... it only finds the most appropriate vhost for the given target_name. + :param str target_name: domain handled by the desired vhost + :param vhosts: vhosts to consider + :type vhosts: `collections.Iterable` of :class:`~certbot_apache.obj.VirtualHost` + :param bool filter_defaults: whether a vhost with a _default_ + addr is acceptable + :returns: VHost or None """ @@ -421,7 +476,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Points 1 - Address name with no SSL best_candidate = None best_points = 0 - for vhost in self.vhosts: + + if vhosts is None: + vhosts = self.vhosts + + for vhost in vhosts: if vhost.modmacro is True: continue names = vhost.get_names() @@ -445,8 +504,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # No winners here... is there only one reasonable vhost? if best_candidate is None: - # reasonable == Not all _default_ addrs - vhosts = self._non_default_vhosts() + if filter_defaults: + vhosts = self._non_default_vhosts(vhosts) # remove mod_macro hosts from reasonable vhosts reasonable_vhosts = [vh for vh in vhosts if vh.modmacro is False] @@ -455,9 +514,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return best_candidate - def _non_default_vhosts(self): + def _non_default_vhosts(self, vhosts): """Return all non _default_ only vhosts.""" - return [vh for vh in self.vhosts if not all( + return [vh for vh in vhosts if not all( addr.get_addr() == "_default_" for addr in vh.addrs )] @@ -585,7 +644,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if addr.get_port() == "443": is_ssl = True - filename = get_file_path(self.aug.get("/augeas/files%s/path" % get_file_path(path))) + filename = apache_util.get_file_path( + self.aug.get("/augeas/files%s/path" % apache_util.get_file_path(path))) if filename is None: return None @@ -624,7 +684,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): new_vhost = self._create_vhost(path) if not new_vhost: continue - internal_path = get_internal_aug_path(new_vhost.path) + internal_path = apache_util.get_internal_aug_path(new_vhost.path) realpath = os.path.realpath(new_vhost.filep) if realpath not in file_paths: file_paths[realpath] = new_vhost.filep @@ -640,7 +700,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): for v in vhs: if v.filep == file_paths[realpath]: internal_paths[realpath].remove( - get_internal_aug_path(v.path)) + apache_util.get_internal_aug_path(v.path)) else: new_vhs.append(v) vhs = new_vhs @@ -704,31 +764,43 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ - # If nonstandard port, add service definition for matching - if port != "443": + self.prepare_https_modules(temp) + self.ensure_listen(port, https=True) + + def ensure_listen(self, port, https=False): + """Make sure that Apache is listening on the port. Checks if the + Listen statement for the port already exists, and adds it to the + configuration if necessary. + + :param str port: Port number to check and add Listen for if not in + place already + :param bool https: If the port will be used for HTTPS + + """ + + # If HTTPS requested for nonstandard port, add service definition + if https and port != "443": port_service = "%s %s" % (port, "https") else: port_service = port - self.prepare_https_modules(temp) # Check for Listen # Note: This could be made to also look for ip:443 combo listens = [self.parser.get_arg(x).split()[0] for x in self.parser.find_dir("Listen")] - # In case no Listens are set (which really is a broken apache config) - if not listens: - listens = ["80"] - # Listen already in place if self._has_port_already(listens, port): return listen_dirs = set(listens) + if not listens: + listen_dirs.add(port_service) + for listen in listens: # For any listen statement, check if the machine also listens on - # Port 443. If not, add such a listen statement. + # the given port. If not, add such a listen statement. if len(listen.split(":")) == 1: # Its listening to all interfaces if port not in listen_dirs and port_service not in listen_dirs: @@ -740,11 +812,39 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if "%s:%s" % (ip, port_service) not in listen_dirs and ( "%s:%s" % (ip, port_service) not in listen_dirs): listen_dirs.add("%s:%s" % (ip, port_service)) - self._add_listens(listen_dirs, listens, port) + if https: + self._add_listens_https(listen_dirs, listens, port) + else: + self._add_listens_http(listen_dirs, listens, port) - def _add_listens(self, listens, listens_orig, port): - """Helper method for prepare_server_https to figure out which new - listen statements need adding + def _add_listens_http(self, listens, listens_orig, port): + """Helper method for ensure_listen to figure out which new + listen statements need adding for listening HTTP on port + + :param set listens: Set of all needed Listen statements + :param list listens_orig: List of existing listen statements + :param string port: Port number we're adding + """ + + new_listens = listens.difference(listens_orig) + + if port in new_listens: + # We have wildcard, skip the rest + self.parser.add_dir(parser.get_aug_path(self.parser.loc["listen"]), + "Listen", port) + self.save_notes += "Added Listen %s directive to %s\n" % ( + port, self.parser.loc["listen"]) + else: + for listen in new_listens: + self.parser.add_dir(parser.get_aug_path( + self.parser.loc["listen"]), "Listen", listen.split(" ")) + self.save_notes += ("Added Listen %s directive to " + "%s\n") % (listen, + self.parser.loc["listen"]) + + def _add_listens_https(self, listens, listens_orig, port): + """Helper method for ensure_listen to figure out which new + listen statements need adding for listening HTTPS on port :param set listens: Set of all needed Listen statements :param list listens_orig: List of existing listen statements @@ -828,7 +928,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): Duplicates vhost and adds default ssl options New vhost will reside as (nonssl_vhost.path) + - ``certbot_apache.constants.os_constant("le_vhost_ext")`` + ``self.constant("le_vhost_ext")`` .. note:: This function saves the configuration @@ -1169,7 +1269,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "insert_cert_file_path") self.parser.add_dir(vh_path, "SSLCertificateKeyFile", "insert_key_file_path") - self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf) + # Only include the TLS configuration if not already included + existing_inc = self.parser.find_dir("Include", self.mod_ssl_conf, vh_path) + if not existing_inc: + self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf) def _add_servername_alias(self, target_name, vhost): vh_path = vhost.path @@ -1702,17 +1805,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return redirects - def enable_site(self, vhost): """Enables an available site, Apache reload required. .. note:: Does not make sure that the site correctly works or that all modules are enabled appropriately. - - .. todo:: This function should number subdomains before the domain - vhost - - .. todo:: Make sure link is not broken... + .. note:: The distribution specific override replaces functionality + of this method where available. :param vhost: vhost to enable :type vhost: :class:`~certbot_apache.obj.VirtualHost` @@ -1724,39 +1823,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if vhost.enabled: return - # Handle non-debian systems - if not self.conf("handle-sites"): - if not self.parser.parsed_in_original(vhost.filep): - # Add direct include to root conf - self.parser.add_include(self.parser.loc["default"], vhost.filep) - vhost.enabled = True - return + if not self.parser.parsed_in_original(vhost.filep): + # Add direct include to root conf + logger.info("Enabling site %s by adding Include to root configuration", + vhost.filep) + self.save_notes += "Enabled site %s\n" % vhost.filep + self.parser.add_include(self.parser.loc["default"], vhost.filep) + vhost.enabled = True + return - enabled_path = ("%s/sites-enabled/%s" % - (self.parser.root, os.path.basename(vhost.filep))) - self.reverter.register_file_creation(False, enabled_path) - try: - os.symlink(vhost.filep, enabled_path) - except OSError as err: - if os.path.islink(enabled_path) and os.path.realpath( - enabled_path) == vhost.filep: - # Already in shape - vhost.enabled = True - return - else: - logger.warning( - "Could not symlink %s to %s, got error: %s", enabled_path, - vhost.filep, err.strerror) - errstring = ("Encountered error while trying to enable a " + - "newly created VirtualHost located at {0} by " + - "linking to it from {1}") - raise errors.NotSupportedError(errstring.format(vhost.filep, - enabled_path)) - vhost.enabled = True - logger.info("Enabling available site: %s", vhost.filep) - self.save_notes += "Enabled site %s\n" % vhost.filep - - def enable_mod(self, mod_name, temp=False): + def enable_mod(self, mod_name, temp=False): # pylint: disable=unused-argument """Enables module in Apache. Both enables and reloads Apache so module is active. @@ -1764,64 +1840,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str mod_name: Name of the module to enable. (e.g. 'ssl') :param bool temp: Whether or not this is a temporary action. - :raises .errors.NotSupportedError: If the filesystem layout is not - supported. - :raises .errors.MisconfigurationError: If a2enmod or a2dismod cannot be - run. + .. note:: The distribution specific override replaces functionality + of this method where available. + + :raises .errors.MisconfigurationError: We cannot enable modules in + generic fashion. """ - # Support Debian specific setup - avail_path = os.path.join(self.parser.root, "mods-available") - enabled_path = os.path.join(self.parser.root, "mods-enabled") - if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path): - raise errors.NotSupportedError( - "Unsupported directory layout. You may try to enable mod %s " - "and try again." % mod_name) - - deps = _get_mod_deps(mod_name) - - # Enable all dependencies - for dep in deps: - if (dep + "_module") not in self.parser.modules: - self._enable_mod_debian(dep, temp) - self._add_parser_mod(dep) - - note = "Enabled dependency of %s module - %s" % (mod_name, dep) - if not temp: - self.save_notes += note + os.linesep - logger.debug(note) - - # Enable actual module - self._enable_mod_debian(mod_name, temp) - self._add_parser_mod(mod_name) - - if not temp: - self.save_notes += "Enabled %s module in Apache\n" % mod_name - logger.info("Enabled Apache %s module", mod_name) - - # Modules can enable additional config files. Variables may be defined - # within these new configuration sections. - # Reload is not necessary as DUMP_RUN_CFG uses latest config. - self.parser.update_runtime_variables() - - def _add_parser_mod(self, mod_name): - """Shortcut for updating parser modules.""" - self.parser.modules.add(mod_name + "_module") - self.parser.modules.add("mod_" + mod_name + ".c") - - def _enable_mod_debian(self, mod_name, temp): - """Assumes mods-available, mods-enabled layout.""" - # Generate reversal command. - # Try to be safe here... check that we can probably reverse before - # applying enmod command - if not util.exe_exists(self.conf("dismod")): - raise errors.MisconfigurationError( - "Unable to find a2dismod, please make sure a2enmod and " - "a2dismod are configured correctly for certbot.") - - self.reverter.register_undo_command( - temp, [self.conf("dismod"), mod_name]) - util.run_script([self.conf("enmod"), mod_name]) + mod_message = ("Apache needs to have module \"{0}\" active for the " + + "requested installation options. Unfortunately Certbot is unable " + + "to install or enable it for you. Please install the module, and " + + "run Certbot again.") + raise errors.MisconfigurationError(mod_message.format(mod_name)) def restart(self): """Runs a config test and reloads the Apache server. @@ -1840,7 +1870,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - util.run_script(constants.os_constant("restart_cmd")) + util.run_script(self.constant("restart_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1851,7 +1881,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - util.run_script(constants.os_constant("conftest_cmd")) + util.run_script(self.constant("conftest_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1867,11 +1897,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - stdout, _ = util.run_script(constants.os_constant("version_cmd")) + stdout, _ = util.run_script(self.constant("version_cmd")) except errors.SubprocessError: raise errors.PluginError( "Unable to run %s -v" % - constants.os_constant("version_cmd")) + self.constant("version_cmd")) regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) matches = regex.findall(stdout) @@ -1896,7 +1926,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.TLSSNI01] + return [challenges.TLSSNI01, challenges.HTTP01] def perform(self, achalls): """Perform the configuration related challenge. @@ -1908,16 +1938,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ self._chall_out.update(achalls) responses = [None] * len(achalls) - chall_doer = tls_sni_01.ApacheTlsSni01(self) + http_doer = http_01.ApacheHttp01(self) + sni_doer = tls_sni_01.ApacheTlsSni01(self) for i, achall in enumerate(achalls): # Currently also have chall_doer hold associated index of the # challenge. This helps to put all of the responses back together # when they are all complete. - chall_doer.add_chall(achall, i) + if isinstance(achall.chall, challenges.HTTP01): + http_doer.add_chall(achall, i) + else: # tls-sni-01 + sni_doer.add_chall(achall, i) - sni_response = chall_doer.perform() - if sni_response: + http_response = http_doer.perform() + sni_response = sni_doer.perform() + if http_response or sni_response: # Must reload in order to activate the challenges. # Handled here because we may be able to load up other challenge # types @@ -1927,14 +1962,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # of identifying when the new configuration is being used. time.sleep(3) - # Go through all of the challenges and assign them to the proper - # place in the responses return value. All responses must be in the - # same order as the original challenges. - for i, resp in enumerate(sni_response): - responses[chall_doer.indices[i]] = resp + self._update_responses(responses, http_response, http_doer) + self._update_responses(responses, sni_response, sni_doer) return responses + def _update_responses(self, responses, chall_response, chall_doer): + # Go through all of the challenges and assign them to the proper + # place in the responses return value. All responses must be in the + # same order as the original challenges. + for i, resp in enumerate(chall_response): + responses[chall_doer.indices[i]] = resp + def cleanup(self, achalls): """Revert all challenges.""" self._chall_out.difference_update(achalls) @@ -1943,86 +1982,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not self._chall_out: self.revert_challenge_config() self.restart() - self.parser.init_modules() + self.parser.reset_modules() + + def install_ssl_options_conf(self, options_ssl, options_ssl_digest): + """Copy Certbot's SSL options file into the system's config dir if required.""" + + # XXX if we ever try to enforce a local privilege boundary (eg, running + # certbot for unprivileged users via setuid), this function will need + # to be modified. + return common.install_version_controlled_file(options_ssl, options_ssl_digest, + self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES) -def _get_mod_deps(mod_name): - """Get known module dependencies. - - .. note:: This does not need to be accurate in order for the client to - run. This simply keeps things clean if the user decides to revert - changes. - .. warning:: If all deps are not included, it may cause incorrect parsing - behavior, due to enable_mod's shortcut for updating the parser's - currently defined modules (`.ApacheConfigurator._add_parser_mod`) - This would only present a major problem in extremely atypical - configs that use ifmod for the missing deps. - - """ - deps = { - "ssl": ["setenvif", "mime"] - } - return deps.get(mod_name, []) - - -def get_file_path(vhost_path): - """Get file path from augeas_vhost_path. - - Takes in Augeas path and returns the file name - - :param str vhost_path: Augeas virtual host path - - :returns: filename of vhost - :rtype: str - - """ - if not vhost_path or not vhost_path.startswith("/files/"): - return None - - return _split_aug_path(vhost_path)[0] - - -def get_internal_aug_path(vhost_path): - """Get the Augeas path for a vhost with the file path removed. - - :param str vhost_path: Augeas virtual host path - - :returns: Augeas path to vhost relative to the containing file - :rtype: str - - """ - return _split_aug_path(vhost_path)[1] - - -def _split_aug_path(vhost_path): - """Splits an Augeas path into a file path and an internal path. - - After removing "/files", this function splits vhost_path into the - file path and the remaining Augeas path. - - :param str vhost_path: Augeas virtual host path - - :returns: file path and internal Augeas path - :rtype: `tuple` of `str` - - """ - # Strip off /files - file_path = vhost_path[6:] - internal_path = [] - - # Remove components from the end of file_path until it becomes valid - while not os.path.exists(file_path): - file_path, _, internal_path_part = file_path.rpartition("/") - internal_path.append(internal_path_part) - - return file_path, "/".join(reversed(internal_path)) - - -def install_ssl_options_conf(options_ssl, options_ssl_digest): - """Copy Certbot's SSL options file into the system's config dir if required.""" - - # XXX if we ever try to enforce a local privilege boundary (eg, running - # certbot for unprivileged users via setuid), this function will need - # to be modified. - return common.install_version_controlled_file(options_ssl, options_ssl_digest, - constants.os_constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index 0c5f7263a..fd6a9eb11 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -1,151 +1,6 @@ """Apache plugin constants.""" import pkg_resources -from certbot import util -CLI_DEFAULTS_DEFAULT = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/sites-available", - vhost_files="*", - logs_root="/var/log/apache2", - version_cmd=['apache2ctl', '-v'], - define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_DEBIAN = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/sites-available", - vhost_files="*", - logs_root="/var/log/apache2", - version_cmd=['apache2ctl', '-v'], - define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod="a2enmod", - dismod="a2dismod", - le_vhost_ext="-le-ssl.conf", - handle_mods=True, - handle_sites=True, - challenge_location="/etc/apache2", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_CENTOS = dict( - server_root="/etc/httpd", - vhost_root="/etc/httpd/conf.d", - vhost_files="*.conf", - logs_root="/var/log/httpd", - version_cmd=['apachectl', '-v'], - define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apachectl', 'graceful'], - conftest_cmd=['apachectl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/httpd/conf.d", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "centos-options-ssl-apache.conf") -) -CLI_DEFAULTS_GENTOO = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/vhosts.d", - vhost_files="*.conf", - logs_root="/var/log/apache2", - version_cmd=['/usr/sbin/apache2', '-v'], - define_cmd=['apache2ctl', 'virtualhosts'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2/vhosts.d", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_DARWIN = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/other", - vhost_files="*.conf", - logs_root="/var/log/apache2", - version_cmd=['/usr/sbin/httpd', '-v'], - define_cmd=['/usr/sbin/httpd', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apachectl', 'graceful'], - conftest_cmd=['apachectl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2/other", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_SUSE = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/vhosts.d", - vhost_files="*.conf", - logs_root="/var/log/apache2", - version_cmd=['apache2ctl', '-v'], - define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod="a2enmod", - dismod="a2dismod", - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2/vhosts.d", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_ARCH = dict( - server_root="/etc/httpd", - vhost_root="/etc/httpd/conf", - vhost_files="*.conf", - logs_root="/var/log/httpd", - version_cmd=['apachectl', '-v'], - define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apachectl', 'graceful'], - conftest_cmd=['apachectl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/httpd/conf", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS = { - "default": CLI_DEFAULTS_DEFAULT, - "debian": CLI_DEFAULTS_DEBIAN, - "ubuntu": CLI_DEFAULTS_DEBIAN, - "centos": CLI_DEFAULTS_CENTOS, - "centos linux": CLI_DEFAULTS_CENTOS, - "fedora": CLI_DEFAULTS_CENTOS, - "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, - "rhel": CLI_DEFAULTS_CENTOS, - "amazon": CLI_DEFAULTS_CENTOS, - "gentoo": CLI_DEFAULTS_GENTOO, - "gentoo base system": CLI_DEFAULTS_GENTOO, - "darwin": CLI_DEFAULTS_DARWIN, - "opensuse": CLI_DEFAULTS_SUSE, - "suse": CLI_DEFAULTS_SUSE, - "arch": CLI_DEFAULTS_ARCH, -} -"""CLI defaults.""" MOD_SSL_CONF_DEST = "options-ssl-apache.conf" """Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" @@ -161,6 +16,8 @@ ALL_SSL_OPTIONS_HASHES = [ '4066b90268c03c9ba0201068eaa39abbc02acf9558bb45a788b630eb85dadf27', 'f175e2e7c673bd88d0aff8220735f385f916142c44aa83b09f1df88dd4767a88', 'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b', + '80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791', + 'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082', ] """SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC""" @@ -191,39 +48,3 @@ UIR_ARGS = ["always", "set", "Content-Security-Policy", HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS, "Upgrade-Insecure-Requests": UIR_ARGS} - - -def os_constant(key): - """ - Get a constant value for operating system - - :param key: name of cli constant - :return: value of constant for active os - """ - - os_info = util.get_os_info() - try: - constants = CLI_DEFAULTS[os_info[0].lower()] - except KeyError: - constants = os_like_constants() - if not constants: - constants = CLI_DEFAULTS["default"] - return constants[key] - - -def os_like_constants(): - """ - Try to get constants for distribution with - similar layout and configuration, indicated by - /etc/os-release variable "LIKE" - - :returns: Constants dictionary - :rtype: `dict` - """ - - os_like = util.get_systemd_os_like() - if os_like: - for os_name in os_like: - if os_name in CLI_DEFAULTS.keys(): - return CLI_DEFAULTS[os_name] - return {} diff --git a/certbot-apache/certbot_apache/entrypoint.py b/certbot-apache/certbot_apache/entrypoint.py new file mode 100644 index 000000000..4267398d5 --- /dev/null +++ b/certbot-apache/certbot_apache/entrypoint.py @@ -0,0 +1,47 @@ +""" Entry point for Apache Plugin """ +from certbot import util + +from certbot_apache import configurator +from certbot_apache import override_arch +from certbot_apache import override_darwin +from certbot_apache import override_debian +from certbot_apache import override_centos +from certbot_apache import override_gentoo +from certbot_apache import override_suse + +OVERRIDE_CLASSES = { + "arch": override_arch.ArchConfigurator, + "darwin": override_darwin.DarwinConfigurator, + "debian": override_debian.DebianConfigurator, + "ubuntu": override_debian.DebianConfigurator, + "centos": override_centos.CentOSConfigurator, + "centos linux": override_centos.CentOSConfigurator, + "fedora": override_centos.CentOSConfigurator, + "red hat enterprise linux server": override_centos.CentOSConfigurator, + "rhel": override_centos.CentOSConfigurator, + "amazon": override_centos.CentOSConfigurator, + "gentoo": override_gentoo.GentooConfigurator, + "gentoo base system": override_gentoo.GentooConfigurator, + "opensuse": override_suse.OpenSUSEConfigurator, + "suse": override_suse.OpenSUSEConfigurator, +} + +def get_configurator(): + """ Get correct configurator class based on the OS fingerprint """ + os_info = util.get_os_info() + override_class = None + try: + override_class = OVERRIDE_CLASSES[os_info[0].lower()] + except KeyError: + # OS not found in the list + os_like = util.get_systemd_os_like() + if os_like: + for os_name in os_like: + if os_name in OVERRIDE_CLASSES.keys(): + override_class = OVERRIDE_CLASSES[os_name] + if not override_class: + # No override class found, return the generic configurator + override_class = configurator.ApacheConfigurator + return override_class + +ENTRYPOINT = get_configurator() diff --git a/certbot-apache/certbot_apache/http_01.py b/certbot-apache/certbot_apache/http_01.py new file mode 100644 index 000000000..cce93a646 --- /dev/null +++ b/certbot-apache/certbot_apache/http_01.py @@ -0,0 +1,174 @@ +"""A class that performs HTTP-01 challenges for Apache""" +import logging +import os + +from certbot import errors + +from certbot.plugins import common + +logger = logging.getLogger(__name__) + +class ApacheHttp01(common.TLSSNI01): + """Class that performs HTTP-01 challenges within the Apache configurator.""" + + CONFIG_TEMPLATE22_PRE = """\ + RewriteEngine on + RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [L] + + """ + CONFIG_TEMPLATE22_POST = """\ + + Order Allow,Deny + Allow from all + + + Order Allow,Deny + Allow from all + + """ + + CONFIG_TEMPLATE24_PRE = """\ + RewriteEngine on + RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [END] + """ + CONFIG_TEMPLATE24_POST = """\ + + Require all granted + + + Require all granted + + """ + + def __init__(self, *args, **kwargs): + super(ApacheHttp01, self).__init__(*args, **kwargs) + self.challenge_conf_pre = os.path.join( + self.configurator.conf("challenge-location"), + "le_http_01_challenge_pre.conf") + self.challenge_conf_post = os.path.join( + self.configurator.conf("challenge-location"), + "le_http_01_challenge_post.conf") + self.challenge_dir = os.path.join( + self.configurator.config.work_dir, + "http_challenges") + self.moded_vhosts = set() + + def perform(self): + """Perform all HTTP-01 challenges.""" + if not self.achalls: + return [] + # Save any changes to the configuration as a precaution + # About to make temporary changes to the config + self.configurator.save("Changes before challenge setup", True) + + self.configurator.ensure_listen(str( + self.configurator.config.http01_port)) + self.prepare_http01_modules() + + responses = self._set_up_challenges() + + self._mod_config() + # Save reversible changes + self.configurator.save("HTTP Challenge", True) + + return responses + + def prepare_http01_modules(self): + """Make sure that we have the needed modules available for http01""" + + if self.configurator.conf("handle-modules"): + needed_modules = ["rewrite"] + if self.configurator.version < (2, 4): + needed_modules.append("authz_host") + else: + needed_modules.append("authz_core") + for mod in needed_modules: + if mod + "_module" not in self.configurator.parser.modules: + self.configurator.enable_mod(mod, temp=True) + + def _mod_config(self): + for chall in self.achalls: + vh = self.configurator.find_best_http_vhost( + chall.domain, filter_defaults=False, + port=str(self.configurator.config.http01_port)) + if vh: + self._set_up_include_directives(vh) + else: + for vh in self._relevant_vhosts(): + self._set_up_include_directives(vh) + + self.configurator.reverter.register_file_creation( + True, self.challenge_conf_pre) + self.configurator.reverter.register_file_creation( + True, self.challenge_conf_post) + + if self.configurator.version < (2, 4): + config_template_pre = self.CONFIG_TEMPLATE22_PRE + config_template_post = self.CONFIG_TEMPLATE22_POST + else: + config_template_pre = self.CONFIG_TEMPLATE24_PRE + config_template_post = self.CONFIG_TEMPLATE24_POST + + config_text_pre = config_template_pre.format(self.challenge_dir) + config_text_post = config_template_post.format(self.challenge_dir) + + logger.debug("writing a pre config file with text:\n %s", config_text_pre) + with open(self.challenge_conf_pre, "w") as new_conf: + new_conf.write(config_text_pre) + logger.debug("writing a post config file with text:\n %s", config_text_post) + with open(self.challenge_conf_post, "w") as new_conf: + new_conf.write(config_text_post) + + def _relevant_vhosts(self): + http01_port = str(self.configurator.config.http01_port) + relevant_vhosts = [] + for vhost in self.configurator.vhosts: + if any(a.is_wildcard() or a.get_port() == http01_port for a in vhost.addrs): + if not vhost.ssl: + relevant_vhosts.append(vhost) + if not relevant_vhosts: + raise errors.PluginError( + "Unable to find a virtual host listening on port {0} which is" + " currently needed for Certbot to prove to the CA that you" + " control your domain. Please add a virtual host for port" + " {0}.".format(http01_port)) + + return relevant_vhosts + + def _set_up_challenges(self): + if not os.path.isdir(self.challenge_dir): + os.makedirs(self.challenge_dir) + os.chmod(self.challenge_dir, 0o755) + + responses = [] + for achall in self.achalls: + responses.append(self._set_up_challenge(achall)) + + return responses + + def _set_up_challenge(self, achall): + response, validation = achall.response_and_validation() + + name = os.path.join(self.challenge_dir, achall.chall.encode("token")) + + self.configurator.reverter.register_file_creation(True, name) + with open(name, 'wb') as f: + f.write(validation.encode()) + os.chmod(name, 0o644) + + return response + + def _set_up_include_directives(self, vhost): + """Includes override configuration to the beginning and to the end of + VirtualHost. Note that this include isn't added to Augeas search tree""" + + if vhost not in self.moded_vhosts: + logger.debug( + "Adding a temporary challenge validation Include for name: %s " + + "in: %s", vhost.name, vhost.filep) + self.configurator.parser.add_dir_beginning( + vhost.path, "Include", self.challenge_conf_pre) + self.configurator.parser.add_dir( + vhost.path, "Include", self.challenge_conf_post) + + self.moded_vhosts.add(vhost) diff --git a/certbot-apache/certbot_apache/options-ssl-apache.conf b/certbot-apache/certbot_apache/options-ssl-apache.conf index 950a02a8b..8113ee81e 100644 --- a/certbot-apache/certbot_apache/options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/options-ssl-apache.conf @@ -8,7 +8,7 @@ SSLEngine on # Intermediate configuration, tweak to your needs SSLProtocol all -SSLv2 -SSLv3 -SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA +SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS SSLHonorCipherOrder on SSLCompression off diff --git a/certbot-apache/certbot_apache/override_arch.py b/certbot-apache/certbot_apache/override_arch.py new file mode 100644 index 000000000..ea5155a3c --- /dev/null +++ b/certbot-apache/certbot_apache/override_arch.py @@ -0,0 +1,31 @@ +""" Distribution specific override class for Arch Linux """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import configurator + +@zope.interface.provider(interfaces.IPluginFactory) +class ArchConfigurator(configurator.ApacheConfigurator): + """Arch Linux specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/httpd", + vhost_root="/etc/httpd/conf", + vhost_files="*.conf", + logs_root="/var/log/httpd", + version_cmd=['apachectl', '-v'], + apache_cmd="apachectl", + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/httpd/conf", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) diff --git a/certbot-apache/certbot_apache/override_centos.py b/certbot-apache/certbot_apache/override_centos.py new file mode 100644 index 000000000..db6cd6fba --- /dev/null +++ b/certbot-apache/certbot_apache/override_centos.py @@ -0,0 +1,59 @@ +""" Distribution specific override class for CentOS family (RHEL, Fedora) """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import apache_util +from certbot_apache import configurator +from certbot_apache import parser + +@zope.interface.provider(interfaces.IPluginFactory) +class CentOSConfigurator(configurator.ApacheConfigurator): + """CentOS specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/httpd", + vhost_root="/etc/httpd/conf.d", + vhost_files="*.conf", + logs_root="/var/log/httpd", + version_cmd=['apachectl', '-v'], + apache_cmd="apachectl", + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/httpd/conf.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "centos-options-ssl-apache.conf") + ) + + def get_parser(self): + """Initializes the ApacheParser""" + return CentOSParser( + self.aug, self.conf("server-root"), self.conf("vhost-root"), + self.version, configurator=self) + + +class CentOSParser(parser.ApacheParser): + """CentOS specific ApacheParser override class""" + def __init__(self, *args, **kwargs): + # CentOS specific configuration file for Apache + self.sysconfig_filep = "/etc/sysconfig/httpd" + super(CentOSParser, self).__init__(*args, **kwargs) + + def update_runtime_variables(self, *args, **kwargs): + """ Override for update_runtime_variables for custom parsing """ + # Opportunistic, works if SELinux not enforced + super(CentOSParser, self).update_runtime_variables(*args, **kwargs) + self.parse_sysconfig_var() + + def parse_sysconfig_var(self): + """ Parses Apache CLI options from CentOS configuration file """ + defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS") + for k in defines.keys(): + self.variables[k] = defines[k] diff --git a/certbot-apache/certbot_apache/override_darwin.py b/certbot-apache/certbot_apache/override_darwin.py new file mode 100644 index 000000000..53741d504 --- /dev/null +++ b/certbot-apache/certbot_apache/override_darwin.py @@ -0,0 +1,31 @@ +""" Distribution specific override class for macOS """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import configurator + +@zope.interface.provider(interfaces.IPluginFactory) +class DarwinConfigurator(configurator.ApacheConfigurator): + """macOS specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/other", + vhost_files="*.conf", + logs_root="/var/log/apache2", + version_cmd=['/usr/sbin/httpd', '-v'], + apache_cmd="/usr/sbin/httpd", + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2/other", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) diff --git a/certbot-apache/certbot_apache/override_debian.py b/certbot-apache/certbot_apache/override_debian.py new file mode 100644 index 000000000..02dffc3f7 --- /dev/null +++ b/certbot-apache/certbot_apache/override_debian.py @@ -0,0 +1,144 @@ +""" Distribution specific override class for Debian family (Ubuntu/Debian) """ +import logging +import os +import pkg_resources + +import zope.interface + +from certbot import errors +from certbot import interfaces +from certbot import util + +from certbot_apache import apache_util +from certbot_apache import configurator + +logger = logging.getLogger(__name__) + +@zope.interface.provider(interfaces.IPluginFactory) +class DebianConfigurator(configurator.ApacheConfigurator): + """Debian specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/sites-available", + vhost_files="*", + logs_root="/var/log/apache2", + version_cmd=['apache2ctl', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod="a2enmod", + dismod="a2dismod", + le_vhost_ext="-le-ssl.conf", + handle_mods=True, + handle_sites=True, + challenge_location="/etc/apache2", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) + + def enable_site(self, vhost): + """Enables an available site, Apache reload required. + + .. note:: Does not make sure that the site correctly works or that all + modules are enabled appropriately. + + :param vhost: vhost to enable + :type vhost: :class:`~certbot_apache.obj.VirtualHost` + + :raises .errors.NotSupportedError: If filesystem layout is not + supported. + + """ + if vhost.enabled: + return + + enabled_path = ("%s/sites-enabled/%s" % + (self.parser.root, + os.path.basename(vhost.filep))) + if not os.path.isdir(os.path.dirname(enabled_path)): + # For some reason, sites-enabled / sites-available do not exist + # Call the parent method + return super(DebianConfigurator, self).enable_site(vhost) + self.reverter.register_file_creation(False, enabled_path) + try: + os.symlink(vhost.filep, enabled_path) + except OSError as err: + if os.path.islink(enabled_path) and os.path.realpath( + enabled_path) == vhost.filep: + # Already in shape + vhost.enabled = True + return + else: + logger.warning( + "Could not symlink %s to %s, got error: %s", enabled_path, + vhost.filep, err.strerror) + errstring = ("Encountered error while trying to enable a " + + "newly created VirtualHost located at {0} by " + + "linking to it from {1}") + raise errors.NotSupportedError(errstring.format(vhost.filep, + enabled_path)) + vhost.enabled = True + logger.info("Enabling available site: %s", vhost.filep) + self.save_notes += "Enabled site %s\n" % vhost.filep + + def enable_mod(self, mod_name, temp=False): + # pylint: disable=unused-argument + """Enables module in Apache. + + Both enables and reloads Apache so module is active. + + :param str mod_name: Name of the module to enable. (e.g. 'ssl') + :param bool temp: Whether or not this is a temporary action. + + :raises .errors.NotSupportedError: If the filesystem layout is not + supported. + :raises .errors.MisconfigurationError: If a2enmod or a2dismod cannot be + run. + + """ + avail_path = os.path.join(self.parser.root, "mods-available") + enabled_path = os.path.join(self.parser.root, "mods-enabled") + if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path): + raise errors.NotSupportedError( + "Unsupported directory layout. You may try to enable mod %s " + "and try again." % mod_name) + + deps = apache_util.get_mod_deps(mod_name) + + # Enable all dependencies + for dep in deps: + if (dep + "_module") not in self.parser.modules: + self._enable_mod_debian(dep, temp) + self.parser.add_mod(dep) + note = "Enabled dependency of %s module - %s" % (mod_name, dep) + if not temp: + self.save_notes += note + os.linesep + logger.debug(note) + + # Enable actual module + self._enable_mod_debian(mod_name, temp) + self.parser.add_mod(mod_name) + + if not temp: + self.save_notes += "Enabled %s module in Apache\n" % mod_name + logger.info("Enabled Apache %s module", mod_name) + + # Modules can enable additional config files. Variables may be defined + # within these new configuration sections. + # Reload is not necessary as DUMP_RUN_CFG uses latest config. + self.parser.update_runtime_variables() + + def _enable_mod_debian(self, mod_name, temp): + """Assumes mods-available, mods-enabled layout.""" + # Generate reversal command. + # Try to be safe here... check that we can probably reverse before + # applying enmod command + if not util.exe_exists(self.conf("dismod")): + raise errors.MisconfigurationError( + "Unable to find a2dismod, please make sure a2enmod and " + "a2dismod are configured correctly for certbot.") + + self.reverter.register_undo_command( + temp, [self.conf("dismod"), "-f", mod_name]) + util.run_script([self.conf("enmod"), mod_name]) diff --git a/certbot-apache/certbot_apache/override_gentoo.py b/certbot-apache/certbot_apache/override_gentoo.py new file mode 100644 index 000000000..92f1d4a20 --- /dev/null +++ b/certbot-apache/certbot_apache/override_gentoo.py @@ -0,0 +1,66 @@ +""" Distribution specific override class for Gentoo Linux """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import apache_util +from certbot_apache import configurator +from certbot_apache import parser + +@zope.interface.provider(interfaces.IPluginFactory) +class GentooConfigurator(configurator.ApacheConfigurator): + """Gentoo specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/vhosts.d", + vhost_files="*.conf", + logs_root="/var/log/apache2", + version_cmd=['/usr/sbin/apache2', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2/vhosts.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) + + def get_parser(self): + """Initializes the ApacheParser""" + return GentooParser( + self.aug, self.conf("server-root"), self.conf("vhost-root"), + self.version, configurator=self) + + +class GentooParser(parser.ApacheParser): + """Gentoo specific ApacheParser override class""" + def __init__(self, *args, **kwargs): + # Gentoo specific configuration file for Apache2 + self.apacheconfig_filep = "/etc/conf.d/apache2" + super(GentooParser, self).__init__(*args, **kwargs) + + def update_runtime_variables(self): + """ Override for update_runtime_variables for custom parsing """ + self.parse_sysconfig_var() + self.update_modules() + + def parse_sysconfig_var(self): + """ Parses Apache CLI options from Gentoo configuration file """ + defines = apache_util.parse_define_file(self.apacheconfig_filep, + "APACHE2_OPTS") + for k in defines.keys(): + self.variables[k] = defines[k] + + def update_modules(self): + """Get loaded modules from httpd process, and add them to DOM""" + mod_cmd = [self.configurator.constant("apache_cmd"), "modules"] + matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module") + for mod in matches: + self.add_mod(mod.strip()) diff --git a/certbot-apache/certbot_apache/override_suse.py b/certbot-apache/certbot_apache/override_suse.py new file mode 100644 index 000000000..a67054b5b --- /dev/null +++ b/certbot-apache/certbot_apache/override_suse.py @@ -0,0 +1,31 @@ +""" Distribution specific override class for OpenSUSE """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import configurator + +@zope.interface.provider(interfaces.IPluginFactory) +class OpenSUSEConfigurator(configurator.ApacheConfigurator): + """OpenSUSE specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/vhosts.d", + vhost_files="*.conf", + logs_root="/var/log/apache2", + version_cmd=['apache2ctl', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod="a2enmod", + dismod="a2dismod", + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2/vhosts.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) diff --git a/certbot-apache/certbot_apache/parser.py b/certbot-apache/certbot_apache/parser.py index b15608d61..d7da1e55e 100644 --- a/certbot-apache/certbot_apache/parser.py +++ b/certbot-apache/certbot_apache/parser.py @@ -11,8 +11,6 @@ import six from certbot import errors -from certbot_apache import constants - logger = logging.getLogger(__name__) @@ -40,14 +38,9 @@ class ApacheParser(object): # issues with aug.load() after adding new files / defines to parse tree self.configurator = configurator - # This uses the binary, so it can be done first. - # https://httpd.apache.org/docs/2.4/mod/core.html#define - # https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine - # This only handles invocation parameters and Define directives! + self.modules = set() self.parser_paths = {} self.variables = {} - if version >= (2, 4): - self.update_runtime_variables() self.aug = aug # Find configuration root and make sure augeas can parse it. @@ -55,24 +48,26 @@ class ApacheParser(object): self.loc = {"root": self._find_config_root()} self.parse_file(self.loc["root"]) + if version >= (2, 4): + # Look up variables from httpd and add to DOM if not already parsed + self.update_runtime_variables() + # This problem has been fixed in Augeas 1.0 self.standardize_excl() - # Temporarily set modules to be empty, so that find_dirs can work - # https://httpd.apache.org/docs/2.4/mod/core.html#ifmodule - # This needs to come before locations are set. - self.modules = set() - self.init_modules() + # Parse LoadModule directives from configuration files + self.parse_modules() # Set up rest of locations self.loc.update(self._set_locations()) + # list of the active include paths, before modifications self.existing_paths = copy.deepcopy(self.parser_paths) # Must also attempt to parse additional virtual host root if vhostroot: self.parse_file(os.path.abspath(vhostroot) + "/" + - constants.os_constant("vhost_files")) + self.configurator.constant("vhost_files")) # check to see if there were unparsed define statements if version < (2, 4): @@ -103,50 +98,61 @@ class ApacheParser(object): # Create a new path self.existing_paths[new_dir] = [new_file] - def init_modules(self): + def add_mod(self, mod_name): + """Shortcut for updating parser modules.""" + if mod_name + "_module" not in self.modules: + self.modules.add(mod_name + "_module") + if "mod_" + mod_name + ".c" not in self.modules: + self.modules.add("mod_" + mod_name + ".c") + + def reset_modules(self): + """Reset the loaded modules list. This is called from cleanup to clear + temporarily loaded modules.""" + self.modules = set() + self.update_modules() + self.parse_modules() + + def parse_modules(self): """Iterates on the configuration until no new modules are loaded. ..todo:: This should be attempted to be done with a binary to avoid the iteration issue. Else... parse and enable mods at same time. """ - # Since modules are being initiated... clear existing set. - self.modules = set() + mods = set() matches = self.find_dir("LoadModule") - iterator = iter(matches) # Make sure prev_size != cur_size for do: while: iteration prev_size = -1 - while len(self.modules) != prev_size: - prev_size = len(self.modules) + while len(mods) != prev_size: + prev_size = len(mods) for match_name, match_filename in six.moves.zip( iterator, iterator): mod_name = self.get_arg(match_name) mod_filename = self.get_arg(match_filename) if mod_name and mod_filename: - self.modules.add(mod_name) - self.modules.add(os.path.basename(mod_filename)[:-2] + "c") + mods.add(mod_name) + mods.add(os.path.basename(mod_filename)[:-2] + "c") else: logger.debug("Could not read LoadModule directive from " + "Augeas path: {0}".format(match_name[6:])) + self.modules.update(mods) def update_runtime_variables(self): - """" + """Update Includes, Defines and Includes from httpd config dump data""" + self.update_defines() + self.update_includes() + self.update_modules() - .. note:: Compile time variables (apache2ctl -V) are not used within - the dynamic configuration files. These should not be parsed or - interpreted. - - .. todo:: Create separate compile time variables... - simply for arg_get() - - """ - stdout = self._get_runtime_cfg() + def update_defines(self): + """Get Defines from httpd process""" variables = dict() - matches = re.compile(r"Define: ([^ \n]*)").findall(stdout) + define_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D", + "DUMP_RUN_CFG"] + matches = self.parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)") try: matches.remove("DUMP_RUN_CFG") except ValueError: @@ -163,15 +169,54 @@ class ApacheParser(object): self.variables = variables - def _get_runtime_cfg(self): # pylint: disable=no-self-use - """Get runtime configuration info. + def update_includes(self): + """Get includes from httpd process, and add them to DOM if needed""" - :returns: stdout from DUMP_RUN_CFG + # Find_dir iterates over configuration for Include and IncludeOptional + # directives to make sure we see the full include tree present in the + # configuration files + _ = self.find_dir("Include") + + inc_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D", + "DUMP_INCLUDES"] + matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)") + if matches: + for i in matches: + if not self.parsed_in_current(i): + self.parse_file(i) + + def update_modules(self): + """Get loaded modules from httpd process, and add them to DOM""" + + mod_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D", + "DUMP_MODULES"] + matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module") + for mod in matches: + self.add_mod(mod.strip()) + + def parse_from_subprocess(self, command, regexp): + """Get values from stdout of subprocess command + + :param list command: Command to run + :param str regexp: Regexp for parsing + + :returns: list parsed from command output + :rtype: list + + """ + stdout = self._get_runtime_cfg(command) + return re.compile(regexp).findall(stdout) + + def _get_runtime_cfg(self, command): # pylint: disable=no-self-use + """Get runtime configuration info. + :param command: Command to run + + :returns: stdout from command """ try: proc = subprocess.Popen( - constants.os_constant("define_cmd"), + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) @@ -180,10 +225,10 @@ class ApacheParser(object): except (OSError, ValueError): logger.error( "Error running command %s for runtime parameters!%s", - constants.os_constant("define_cmd"), os.linesep) + command, os.linesep) raise errors.MisconfigurationError( "Error accessing loaded Apache parameters: %s", - constants.os_constant("define_cmd")) + command) # Small errors that do not impede if proc.returncode != 0: logger.warning("Error in checking parameter list: %s", stderr) @@ -287,6 +332,23 @@ class ApacheParser(object): else: self.aug.set(aug_conf_path + "/directive[last()]/arg", args) + def add_dir_beginning(self, aug_conf_path, dirname, args): + """Adds the directive to the beginning of defined aug_conf_path. + + :param str aug_conf_path: Augeas configuration path to add directive + :param str dirname: Directive to add + :param args: Value of the directive. ie. Listen 443, 443 is arg + :type args: list or str + """ + first_dir = aug_conf_path + "/directive[1]" + self.aug.insert(first_dir, "directive", True) + self.aug.set(first_dir, dirname) + if isinstance(args, list): + for i, value in enumerate(args, 1): + self.aug.set(first_dir + "/arg[%d]" % (i), value) + else: + self.aug.set(first_dir + "/arg", args) + def find_dir(self, directive, arg=None, start=None, exclude=True): """Finds directive in the configuration. diff --git a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py index 742afce2d..c121ecdf3 100644 --- a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py +++ b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py @@ -13,7 +13,6 @@ from certbot_apache.tests import util class AugeasConfiguratorTest(util.ApacheTest): """Test for Augeas Configurator base class.""" - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=arguments-differ super(AugeasConfiguratorTest, self).setUp() diff --git a/certbot-apache/certbot_apache/tests/centos_test.py b/certbot-apache/certbot_apache/tests/centos_test.py new file mode 100644 index 000000000..d7a2a2fd9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/centos_test.py @@ -0,0 +1,125 @@ +"""Test for certbot_apache.configurator for Centos overrides""" +import os +import unittest + +import mock + +from certbot_apache import obj +from certbot_apache import override_centos +from certbot_apache.tests import util + +def get_vh_truth(temp_dir, config_name): + """Return the ground truth for the specified directory.""" + prefix = os.path.join( + temp_dir, config_name, "httpd/conf.d") + + aug_pre = "/files" + prefix + vh_truth = [ + obj.VirtualHost( + os.path.join(prefix, "centos.example.com.conf"), + os.path.join(aug_pre, "centos.example.com.conf/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), + False, True, "centos.example.com"), + obj.VirtualHost( + os.path.join(prefix, "ssl.conf"), + os.path.join(aug_pre, "ssl.conf/VirtualHost"), + set([obj.Addr.fromstring("_default_:443")]), + True, True, None) + ] + return vh_truth + +class MultipleVhostsTestCentOS(util.ApacheTest): + """Multiple vhost tests for CentOS / RHEL family of distros""" + + _multiprocess_can_split_ = True + + def setUp(self): # pylint: disable=arguments-differ + test_dir = "centos7_apache/apache" + config_root = "centos7_apache/apache/httpd" + vhost_root = "centos7_apache/apache/httpd/conf.d" + super(MultipleVhostsTestCentOS, self).setUp(test_dir=test_dir, + config_root=config_root, + vhost_root=vhost_root) + + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir, + os_info="centos") + self.vh_truth = get_vh_truth( + self.temp_dir, "centos7_apache/apache") + + def test_get_parser(self): + self.assertTrue(isinstance(self.config.parser, + override_centos.CentOSParser)) + + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_opportunistic_httpd_runtime_parsing(self, mock_get): + define_val = ( + 'Define: TEST1\n' + 'Define: TEST2\n' + 'Define: DUMP_RUN_CFG\n' + ) + mod_val = ( + 'Loaded Modules:\n' + ' mock_module (static)\n' + ' another_module (static)\n' + ) + def mock_get_cfg(command): + """Mock httpd process stdout""" + if command == ['apachectl', '-t', '-D', 'DUMP_RUN_CFG']: + return define_val + elif command == ['apachectl', '-t', '-D', 'DUMP_MODULES']: + return mod_val + return "" + mock_get.side_effect = mock_get_cfg + self.config.parser.modules = set() + self.config.parser.variables = {} + + with mock.patch("certbot.util.get_os_info") as mock_osi: + # Make sure we have the have the CentOS httpd constants + mock_osi.return_value = ("centos", "7") + self.config.parser.update_runtime_variables() + + self.assertEquals(mock_get.call_count, 3) + self.assertEquals(len(self.config.parser.modules), 4) + self.assertEquals(len(self.config.parser.variables), 2) + self.assertTrue("TEST2" in self.config.parser.variables.keys()) + self.assertTrue("mod_another.c" in self.config.parser.modules) + + def test_get_virtual_hosts(self): + """Make sure all vhosts are being properly found.""" + vhs = self.config.get_virtual_hosts() + self.assertEqual(len(vhs), 2) + found = 0 + + for vhost in vhs: + for centos_truth in self.vh_truth: + if vhost == centos_truth: + found += 1 + break + else: + raise Exception("Missed: %s" % vhost) # pragma: no cover + self.assertEqual(found, 2) + + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_get_sysconfig_vars(self, mock_cfg): + """Make sure we read the sysconfig OPTIONS variable correctly""" + # Return nothing for the process calls + mock_cfg.return_value = "" + self.config.parser.sysconfig_filep = os.path.realpath( + os.path.join(self.config.parser.root, "../sysconfig/httpd")) + self.config.parser.variables = {} + + with mock.patch("certbot.util.get_os_info") as mock_osi: + # Make sure we have the have the CentOS httpd constants + mock_osi.return_value = ("centos", "7") + self.config.parser.update_runtime_variables() + + self.assertTrue("mock_define" in self.config.parser.variables.keys()) + self.assertTrue("mock_define_too" in self.config.parser.variables.keys()) + self.assertTrue("mock_value" in self.config.parser.variables.keys()) + self.assertEqual("TRUE", self.config.parser.variables["mock_value"]) + self.assertTrue("MOCK_NOSEP" in self.config.parser.variables.keys()) + self.assertEqual("NOSEP_VAL", self.config.parser.variables["NOSEP_TWO"]) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/complex_parsing_test.py b/certbot-apache/certbot_apache/tests/complex_parsing_test.py index 079d7e95f..a296fb0eb 100644 --- a/certbot-apache/certbot_apache/tests/complex_parsing_test.py +++ b/certbot-apache/certbot_apache/tests/complex_parsing_test.py @@ -18,7 +18,7 @@ class ComplexParserTest(util.ParserTest): self.setup_variables() # This needs to happen after due to setup_variables not being run # until after - self.parser.init_modules() # pylint: disable=protected-access + self.parser.parse_modules() # pylint: disable=protected-access def tearDown(self): shutil.rmtree(self.temp_dir) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 1d6dd9b05..8f34d33d3 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -3,12 +3,12 @@ import os import shutil import socket +import tempfile import unittest import mock # six is used in mock.patch() import six # pylint: disable=unused-import -import tempfile from acme import challenges @@ -19,7 +19,7 @@ from certbot import errors from certbot.tests import acme_util from certbot.tests import util as certbot_util -from certbot_apache import configurator +from certbot_apache import apache_util from certbot_apache import constants from certbot_apache import parser from certbot_apache import obj @@ -30,44 +30,28 @@ from certbot_apache.tests import util class MultipleVhostsTest(util.ApacheTest): """Test two standard well-configured HTTP vhosts.""" - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=arguments-differ super(MultipleVhostsTest, self).setUp() - from certbot_apache.constants import os_constant - orig_os_constant = os_constant - def mock_os_constant(key, vhost_path=self.vhost_path): - """Mock default vhost path""" - if key == "vhost_root": - return vhost_path - else: - return orig_os_constant(key) - - with mock.patch("certbot_apache.constants.os_constant") as mock_c: - mock_c.side_effect = mock_os_constant - self.config = util.get_apache_configurator( - self.config_path, None, self.config_dir, self.work_dir) - self.config = self.mock_deploy_cert(self.config) + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir) + self.config = self.mock_deploy_cert(self.config) self.vh_truth = util.get_vh_truth( self.temp_dir, "debian_apache_2_4/multiple_vhosts") def mock_deploy_cert(self, config): """A test for a mock deploy cert""" - self.config.real_deploy_cert = self.config.deploy_cert + config.real_deploy_cert = self.config.deploy_cert def mocked_deploy_cert(*args, **kwargs): """a helper to mock a deployed cert""" - with mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod"): + g_mod = "certbot_apache.configurator.ApacheConfigurator.enable_mod" + with mock.patch(g_mod): config.real_deploy_cert(*args, **kwargs) self.config.deploy_cert = mocked_deploy_cert return self.config - def tearDown(self): - shutil.rmtree(self.temp_dir) - shutil.rmtree(self.config_dir) - shutil.rmtree(self.work_dir) - @mock.patch("certbot_apache.configurator.ApacheConfigurator.init_augeas") @mock.patch("certbot_apache.configurator.path_surgery") def test_prepare_no_install(self, mock_surgery, _init_augeas): @@ -131,6 +115,10 @@ class MultipleVhostsTest(util.ApacheTest): # Weak test.. ApacheConfigurator.add_parser_arguments(mock.MagicMock()) + def test_constant(self): + self.assertEqual(self.config.constant("server_root"), "/etc/apache2") + self.assertEqual(self.config.constant("nonexistent"), None) + @certbot_util.patch_get_utility() def test_get_all_names(self, mock_getutility): mock_utility = mock_getutility() @@ -138,7 +126,7 @@ class MultipleVhostsTest(util.ApacheTest): names = self.config.get_all_names() self.assertEqual(names, set( ["certbot.demo", "ocspvhost.com", "encryption-example.demo", - "nonsym.link", "vhost.in.rootconf"] + "nonsym.link", "vhost.in.rootconf", "www.certbot.demo"] )) @certbot_util.patch_get_utility() @@ -158,19 +146,18 @@ class MultipleVhostsTest(util.ApacheTest): names = self.config.get_all_names() # Names get filtered, only 5 are returned - self.assertEqual(len(names), 7) + self.assertEqual(len(names), 8) self.assertTrue("zombo.com" in names) self.assertTrue("google.com" in names) self.assertTrue("certbot.demo" in names) def test_get_bad_path(self): - from certbot_apache.configurator import get_file_path - self.assertEqual(get_file_path(None), None) - self.assertEqual(get_file_path("nonexistent"), None) + self.assertEqual(apache_util.get_file_path(None), None) + self.assertEqual(apache_util.get_file_path("nonexistent"), None) self.assertEqual(self.config._create_vhost("nonexistent"), None) # pylint: disable=protected-access def test_get_aug_internal_path(self): - from certbot_apache.configurator import get_internal_aug_path + from certbot_apache.apache_util import get_internal_aug_path internal_paths = [ "Virtualhost", "IfModule/VirtualHost", "VirtualHost", "VirtualHost", "Macro/VirtualHost", "IfModule/VirtualHost", "VirtualHost", @@ -273,6 +260,20 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.PluginError, self.config.choose_vhost, "none.com") + def test_find_best_http_vhost_default(self): + vh = obj.VirtualHost( + "fp", "ap", set([obj.Addr.fromstring("_default_:80")]), False, True) + self.config.vhosts = [vh] + self.assertEqual(self.config.find_best_http_vhost("foo.bar", False), vh) + + def test_find_best_http_vhost_port(self): + port = "8080" + vh = obj.VirtualHost( + "fp", "ap", set([obj.Addr.fromstring("*:" + port)]), + False, True, "encryption-example.demo") + self.config.vhosts.append(vh) + self.assertEqual(self.config.find_best_http_vhost("foo.bar", False, port), vh) + def test_findbest_continues_on_short_domain(self): # pylint: disable=protected-access chosen_vhost = self.config._find_best_vhost("purple.com") @@ -318,192 +319,53 @@ class MultipleVhostsTest(util.ApacheTest): def test_non_default_vhosts(self): # pylint: disable=protected-access - self.assertEqual(len(self.config._non_default_vhosts()), 8) - - @mock.patch("certbot.util.run_script") - @mock.patch("certbot.util.exe_exists") - @mock.patch("certbot_apache.parser.subprocess.Popen") - def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script): - mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "") - mock_popen().returncode = 0 - mock_exe_exists.return_value = True - - self.config.enable_mod("ssl") - self.assertTrue("ssl_module" in self.config.parser.modules) - self.assertTrue("mod_ssl.c" in self.config.parser.modules) - - self.assertTrue(mock_run_script.called) - - def test_enable_mod_unsupported_dirs(self): - shutil.rmtree(os.path.join(self.config.parser.root, "mods-enabled")) - self.assertRaises( - errors.NotSupportedError, self.config.enable_mod, "ssl") - - @mock.patch("certbot.util.exe_exists") - def test_enable_mod_no_disable(self, mock_exe_exists): - mock_exe_exists.return_value = False - self.assertRaises( - errors.MisconfigurationError, self.config.enable_mod, "ssl") - - def test_enable_site_already_enabled(self): - self.assertTrue(self.vh_truth[1].enabled) - self.config.enable_site(self.vh_truth[1]) - - def test_enable_site_failure(self): - self.config.parser.root = "/tmp/nonexistent" - self.assertRaises( - errors.NotSupportedError, - self.config.enable_site, - obj.VirtualHost("asdf", "afsaf", set(), False, False)) - - def test_enable_site_nondebian(self): - mock_c = "certbot_apache.configurator.ApacheConfigurator.conf" - def conf_side_effect(arg): - """ Mock function for ApacheConfigurator.conf """ - confvars = {"handle-sites": False} - if arg in confvars: - return confvars[arg] - inc_path = "/path/to/whereever" - vhost = self.vh_truth[0] - with mock.patch(mock_c) as mock_conf: - mock_conf.side_effect = conf_side_effect - vhost.enabled = False - vhost.filep = inc_path - self.assertFalse(self.config.parser.find_dir("Include", inc_path)) - self.assertFalse( - os.path.dirname(inc_path) in self.config.parser.existing_paths) - self.config.enable_site(vhost) - self.assertTrue(self.config.parser.find_dir("Include", inc_path)) - self.assertTrue( - os.path.dirname(inc_path) in self.config.parser.existing_paths) - self.assertTrue( - os.path.basename(inc_path) in self.config.parser.existing_paths[ - os.path.dirname(inc_path)]) + vhosts = self.config._non_default_vhosts(self.config.vhosts) + self.assertEqual(len(vhosts), 8) def test_deploy_cert_enable_new_vhost(self): # Create ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) self.config.parser.modules.add("ssl_module") self.config.parser.modules.add("mod_ssl.c") - self.assertFalse(ssl_vhost.enabled) - self.config.deploy_cert( - "encryption-example.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.assertTrue(ssl_vhost.enabled) - # Make sure that we don't error out if symlink already exists - ssl_vhost.enabled = False + self.config.parser.modules.add("socache_shmcb_module") + self.assertFalse(ssl_vhost.enabled) self.config.deploy_cert( "encryption-example.demo", "example/cert.pem", "example/key.pem", "example/cert_chain.pem", "example/fullchain.pem") self.assertTrue(ssl_vhost.enabled) - def test_deploy_cert_newssl(self): - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, - self.work_dir, version=(2, 4, 16)) + def test_no_duplicate_include(self): + def mock_find_dir(directive, argument, _): + """Mock method for parser.find_dir""" + if directive == "Include" and argument.endswith("options-ssl-apache.conf"): + return ["/path/to/whatever"] - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") + mock_add = mock.MagicMock() + self.config.parser.add_dir = mock_add + self.config._add_dummy_ssl_directives(self.vh_truth[0]) # pylint: disable=protected-access + tried_to_add = False + for a in mock_add.call_args_list: + if a[0][1] == "Include" and a[0][2] == self.config.mod_ssl_conf: + tried_to_add = True + # Include should be added, find_dir is not patched, and returns falsy + self.assertTrue(tried_to_add) - # Get the default 443 vhost - self.config.assoc["random.demo"] = self.vh_truth[1] - self.config = self.mock_deploy_cert(self.config) - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - - # Verify ssl_module was enabled. - self.assertTrue(self.vh_truth[1].enabled) - self.assertTrue("ssl_module" in self.config.parser.modules) - - loc_cert = self.config.parser.find_dir( - "sslcertificatefile", "example/fullchain.pem", - self.vh_truth[1].path) - loc_key = self.config.parser.find_dir( - "sslcertificateKeyfile", "example/key.pem", self.vh_truth[1].path) - - # Verify one directive was found in the correct file - self.assertEqual(len(loc_cert), 1) - self.assertEqual( - configurator.get_file_path(loc_cert[0]), - self.vh_truth[1].filep) - - self.assertEqual(len(loc_key), 1) - self.assertEqual( - configurator.get_file_path(loc_key[0]), - self.vh_truth[1].filep) - - def test_deploy_cert_newssl_no_fullchain(self): - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, - self.work_dir, version=(2, 4, 16)) - self.config = self.mock_deploy_cert(self.config) - - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") - - # Get the default 443 vhost - self.config.assoc["random.demo"] = self.vh_truth[1] - self.assertRaises(errors.PluginError, - lambda: self.config.deploy_cert( - "random.demo", "example/cert.pem", - "example/key.pem")) - - def test_deploy_cert_old_apache_no_chain(self): - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, - self.work_dir, version=(2, 4, 7)) - self.config = self.mock_deploy_cert(self.config) - - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") - - # Get the default 443 vhost - self.config.assoc["random.demo"] = self.vh_truth[1] - self.assertRaises(errors.PluginError, - lambda: self.config.deploy_cert( - "random.demo", "example/cert.pem", - "example/key.pem")) - - def test_deploy_cert_not_parsed_path(self): - # Make sure that we add include to root config for vhosts when - # handle-sites is false - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") - tmp_path = os.path.realpath(tempfile.mkdtemp("vhostroot")) - os.chmod(tmp_path, 0o755) - mock_p = "certbot_apache.configurator.ApacheConfigurator._get_ssl_vhost_path" - mock_a = "certbot_apache.parser.ApacheParser.add_include" - mock_c = "certbot_apache.configurator.ApacheConfigurator.conf" - orig_conf = self.config.conf - def conf_side_effect(arg): - """ Mock function for ApacheConfigurator.conf """ - confvars = {"handle-sites": False} - if arg in confvars: - return confvars[arg] - else: - return orig_conf("arg") - - with mock.patch(mock_c) as mock_conf: - mock_conf.side_effect = conf_side_effect - with mock.patch(mock_p) as mock_path: - mock_path.return_value = os.path.join(tmp_path, "whatever.conf") - with mock.patch(mock_a) as mock_add: - self.config.deploy_cert( - "encryption-example.demo", - "example/cert.pem", "example/key.pem", - "example/cert_chain.pem") - # Test that we actually called add_include - self.assertTrue(mock_add.called) - shutil.rmtree(tmp_path) + self.config.parser.find_dir = mock_find_dir + mock_add.reset_mock() + self.config._add_dummy_ssl_directives(self.vh_truth[0]) # pylint: disable=protected-access + tried_to_add = [] + for a in mock_add.call_args_list: + tried_to_add.append(a[0][1] == "Include" and + a[0][2] == self.config.mod_ssl_conf) + # Include shouldn't be added, as patched find_dir "finds" existing one + self.assertFalse(any(tried_to_add)) def test_deploy_cert(self): self.config.parser.modules.add("ssl_module") self.config.parser.modules.add("mod_ssl.c") - + self.config.parser.modules.add("socache_shmcb_module") # Patch _add_dummy_ssl_directives to make sure we write them correctly # pylint: disable=protected-access orig_add_dummy = self.config._add_dummy_ssl_directives @@ -532,7 +394,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue( "insert_key_file_path" in find_args(vhostpath, "SSLCertificateKeyFile")) - # pylint: disable=protected-access self.config._add_dummy_ssl_directives = mock_add_dummy_ssl @@ -558,17 +419,17 @@ class MultipleVhostsTest(util.ApacheTest): # Verify one directive was found in the correct file self.assertEqual(len(loc_cert), 1) self.assertEqual( - configurator.get_file_path(loc_cert[0]), + apache_util.get_file_path(loc_cert[0]), self.vh_truth[1].filep) self.assertEqual(len(loc_key), 1) self.assertEqual( - configurator.get_file_path(loc_key[0]), + apache_util.get_file_path(loc_key[0]), self.vh_truth[1].filep) self.assertEqual(len(loc_chain), 1) self.assertEqual( - configurator.get_file_path(loc_chain[0]), + apache_util.get_file_path(loc_chain[0]), self.vh_truth[1].filep) # One more time for chain directive setting @@ -605,6 +466,47 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue(self.config.parser.find_dir( "NameVirtualHost", "*:80")) + def test_add_listen_80(self): + mock_find = mock.Mock() + mock_add_dir = mock.Mock() + mock_find.return_value = [] + self.config.parser.find_dir = mock_find + self.config.parser.add_dir = mock_add_dir + self.config.ensure_listen("80") + self.assertTrue(mock_add_dir.called) + self.assertTrue(mock_find.called) + self.assertEqual(mock_add_dir.call_args[0][1], "Listen") + self.assertEqual(mock_add_dir.call_args[0][2], "80") + + def test_add_listen_80_named(self): + mock_find = mock.Mock() + mock_find.return_value = ["test1", "test2", "test3"] + mock_get = mock.Mock() + mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"] + mock_add_dir = mock.Mock() + + self.config.parser.find_dir = mock_find + self.config.parser.get_arg = mock_get + self.config.parser.add_dir = mock_add_dir + + self.config.ensure_listen("80") + self.assertEqual(mock_add_dir.call_count, 0) + + # Reset return lists and inputs + mock_add_dir.reset_mock() + mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"] + + # Test + self.config.ensure_listen("8080") + self.assertEqual(mock_add_dir.call_count, 3) + self.assertTrue(mock_add_dir.called) + self.assertEqual(mock_add_dir.call_args[0][1], "Listen") + call_found = False + for mock_call in mock_add_dir.mock_calls: + if mock_call[1][2] == ['1.2.3.4:8080']: + call_found = True + self.assertTrue(call_found) + def test_prepare_server_https(self): mock_enable = mock.Mock() self.config.enable_mod = mock_enable @@ -616,7 +518,6 @@ class MultipleVhostsTest(util.ApacheTest): # This will test the Add listen self.config.parser.find_dir = mock_find self.config.parser.add_dir_to_ifmodssl = mock_add_dir - self.config.prepare_server_https("443") # Changing the order these modules are enabled breaks the reverter self.assertEqual(mock_enable.call_args_list[0][0][0], "socache_shmcb") @@ -857,49 +758,66 @@ class MultipleVhostsTest(util.ApacheTest): self.config._add_name_vhost_if_necessary(self.vh_truth[0]) self.assertEqual(self.config.add_name_vhost.call_count, 2) + @mock.patch("certbot_apache.configurator.http_01.ApacheHttp01.perform") @mock.patch("certbot_apache.configurator.tls_sni_01.ApacheTlsSni01.perform") @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") - def test_perform(self, mock_restart, mock_perform): + def test_perform(self, mock_restart, mock_tls_perform, mock_http_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - account_key, achall1, achall2 = self.get_achalls() + account_key, achalls = self.get_key_and_achalls() - expected = [ - achall1.response(account_key), - achall2.response(account_key), - ] + all_expected = [] + http_expected = [] + tls_expected = [] + for achall in achalls: + response = achall.response(account_key) + if isinstance(achall.chall, challenges.HTTP01): + http_expected.append(response) + else: + tls_expected.append(response) + all_expected.append(response) - mock_perform.return_value = expected - responses = self.config.perform([achall1, achall2]) + mock_http_perform.return_value = http_expected + mock_tls_perform.return_value = tls_expected - self.assertEqual(mock_perform.call_count, 1) - self.assertEqual(responses, expected) + responses = self.config.perform(achalls) + + self.assertEqual(mock_http_perform.call_count, 1) + self.assertEqual(mock_tls_perform.call_count, 1) + self.assertEqual(responses, all_expected) self.assertEqual(mock_restart.call_count, 1) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") - def test_cleanup(self, mock_restart): - _, achall1, achall2 = self.get_achalls() + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_cleanup(self, mock_cfg, mock_restart): + mock_cfg.return_value = "" + _, achalls = self.get_key_and_achalls() - self.config._chall_out.add(achall1) # pylint: disable=protected-access - self.config._chall_out.add(achall2) # pylint: disable=protected-access + for achall in achalls: + self.config._chall_out.add(achall) # pylint: disable=protected-access - self.config.cleanup([achall1]) - self.assertFalse(mock_restart.called) - - self.config.cleanup([achall2]) - self.assertTrue(mock_restart.called) + for i, achall in enumerate(achalls): + self.config.cleanup([achall]) + if i == len(achalls) - 1: + self.assertTrue(mock_restart.called) + else: + self.assertFalse(mock_restart.called) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") - def test_cleanup_no_errors(self, mock_restart): - _, achall1, achall2 = self.get_achalls() + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_cleanup_no_errors(self, mock_cfg, mock_restart): + mock_cfg.return_value = "" + _, achalls = self.get_key_and_achalls() + self.config.http_doer = mock.MagicMock() - self.config._chall_out.add(achall1) # pylint: disable=protected-access + for achall in achalls: + self.config._chall_out.add(achall) # pylint: disable=protected-access - self.config.cleanup([achall2]) + self.config.cleanup([achalls[-1]]) self.assertFalse(mock_restart.called) - self.config.cleanup([achall1, achall2]) + self.config.cleanup(achalls) self.assertTrue(mock_restart.called) @mock.patch("certbot.util.run_script") @@ -952,10 +870,9 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue(isinstance(self.config.get_chall_pref(""), list)) def test_install_ssl_options_conf(self): - from certbot_apache.configurator import install_ssl_options_conf path = os.path.join(self.work_dir, "test_it") other_path = os.path.join(self.work_dir, "other_test_it") - install_ssl_options_conf(path, other_path) + self.config.install_ssl_options_conf(path, other_path) self.assertTrue(os.path.isfile(path)) self.assertTrue(os.path.isfile(other_path)) @@ -995,20 +912,17 @@ class MultipleVhostsTest(util.ApacheTest): errors.PluginError, self.config.enhance, "certbot.demo", "unknown_enhancement") - @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") - def test_ocsp_stapling(self, mock_exe, mock_run_script): + def test_ocsp_stapling(self, mock_exe): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") self.config.get_version = mock.Mock(return_value=(2, 4, 7)) mock_exe.return_value = True # This will create an ssl vhost for certbot.demo self.config.enhance("certbot.demo", "staple-ocsp") - self.assertTrue("socache_shmcb_module" in self.config.parser.modules) - self.assertTrue(mock_run_script.called) - # Get the ssl vhost for certbot.demo ssl_vhost = self.config.assoc["certbot.demo"] @@ -1078,14 +992,13 @@ class MultipleVhostsTest(util.ApacheTest): def test_http_header_hsts(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("headers_module") mock_exe.return_value = True # This will create an ssl vhost for certbot.demo self.config.enhance("certbot.demo", "ensure-http-header", "Strict-Transport-Security") - self.assertTrue("headers_module" in self.config.parser.modules) - # Get the ssl vhost for certbot.demo ssl_vhost = self.config.assoc["certbot.demo"] @@ -1116,6 +1029,8 @@ class MultipleVhostsTest(util.ApacheTest): def test_http_header_uir(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("headers_module") + mock_exe.return_value = True # This will create an ssl vhost for certbot.demo @@ -1152,6 +1067,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") def test_redirect_well_formed_http(self, mock_exe, _): + self.config.parser.modules.add("rewrite_module") self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True self.config.get_version = mock.Mock(return_value=(2, 2)) @@ -1174,8 +1090,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path[:-3])) self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path[:-3])) - self.assertTrue("rewrite_module" in self.config.parser.modules) - def test_rewrite_rule_exists(self): # Skip the enable mod self.config.parser.modules.add("rewrite_module") @@ -1197,6 +1111,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") def test_redirect_with_existing_rewrite(self, mock_exe, _): + self.config.parser.modules.add("rewrite_module") self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True self.config.get_version = mock.Mock(return_value=(2, 2, 0)) @@ -1229,6 +1144,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") def test_redirect_with_old_https_redirection(self, mock_exe, _): + self.config.parser.modules.add("rewrite_module") self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True self.config.get_version = mock.Mock(return_value=(2, 2, 0)) @@ -1330,7 +1246,7 @@ class MultipleVhostsTest(util.ApacheTest): not_rewriterule = "NotRewriteRule ^ ..." self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule)) - def get_achalls(self): + def get_key_and_achalls(self): """Return testing achallenges.""" account_key = self.rsa512jwk achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( @@ -1345,8 +1261,12 @@ class MultipleVhostsTest(util.ApacheTest): token=b"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"), "pending"), domain="certbot.demo", account_key=account_key) + achall3 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=(b'x' * 16)), "pending"), + domain="example.org", account_key=account_key) - return account_key, achall1, achall2 + return account_key, (achall1, achall2, achall3) def test_make_addrs_sni_ready(self): self.config.version = (2, 2) @@ -1366,10 +1286,60 @@ class MultipleVhostsTest(util.ApacheTest): self.config.aug.match.side_effect = RuntimeError self.assertFalse(self.config._check_aug_version()) + def test_enable_site_nondebian(self): + inc_path = "/path/to/wherever" + vhost = self.vh_truth[0] + vhost.enabled = False + vhost.filep = inc_path + self.assertFalse(self.config.parser.find_dir("Include", inc_path)) + self.assertFalse( + os.path.dirname(inc_path) in self.config.parser.existing_paths) + self.config.enable_site(vhost) + self.assertTrue(self.config.parser.find_dir("Include", inc_path)) + self.assertTrue( + os.path.dirname(inc_path) in self.config.parser.existing_paths) + self.assertTrue( + os.path.basename(inc_path) in self.config.parser.existing_paths[ + os.path.dirname(inc_path)]) + + def test_deploy_cert_not_parsed_path(self): + # Make sure that we add include to root config for vhosts when + # handle-sites is false + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") + tmp_path = os.path.realpath(tempfile.mkdtemp("vhostroot")) + os.chmod(tmp_path, 0o755) + mock_p = "certbot_apache.configurator.ApacheConfigurator._get_ssl_vhost_path" + mock_a = "certbot_apache.parser.ApacheParser.add_include" + + with mock.patch(mock_p) as mock_path: + mock_path.return_value = os.path.join(tmp_path, "whatever.conf") + with mock.patch(mock_a) as mock_add: + self.config.deploy_cert( + "encryption-example.demo", + "example/cert.pem", "example/key.pem", + "example/cert_chain.pem") + # Test that we actually called add_include + self.assertTrue(mock_add.called) + shutil.rmtree(tmp_path) + + @mock.patch("certbot_apache.parser.ApacheParser.parsed_in_original") + def test_choose_vhost_and_servername_addition_parsed(self, mock_parsed): + ret_vh = self.vh_truth[8] + ret_vh.enabled = True + self.config.enable_site(ret_vh) + # Make sure that we return early + self.assertFalse(mock_parsed.called) + + def test_enable_mod_unsupported(self): + self.assertRaises(errors.MisconfigurationError, + self.config.enable_mod, + "whatever") + class AugeasVhostsTest(util.ApacheTest): - """Test vhosts with illegal names dependant on augeas version.""" + """Test vhosts with illegal names dependent on augeas version.""" # pylint: disable=protected-access - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=arguments-differ td = "debian_apache_2_4/augeas_vhosts" @@ -1380,12 +1350,8 @@ class AugeasVhostsTest(util.ApacheTest): vhost_root=vr) self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir) - - def tearDown(self): - shutil.rmtree(self.temp_dir) - shutil.rmtree(self.config_dir) - shutil.rmtree(self.work_dir) + self.config_path, self.vhost_path, self.config_dir, + self.work_dir) def test_choosevhost_with_illegal_name(self): self.config.aug = mock.MagicMock() @@ -1451,7 +1417,7 @@ class AugeasVhostsTest(util.ApacheTest): broken_vhost) class MultiVhostsTest(util.ApacheTest): - """Test vhosts with illegal names dependant on augeas version.""" + """Test vhosts with illegal names dependent on augeas version.""" # pylint: disable=protected-access def setUp(self): # pylint: disable=arguments-differ @@ -1463,15 +1429,11 @@ class MultiVhostsTest(util.ApacheTest): vhost_root=vr) self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir) + self.config_path, self.vhost_path, + self.config_dir, self.work_dir, conf_vhost_path=self.vhost_path) self.vh_truth = util.get_vh_truth( self.temp_dir, "debian_apache_2_4/multi_vhosts") - def tearDown(self): - shutil.rmtree(self.temp_dir) - shutil.rmtree(self.config_dir) - shutil.rmtree(self.work_dir) - def test_make_vhost_ssl(self): ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1]) @@ -1571,11 +1533,11 @@ class InstallSslOptionsConfTest(util.ApacheTest): self.config_path, self.vhost_path, self.config_dir, self.work_dir) def _call(self): - from certbot_apache.configurator import install_ssl_options_conf - install_ssl_options_conf(self.config.mod_ssl_conf, self.config.updated_mod_ssl_conf_digest) + self.config.install_ssl_options_conf(self.config.mod_ssl_conf, + self.config.updated_mod_ssl_conf_digest) def _current_ssl_options_hash(self): - return crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")) + return crypto_util.sha256sum(self.config.constant("MOD_SSL_CONF_SRC")) def _assert_current_file(self): self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) @@ -1610,7 +1572,8 @@ class InstallSslOptionsConfTest(util.ApacheTest): self._call() self.assertFalse(mock_logger.warning.called) self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) - self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self.assertEqual(crypto_util.sha256sum( + self.config.constant("MOD_SSL_CONF_SRC")), self._current_ssl_options_hash()) self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf), self._current_ssl_options_hash()) @@ -1625,7 +1588,8 @@ class InstallSslOptionsConfTest(util.ApacheTest): self.assertEqual(mock_logger.warning.call_args[0][0], "%s has been manually modified; updated file " "saved to %s. We recommend updating %s for security purposes.") - self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self.assertEqual(crypto_util.sha256sum( + self.config.constant("MOD_SSL_CONF_SRC")), self._current_ssl_options_hash()) # only print warning once with mock.patch("certbot.plugins.common.logger") as mock_logger: diff --git a/certbot-apache/certbot_apache/tests/constants_test.py b/certbot-apache/certbot_apache/tests/constants_test.py deleted file mode 100644 index 5ab324101..000000000 --- a/certbot-apache/certbot_apache/tests/constants_test.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Test for certbot_apache.configurator.""" - -import mock -import unittest - -from certbot_apache import constants - - -class ConstantsTest(unittest.TestCase): - - @mock.patch("certbot.util.get_os_info") - def test_get_debian_value(self, os_info): - os_info.return_value = ('Debian', '', '') - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/apache2/sites-available") - - @mock.patch("certbot.util.get_os_info") - def test_get_centos_value(self, os_info): - os_info.return_value = ('CentOS Linux', '', '') - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/httpd/conf.d") - - @mock.patch("certbot.util.get_systemd_os_like") - @mock.patch("certbot.util.get_os_info") - def test_get_default_values(self, os_info, os_like): - os_info.return_value = ('Nonexistent Linux', '', '') - os_like.return_value = {} - self.assertFalse(constants.os_constant("handle_mods")) - self.assertEqual(constants.os_constant("server_root"), "/etc/apache2") - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/apache2/sites-available") - - @mock.patch("certbot.util.get_systemd_os_like") - @mock.patch("certbot.util.get_os_info") - def test_get_darwin_like_values(self, os_info, os_like): - os_info.return_value = ('Nonexistent Linux', '', '') - os_like.return_value = ["something", "nonexistent", "darwin"] - self.assertFalse(constants.os_constant("enmod")) - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/apache2/other") - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/debian_test.py b/certbot-apache/certbot_apache/tests/debian_test.py new file mode 100644 index 000000000..a648101e9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/debian_test.py @@ -0,0 +1,209 @@ +"""Test for certbot_apache.configurator for Debian overrides""" +import os +import shutil +import unittest + +import mock + +from certbot import errors + +from certbot_apache import apache_util +from certbot_apache import obj +from certbot_apache.tests import util + + +class MultipleVhostsTestDebian(util.ApacheTest): + """Multiple vhost tests for Debian family of distros""" + + _multiprocess_can_split_ = True + + def setUp(self): # pylint: disable=arguments-differ + super(MultipleVhostsTestDebian, self).setUp() + self.config = util.get_apache_configurator( + self.config_path, None, self.config_dir, self.work_dir, + os_info="debian") + self.config = self.mock_deploy_cert(self.config) + self.vh_truth = util.get_vh_truth(self.temp_dir, + "debian_apache_2_4/multiple_vhosts") + + def mock_deploy_cert(self, config): + """A test for a mock deploy cert""" + config.real_deploy_cert = self.config.deploy_cert + + def mocked_deploy_cert(*args, **kwargs): + """a helper to mock a deployed cert""" + g_mod = "certbot_apache.configurator.ApacheConfigurator.enable_mod" + d_mod = "certbot_apache.override_debian.DebianConfigurator.enable_mod" + with mock.patch(g_mod): + with mock.patch(d_mod): + config.real_deploy_cert(*args, **kwargs) + self.config.deploy_cert = mocked_deploy_cert + return self.config + + def test_enable_mod_unsupported_dirs(self): + shutil.rmtree(os.path.join(self.config.parser.root, "mods-enabled")) + self.assertRaises( + errors.NotSupportedError, self.config.enable_mod, "ssl") + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + @mock.patch("certbot_apache.parser.subprocess.Popen") + def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script): + mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "") + mock_popen().returncode = 0 + mock_exe_exists.return_value = True + + self.config.enable_mod("ssl") + self.assertTrue("ssl_module" in self.config.parser.modules) + self.assertTrue("mod_ssl.c" in self.config.parser.modules) + + self.assertTrue(mock_run_script.called) + + def test_deploy_cert_enable_new_vhost(self): + # Create + ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + self.assertFalse(ssl_vhost.enabled) + self.config.deploy_cert( + "encryption-example.demo", "example/cert.pem", "example/key.pem", + "example/cert_chain.pem", "example/fullchain.pem") + self.assertTrue(ssl_vhost.enabled) + # Make sure that we don't error out if symlink already exists + ssl_vhost.enabled = False + self.assertFalse(ssl_vhost.enabled) + self.config.deploy_cert( + "encryption-example.demo", "example/cert.pem", "example/key.pem", + "example/cert_chain.pem", "example/fullchain.pem") + self.assertTrue(ssl_vhost.enabled) + + def test_enable_site_failure(self): + self.config.parser.root = "/tmp/nonexistent" + with mock.patch("os.path.isdir") as mock_dir: + mock_dir.return_value = True + with mock.patch("os.path.islink") as mock_link: + mock_link.return_value = False + self.assertRaises( + errors.NotSupportedError, + self.config.enable_site, + obj.VirtualHost("asdf", "afsaf", set(), False, False)) + + def test_deploy_cert_newssl(self): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, + self.work_dir, version=(2, 4, 16)) + self.config = self.mock_deploy_cert(self.config) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + + # Get the default 443 vhost + self.config.assoc["random.demo"] = self.vh_truth[1] + self.config.deploy_cert( + "random.demo", "example/cert.pem", "example/key.pem", + "example/cert_chain.pem", "example/fullchain.pem") + self.config.save() + + # Verify ssl_module was enabled. + self.assertTrue(self.vh_truth[1].enabled) + self.assertTrue("ssl_module" in self.config.parser.modules) + + loc_cert = self.config.parser.find_dir( + "sslcertificatefile", "example/fullchain.pem", + self.vh_truth[1].path) + loc_key = self.config.parser.find_dir( + "sslcertificateKeyfile", "example/key.pem", self.vh_truth[1].path) + + # Verify one directive was found in the correct file + self.assertEqual(len(loc_cert), 1) + self.assertEqual( + apache_util.get_file_path(loc_cert[0]), + self.vh_truth[1].filep) + + self.assertEqual(len(loc_key), 1) + self.assertEqual( + apache_util.get_file_path(loc_key[0]), + self.vh_truth[1].filep) + + def test_deploy_cert_newssl_no_fullchain(self): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, + self.work_dir, version=(2, 4, 16)) + self.config = self.mock_deploy_cert(self.config) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + + # Get the default 443 vhost + self.config.assoc["random.demo"] = self.vh_truth[1] + self.assertRaises(errors.PluginError, + lambda: self.config.deploy_cert( + "random.demo", "example/cert.pem", + "example/key.pem")) + + def test_deploy_cert_old_apache_no_chain(self): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, + self.work_dir, version=(2, 4, 7)) + self.config = self.mock_deploy_cert(self.config) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + + # Get the default 443 vhost + self.config.assoc["random.demo"] = self.vh_truth[1] + self.assertRaises(errors.PluginError, + lambda: self.config.deploy_cert( + "random.demo", "example/cert.pem", + "example/key.pem")) + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + def test_ocsp_stapling_enable_mod(self, mock_exe, _): + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + self.config.get_version = mock.Mock(return_value=(2, 4, 7)) + mock_exe.return_value = True + self.config.enhance("certbot.demo", "staple-ocsp") + self.assertTrue("socache_shmcb_module" in self.config.parser.modules) + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + def test_ensure_http_header_enable_mod(self, mock_exe, _): + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + mock_exe.return_value = True + + # This will create an ssl vhost for certbot.demo + self.config.enhance("certbot.demo", "ensure-http-header", + "Strict-Transport-Security") + self.assertTrue("headers_module" in self.config.parser.modules) + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + def test_redirect_enable_mod(self, mock_exe, _): + self.config.parser.update_runtime_variables = mock.Mock() + mock_exe.return_value = True + self.config.get_version = mock.Mock(return_value=(2, 2)) + # This will create an ssl vhost for certbot.demo + self.config.enhance("certbot.demo", "redirect") + self.assertTrue("rewrite_module" in self.config.parser.modules) + + def test_enable_site_already_enabled(self): + self.assertTrue(self.vh_truth[1].enabled) + self.config.enable_site(self.vh_truth[1]) + + def test_enable_site_call_parent(self): + with mock.patch( + "certbot_apache.configurator.ApacheConfigurator.enable_site") as e_s: + self.config.parser.root = "/tmp/nonexistent" + vh = self.vh_truth[0] + vh.enabled = False + self.config.enable_site(vh) + self.assertTrue(e_s.called) + + @mock.patch("certbot.util.exe_exists") + def test_enable_mod_no_disable(self, mock_exe_exists): + mock_exe_exists.return_value = False + self.assertRaises( + errors.MisconfigurationError, self.config.enable_mod, "ssl") + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/entrypoint_test.py b/certbot-apache/certbot_apache/tests/entrypoint_test.py new file mode 100644 index 000000000..c04611465 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/entrypoint_test.py @@ -0,0 +1,41 @@ +"""Test for certbot_apache.entrypoint for override class resolution""" +import unittest + +import mock + +from certbot_apache import configurator +from certbot_apache import entrypoint + +class EntryPointTest(unittest.TestCase): + """Entrypoint tests""" + + _multiprocess_can_split_ = True + + def test_get_configurator(self): + + with mock.patch("certbot.util.get_os_info") as mock_info: + for distro in entrypoint.OVERRIDE_CLASSES.keys(): + mock_info.return_value = (distro, "whatever") + self.assertEqual(entrypoint.get_configurator(), + entrypoint.OVERRIDE_CLASSES[distro]) + + def test_nonexistent_like(self): + with mock.patch("certbot.util.get_os_info") as mock_info: + mock_info.return_value = ("nonexistent", "irrelevant") + with mock.patch("certbot.util.get_systemd_os_like") as mock_like: + for like in entrypoint.OVERRIDE_CLASSES.keys(): + mock_like.return_value = [like] + self.assertEqual(entrypoint.get_configurator(), + entrypoint.OVERRIDE_CLASSES[like]) + + def test_nonexistent_generic(self): + with mock.patch("certbot.util.get_os_info") as mock_info: + mock_info.return_value = ("nonexistent", "irrelevant") + with mock.patch("certbot.util.get_systemd_os_like") as mock_like: + mock_like.return_value = ["unknonwn"] + self.assertEqual(entrypoint.get_configurator(), + configurator.ApacheConfigurator) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/gentoo_test.py b/certbot-apache/certbot_apache/tests/gentoo_test.py new file mode 100644 index 000000000..cfbaffac7 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/gentoo_test.py @@ -0,0 +1,127 @@ +"""Test for certbot_apache.configurator for Gentoo overrides""" +import os +import unittest + +import mock + +from certbot_apache import override_gentoo +from certbot_apache import obj +from certbot_apache.tests import util + +def get_vh_truth(temp_dir, config_name): + """Return the ground truth for the specified directory.""" + prefix = os.path.join( + temp_dir, config_name, "apache2/vhosts.d") + + aug_pre = "/files" + prefix + vh_truth = [ + obj.VirtualHost( + os.path.join(prefix, "gentoo.example.com.conf"), + os.path.join(aug_pre, "gentoo.example.com.conf/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), + False, True, "gentoo.example.com"), + obj.VirtualHost( + os.path.join(prefix, "00_default_vhost.conf"), + os.path.join(aug_pre, "00_default_vhost.conf/IfDefine/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), + False, True, "localhost"), + obj.VirtualHost( + os.path.join(prefix, "00_default_ssl_vhost.conf"), + os.path.join(aug_pre, + "00_default_ssl_vhost.conf" + + "/IfDefine/IfDefine/IfModule/VirtualHost"), + set([obj.Addr.fromstring("_default_:443")]), + True, True, "localhost") + ] + return vh_truth + +class MultipleVhostsTestGentoo(util.ApacheTest): + """Multiple vhost tests for non-debian distro""" + + _multiprocess_can_split_ = True + + def setUp(self): # pylint: disable=arguments-differ + test_dir = "gentoo_apache/apache" + config_root = "gentoo_apache/apache/apache2" + vhost_root = "gentoo_apache/apache/apache2/vhosts.d" + super(MultipleVhostsTestGentoo, self).setUp(test_dir=test_dir, + config_root=config_root, + vhost_root=vhost_root) + + with mock.patch("certbot_apache.override_gentoo.GentooParser.update_runtime_variables"): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir, + os_info="gentoo") + self.vh_truth = get_vh_truth( + self.temp_dir, "gentoo_apache/apache") + + def test_get_parser(self): + self.assertTrue(isinstance(self.config.parser, + override_gentoo.GentooParser)) + + def test_get_virtual_hosts(self): + """Make sure all vhosts are being properly found.""" + vhs = self.config.get_virtual_hosts() + self.assertEqual(len(vhs), 3) + found = 0 + + for vhost in vhs: + for gentoo_truth in self.vh_truth: + if vhost == gentoo_truth: + found += 1 + break + else: + raise Exception("Missed: %s" % vhost) # pragma: no cover + self.assertEqual(found, 3) + + def test_get_sysconfig_vars(self): + """Make sure we read the Gentoo APACHE2_OPTS variable correctly""" + defines = ['DEFAULT_VHOST', 'INFO', + 'SSL', 'SSL_DEFAULT_VHOST', 'LANGUAGE'] + self.config.parser.apacheconfig_filep = os.path.realpath( + os.path.join(self.config.parser.root, "../conf.d/apache2")) + self.config.parser.variables = {} + with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"): + self.config.parser.update_runtime_variables() + for define in defines: + self.assertTrue(define in self.config.parser.variables.keys()) + + @mock.patch("certbot_apache.parser.ApacheParser.parse_from_subprocess") + def test_no_binary_configdump(self, mock_subprocess): + """Make sure we don't call binary dumps other than modules from Apache + as this is not supported in Gentoo currently""" + + with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"): + self.config.parser.update_runtime_variables() + self.config.parser.reset_modules() + self.assertFalse(mock_subprocess.called) + + self.config.parser.update_runtime_variables() + self.config.parser.reset_modules() + self.assertTrue(mock_subprocess.called) + + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_opportunistic_httpd_runtime_parsing(self, mock_get): + mod_val = ( + 'Loaded Modules:\n' + ' mock_module (static)\n' + ' another_module (static)\n' + ) + def mock_get_cfg(command): + """Mock httpd process stdout""" + if command == ['apache2ctl', 'modules']: + return mod_val + mock_get.side_effect = mock_get_cfg + self.config.parser.modules = set() + + with mock.patch("certbot.util.get_os_info") as mock_osi: + # Make sure we have the have the CentOS httpd constants + mock_osi.return_value = ("gentoo", "123") + self.config.parser.update_runtime_variables() + + self.assertEquals(mock_get.call_count, 1) + self.assertEquals(len(self.config.parser.modules), 4) + self.assertTrue("mod_another.c" in self.config.parser.modules) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/http_01_test.py b/certbot-apache/certbot_apache/tests/http_01_test.py new file mode 100644 index 000000000..9ed4ee509 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/http_01_test.py @@ -0,0 +1,202 @@ +"""Test for certbot_apache.http_01.""" +import mock +import os +import unittest + +from acme import challenges + +from certbot import achallenges +from certbot import errors + +from certbot.tests import acme_util + +from certbot_apache.tests import util + + +NUM_ACHALLS = 3 + + +class ApacheHttp01TestMeta(type): + """Generates parmeterized tests for testing perform.""" + def __new__(mcs, name, bases, class_dict): + + def _gen_test(num_achalls, minor_version): + def _test(self): + achalls = self.achalls[:num_achalls] + vhosts = self.vhosts[:num_achalls] + self.config.version = (2, minor_version) + self.common_perform_test(achalls, vhosts) + return _test + + for i in range(1, NUM_ACHALLS + 1): + for j in (2, 4): + test_name = "test_perform_{0}_{1}".format(i, j) + class_dict[test_name] = _gen_test(i, j) + return type.__new__(mcs, name, bases, class_dict) + + +class ApacheHttp01Test(util.ApacheTest): + """Test for certbot_apache.http_01.ApacheHttp01.""" + + __metaclass__ = ApacheHttp01TestMeta + + def setUp(self, *args, **kwargs): + super(ApacheHttp01Test, self).setUp(*args, **kwargs) + + self.account_key = self.rsa512jwk + self.achalls = [] + vh_truth = util.get_vh_truth( + self.temp_dir, "debian_apache_2_4/multiple_vhosts") + # Takes the vhosts for encryption-example.demo, certbot.demo, and + # vhost.in.rootconf + self.vhosts = [vh_truth[0], vh_truth[3], vh_truth[10]] + + for i in range(NUM_ACHALLS): + self.achalls.append( + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=((chr(ord('a') + i).encode() * 16))), + "pending"), + domain=self.vhosts[i].name, account_key=self.account_key)) + + modules = ["rewrite", "authz_core", "authz_host"] + for mod in modules: + self.config.parser.modules.add("mod_{0}.c".format(mod)) + self.config.parser.modules.add(mod + "_module") + + from certbot_apache.http_01 import ApacheHttp01 + self.http = ApacheHttp01(self.config) + + def test_empty_perform(self): + self.assertFalse(self.http.perform()) + + @mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod") + def test_enable_modules_22(self, mock_enmod): + self.config.version = (2, 2) + self.config.parser.modules.remove("authz_host_module") + self.config.parser.modules.remove("mod_authz_host.c") + + enmod_calls = self.common_enable_modules_test(mock_enmod) + self.assertEqual(enmod_calls[0][0][0], "authz_host") + + @mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod") + def test_enable_modules_24(self, mock_enmod): + self.config.parser.modules.remove("authz_core_module") + self.config.parser.modules.remove("mod_authz_core.c") + + enmod_calls = self.common_enable_modules_test(mock_enmod) + self.assertEqual(enmod_calls[0][0][0], "authz_core") + + def common_enable_modules_test(self, mock_enmod): + """Tests enabling mod_rewrite and other modules.""" + self.config.parser.modules.remove("rewrite_module") + self.config.parser.modules.remove("mod_rewrite.c") + + self.http.prepare_http01_modules() + + self.assertTrue(mock_enmod.called) + calls = mock_enmod.call_args_list + other_calls = [] + for call in calls: + if "rewrite" != call[0][0]: + other_calls.append(call) + + # If these lists are equal, we never enabled mod_rewrite + self.assertNotEqual(calls, other_calls) + return other_calls + + def test_same_vhost(self): + vhost = next(v for v in self.config.vhosts if v.name == "certbot.demo") + achalls = [ + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=((b'a' * 16))), + "pending"), + domain=vhost.name, account_key=self.account_key), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=((b'b' * 16))), + "pending"), + domain=next(iter(vhost.aliases)), account_key=self.account_key) + ] + self.common_perform_test(achalls, [vhost]) + + def test_anonymous_vhost(self): + vhosts = [v for v in self.config.vhosts if not v.ssl] + achalls = [ + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=((b'a' * 16))), + "pending"), + domain="something.nonexistent", account_key=self.account_key)] + self.common_perform_test(achalls, vhosts) + + def test_no_vhost(self): + for achall in self.achalls: + self.http.add_chall(achall) + self.config.config.http01_port = 12345 + self.assertRaises(errors.PluginError, self.http.perform) + + def common_perform_test(self, achalls, vhosts): + """Tests perform with the given achalls.""" + challenge_dir = self.http.challenge_dir + self.assertFalse(os.path.exists(challenge_dir)) + for achall in achalls: + self.http.add_chall(achall) + + expected_response = [ + achall.response(self.account_key) for achall in achalls] + self.assertEqual(self.http.perform(), expected_response) + + self.assertTrue(os.path.isdir(self.http.challenge_dir)) + self._has_min_permissions(self.http.challenge_dir, 0o755) + self._test_challenge_conf() + + for achall in achalls: + self._test_challenge_file(achall) + + for vhost in vhosts: + if not vhost.ssl: + matches = self.config.parser.find_dir("Include", + self.http.challenge_conf_pre, + vhost.path) + self.assertEqual(len(matches), 1) + matches = self.config.parser.find_dir("Include", + self.http.challenge_conf_post, + vhost.path) + self.assertEqual(len(matches), 1) + + self.assertTrue(os.path.exists(challenge_dir)) + + def _test_challenge_conf(self): + with open(self.http.challenge_conf_pre) as f: + pre_conf_contents = f.read() + + with open(self.http.challenge_conf_post) as f: + post_conf_contents = f.read() + + self.assertTrue("RewriteEngine on" in pre_conf_contents) + self.assertTrue("RewriteRule" in pre_conf_contents) + + self.assertTrue(self.http.challenge_dir in post_conf_contents) + if self.config.version < (2, 4): + self.assertTrue("Allow from all" in post_conf_contents) + else: + self.assertTrue("Require all granted" in post_conf_contents) + + def _test_challenge_file(self, achall): + name = os.path.join(self.http.challenge_dir, achall.chall.encode("token")) + validation = achall.validation(self.account_key) + + self._has_min_permissions(name, 0o644) + with open(name, 'rb') as f: + self.assertEqual(f.read(), validation.encode()) + + def _has_min_permissions(self, path, min_mode): + """Tests the given file has at least the permissions in mode.""" + st_mode = os.stat(path).st_mode + self.assertEqual(st_mode, st_mode | min_mode) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/parser_test.py b/certbot-apache/certbot_apache/tests/parser_test.py index 38c5b29c7..4496781c9 100644 --- a/certbot-apache/certbot_apache/tests/parser_test.py +++ b/certbot-apache/certbot_apache/tests/parser_test.py @@ -66,6 +66,23 @@ class BasicParserTest(util.ParserTest): for i, match in enumerate(matches): self.assertEqual(self.parser.aug.get(match), str(i + 1)) + def test_add_dir_beginning(self): + aug_default = "/files" + self.parser.loc["default"] + self.parser.add_dir_beginning(aug_default, + "AddDirectiveBeginning", + "testBegin") + + self.assertTrue( + self.parser.find_dir("AddDirectiveBeginning", "testBegin", aug_default)) + + self.assertEqual( + self.parser.aug.get(aug_default+"/directive[1]"), + "AddDirectiveBeginning") + self.parser.add_dir_beginning(aug_default, "AddList", ["1", "2", "3", "4"]) + matches = self.parser.find_dir("AddList", None, aug_default) + for i, match in enumerate(matches): + self.assertEqual(self.parser.aug.get(match), str(i + 1)) + def test_empty_arg(self): self.assertEquals(None, self.parser.get_arg("/files/whatever/nonexistent")) @@ -120,17 +137,18 @@ class BasicParserTest(util.ParserTest): @mock.patch("certbot_apache.parser.ApacheParser.find_dir") @mock.patch("certbot_apache.parser.ApacheParser.get_arg") - def test_init_modules_bad_syntax(self, mock_arg, mock_find): + def test_parse_modules_bad_syntax(self, mock_arg, mock_find): mock_find.return_value = ["1", "2", "3", "4", "5", "6", "7", "8"] mock_arg.return_value = None with mock.patch("certbot_apache.parser.logger") as mock_logger: - self.parser.init_modules() + self.parser.parse_modules() # Make sure that we got None return value and logged the file self.assertTrue(mock_logger.debug.called) + @mock.patch("certbot_apache.parser.ApacheParser.find_dir") @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") - def test_update_runtime_variables(self, mock_cfg): - mock_cfg.return_value = ( + def test_update_runtime_variables(self, mock_cfg, _): + define_val = ( 'ServerRoot: "/etc/apache2"\n' 'Main DocumentRoot: "/var/www"\n' 'Main ErrorLog: "/var/log/apache2/error.log"\n' @@ -147,11 +165,113 @@ class BasicParserTest(util.ParserTest): 'User: name="www-data" id=33 not_used\n' 'Group: name="www-data" id=33 not_used\n' ) + inc_val = ( + 'Included configuration files:\n' + ' (*) /etc/apache2/apache2.conf\n' + ' (146) /etc/apache2/mods-enabled/access_compat.load\n' + ' (146) /etc/apache2/mods-enabled/alias.load\n' + ' (146) /etc/apache2/mods-enabled/auth_basic.load\n' + ' (146) /etc/apache2/mods-enabled/authn_core.load\n' + ' (146) /etc/apache2/mods-enabled/authn_file.load\n' + ' (146) /etc/apache2/mods-enabled/authz_core.load\n' + ' (146) /etc/apache2/mods-enabled/authz_host.load\n' + ' (146) /etc/apache2/mods-enabled/authz_user.load\n' + ' (146) /etc/apache2/mods-enabled/autoindex.load\n' + ' (146) /etc/apache2/mods-enabled/deflate.load\n' + ' (146) /etc/apache2/mods-enabled/dir.load\n' + ' (146) /etc/apache2/mods-enabled/env.load\n' + ' (146) /etc/apache2/mods-enabled/filter.load\n' + ' (146) /etc/apache2/mods-enabled/mime.load\n' + ' (146) /etc/apache2/mods-enabled/mpm_event.load\n' + ' (146) /etc/apache2/mods-enabled/negotiation.load\n' + ' (146) /etc/apache2/mods-enabled/reqtimeout.load\n' + ' (146) /etc/apache2/mods-enabled/setenvif.load\n' + ' (146) /etc/apache2/mods-enabled/socache_shmcb.load\n' + ' (146) /etc/apache2/mods-enabled/ssl.load\n' + ' (146) /etc/apache2/mods-enabled/status.load\n' + ' (147) /etc/apache2/mods-enabled/alias.conf\n' + ' (147) /etc/apache2/mods-enabled/autoindex.conf\n' + ' (147) /etc/apache2/mods-enabled/deflate.conf\n' + ) + mod_val = ( + 'Loaded Modules:\n' + ' core_module (static)\n' + ' so_module (static)\n' + ' watchdog_module (static)\n' + ' http_module (static)\n' + ' log_config_module (static)\n' + ' logio_module (static)\n' + ' version_module (static)\n' + ' unixd_module (static)\n' + ' access_compat_module (shared)\n' + ' alias_module (shared)\n' + ' auth_basic_module (shared)\n' + ' authn_core_module (shared)\n' + ' authn_file_module (shared)\n' + ' authz_core_module (shared)\n' + ' authz_host_module (shared)\n' + ' authz_user_module (shared)\n' + ' autoindex_module (shared)\n' + ' deflate_module (shared)\n' + ' dir_module (shared)\n' + ' env_module (shared)\n' + ' filter_module (shared)\n' + ' mime_module (shared)\n' + ' mpm_event_module (shared)\n' + ' negotiation_module (shared)\n' + ' reqtimeout_module (shared)\n' + ' setenvif_module (shared)\n' + ' socache_shmcb_module (shared)\n' + ' ssl_module (shared)\n' + ' status_module (shared)\n' + ) + + def mock_get_vars(cmd): + """Mock command output""" + if cmd[-1] == "DUMP_RUN_CFG": + return define_val + elif cmd[-1] == "DUMP_INCLUDES": + return inc_val + elif cmd[-1] == "DUMP_MODULES": + return mod_val + + mock_cfg.side_effect = mock_get_vars + expected_vars = {"TEST": "", "U_MICH": "", "TLS": "443", "example_path": "Documents/path"} - self.parser.update_runtime_variables() - self.assertEqual(self.parser.variables, expected_vars) + self.parser.modules = set() + with mock.patch( + "certbot_apache.parser.ApacheParser.parse_file") as mock_parse: + self.parser.update_runtime_variables() + self.assertEqual(self.parser.variables, expected_vars) + self.assertEqual(len(self.parser.modules), 58) + # None of the includes in inc_val should be in parsed paths. + # Make sure we tried to include them all. + self.assertEqual(mock_parse.call_count, 25) + + @mock.patch("certbot_apache.parser.ApacheParser.find_dir") + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_update_runtime_variables_alt_values(self, mock_cfg, _): + inc_val = ( + 'Included configuration files:\n' + ' (*) {0}\n' + ' (146) /etc/apache2/mods-enabled/access_compat.load\n' + ' (146) {1}/mods-enabled/alias.load\n' + ).format(self.parser.loc["root"], + os.path.dirname(self.parser.loc["root"])) + + mock_cfg.return_value = inc_val + self.parser.modules = set() + + with mock.patch( + "certbot_apache.parser.ApacheParser.parse_file") as mock_parse: + self.parser.update_runtime_variables() + # No matching modules should have been found + self.assertEqual(len(self.parser.modules), 0) + # Only one of the three includes do not exist in already parsed + # path derived from root configuration Include statements + self.assertEqual(mock_parse.call_count, 1) @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") def test_update_runtime_vars_bad_output(self, mock_cfg): @@ -162,7 +282,7 @@ class BasicParserTest(util.ParserTest): self.assertRaises( errors.PluginError, self.parser.update_runtime_variables) - @mock.patch("certbot_apache.constants.os_constant") + @mock.patch("certbot_apache.configurator.ApacheConfigurator.constant") @mock.patch("certbot_apache.parser.subprocess.Popen") def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_const): mock_popen.side_effect = OSError @@ -198,7 +318,7 @@ class ParserInitTest(util.ApacheTest): self.assertRaises( errors.PluginError, ApacheParser, self.aug, os.path.relpath(self.config_path), - "/dummy/vhostpath", version=(2, 2, 22)) + "/dummy/vhostpath", version=(2, 2, 22), configurator=self.config) def test_root_normalized(self): from certbot_apache.parser import ApacheParser @@ -210,7 +330,7 @@ class ParserInitTest(util.ApacheTest): "debian_apache_2_4/////multiple_vhosts/../multiple_vhosts/apache2") parser = ApacheParser(self.aug, path, - "/dummy/vhostpath") + "/dummy/vhostpath", configurator=self.config) self.assertEqual(parser.root, self.config_path) @@ -220,7 +340,7 @@ class ParserInitTest(util.ApacheTest): "update_runtime_variables"): parser = ApacheParser( self.aug, os.path.relpath(self.config_path), - "/dummy/vhostpath") + "/dummy/vhostpath", configurator=self.config) self.assertEqual(parser.root, self.config_path) @@ -230,7 +350,7 @@ class ParserInitTest(util.ApacheTest): "update_runtime_variables"): parser = ApacheParser( self.aug, self.config_path + os.path.sep, - "/dummy/vhostpath") + "/dummy/vhostpath", configurator=self.config) self.assertEqual(parser.root, self.config_path) diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/README b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/README new file mode 100644 index 000000000..f5e96615a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/README @@ -0,0 +1,9 @@ + +This directory holds configuration files for the Apache HTTP Server; +any files in this directory which have the ".conf" extension will be +processed as httpd configuration files. The directory is used in +addition to the directory /etc/httpd/conf.modules.d/, which contains +configuration files necessary to load modules. + +Files are processed in alphabetical order. + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf new file mode 100644 index 000000000..a85cf5dca --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf @@ -0,0 +1,94 @@ +# +# Directives controlling the display of server-generated directory listings. +# +# Required modules: mod_authz_core, mod_authz_host, +# mod_autoindex, mod_alias +# +# To see the listing of a directory, the Options directive for the +# directory must include "Indexes", and the directory must not contain +# a file matching those listed in the DirectoryIndex directive. +# + +# +# IndexOptions: Controls the appearance of server-generated directory +# listings. +# +IndexOptions FancyIndexing HTMLTable VersionSort + +# We include the /icons/ alias for FancyIndexed directory listings. If +# you do not use FancyIndexing, you may comment this out. +# +Alias /icons/ "/usr/share/httpd/icons/" + + + Options Indexes MultiViews FollowSymlinks + AllowOverride None + Require all granted + + +# +# AddIcon* directives tell the server which icon to show for different +# files or filename extensions. These are only displayed for +# FancyIndexed directories. +# +AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip + +AddIconByType (TXT,/icons/text.gif) text/* +AddIconByType (IMG,/icons/image2.gif) image/* +AddIconByType (SND,/icons/sound2.gif) audio/* +AddIconByType (VID,/icons/movie.gif) video/* + +AddIcon /icons/binary.gif .bin .exe +AddIcon /icons/binhex.gif .hqx +AddIcon /icons/tar.gif .tar +AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv +AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip +AddIcon /icons/a.gif .ps .ai .eps +AddIcon /icons/layout.gif .html .shtml .htm .pdf +AddIcon /icons/text.gif .txt +AddIcon /icons/c.gif .c +AddIcon /icons/p.gif .pl .py +AddIcon /icons/f.gif .for +AddIcon /icons/dvi.gif .dvi +AddIcon /icons/uuencoded.gif .uu +AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl +AddIcon /icons/tex.gif .tex +AddIcon /icons/bomb.gif /core +AddIcon /icons/bomb.gif */core.* + +AddIcon /icons/back.gif .. +AddIcon /icons/hand.right.gif README +AddIcon /icons/folder.gif ^^DIRECTORY^^ +AddIcon /icons/blank.gif ^^BLANKICON^^ + +# +# DefaultIcon is which icon to show for files which do not have an icon +# explicitly set. +# +DefaultIcon /icons/unknown.gif + +# +# AddDescription allows you to place a short description after a file in +# server-generated indexes. These are only displayed for FancyIndexed +# directories. +# Format: AddDescription "description" filename +# +#AddDescription "GZIP compressed document" .gz +#AddDescription "tar archive" .tar +#AddDescription "GZIP compressed tar archive" .tgz + +# +# ReadmeName is the name of the README file the server will look for by +# default, and append to directory listings. +# +# HeaderName is the name of a file which should be prepended to +# directory indexes. +ReadmeName README.html +HeaderName HEADER.html + +# +# IndexIgnore is a set of filenames which directory indexing should ignore +# and not include in the listing. Shell-style wildcarding is permitted. +# +IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf new file mode 100644 index 000000000..de7ac2777 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf @@ -0,0 +1,7 @@ + + ServerName centos.example.com + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf new file mode 100644 index 000000000..6e2502e9a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf @@ -0,0 +1,211 @@ +# +# When we also provide SSL we have to listen to the +# the HTTPS port in addition. +# +Listen 443 https + +## +## SSL Global Context +## +## All SSL configuration in this context applies both to +## the main server and all SSL-enabled virtual hosts. +## + +# Pass Phrase Dialog: +# Configure the pass phrase gathering process. +# The filtering dialog program (`builtin' is a internal +# terminal dialog) has to provide the pass phrase on stdout. +SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog + +# Inter-Process Session Cache: +# Configure the SSL Session Cache: First the mechanism +# to use and second the expiring timeout (in seconds). +SSLSessionCache shmcb:/run/httpd/sslcache(512000) +SSLSessionCacheTimeout 300 + +# Pseudo Random Number Generator (PRNG): +# Configure one or more sources to seed the PRNG of the +# SSL library. The seed data should be of good random quality. +# WARNING! On some platforms /dev/random blocks if not enough entropy +# is available. This means you then cannot use the /dev/random device +# because it would lead to very long connection times (as long as +# it requires to make more entropy available). But usually those +# platforms additionally provide a /dev/urandom device which doesn't +# block. So, if available, use this one instead. Read the mod_ssl User +# Manual for more details. +SSLRandomSeed startup file:/dev/urandom 256 +SSLRandomSeed connect builtin +#SSLRandomSeed startup file:/dev/random 512 +#SSLRandomSeed connect file:/dev/random 512 +#SSLRandomSeed connect file:/dev/urandom 512 + +# +# Use "SSLCryptoDevice" to enable any supported hardware +# accelerators. Use "openssl engine -v" to list supported +# engine names. NOTE: If you enable an accelerator and the +# server does not start, consult the error logs and ensure +# your accelerator is functioning properly. +# +SSLCryptoDevice builtin +#SSLCryptoDevice ubsec + +## +## SSL Virtual Host Context +## + + + +# General setup for the virtual host, inherited from global configuration +#DocumentRoot "/var/www/html" +#ServerName www.example.com:443 + +# Use separate log files for the SSL virtual host; note that LogLevel +# is not inherited from httpd.conf. +ErrorLog logs/ssl_error_log +TransferLog logs/ssl_access_log +LogLevel warn + +# SSL Engine Switch: +# Enable/Disable SSL for this virtual host. +SSLEngine on + +# SSL Protocol support: +# List the enable protocol levels with which clients will be able to +# connect. Disable SSLv2 access by default: +SSLProtocol all -SSLv2 + +# SSL Cipher Suite: +# List the ciphers that the client is permitted to negotiate. +# See the mod_ssl documentation for a complete list. +SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!SEED:!IDEA + +# Speed-optimized SSL Cipher configuration: +# If speed is your main concern (on busy HTTPS servers e.g.), +# you might want to force clients to specific, performance +# optimized ciphers. In this case, prepend those ciphers +# to the SSLCipherSuite list, and enable SSLHonorCipherOrder. +# Caveat: by giving precedence to RC4-SHA and AES128-SHA +# (as in the example below), most connections will no longer +# have perfect forward secrecy - if the server's key is +# compromised, captures of past or future traffic must be +# considered compromised, too. +#SSLCipherSuite RC4-SHA:AES128-SHA:HIGH:MEDIUM:!aNULL:!MD5 +#SSLHonorCipherOrder on + +# Server Certificate: +# Point SSLCertificateFile at a PEM encoded certificate. If +# the certificate is encrypted, then you will be prompted for a +# pass phrase. Note that a kill -HUP will prompt again. A new +# certificate can be generated using the genkey(1) command. + +# Server Private Key: +# If the key is not combined with the certificate, use this +# directive to point at the key file. Keep in mind that if +# you've both a RSA and a DSA private key you can configure +# both in parallel (to also allow the use of DSA ciphers, etc.) + +# Server Certificate Chain: +# Point SSLCertificateChainFile at a file containing the +# concatenation of PEM encoded CA certificates which form the +# certificate chain for the server certificate. Alternatively +# the referenced file can be the same as SSLCertificateFile +# when the CA certificates are directly appended to the server +# certificate for convinience. +#SSLCertificateChainFile /etc/pki/tls/certs/server-chain.crt + +# Certificate Authority (CA): +# Set the CA certificate verification path where to find CA +# certificates for client authentication or alternatively one +# huge file containing all of them (file must be PEM encoded) +#SSLCACertificateFile /etc/pki/tls/certs/ca-bundle.crt + +# Client Authentication (Type): +# Client certificate verification type and depth. Types are +# none, optional, require and optional_no_ca. Depth is a +# number which specifies how deeply to verify the certificate +# issuer chain before deciding the certificate is not valid. +#SSLVerifyClient require +#SSLVerifyDepth 10 + +# Access Control: +# With SSLRequire you can do per-directory access control based +# on arbitrary complex boolean expressions containing server +# variable checks and other lookup directives. The syntax is a +# mixture between C and Perl. See the mod_ssl documentation +# for more details. +# +#SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ +# and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ +# and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ +# and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ +# and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ +# or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ +# + +# SSL Engine Options: +# Set various options for the SSL engine. +# o FakeBasicAuth: +# Translate the client X.509 into a Basic Authorisation. This means that +# the standard Auth/DBMAuth methods can be used for access control. The +# user name is the `one line' version of the client's X.509 certificate. +# Note that no password is obtained from the user. Every entry in the user +# file needs this password: `xxj31ZMTZzkVA'. +# o ExportCertData: +# This exports two additional environment variables: SSL_CLIENT_CERT and +# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the +# server (always existing) and the client (only existing when client +# authentication is used). This can be used to import the certificates +# into CGI scripts. +# o StdEnvVars: +# This exports the standard SSL/TLS related `SSL_*' environment variables. +# Per default this exportation is switched off for performance reasons, +# because the extraction step is an expensive operation and is usually +# useless for serving static content. So one usually enables the +# exportation for CGI and SSI requests only. +# o StrictRequire: +# This denies access when "SSLRequireSSL" or "SSLRequire" applied even +# under a "Satisfy any" situation, i.e. when it applies access is denied +# and no other module can change it. +# o OptRenegotiate: +# This enables optimized SSL connection renegotiation handling when SSL +# directives are used in per-directory context. +#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + +# SSL Protocol Adjustments: +# The safe and default but still SSL/TLS standard compliant shutdown +# approach is that mod_ssl sends the close notify alert but doesn't wait for +# the close notify alert from client. When you need a different shutdown +# approach you can use one of the following variables: +# o ssl-unclean-shutdown: +# This forces an unclean shutdown when the connection is closed, i.e. no +# SSL close notify alert is send or allowed to received. This violates +# the SSL/TLS standard but is needed for some brain-dead browsers. Use +# this when you receive I/O errors because of the standard approach where +# mod_ssl sends the close notify alert. +# o ssl-accurate-shutdown: +# This forces an accurate shutdown when the connection is closed, i.e. a +# SSL close notify alert is send and mod_ssl waits for the close notify +# alert of the client. This is 100% SSL/TLS standard compliant, but in +# practice often causes hanging connections with brain-dead browsers. Use +# this only for browsers where you know that their SSL implementation +# works correctly. +# Notice: Most problems of broken clients are also related to the HTTP +# keep-alive facility, so you usually additionally want to disable +# keep-alive for those clients, too. Use variable "nokeepalive" for this. +# Similarly, one has to force some clients to use HTTP/1.0 to workaround +# their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and +# "force-response-1.0" for this. +BrowserMatch "MSIE [2-5]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0 + +# Per-Server Logging: +# The home of a custom SSL log file. Use this when you want a +# compact non-error SSL logfile on a virtual host basis. +CustomLog logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf new file mode 100644 index 000000000..b5d7a49ef --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf @@ -0,0 +1,36 @@ +# +# UserDir: The name of the directory that is appended onto a user's home +# directory if a ~user request is received. +# +# The path to the end user account 'public_html' directory must be +# accessible to the webserver userid. This usually means that ~userid +# must have permissions of 711, ~userid/public_html must have permissions +# of 755, and documents contained therein must be world-readable. +# Otherwise, the client will only receive a "403 Forbidden" message. +# + + # + # UserDir is disabled by default since it can confirm the presence + # of a username on the system (depending on home directory + # permissions). + # + UserDir disabled + + # + # To enable requests to /~user/ to serve the user's public_html + # directory, remove the "UserDir disabled" line above, and uncomment + # the following line instead: + # + #UserDir public_html + + +# +# Control access to UserDir directories. The following is an example +# for a site where these directories are restricted to read-only. +# + + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + Require method GET POST OPTIONS + + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf new file mode 100644 index 000000000..c1b6c11d9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf @@ -0,0 +1,22 @@ +# +# This configuration file enables the default "Welcome" page if there +# is no default index page present for the root URL. To disable the +# Welcome page, comment out all the lines below. +# +# NOTE: if this file is removed, it will be restored on upgrades. +# + + Options -Indexes + ErrorDocument 403 /.noindex.html + + + + AllowOverride None + Require all granted + + +Alias /.noindex.html /usr/share/httpd/noindex/index.html +Alias /noindex/css/bootstrap.min.css /usr/share/httpd/noindex/css/bootstrap.min.css +Alias /noindex/css/open-sans.css /usr/share/httpd/noindex/css/open-sans.css +Alias /images/apache_pb.gif /usr/share/httpd/noindex/images/apache_pb.gif +Alias /images/poweredby.png /usr/share/httpd/noindex/images/poweredby.png diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf new file mode 100644 index 000000000..31d979f20 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf @@ -0,0 +1,77 @@ +# +# This file loads most of the modules included with the Apache HTTP +# Server itself. +# + +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule actions_module modules/mod_actions.so +LoadModule alias_module modules/mod_alias.so +LoadModule allowmethods_module modules/mod_allowmethods.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule auth_digest_module modules/mod_auth_digest.so +LoadModule authn_anon_module modules/mod_authn_anon.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authn_dbd_module modules/mod_authn_dbd.so +LoadModule authn_dbm_module modules/mod_authn_dbm.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authn_socache_module modules/mod_authn_socache.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule authz_dbd_module modules/mod_authz_dbd.so +LoadModule authz_dbm_module modules/mod_authz_dbm.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_owner_module modules/mod_authz_owner.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule autoindex_module modules/mod_autoindex.so +LoadModule cache_module modules/mod_cache.so +LoadModule cache_disk_module modules/mod_cache_disk.so +LoadModule data_module modules/mod_data.so +LoadModule dbd_module modules/mod_dbd.so +LoadModule deflate_module modules/mod_deflate.so +LoadModule dir_module modules/mod_dir.so +LoadModule dumpio_module modules/mod_dumpio.so +LoadModule echo_module modules/mod_echo.so +LoadModule env_module modules/mod_env.so +LoadModule expires_module modules/mod_expires.so +LoadModule ext_filter_module modules/mod_ext_filter.so +LoadModule filter_module modules/mod_filter.so +LoadModule headers_module modules/mod_headers.so +LoadModule include_module modules/mod_include.so +LoadModule info_module modules/mod_info.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule logio_module modules/mod_logio.so +LoadModule mime_magic_module modules/mod_mime_magic.so +LoadModule mime_module modules/mod_mime.so +LoadModule negotiation_module modules/mod_negotiation.so +LoadModule remoteip_module modules/mod_remoteip.so +LoadModule reqtimeout_module modules/mod_reqtimeout.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule setenvif_module modules/mod_setenvif.so +LoadModule slotmem_plain_module modules/mod_slotmem_plain.so +LoadModule slotmem_shm_module modules/mod_slotmem_shm.so +LoadModule socache_dbm_module modules/mod_socache_dbm.so +LoadModule socache_memcache_module modules/mod_socache_memcache.so +LoadModule socache_shmcb_module modules/mod_socache_shmcb.so +LoadModule status_module modules/mod_status.so +LoadModule substitute_module modules/mod_substitute.so +LoadModule suexec_module modules/mod_suexec.so +LoadModule unique_id_module modules/mod_unique_id.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule userdir_module modules/mod_userdir.so +LoadModule version_module modules/mod_version.so +LoadModule vhost_alias_module modules/mod_vhost_alias.so + +#LoadModule buffer_module modules/mod_buffer.so +#LoadModule watchdog_module modules/mod_watchdog.so +#LoadModule heartbeat_module modules/mod_heartbeat.so +#LoadModule heartmonitor_module modules/mod_heartmonitor.so +#LoadModule usertrack_module modules/mod_usertrack.so +#LoadModule dialup_module modules/mod_dialup.so +#LoadModule charset_lite_module modules/mod_charset_lite.so +#LoadModule log_debug_module modules/mod_log_debug.so +#LoadModule ratelimit_module modules/mod_ratelimit.so +#LoadModule reflector_module modules/mod_reflector.so +#LoadModule request_module modules/mod_request.so +#LoadModule sed_module modules/mod_sed.so +#LoadModule speling_module modules/mod_speling.so + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf new file mode 100644 index 000000000..e6af8decd --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf @@ -0,0 +1,3 @@ +LoadModule dav_module modules/mod_dav.so +LoadModule dav_fs_module modules/mod_dav_fs.so +LoadModule dav_lock_module modules/mod_dav_lock.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf new file mode 100644 index 000000000..9e0d0db6e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf @@ -0,0 +1 @@ +LoadModule lua_module modules/mod_lua.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf new file mode 100644 index 000000000..7bfd1d413 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf @@ -0,0 +1,19 @@ +# Select the MPM module which should be used by uncommenting exactly +# one of the following LoadModule lines: + +# prefork MPM: Implements a non-threaded, pre-forking web server +# See: http://httpd.apache.org/docs/2.4/mod/prefork.html +LoadModule mpm_prefork_module modules/mod_mpm_prefork.so + +# worker MPM: Multi-Processing Module implementing a hybrid +# multi-threaded multi-process web server +# See: http://httpd.apache.org/docs/2.4/mod/worker.html +# +#LoadModule mpm_worker_module modules/mod_mpm_worker.so + +# event MPM: A variant of the worker MPM with the goal of consuming +# threads only for connections with active processing +# See: http://httpd.apache.org/docs/2.4/mod/event.html +# +#LoadModule mpm_event_module modules/mod_mpm_event.so + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf new file mode 100644 index 000000000..cc0bca077 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf @@ -0,0 +1,16 @@ +# This file configures all the proxy modules: +LoadModule proxy_module modules/mod_proxy.so +LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so +LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so +LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so +LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so +LoadModule proxy_ajp_module modules/mod_proxy_ajp.so +LoadModule proxy_balancer_module modules/mod_proxy_balancer.so +LoadModule proxy_connect_module modules/mod_proxy_connect.so +LoadModule proxy_express_module modules/mod_proxy_express.so +LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so +LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so +LoadModule proxy_ftp_module modules/mod_proxy_ftp.so +LoadModule proxy_http_module modules/mod_proxy_http.so +LoadModule proxy_scgi_module modules/mod_proxy_scgi.so +LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf new file mode 100644 index 000000000..53235cd76 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf @@ -0,0 +1 @@ +LoadModule ssl_module modules/mod_ssl.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf new file mode 100644 index 000000000..b208c972d --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf @@ -0,0 +1,2 @@ +# This file configures systemd module: +LoadModule systemd_module modules/mod_systemd.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf new file mode 100644 index 000000000..5b8b9362e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf @@ -0,0 +1,14 @@ +# This configuration file loads a CGI module appropriate to the MPM +# which has been configured in 00-mpm.conf. mod_cgid should be used +# with a threaded MPM; mod_cgi with the prefork MPM. + + + LoadModule cgid_module modules/mod_cgid.so + + + LoadModule cgid_module modules/mod_cgid.so + + + LoadModule cgi_module modules/mod_cgi.so + + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf new file mode 100644 index 000000000..a7af0dc1e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf @@ -0,0 +1,353 @@ +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See for detailed information. +# In particular, see +# +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so 'log/access_log' +# with ServerRoot set to '/www' will be interpreted by the +# server as '/www/log/access_log', where as '/log/access_log' will be +# interpreted as '/log/access_log'. + +# +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to specify a local disk on the +# Mutex directive, if file-based mutexes are used. If you wish to share the +# same ServerRoot for multiple httpd daemons, you will need to change at +# least PidFile. +# +ServerRoot "/etc/httpd" + +# +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 + +# +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +Include conf.modules.d/*.conf + +# +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +# +User apache +Group apache + +# 'Main' server configuration +# +# The directives in this section set up the values used by the 'main' +# server, which responds to any requests that aren't handled by a +# definition. These values also provide defaults for +# any containers you may define later in the file. +# +# All of these directives may appear inside containers, +# in which case these default settings will be overridden for the +# virtual host being defined. +# + +# +# ServerAdmin: Your address, where problems with the server should be +# e-mailed. This address appears on some server-generated pages, such +# as error documents. e.g. admin@your-domain.com +# +ServerAdmin root@localhost + +# +# ServerName gives the name and port that the server uses to identify itself. +# This can often be determined automatically, but we recommend you specify +# it explicitly to prevent problems during startup. +# +# If your host doesn't have a registered DNS name, enter its IP address here. +# +#ServerName www.example.com:80 + +# +# Deny access to the entirety of your server's filesystem. You must +# explicitly permit access to web content directories in other +# blocks below. +# + + AllowOverride none + Require all denied + + +# +# Note that from this point forward you must specifically allow +# particular features to be enabled - so if something's not working as +# you might expect, make sure that you have specifically enabled it +# below. +# + +# +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +DocumentRoot "/var/www/html" + +# +# Relax access to content within /var/www. +# + + AllowOverride None + # Allow open access: + Require all granted + + +# Further relax access to the default document root: + + # + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + # + Options Indexes FollowSymLinks + + # + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # Options FileInfo AuthConfig Limit + # + AllowOverride None + + # + # Controls who can get stuff from this server. + # + Require all granted + + +# +# DirectoryIndex: sets the file that Apache will serve if a directory +# is requested. +# + + DirectoryIndex index.html + + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + +# +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +ErrorLog "logs/error_log" + +# +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +# +LogLevel warn + + + # + # The following directives define some format nicknames for use with + # a CustomLog directive (see below). + # + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + + # You need to enable mod_logio.c to use %I and %O + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + + # + # The location and format of the access logfile (Common Logfile Format). + # If you do not define any access logfiles within a + # container, they will be logged here. Contrariwise, if you *do* + # define per- access logfiles, transactions will be + # logged therein and *not* in this file. + # + #CustomLog "logs/access_log" common + + # + # If you prefer a logfile with access, agent, and referer information + # (Combined Logfile Format) you can use the following directive. + # + CustomLog "logs/access_log" combined + + + + # + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a section to allow access to + # the filesystem path. + + # + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + # + ScriptAlias /cgi-bin/ "/var/www/cgi-bin/" + + + +# +# "/var/www/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. +# + + AllowOverride None + Options None + Require all granted + + + + # + # TypesConfig points to the file containing the list of mappings from + # filename extension to MIME-type. + # + TypesConfig /etc/mime.types + + # + # AddType allows you to add to or override the MIME configuration + # file specified in TypesConfig for specific file types. + # + #AddType application/x-gzip .tgz + # + # AddEncoding allows you to have certain browsers uncompress + # information on the fly. Note: Not all browsers support this. + # + #AddEncoding x-compress .Z + #AddEncoding x-gzip .gz .tgz + # + # If the AddEncoding directives above are commented-out, then you + # probably should define those extensions to indicate media types: + # + AddType application/x-compress .Z + AddType application/x-gzip .gz .tgz + + # + # AddHandler allows you to map certain file extensions to "handlers": + # actions unrelated to filetype. These can be either built into the server + # or added with the Action directive (see below) + # + # To use CGI scripts outside of ScriptAliased directories: + # (You will also need to add "ExecCGI" to the "Options" directive.) + # + #AddHandler cgi-script .cgi + + # For type maps (negotiated resources): + #AddHandler type-map var + + # + # Filters allow you to process content before it is sent to the client. + # + # To parse .shtml files for server-side includes (SSI): + # (You will also need to add "Includes" to the "Options" directive.) + # + AddType text/html .shtml + AddOutputFilter INCLUDES .shtml + + +# +# Specify a default charset for all content served; this enables +# interpretation of all content as UTF-8 by default. To use the +# default browser choice (ISO-8859-1), or to allow the META tags +# in HTML content to override this choice, comment out this +# directive: +# +AddDefaultCharset UTF-8 + + + # + # The mod_mime_magic module allows the server to use various hints from the + # contents of the file itself to determine its type. The MIMEMagicFile + # directive tells the module where the hint definitions are located. + # + MIMEMagicFile conf/magic + + +# +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html +# + +# +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall may be used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +# Defaults if commented: EnableMMAP On, EnableSendfile Off +# +#EnableMMAP off +EnableSendfile on + +# Supplemental configuration +# +# Load config files in the "/etc/httpd/conf.d" directory, if any. +IncludeOptional conf.d/*.conf diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/magic b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/magic new file mode 100644 index 000000000..7c56119e9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/magic @@ -0,0 +1,385 @@ +# Magic data for mod_mime_magic Apache module (originally for file(1) command) +# The module is described in /manual/mod/mod_mime_magic.html +# +# The format is 4-5 columns: +# Column #1: byte number to begin checking from, ">" indicates continuation +# Column #2: type of data to match +# Column #3: contents of data to match +# Column #4: MIME type of result +# Column #5: MIME encoding of result (optional) + +#------------------------------------------------------------------------------ +# Localstuff: file(1) magic for locally observed files +# Add any locally observed files here. + +#------------------------------------------------------------------------------ +# end local stuff +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# Java + +0 short 0xcafe +>2 short 0xbabe application/java + +#------------------------------------------------------------------------------ +# audio: file(1) magic for sound formats +# +# from Jan Nicolai Langfeldt , +# + +# Sun/NeXT audio data +0 string .snd +>12 belong 1 audio/basic +>12 belong 2 audio/basic +>12 belong 3 audio/basic +>12 belong 4 audio/basic +>12 belong 5 audio/basic +>12 belong 6 audio/basic +>12 belong 7 audio/basic + +>12 belong 23 audio/x-adpcm + +# DEC systems (e.g. DECstation 5000) use a variant of the Sun/NeXT format +# that uses little-endian encoding and has a different magic number +# (0x0064732E in little-endian encoding). +0 lelong 0x0064732E +>12 lelong 1 audio/x-dec-basic +>12 lelong 2 audio/x-dec-basic +>12 lelong 3 audio/x-dec-basic +>12 lelong 4 audio/x-dec-basic +>12 lelong 5 audio/x-dec-basic +>12 lelong 6 audio/x-dec-basic +>12 lelong 7 audio/x-dec-basic +# compressed (G.721 ADPCM) +>12 lelong 23 audio/x-dec-adpcm + +# Bytes 0-3 of AIFF, AIFF-C, & 8SVX audio files are "FORM" +# AIFF audio data +8 string AIFF audio/x-aiff +# AIFF-C audio data +8 string AIFC audio/x-aiff +# IFF/8SVX audio data +8 string 8SVX audio/x-aiff + +# Creative Labs AUDIO stuff +# Standard MIDI data +0 string MThd audio/unknown +#>9 byte >0 (format %d) +#>11 byte >1 using %d channels +# Creative Music (CMF) data +0 string CTMF audio/unknown +# SoundBlaster instrument data +0 string SBI audio/unknown +# Creative Labs voice data +0 string Creative\ Voice\ File audio/unknown +## is this next line right? it came this way... +#>19 byte 0x1A +#>23 byte >0 - version %d +#>22 byte >0 \b.%d + +# [GRR 950115: is this also Creative Labs? Guessing that first line +# should be string instead of unknown-endian long...] +#0 long 0x4e54524b MultiTrack sound data +#0 string NTRK MultiTrack sound data +#>4 long x - version %ld + +# Microsoft WAVE format (*.wav) +# [GRR 950115: probably all of the shorts and longs should be leshort/lelong] +# Microsoft RIFF +0 string RIFF audio/unknown +# - WAVE format +>8 string WAVE audio/x-wav +# MPEG audio. +0 beshort&0xfff0 0xfff0 audio/mpeg +# C64 SID Music files, from Linus Walleij +0 string PSID audio/prs.sid + +#------------------------------------------------------------------------------ +# c-lang: file(1) magic for C programs or various scripts +# + +# XPM icons (Greg Roelofs, newt@uchicago.edu) +# ideally should go into "images", but entries below would tag XPM as C source +0 string /*\ XPM image/x-xbm 7bit + +# this first will upset you if you're a PL/1 shop... (are there any left?) +# in which case rm it; ascmagic will catch real C programs +# C or REXX program text +0 string /* text/plain +# C++ program text +0 string // text/plain + +#------------------------------------------------------------------------------ +# compress: file(1) magic for pure-compression formats (no archives) +# +# compress, gzip, pack, compact, huf, squeeze, crunch, freeze, yabba, whap, etc. +# +# Formats for various forms of compressed data +# Formats for "compress" proper have been moved into "compress.c", +# because it tries to uncompress it to figure out what's inside. + +# standard unix compress +0 string \037\235 application/octet-stream x-compress + +# gzip (GNU zip, not to be confused with [Info-ZIP/PKWARE] zip archiver) +0 string \037\213 application/octet-stream x-gzip + +# According to gzip.h, this is the correct byte order for packed data. +0 string \037\036 application/octet-stream +# +# This magic number is byte-order-independent. +# +0 short 017437 application/octet-stream + +# XXX - why *two* entries for "compacted data", one of which is +# byte-order independent, and one of which is byte-order dependent? +# +# compacted data +0 short 0x1fff application/octet-stream +0 string \377\037 application/octet-stream +# huf output +0 short 0145405 application/octet-stream + +# Squeeze and Crunch... +# These numbers were gleaned from the Unix versions of the programs to +# handle these formats. Note that I can only uncrunch, not crunch, and +# I didn't have a crunched file handy, so the crunch number is untested. +# Keith Waclena +#0 leshort 0x76FF squeezed data (CP/M, DOS) +#0 leshort 0x76FE crunched data (CP/M, DOS) + +# Freeze +#0 string \037\237 Frozen file 2.1 +#0 string \037\236 Frozen file 1.0 (or gzip 0.5) + +# lzh? +#0 string \037\240 LZH compressed data + +#------------------------------------------------------------------------------ +# frame: file(1) magic for FrameMaker files +# +# This stuff came on a FrameMaker demo tape, most of which is +# copyright, but this file is "published" as witness the following: +# +0 string \ +# and Anna Shergold +# +0 string \ +0 string \14 byte 12 (OS/2 1.x format) +#>14 byte 64 (OS/2 2.x format) +#>14 byte 40 (Windows 3.x format) +#0 string IC icon +#0 string PI pointer +#0 string CI color icon +#0 string CP color pointer +#0 string BA bitmap array + +0 string \x89PNG image/png +0 string FWS application/x-shockwave-flash +0 string CWS application/x-shockwave-flash + +#------------------------------------------------------------------------------ +# lisp: file(1) magic for lisp programs +# +# various lisp types, from Daniel Quinlan (quinlan@yggdrasil.com) +0 string ;; text/plain 8bit +# Emacs 18 - this is always correct, but not very magical. +0 string \012( application/x-elc +# Emacs 19 +0 string ;ELC\023\000\000\000 application/x-elc + +#------------------------------------------------------------------------------ +# mail.news: file(1) magic for mail and news +# +# There are tests to ascmagic.c to cope with mail and news. +0 string Relay-Version: message/rfc822 7bit +0 string #!\ rnews message/rfc822 7bit +0 string N#!\ rnews message/rfc822 7bit +0 string Forward\ to message/rfc822 7bit +0 string Pipe\ to message/rfc822 7bit +0 string Return-Path: message/rfc822 7bit +0 string Path: message/news 8bit +0 string Xref: message/news 8bit +0 string From: message/rfc822 7bit +0 string Article message/news 8bit +#------------------------------------------------------------------------------ +# msword: file(1) magic for MS Word files +# +# Contributor claims: +# Reversed-engineered MS Word magic numbers +# + +0 string \376\067\0\043 application/msword +0 string \333\245-\0\0\0 application/msword + +# disable this one because it applies also to other +# Office/OLE documents for which msword is not correct. See PR#2608. +#0 string \320\317\021\340\241\261 application/msword + + + +#------------------------------------------------------------------------------ +# printer: file(1) magic for printer-formatted files +# + +# PostScript +0 string %! application/postscript +0 string \004%! application/postscript + +# Acrobat +# (due to clamen@cs.cmu.edu) +0 string %PDF- application/pdf + +#------------------------------------------------------------------------------ +# sc: file(1) magic for "sc" spreadsheet +# +38 string Spreadsheet application/x-sc + +#------------------------------------------------------------------------------ +# tex: file(1) magic for TeX files +# +# XXX - needs byte-endian stuff (big-endian and little-endian DVI?) +# +# From + +# Although we may know the offset of certain text fields in TeX DVI +# and font files, we can't use them reliably because they are not +# zero terminated. [but we do anyway, christos] +0 string \367\002 application/x-dvi +#0 string \367\203 TeX generic font data +#0 string \367\131 TeX packed font data +#0 string \367\312 TeX virtual font data +#0 string This\ is\ TeX, TeX transcript text +#0 string This\ is\ METAFONT, METAFONT transcript text + +# There is no way to detect TeX Font Metric (*.tfm) files without +# breaking them apart and reading the data. The following patterns +# match most *.tfm files generated by METAFONT or afm2tfm. +#2 string \000\021 TeX font metric data +#2 string \000\022 TeX font metric data +#>34 string >\0 (%s) + +# Texinfo and GNU Info, from Daniel Quinlan (quinlan@yggdrasil.com) +#0 string \\input\ texinfo Texinfo source text +#0 string This\ is\ Info\ file GNU Info text + +# correct TeX magic for Linux (and maybe more) +# from Peter Tobias (tobias@server.et-inf.fho-emden.de) +# +0 leshort 0x02f7 application/x-dvi + +# RTF - Rich Text Format +0 string {\\rtf application/rtf + +#------------------------------------------------------------------------------ +# animation: file(1) magic for animation/movie formats +# +# animation formats, originally from vax@ccwf.cc.utexas.edu (VaX#n8) +# MPEG file +0 string \000\000\001\263 video/mpeg +# +# The contributor claims: +# I couldn't find a real magic number for these, however, this +# -appears- to work. Note that it might catch other files, too, +# so BE CAREFUL! +# +# Note that title and author appear in the two 20-byte chunks +# at decimal offsets 2 and 22, respectively, but they are XOR'ed with +# 255 (hex FF)! DL format SUCKS BIG ROCKS. +# +# DL file version 1 , medium format (160x100, 4 images/screen) +0 byte 1 video/unknown +0 byte 2 video/unknown +# Quicktime video, from Linus Walleij +# from Apple quicktime file format documentation. +4 string moov video/quicktime +4 string mdat video/quicktime + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sites b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sites new file mode 100644 index 000000000..6af1f63fa --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sites @@ -0,0 +1 @@ +conf.d/centos.example.com.conf, centos.example.com diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd new file mode 100644 index 000000000..4bcb300c2 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd @@ -0,0 +1,25 @@ +# +# This file can be used to set additional environment variables for +# the httpd process, or pass additional options to the httpd +# executable. +# +# Note: With previous versions of httpd, the MPM could be changed by +# editing an "HTTPD" variable here. With the current version, that +# variable is now ignored. The MPM is a loadable module, and the +# choice of MPM can be changed by editing the configuration file +# /etc/httpd/conf.modules.d/00-mpm.conf. +# + +# +# To pass additional options (for instance, -D definitions) to the +# httpd binary at startup, set OPTIONS here. +# +OPTIONS="-D mock_define -D mock_define_too -D mock_value=TRUE -DMOCK_NOSEP -DNOSEP_TWO=NOSEP_VAL" + +# +# This setting ensures the httpd process is started in the "C" locale +# by default. (Some modules will not behave correctly if +# case-sensitive string comparisons are performed in a different +# locale.) +# +LANG=C diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/certbot.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/certbot.conf index b3147a523..965ca2222 100644 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/certbot.conf +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/certbot.conf @@ -1,5 +1,6 @@ ServerName certbot.demo +ServerAlias www.certbot.demo ServerAdmin webmaster@localhost DocumentRoot /var/www-certbot-reworld/static/ diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/httpd.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/httpd.conf new file mode 100644 index 000000000..e5693ffff --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/httpd.conf @@ -0,0 +1,157 @@ +# This is a modification of the default Apache 2.4 configuration file +# for Gentoo Linux. +# +# Support: +# http://www.gentoo.org/main/en/lists.xml [mailing lists] +# http://forums.gentoo.org/ [web forums] +# irc://irc.freenode.net#gentoo-apache [irc chat] +# +# Bug Reports: +# http://bugs.gentoo.org [gentoo related bugs] +# http://httpd.apache.org/bug_report.html [apache httpd related bugs] +# +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See for detailed information. +# In particular, see +# +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so "var/log/apache2/foo_log" +# with ServerRoot set to "/usr" will be interpreted by the +# server as "/usr/var/log/apache2/foo.log". + +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to point the LockFile directive +# at a local disk. If you wish to share the same ServerRoot for multiple +# httpd daemons, you will need to change at least LockFile and PidFile. +# Comment: The LockFile directive has been replaced by the Mutex directive +ServerRoot "/usr/lib64/apache2" + +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +# GENTOO: Automatically defined based on APACHE2_MODULES USE_EXPAND variable. +# Do not change manually, it will be overwritten on upgrade. +# +# The following modules are considered as the default configuration. +# If you wish to disable one of them, you may have to alter other +# configuration directives. +# +# Change these at your own risk! + +LoadModule actions_module modules/mod_actions.so +LoadModule alias_module modules/mod_alias.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule authn_anon_module modules/mod_authn_anon.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authn_dbm_module modules/mod_authn_dbm.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule authz_dbm_module modules/mod_authz_dbm.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_owner_module modules/mod_authz_owner.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule autoindex_module modules/mod_autoindex.so + +LoadModule cache_module modules/mod_cache.so + +LoadModule cgi_module modules/mod_cgi.so +LoadModule cgid_module modules/mod_cgid.so + +LoadModule dav_module modules/mod_dav.so + + +LoadModule dav_fs_module modules/mod_dav_fs.so + + +LoadModule dav_lock_module modules/mod_dav_lock.so + +LoadModule deflate_module modules/mod_deflate.so +LoadModule dir_module modules/mod_dir.so +LoadModule env_module modules/mod_env.so +LoadModule expires_module modules/mod_expires.so +LoadModule ext_filter_module modules/mod_ext_filter.so + +LoadModule file_cache_module modules/mod_file_cache.so + +LoadModule filter_module modules/mod_filter.so +LoadModule headers_module modules/mod_headers.so +LoadModule include_module modules/mod_include.so + +LoadModule info_module modules/mod_info.so + +LoadModule log_config_module modules/mod_log_config.so +LoadModule logio_module modules/mod_logio.so +LoadModule mime_module modules/mod_mime.so +LoadModule mime_magic_module modules/mod_mime_magic.so +LoadModule negotiation_module modules/mod_negotiation.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule setenvif_module modules/mod_setenvif.so + +LoadModule socache_shmcb_module modules/mod_socache_shmcb.so + +LoadModule speling_module modules/mod_speling.so + +LoadModule ssl_module modules/mod_ssl.so + + +LoadModule status_module modules/mod_status.so + +LoadModule unique_id_module modules/mod_unique_id.so +LoadModule unixd_module modules/mod_unixd.so + +LoadModule userdir_module modules/mod_userdir.so + +LoadModule usertrack_module modules/mod_usertrack.so +LoadModule vhost_alias_module modules/mod_vhost_alias.so + +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +User apache +Group apache + +# Supplemental configuration +# +# Most of the configuration files in the /etc/apache2/modules.d/ directory can +# be turned on using APACHE2_OPTS in /etc/conf.d/apache2 to add extra features +# or to modify the default configuration of the server. +# +# To know which flag to add to APACHE2_OPTS, look at the first line of the +# the file, which will usually be an where OPTION is the +# flag to use. + +Include modules.d/*.conf + +# Virtual-host support +# +# Gentoo has made using virtual-hosts easy. In /etc/apache2/vhosts.d/ we +# include a default vhost (enabled by adding -D DEFAULT_VHOST to +# APACHE2_OPTS in /etc/conf.d/apache2). +Include vhosts.d/*.conf + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/magic b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/magic new file mode 100644 index 000000000..7c56119e9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/magic @@ -0,0 +1,385 @@ +# Magic data for mod_mime_magic Apache module (originally for file(1) command) +# The module is described in /manual/mod/mod_mime_magic.html +# +# The format is 4-5 columns: +# Column #1: byte number to begin checking from, ">" indicates continuation +# Column #2: type of data to match +# Column #3: contents of data to match +# Column #4: MIME type of result +# Column #5: MIME encoding of result (optional) + +#------------------------------------------------------------------------------ +# Localstuff: file(1) magic for locally observed files +# Add any locally observed files here. + +#------------------------------------------------------------------------------ +# end local stuff +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# Java + +0 short 0xcafe +>2 short 0xbabe application/java + +#------------------------------------------------------------------------------ +# audio: file(1) magic for sound formats +# +# from Jan Nicolai Langfeldt , +# + +# Sun/NeXT audio data +0 string .snd +>12 belong 1 audio/basic +>12 belong 2 audio/basic +>12 belong 3 audio/basic +>12 belong 4 audio/basic +>12 belong 5 audio/basic +>12 belong 6 audio/basic +>12 belong 7 audio/basic + +>12 belong 23 audio/x-adpcm + +# DEC systems (e.g. DECstation 5000) use a variant of the Sun/NeXT format +# that uses little-endian encoding and has a different magic number +# (0x0064732E in little-endian encoding). +0 lelong 0x0064732E +>12 lelong 1 audio/x-dec-basic +>12 lelong 2 audio/x-dec-basic +>12 lelong 3 audio/x-dec-basic +>12 lelong 4 audio/x-dec-basic +>12 lelong 5 audio/x-dec-basic +>12 lelong 6 audio/x-dec-basic +>12 lelong 7 audio/x-dec-basic +# compressed (G.721 ADPCM) +>12 lelong 23 audio/x-dec-adpcm + +# Bytes 0-3 of AIFF, AIFF-C, & 8SVX audio files are "FORM" +# AIFF audio data +8 string AIFF audio/x-aiff +# AIFF-C audio data +8 string AIFC audio/x-aiff +# IFF/8SVX audio data +8 string 8SVX audio/x-aiff + +# Creative Labs AUDIO stuff +# Standard MIDI data +0 string MThd audio/unknown +#>9 byte >0 (format %d) +#>11 byte >1 using %d channels +# Creative Music (CMF) data +0 string CTMF audio/unknown +# SoundBlaster instrument data +0 string SBI audio/unknown +# Creative Labs voice data +0 string Creative\ Voice\ File audio/unknown +## is this next line right? it came this way... +#>19 byte 0x1A +#>23 byte >0 - version %d +#>22 byte >0 \b.%d + +# [GRR 950115: is this also Creative Labs? Guessing that first line +# should be string instead of unknown-endian long...] +#0 long 0x4e54524b MultiTrack sound data +#0 string NTRK MultiTrack sound data +#>4 long x - version %ld + +# Microsoft WAVE format (*.wav) +# [GRR 950115: probably all of the shorts and longs should be leshort/lelong] +# Microsoft RIFF +0 string RIFF audio/unknown +# - WAVE format +>8 string WAVE audio/x-wav +# MPEG audio. +0 beshort&0xfff0 0xfff0 audio/mpeg +# C64 SID Music files, from Linus Walleij +0 string PSID audio/prs.sid + +#------------------------------------------------------------------------------ +# c-lang: file(1) magic for C programs or various scripts +# + +# XPM icons (Greg Roelofs, newt@uchicago.edu) +# ideally should go into "images", but entries below would tag XPM as C source +0 string /*\ XPM image/x-xbm 7bit + +# this first will upset you if you're a PL/1 shop... (are there any left?) +# in which case rm it; ascmagic will catch real C programs +# C or REXX program text +0 string /* text/plain +# C++ program text +0 string // text/plain + +#------------------------------------------------------------------------------ +# compress: file(1) magic for pure-compression formats (no archives) +# +# compress, gzip, pack, compact, huf, squeeze, crunch, freeze, yabba, whap, etc. +# +# Formats for various forms of compressed data +# Formats for "compress" proper have been moved into "compress.c", +# because it tries to uncompress it to figure out what's inside. + +# standard unix compress +0 string \037\235 application/octet-stream x-compress + +# gzip (GNU zip, not to be confused with [Info-ZIP/PKWARE] zip archiver) +0 string \037\213 application/octet-stream x-gzip + +# According to gzip.h, this is the correct byte order for packed data. +0 string \037\036 application/octet-stream +# +# This magic number is byte-order-independent. +# +0 short 017437 application/octet-stream + +# XXX - why *two* entries for "compacted data", one of which is +# byte-order independent, and one of which is byte-order dependent? +# +# compacted data +0 short 0x1fff application/octet-stream +0 string \377\037 application/octet-stream +# huf output +0 short 0145405 application/octet-stream + +# Squeeze and Crunch... +# These numbers were gleaned from the Unix versions of the programs to +# handle these formats. Note that I can only uncrunch, not crunch, and +# I didn't have a crunched file handy, so the crunch number is untested. +# Keith Waclena +#0 leshort 0x76FF squeezed data (CP/M, DOS) +#0 leshort 0x76FE crunched data (CP/M, DOS) + +# Freeze +#0 string \037\237 Frozen file 2.1 +#0 string \037\236 Frozen file 1.0 (or gzip 0.5) + +# lzh? +#0 string \037\240 LZH compressed data + +#------------------------------------------------------------------------------ +# frame: file(1) magic for FrameMaker files +# +# This stuff came on a FrameMaker demo tape, most of which is +# copyright, but this file is "published" as witness the following: +# +0 string \ +# and Anna Shergold +# +0 string \ +0 string \14 byte 12 (OS/2 1.x format) +#>14 byte 64 (OS/2 2.x format) +#>14 byte 40 (Windows 3.x format) +#0 string IC icon +#0 string PI pointer +#0 string CI color icon +#0 string CP color pointer +#0 string BA bitmap array + +0 string \x89PNG image/png +0 string FWS application/x-shockwave-flash +0 string CWS application/x-shockwave-flash + +#------------------------------------------------------------------------------ +# lisp: file(1) magic for lisp programs +# +# various lisp types, from Daniel Quinlan (quinlan@yggdrasil.com) +0 string ;; text/plain 8bit +# Emacs 18 - this is always correct, but not very magical. +0 string \012( application/x-elc +# Emacs 19 +0 string ;ELC\023\000\000\000 application/x-elc + +#------------------------------------------------------------------------------ +# mail.news: file(1) magic for mail and news +# +# There are tests to ascmagic.c to cope with mail and news. +0 string Relay-Version: message/rfc822 7bit +0 string #!\ rnews message/rfc822 7bit +0 string N#!\ rnews message/rfc822 7bit +0 string Forward\ to message/rfc822 7bit +0 string Pipe\ to message/rfc822 7bit +0 string Return-Path: message/rfc822 7bit +0 string Path: message/news 8bit +0 string Xref: message/news 8bit +0 string From: message/rfc822 7bit +0 string Article message/news 8bit +#------------------------------------------------------------------------------ +# msword: file(1) magic for MS Word files +# +# Contributor claims: +# Reversed-engineered MS Word magic numbers +# + +0 string \376\067\0\043 application/msword +0 string \333\245-\0\0\0 application/msword + +# disable this one because it applies also to other +# Office/OLE documents for which msword is not correct. See PR#2608. +#0 string \320\317\021\340\241\261 application/msword + + + +#------------------------------------------------------------------------------ +# printer: file(1) magic for printer-formatted files +# + +# PostScript +0 string %! application/postscript +0 string \004%! application/postscript + +# Acrobat +# (due to clamen@cs.cmu.edu) +0 string %PDF- application/pdf + +#------------------------------------------------------------------------------ +# sc: file(1) magic for "sc" spreadsheet +# +38 string Spreadsheet application/x-sc + +#------------------------------------------------------------------------------ +# tex: file(1) magic for TeX files +# +# XXX - needs byte-endian stuff (big-endian and little-endian DVI?) +# +# From + +# Although we may know the offset of certain text fields in TeX DVI +# and font files, we can't use them reliably because they are not +# zero terminated. [but we do anyway, christos] +0 string \367\002 application/x-dvi +#0 string \367\203 TeX generic font data +#0 string \367\131 TeX packed font data +#0 string \367\312 TeX virtual font data +#0 string This\ is\ TeX, TeX transcript text +#0 string This\ is\ METAFONT, METAFONT transcript text + +# There is no way to detect TeX Font Metric (*.tfm) files without +# breaking them apart and reading the data. The following patterns +# match most *.tfm files generated by METAFONT or afm2tfm. +#2 string \000\021 TeX font metric data +#2 string \000\022 TeX font metric data +#>34 string >\0 (%s) + +# Texinfo and GNU Info, from Daniel Quinlan (quinlan@yggdrasil.com) +#0 string \\input\ texinfo Texinfo source text +#0 string This\ is\ Info\ file GNU Info text + +# correct TeX magic for Linux (and maybe more) +# from Peter Tobias (tobias@server.et-inf.fho-emden.de) +# +0 leshort 0x02f7 application/x-dvi + +# RTF - Rich Text Format +0 string {\\rtf application/rtf + +#------------------------------------------------------------------------------ +# animation: file(1) magic for animation/movie formats +# +# animation formats, originally from vax@ccwf.cc.utexas.edu (VaX#n8) +# MPEG file +0 string \000\000\001\263 video/mpeg +# +# The contributor claims: +# I couldn't find a real magic number for these, however, this +# -appears- to work. Note that it might catch other files, too, +# so BE CAREFUL! +# +# Note that title and author appear in the two 20-byte chunks +# at decimal offsets 2 and 22, respectively, but they are XOR'ed with +# 255 (hex FF)! DL format SUCKS BIG ROCKS. +# +# DL file version 1 , medium format (160x100, 4 images/screen) +0 byte 1 video/unknown +0 byte 2 video/unknown +# Quicktime video, from Linus Walleij +# from Apple quicktime file format documentation. +4 string moov video/quicktime +4 string mdat video/quicktime + diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/.keep_www-servers_apache-2 b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/.keep_www-servers_apache-2 new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf new file mode 100644 index 000000000..38635aa9d --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf @@ -0,0 +1,131 @@ +# This configuration file reflects default settings for Apache HTTP Server. +# You may change these, but chances are that you may not need to. + +# Timeout: The number of seconds before receives and sends time out. +Timeout 300 + +# KeepAlive: Whether or not to allow persistent connections (more than +# one request per connection). Set to "Off" to deactivate. +KeepAlive On + +# MaxKeepAliveRequests: The maximum number of requests to allow +# during a persistent connection. Set to 0 to allow an unlimited amount. +# We recommend you leave this number high, for maximum performance. +MaxKeepAliveRequests 100 + +# KeepAliveTimeout: Number of seconds to wait for the next request from the +# same client on the same connection. +KeepAliveTimeout 15 + +# UseCanonicalName: Determines how Apache constructs self-referencing +# URLs and the SERVER_NAME and SERVER_PORT variables. +# When set "Off", Apache will use the Hostname and Port supplied +# by the client. When set "On", Apache will use the value of the +# ServerName directive. +UseCanonicalName Off + +# AccessFileName: The name of the file to look for in each directory +# for additional configuration directives. See also the AllowOverride +# directive. +AccessFileName .htaccess + +# ServerTokens +# This directive configures what you return as the Server HTTP response +# Header. The default is 'Full' which sends information about the OS-Type +# and compiled in modules. +# Set to one of: Full | OS | Minor | Minimal | Major | Prod +# where Full conveys the most information, and Prod the least. +ServerTokens Prod + +# TraceEnable +# This directive overrides the behavior of TRACE for both the core server and +# mod_proxy. The default TraceEnable on permits TRACE requests per RFC 2616, +# which disallows any request body to accompany the request. TraceEnable off +# causes the core server and mod_proxy to return a 405 (Method not allowed) +# error to the client. +# For security reasons this is turned off by default. (bug #240680) +TraceEnable off + +# Optionally add a line containing the server version and virtual host +# name to server-generated pages (internal error documents, FTP directory +# listings, mod_status and mod_info output etc., but not CGI generated +# documents or custom error documents). +# Set to "EMail" to also include a mailto: link to the ServerAdmin. +# Set to one of: On | Off | EMail +ServerSignature On + +# HostnameLookups: Log the names of clients or just their IP addresses +# e.g., www.apache.org (on) or 204.62.129.132 (off). +# The default is off because it'd be overall better for the net if people +# had to knowingly turn this feature on, since enabling it means that +# each client request will result in AT LEAST one lookup request to the +# nameserver. +HostnameLookups Off + +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall is used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +EnableMMAP On +EnableSendfile Off + +# FileETag: Configures the file attributes that are used to create +# the ETag (entity tag) response header field when the document is +# based on a static file. (The ETag value is used in cache management +# to save network bandwidth.) +FileETag MTime Size + +# ContentDigest: This directive enables the generation of Content-MD5 +# headers as defined in RFC1864 respectively RFC2616. +# The Content-MD5 header provides an end-to-end message integrity +# check (MIC) of the entity-body. A proxy or client may check this +# header for detecting accidental modification of the entity-body +# in transit. +# Note that this can cause performance problems on your server since +# the message digest is computed on every request (the values are +# not cached). +# Content-MD5 is only sent for documents served by the core, and not +# by any module. For example, SSI documents, output from CGI scripts, +# and byte range responses do not have this header. +ContentDigest Off + +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +ErrorLog /var/log/apache2/error_log + +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +LogLevel warn + +# We configure the "default" to be a very restrictive set of features. + + Options FollowSymLinks + AllowOverride None + Require all denied + + +# DirectoryIndex: sets the file that Apache will serve if a directory +# is requested. +# +# The index.html.var file (a type-map) is used to deliver content- +# negotiated documents. The MultiViews Options can be used for the +# same purpose, but it is much slower. +# +# Do not change this entry unless you know what you are doing. + + DirectoryIndex index.html index.html.var + + +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. + + Require all denied + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf new file mode 100644 index 000000000..61479fa53 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf @@ -0,0 +1,57 @@ +# The configuration below implements multi-language error documents through +# content-negotiation. + +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html + +# Required modules: mod_alias, mod_include, mod_negotiation +# We use Alias to redirect any /error/HTTP_.html.var response to +# our collection of by-error message multi-language collections. We use +# includes to substitute the appropriate text. +# You can modify the messages' appearance without changing any of the +# default HTTP_.html.var files by adding the line: +# Alias /error/include/ "/your/include/path/" +# which allows you to create your own set of files by starting with the +# /var/www/localhost/error/include/ files and copying them to /your/include/path/, +# even on a per-VirtualHost basis. The default include files will display +# your Apache version number and your ServerAdmin email address regardless +# of the setting of ServerSignature. + + +Alias /error/ "/usr/share/apache2/error/" + + + AllowOverride None + Options IncludesNoExec + AddOutputFilter Includes html + AddHandler type-map var + Require all granted + LanguagePriority en cs de es fr it ja ko nl pl pt-br ro sv tr + ForceLanguagePriority Prefer Fallback + + +ErrorDocument 400 /error/HTTP_BAD_REQUEST.html.var +ErrorDocument 401 /error/HTTP_UNAUTHORIZED.html.var +ErrorDocument 403 /error/HTTP_FORBIDDEN.html.var +ErrorDocument 404 /error/HTTP_NOT_FOUND.html.var +ErrorDocument 405 /error/HTTP_METHOD_NOT_ALLOWED.html.var +ErrorDocument 408 /error/HTTP_REQUEST_TIME_OUT.html.var +ErrorDocument 410 /error/HTTP_GONE.html.var +ErrorDocument 411 /error/HTTP_LENGTH_REQUIRED.html.var +ErrorDocument 412 /error/HTTP_PRECONDITION_FAILED.html.var +ErrorDocument 413 /error/HTTP_REQUEST_ENTITY_TOO_LARGE.html.var +ErrorDocument 414 /error/HTTP_REQUEST_URI_TOO_LARGE.html.var +ErrorDocument 415 /error/HTTP_UNSUPPORTED_MEDIA_TYPE.html.var +ErrorDocument 500 /error/HTTP_INTERNAL_SERVER_ERROR.html.var +ErrorDocument 501 /error/HTTP_NOT_IMPLEMENTED.html.var +ErrorDocument 502 /error/HTTP_BAD_GATEWAY.html.var +ErrorDocument 503 /error/HTTP_SERVICE_UNAVAILABLE.html.var +ErrorDocument 506 /error/HTTP_VARIANT_ALSO_VARIES.html.var + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf new file mode 100644 index 000000000..c429bf94c --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf @@ -0,0 +1,133 @@ +# Settings for hosting different languages. + +# DefaultLanguage and AddLanguage allows you to specify the language of +# a document. You can then use content negotiation to give a browser a +# file in a language the user can understand. +# +# Specify a default language. This means that all data +# going out without a specific language tag (see below) will +# be marked with this one. You probably do NOT want to set +# this unless you are sure it is correct for all cases. +# +# It is generally better to not mark a page as +# being a certain language than marking it with the wrong +# language! +# +# DefaultLanguage nl +# +# Note 1: The suffix does not have to be the same as the language +# keyword --- those with documents in Polish (whose net-standard +# language code is pl) may wish to use "AddLanguage pl .po" to +# avoid the ambiguity with the common suffix for perl scripts. +# +# Note 2: The example entries below illustrate that in some cases +# the two character 'Language' abbreviation is not identical to +# the two character 'Country' code for its country, +# E.g. 'Danmark/dk' versus 'Danish/da'. +# +# Note 3: In the case of 'ltz' we violate the RFC by using a three char +# specifier. There is 'work in progress' to fix this and get +# the reference data for rfc1766 cleaned up. +# +# Catalan (ca) - Croatian (hr) - Czech (cs) - Danish (da) - Dutch (nl) +# English (en) - Esperanto (eo) - Estonian (et) - French (fr) - German (de) +# Greek-Modern (el) - Hebrew (he) - Italian (it) - Japanese (ja) +# Korean (ko) - Luxembourgeois* (ltz) - Norwegian Nynorsk (nn) +# Norwegian (no) - Polish (pl) - Portugese (pt) +# Brazilian Portuguese (pt-BR) - Russian (ru) - Swedish (sv) +# Simplified Chinese (zh-CN) - Spanish (es) - Traditional Chinese (zh-TW) +AddLanguage ca .ca +AddLanguage cs .cz .cs +AddLanguage da .dk +AddLanguage de .de +AddLanguage el .el +AddLanguage en .en +AddLanguage eo .eo +AddLanguage es .es +AddLanguage et .et +AddLanguage fr .fr +AddLanguage he .he +AddLanguage hr .hr +AddLanguage it .it +AddLanguage ja .ja +AddLanguage ko .ko +AddLanguage ltz .ltz +AddLanguage nl .nl +AddLanguage nn .nn +AddLanguage no .no +AddLanguage pl .po +AddLanguage pt .pt +AddLanguage pt-BR .pt-br +AddLanguage ru .ru +AddLanguage sv .sv +AddLanguage zh-CN .zh-cn +AddLanguage zh-TW .zh-tw + +# LanguagePriority allows you to give precedence to some languages +# in case of a tie during content negotiation. +# +# Just list the languages in decreasing order of preference. We have +# more or less alphabetized them here. You probably want to change this. +LanguagePriority en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv zh-CN zh-TW + +# ForceLanguagePriority allows you to serve a result page rather than +# MULTIPLE CHOICES (Prefer) [in case of a tie] or NOT ACCEPTABLE (Fallback) +# [in case no accepted languages matched the available variants] +ForceLanguagePriority Prefer Fallback + +# Commonly used filename extensions to character sets. You probably +# want to avoid clashes with the language extensions, unless you +# are good at carefully testing your setup after each change. +# See http://www.iana.org/assignments/character-sets for the +# official list of charset names and their respective RFCs. +AddCharset us-ascii.ascii .us-ascii +AddCharset ISO-8859-1 .iso8859-1 .latin1 +AddCharset ISO-8859-2 .iso8859-2 .latin2 .cen +AddCharset ISO-8859-3 .iso8859-3 .latin3 +AddCharset ISO-8859-4 .iso8859-4 .latin4 +AddCharset ISO-8859-5 .iso8859-5 .cyr .iso-ru +AddCharset ISO-8859-6 .iso8859-6 .arb .arabic +AddCharset ISO-8859-7 .iso8859-7 .grk .greek +AddCharset ISO-8859-8 .iso8859-8 .heb .hebrew +AddCharset ISO-8859-9 .iso8859-9 .latin5 .trk +AddCharset ISO-8859-10 .iso8859-10 .latin6 +AddCharset ISO-8859-13 .iso8859-13 +AddCharset ISO-8859-14 .iso8859-14 .latin8 +AddCharset ISO-8859-15 .iso8859-15 .latin9 +AddCharset ISO-8859-16 .iso8859-16 .latin10 +AddCharset ISO-2022-JP .iso2022-jp .jis +AddCharset ISO-2022-KR .iso2022-kr .kis +AddCharset ISO-2022-CN .iso2022-cn .cis +AddCharset Big5.Big5 .big5 .b5 +AddCharset cn-Big5 .cn-big5 +# For russian, more than one charset is used (depends on client, mostly): +AddCharset WINDOWS-1251 .cp-1251 .win-1251 +AddCharset CP866 .cp866 +AddCharset KOI8 .koi8 +AddCharset KOI8-E .koi8-e +AddCharset KOI8-r .koi8-r .koi8-ru +AddCharset KOI8-U .koi8-u +AddCharset KOI8-ru .koi8-uk .ua +AddCharset ISO-10646-UCS-2 .ucs2 +AddCharset ISO-10646-UCS-4 .ucs4 +AddCharset UTF-7 .utf7 +AddCharset UTF-8 .utf8 +AddCharset UTF-16 .utf16 +AddCharset UTF-16BE .utf16be +AddCharset UTF-16LE .utf16le +AddCharset UTF-32 .utf32 +AddCharset UTF-32BE .utf32be +AddCharset UTF-32LE .utf32le +AddCharset euc-cn .euc-cn +AddCharset euc-gb .euc-gb +AddCharset euc-jp .euc-jp +AddCharset euc-kr .euc-kr +# Not sure how euc-tw got in - IANA doesn't list it??? +AddCharset EUC-TW .euc-tw +AddCharset gb2312 .gb2312 .gb +AddCharset iso-10646-ucs-2 .ucs-2 .iso-10646-ucs-2 +AddCharset iso-10646-ucs-4 .ucs-4 .iso-10646-ucs-4 +AddCharset shift_jis .shift_jis .sjis + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf new file mode 100644 index 000000000..10bf48317 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf @@ -0,0 +1,85 @@ + + + + +# We include the /icons/ alias for FancyIndexed directory listings. If +# you do not use FancyIndexing, you may comment this out. +Alias /icons/ "/usr/share/apache2/icons/" + + + Options Indexes MultiViews + AllowOverride None + Require all granted + + + +# Directives controlling the display of server-generated directory listings. +# +# To see the listing of a directory, the Options directive for the +# directory must include "Indexes", and the directory must not contain +# a file matching those listed in the DirectoryIndex directive. + +# IndexOptions: Controls the appearance of server-generated directory +# listings. +IndexOptions FancyIndexing VersionSort + +# AddIcon* directives tell the server which icon to show for different +# files or filename extensions. These are only displayed for +# FancyIndexed directories. +AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip + +AddIconByType (TXT,/icons/text.gif) text/* +AddIconByType (IMG,/icons/image2.gif) image/* +AddIconByType (SND,/icons/sound2.gif) audio/* +AddIconByType (VID,/icons/movie.gif) video/* + +AddIcon /icons/binary.gif .bin .exe +AddIcon /icons/binhex.gif .hqx +AddIcon /icons/tar.gif .tar +AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv +AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip +AddIcon /icons/a.gif .ps .ai .eps +AddIcon /icons/layout.gif .html .shtml .htm .pdf +AddIcon /icons/text.gif .txt +AddIcon /icons/c.gif .c +AddIcon /icons/p.gif .pl .py +AddIcon /icons/f.gif .for +AddIcon /icons/dvi.gif .dvi +AddIcon /icons/uuencoded.gif .uu +AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl +AddIcon /icons/tex.gif .tex +AddIcon /icons/bomb.gif core + +AddIcon /icons/back.gif .. +AddIcon /icons/hand.right.gif README +AddIcon /icons/folder.gif ^^DIRECTORY^^ +AddIcon /icons/blank.gif ^^BLANKICON^^ + +# DefaultIcon is which icon to show for files which do not have an icon +# explicitly set. +DefaultIcon /icons/unknown.gif + +# AddDescription allows you to place a short description after a file in +# server-generated indexes. These are only displayed for FancyIndexed +# directories. +# Format: AddDescription "description" filename + +#AddDescription "GZIP compressed document" .gz +#AddDescription "tar archive" .tar +#AddDescription "GZIP compressed tar archive" .tgz + +# ReadmeName is the name of the README file the server will look for by +# default, and append to directory listings. + +# HeaderName is the name of a file which should be prepended to +# directory indexes. +ReadmeName README.html +HeaderName HEADER.html + +# IndexIgnore is a set of filenames which directory indexing should ignore +# and not include in the listing. Shell-style wildcarding is permitted. +IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf new file mode 100644 index 000000000..2cd32c477 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf @@ -0,0 +1,10 @@ + +# Allow remote server configuration reports, with the URL of +# http://servername/server-info + + SetHandler server-info + Require local + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf new file mode 100644 index 000000000..ce0238eee --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf @@ -0,0 +1,35 @@ + +# The following directives define some format nicknames for use with +# a CustomLog directive (see below). +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined +LogFormat "%h %l %u %t \"%r\" %>s %b" common + +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-Agent}i" agent +LogFormat "%v %h %l %u %t \"%r\" %>s %b %T" script +LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" VLOG=%{VLOG}e" vhost + + +# You need to enable mod_logio.c to use %I and %O +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio +LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" vhostio + + +# The location and format of the access logfile (Common Logfile Format). +# If you do not define any access logfiles within a +# container, they will be logged here. Contrariwise, if you *do* +# define per- access logfiles, transactions will be +# logged therein and *not* in this file. +CustomLog /var/log/apache2/access_log common + +# If you would like to have agent and referer logfiles, +# uncomment the following directives. +#CustomLog /var/log/apache2/referer_log referer +#CustomLog /var/log/apache2/agent_logs agent + +# If you prefer a logfile with access, agent, and referer information +# (Combined Logfile Format) you can use the following directive. +#CustomLog /var/log/apache2/access_log combined + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf new file mode 100644 index 000000000..fb8a9a5d5 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf @@ -0,0 +1,46 @@ + +# TypesConfig points to the file containing the list of mappings from +# filename extension to MIME-type. +TypesConfig /etc/mime.types + +# AddType allows you to add to or override the MIME configuration +# file specified in TypesConfig for specific file types. +#AddType application/x-gzip .tgz + +# AddEncoding allows you to have certain browsers uncompress +# information on the fly. Note: Not all browsers support this. +#AddEncoding x-compress .Z +#AddEncoding x-gzip .gz .tgz + +# If the AddEncoding directives above are commented-out, then you +# probably should define those extensions to indicate media types: +AddType application/x-compress .Z +AddType application/x-gzip .gz .tgz + +# AddHandler allows you to map certain file extensions to "handlers": +# actions unrelated to filetype. These can be either built into the server +# or added with the Action directive (see below) + +# To use CGI scripts outside of ScriptAliased directories: +# (You will also need to add "ExecCGI" to the "Options" directive.) +#AddHandler cgi-script .cgi + +# For type maps (negotiated resources): +#AddHandler type-map var + +# Filters allow you to process content before it is sent to the client. +# +# To parse .shtml files for server-side includes (SSI): +# (You will also need to add "Includes" to the "Options" directive.) +#AddType text/html .shtml +#AddOutputFilter INCLUDES .shtml + + + +# The mod_mime_magic module allows the server to use various hints from the +# contents of the file itself to determine its type. The MIMEMagicFile +# directive tells the module where the hint definitions are located. +MIMEMagicFile /etc/apache2/magic + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf new file mode 100644 index 000000000..ed8b3c7cb --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf @@ -0,0 +1,15 @@ + +# Allow server status reports generated by mod_status, +# with the URL of http://servername/server-status + + SetHandler server-status + Require local + + +# ExtendedStatus controls whether Apache will generate "full" status +# information (ExtendedStatus On) or just basic information (ExtendedStatus +# Off) when the "server-status" handler is called. +ExtendedStatus On + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf new file mode 100644 index 000000000..0087126c4 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf @@ -0,0 +1,32 @@ +# Settings for user home directories + +# UserDir: The name of the directory that is appended onto a user's home +# directory if a ~user request is received. Note that you must also set +# the default access control for these directories, as in the example below. +UserDir public_html + +# Control access to UserDir directories. The following is an example +# for a site where these directories are restricted to read-only. + + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + + Require all granted + + + Require all denied + + + +# Suexec isn't really required to run cgi-scripts, but it's a really good +# idea if you have multiple users serving websites... + + + Options ExecCGI + SetHandler cgi-script + + + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf new file mode 100644 index 000000000..bcb9b6b47 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf @@ -0,0 +1,99 @@ +# Server-Pool Management (MPM specific) + +# PidFile: The file in which the server should record its process +# identification number when it starts. +# +# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING +PidFile /run/apache2.pid + +# The accept serialization lock file MUST BE STORED ON A LOCAL DISK. +# Mutex file:/run/apache_mpm_mutex + +# Only one of the below sections will be relevant on your +# installed httpd. Use "/usr/sbin/apache2 -l" to find out the +# active mpm. + +# common MPM configuration +# These configuration directives apply to all MPMs +# +# StartServers: Number of child server processes created at startup +# MaxRequestWorkers: Maximum number of child processes to serve requests +# MaxConnectionsPerChild: Limit on the number of connections that an individual +# child server will handle during its life + + +# prefork MPM +# This is the default MPM if USE=-threads +# +# MinSpareServers: Minimum number of idle child server processes +# MaxSpareServers: Maximum number of idle child server processes + + StartServers 5 + MinSpareServers 5 + MaxSpareServers 10 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# worker MPM +# This is the default MPM if USE=threads +# +# MinSpareThreads: Minimum number of idle threads available to handle request spikes +# MaxSpareThreads: Maximum number of idle threads +# ThreadsPerChild: Number of threads created by each child process + + StartServers 2 + MinSpareThreads 25 + MaxSpareThreads 75 + ThreadsPerChild 25 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# event MPM +# +# MinSpareThreads: Minimum number of idle threads available to handle request spikes +# MaxSpareThreads: Maximum number of idle threads +# ThreadsPerChild: Number of threads created by each child process + + StartServers 2 + MinSpareThreads 25 + MaxSpareThreads 75 + ThreadsPerChild 25 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# peruser MPM +# +# MinSpareProcessors: Minimum number of idle child server processes +# MinProcessors: Minimum number of processors per virtual host +# MaxProcessors: Maximum number of processors per virtual host +# ExpireTimeout: Maximum idle time before a child is killed, 0 to disable +# Multiplexer: Specify a Multiplexer child configuration. +# Processor: Specify a user and group for a specific child process + + MinSpareProcessors 2 + MinProcessors 2 + MaxProcessors 10 + MaxRequestWorkers 150 + MaxConnectionsPerChild 1000 + ExpireTimeout 1800 + + Multiplexer nobody nobody + Processor apache apache + + +# itk MPM +# +# MinSpareServers: Minimum number of idle child server processes +# MaxSpareServers: Maximum number of idle child server processes + + StartServers 5 + MinSpareServers 5 + MaxSpareServers 10 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf new file mode 100644 index 000000000..520d9fd82 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf @@ -0,0 +1,10 @@ + +# 128MB cache for objects < 2MB +CacheEnable mem / +MCacheSize 131072 +MCacheMaxObjectCount 1000 +MCacheMinObjectSize 1 +MCacheMaxObjectSize 2097152 + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf new file mode 100644 index 000000000..f51de4641 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf @@ -0,0 +1,67 @@ +# Note: The following must must be present to support +# starting without SSL on platforms with no /dev/random equivalent +# but a statically compiled-in mod_ssl. + +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin + + + +# This is the Apache server configuration file providing SSL support. +# It contains the configuration directives to instruct the server how to +# serve pages over an https connection. For detailing information about these +# directives see + +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. + +## Pseudo Random Number Generator (PRNG): +# Configure one or more sources to seed the PRNG of the SSL library. +# The seed data should be of good random quality. +# WARNING! On some platforms /dev/random blocks if not enough entropy +# is available. This means you then cannot use the /dev/random device +# because it would lead to very long connection times (as long as +# it requires to make more entropy available). But usually those +# platforms additionally provide a /dev/urandom device which doesn't +# block. So, if available, use this one instead. Read the mod_ssl User +# Manual for more details. +#SSLRandomSeed startup file:/dev/random 512 +#SSLRandomSeed startup file:/dev/urandom 512 +#SSLRandomSeed connect file:/dev/random 512 +#SSLRandomSeed connect file:/dev/urandom 512 + +## SSL Global Context: +# All SSL configuration in this context applies both to the main server and +# all SSL-enabled virtual hosts. + +# Some MIME-types for downloading Certificates and CRLs + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + + +## Pass Phrase Dialog: +# Configure the pass phrase gathering process. The filtering dialog program +# (`builtin' is a internal terminal dialog) has to provide the pass phrase on +# stdout. +SSLPassPhraseDialog builtin + +## Inter-Process Session Cache: +# Configure the SSL Session Cache: First the mechanism to use and second the +# expiring timeout (in seconds). +#SSLSessionCache dbm:/run/ssl_scache +SSLSessionCache shmcb:/run/ssl_scache(512000) +SSLSessionCacheTimeout 300 + +## Semaphore: +# Configure the path to the mutual exclusion semaphore the SSL engine uses +# internally for inter-process synchronization. +Mutex file:/run/apache_ssl_mutex ssl-cache + +## SSL Compression: +# Known to be vulnerable thus disabled by default (bug #507324). +SSLCompression off + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf new file mode 100644 index 000000000..e4c9454e0 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf @@ -0,0 +1,9 @@ + + + # enable debugging for this module + #LogLevel http2:info + + #Enable HTTP/2 support + Protocols h2 h2c http/1.1 + + diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf new file mode 100644 index 000000000..36f6b9cca --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf @@ -0,0 +1,19 @@ + +DavLockDB "/var/lib/dav/lockdb" + +# The following directives disable redirects on non-GET requests for +# a directory that does not include the trailing slash. This fixes a +# problem with several clients that do not appropriately handle +# redirects for folders with DAV methods. + +BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully +BrowserMatch "MS FrontPage" redirect-carefully +BrowserMatch "^WebDrive" redirect-carefully +BrowserMatch "^WebDAVFS/1.[012345678]" redirect-carefully +BrowserMatch "^gnome-vfs/1.0" redirect-carefully +BrowserMatch "^XML Spy" redirect-carefully +BrowserMatch "^Dreamweaver-WebDAV-SCM1" redirect-carefully + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf new file mode 100644 index 000000000..883061fee --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf @@ -0,0 +1,18 @@ +# Examples below are taken from the online documentation +# Refer to: +# http://localhost/manual/mod/mod_ldap.html +# http://localhost/manual/mod/mod_auth_ldap.html + +LDAPSharedCacheSize 200000 +LDAPCacheEntries 1024 +LDAPCacheTTL 600 +LDAPOpCacheEntries 1024 +LDAPOpCacheTTL 600 + + + SetHandler ldap-status + Require local + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/.keep_www-servers_apache-2 b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/.keep_www-servers_apache-2 new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf new file mode 100644 index 000000000..bb395473c --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf @@ -0,0 +1,191 @@ + + + +# see bug #178966 why this is in here + +# When we also provide SSL we have to listen to the HTTPS port +# Note: Configurations that use IPv6 but not IPv4-mapped addresses need two +# Listen directives: "Listen [::]:443" and "Listen 0.0.0.0:443" +Listen 443 + + + ServerName localhost + Include /etc/apache2/vhosts.d/default_vhost.include + ErrorLog /var/log/apache2/ssl_error_log + + + TransferLog /var/log/apache2/ssl_access_log + + + ## SSL Engine Switch: + # Enable/Disable SSL for this virtual host. + SSLEngine on + + ## SSLProtocol: + # Don't use SSLv2 anymore as it's considered to be broken security-wise. + # Also disable SSLv3 as most modern browsers are capable of TLS. + SSLProtocol ALL -SSLv2 -SSLv3 + + ## SSL Cipher Suite: + # List the ciphers that the client is permitted to negotiate. + # See the mod_ssl documentation for a complete list. + # This list of ciphers is recommended by mozilla and was stripped off + # its RC4 ciphers. (bug #506924) + SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:HIGH:!RC4:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK + + ## SSLHonorCipherOrder: + # Prefer the server's cipher preference order as the client may have a + # weak default order. + SSLHonorCipherOrder On + + ## Server Certificate: + # Point SSLCertificateFile at a PEM encoded certificate. If the certificate + # is encrypted, then you will be prompted for a pass phrase. Note that a + # kill -HUP will prompt again. Keep in mind that if you have both an RSA + # and a DSA certificate you can configure both in parallel (to also allow + # the use of DSA ciphers, etc.) + SSLCertificateFile /etc/ssl/apache2/server.crt + + ## Server Private Key: + # If the key is not combined with the certificate, use this directive to + # point at the key file. Keep in mind that if you've both a RSA and a DSA + # private key you can configure both in parallel (to also allow the use of + # DSA ciphers, etc.) + SSLCertificateKeyFile /etc/ssl/apache2/server.key + + ## Server Certificate Chain: + # Point SSLCertificateChainFile at a file containing the concatenation of + # PEM encoded CA certificates which form the certificate chain for the + # server certificate. Alternatively the referenced file can be the same as + # SSLCertificateFile when the CA certificates are directly appended to the + # server certificate for convinience. + #SSLCertificateChainFile /etc/ssl/apache2/ca.crt + + ## Certificate Authority (CA): + # Set the CA certificate verification path where to find CA certificates + # for client authentication or alternatively one huge file containing all + # of them (file must be PEM encoded). + # Note: Inside SSLCACertificatePath you need hash symlinks to point to the + # certificate files. Use the provided Makefile to update the hash symlinks + # after changes. + #SSLCACertificatePath /etc/ssl/apache2/ssl.crt + #SSLCACertificateFile /etc/ssl/apache2/ca-bundle.crt + + ## Certificate Revocation Lists (CRL): + # Set the CA revocation path where to find CA CRLs for client authentication + # or alternatively one huge file containing all of them (file must be PEM + # encoded). + # Note: Inside SSLCARevocationPath you need hash symlinks to point to the + # certificate files. Use the provided Makefile to update the hash symlinks + # after changes. + #SSLCARevocationPath /etc/ssl/apache2/ssl.crl + #SSLCARevocationFile /etc/ssl/apache2/ca-bundle.crl + + ## Client Authentication (Type): + # Client certificate verification type and depth. Types are none, optional, + # require and optional_no_ca. Depth is a number which specifies how deeply + # to verify the certificate issuer chain before deciding the certificate is + # not valid. + #SSLVerifyClient require + #SSLVerifyDepth 10 + + ## Access Control: + # With SSLRequire you can do per-directory access control based on arbitrary + # complex boolean expressions containing server variable checks and other + # lookup directives. The syntax is a mixture between C and Perl. See the + # mod_ssl documentation for more details. + # + # #SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ + # and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ + # and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ + # and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ + # and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ + # or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ + # + + ## SSL Engine Options: + # Set various options for the SSL engine. + + ## FakeBasicAuth: + # Translate the client X.509 into a Basic Authorisation. This means that the + # standard Auth/DBMAuth methods can be used for access control. The user + # name is the `one line' version of the client's X.509 certificate. + # Note that no password is obtained from the user. Every entry in the user + # file needs this password: `xxj31ZMTZzkVA'. + + ## ExportCertData: + # This exports two additional environment variables: SSL_CLIENT_CERT and + # SSL_SERVER_CERT. These contain the PEM-encoded certificates of the server + # (always existing) and the client (only existing when client + # authentication is used). This can be used to import the certificates into + # CGI scripts. + + ## StdEnvVars: + # This exports the standard SSL/TLS related `SSL_*' environment variables. + # Per default this exportation is switched off for performance reasons, + # because the extraction step is an expensive operation and is usually + # useless for serving static content. So one usually enables the exportation + # for CGI and SSI requests only. + + ## StrictRequire: + # This denies access when "SSLRequireSSL" or "SSLRequire" applied even under + # a "Satisfy any" situation, i.e. when it applies access is denied and no + # other module can change it. + + ## OptRenegotiate: + # This enables optimized SSL connection renegotiation handling when SSL + # directives are used in per-directory context. + #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + + SSLOptions +StdEnvVars + + + ## SSL Protocol Adjustments: + # The safe and default but still SSL/TLS standard compliant shutdown + # approach is that mod_ssl sends the close notify alert but doesn't wait + # for the close notify alert from client. When you need a different + # shutdown approach you can use one of the following variables: + + ## ssl-unclean-shutdown: + # This forces an unclean shutdown when the connection is closed, i.e. no + # SSL close notify alert is send or allowed to received. This violates the + # SSL/TLS standard but is needed for some brain-dead browsers. Use this when + # you receive I/O errors because of the standard approach where mod_ssl + # sends the close notify alert. + + ## ssl-accurate-shutdown: + # This forces an accurate shutdown when the connection is closed, i.e. a + # SSL close notify alert is send and mod_ssl waits for the close notify + # alert of the client. This is 100% SSL/TLS standard compliant, but in + # practice often causes hanging connections with brain-dead browsers. Use + # this only for browsers where you know that their SSL implementation works + # correctly. + # Notice: Most problems of broken clients are also related to the HTTP + # keep-alive facility, so you usually additionally want to disable + # keep-alive for those clients, too. Use variable "nokeepalive" for this. + # Similarly, one has to force some clients to use HTTP/1.0 to workaround + # their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and + # "force-response-1.0" for this. + + BrowserMatch ".*MSIE.*" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + + + ## Per-Server Logging: + # The home of a custom SSL log file. Use this when you want a compact + # non-error SSL logfile on a virtual host basis. + + CustomLog /var/log/apache2/ssl_request_log \ + "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf new file mode 100644 index 000000000..b9766b5f1 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf @@ -0,0 +1,45 @@ +# Virtual Hosts +# +# If you want to maintain multiple domains/hostnames on your +# machine you can setup VirtualHost containers for them. Most configurations +# use only name-based virtual hosts so the server doesn't need to worry about +# IP addresses. This is indicated by the asterisks in the directives below. +# +# Please see the documentation at +# +# for further details before you try to setup virtual hosts. +# +# You may use the command line option '-S' to verify your virtual host +# configuration. + + +# see bug #178966 why this is in here + +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 + +# When virtual hosts are enabled, the main host defined in the default +# httpd.conf configuration will go away. We redefine it here so that it is +# still available. +# +# If you disable this vhost by removing -D DEFAULT_VHOST from +# /etc/conf.d/apache2, the first defined virtual host elsewhere will be +# the default. + + ServerName localhost + Include /etc/apache2/vhosts.d/default_vhost.include + + + ServerEnvironment apache apache + + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include new file mode 100644 index 000000000..af6ece85b --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include @@ -0,0 +1,71 @@ +# ServerAdmin: Your address, where problems with the server should be +# e-mailed. This address appears on some server-generated pages, such +# as error documents. e.g. admin@your-domain.com +ServerAdmin root@localhost + +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +# If you change this to something that isn't under /var/www then suexec +# will no longer work. +DocumentRoot "/var/www/localhost/htdocs" + +# This should be changed to whatever you set DocumentRoot to. + + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + Options Indexes FollowSymLinks + + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # Options FileInfo AuthConfig Limit + AllowOverride All + + # Controls who can get stuff from this server. + Require all granted + + + + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a section to allow access to + # the filesystem path. + + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + ScriptAlias /cgi-bin/ "/var/www/localhost/cgi-bin/" + + +# "/var/www/localhost/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. + + AllowOverride None + Options None + Require all granted + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf new file mode 100644 index 000000000..41de4d236 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf @@ -0,0 +1,7 @@ + + ServerName gentoo.example.com + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/conf.d/apache2 b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/conf.d/apache2 new file mode 100644 index 000000000..b7ecb4f2a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/conf.d/apache2 @@ -0,0 +1,74 @@ +# /etc/conf.d/apache2: config file for /etc/init.d/apache2 + +# When you install a module it is easy to activate or deactivate the modules +# and other features of apache using the APACHE2_OPTS line. Every module should +# install a configuration in /etc/apache2/modules.d. In that file will have an +# directive where NNN is the option to enable that module. +# +# Here are the options available in the default configuration: +# +# AUTH_DIGEST Enables mod_auth_digest +# AUTHNZ_LDAP Enables authentication through mod_ldap (available if USE=ldap) +# CACHE Enables mod_cache +# DAV Enables mod_dav +# ERRORDOCS Enables default error documents for many languages. +# INFO Enables mod_info, a useful module for debugging +# LANGUAGE Enables content-negotiation based on language and charset. +# LDAP Enables mod_ldap (available if USE=ldap) +# MANUAL Enables /manual/ to be the apache manual (available if USE=docs) +# MEM_CACHE Enables default configuration mod_mem_cache +# PROXY Enables mod_proxy +# SSL Enables SSL (available if USE=ssl) +# STATUS Enabled mod_status, a useful module for statistics +# SUEXEC Enables running CGI scripts (in USERDIR) through suexec. +# USERDIR Enables /~username mapping to /home/username/public_html +# +# +# The following two options provide the default virtual host for the HTTP and +# HTTPS protocol. YOU NEED TO ENABLE AT LEAST ONE OF THEM, otherwise apache +# will not listen for incomming connections on the approriate port. +# +# DEFAULT_VHOST Enables name-based virtual hosts, with the default +# virtual host being in /var/www/localhost/htdocs +# SSL_DEFAULT_VHOST Enables default vhost for SSL (you should enable this +# when you enable SSL) +# +APACHE2_OPTS="-D DEFAULT_VHOST -D INFO -D SSL -D SSL_DEFAULT_VHOST -D LANGUAGE" + +# Extended options for advanced uses of Apache ONLY +# You don't need to edit these unless you are doing crazy Apache stuff +# As not having them set correctly, or feeding in an incorrect configuration +# via them will result in Apache failing to start +# YOU HAVE BEEN WARNED. + +# PID file +#PIDFILE=/var/run/apache2.pid + +# timeout for startup/shutdown checks +#TIMEOUT=10 + +# ServerRoot setting +#SERVERROOT=/usr/lib64/apache2 + +# Configuration file location +# - If this does NOT start with a '/', then it is treated relative to +# $SERVERROOT by Apache +#CONFIGFILE=/etc/apache2/httpd.conf + +# Location to log startup errors to +# They are normally dumped to your terminal. +#STARTUPERRORLOG="/var/log/apache2/startuperror.log" + +# A command that outputs a formatted text version of the HTML at the URL +# of the command line. Designed for lynx, however other programs may work. +#LYNX="lynx -dump" + +# The URL to your server's mod_status status page. +# Required for status and fullstatus +#STATUSURL="http://localhost/server-status" + +# Method to use when reloading the server +# Valid options are 'restart' and 'graceful' +# See http://httpd.apache.org/docs/2.2/stopping.html for information on +# what they do and how they differ. +#RELOAD_TYPE="graceful" diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/sites b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/sites new file mode 100644 index 000000000..7f0b3a8b3 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/sites @@ -0,0 +1,3 @@ +vhosts.d/gentoo.example.com.conf, gentoo.example.com +vhosts.d/00_default_vhost.conf, localhost +vhosts.d/00_default_ssl_vhost.conf, localhost diff --git a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py index 62464d5d0..8cea97f04 100644 --- a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py +++ b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py @@ -1,6 +1,6 @@ """Test for certbot_apache.tls_sni_01.""" -import unittest import shutil +import unittest import mock @@ -16,14 +16,15 @@ from six.moves import xrange # pylint: disable=redefined-builtin, import-error class TlsSniPerformTest(util.ApacheTest): """Test the ApacheTlsSni01 challenge.""" - auth_key = common_test.TLSSNI01Test.auth_key - achalls = common_test.TLSSNI01Test.achalls + auth_key = common_test.AUTH_KEY + achalls = common_test.ACHALLS def setUp(self): # pylint: disable=arguments-differ super(TlsSniPerformTest, self).setUp() config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir) + self.config_path, self.vhost_path, self.config_dir, + self.work_dir) config.config.tls_sni_01_port = 443 from certbot_apache import tls_sni_01 @@ -41,8 +42,8 @@ class TlsSniPerformTest(util.ApacheTest): @mock.patch("certbot.util.exe_exists") @mock.patch("certbot.util.run_script") def test_perform1(self, _, mock_exists): - mock_register = mock.Mock() - self.sni.configurator.reverter.register_undo_command = mock_register + self.sni.configurator.parser.modules.add("socache_shmcb_module") + self.sni.configurator.parser.modules.add("ssl_module") mock_exists.return_value = True self.sni.configurator.parser.update_runtime_variables = mock.Mock() @@ -55,10 +56,6 @@ class TlsSniPerformTest(util.ApacheTest): self.sni._setup_challenge_cert = mock_setup_cert responses = self.sni.perform() - - # Make sure that register_undo_command was called into temp directory. - self.assertEqual(True, mock_register.call_args[0][0]) - mock_setup_cert.assert_called_once_with(achall) # Check to make sure challenge config path is included in apache config @@ -71,7 +68,7 @@ class TlsSniPerformTest(util.ApacheTest): def test_perform2(self): # Avoid load module self.sni.configurator.parser.modules.add("ssl_module") - + self.sni.configurator.parser.modules.add("socache_shmcb_module") acme_responses = [] for achall in self.achalls: self.sni.add_chall(achall) @@ -81,7 +78,8 @@ class TlsSniPerformTest(util.ApacheTest): # pylint: disable=protected-access self.sni._setup_challenge_cert = mock_setup_cert - with mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod"): + with mock.patch( + "certbot_apache.override_debian.DebianConfigurator.enable_mod"): sni_responses = self.sni.perform() self.assertEqual(mock_setup_cert.call_count, 2) diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 34d2476f7..1daaa00c5 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -1,14 +1,14 @@ """Common utilities for certbot_apache.""" import os +import shutil import sys import unittest import augeas +import josepy as jose import mock import zope.component -from acme import jose - from certbot.display import util as display_util from certbot.plugins import common @@ -16,7 +16,7 @@ from certbot.plugins import common from certbot.tests import util as test_util from certbot_apache import configurator -from certbot_apache import constants +from certbot_apache import entrypoint from certbot_apache import obj @@ -38,6 +38,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods self.rsa512jwk = jose.JWKRSA.load(test_util.load_vector( "rsa512_key.pem")) + self.config = get_apache_configurator(self.config_path, vhost_root, + self.config_dir, self.work_dir) + # Make sure all vhosts in sites-enabled are symlinks (Python packaging # does not preserve symlinks) sites_enabled = os.path.join(self.config_path, "sites-enabled") @@ -55,8 +58,13 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods os.path.pardir, "sites-available", vhost_basename) os.symlink(target, vhost) + def tearDown(self): + shutil.rmtree(self.temp_dir) + shutil.rmtree(self.config_dir) + shutil.rmtree(self.work_dir) -class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods + +class ParserTest(ApacheTest): def setUp(self, test_dir="debian_apache_2_4/multiple_vhosts", config_root="debian_apache_2_4/multiple_vhosts/apache2", @@ -72,12 +80,16 @@ class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods with mock.patch("certbot_apache.parser.ApacheParser." "update_runtime_variables"): self.parser = ApacheParser( - self.aug, self.config_path, self.vhost_path) + self.aug, self.config_path, self.vhost_path, + configurator=self.config) -def get_apache_configurator( +def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-locals config_path, vhost_path, - config_dir, work_dir, version=(2, 4, 7), conf=None): + config_dir, work_dir, version=(2, 4, 7), + conf=None, + os_info="generic", + conf_vhost_path=None): """Create an Apache Configurator with the specified options. :param conf: Function that returns binary paths. self.conf in Configurator @@ -86,31 +98,47 @@ def get_apache_configurator( backups = os.path.join(work_dir, "backups") mock_le_config = mock.MagicMock( apache_server_root=config_path, - apache_vhost_root=vhost_path, - apache_le_vhost_ext=constants.os_constant("le_vhost_ext"), + apache_vhost_root=conf_vhost_path, + apache_le_vhost_ext="-le-ssl.conf", apache_challenge_location=config_path, backup_dir=backups, config_dir=config_dir, + http01_port=80, temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) - with mock.patch("certbot_apache.configurator.util.run_script"): - with mock.patch("certbot_apache.configurator.util." - "exe_exists") as mock_exe_exists: - mock_exe_exists.return_value = True - with mock.patch("certbot_apache.parser.ApacheParser." - "update_runtime_variables"): - config = configurator.ApacheConfigurator( - config=mock_le_config, - name="apache", - version=version) - # This allows testing scripts to set it a bit more quickly - if conf is not None: - config.conf = conf # pragma: no cover + orig_os_constant = configurator.ApacheConfigurator(mock_le_config, + name="apache", + version=version).constant - config.prepare() + def mock_os_constant(key, vhost_path=vhost_path): + """Mock default vhost path""" + if key == "vhost_root": + return vhost_path + else: + return orig_os_constant(key) + with mock.patch("certbot_apache.configurator.ApacheConfigurator.constant") as mock_cons: + mock_cons.side_effect = mock_os_constant + with mock.patch("certbot_apache.configurator.util.run_script"): + with mock.patch("certbot_apache.configurator.util." + "exe_exists") as mock_exe_exists: + mock_exe_exists.return_value = True + with mock.patch("certbot_apache.parser.ApacheParser." + "update_runtime_variables"): + try: + config_class = entrypoint.OVERRIDE_CLASSES[os_info] + except KeyError: + config_class = configurator.ApacheConfigurator + config = config_class(config=mock_le_config, name="apache", + version=version) + # This allows testing scripts to set it a bit more + # quickly + if conf is not None: + config.conf = conf # pragma: no cover + + config.prepare() return config @@ -142,7 +170,7 @@ def get_vh_truth(temp_dir, config_name): os.path.join(prefix, "certbot.conf"), os.path.join(aug_pre, "certbot.conf/VirtualHost"), set([obj.Addr.fromstring("*:80")]), False, True, - "certbot.demo"), + "certbot.demo", aliases=["www.certbot.demo"]), obj.VirtualHost( os.path.join(prefix, "mod_macro-example.conf"), os.path.join(aug_pre, diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index b276f49f8..38f41e9f1 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -63,7 +63,7 @@ setup( }, entry_points={ 'certbot.plugins': [ - 'apache = certbot_apache.configurator:ApacheConfigurator', + 'apache = certbot_apache.entrypoint:ENTRYPOINT', ], }, test_suite='certbot_apache', diff --git a/certbot-auto b/certbot-auto index 25f2ce889..d3a5c23e5 100755 --- a/certbot-auto +++ b/certbot-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.19.0" +LE_AUTO_VERSION="0.21.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -68,10 +68,12 @@ for arg in "$@" ; do NO_BOOTSTRAP=1;; --help) HELP=1;; - --noninteractive|--non-interactive|renew) - ASSUME_YES=1;; + --noninteractive|--non-interactive) + NONINTERACTIVE=1;; --quiet) QUIET=1;; + renew) + ASSUME_YES=1;; --verbose) VERBOSE=1;; -[!-]*) @@ -93,7 +95,7 @@ done if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 + NONINTERACTIVE=1 HELP=0 fi @@ -244,23 +246,42 @@ DeprecationBootstrap() { fi } - +MIN_PYTHON_VERSION="2.6" +MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') +# Sets LE_PYTHON to Python version string and PYVER to the first two +# digits of the python version DeterminePythonVersion() { - for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do - # Break (while keeping the LE_PYTHON value) if found. - $EXISTS "$LE_PYTHON" > /dev/null && break - done - if [ "$?" != "0" ]; then - error "Cannot find any Pythons; please install one!" - exit 1 + # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + # + # If no Python is found, PYVER is set to 0. + if [ "$USE_PYTHON_3" = 1 ]; then + for LE_PYTHON in "$LE_PYTHON" python3; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + else + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + fi + if [ "$?" != "0" ]; then + if [ "$1" != "NOCRASH" ]; then + error "Cannot find any Pythons; please install one!" + exit 1 + else + PYVER=0 + return 0 + fi fi - export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ "$PYVER" -lt 26 ]; then - error "You have an ancient version of Python entombed in your operating system..." - error "This isn't going to work; you'll need at least version 2.6." - exit 1 + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + if [ "$1" != "NOCRASH" ]; then + error "You have an ancient version of Python entombed in your operating system..." + error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION." + exit 1 + fi fi } @@ -384,23 +405,19 @@ BootstrapDebCommon() { fi } -# If new packages are installed by BootstrapRpmCommon below, this version -# number must be increased. -BOOTSTRAP_RPM_COMMON_VERSION=1 - -BootstrapRpmCommon() { - # Tested with: - # - Fedora 20, 21, 22, 23 (x64) - # - Centos 7 (x64: on DigitalOcean droplet) - # - CentOS 7 Minimal install in a Hyper-V VM - # - CentOS 6 (EPEL must be installed manually) +# If new packages are installed by BootstrapRpmCommonBase below, version +# numbers in rpm_common.sh and rpm_python3.sh must be increased. +# Sets TOOL to the name of the package manager +# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG. +# Enables EPEL if applicable and possible. +InitializeRPMCommonBase() { if type dnf 2>/dev/null then - tool=dnf + TOOL=dnf elif type yum 2>/dev/null then - tool=yum + TOOL=yum else error "Neither yum nor dnf found. Aborting bootstrap!" @@ -408,15 +425,15 @@ BootstrapRpmCommon() { fi if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" + YES_FLAG="-y" fi if [ "$QUIET" = 1 ]; then QUIET_FLAG='--quiet' fi - if ! $tool list *virtualenv >/dev/null 2>&1; then + if ! $TOOL list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $tool list epel-release >/dev/null 2>&1; then + if ! $TOOL list epel-release >/dev/null 2>&1; then error "Enable the EPEL repository and try running Certbot again." exit 1 fi @@ -425,14 +442,20 @@ BootstrapRpmCommon() { sleep 1s /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 second..." sleep 1s fi - if ! $tool install $yes_flag $QUIET_FLAG epel-release; then + if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then error "Could not enable EPEL. Aborting bootstrap!" exit 1 fi fi +} + +BootstrapRpmCommonBase() { + # Arguments: whitespace-delimited python packages to install + + InitializeRPMCommonBase # This call is superfluous in practice pkgs=" gcc @@ -444,10 +467,39 @@ BootstrapRpmCommon() { ca-certificates " - # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $tool list python >/dev/null 2>&1; then + # Add the python packages + pkgs="$pkgs + $1 + " + + if $TOOL list installed "httpd" >/dev/null 2>&1; then pkgs="$pkgs - python + mod_ssl + " + fi + + if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then + error "Could not install OS dependencies. Aborting bootstrap!" + exit 1 + fi +} + +# If new packages are installed by BootstrapRpmCommon below, this version +# number must be increased. +BOOTSTRAP_RPM_COMMON_VERSION=1 + +BootstrapRpmCommon() { + # Tested with: + # - Fedora 20, 21, 22, 23 (x64) + # - Centos 7 (x64: on DigitalOcean droplet) + # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 + + InitializeRPMCommonBase + + # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. + if $TOOL list python >/dev/null 2>&1; then + python_pkgs="$python python-devel python-virtualenv python-tools @@ -455,9 +507,8 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $tool list python2 >/dev/null 2>&1; then - pkgs="$pkgs - python2 + elif $TOOL list python2 >/dev/null 2>&1; then + python_pkgs="$python2 python2-libs python2-setuptools python2-devel @@ -468,8 +519,7 @@ BootstrapRpmCommon() { # Some distros and older versions of current distros use a "python27" # instead of the "python" or "python-" naming convention. else - pkgs="$pkgs - python27 + python_pkgs="$python27 python27-devel python27-virtualenv python27-tools @@ -477,16 +527,31 @@ BootstrapRpmCommon() { " fi - if $tool list installed "httpd" >/dev/null 2>&1; then - pkgs="$pkgs - mod_ssl - " - fi + BootstrapRpmCommonBase "$python_pkgs" +} - if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then - error "Could not install OS dependencies. Aborting bootstrap!" +# If new packages are installed by BootstrapRpmPython3 below, this version +# number must be increased. +BOOTSTRAP_RPM_PYTHON3_VERSION=1 + +BootstrapRpmPython3() { + # Tested with: + # - CentOS 6 + + InitializeRPMCommonBase + + # EPEL uses python34 + if $TOOL list python34 >/dev/null 2>&1; then + python_pkgs="python34 + python34-devel + python34-tools + " + else + error "No supported Python package available to install. Aborting bootstrap!" exit 1 fi + + BootstrapRpmCommonBase "$python_pkgs" } # If new packages are installed by BootstrapSuseCommon below, this version @@ -696,13 +761,8 @@ BootstrapMageiaCommon() { # Set Bootstrap to the function that installs OS dependencies on this system # and BOOTSTRAP_VERSION to the unique identifier for the current version of # that function. If Bootstrap is set to a function that doesn't install any -# packages (either because --no-bootstrap was included on the command line or -# we don't know how to bootstrap on this system), BOOTSTRAP_VERSION is not set. -if [ "$NO_BOOTSTRAP" = 1 ]; then - Bootstrap() { - : - } -elif [ -f /etc/debian_version ]; then +# packages BOOTSTRAP_VERSION is not set. +if [ -f /etc/debian_version ]; then Bootstrap() { BootstrapMessage "Debian-based OSes" BootstrapDebCommon @@ -715,11 +775,27 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + # Run DeterminePythonVersion to decide on the basis of available Python versions + # whether to use 2.x or 3.x on RedHat-like systems. + # Then, revert LE_PYTHON to its previous state. + prev_le_python="$LE_PYTHON" + unset LE_PYTHON + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -eq 26 ]; then + Bootstrap() { + BootstrapMessage "RedHat-based OSes that will use Python3" + BootstrapRpmPython3 + } + USE_PYTHON_3=1 + BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" + else + Bootstrap() { + BootstrapMessage "RedHat-based OSes" + BootstrapRpmCommon + } + BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + fi + LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -782,6 +858,17 @@ else } fi +# We handle this case after determining the normal bootstrap version to allow +# variables like USE_PYTHON_3 to be properly set. As described above, if the +# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not +# be set so we unset it here. +if [ "$NO_BOOTSTRAP" = 1 ]; then + Bootstrap() { + : + } + unset BOOTSTRAP_VERSION +fi + # Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used # to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set # if it is unknown how OS dependencies were installed on this system. @@ -816,7 +903,11 @@ TempDir() { mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS } - +# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise, +# returns a non-zero number. +OldVenvExists() { + [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] +} if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -824,14 +915,26 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg SetPrevBootstrapVersion + if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then + unset LE_PYTHON + fi + INSTALLED_VERSION="none" - if [ -d "$VENV_PATH" ]; then + if [ -d "$VENV_PATH" ] || OldVenvExists; then # If the selected Bootstrap function isn't a noop and it differs from the # previously used version if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then # if non-interactive mode or stdin and stdout are connected to a terminal if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then - rm -rf "$VENV_PATH" + if [ -d "$VENV_PATH" ]; then + rm -rf "$VENV_PATH" + fi + # In the case the old venv was just a symlink to the new one, + # OldVenvExists is now false because we deleted the venv at VENV_PATH. + if OldVenvExists; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi RerunWithArgs "$@" else error "Skipping upgrade because new OS dependencies may need to be installed." @@ -841,6 +944,10 @@ if [ "$1" = "--le-auto-phase2" ]; then error "install any required packages." # Set INSTALLED_VERSION to be the same so we don't update the venv INSTALLED_VERSION="$LE_AUTO_VERSION" + # Continue to use OLD_VENV_PATH if the new venv doesn't exist + if [ ! -d "$VENV_PATH" ]; then + VENV_BIN="$OLD_VENV_PATH/bin" + fi fi elif [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings @@ -858,10 +965,18 @@ if [ "$1" = "--le-auto-phase2" ]; then say "Creating virtual environment..." DeterminePythonVersion rm -rf "$VENV_PATH" - if [ "$VERBOSE" = 1 ]; then - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + if [ "$PYVER" -le 27 ]; then + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi else - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + if [ "$VERBOSE" = 1 ]; then + "$LE_PYTHON" -m venv "$VENV_PATH" + else + "$LE_PYTHON" -m venv "$VENV_PATH" > /dev/null + fi fi if [ -n "$BOOTSTRAP_VERSION" ]; then @@ -983,9 +1098,16 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +josepy==1.0.1 \ + --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ + --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f packaging==16.8 \ @@ -1062,9 +1184,6 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba # Contains the requirements for the letsencrypt package. # @@ -1077,18 +1196,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.19.0 \ - --hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \ - --hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53 -acme==0.19.0 \ - --hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \ - --hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822 -certbot-apache==0.19.0 \ - --hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \ - --hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619 -certbot-nginx==0.19.0 \ - --hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \ - --hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1 +certbot==0.21.1 \ + --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ + --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea +acme==0.21.1 \ + --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ + --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b +certbot-apache==0.21.1 \ + --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ + --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 +certbot-nginx==0.21.1 \ + --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ + --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -1318,9 +1437,10 @@ else # upgrading. Phase 1 checks the version of the latest release of # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. + export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then - if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if ! OldVenvExists; then if [ "$HELP" = 1 ]; then echo "$USAGE" exit 0 @@ -1352,17 +1472,22 @@ On failure, return non-zero. """ -from __future__ import print_function +from __future__ import print_function, unicode_literals from distutils.version import LooseVersion from json import loads from os import devnull, environ from os.path import dirname, join import re +import ssl from subprocess import check_call, CalledProcessError from sys import argv, exit -from urllib2 import build_opener, HTTPHandler, HTTPSHandler -from urllib2 import HTTPError, URLError +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler + from urllib2 import HTTPError, URLError +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler + from urllib.error import HTTPError, URLError PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq @@ -1384,8 +1509,11 @@ class HttpsGetter(object): def __init__(self): """Build an HTTPS opener.""" # Based on pip 1.4.1's URLOpener - # This verifies certs on only Python >=2.7.9. - self._opener = build_opener(HTTPSHandler()) + # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. + if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): + self._opener = build_opener(HTTPSHandler(context=cert_none_context())) + else: + self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: for handler in self._opener.handlers: if isinstance(handler, HTTPHandler): @@ -1407,7 +1535,7 @@ class HttpsGetter(object): def write(contents, dir, filename): """Write something to a file in a certain directory.""" - with open(join(dir, filename), 'w') as file: + with open(join(dir, filename), 'wb') as file: file.write(contents) @@ -1415,13 +1543,13 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/certbot/json'))) + 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8')) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in metadata['releases'].iterkeys() + in metadata['releases'].keys() if re.match('^[0-9.]+$', r))) @@ -1438,7 +1566,7 @@ def verified_new_le_auto(get, tag, temp_dir): 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') - write(PUBLIC_KEY, temp_dir, 'public_key.pem') + write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem') try: with open(devnull, 'w') as dev_null: check_call(['openssl', 'dgst', '-sha256', '-verify', @@ -1453,6 +1581,14 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) +def cert_none_context(): + """Create a SSLContext object to not check hostname.""" + # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.verify_mode = ssl.CERT_NONE + return context + + def main(): get = HttpsGetter().get flag = argv[1] @@ -1474,8 +1610,10 @@ if __name__ == '__main__': UNLIKELY_EOF # --------------------------------------------------------------------------- - DeterminePythonVersion - if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." + elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 2e9e68daf..1d2cfdeca 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -9,8 +9,7 @@ import zope.interface from certbot import configuration from certbot import errors as le_errors from certbot import util as certbot_util -from certbot_apache import configurator -from certbot_apache import constants +from certbot_apache import entrypoint from certbot_compatibility_test import errors from certbot_compatibility_test import interfaces from certbot_compatibility_test import util @@ -56,13 +55,14 @@ class Proxy(configurators_common.Proxy): def _prepare_configurator(self): """Prepares the Apache plugin for testing""" - for k in constants.CLI_DEFAULTS_DEBIAN.keys(): - setattr(self.le_config, "apache_" + k, constants.os_constant(k)) + for k in entrypoint.ENTRYPOINT.OS_DEFAULTS.keys(): + setattr(self.le_config, "apache_" + k, + entrypoint.ENTRYPOINT.OS_DEFAULTS[k]) # An alias self.le_config.apache_handle_modules = self.le_config.apache_handle_mods - self._configurator = configurator.ApacheConfigurator( + self._configurator = entrypoint.ENTRYPOINT( config=configuration.NamespaceConfig(self.le_config), name="apache") self._configurator.prepare() diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index af951aa6a..4155944bd 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -6,7 +6,8 @@ import re import shutil import tarfile -from acme import jose +import josepy as jose + from acme import test_util from certbot import constants diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 166c383b3..8f9f897cf 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 6392e483c..612e7259f 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 304acf110..3157400c6 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py index 11c8c57d5..3b8edce64 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py @@ -5,7 +5,6 @@ import unittest import digitalocean import mock -import six from certbot import errors from certbot.plugins import dns_test_common @@ -132,10 +131,10 @@ class DigitalOceanClientTest(unittest.TestCase): self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - correct_record_mock.destroy.assert_called() + self.assertTrue(correct_record_mock.destroy.called) - six.assertCountEqual(self, first_record_mock.destroy.call_args_list, []) - six.assertCountEqual(self, last_record_mock.destroy.call_args_list, []) + self.assertFalse(first_record_mock.destroy.call_args_list) + self.assertFalse(last_record_mock.destroy.call_args_list) def test_del_txt_record_error_finding_domain(self): self.manager.get_all_domains.side_effect = API_ERROR diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 489321435..1a68400fa 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 67d68ee16..35de47308 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index 88e02304e..a946d00a4 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index b40899e80..8585fc848 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 1b72168e8..4fec37e29 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index e9dc2b31d..dca9ebf27 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 79b523aed..bfa72b50b 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py index ff07b6ccd..d5f1b2816 100644 --- a/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py +++ b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py @@ -31,7 +31,7 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest self.auth._change_txt_record.assert_called_once_with("UPSERT", '_acme-challenge.' + DOMAIN, mock.ANY) - self.auth._wait_for_change.assert_called_once() + self.assertEqual(self.auth._wait_for_change.call_count, 1) def test_perform_no_credentials_error(self): self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) @@ -183,7 +183,8 @@ class ClientTest(unittest.TestCase): self.client._change_txt_record("FOO", DOMAIN, "foo") - self.client.r53.change_resource_record_sets.assert_called_once() + call_count = self.client.r53.change_resource_record_sets.call_count + self.assertEqual(call_count, 1) def test_wait_for_change(self): self.client.r53.get_change = mock.MagicMock( @@ -192,7 +193,7 @@ class ClientTest(unittest.TestCase): self.client._wait_for_change(1) - self.client.r53.get_change.assert_called() + self.assertTrue(self.client.r53.get_change.called) if __name__ == "__main__": diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 2a14e8ab1..8df687972 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.22.0.dev0' install_requires = [ 'acme=={0}'.format(version), diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 7e55ff0ea..9f091c0fd 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -23,42 +23,14 @@ from certbot import util from certbot.plugins import common from certbot_nginx import constants -from certbot_nginx import tls_sni_01 +from certbot_nginx import nginxparser from certbot_nginx import parser +from certbot_nginx import tls_sni_01 +from certbot_nginx import http_01 logger = logging.getLogger(__name__) -REDIRECT_BLOCK = [[ - ['\n ', 'if', ' ', '($scheme', ' ', '!=', ' ', '"https") '], - [['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'], - '\n '] -], ['\n']] - -TEST_REDIRECT_BLOCK = [ - [ - ['if', '($scheme', '!=', '"https")'], - [ - ['return', '301', 'https://$host$request_uri'] - ] - ], - ['#', ' managed by Certbot'] -] - -REDIRECT_COMMENT_BLOCK = [ - ['\n ', '#', ' Redirect non-https traffic to https'], - ['\n ', '#', ' if ($scheme != "https") {'], - ['\n ', '#', " return 301 https://$host$request_uri;"], - ['\n ', '#', " } # managed by Certbot"], - ['\n'] -] - -TEST_REDIRECT_COMMENT_BLOCK = [ - ['#', ' Redirect non-https traffic to https'], - ['#', ' if ($scheme != "https") {'], - ['#', " return 301 https://$host$request_uri;"], - ['#', " } # managed by Certbot"], -] @zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller) @zope.interface.provider(interfaces.IPluginFactory) @@ -117,6 +89,9 @@ class NginxConfigurator(common.Installer): # Files to save self.save_notes = "" + # For creating new vhosts if no names match + self.new_vhost = None + # Add number of outstanding challenges self._chall_out = 0 @@ -191,14 +166,14 @@ class NginxConfigurator(common.Installer): "The nginx plugin currently requires --fullchain-path to " "install a cert.") - vhost = self.choose_vhost(domain) - cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path], - ['\n', 'ssl_certificate_key', ' ', key_path]] + vhost = self.choose_vhost(domain, create_if_no_match=True) + cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path], + ['\n ', 'ssl_certificate_key', ' ', key_path]] self.parser.add_server_directives(vhost, cert_directives, replace=True) logger.info("Deployed Certificate to VirtualHost %s for %s", - vhost.filep, vhost.names) + vhost.filep, ", ".join(vhost.names)) self.save_notes += ("Changed vhost at %s with addresses of %s\n" % (vhost.filep, @@ -209,7 +184,7 @@ class NginxConfigurator(common.Installer): ####################### # Vhost parsing methods ####################### - def choose_vhost(self, target_name): + def choose_vhost(self, target_name, create_if_no_match=False): """Chooses a virtual host based on the given domain name. .. note:: This makes the vhost SSL-enabled if it isn't already. Follows @@ -223,6 +198,9 @@ class NginxConfigurator(common.Installer): hostname. Currently we just ignore this. :param str target_name: domain name + :param bool create_if_no_match: If we should create a new vhost from default + when there is no match found. If we can't choose a default, raise a + MisconfigurationError. :returns: ssl vhost associated with name :rtype: :class:`~certbot_nginx.obj.VirtualHost` @@ -233,20 +211,82 @@ class NginxConfigurator(common.Installer): matches = self._get_ranked_matches(target_name) vhost = self._select_best_name_match(matches) if not vhost: - # No matches. Raise a misconfiguration error. - raise errors.MisconfigurationError( - ("Cannot find a VirtualHost matching domain %s. " - "In order for Certbot to correctly perform the challenge " - "please add a corresponding server_name directive to your " - "nginx configuration: " - "https://nginx.org/en/docs/http/server_names.html") % (target_name)) - else: - # Note: if we are enhancing with ocsp, vhost should already be ssl. - if not vhost.ssl: - self._make_server_ssl(vhost) + if create_if_no_match: + vhost = self._vhost_from_duplicated_default(target_name) + else: + # No matches. Raise a misconfiguration error. + raise errors.MisconfigurationError( + ("Cannot find a VirtualHost matching domain %s. " + "In order for Certbot to correctly perform the challenge " + "please add a corresponding server_name directive to your " + "nginx configuration: " + "https://nginx.org/en/docs/http/server_names.html") % (target_name)) + # Note: if we are enhancing with ocsp, vhost should already be ssl. + if not vhost.ssl: + self._make_server_ssl(vhost) return vhost + def ipv6_info(self, port): + """Returns tuple of booleans (ipv6_active, ipv6only_present) + ipv6_active is true if any server block listens ipv6 address in any port + + ipv6only_present is true if ipv6only=on option exists in any server + block ipv6 listen directive for the specified port. + + :param str port: Port to check ipv6only=on directive for + + :returns: Tuple containing information if IPv6 is enabled in the global + configuration, and existence of ipv6only directive for specified port + :rtype: tuple of type (bool, bool) + """ + vhosts = self.parser.get_vhosts() + ipv6_active = False + ipv6only_present = False + for vh in vhosts: + for addr in vh.addrs: + if addr.ipv6: + ipv6_active = True + if addr.ipv6only and addr.get_port() == port: + ipv6only_present = True + return (ipv6_active, ipv6only_present) + + def _vhost_from_duplicated_default(self, domain, port=None): + if self.new_vhost is None: + default_vhost = self._get_default_vhost(port) + self.new_vhost = self.parser.duplicate_vhost(default_vhost, delete_default=True) + self.new_vhost.names = set() + + self._add_server_name_to_vhost(self.new_vhost, domain) + return self.new_vhost + + def _add_server_name_to_vhost(self, vhost, domain): + vhost.names.add(domain) + name_block = [['\n ', 'server_name']] + for name in vhost.names: + name_block[0].append(' ') + name_block[0].append(name) + self.parser.add_server_directives(vhost, name_block, replace=True) + + def _get_default_vhost(self, port): + vhost_list = self.parser.get_vhosts() + # if one has default_server set, return that one + default_vhosts = [] + for vhost in vhost_list: + for addr in vhost.addrs: + if addr.default: + if port is None or self._port_matches(port, addr.get_port()): + default_vhosts.append(vhost) + break + + if len(default_vhosts) == 1: + return default_vhosts[0] + + # TODO: present a list of vhosts for user to choose from + + raise errors.MisconfigurationError("Could not automatically find a matching server" + " block. Set the `server_name` directive to use the Nginx installer.") + def _get_ranked_matches(self, target_name): """Returns a ranked list of vhosts that match target_name. The ranking gives preference to SSL vhosts. @@ -319,7 +359,7 @@ class NginxConfigurator(common.Installer): return sorted(matches, key=lambda x: x['rank']) - def choose_redirect_vhost(self, target_name, port): + def choose_redirect_vhost(self, target_name, port, create_if_no_match=False): """Chooses a single virtual host for redirect enhancement. Chooses the vhost most closely matching target_name that is @@ -333,12 +373,27 @@ class NginxConfigurator(common.Installer): :param str target_name: domain name :param str port: port number + :param bool create_if_no_match: If we should create a new vhost from default + when there is no match found. If we can't choose a default, raise a + MisconfigurationError. + :returns: vhost associated with name :rtype: :class:`~certbot_nginx.obj.VirtualHost` """ matches = self._get_redirect_ranked_matches(target_name, port) - return self._select_best_name_match(matches) + vhost = self._select_best_name_match(matches) + if not vhost and create_if_no_match: + vhost = self._vhost_from_duplicated_default(target_name, port=port) + return vhost + + def _port_matches(self, test_port, matching_port): + # test_port is a number, matching is a number or "" or None + if matching_port == "" or matching_port is None: + # if no port is specified, Nginx defaults to listening on port 80. + return test_port == self.DEFAULT_LISTEN_PORT + else: + return test_port == matching_port def _get_redirect_ranked_matches(self, target_name, port): """Gets a ranked list of plaintextish port-listening vhosts matching target_name @@ -347,20 +402,13 @@ class NginxConfigurator(common.Installer): Rank by how well these match target_name. :param str target_name: The name to match - :param str port: port number + :param str port: port number as a string :returns: list of dicts containing the vhost, the matching name, and the numerical rank :rtype: list """ all_vhosts = self.parser.get_vhosts() - def _port_matches(test_port, matching_port): - # test_port is a number, matching is a number or "" or None - if matching_port == "" or matching_port is None: - # if no port is specified, Nginx defaults to listening on port 80. - return test_port == self.DEFAULT_LISTEN_PORT - else: - return test_port == matching_port def _vhost_matches(vhost, port): found_matching_port = False @@ -370,7 +418,7 @@ class NginxConfigurator(common.Installer): found_matching_port = (port == self.DEFAULT_LISTEN_PORT) else: for addr in vhost.addrs: - if _port_matches(port, addr.get_port()) and addr.ssl == False: + if self._port_matches(port, addr.get_port()) and addr.ssl == False: found_matching_port = True if found_matching_port: @@ -405,9 +453,12 @@ class NginxConfigurator(common.Installer): all_names.add(host) elif not common.private_ips_regex.match(host): # If it isn't a private IP, do a reverse DNS lookup - # TODO: IPv6 support try: - socket.inet_aton(host) + if addr.ipv6: + host = addr.get_ipv6_exploded() + socket.inet_pton(socket.AF_INET6, host) + else: + socket.inet_pton(socket.AF_INET, host) all_names.add(socket.gethostbyaddr(host)[0]) except (socket.error, socket.herror, socket.timeout): continue @@ -433,26 +484,47 @@ class NginxConfigurator(common.Installer): def _make_server_ssl(self, vhost): """Make a server SSL. - Make a server SSL based on server_name and filename by adding a - ``listen IConfig.tls_sni_01_port ssl`` directive to the server block. - - .. todo:: Maybe this should create a new block instead of modifying - the existing one? + Make a server SSL by adding new listen and SSL directives. :param vhost: The vhost to add SSL to. :type vhost: :class:`~certbot_nginx.obj.VirtualHost` """ + ipv6info = self.ipv6_info(self.config.tls_sni_01_port) + ipv6_block = [''] + ipv4_block = [''] + # If the vhost was implicitly listening on the default Nginx port, # have it continue to do so. if len(vhost.addrs) == 0: listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]] self.parser.add_server_directives(vhost, listen_block, replace=False) + if vhost.ipv6_enabled(): + ipv6_block = ['\n ', + 'listen', + ' ', + '[::]:{0}'.format(self.config.tls_sni_01_port), + ' ', + 'ssl'] + if not ipv6info[1]: + # ipv6only=on is absent in global config + ipv6_block.append(' ') + ipv6_block.append('ipv6only=on') + + if vhost.ipv4_enabled(): + ipv4_block = ['\n ', + 'listen', + ' ', + '{0}'.format(self.config.tls_sni_01_port), + ' ', + 'ssl'] + snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() ssl_block = ([ - ['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], + ipv6_block, + ipv4_block, ['\n ', 'ssl_certificate', ' ', snakeoil_cert], ['\n ', 'ssl_certificate_key', ' ', snakeoil_key], ['\n ', 'include', ' ', self.mod_ssl_conf], @@ -489,27 +561,23 @@ class NginxConfigurator(common.Installer): logger.warning("Failed %s for %s", enhancement, domain) raise - def _has_certbot_redirect(self, vhost): - return vhost.contains_list(TEST_REDIRECT_BLOCK) + def _has_certbot_redirect(self, vhost, domain): + test_redirect_block = _test_block_from_block(_redirect_block_for_domain(domain)) + return vhost.contains_list(test_redirect_block) - def _has_certbot_redirect_comment(self, vhost): - return vhost.contains_list(TEST_REDIRECT_COMMENT_BLOCK) - - def _add_redirect_block(self, vhost, active=True): + def _add_redirect_block(self, vhost, domain): """Add redirect directive to vhost """ - if active: - redirect_block = REDIRECT_BLOCK - else: - redirect_block = REDIRECT_COMMENT_BLOCK + redirect_block = _redirect_block_for_domain(domain) self.parser.add_server_directives( - vhost, redirect_block, replace=False) + vhost, redirect_block, replace=False, insert_at_top=True) def _enable_redirect(self, domain, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. - Add rewrite directive to non https traffic + If the vhost is listening plaintextishly, separate out the + relevant directives into a new server block and add a rewrite directive. .. note:: This function saves the configuration @@ -522,26 +590,45 @@ class NginxConfigurator(common.Installer): vhost = None # If there are blocks listening plaintextishly on self.DEFAULT_LISTEN_PORT, # choose the most name-matching one. + vhost = self.choose_redirect_vhost(domain, port) if vhost is None: logger.info("No matching insecure server blocks listening on port %s found.", self.DEFAULT_LISTEN_PORT) + return + + new_vhost = None + if vhost.ssl: + new_vhost = self.parser.duplicate_vhost(vhost, + only_directives=['listen', 'server_name']) + + def _ssl_match_func(directive): + return 'ssl' in directive + + def _no_ssl_match_func(directive): + return 'ssl' not in directive + + # remove all ssl addresses from the new block + self.parser.remove_server_directives(new_vhost, 'listen', match_func=_ssl_match_func) + + # remove all non-ssl addresses from the existing block + self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func) + + # Add this at the bottom to get the right order of directives + return_404_directive = [['\n ', 'return', ' ', '404']] + self.parser.add_server_directives(new_vhost, return_404_directive, replace=False) + + vhost = new_vhost + + if self._has_certbot_redirect(vhost, domain): + logger.info("Traffic on port %s already redirecting to ssl in %s", + self.DEFAULT_LISTEN_PORT, vhost.filep) else: - if self._has_certbot_redirect(vhost): - logger.info("Traffic on port %s already redirecting to ssl in %s", - self.DEFAULT_LISTEN_PORT, vhost.filep) - elif vhost.has_redirect(): - if not self._has_certbot_redirect_comment(vhost): - self._add_redirect_block(vhost, active=False) - logger.info("The appropriate server block is already redirecting " - "traffic. To enable redirect anyway, uncomment the " - "redirect lines in %s.", vhost.filep) - else: - # Redirect plaintextish host to https - self._add_redirect_block(vhost, active=True) - logger.info("Redirecting all traffic on port %s to ssl in %s", - self.DEFAULT_LISTEN_PORT, vhost.filep) + # Redirect plaintextish host to https + self._add_redirect_block(vhost, domain) + logger.info("Redirecting all traffic on port %s to ssl in %s", + self.DEFAULT_LISTEN_PORT, vhost.filep) def _enable_ocsp_stapling(self, domain, chain_path): """Include OCSP response in TLS handshake @@ -715,6 +802,7 @@ class NginxConfigurator(common.Installer): """ super(NginxConfigurator, self).recovery_routine() + self.new_vhost = None self.parser.load() def revert_challenge_config(self): @@ -724,6 +812,7 @@ class NginxConfigurator(common.Installer): """ self.revert_temporary_config() + self.new_vhost = None self.parser.load() def rollback_checkpoints(self, rollback=1): @@ -736,6 +825,7 @@ class NginxConfigurator(common.Installer): """ super(NginxConfigurator, self).rollback_checkpoints(rollback) + self.new_vhost = None self.parser.load() ########################################################################### @@ -743,7 +833,7 @@ class NginxConfigurator(common.Installer): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.TLSSNI01] + return [challenges.TLSSNI01, challenges.HTTP01] # Entry point in main.py for performing challenges def perform(self, achalls): @@ -756,15 +846,20 @@ class NginxConfigurator(common.Installer): """ self._chall_out += len(achalls) responses = [None] * len(achalls) - chall_doer = tls_sni_01.NginxTlsSni01(self) + sni_doer = tls_sni_01.NginxTlsSni01(self) + http_doer = http_01.NginxHttp01(self) for i, achall in enumerate(achalls): # Currently also have chall_doer hold associated index of the # challenge. This helps to put all of the responses back together # when they are all complete. - chall_doer.add_chall(achall, i) + if isinstance(achall.chall, challenges.HTTP01): + http_doer.add_chall(achall, i) + else: # tls-sni-01 + sni_doer.add_chall(achall, i) - sni_response = chall_doer.perform() + sni_response = sni_doer.perform() + http_response = http_doer.perform() # Must restart in order to activate the challenges. # Handled here because we may be able to load up other challenge types self.restart() @@ -772,8 +867,9 @@ class NginxConfigurator(common.Installer): # Go through all of the challenges and assign them to the proper place # in the responses return value. All responses must be in the same order # as the original challenges. - for i, resp in enumerate(sni_response): - responses[chall_doer.indices[i]] = resp + for chall_response, chall_doer in ((sni_response, sni_doer), (http_response, http_doer)): + for i, resp in enumerate(chall_response): + responses[chall_doer.indices[i]] = resp return responses @@ -788,6 +884,19 @@ class NginxConfigurator(common.Installer): self.restart() +def _test_block_from_block(block): + test_block = nginxparser.UnspacedList(block) + parser.comment_directive(test_block, 0) + return test_block[:-1] + +def _redirect_block_for_domain(domain): + redirect_block = [[ + ['\n ', 'if', ' ', '($host', ' ', '=', ' ', '%s)' % domain, ' '], + [['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'], + '\n ']], + ['\n']] + return redirect_block + def nginx_restart(nginx_ctl, nginx_conf): """Restarts the Nginx Server. diff --git a/certbot-nginx/certbot_nginx/http_01.py b/certbot-nginx/certbot_nginx/http_01.py new file mode 100644 index 000000000..c0dec061a --- /dev/null +++ b/certbot-nginx/certbot_nginx/http_01.py @@ -0,0 +1,203 @@ +"""A class that performs HTTP-01 challenges for Nginx""" + +import logging +import os + +from acme import challenges + +from certbot import errors +from certbot.plugins import common + +from certbot_nginx import obj +from certbot_nginx import nginxparser + + +logger = logging.getLogger(__name__) + + +class NginxHttp01(common.ChallengePerformer): + """HTTP-01 authenticator for Nginx + + :ivar configurator: NginxConfigurator object + :type configurator: :class:`~nginx.configurator.NginxConfigurator` + + :ivar list achalls: Annotated + class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge` + challenges + + :param list indices: Meant to hold indices of challenges in a + larger array. NginxHttp01 is capable of solving many challenges + at once which causes an indexing issue within NginxConfigurator + who must return all responses in order. Imagine NginxConfigurator + maintaining state about where all of the http-01 Challenges, + TLS-SNI-01 Challenges belong in the response array. This is an + optional utility. + + """ + + def __init__(self, configurator): + super(NginxHttp01, self).__init__(configurator) + self.challenge_conf = os.path.join( + configurator.config.config_dir, "le_http_01_cert_challenge.conf") + self._ipv6 = None + self._ipv6only = None + + def perform(self): + """Perform a challenge on Nginx. + + :returns: list of :class:`certbot.acme.challenges.HTTP01Response` + :rtype: list + + """ + if not self.achalls: + return [] + + responses = [x.response(x.account_key) for x in self.achalls] + + # Set up the configuration + self._mod_config() + + # Save reversible changes + self.configurator.save("HTTP Challenge", True) + + return responses + + def _mod_config(self): + """Modifies Nginx config to include server_names_hash_bucket_size directive + and server challenge blocks. + + :raises .MisconfigurationError: + Unable to find a suitable HTTP block in which to include + authenticator hosts. + """ + included = False + include_directive = ['\n', 'include', ' ', self.challenge_conf] + root = self.configurator.parser.config_root + + bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128'] + + main = self.configurator.parser.parsed[root] + for line in main: + if line[0] == ['http']: + body = line[1] + found_bucket = False + posn = 0 + for inner_line in body: + if inner_line[0] == bucket_directive[1]: + if int(inner_line[1]) < int(bucket_directive[3]): + body[posn] = bucket_directive + found_bucket = True + posn += 1 + if not found_bucket: + body.insert(0, bucket_directive) + if include_directive not in body: + body.insert(0, include_directive) + included = True + break + if not included: + raise errors.MisconfigurationError( + 'Certbot could not find a block to include ' + 'challenges in %s.' % root) + config = [self._make_or_mod_server_block(achall) for achall in self.achalls] + config = [x for x in config if x is not None] + config = nginxparser.UnspacedList(config) + + self.configurator.reverter.register_file_creation( + True, self.challenge_conf) + + with open(self.challenge_conf, "w") as new_conf: + nginxparser.dump(config, new_conf) + + def _default_listen_addresses(self): + """Finds addresses for a challenge block to listen on. + :returns: list of :class:`certbot_nginx.obj.Addr` to apply + :rtype: list + """ + addresses = [] + default_addr = "%s" % self.configurator.config.http01_port + ipv6_addr = "[::]:{0}".format( + self.configurator.config.http01_port) + port = self.configurator.config.http01_port + + if self._ipv6 is None or self._ipv6only is None: + self._ipv6, self._ipv6only = self.configurator.ipv6_info(port) + ipv6, ipv6only = self._ipv6, self._ipv6only + + if ipv6: + # If IPv6 is active in Nginx configuration + if not ipv6only: + # If ipv6only=on is not already present in the config + ipv6_addr = ipv6_addr + " ipv6only=on" + addresses = [obj.Addr.fromstring(default_addr), + obj.Addr.fromstring(ipv6_addr)] + logger.info(("Using default addresses %s and %s for authentication."), + default_addr, + ipv6_addr) + else: + addresses = [obj.Addr.fromstring(default_addr)] + logger.info("Using default address %s for authentication.", + default_addr) + return addresses + + def _get_validation_path(self, achall): + return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, achall.chall.encode("token")) + + def _make_server_block(self, achall): + """Creates a server block for a challenge. + :param achall: Annotated HTTP-01 challenge + :type achall: + :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge` + :param list addrs: addresses of challenged domain + :class:`list` of type :class:`~nginx.obj.Addr` + :returns: server block for the challenge host + :rtype: list + """ + addrs = self._default_listen_addresses() + block = [['listen', ' ', addr.to_string(include_default=False)] for addr in addrs] + + # Ensure we 404 on any other request by setting a root + document_root = os.path.join( + self.configurator.config.work_dir, "http_01_nonexistent") + + validation = achall.validation(achall.account_key) + validation_path = self._get_validation_path(achall) + + block.extend([['server_name', ' ', achall.domain], + ['root', ' ', document_root], + [['location', ' ', '=', ' ', validation_path], + [['default_type', ' ', 'text/plain'], + ['return', ' ', '200', ' ', validation]]]]) + # TODO: do we want to return something else if they otherwise access this block? + return [['server'], block] + + def _make_or_mod_server_block(self, achall): + """Modifies a server block to respond to a challenge. + + :param achall: Annotated HTTP-01 challenge + :type achall: + :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge` + + """ + try: + vhost = self.configurator.choose_redirect_vhost(achall.domain, + '%i' % self.configurator.config.http01_port, create_if_no_match=True) + except errors.MisconfigurationError: + # Couldn't find either a matching name+port server block + # or a port+default_server block, so create a dummy block + return self._make_server_block(achall) + + # Modify existing server block + validation = achall.validation(achall.account_key) + validation_path = self._get_validation_path(achall) + + location_directive = [[['location', ' ', '=', ' ', validation_path], + [['default_type', ' ', 'text/plain'], + ['return', ' ', '200', ' ', validation]]]] + + self.configurator.parser.add_server_directives(vhost, + location_directive, replace=False) + + rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)', + ' ', '$1', ' ', 'break']] + self.configurator.parser.add_server_directives(vhost, + rewrite_directive, replace=False, insert_at_top=True) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 20aeeb554..14481e298 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -7,6 +7,7 @@ from pyparsing import ( Literal, White, Forward, Group, Optional, OneOrMore, QuotedString, Regex, ZeroOrMore, Combine) from pyparsing import stringEnd from pyparsing import restOfLine +import six logger = logging.getLogger(__name__) @@ -71,7 +72,7 @@ class RawNginxDumper(object): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks for b0 in blocks: - if isinstance(b0, str): + if isinstance(b0, six.string_types): yield b0 continue item = copy.deepcopy(b0) @@ -88,7 +89,7 @@ class RawNginxDumper(object): yield '}' else: # not a block - list of strings semicolon = ";" - if isinstance(item[0], str) and item[0].strip() == '#': # comment + if isinstance(item[0], six.string_types) and item[0].strip() == '#': # comment semicolon = "" yield "".join(item) + semicolon @@ -145,7 +146,7 @@ def dump(blocks, _file): return _file.write(dumps(blocks)) -spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == '' +spacey = lambda x: (isinstance(x, six.string_types) and x.isspace()) or x == '' class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" @@ -189,13 +190,15 @@ class UnspacedList(list): item, spaced_item = self._coerce(x) slicepos = self._spaced_position(i) if i < len(self) else len(self.spaced) self.spaced.insert(slicepos, spaced_item) - list.insert(self, i, item) + if not spacey(item): + list.insert(self, i, item) self.dirty = True def append(self, x): item, spaced_item = self._coerce(x) self.spaced.append(spaced_item) - list.append(self, item) + if not spacey(item): + list.append(self, item) self.dirty = True def extend(self, x): @@ -226,7 +229,8 @@ class UnspacedList(list): raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") item, spaced_item = self._coerce(value) self.spaced.__setitem__(self._spaced_position(i), spaced_item) - list.__setitem__(self, i, item) + if not spacey(item): + list.__setitem__(self, i, item) self.dirty = True def __delitem__(self, i): @@ -235,8 +239,8 @@ class UnspacedList(list): self.dirty = True def __deepcopy__(self, memo): - l = UnspacedList(self[:]) - l.spaced = copy.deepcopy(self.spaced, memo=memo) + new_spaced = copy.deepcopy(self.spaced, memo=memo) + l = UnspacedList(new_spaced) l.dirty = self.dirty return l diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 849cefe1f..e8dc8936d 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -34,10 +34,13 @@ class Addr(common.Addr): UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0') CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0] - def __init__(self, host, port, ssl, default): + def __init__(self, host, port, ssl, default, ipv6, ipv6only): + # pylint: disable=too-many-arguments super(Addr, self).__init__((host, port)) self.ssl = ssl self.default = default + self.ipv6 = ipv6 + self.ipv6only = ipv6only self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES @classmethod @@ -46,6 +49,8 @@ class Addr(common.Addr): parts = str_addr.split(' ') ssl = False default = False + ipv6 = False + ipv6only = False host = '' port = '' @@ -56,15 +61,25 @@ class Addr(common.Addr): if addr.startswith('unix:'): return None - tup = addr.partition(':') - if re.match(r'^\d+$', tup[0]): - # This is a bare port, not a hostname. E.g. listen 80 - host = '' - port = tup[0] + # IPv6 check + ipv6_match = re.match(r'\[.*\]', addr) + if ipv6_match: + ipv6 = True + # IPv6 handling + host = ipv6_match.group() + # The rest of the addr string will be the port, if any + port = addr[ipv6_match.end()+1:] else: - # This is a host-port tuple. E.g. listen 127.0.0.1:* - host = tup[0] - port = tup[2] + # IPv4 handling + tup = addr.partition(':') + if re.match(r'^\d+$', tup[0]): + # This is a bare port, not a hostname. E.g. listen 80 + host = '' + port = tup[0] + else: + # This is a host-port tuple. E.g. listen 127.0.0.1:* + host = tup[0] + port = tup[2] # The rest of the parts are options; we only care about ssl and default while len(parts) > 0: @@ -73,8 +88,10 @@ class Addr(common.Addr): ssl = True elif nextpart == 'default_server': default = True + elif nextpart == "ipv6only=on": + ipv6only = True - return cls(host, port, ssl, default) + return cls(host, port, ssl, default, ipv6, ipv6only) def to_string(self, include_default=True): """Return string representation of Addr""" @@ -114,8 +131,6 @@ class Addr(common.Addr): self.tup[1]), self.ipv6) == \ common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS, other.tup[1]), other.ipv6) - # Nginx plugin currently doesn't support IPv6 but this will - # future-proof it return super(Addr, self).__eq__(other) def __eq__(self, other): @@ -178,31 +193,26 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods return False - def has_redirect(self): - """Determine if this vhost has a redirecting statement - """ - for directive_name in REDIRECT_DIRECTIVES: - found = _find_directive(self.raw, directive_name) - if found is not None: - return True - return False - def contains_list(self, test): """Determine if raw server block contains test list at top level """ - for i in six.moves.range(0, len(self.raw) - len(test)): + for i in six.moves.range(0, len(self.raw) - len(test) + 1): if self.raw[i:i + len(test)] == test: return True return False -def _find_directive(directives, directive_name): - """Find a directive of type directive_name in directives - """ - if not directives or isinstance(directives, str) or len(directives) == 0: - return None + def ipv6_enabled(self): + """Return true if one or more of the listen directives in vhost supports + IPv6""" + for a in self.addrs: + if a.ipv6: + return True - if directives[0] == directive_name: - return directives - - matches = (_find_directive(line, directive_name) for line in directives) - return next((m for m in matches if m is not None), None) + def ipv4_enabled(self): + """Return true if one or more of the listen directives in vhost are IPv4 + only""" + if self.addrs is None or len(self.addrs) == 0: + return True + for a in self.addrs: + if not a.ipv6: + return True diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 158cb9929..fbd6c0ade 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -1,11 +1,14 @@ """NginxParser is a member object of the NginxConfigurator class.""" import copy +import functools import glob import logging import os import pyparsing import re +import six + from certbot import errors from certbot_nginx import obj @@ -273,7 +276,7 @@ class NginxParser(object): return False - def add_server_directives(self, vhost, directives, replace): + def add_server_directives(self, vhost, directives, replace, insert_at_top=False): """Add or replace directives in the server block identified by vhost. This method modifies vhost to be fully consistent with the new directives. @@ -290,8 +293,34 @@ class NginxParser(object): whose information we use to match on :param list directives: The directives to add :param bool replace: Whether to only replace existing directives + :param bool insert_at_top: True if the directives need to be inserted at the top + of the server block instead of the bottom """ + self._modify_server_directives(vhost, + functools.partial(_add_directives, directives, replace, insert_at_top)) + + def remove_server_directives(self, vhost, directive_name, match_func=None): + """Remove all directives of type directive_name. + + :param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost + to remove directives from + :param string directive_name: The directive type to remove + :param callable match_func: Function of the directive that returns true for directives + to be deleted. + """ + self._modify_server_directives(vhost, + functools.partial(_remove_directives, directive_name, match_func)) + + def _update_vhost_based_on_new_directives(self, vhost, directives_list): + new_server = self._get_included_directives(directives_list) + parsed_server = self.parse_server(new_server) + vhost.addrs = parsed_server['addrs'] + vhost.ssl = parsed_server['ssl'] + vhost.names = parsed_server['names'] + vhost.raw = new_server + + def _modify_server_directives(self, vhost, block_func): filename = vhost.filep try: result = self.parsed[filename] @@ -300,18 +329,54 @@ class NginxParser(object): if not isinstance(result, list) or len(result) != 2: raise errors.MisconfigurationError("Not a server block.") result = result[1] - _add_directives(result, directives, replace) + block_func(result) - # update vhost based on new directives - new_server = self._get_included_directives(result) - parsed_server = self.parse_server(new_server) - vhost.addrs = parsed_server['addrs'] - vhost.ssl = parsed_server['ssl'] - vhost.names = parsed_server['names'] - vhost.raw = new_server + self._update_vhost_based_on_new_directives(vhost, result) except errors.MisconfigurationError as err: raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err))) + def duplicate_vhost(self, vhost_template, delete_default=False, only_directives=None): + """Duplicate the vhost in the configuration files. + + :param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost + whose information we copy + :param bool delete_default: If we should remove default_server + from listen directives in the block. + :param list only_directives: If it exists, only duplicate the named directives. Only + looks at first level of depth; does not expand includes. + + :returns: A vhost object for the newly created vhost + :rtype: :class:`~certbot_nginx.obj.VirtualHost` + """ + # TODO: https://github.com/certbot/certbot/issues/5185 + # put it in the same file as the template, at the same level + new_vhost = copy.deepcopy(vhost_template) + + enclosing_block = self.parsed[vhost_template.filep] + for index in vhost_template.path[:-1]: + enclosing_block = enclosing_block[index] + raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]]) + + if only_directives is not None: + new_directives = nginxparser.UnspacedList([]) + for directive in raw_in_parsed[1]: + if len(directive) > 0 and directive[0] in only_directives: + new_directives.append(directive) + raw_in_parsed[1] = new_directives + + self._update_vhost_based_on_new_directives(new_vhost, new_directives) + + enclosing_block.append(raw_in_parsed) + new_vhost.path[-1] = len(enclosing_block) - 1 + if delete_default: + for addr in new_vhost.addrs: + addr.default = False + for directive in enclosing_block[new_vhost.path[-1]][1]: + if (len(directive) > 0 and directive[0] == 'listen' + and 'default_server' in directive): + del directive[directive.index('default_server')] + return new_vhost + def _parse_ssl_options(ssl_options): if ssl_options is not None: try: @@ -444,7 +509,7 @@ def _is_include_directive(entry): """ return (isinstance(entry, list) and len(entry) == 2 and entry[0] == 'include' and - isinstance(entry[1], str)) + isinstance(entry[1], six.string_types)) def _is_ssl_on_directive(entry): """Checks if an nginx parsed entry is an 'ssl on' directive. @@ -458,10 +523,10 @@ def _is_ssl_on_directive(entry): len(entry) == 2 and entry[0] == 'ssl' and entry[1] == 'on') -def _add_directives(block, directives, replace): +def _add_directives(directives, replace, insert_at_top, block): """Adds or replaces directives in a config block. - When replace=False, it's an error to try and add a directive that already + When replace=False, it's an error to try and add a nonrepeatable directive that already exists in the config block with a conflicting value. When replace=True and a directive with the same name already exists in the @@ -470,23 +535,29 @@ def _add_directives(block, directives, replace): ..todo :: Find directives that are in included files. - :param list block: The block to replace in :param list directives: The new directives. + :param bool replace: Described above. + :param bool insert_at_top: Described above. + :param list block: The block to replace in """ for directive in directives: - _add_directive(block, directive, replace) + _add_directive(block, directive, replace, insert_at_top) if block and '\n' not in block[-1]: # could be " \n " or ["\n"] ! block.append(nginxparser.UnspacedList('\n')) INCLUDE = 'include' -REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE]) +REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'location', 'rewrite']) COMMENT = ' managed by Certbot' COMMENT_BLOCK = [' ', '#', COMMENT] -def _comment_directive(block, location): - """Add a comment to the end of the line at location.""" +def comment_directive(block, location): + """Add a ``#managed by Certbot`` comment to the end of the line at location. + + :param list block: The block containing the directive to be commented + :param int location: The location within ``block`` of the directive to be commented + """ next_entry = block[location + 1] if location + 1 < len(block) else None if isinstance(next_entry, list) and next_entry: if len(next_entry) >= 2 and next_entry[-2] == "#" and COMMENT in next_entry[-1]: @@ -523,7 +594,13 @@ def _comment_out_directive(block, location, include_location): block[location] = new_dir[0] # set the now-single-line-comment directive back in place -def _add_directive(block, directive, replace): +def _find_location(block, directive_name, match_func=None): + """Finds the index of the first instance of directive_name in block. + If no line exists, use None.""" + return next((index for index, line in enumerate(block) \ + if line and line[0] == directive_name and (match_func is None or match_func(line))), None) + +def _add_directive(block, directive, replace, insert_at_top): """Adds or replaces a single directive in a config block. See _add_directives for more documentation. @@ -538,21 +615,14 @@ def _add_directive(block, directive, replace): block.append(directive) return - def find_location(direc): - """ Find the index of a config line where the name of the directive matches - the name of the directive we want to add. If no line exists, use None. - """ - return next((index for index, line in enumerate(block) \ - if line and line[0] == direc[0]), None) - - location = find_location(directive) + location = _find_location(block, directive[0]) if replace: if location is not None: block[location] = directive - _comment_directive(block, location) + comment_directive(block, location) return - # Append directive. Fail if the name is not a repeatable directive name, + # Append or prepend directive. Fail if the name is not a repeatable directive name, # and there is already a copy of that directive with a different value # in the config file. @@ -561,7 +631,8 @@ def _add_directive(block, directive, replace): directive_name = directive[0] def can_append(loc, dir_name): """ Can we append this directive to the block? """ - return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES) + return loc is None or (isinstance(dir_name, six.string_types) + and dir_name in REPEATABLE_DIRECTIVES) err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".' @@ -573,7 +644,7 @@ def _add_directive(block, directive, replace): included_directives = _parse_ssl_options(directive[1]) for included_directive in included_directives: - included_dir_loc = find_location(included_directive) + included_dir_loc = _find_location(block, included_directive[0]) included_dir_name = included_directive[0] if not is_whitespace_or_comment(included_directive) \ and not can_append(included_dir_loc, included_dir_name): @@ -584,11 +655,27 @@ def _add_directive(block, directive, replace): _comment_out_directive(block, included_dir_loc, directive[1]) if can_append(location, directive_name): - block.append(directive) - _comment_directive(block, len(block) - 1) + if insert_at_top: + # Add a newline so the comment doesn't comment + # out existing directives + block.insert(0, nginxparser.UnspacedList('\n')) + block.insert(0, directive) + comment_directive(block, 0) + else: + block.append(directive) + comment_directive(block, len(block) - 1) elif block[location] != directive: raise errors.MisconfigurationError(err_fmt.format(directive, block[location])) +def _remove_directives(directive_name, match_func, block): + """Removes directives of name directive_name from a config block if match_func matches. + """ + while True: + location = _find_location(block, directive_name, match_func=match_func) + if location is None: + return + del block[location] + def _apply_global_addr_ssl(addr_to_ssl, parsed_server): """Apply global sslishness information to the parsed server block """ diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 6a917204c..acb7ee282 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -18,13 +18,14 @@ from certbot.tests import util as certbot_test_util from certbot_nginx import constants from certbot_nginx import obj from certbot_nginx import parser +from certbot_nginx.configurator import _redirect_block_for_domain +from certbot_nginx.nginxparser import UnspacedList from certbot_nginx.tests import util class NginxConfiguratorTest(util.NginxTest): """Test a semi complex vhost configuration.""" - _multiprocess_can_split_ = True def setUp(self): super(NginxConfiguratorTest, self).setUp() @@ -46,7 +47,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) - self.assertEqual(8, len(self.config.parser.parsed)) + self.assertEqual(10, len(self.config.parser.parsed)) @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") @@ -90,7 +91,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(names, set( ["155.225.50.69.nephoscale.net", "www.example.org", "another.alias", "migration.com", "summer.com", "geese.com", "sslon.com", - "globalssl.com", "globalsslsetssl.com"])) + "globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com"])) def test_supported_enhancements(self): self.assertEqual(['redirect', 'staple-ocsp'], @@ -101,7 +102,7 @@ class NginxConfiguratorTest(util.NginxTest): errors.PluginError, self.config.enhance, 'myhost', 'unknown_enhancement') def test_get_chall_pref(self): - self.assertEqual([challenges.TLSSNI01], + self.assertEqual([challenges.TLSSNI01, challenges.HTTP01], self.config.get_chall_pref('myhost')) def test_save(self): @@ -132,6 +133,7 @@ class NginxConfiguratorTest(util.NginxTest): server_conf = set(['somename', 'another.alias', 'alias']) example_conf = set(['.example.com', 'example.*']) foo_conf = set(['*.www.foo.com', '*.www.example.com']) + ipv6_conf = set(['ipv6.com']) results = {'localhost': localhost_conf, 'alias': server_conf, @@ -140,7 +142,8 @@ class NginxConfiguratorTest(util.NginxTest): 'www.example.com': example_conf, 'test.www.example.com': foo_conf, 'abc.www.foo.com': foo_conf, - 'www.bar.co.uk': localhost_conf} + 'www.bar.co.uk': localhost_conf, + 'ipv6.com': ipv6_conf} conf_path = {'localhost': "etc_nginx/nginx.conf", 'alias': "etc_nginx/nginx.conf", @@ -149,7 +152,8 @@ class NginxConfiguratorTest(util.NginxTest): 'www.example.com': "etc_nginx/sites-enabled/example.com", 'test.www.example.com': "etc_nginx/foo.conf", 'abc.www.foo.com': "etc_nginx/foo.conf", - 'www.bar.co.uk': "etc_nginx/nginx.conf"} + 'www.bar.co.uk': "etc_nginx/nginx.conf", + 'ipv6.com': "etc_nginx/sites-enabled/ipv6.com"} bad_results = ['www.foo.com', 'example', 't.www.bar.co', '69.255.225.155'] @@ -160,11 +164,24 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(results[name], vhost.names) self.assertEqual(conf_path[name], path) + # IPv6 specific checks + if name == "ipv6.com": + self.assertTrue(vhost.ipv6_enabled()) + # Make sure that we have SSL enabled also for IPv6 addr + self.assertTrue( + any([True for x in vhost.addrs if x.ssl and x.ipv6])) for name in bad_results: self.assertRaises(errors.MisconfigurationError, self.config.choose_vhost, name) + def test_ipv6only(self): + # ipv6_info: (ipv6_active, ipv6only_present) + self.assertEquals((True, False), self.config.ipv6_info("80")) + # Port 443 has ipv6only=on because of ipv6ssl.com vhost + self.assertEquals((True, True), self.config.ipv6_info("443")) + + def test_more_info(self): self.assertTrue('nginx.conf' in self.config.more_info()) @@ -276,9 +293,11 @@ class NginxConfiguratorTest(util.NginxTest): parsed_migration_conf[0]) @mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") + @mock.patch("certbot_nginx.configurator.http_01.NginxHttp01.perform") @mock.patch("certbot_nginx.configurator.NginxConfigurator.restart") @mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config") - def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_perform): + def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_http_perform, + mock_tls_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( @@ -289,7 +308,7 @@ class NginxConfiguratorTest(util.NginxTest): ), domain="localhost", account_key=self.rsa512jwk) achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.TLSSNI01(token=b"m8TdO1qik4JVFtgPPurJmg"), + chall=challenges.HTTP01(token=b"m8TdO1qik4JVFtgPPurJmg"), uri="https://ca.org/chall1_uri", status=messages.Status("pending"), ), domain="example.com", account_key=self.rsa512jwk) @@ -299,10 +318,12 @@ class NginxConfiguratorTest(util.NginxTest): achall2.response(self.rsa512jwk), ] - mock_perform.return_value = expected + mock_tls_perform.return_value = expected[:1] + mock_http_perform.return_value = expected[1:] responses = self.config.perform([achall1, achall2]) - self.assertEqual(mock_perform.call_count, 1) + self.assertEqual(mock_tls_perform.call_count, 1) + self.assertEqual(mock_http_perform.call_count, 1) self.assertEqual(responses, expected) self.config.cleanup([achall1, achall2]) @@ -428,10 +449,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_redirect_enhance(self): # Test that we successfully add a redirect when there is # a listen directive - expected = [ - ['if', '($scheme', '!=', '"https") '], - [['return', '301', 'https://$host$request_uri']] - ] + expected = UnspacedList(_redirect_block_for_domain("www.example.com"))[0] example_conf = self.config.parser.abs_path('sites-enabled/example.com') self.config.enhance("www.example.com", "redirect") @@ -444,67 +462,53 @@ class NginxConfiguratorTest(util.NginxTest): migration_conf = self.config.parser.abs_path('sites-enabled/migration.com') self.config.enhance("migration.com", "redirect") + expected = UnspacedList(_redirect_block_for_domain("migration.com"))[0] + generated_conf = self.config.parser.parsed[migration_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + def test_split_for_redirect(self): + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + self.config.deploy_cert( + "example.org", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.enhance("www.example.com", "redirect") + generated_conf = self.config.parser.parsed[example_conf] + self.assertEqual( + [[['server'], [ + ['server_name', '.example.com'], + ['server_name', 'example.*'], [], + ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'], + ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'], + ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'], + ['include', self.config.mod_ssl_conf], ['#', ' managed by Certbot'], + ['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'], + [], []]], + [['server'], [ + [['if', '($host', '=', 'www.example.com)'], [ + ['return', '301', 'https://$host$request_uri']]], + ['#', ' managed by Certbot'], [], + ['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + ['return', '404'], ['#', ' managed by Certbot'], [], [], []]]], + generated_conf) + @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') - @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') - def test_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list): + def test_certbot_redirect_exists(self, mock_contains_list): # Test that we add no redirect statement if there is already a # redirect in the block that is managed by certbot # Has a certbot redirect - mock_has_redirect.return_value = True mock_contains_list.return_value = True with mock.patch("certbot_nginx.configurator.logger") as mock_logger: self.config.enhance("www.example.com", "redirect") self.assertEqual(mock_logger.info.call_args[0][0], "Traffic on port %s already redirecting to ssl in %s") - @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') - @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') - def test_non_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list): - # Test that we add a redirect as a comment if there is already a - # redirect-class statement in the block that isn't managed by certbot - example_conf = self.config.parser.abs_path('sites-enabled/example.com') - - # Has a non-Certbot redirect, and has no existing comment - mock_contains_list.return_value = False - mock_has_redirect.return_value = True - with mock.patch("certbot_nginx.configurator.logger") as mock_logger: - self.config.enhance("www.example.com", "redirect") - self.assertEqual(mock_logger.info.call_args[0][0], - "The appropriate server block is already redirecting " - "traffic. To enable redirect anyway, uncomment the " - "redirect lines in %s.") - generated_conf = self.config.parser.parsed[example_conf] - expected = [ - ['#', ' Redirect non-https traffic to https'], - ['#', ' if ($scheme != "https") {'], - ['#', ' return 301 https://$host$request_uri;'], - ['#', ' } # managed by Certbot'] - ] - for line in expected: - self.assertTrue(util.contains_at_depth(generated_conf, line, 2)) - - @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') - @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') - @mock.patch('certbot_nginx.configurator.NginxConfigurator._has_certbot_redirect_comment') - @mock.patch('certbot_nginx.configurator.NginxConfigurator._add_redirect_block') - def test_redirect_comment_exists(self, mock_add_redirect_block, - mock_has_cb_redirect_comment, mock_has_redirect, mock_contains_list): - # Test that we add nothing if there is a non-Certbot redirect and a - # preexisting comment - # Has a non-Certbot redirect and a comment - mock_has_redirect.return_value = True - mock_contains_list.return_value = False # self._has_certbot_redirect(vhost): - mock_has_cb_redirect_comment.return_value = True - - # assert _add_redirect_block not called - with mock.patch("certbot_nginx.configurator.logger") as mock_logger: - self.config.enhance("www.example.com", "redirect") - self.assertFalse(mock_add_redirect_block.called) - self.assertTrue(mock_logger.info.called) - def test_redirect_dont_enhance(self): # Test that we don't accidentally add redirect to ssl-only block with mock.patch("certbot_nginx.configurator.logger") as mock_logger: @@ -512,6 +516,19 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(mock_logger.info.call_args[0][0], 'No matching insecure server blocks listening on port %s found.') + def test_double_redirect(self): + # Test that we add one redirect for each domain + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + self.config.enhance("example.com", "redirect") + self.config.enhance("example.org", "redirect") + + expected1 = UnspacedList(_redirect_block_for_domain("example.com"))[0] + expected2 = UnspacedList(_redirect_block_for_domain("example.org"))[0] + + generated_conf = self.config.parser.parsed[example_conf] + self.assertTrue(util.contains_at_depth(generated_conf, expected1, 2)) + self.assertTrue(util.contains_at_depth(generated_conf, expected2, 2)) + def test_staple_ocsp_bad_version(self): self.config.version = (1, 3, 1) self.assertRaises(errors.PluginError, self.config.enhance, @@ -542,6 +559,149 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue(util.contains_at_depth( generated_conf, ['ssl_stapling_verify', 'on'], 2)) + def test_deploy_no_match_default_set(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server + self.config.version = (1, 3, 1) + + self.config.deploy_cert( + "www.nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.save() + + self.config.parser.load() + + parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf]) + + self.assertEqual([[['server'], + [['listen', 'myhost', 'default_server'], + ['listen', 'otherhost', 'default_server'], + ['server_name', 'www.example.org'], + [['location', '/'], + [['root', 'html'], + ['index', 'index.html', 'index.htm']]]]], + [['server'], + [['listen', 'myhost'], + ['listen', 'otherhost'], + ['server_name', 'www.nomatch.com'], + [['location', '/'], + [['root', 'html'], + ['index', 'index.html', 'index.htm']]], + ['listen', '5001', 'ssl'], + ['ssl_certificate', 'example/fullchain.pem'], + ['ssl_certificate_key', 'example/key.pem'], + ['include', self.config.mod_ssl_conf], + ['ssl_dhparam', self.config.ssl_dhparams]]]], + parsed_default_conf) + + self.config.deploy_cert( + "nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.save() + + self.config.parser.load() + + parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf]) + + self.assertTrue(util.contains_at_depth(parsed_default_conf, "nomatch.com", 3)) + + def test_deploy_no_match_default_set_multi_level_path(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[default_conf][0][1][0] + del self.config.parser.parsed[default_conf][0][1][0] + self.config.version = (1, 3, 1) + + self.config.deploy_cert( + "www.nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.save() + + self.config.parser.load() + + parsed_foo_conf = util.filter_comments(self.config.parser.parsed[foo_conf]) + + self.assertEqual([['server'], + [['listen', '*:80', 'ssl'], + ['server_name', 'www.nomatch.com'], + ['root', '/home/ubuntu/sites/foo/'], + [['location', '/status'], [[['types'], [['image/jpeg', 'jpg']]]]], + [['location', '~', 'case_sensitive\\.php$'], [['index', 'index.php'], + ['root', '/var/root']]], + [['location', '~*', 'case_insensitive\\.php$'], []], + [['location', '=', 'exact_match\\.php$'], []], + [['location', '^~', 'ignore_regex\\.php$'], []], + ['ssl_certificate', 'example/fullchain.pem'], + ['ssl_certificate_key', 'example/key.pem']]], + parsed_foo_conf[1][1][1]) + + def test_deploy_no_match_no_default_set(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[default_conf][0][1][0] + del self.config.parser.parsed[default_conf][0][1][0] + del self.config.parser.parsed[foo_conf][2][1][0][1][0] + self.config.version = (1, 3, 1) + + self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert, + "www.nomatch.com", "example/cert.pem", "example/key.pem", + "example/chain.pem", "example/fullchain.pem") + + def test_deploy_no_match_fail_multiple_defaults(self): + self.config.version = (1, 3, 1) + self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert, + "www.nomatch.com", "example/cert.pem", "example/key.pem", + "example/chain.pem", "example/fullchain.pem") + + def test_deploy_no_match_add_redirect(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server + self.config.version = (1, 3, 1) + + self.config.deploy_cert( + "www.nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + + self.config.deploy_cert( + "nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + + self.config.enhance("www.nomatch.com", "redirect") + + self.config.save() + + self.config.parser.load() + + expected = UnspacedList(_redirect_block_for_domain("www.nomatch.com"))[0] + + generated_conf = self.config.parser.parsed[default_conf] + self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + + @mock.patch('certbot.reverter.logger') + @mock.patch('certbot_nginx.parser.NginxParser.load') + def test_parser_reload_after_config_changes(self, mock_parser_load, unused_mock_logger): + self.config.recovery_routine() + self.config.revert_challenge_config() + self.config.rollback_checkpoints() + self.assertTrue(mock_parser_load.call_count == 3) + class InstallSslOptionsConfTest(util.NginxTest): """Test that the options-ssl-nginx.conf file is installed and updated properly.""" diff --git a/certbot-nginx/certbot_nginx/tests/http_01_test.py b/certbot-nginx/certbot_nginx/tests/http_01_test.py new file mode 100644 index 000000000..0f764e92e --- /dev/null +++ b/certbot-nginx/certbot_nginx/tests/http_01_test.py @@ -0,0 +1,113 @@ +"""Tests for certbot_nginx.http_01""" +import unittest +import shutil + +import mock +import six + +from acme import challenges + +from certbot import achallenges + +from certbot.plugins import common_test +from certbot.tests import acme_util + +from certbot_nginx.tests import util + + +class HttpPerformTest(util.NginxTest): + """Test the NginxHttp01 challenge.""" + + account_key = common_test.AUTH_KEY + achalls = [ + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=b"kNdwjwOeX0I_A8DXt9Msmg"), "pending"), + domain="www.example.com", account_key=account_key), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01( + token=b"\xba\xa9\xda? CURRENT_VERSION): - logger.warning( + logger.info( "Attempting to parse the version %s renewal configuration " "file found at %s with version %s of Certbot. This might not " "work.", conf_version, config_filename, certbot.__version__) diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index 7245ad6a1..8ebda56af 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -6,10 +6,10 @@ import shutil import stat import unittest +import josepy as jose import mock import pytz -from acme import jose from acme import messages from certbot import errors diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py index f0549666a..53a2f214a 100644 --- a/certbot/tests/acme_util.py +++ b/certbot/tests/acme_util.py @@ -1,10 +1,10 @@ """ACME utilities for testing.""" import datetime +import josepy as jose import six from acme import challenges -from acme import jose from acme import messages from certbot import auth_handler diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index ba5e2775f..c5935d722 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -26,7 +26,6 @@ PLUGINS = disco.PluginsRegistry.find_all() class TestReadFile(TempDirTestCase): '''Test cli.read_file''' - _multiprocess_can_split_ = True def test_read_file(self): rel_test_path = os.path.relpath(os.path.join(self.tempdir, 'foo')) @@ -46,7 +45,6 @@ class TestReadFile(TempDirTestCase): class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods '''Test the cli args entrypoint''' - _multiprocess_can_split_ = True def setUp(self): reload_module(cli) @@ -166,6 +164,8 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue("--cert-path" in out) self.assertTrue("--key-path" in out) self.assertTrue("--reason" in out) + self.assertTrue("--delete-after-revoke" in out) + self.assertTrue("--no-delete-after-revoke" in out) out = self._help_output(['-h', 'config_changes']) self.assertTrue("--cert-path" not in out) @@ -414,11 +414,22 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods def test_no_directory_hooks_unset(self): self.assertTrue(self.parse([]).directory_hooks) + def test_delete_after_revoke(self): + namespace = self.parse(["--delete-after-revoke"]) + self.assertTrue(namespace.delete_after_revoke) + + def test_delete_after_revoke_default(self): + namespace = self.parse([]) + self.assertEqual(namespace.delete_after_revoke, None) + + def test_no_delete_after_revoke(self): + namespace = self.parse(["--no-delete-after-revoke"]) + self.assertFalse(namespace.delete_after_revoke) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=protected-access @@ -439,7 +450,6 @@ class DefaultTest(unittest.TestCase): class SetByCliTest(unittest.TestCase): """Tests for certbot.set_by_cli and related functions.""" - _multiprocess_can_split_ = True def setUp(self): reload_module(cli) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 09c4a50ca..204f46323 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -4,11 +4,11 @@ import shutil import tempfile import unittest +import josepy as jose import OpenSSL import mock from acme import errors as acme_errors -from acme import jose from certbot import account from certbot import errors diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index cb0fb32e3..57d82f839 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -4,10 +4,10 @@ import os import sys import unittest +import josepy as jose import mock import zope.component -from acme import jose from acme import messages from certbot import account diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 45e5db1df..e84de54d0 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -11,11 +11,10 @@ import unittest import datetime import pytz +import josepy as jose import six from six.moves import reload_module # pylint: disable=import-error -from acme import jose - from certbot import account from certbot import cli from certbot import constants @@ -299,25 +298,29 @@ class RevokeTest(test_util.TempDirTestCase): self._call() self.assertFalse(mock_delete.called) -class DeleteIfAppropriateTest(unittest.TestCase): +class DeleteIfAppropriateTest(test_util.ConfigTestCase): """Tests for certbot.main._delete_if_appropriate """ - def setUp(self): - self.config = mock.Mock() - self.config.namespace = mock.Mock() - self.config.namespace.noninteractive_mode = False - def _call(self, mock_config): from certbot.main import _delete_if_appropriate _delete_if_appropriate(mock_config) - @mock.patch('certbot.cert_manager.delete') + def _test_delete_opt_out_common(self, mock_get_utility): + with mock.patch('certbot.cert_manager.delete') as mock_delete: + self._call(self.config) + mock_delete.assert_not_called() + self.assertTrue(mock_get_utility().add_message.called) + @test_util.patch_get_utility() - def test_delete_opt_out(self, mock_get_utility, mock_delete): + def test_delete_flag_opt_out(self, mock_get_utility): + self.config.delete_after_revoke = False + self._test_delete_opt_out_common(mock_get_utility) + + @test_util.patch_get_utility() + def test_delete_prompt_opt_out(self, mock_get_utility): util_mock = mock_get_utility() util_mock.yesno.return_value = False - self._call(self.config) - mock_delete.assert_not_called() + self._test_delete_opt_out_common(mock_get_utility) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -356,7 +359,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_cert_path_for_cert_name.return_value = "/some/reasonable/path" mock_overlapping_archive_dirs.return_value = False self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -375,7 +378,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_cert_path_to_lineage.return_value = "example.com" mock_overlapping_archive_dirs.return_value = False self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -396,7 +399,29 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_full_archive_dir.return_value = "" mock_match_and_check_overlaps.return_value = "" self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) + + # pylint: disable=too-many-arguments + @mock.patch('certbot.storage.renewal_file_for_certname') + @mock.patch('certbot.cert_manager.match_and_check_overlaps') + @mock.patch('certbot.storage.full_archive_path') + @mock.patch('certbot.cert_manager.cert_path_to_lineage') + @mock.patch('certbot.cert_manager.delete') + @test_util.patch_get_utility() + def test_opt_in_deletion(self, mock_get_utility, mock_delete, + mock_cert_path_to_lineage, mock_full_archive_dir, + mock_match_and_check_overlaps, mock_renewal_file_for_certname): + # pylint: disable = unused-argument + config = self.config + config.namespace.delete_after_revoke = True + config.cert_path = "/some/reasonable/path" + config.certname = "" + mock_cert_path_to_lineage.return_value = "example.com" + mock_full_archive_dir.return_value = "" + mock_match_and_check_overlaps.return_value = "" + self._call(config) + self.assertEqual(mock_delete.call_count, 1) + self.assertFalse(mock_get_utility().yesno.called) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -415,7 +440,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_cert_path_to_lineage.return_value = config.certname mock_overlapping_archive_dirs.return_value = False self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.cert_manager.match_and_check_overlaps') @@ -442,7 +467,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): util_mock = mock_get_utility() util_mock.menu.return_value = (display_util.OK, 0) self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.cert_manager.match_and_check_overlaps') @@ -567,11 +592,30 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met super(MainTest, self).tearDown() - def _call(self, args, stdout=None): - "Run the cli with output streams and actual client mocked out" - with mock.patch('certbot.main.client') as client: - ret, stdout, stderr = self._call_no_clientmock(args, stdout) - return ret, stdout, stderr, client + def _call(self, args, stdout=None, mockisfile=False): + """Run the cli with output streams, actual client and optionally + os.path.isfile() mocked out""" + + if mockisfile: + orig_open = os.path.isfile + def mock_isfile(fn, *args, **kwargs): + """Mock os.path.isfile()""" + if (fn.endswith("cert") or + fn.endswith("chain") or + fn.endswith("privkey")): + return True + else: + return orig_open(fn, *args, **kwargs) + + with mock.patch("os.path.isfile") as mock_if: + mock_if.side_effect = mock_isfile + with mock.patch('certbot.main.client') as client: + ret, stdout, stderr = self._call_no_clientmock(args, stdout) + return ret, stdout, stderr, client + else: + with mock.patch('certbot.main.client') as client: + ret, stdout, stderr = self._call_no_clientmock(args, stdout) + return ret, stdout, stderr, client def _call_no_clientmock(self, args, stdout=None): "Run the client with output streams mocked out" @@ -655,9 +699,68 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met @mock.patch('certbot.main.plug_sel.pick_installer') def test_installer_selection(self, mock_pick_installer, _rec): self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', - '--key-path', 'key', '--chain-path', 'chain']) + '--key-path', 'privkey', '--chain-path', 'chain'], mockisfile=True) self.assertEqual(mock_pick_installer.call_count, 1) + @mock.patch('certbot.main._install_cert') + @mock.patch('certbot.main.plug_sel.record_chosen_plugins') + @mock.patch('certbot.main.plug_sel.pick_installer') + def test_installer_certname(self, _inst, _rec, mock_install): + mock_lineage = mock.MagicMock(cert_path="/tmp/cert", chain_path="/tmp/chain", + fullchain_path="/tmp/chain", + key_path="/tmp/privkey") + + with mock.patch("certbot.cert_manager.lineage_for_certname") as mock_getlin: + mock_getlin.return_value = mock_lineage + self._call(['install', '--cert-name', 'whatever'], mockisfile=True) + call_config = mock_install.call_args[0][0] + self.assertEqual(call_config.cert_path, "/tmp/cert") + self.assertEqual(call_config.fullchain_path, "/tmp/chain") + self.assertEqual(call_config.key_path, "/tmp/privkey") + + @mock.patch('certbot.main._install_cert') + @mock.patch('certbot.main.plug_sel.record_chosen_plugins') + @mock.patch('certbot.main.plug_sel.pick_installer') + def test_installer_param_override(self, _inst, _rec, mock_install): + mock_lineage = mock.MagicMock(cert_path="/tmp/cert", chain_path="/tmp/chain", + fullchain_path="/tmp/chain", + key_path="/tmp/privkey") + with mock.patch("certbot.cert_manager.lineage_for_certname") as mock_getlin: + mock_getlin.return_value = mock_lineage + self._call(['install', '--cert-name', 'whatever', + '--key-path', '/tmp/overriding_privkey'], mockisfile=True) + call_config = mock_install.call_args[0][0] + self.assertEqual(call_config.cert_path, "/tmp/cert") + self.assertEqual(call_config.fullchain_path, "/tmp/chain") + self.assertEqual(call_config.chain_path, "/tmp/chain") + self.assertEqual(call_config.key_path, "/tmp/overriding_privkey") + + mock_install.reset() + + self._call(['install', '--cert-name', 'whatever', + '--cert-path', '/tmp/overriding_cert'], mockisfile=True) + call_config = mock_install.call_args[0][0] + self.assertEqual(call_config.cert_path, "/tmp/overriding_cert") + self.assertEqual(call_config.fullchain_path, "/tmp/chain") + self.assertEqual(call_config.key_path, "/tmp/privkey") + + @mock.patch('certbot.main.plug_sel.record_chosen_plugins') + @mock.patch('certbot.main.plug_sel.pick_installer') + def test_installer_param_error(self, _inst, _rec): + self.assertRaises(errors.ConfigurationError, + self._call, + ['install', '--key-path', '/tmp/key_path']) + self.assertRaises(errors.ConfigurationError, + self._call, + ['install', '--cert-path', '/tmp/key_path']) + self.assertRaises(errors.ConfigurationError, + self._call, + ['install']) + self.assertRaises(errors.ConfigurationError, + self._call, + ['install', '--cert-name', 'notfound', + '--key-path', 'invalid']) + @mock.patch('certbot.main._report_new_cert') @mock.patch('certbot.util.exe_exists') def test_configurator_selection(self, mock_exe_exists, unused_report): @@ -926,7 +1029,7 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, args=None, should_renew=True, error_expected=False, - quiet_mode=False): + quiet_mode=False, expiry_date=datetime.datetime.now()): # pylint: disable=too-many-locals,too-many-arguments cert_path = test_util.vector_path('cert_512.pem') chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' @@ -959,7 +1062,8 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met mock_latest = mock.MagicMock() mock_latest.get_issuer.return_value = "Fake fake" mock_ssl.crypto.load_certificate.return_value = mock_latest - with mock.patch('certbot.main.renewal.crypto_util'): + with mock.patch('certbot.main.renewal.crypto_util') as mock_crypto_util: + mock_crypto_util.notAfter.return_value = expiry_date if not args: args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly'] if extra_args: @@ -1027,6 +1131,16 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) + @mock.patch('certbot.renewal.should_renew') + def test_renew_skips_recent_certs(self, should_renew): + should_renew.return_value = False + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') + expiry = datetime.datetime.now() + datetime.timedelta(days=90) + _, _, stdout = self._test_renewal_common(False, extra_args=None, should_renew=False, + args=['renew'], expiry_date=expiry) + self.assertTrue('No renewals were attempted.' in stdout.getvalue()) + self.assertTrue('The following certs are not due for renewal yet:' in stdout.getvalue()) + def test_quiet_renew(self): test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') args = ["renew", "--dry-run"] diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index 91dd6f8d6..2d54274f0 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -13,7 +13,6 @@ ocsp: Use -help for summary. class OCSPTest(unittest.TestCase): - _multiprocess_can_split_ = True def setUp(self): from certbot import ocsp diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index df6391758..6c0970e72 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -43,7 +43,6 @@ class BaseRenewableCertTest(test_util.ConfigTestCase): your test. Check :class:`.cli_test.DuplicateCertTest` for an example. """ - _multiprocess_can_split_ = True def setUp(self): from certbot import storage @@ -159,8 +158,8 @@ class RenewableCertTests(BaseRenewableCertTest): with mock.patch("certbot.storage.logger") as mock_logger: storage.RenewableCert(self.config_file.filename, self.config) - self.assertTrue(mock_logger.warning.called) - self.assertTrue("version" in mock_logger.warning.call_args[0][0]) + self.assertTrue(mock_logger.info.called) + self.assertTrue("version" in mock_logger.info.call_args[0][0]) def test_consistent(self): # pylint: disable=too-many-statements,protected-access diff --git a/certbot/tests/util.py b/certbot/tests/util.py index c43b44522..ddd4a1aec 100644 --- a/certbot/tests/util.py +++ b/certbot/tests/util.py @@ -14,11 +14,10 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import mock import OpenSSL +import josepy as jose import six from six.moves import reload_module # pylint: disable=import-error -from acme import jose - from certbot import constants from certbot import interfaces from certbot import storage diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 7e320012a..50d323ffd 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -258,7 +258,7 @@ class UniqueLineageNameTest(test_util.TempDirTestCase): for _ in six.moves.range(10): f, name = self._call("wow") self.assertTrue(isinstance(f, file_type)) - self.assertTrue(isinstance(name, str)) + self.assertTrue(isinstance(name, six.string_types)) self.assertTrue("wow-0009.conf" in name) @mock.patch("certbot.util.os.fdopen") diff --git a/certbot/util.py b/certbot/util.py index 30de0c157..b7e60a225 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -342,9 +342,9 @@ def get_os_info_ua(filepath="/etc/os-release"): """ if os.path.isfile(filepath): - os_ua = _get_systemd_os_release_var("PRETTY_NAME", filepath=filepath) + os_ua = get_var_from_file("PRETTY_NAME", filepath=filepath) if not os_ua: - os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + os_ua = get_var_from_file("NAME", filepath=filepath) if os_ua: return os_ua @@ -361,8 +361,8 @@ def get_systemd_os_info(filepath="/etc/os-release"): :rtype: `tuple` of `str` """ - os_name = _get_systemd_os_release_var("ID", filepath=filepath) - os_version = _get_systemd_os_release_var("VERSION_ID", filepath=filepath) + os_name = get_var_from_file("ID", filepath=filepath) + os_version = get_var_from_file("VERSION_ID", filepath=filepath) return (os_name, os_version) @@ -377,10 +377,10 @@ def get_systemd_os_like(filepath="/etc/os-release"): :rtype: `list` of `str` """ - return _get_systemd_os_release_var("ID_LIKE", filepath).split(" ") + return get_var_from_file("ID_LIKE", filepath).split(" ") -def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): +def get_var_from_file(varname, filepath="/etc/os-release"): """ Get single value from systemd /etc/os-release @@ -405,7 +405,7 @@ def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): def _normalize_string(orig): """ - Helper function for _get_systemd_os_release_var() to remove quotes + Helper function for get_var_from_file() to remove quotes and whitespaces """ return orig.replace('"', '').replace("'", "").strip() diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html new file mode 100644 index 000000000..8fd0f127d --- /dev/null +++ b/docs/_templates/footer.html @@ -0,0 +1,52 @@ + diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 6b43fd0a2..abebdb9c9 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -107,9 +107,9 @@ optional arguments: case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/0.19.0 (certbot; - Ubuntu 16.04.3 LTS) Authenticator/XXX Installer/YYY - (SUBCOMMAND; flags: FLAGS) Py/2.7.12). The flags + "". (default: CertbotACMEClient/0.21.1 (certbot; + darwin 10.13.3) Authenticator/XXX Installer/YYY + (SUBCOMMAND; flags: FLAGS) Py/2.7.14). The flags encoded in the user agent are: --duplicate, --force- renew, --allow-subset-of-names, -n, and whether any hooks are set. @@ -121,7 +121,7 @@ optional arguments: (Example: Foo-Wrapper/1.0) (default: None) automation: - Arguments for automating execution & other tweaks + Flags for automating execution & other tweaks --keep-until-expiring, --keep, --reinstall If the requested certificate matches an existing @@ -228,7 +228,7 @@ testing: False) paths: - Arguments changing execution paths & servers + Flags for changing execution paths & servers --cert-path CERT_PATH Path to where certificate is saved (with auth --csr), @@ -331,6 +331,14 @@ revoke: --reason {unspecified,keycompromise,affiliationchanged,superseded,cessationofoperation} Specify reason for revoking certificate. (default: unspecified) + --delete-after-revoke + Delete certificates after revoking them. (default: + None) + --no-delete-after-revoke + Do not delete certificates after revoking them. This + option should be used with caution because the 'renew' + subcommand will attempt to renew undeleted revoked + certificates. (default: None) register: Options for account registration & modification @@ -440,11 +448,9 @@ apache: Apache Web Server plugin - Beta --apache-enmod APACHE_ENMOD - Path to the Apache 'a2enmod' binary. (default: - a2enmod) + Path to the Apache 'a2enmod' binary. (default: None) --apache-dismod APACHE_DISMOD - Path to the Apache 'a2dismod' binary. (default: - a2dismod) + Path to the Apache 'a2dismod' binary. (default: None) --apache-le-vhost-ext APACHE_LE_VHOST_EXT SSL vhost configuration extension. (default: -le- ssl.conf) @@ -458,13 +464,13 @@ apache: /var/log/apache2) --apache-challenge-location APACHE_CHALLENGE_LOCATION Directory path for challenge configuration. (default: - /etc/apache2) + /etc/apache2/other) --apache-handle-modules APACHE_HANDLE_MODULES Let installer handle enabling required modules for - you.(Only Ubuntu/Debian currently) (default: True) + you.(Only Ubuntu/Debian currently) (default: False) --apache-handle-sites APACHE_HANDLE_SITES Let installer handle enabling sites for you.(Only - Ubuntu/Debian currently) (default: True) + Ubuntu/Debian currently) (default: False) certbot-route53:auth: Obtain certificates using a DNS TXT record (if you are using AWS Route53 diff --git a/docs/conf.py b/docs/conf.py index 73df47dbd..09bb44285 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,7 +64,8 @@ master_doc = 'index' # General information about the project. project = u'Certbot' -copyright = u'2014-2016 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license ' +# this is now overridden by the footer.html template +#copyright = u'2014-2018 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/contributing.rst b/docs/contributing.rst index c144a4f74..83b607e15 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -32,10 +32,13 @@ a new plugin is introduced. .. code-block:: shell cd certbot - ./certbot-auto --os-packages-only + sudo ./certbot-auto --os-packages-only ./tools/venv.sh -Then in each shell where you're working on the client, do: +You can now run the copy of Certbot from git either by executing +``venv/bin/certbot``, or by activating the virtual environment. If you're +actively modifying and testing the code, you may want to run commands like this in +each shell where you're working: .. code-block:: shell @@ -43,7 +46,8 @@ Then in each shell where you're working on the client, do: export SERVER=https://acme-staging.api.letsencrypt.org/directory source tests/integration/_common.sh -After that, your shell will be using the virtual environment, and you run the +After that, your shell will be using the virtual environment, your copy of +Certbot will default to requesting test (staging) certificates, and you run the client by typing `certbot` or `certbot_test`. The latter is an alias that includes several flags useful for testing. For instance, it sets various output directories to point to /tmp/, and uses non-privileged ports for challenges, so diff --git a/docs/install.rst b/docs/install.rst index 1f6a45e07..c18c3cdbc 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -116,7 +116,7 @@ certbot-auto_ method, which enables you to use installer plugins that cover both of those hard topics. If you're still not convinced and have decided to use this method, -from the server that the domain you're requesting a cert for resolves +from the server that the domain you're requesting a certficate for resolves to, `install Docker`_, then issue the following command: .. code-block:: shell @@ -156,7 +156,7 @@ If you run Debian Stretch or Debian Sid, you can install certbot packages. sudo apt-get install certbot python-certbot-apache If you don't want to use the Apache plugin, you can omit the -``python-certbot-apache`` package. +``python-certbot-apache`` package. Or you can install ``python-certbot-nginx`` instead. Packages exist for Debian Jessie via backports. First you'll have to follow the instructions at http://backports.debian.org/Instructions/ to enable the Jessie backports diff --git a/docs/using.rst b/docs/using.rst index 4fd0b5ec8..e8f84e2d7 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -106,7 +106,7 @@ specified ``--webroot-path``. So, for instance, :: - certbot certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net + certbot certonly --webroot -w /var/www/example -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and @@ -557,8 +557,8 @@ apologize for any inconvenience you encounter in integrating these commands into your individual environment. .. note:: ``certbot renew`` exit status will only be 1 if a renewal attempt failed. - This means ``certbot renew`` exit status will be 0 if no cert needs to be updated. - If you write a custom script and expect to run a command only after a cert was actually renewed + This means ``certbot renew`` exit status will be 0 if no certificate needs to be updated. + If you write a custom script and expect to run a command only after a certificate was actually renewed you will need to use the ``--post-hook`` since the exit status will be 0 both on successful renewal and when renewal is not necessary. diff --git a/letsencrypt-auto b/letsencrypt-auto index 25f2ce889..d3a5c23e5 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.19.0" +LE_AUTO_VERSION="0.21.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -68,10 +68,12 @@ for arg in "$@" ; do NO_BOOTSTRAP=1;; --help) HELP=1;; - --noninteractive|--non-interactive|renew) - ASSUME_YES=1;; + --noninteractive|--non-interactive) + NONINTERACTIVE=1;; --quiet) QUIET=1;; + renew) + ASSUME_YES=1;; --verbose) VERBOSE=1;; -[!-]*) @@ -93,7 +95,7 @@ done if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 + NONINTERACTIVE=1 HELP=0 fi @@ -244,23 +246,42 @@ DeprecationBootstrap() { fi } - +MIN_PYTHON_VERSION="2.6" +MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') +# Sets LE_PYTHON to Python version string and PYVER to the first two +# digits of the python version DeterminePythonVersion() { - for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do - # Break (while keeping the LE_PYTHON value) if found. - $EXISTS "$LE_PYTHON" > /dev/null && break - done - if [ "$?" != "0" ]; then - error "Cannot find any Pythons; please install one!" - exit 1 + # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + # + # If no Python is found, PYVER is set to 0. + if [ "$USE_PYTHON_3" = 1 ]; then + for LE_PYTHON in "$LE_PYTHON" python3; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + else + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + fi + if [ "$?" != "0" ]; then + if [ "$1" != "NOCRASH" ]; then + error "Cannot find any Pythons; please install one!" + exit 1 + else + PYVER=0 + return 0 + fi fi - export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ "$PYVER" -lt 26 ]; then - error "You have an ancient version of Python entombed in your operating system..." - error "This isn't going to work; you'll need at least version 2.6." - exit 1 + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + if [ "$1" != "NOCRASH" ]; then + error "You have an ancient version of Python entombed in your operating system..." + error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION." + exit 1 + fi fi } @@ -384,23 +405,19 @@ BootstrapDebCommon() { fi } -# If new packages are installed by BootstrapRpmCommon below, this version -# number must be increased. -BOOTSTRAP_RPM_COMMON_VERSION=1 - -BootstrapRpmCommon() { - # Tested with: - # - Fedora 20, 21, 22, 23 (x64) - # - Centos 7 (x64: on DigitalOcean droplet) - # - CentOS 7 Minimal install in a Hyper-V VM - # - CentOS 6 (EPEL must be installed manually) +# If new packages are installed by BootstrapRpmCommonBase below, version +# numbers in rpm_common.sh and rpm_python3.sh must be increased. +# Sets TOOL to the name of the package manager +# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG. +# Enables EPEL if applicable and possible. +InitializeRPMCommonBase() { if type dnf 2>/dev/null then - tool=dnf + TOOL=dnf elif type yum 2>/dev/null then - tool=yum + TOOL=yum else error "Neither yum nor dnf found. Aborting bootstrap!" @@ -408,15 +425,15 @@ BootstrapRpmCommon() { fi if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" + YES_FLAG="-y" fi if [ "$QUIET" = 1 ]; then QUIET_FLAG='--quiet' fi - if ! $tool list *virtualenv >/dev/null 2>&1; then + if ! $TOOL list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $tool list epel-release >/dev/null 2>&1; then + if ! $TOOL list epel-release >/dev/null 2>&1; then error "Enable the EPEL repository and try running Certbot again." exit 1 fi @@ -425,14 +442,20 @@ BootstrapRpmCommon() { sleep 1s /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 second..." sleep 1s fi - if ! $tool install $yes_flag $QUIET_FLAG epel-release; then + if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then error "Could not enable EPEL. Aborting bootstrap!" exit 1 fi fi +} + +BootstrapRpmCommonBase() { + # Arguments: whitespace-delimited python packages to install + + InitializeRPMCommonBase # This call is superfluous in practice pkgs=" gcc @@ -444,10 +467,39 @@ BootstrapRpmCommon() { ca-certificates " - # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $tool list python >/dev/null 2>&1; then + # Add the python packages + pkgs="$pkgs + $1 + " + + if $TOOL list installed "httpd" >/dev/null 2>&1; then pkgs="$pkgs - python + mod_ssl + " + fi + + if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then + error "Could not install OS dependencies. Aborting bootstrap!" + exit 1 + fi +} + +# If new packages are installed by BootstrapRpmCommon below, this version +# number must be increased. +BOOTSTRAP_RPM_COMMON_VERSION=1 + +BootstrapRpmCommon() { + # Tested with: + # - Fedora 20, 21, 22, 23 (x64) + # - Centos 7 (x64: on DigitalOcean droplet) + # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 + + InitializeRPMCommonBase + + # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. + if $TOOL list python >/dev/null 2>&1; then + python_pkgs="$python python-devel python-virtualenv python-tools @@ -455,9 +507,8 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $tool list python2 >/dev/null 2>&1; then - pkgs="$pkgs - python2 + elif $TOOL list python2 >/dev/null 2>&1; then + python_pkgs="$python2 python2-libs python2-setuptools python2-devel @@ -468,8 +519,7 @@ BootstrapRpmCommon() { # Some distros and older versions of current distros use a "python27" # instead of the "python" or "python-" naming convention. else - pkgs="$pkgs - python27 + python_pkgs="$python27 python27-devel python27-virtualenv python27-tools @@ -477,16 +527,31 @@ BootstrapRpmCommon() { " fi - if $tool list installed "httpd" >/dev/null 2>&1; then - pkgs="$pkgs - mod_ssl - " - fi + BootstrapRpmCommonBase "$python_pkgs" +} - if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then - error "Could not install OS dependencies. Aborting bootstrap!" +# If new packages are installed by BootstrapRpmPython3 below, this version +# number must be increased. +BOOTSTRAP_RPM_PYTHON3_VERSION=1 + +BootstrapRpmPython3() { + # Tested with: + # - CentOS 6 + + InitializeRPMCommonBase + + # EPEL uses python34 + if $TOOL list python34 >/dev/null 2>&1; then + python_pkgs="python34 + python34-devel + python34-tools + " + else + error "No supported Python package available to install. Aborting bootstrap!" exit 1 fi + + BootstrapRpmCommonBase "$python_pkgs" } # If new packages are installed by BootstrapSuseCommon below, this version @@ -696,13 +761,8 @@ BootstrapMageiaCommon() { # Set Bootstrap to the function that installs OS dependencies on this system # and BOOTSTRAP_VERSION to the unique identifier for the current version of # that function. If Bootstrap is set to a function that doesn't install any -# packages (either because --no-bootstrap was included on the command line or -# we don't know how to bootstrap on this system), BOOTSTRAP_VERSION is not set. -if [ "$NO_BOOTSTRAP" = 1 ]; then - Bootstrap() { - : - } -elif [ -f /etc/debian_version ]; then +# packages BOOTSTRAP_VERSION is not set. +if [ -f /etc/debian_version ]; then Bootstrap() { BootstrapMessage "Debian-based OSes" BootstrapDebCommon @@ -715,11 +775,27 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + # Run DeterminePythonVersion to decide on the basis of available Python versions + # whether to use 2.x or 3.x on RedHat-like systems. + # Then, revert LE_PYTHON to its previous state. + prev_le_python="$LE_PYTHON" + unset LE_PYTHON + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -eq 26 ]; then + Bootstrap() { + BootstrapMessage "RedHat-based OSes that will use Python3" + BootstrapRpmPython3 + } + USE_PYTHON_3=1 + BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" + else + Bootstrap() { + BootstrapMessage "RedHat-based OSes" + BootstrapRpmCommon + } + BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + fi + LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -782,6 +858,17 @@ else } fi +# We handle this case after determining the normal bootstrap version to allow +# variables like USE_PYTHON_3 to be properly set. As described above, if the +# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not +# be set so we unset it here. +if [ "$NO_BOOTSTRAP" = 1 ]; then + Bootstrap() { + : + } + unset BOOTSTRAP_VERSION +fi + # Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used # to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set # if it is unknown how OS dependencies were installed on this system. @@ -816,7 +903,11 @@ TempDir() { mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS } - +# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise, +# returns a non-zero number. +OldVenvExists() { + [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] +} if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -824,14 +915,26 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg SetPrevBootstrapVersion + if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then + unset LE_PYTHON + fi + INSTALLED_VERSION="none" - if [ -d "$VENV_PATH" ]; then + if [ -d "$VENV_PATH" ] || OldVenvExists; then # If the selected Bootstrap function isn't a noop and it differs from the # previously used version if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then # if non-interactive mode or stdin and stdout are connected to a terminal if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then - rm -rf "$VENV_PATH" + if [ -d "$VENV_PATH" ]; then + rm -rf "$VENV_PATH" + fi + # In the case the old venv was just a symlink to the new one, + # OldVenvExists is now false because we deleted the venv at VENV_PATH. + if OldVenvExists; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi RerunWithArgs "$@" else error "Skipping upgrade because new OS dependencies may need to be installed." @@ -841,6 +944,10 @@ if [ "$1" = "--le-auto-phase2" ]; then error "install any required packages." # Set INSTALLED_VERSION to be the same so we don't update the venv INSTALLED_VERSION="$LE_AUTO_VERSION" + # Continue to use OLD_VENV_PATH if the new venv doesn't exist + if [ ! -d "$VENV_PATH" ]; then + VENV_BIN="$OLD_VENV_PATH/bin" + fi fi elif [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings @@ -858,10 +965,18 @@ if [ "$1" = "--le-auto-phase2" ]; then say "Creating virtual environment..." DeterminePythonVersion rm -rf "$VENV_PATH" - if [ "$VERBOSE" = 1 ]; then - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + if [ "$PYVER" -le 27 ]; then + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi else - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + if [ "$VERBOSE" = 1 ]; then + "$LE_PYTHON" -m venv "$VENV_PATH" + else + "$LE_PYTHON" -m venv "$VENV_PATH" > /dev/null + fi fi if [ -n "$BOOTSTRAP_VERSION" ]; then @@ -983,9 +1098,16 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +josepy==1.0.1 \ + --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ + --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f packaging==16.8 \ @@ -1062,9 +1184,6 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba # Contains the requirements for the letsencrypt package. # @@ -1077,18 +1196,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.19.0 \ - --hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \ - --hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53 -acme==0.19.0 \ - --hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \ - --hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822 -certbot-apache==0.19.0 \ - --hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \ - --hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619 -certbot-nginx==0.19.0 \ - --hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \ - --hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1 +certbot==0.21.1 \ + --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ + --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea +acme==0.21.1 \ + --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ + --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b +certbot-apache==0.21.1 \ + --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ + --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 +certbot-nginx==0.21.1 \ + --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ + --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -1318,9 +1437,10 @@ else # upgrading. Phase 1 checks the version of the latest release of # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. + export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then - if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if ! OldVenvExists; then if [ "$HELP" = 1 ]; then echo "$USAGE" exit 0 @@ -1352,17 +1472,22 @@ On failure, return non-zero. """ -from __future__ import print_function +from __future__ import print_function, unicode_literals from distutils.version import LooseVersion from json import loads from os import devnull, environ from os.path import dirname, join import re +import ssl from subprocess import check_call, CalledProcessError from sys import argv, exit -from urllib2 import build_opener, HTTPHandler, HTTPSHandler -from urllib2 import HTTPError, URLError +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler + from urllib2 import HTTPError, URLError +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler + from urllib.error import HTTPError, URLError PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq @@ -1384,8 +1509,11 @@ class HttpsGetter(object): def __init__(self): """Build an HTTPS opener.""" # Based on pip 1.4.1's URLOpener - # This verifies certs on only Python >=2.7.9. - self._opener = build_opener(HTTPSHandler()) + # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. + if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): + self._opener = build_opener(HTTPSHandler(context=cert_none_context())) + else: + self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: for handler in self._opener.handlers: if isinstance(handler, HTTPHandler): @@ -1407,7 +1535,7 @@ class HttpsGetter(object): def write(contents, dir, filename): """Write something to a file in a certain directory.""" - with open(join(dir, filename), 'w') as file: + with open(join(dir, filename), 'wb') as file: file.write(contents) @@ -1415,13 +1543,13 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/certbot/json'))) + 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8')) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in metadata['releases'].iterkeys() + in metadata['releases'].keys() if re.match('^[0-9.]+$', r))) @@ -1438,7 +1566,7 @@ def verified_new_le_auto(get, tag, temp_dir): 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') - write(PUBLIC_KEY, temp_dir, 'public_key.pem') + write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem') try: with open(devnull, 'w') as dev_null: check_call(['openssl', 'dgst', '-sha256', '-verify', @@ -1453,6 +1581,14 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) +def cert_none_context(): + """Create a SSLContext object to not check hostname.""" + # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.verify_mode = ssl.CERT_NONE + return context + + def main(): get = HttpsGetter().get flag = argv[1] @@ -1474,8 +1610,10 @@ if __name__ == '__main__': UNLIKELY_EOF # --------------------------------------------------------------------------- - DeterminePythonVersion - if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." + elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." diff --git a/letsencrypt-auto-source/Dockerfile.centos6 b/letsencrypt-auto-source/Dockerfile.centos6 index e1280109b..47eb48f50 100644 --- a/letsencrypt-auto-source/Dockerfile.centos6 +++ b/letsencrypt-auto-source/Dockerfile.centos6 @@ -5,9 +5,13 @@ FROM centos:6 RUN yum install -y epel-release -# Install pip, sudo and nose: +# Install pip and sudo: RUN yum install -y python-pip sudo -RUN pip install nose +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 # Add an unprivileged user: RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups wheel --uid 1000 lea @@ -29,4 +33,5 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +RUN sudo chmod +x certbot/letsencrypt-auto-source/tests/centos6_tests.sh +CMD sudo certbot/letsencrypt-auto-source/tests/centos6_tests.sh diff --git a/letsencrypt-auto-source/Dockerfile.precise b/letsencrypt-auto-source/Dockerfile.precise index c8b593774..71d572315 100644 --- a/letsencrypt-auto-source/Dockerfile.precise +++ b/letsencrypt-auto-source/Dockerfile.precise @@ -6,11 +6,16 @@ FROM ubuntu:precise # Add an unprivileged user: RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea -# Install pip, sudo, openssl, and nose: +# Install pip, sudo, and openssl: RUN apt-get update && \ apt-get -q -y install python-pip sudo openssl && \ apt-get clean -RUN pip install nose + +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 # Let that user sudo: RUN sed -i.bkp -e \ @@ -28,4 +33,4 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/Dockerfile.trusty b/letsencrypt-auto-source/Dockerfile.trusty index 23e8f26de..e0aacd118 100644 --- a/letsencrypt-auto-source/Dockerfile.trusty +++ b/letsencrypt-auto-source/Dockerfile.trusty @@ -11,11 +11,15 @@ RUN sed -i.bkp -e \ 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \ /etc/sudoers -# Install pip and nose: +# Install pip: RUN apt-get update && \ apt-get -q -y install python-pip && \ apt-get clean -RUN pip install nose +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 RUN mkdir -p /home/lea/certbot @@ -29,4 +33,4 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/Dockerfile.wheezy b/letsencrypt-auto-source/Dockerfile.wheezy index f86795e08..56948d22a 100644 --- a/letsencrypt-auto-source/Dockerfile.wheezy +++ b/letsencrypt-auto-source/Dockerfile.wheezy @@ -6,11 +6,15 @@ FROM debian:wheezy # Add an unprivileged user: RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea -# Install pip, sudo, openssl, and nose: +# Install pip, sudo, and openssl: RUN apt-get update && \ apt-get -q -y install python-pip sudo openssl && \ apt-get clean -RUN pip install nose +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 # Let that user sudo: RUN sed -i.bkp -e \ @@ -28,4 +32,4 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 834358464..f28fd9893 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- -Version: GnuPG v2 -iQEcBAABCAAGBQJZ1TJAAAoJEE0XyZXNl3XyWjAIAKxR5v0qbSyOEwM1LrSoLqud -V3KkyEUlMq7IPHxoPKXbqUrIi4eZuhpJz+84LtVJe4ZQ6HYP9lPogX+PtmWW7dyO -YerxA2rUVGB9rFZofZYwTuJyvO5Nc0aDyp1FHHPg/5khWWhhhxKpWqqG3zT01+Vf -W8Lvvn7vr7sjTvxBdqHQ3z3hlUY62P2IKui9C5un5ozlSQpDrWh3Thi9r6CxbASL -/r1PQ6EfnNdPAizVrJWe5iUd0Nzj7VMkFwZ02A3OlOUvrHGVb1H6oj0S1lZ8LEpj -awOTys8PVBQ3vW2qbAL3Zk7Lr+CGfVfmoWC9TQEKiSN1woYFrFD39S527vB1onc= -=Meks +iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlpqMlYACgkQTRfJlc2X +dfKHfQgAnZQJ34jFoVqEodT0EjvkFKZif4V/zXTsVwTHn107BcLCpH/9gjANrSo3 +JpvseH2q0odhOAZA4rZKH4Geh+5fsUl3Ew9YB28RXeyqEfCATUqPq6q+jAi55SLc +a064Ux5N7eOIh9gxvpDKBeSFD0eNB8IDtPQhUspr+WnoycawrJHNGawL8WIfrWY3 +0ZPF981iPCWCdN3woDP9wHA2QtBClAk2pQ1aMgdkK9r/QLO+DY92xmT/Uu4ik2jR +zv+QplsQLftjD+bRar5R9jiCWV5phPqrOF3ypMiU0K5bsnrZfGBzBcoEyfKuB+UR +F/j/631OC6yLRasr+xcL1gc+SCryfA== +=tkZT -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 215b684cf..c84499eab 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.20.0.dev0" +LE_AUTO_VERSION="0.22.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -47,6 +47,7 @@ Help for certbot itself cannot be provided until it is installed. --no-bootstrap do not install OS dependencies --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + --install-only install certbot, upgrade if needed, and exit -v, --verbose provide more output -q, --quiet provide only update/error output; implies --non-interactive @@ -60,6 +61,8 @@ for arg in "$@" ; do DEBUG=1;; --os-packages-only) OS_PACKAGES_ONLY=1;; + --install-only) + INSTALL_ONLY=1;; --no-self-upgrade) # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) @@ -68,10 +71,12 @@ for arg in "$@" ; do NO_BOOTSTRAP=1;; --help) HELP=1;; - --noninteractive|--non-interactive|renew) - ASSUME_YES=1;; + --noninteractive|--non-interactive) + NONINTERACTIVE=1;; --quiet) QUIET=1;; + renew) + ASSUME_YES=1;; --verbose) VERBOSE=1;; -[!-]*) @@ -93,7 +98,7 @@ done if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 + NONINTERACTIVE=1 HELP=0 fi @@ -244,23 +249,42 @@ DeprecationBootstrap() { fi } - +MIN_PYTHON_VERSION="2.6" +MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') +# Sets LE_PYTHON to Python version string and PYVER to the first two +# digits of the python version DeterminePythonVersion() { - for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do - # Break (while keeping the LE_PYTHON value) if found. - $EXISTS "$LE_PYTHON" > /dev/null && break - done - if [ "$?" != "0" ]; then - error "Cannot find any Pythons; please install one!" - exit 1 + # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + # + # If no Python is found, PYVER is set to 0. + if [ "$USE_PYTHON_3" = 1 ]; then + for LE_PYTHON in "$LE_PYTHON" python3; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + else + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + fi + if [ "$?" != "0" ]; then + if [ "$1" != "NOCRASH" ]; then + error "Cannot find any Pythons; please install one!" + exit 1 + else + PYVER=0 + return 0 + fi fi - export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ "$PYVER" -lt 26 ]; then - error "You have an ancient version of Python entombed in your operating system..." - error "This isn't going to work; you'll need at least version 2.6." - exit 1 + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + if [ "$1" != "NOCRASH" ]; then + error "You have an ancient version of Python entombed in your operating system..." + error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION." + exit 1 + fi fi } @@ -384,23 +408,19 @@ BootstrapDebCommon() { fi } -# If new packages are installed by BootstrapRpmCommon below, this version -# number must be increased. -BOOTSTRAP_RPM_COMMON_VERSION=1 - -BootstrapRpmCommon() { - # Tested with: - # - Fedora 20, 21, 22, 23 (x64) - # - Centos 7 (x64: on DigitalOcean droplet) - # - CentOS 7 Minimal install in a Hyper-V VM - # - CentOS 6 (EPEL must be installed manually) +# If new packages are installed by BootstrapRpmCommonBase below, version +# numbers in rpm_common.sh and rpm_python3.sh must be increased. +# Sets TOOL to the name of the package manager +# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG. +# Enables EPEL if applicable and possible. +InitializeRPMCommonBase() { if type dnf 2>/dev/null then - tool=dnf + TOOL=dnf elif type yum 2>/dev/null then - tool=yum + TOOL=yum else error "Neither yum nor dnf found. Aborting bootstrap!" @@ -408,15 +428,15 @@ BootstrapRpmCommon() { fi if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" + YES_FLAG="-y" fi if [ "$QUIET" = 1 ]; then QUIET_FLAG='--quiet' fi - if ! $tool list *virtualenv >/dev/null 2>&1; then + if ! $TOOL list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $tool list epel-release >/dev/null 2>&1; then + if ! $TOOL list epel-release >/dev/null 2>&1; then error "Enable the EPEL repository and try running Certbot again." exit 1 fi @@ -425,14 +445,20 @@ BootstrapRpmCommon() { sleep 1s /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 second..." sleep 1s fi - if ! $tool install $yes_flag $QUIET_FLAG epel-release; then + if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then error "Could not enable EPEL. Aborting bootstrap!" exit 1 fi fi +} + +BootstrapRpmCommonBase() { + # Arguments: whitespace-delimited python packages to install + + InitializeRPMCommonBase # This call is superfluous in practice pkgs=" gcc @@ -444,10 +470,39 @@ BootstrapRpmCommon() { ca-certificates " - # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $tool list python >/dev/null 2>&1; then + # Add the python packages + pkgs="$pkgs + $1 + " + + if $TOOL list installed "httpd" >/dev/null 2>&1; then pkgs="$pkgs - python + mod_ssl + " + fi + + if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then + error "Could not install OS dependencies. Aborting bootstrap!" + exit 1 + fi +} + +# If new packages are installed by BootstrapRpmCommon below, this version +# number must be increased. +BOOTSTRAP_RPM_COMMON_VERSION=1 + +BootstrapRpmCommon() { + # Tested with: + # - Fedora 20, 21, 22, 23 (x64) + # - Centos 7 (x64: on DigitalOcean droplet) + # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 + + InitializeRPMCommonBase + + # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. + if $TOOL list python >/dev/null 2>&1; then + python_pkgs="$python python-devel python-virtualenv python-tools @@ -455,9 +510,8 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $tool list python2 >/dev/null 2>&1; then - pkgs="$pkgs - python2 + elif $TOOL list python2 >/dev/null 2>&1; then + python_pkgs="$python2 python2-libs python2-setuptools python2-devel @@ -468,8 +522,7 @@ BootstrapRpmCommon() { # Some distros and older versions of current distros use a "python27" # instead of the "python" or "python-" naming convention. else - pkgs="$pkgs - python27 + python_pkgs="$python27 python27-devel python27-virtualenv python27-tools @@ -477,16 +530,31 @@ BootstrapRpmCommon() { " fi - if $tool list installed "httpd" >/dev/null 2>&1; then - pkgs="$pkgs - mod_ssl - " - fi + BootstrapRpmCommonBase "$python_pkgs" +} - if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then - error "Could not install OS dependencies. Aborting bootstrap!" +# If new packages are installed by BootstrapRpmPython3 below, this version +# number must be increased. +BOOTSTRAP_RPM_PYTHON3_VERSION=1 + +BootstrapRpmPython3() { + # Tested with: + # - CentOS 6 + + InitializeRPMCommonBase + + # EPEL uses python34 + if $TOOL list python34 >/dev/null 2>&1; then + python_pkgs="python34 + python34-devel + python34-tools + " + else + error "No supported Python package available to install. Aborting bootstrap!" exit 1 fi + + BootstrapRpmCommonBase "$python_pkgs" } # If new packages are installed by BootstrapSuseCommon below, this version @@ -696,13 +764,8 @@ BootstrapMageiaCommon() { # Set Bootstrap to the function that installs OS dependencies on this system # and BOOTSTRAP_VERSION to the unique identifier for the current version of # that function. If Bootstrap is set to a function that doesn't install any -# packages (either because --no-bootstrap was included on the command line or -# we don't know how to bootstrap on this system), BOOTSTRAP_VERSION is not set. -if [ "$NO_BOOTSTRAP" = 1 ]; then - Bootstrap() { - : - } -elif [ -f /etc/debian_version ]; then +# packages BOOTSTRAP_VERSION is not set. +if [ -f /etc/debian_version ]; then Bootstrap() { BootstrapMessage "Debian-based OSes" BootstrapDebCommon @@ -715,11 +778,27 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + # Run DeterminePythonVersion to decide on the basis of available Python versions + # whether to use 2.x or 3.x on RedHat-like systems. + # Then, revert LE_PYTHON to its previous state. + prev_le_python="$LE_PYTHON" + unset LE_PYTHON + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -eq 26 ]; then + Bootstrap() { + BootstrapMessage "RedHat-based OSes that will use Python3" + BootstrapRpmPython3 + } + USE_PYTHON_3=1 + BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" + else + Bootstrap() { + BootstrapMessage "RedHat-based OSes" + BootstrapRpmCommon + } + BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + fi + LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -782,6 +861,17 @@ else } fi +# We handle this case after determining the normal bootstrap version to allow +# variables like USE_PYTHON_3 to be properly set. As described above, if the +# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not +# be set so we unset it here. +if [ "$NO_BOOTSTRAP" = 1 ]; then + Bootstrap() { + : + } + unset BOOTSTRAP_VERSION +fi + # Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used # to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set # if it is unknown how OS dependencies were installed on this system. @@ -816,7 +906,11 @@ TempDir() { mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS } - +# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise, +# returns a non-zero number. +OldVenvExists() { + [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] +} if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -824,14 +918,26 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg SetPrevBootstrapVersion + if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then + unset LE_PYTHON + fi + INSTALLED_VERSION="none" - if [ -d "$VENV_PATH" ]; then + if [ -d "$VENV_PATH" ] || OldVenvExists; then # If the selected Bootstrap function isn't a noop and it differs from the # previously used version if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then # if non-interactive mode or stdin and stdout are connected to a terminal if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then - rm -rf "$VENV_PATH" + if [ -d "$VENV_PATH" ]; then + rm -rf "$VENV_PATH" + fi + # In the case the old venv was just a symlink to the new one, + # OldVenvExists is now false because we deleted the venv at VENV_PATH. + if OldVenvExists; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi RerunWithArgs "$@" else error "Skipping upgrade because new OS dependencies may need to be installed." @@ -841,6 +947,10 @@ if [ "$1" = "--le-auto-phase2" ]; then error "install any required packages." # Set INSTALLED_VERSION to be the same so we don't update the venv INSTALLED_VERSION="$LE_AUTO_VERSION" + # Continue to use OLD_VENV_PATH if the new venv doesn't exist + if [ ! -d "$VENV_PATH" ]; then + VENV_BIN="$OLD_VENV_PATH/bin" + fi fi elif [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings @@ -858,10 +968,18 @@ if [ "$1" = "--le-auto-phase2" ]; then say "Creating virtual environment..." DeterminePythonVersion rm -rf "$VENV_PATH" - if [ "$VERBOSE" = 1 ]; then - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + if [ "$PYVER" -le 27 ]; then + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi else - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + if [ "$VERBOSE" = 1 ]; then + "$LE_PYTHON" -m venv "$VENV_PATH" + else + "$LE_PYTHON" -m venv "$VENV_PATH" > /dev/null + fi fi if [ -n "$BOOTSTRAP_VERSION" ]; then @@ -983,9 +1101,16 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +josepy==1.0.1 \ + --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ + --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f packaging==16.8 \ @@ -1062,9 +1187,6 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba # Contains the requirements for the letsencrypt package. # @@ -1077,18 +1199,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.19.0 \ - --hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \ - --hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53 -acme==0.19.0 \ - --hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \ - --hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822 -certbot-apache==0.19.0 \ - --hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \ - --hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619 -certbot-nginx==0.19.0 \ - --hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \ - --hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1 +certbot==0.21.1 \ + --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ + --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea +acme==0.21.1 \ + --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ + --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b +certbot-apache==0.21.1 \ + --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ + --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 +certbot-nginx==0.21.1 \ + --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ + --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -1309,6 +1431,12 @@ UNLIKELY_EOF say "Installation succeeded." fi + + if [ "$INSTALL_ONLY" = 1 ]; then + say "Certbot is installed." + exit 0 + fi + "$VENV_BIN/letsencrypt" "$@" else @@ -1318,9 +1446,10 @@ else # upgrading. Phase 1 checks the version of the latest release of # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. + export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then - if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if ! OldVenvExists; then if [ "$HELP" = 1 ]; then echo "$USAGE" exit 0 @@ -1352,17 +1481,22 @@ On failure, return non-zero. """ -from __future__ import print_function +from __future__ import print_function, unicode_literals from distutils.version import LooseVersion from json import loads from os import devnull, environ from os.path import dirname, join import re +import ssl from subprocess import check_call, CalledProcessError from sys import argv, exit -from urllib2 import build_opener, HTTPHandler, HTTPSHandler -from urllib2 import HTTPError, URLError +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler + from urllib2 import HTTPError, URLError +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler + from urllib.error import HTTPError, URLError PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq @@ -1384,8 +1518,11 @@ class HttpsGetter(object): def __init__(self): """Build an HTTPS opener.""" # Based on pip 1.4.1's URLOpener - # This verifies certs on only Python >=2.7.9. - self._opener = build_opener(HTTPSHandler()) + # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. + if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): + self._opener = build_opener(HTTPSHandler(context=cert_none_context())) + else: + self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: for handler in self._opener.handlers: if isinstance(handler, HTTPHandler): @@ -1407,7 +1544,7 @@ class HttpsGetter(object): def write(contents, dir, filename): """Write something to a file in a certain directory.""" - with open(join(dir, filename), 'w') as file: + with open(join(dir, filename), 'wb') as file: file.write(contents) @@ -1415,13 +1552,13 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/certbot/json'))) + 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8')) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in metadata['releases'].iterkeys() + in metadata['releases'].keys() if re.match('^[0-9.]+$', r))) @@ -1438,7 +1575,7 @@ def verified_new_le_auto(get, tag, temp_dir): 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') - write(PUBLIC_KEY, temp_dir, 'public_key.pem') + write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem') try: with open(devnull, 'w') as dev_null: check_call(['openssl', 'dgst', '-sha256', '-verify', @@ -1453,6 +1590,14 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) +def cert_none_context(): + """Create a SSLContext object to not check hostname.""" + # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.verify_mode = ssl.CERT_NONE + return context + + def main(): get = HttpsGetter().get flag = argv[1] @@ -1474,8 +1619,10 @@ if __name__ == '__main__': UNLIKELY_EOF # --------------------------------------------------------------------------- - DeterminePythonVersion - if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." + elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 708bbbee6..8dd683775 100644 Binary files a/letsencrypt-auto-source/letsencrypt-auto.sig and b/letsencrypt-auto-source/letsencrypt-auto.sig differ diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 4eef10c80..365fc6752 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -47,6 +47,7 @@ Help for certbot itself cannot be provided until it is installed. --no-bootstrap do not install OS dependencies --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + --install-only install certbot, upgrade if needed, and exit -v, --verbose provide more output -q, --quiet provide only update/error output; implies --non-interactive @@ -60,6 +61,8 @@ for arg in "$@" ; do DEBUG=1;; --os-packages-only) OS_PACKAGES_ONLY=1;; + --install-only) + INSTALL_ONLY=1;; --no-self-upgrade) # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) @@ -68,10 +71,12 @@ for arg in "$@" ; do NO_BOOTSTRAP=1;; --help) HELP=1;; - --noninteractive|--non-interactive|renew) - ASSUME_YES=1;; + --noninteractive|--non-interactive) + NONINTERACTIVE=1;; --quiet) QUIET=1;; + renew) + ASSUME_YES=1;; --verbose) VERBOSE=1;; -[!-]*) @@ -93,7 +98,7 @@ done if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 + NONINTERACTIVE=1 HELP=0 fi @@ -244,28 +249,49 @@ DeprecationBootstrap() { fi } - +MIN_PYTHON_VERSION="2.6" +MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') +# Sets LE_PYTHON to Python version string and PYVER to the first two +# digits of the python version DeterminePythonVersion() { - for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do - # Break (while keeping the LE_PYTHON value) if found. - $EXISTS "$LE_PYTHON" > /dev/null && break - done - if [ "$?" != "0" ]; then - error "Cannot find any Pythons; please install one!" - exit 1 + # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + # + # If no Python is found, PYVER is set to 0. + if [ "$USE_PYTHON_3" = 1 ]; then + for LE_PYTHON in "$LE_PYTHON" python3; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + else + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + fi + if [ "$?" != "0" ]; then + if [ "$1" != "NOCRASH" ]; then + error "Cannot find any Pythons; please install one!" + exit 1 + else + PYVER=0 + return 0 + fi fi - export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ "$PYVER" -lt 26 ]; then - error "You have an ancient version of Python entombed in your operating system..." - error "This isn't going to work; you'll need at least version 2.6." - exit 1 + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + if [ "$1" != "NOCRASH" ]; then + error "You have an ancient version of Python entombed in your operating system..." + error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION." + exit 1 + fi fi } {{ bootstrappers/deb_common.sh }} +{{ bootstrappers/rpm_common_base.sh }} {{ bootstrappers/rpm_common.sh }} +{{ bootstrappers/rpm_python3.sh }} {{ bootstrappers/suse_common.sh }} {{ bootstrappers/arch_common.sh }} {{ bootstrappers/gentoo_common.sh }} @@ -277,13 +303,8 @@ DeterminePythonVersion() { # Set Bootstrap to the function that installs OS dependencies on this system # and BOOTSTRAP_VERSION to the unique identifier for the current version of # that function. If Bootstrap is set to a function that doesn't install any -# packages (either because --no-bootstrap was included on the command line or -# we don't know how to bootstrap on this system), BOOTSTRAP_VERSION is not set. -if [ "$NO_BOOTSTRAP" = 1 ]; then - Bootstrap() { - : - } -elif [ -f /etc/debian_version ]; then +# packages BOOTSTRAP_VERSION is not set. +if [ -f /etc/debian_version ]; then Bootstrap() { BootstrapMessage "Debian-based OSes" BootstrapDebCommon @@ -296,11 +317,27 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + # Run DeterminePythonVersion to decide on the basis of available Python versions + # whether to use 2.x or 3.x on RedHat-like systems. + # Then, revert LE_PYTHON to its previous state. + prev_le_python="$LE_PYTHON" + unset LE_PYTHON + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -eq 26 ]; then + Bootstrap() { + BootstrapMessage "RedHat-based OSes that will use Python3" + BootstrapRpmPython3 + } + USE_PYTHON_3=1 + BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" + else + Bootstrap() { + BootstrapMessage "RedHat-based OSes" + BootstrapRpmCommon + } + BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + fi + LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -363,6 +400,17 @@ else } fi +# We handle this case after determining the normal bootstrap version to allow +# variables like USE_PYTHON_3 to be properly set. As described above, if the +# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not +# be set so we unset it here. +if [ "$NO_BOOTSTRAP" = 1 ]; then + Bootstrap() { + : + } + unset BOOTSTRAP_VERSION +fi + # Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used # to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set # if it is unknown how OS dependencies were installed on this system. @@ -397,7 +445,11 @@ TempDir() { mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS } - +# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise, +# returns a non-zero number. +OldVenvExists() { + [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] +} if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -405,14 +457,26 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg SetPrevBootstrapVersion + if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then + unset LE_PYTHON + fi + INSTALLED_VERSION="none" - if [ -d "$VENV_PATH" ]; then + if [ -d "$VENV_PATH" ] || OldVenvExists; then # If the selected Bootstrap function isn't a noop and it differs from the # previously used version if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then # if non-interactive mode or stdin and stdout are connected to a terminal if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then - rm -rf "$VENV_PATH" + if [ -d "$VENV_PATH" ]; then + rm -rf "$VENV_PATH" + fi + # In the case the old venv was just a symlink to the new one, + # OldVenvExists is now false because we deleted the venv at VENV_PATH. + if OldVenvExists; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi RerunWithArgs "$@" else error "Skipping upgrade because new OS dependencies may need to be installed." @@ -422,6 +486,10 @@ if [ "$1" = "--le-auto-phase2" ]; then error "install any required packages." # Set INSTALLED_VERSION to be the same so we don't update the venv INSTALLED_VERSION="$LE_AUTO_VERSION" + # Continue to use OLD_VENV_PATH if the new venv doesn't exist + if [ ! -d "$VENV_PATH" ]; then + VENV_BIN="$OLD_VENV_PATH/bin" + fi fi elif [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings @@ -439,10 +507,18 @@ if [ "$1" = "--le-auto-phase2" ]; then say "Creating virtual environment..." DeterminePythonVersion rm -rf "$VENV_PATH" - if [ "$VERBOSE" = 1 ]; then - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + if [ "$PYVER" -le 27 ]; then + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi else - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + if [ "$VERBOSE" = 1 ]; then + "$LE_PYTHON" -m venv "$VENV_PATH" + else + "$LE_PYTHON" -m venv "$VENV_PATH" > /dev/null + fi fi if [ -n "$BOOTSTRAP_VERSION" ]; then @@ -514,6 +590,12 @@ UNLIKELY_EOF say "Installation succeeded." fi + + if [ "$INSTALL_ONLY" = 1 ]; then + say "Certbot is installed." + exit 0 + fi + "$VENV_BIN/letsencrypt" "$@" else @@ -523,9 +605,10 @@ else # upgrading. Phase 1 checks the version of the latest release of # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. + export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then - if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if ! OldVenvExists; then if [ "$HELP" = 1 ]; then echo "$USAGE" exit 0 @@ -547,8 +630,10 @@ else {{ fetch.py }} UNLIKELY_EOF # --------------------------------------------------------------------------- - DeterminePythonVersion - if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." + elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 5b120a9e6..80d55a393 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -7,61 +7,13 @@ BootstrapRpmCommon() { # - Fedora 20, 21, 22, 23 (x64) # - Centos 7 (x64: on DigitalOcean droplet) # - CentOS 7 Minimal install in a Hyper-V VM - # - CentOS 6 (EPEL must be installed manually) + # - CentOS 6 - if type dnf 2>/dev/null - then - tool=dnf - elif type yum 2>/dev/null - then - tool=yum - - else - error "Neither yum nor dnf found. Aborting bootstrap!" - exit 1 - fi - - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if [ "$QUIET" = 1 ]; then - QUIET_FLAG='--quiet' - fi - - if ! $tool list *virtualenv >/dev/null 2>&1; then - echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $tool list epel-release >/dev/null 2>&1; then - error "Enable the EPEL repository and try running Certbot again." - exit 1 - fi - if [ "$ASSUME_YES" = 1 ]; then - /bin/echo -n "Enabling the EPEL repository in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." - sleep 1s - fi - if ! $tool install $yes_flag $QUIET_FLAG epel-release; then - error "Could not enable EPEL. Aborting bootstrap!" - exit 1 - fi - fi - - pkgs=" - gcc - augeas-libs - openssl - openssl-devel - libffi-devel - redhat-rpm-config - ca-certificates - " + InitializeRPMCommonBase # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $tool list python >/dev/null 2>&1; then - pkgs="$pkgs - python + if $TOOL list python >/dev/null 2>&1; then + python_pkgs="$python python-devel python-virtualenv python-tools @@ -69,9 +21,8 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $tool list python2 >/dev/null 2>&1; then - pkgs="$pkgs - python2 + elif $TOOL list python2 >/dev/null 2>&1; then + python_pkgs="$python2 python2-libs python2-setuptools python2-devel @@ -82,8 +33,7 @@ BootstrapRpmCommon() { # Some distros and older versions of current distros use a "python27" # instead of the "python" or "python-" naming convention. else - pkgs="$pkgs - python27 + python_pkgs="$python27 python27-devel python27-virtualenv python27-tools @@ -91,14 +41,5 @@ BootstrapRpmCommon() { " fi - if $tool list installed "httpd" >/dev/null 2>&1; then - pkgs="$pkgs - mod_ssl - " - fi - - if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then - error "Could not install OS dependencies. Aborting bootstrap!" - exit 1 - fi + BootstrapRpmCommonBase "$python_pkgs" } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh new file mode 100644 index 000000000..326ad8b3f --- /dev/null +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh @@ -0,0 +1,78 @@ +# If new packages are installed by BootstrapRpmCommonBase below, version +# numbers in rpm_common.sh and rpm_python3.sh must be increased. + +# Sets TOOL to the name of the package manager +# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG. +# Enables EPEL if applicable and possible. +InitializeRPMCommonBase() { + if type dnf 2>/dev/null + then + TOOL=dnf + elif type yum 2>/dev/null + then + TOOL=yum + + else + error "Neither yum nor dnf found. Aborting bootstrap!" + exit 1 + fi + + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='--quiet' + fi + + if ! $TOOL list *virtualenv >/dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if ! $TOOL list epel-release >/dev/null 2>&1; then + error "Enable the EPEL repository and try running Certbot again." + exit 1 + fi + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 second..." + sleep 1s + fi + if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then + error "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi +} + +BootstrapRpmCommonBase() { + # Arguments: whitespace-delimited python packages to install + + InitializeRPMCommonBase # This call is superfluous in practice + + pkgs=" + gcc + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + + # Add the python packages + pkgs="$pkgs + $1 + " + + if $TOOL list installed "httpd" >/dev/null 2>&1; then + pkgs="$pkgs + mod_ssl + " + fi + + if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then + error "Could not install OS dependencies. Aborting bootstrap!" + exit 1 + fi +} diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_python3.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_python3.sh new file mode 100644 index 000000000..b011a7235 --- /dev/null +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_python3.sh @@ -0,0 +1,23 @@ +# If new packages are installed by BootstrapRpmPython3 below, this version +# number must be increased. +BOOTSTRAP_RPM_PYTHON3_VERSION=1 + +BootstrapRpmPython3() { + # Tested with: + # - CentOS 6 + + InitializeRPMCommonBase + + # EPEL uses python34 + if $TOOL list python34 >/dev/null 2>&1; then + python_pkgs="python34 + python34-devel + python34-tools + " + else + error "No supported Python package available to install. Aborting bootstrap!" + exit 1 + fi + + BootstrapRpmCommonBase "$python_pkgs" +} diff --git a/letsencrypt-auto-source/pieces/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index dee80dbd1..0a5994afc 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -certbot==0.19.0 \ - --hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \ - --hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53 -acme==0.19.0 \ - --hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \ - --hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822 -certbot-apache==0.19.0 \ - --hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \ - --hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619 -certbot-nginx==0.19.0 \ - --hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \ - --hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1 +certbot==0.21.1 \ + --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ + --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea +acme==0.21.1 \ + --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ + --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b +certbot-apache==0.21.1 \ + --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ + --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 +certbot-nginx==0.21.1 \ + --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ + --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index 4b3da685c..0e2cec984 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -105,9 +105,16 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +josepy==1.0.1 \ + --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ + --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f packaging==16.8 \ @@ -184,6 +191,3 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index 8f34351c9..1515fe353 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -11,17 +11,22 @@ On failure, return non-zero. """ -from __future__ import print_function +from __future__ import print_function, unicode_literals from distutils.version import LooseVersion from json import loads from os import devnull, environ from os.path import dirname, join import re +import ssl from subprocess import check_call, CalledProcessError from sys import argv, exit -from urllib2 import build_opener, HTTPHandler, HTTPSHandler -from urllib2 import HTTPError, URLError +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler + from urllib2 import HTTPError, URLError +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler + from urllib.error import HTTPError, URLError PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq @@ -43,8 +48,11 @@ class HttpsGetter(object): def __init__(self): """Build an HTTPS opener.""" # Based on pip 1.4.1's URLOpener - # This verifies certs on only Python >=2.7.9. - self._opener = build_opener(HTTPSHandler()) + # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. + if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): + self._opener = build_opener(HTTPSHandler(context=cert_none_context())) + else: + self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: for handler in self._opener.handlers: if isinstance(handler, HTTPHandler): @@ -66,7 +74,7 @@ class HttpsGetter(object): def write(contents, dir, filename): """Write something to a file in a certain directory.""" - with open(join(dir, filename), 'w') as file: + with open(join(dir, filename), 'wb') as file: file.write(contents) @@ -74,13 +82,13 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/certbot/json'))) + 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8')) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in metadata['releases'].iterkeys() + in metadata['releases'].keys() if re.match('^[0-9.]+$', r))) @@ -97,7 +105,7 @@ def verified_new_le_auto(get, tag, temp_dir): 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') - write(PUBLIC_KEY, temp_dir, 'public_key.pem') + write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem') try: with open(devnull, 'w') as dev_null: check_call(['openssl', 'dgst', '-sha256', '-verify', @@ -112,6 +120,14 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) +def cert_none_context(): + """Create a SSLContext object to not check hostname.""" + # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.verify_mode = ssl.CERT_NONE + return context + + def main(): get = HttpsGetter().get flag = argv[1] diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 5c63325ee..d187452a1 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -17,10 +17,10 @@ from tempfile import mkdtemp from threading import Thread from unittest import TestCase -from nose.tools import eq_, nottest, ok_ +from pytest import mark -@nottest +@mark.skip def tests_dir(): """Return a path to the "tests" directory.""" return dirname(abspath(__file__)) @@ -30,6 +30,10 @@ sys.path.insert(0, dirname(tests_dir())) from build import build as build_le_auto +BOOTSTRAP_FILENAME = 'certbot-auto-bootstrap-version.txt' +"""Name of the file where certbot-auto saves its bootstrap version.""" + + class RequestHandler(BaseHTTPRequestHandler): """An HTTPS request handler which is quiet and serves a specific folder.""" @@ -198,6 +202,7 @@ LsIVPBuy9IcgHidUQ96hJnoPsDCWsHwX62495QKEarauyKQrJzFes0EY95orDM47 Z5o/NDiQB11m91yNB0MmPYY9QSbnOA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68 iQIDAQAB -----END PUBLIC KEY-----""", + NO_CERT_VERIFY='1', **kwargs) env.update(d) return out_and_err( @@ -279,8 +284,8 @@ class AutoTests(TestCase): # installed, and pip hashes verify: install_le_auto(build_le_auto(version='50.0.0'), le_auto_path) out, err = run_letsencrypt_auto() - ok_(re.match(r'letsencrypt \d+\.\d+\.\d+', - err.strip().splitlines()[-1])) + self.assertTrue(re.match(r'letsencrypt \d+\.\d+\.\d+', + err.strip().splitlines()[-1])) # Make a few assertions to test the validity of the next tests: self.assertTrue('Upgrading certbot-auto ' in out) self.assertTrue('Creating virtual environment...' in out) @@ -296,17 +301,31 @@ class AutoTests(TestCase): def test_phase2_upgrade(self): """Test a phase-2 upgrade without a phase-1 upgrade.""" - with temp_paths() as (le_auto_path, venv_dir): - resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, - 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} - with serving(resources) as base_url: + resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), + 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, + 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} + with serving(resources) as base_url: + pip_find_links=join(tests_dir(), 'fake-letsencrypt', 'dist') + with temp_paths() as (le_auto_path, venv_dir): + install_le_auto(self.NEW_LE_AUTO, le_auto_path) + + # Create venv saving the correct bootstrap script version + out, err = run_le_auto(le_auto_path, venv_dir, base_url, + PIP_FIND_LINKS=pip_find_links) + self.assertFalse('Upgrading certbot-auto ' in out) + self.assertTrue('Creating virtual environment...' in out) + with open(join(venv_dir, BOOTSTRAP_FILENAME)) as f: + bootstrap_version = f.read() + + # Create a new venv with an old letsencrypt version + with temp_paths() as (le_auto_path, venv_dir): venv_bin = join(venv_dir, 'bin') makedirs(venv_bin) set_le_script_version(venv_dir, '0.0.1') + with open(join(venv_dir, BOOTSTRAP_FILENAME), 'w') as f: + f.write(bootstrap_version) install_le_auto(self.NEW_LE_AUTO, le_auto_path) - pip_find_links=join(tests_dir(), 'fake-letsencrypt', 'dist') out, err = run_le_auto(le_auto_path, venv_dir, base_url, PIP_FIND_LINKS=pip_find_links) @@ -327,10 +346,11 @@ class AutoTests(TestCase): try: out, err = run_le_auto(le_auto_path, venv_dir, base_url) except CalledProcessError as exc: - eq_(exc.returncode, 1) + self.assertEqual(exc.returncode, 1) self.assertTrue("Couldn't verify signature of downloaded " "certbot-auto." in exc.output) else: + print(out) self.fail('Signature check on certbot-auto erroneously passed.') def test_pip_failure(self): @@ -348,10 +368,11 @@ class AutoTests(TestCase): try: out, err = run_le_auto(le_auto_path, venv_dir, base_url) except CalledProcessError as exc: - eq_(exc.returncode, 1) + self.assertEqual(exc.returncode, 1) self.assertTrue("THESE PACKAGES DO NOT MATCH THE HASHES " "FROM THE REQUIREMENTS FILE" in exc.output) - ok_(not exists(venv_dir), + self.assertFalse( + exists(venv_dir), msg="The virtualenv was left around, even though " "installation didn't succeed. We shouldn't do " "this, as it foils our detection of whether we " diff --git a/letsencrypt-auto-source/tests/centos6_tests.sh b/letsencrypt-auto-source/tests/centos6_tests.sh new file mode 100644 index 000000000..2c6dcf734 --- /dev/null +++ b/letsencrypt-auto-source/tests/centos6_tests.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Start by making sure your system is up-to-date: +yum update -y > /dev/null +yum install -y centos-release-scl > /dev/null +yum install -y python27 > /dev/null 2> /dev/null + +LE_AUTO="certbot/letsencrypt-auto-source/letsencrypt-auto" + +# we're going to modify env variables, so do this in a subshell +( +source /opt/rh/python27/enable + +# ensure python 3 isn't installed +python3 --version 2> /dev/null +RESULT=$? +if [ $RESULT -eq 0 ]; then + error "Python3 is already installed." + exit 1 +fi + +# ensure python2.7 is available +python2.7 --version 2> /dev/null +RESULT=$? +if [ $RESULT -ne 0 ]; then + error "Python3 is not available." + exit 1 +fi + +# bootstrap, but don't install python 3. +"$LE_AUTO" --no-self-upgrade -n > /dev/null 2> /dev/null + +# ensure python 3 isn't installed +python3 --version 2> /dev/null +RESULT=$? +if [ $RESULT -eq 0 ]; then + error "letsencrypt-auto installed Python3 even though Python2.7 is present." + exit 1 +fi + +echo "" +echo "PASSED: Did not upgrade to Python3 when Python2.7 is present." +) + +# ensure python2.7 isn't available +python2.7 --version 2> /dev/null +RESULT=$? +if [ $RESULT -eq 0 ]; then + error "Python2.7 is still available." + exit 1 +fi + +# Skip self upgrade due to Python 3 not being available. +if ! "$LE_AUTO" 2>&1 | grep -q "WARNING: couldn't find Python"; then + echo "Python upgrade failure warning not printed!" + exit 1 +fi + +# bootstrap, this time installing python3 +"$LE_AUTO" --no-self-upgrade -n > /dev/null 2> /dev/null + +# ensure python 3 is installed +python3 --version > /dev/null +RESULT=$? +if [ $RESULT -ne 0 ]; then + error "letsencrypt-auto failed to install Python3 when only Python2.6 is present." + exit 1 +fi + +echo "PASSED: Successfully upgraded to Python3 when only Python2.6 is present." +echo "" + +export VENV_PATH=$(mktemp -d) +"$LE_AUTO" -n --no-bootstrap --no-self-upgrade --version >/dev/null 2>&1 +if [ "$($VENV_PATH/bin/python -V 2>&1 | cut -d" " -f2 | cut -d. -f1)" != 3 ]; then + echo "Python 3 wasn't used with --no-bootstrap!" + exit 1 +fi +unset VENV_PATH + +# test using python3 +pytest -v -s certbot/letsencrypt-auto-source/tests diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..b64550cb7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --quiet diff --git a/setup.cfg b/setup.cfg index 3b4dbaf87..a21bab793 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,9 +3,3 @@ universal = 1 [easy_install] zip_ok = false - -[nosetests] -nocapture=1 -cover-package=certbot,acme,certbot_apache,certbot_nginx -cover-erase=1 -cover-tests=1 diff --git a/setup.py b/setup.py index 6ffc9a134..ce505a62e 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,9 @@ readme = read_file(os.path.join(here, 'README.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst')) version = meta['version'] -# Please update tox.ini when modifying dependency version requirements -# This package relies on requests, however, it isn't specified here to avoid -# masking the more specific request requirements in acme. See -# https://github.com/pypa/pip/issues/988 for more info. +# This package relies on PyOpenSSL, requests, and six, however, it isn't +# specified here to avoid masking the more specific request requirements in +# acme. See https://github.com/pypa/pip/issues/988 for more info. install_requires = [ 'acme=={0}'.format(version), # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but @@ -44,13 +43,11 @@ install_requires = [ 'cryptography>=1.2', # load_pem_x509_certificate 'mock', 'parsedatetime>=1.3', # Calendar.parseDT - 'PyOpenSSL', 'pyrfc3339', 'pytz', # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: 'setuptools>=1.0', - 'six', 'zope.component', 'zope.interface', ] @@ -67,7 +64,9 @@ dev_extras = [ 'astroid==1.3.5', 'coverage', 'ipdb', - 'nose', + 'pytest', + 'pytest-cov', + 'pytest-xdist', 'pylint==1.4.2', # upstream #248 'tox', 'twine', diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index 60538362e..08eb736c2 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -17,3 +17,9 @@ FAKE_DNS=$(ifconfig docker0 | grep "inet addr:" | cut -d: -f2 | awk '{ print $1} [ -z "$FAKE_DNS" ] && echo Unable to find the IP for docker0 && exit 1 sed -i "s/FAKE_DNS: .*/FAKE_DNS: ${FAKE_DNS}/" docker-compose.yml docker-compose up -d + +set +x # reduce verbosity while waiting for boulder +until curl http://localhost:4000/directory 2>/dev/null; do + echo waiting for boulder + sleep 1 +done diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 7afba19df..24d224cb0 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -20,14 +20,6 @@ cleanup_and_exit() { echo Kill server subprocess, left running by abnormal exit kill $SERVER_STILL_RUNNING fi - # Dump boulder logs in case they contain useful debugging information. - : "------------------ ------------------ ------------------" - : "------------------ begin boulder logs ------------------" - : "------------------ ------------------ ------------------" - docker logs boulder_boulder_1 - : "------------------ ------------------ ------------------" - : "------------------ end boulder logs ------------------" - : "------------------ ------------------ ------------------" if [ -f "$HOOK_DIRS_TEST" ]; then rm -f "$HOOK_DIRS_TEST" fi @@ -259,7 +251,7 @@ openssl x509 -in "${root}/csr/chain.pem" -text common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ - --key-path "${root}/csr/key.pem" + --key-path "${root}/key.pem" CheckCertCount() { CERTCOUNT=`ls "${root}/conf/archive/$1/cert"* | wc -l` @@ -353,9 +345,14 @@ common auth --must-staple --domains "must-staple.le.wtf" openssl x509 -in "${root}/conf/live/must-staple.le.wtf/cert.pem" -text | grep '1.3.6.1.5.5.7.1.24' # revoke by account key -common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" +common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" --delete-after-revoke # revoke renewed -common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem" +common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem" --no-delete-after-revoke +if [ ! -d "$root/conf/live/le1.wtf" ]; then + echo "cert deleted when --no-delete-after-revoke was used!" + exit 1 +fi +common delete --cert-name le1.wtf # revoke by cert key common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \ --key-path "$root/conf/live/le2.wtf/privkey.pem" diff --git a/tests/letstest/scripts/test_leauto_upgrades.sh b/tests/letstest/scripts/test_leauto_upgrades.sh index cb659786e..355fead2e 100755 --- a/tests/letstest/scripts/test_leauto_upgrades.sh +++ b/tests/letstest/scripts/test_leauto_upgrades.sh @@ -15,22 +15,105 @@ if ! command -v git ; then exit 1 fi fi -BRANCH=`git rev-parse --abbrev-ref HEAD` # 0.5.0 is the oldest version of letsencrypt-auto that can be used because it's # the first version that pins package versions, properly supports # --no-self-upgrade, and works with newer versions of pip. -git checkout -f v0.5.0 +git checkout -f v0.5.0 letsencrypt-auto if ! ./letsencrypt-auto -v --debug --version --no-self-upgrade 2>&1 | grep 0.5.0 ; then echo initial installation appeared to fail exit 1 fi -git checkout -f "$BRANCH" -EXPECTED_VERSION=$(grep -m1 LE_AUTO_VERSION letsencrypt-auto | cut -d\" -f2) -if ! ./letsencrypt-auto -v --debug --version --no-self-upgrade 2>&1 | grep $EXPECTED_VERSION ; then +# Now that python and openssl have been installed, we can set up a fake server +# to provide a new version of letsencrypt-auto. First, we start the server and +# directory to be served. +MY_TEMP_DIR=$(mktemp -d) +PORT_FILE="$MY_TEMP_DIR/port" +SERVER_PATH=$(tools/readlink.py tools/simple_http_server.py) +cd "$MY_TEMP_DIR" +"$SERVER_PATH" 0 > $PORT_FILE & +SERVER_PID=$! +trap 'kill "$SERVER_PID" && rm -rf "$MY_TEMP_DIR"' EXIT +cd ~- + +# Then, we set up the files to be served. +FAKE_VERSION_NUM="99.99.99" +echo "{\"releases\": {\"$FAKE_VERSION_NUM\": null}}" > "$MY_TEMP_DIR/json" +LE_AUTO_SOURCE_DIR="$MY_TEMP_DIR/v$FAKE_VERSION_NUM" +NEW_LE_AUTO_PATH="$LE_AUTO_SOURCE_DIR/letsencrypt-auto" +mkdir "$LE_AUTO_SOURCE_DIR" +cp letsencrypt-auto-source/letsencrypt-auto "$LE_AUTO_SOURCE_DIR/letsencrypt-auto" +SIGNING_KEY="letsencrypt-auto-source/tests/signing.key" +openssl dgst -sha256 -sign "$SIGNING_KEY" -out "$NEW_LE_AUTO_PATH.sig" "$NEW_LE_AUTO_PATH" + +# Next, we wait for the server to start and get the port number. +sleep 5s +SERVER_PORT=$(sed -n 's/.*port \([0-9]\+\).*/\1/p' "$PORT_FILE") + +# Finally, we set the necessary certbot-auto environment variables. +export LE_AUTO_DIR_TEMPLATE="http://localhost:$SERVER_PORT/%s/" +export LE_AUTO_JSON_URL="http://localhost:$SERVER_PORT/json" +export LE_AUTO_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMoSzLYQ7E1sdSOkwelg +tzKIh2qi3bpXuYtcfFC0XrvWig071NwIj+dZiT0OLZ2hPispEH0B7ISuuWg1ll7G +hFW0VdbxL6JdGzS2ShNWkX9hE9z+j8VqwDPOBn3ZHm03qwpYkBDwQib3KqOdYbTT +uUtJmmGcuk3a9Aq/sCT6DdfmTSdP5asdQYwIcaQreDrOosaS84DTWI3IU+UYJVgl +LsIVPBuy9IcgHidUQ96hJnoPsDCWsHwX62495QKEarauyKQrJzFes0EY95orDM47 +Z5o/NDiQB11m91yNB0MmPYY9QSbnOA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68 +iQIDAQAB +-----END PUBLIC KEY----- +" + +if [ $(python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//') -eq 26 ]; then + RUN_PYTHON3_TESTS=1 + if command -v python3; then + echo "Didn't expect Python 3 to be installed!" + exit 1 + fi + cp letsencrypt-auto cb-auto + if ! ./cb-auto -v --debug --version 2>&1 | grep 0.5.0 ; then + echo "Certbot shouldn't have updated to a new version!" + exit 1 + fi + if [ -d "/opt/eff.org" ]; then + echo "New directory shouldn't have been created!" + exit 1 + fi + # Create a 2nd venv at the new path to ensure we properly handle this case + export VENV_PATH="/opt/eff.org/certbot/venv" + if ! sudo -E ./letsencrypt-auto -v --debug --version --no-self-upgrade 2>&1 | grep 0.5.0 ; then + echo second installation appeared to fail + exit 1 + fi + unset VENV_PATH +fi + +if ./letsencrypt-auto -v --debug --version | grep "WARNING: couldn't find Python" ; then + echo "Had problems checking for updates!" + exit 1 +fi + +EXPECTED_VERSION=$(grep -m1 LE_AUTO_VERSION certbot-auto | cut -d\" -f2) +if ! /opt/eff.org/certbot/venv/bin/letsencrypt --version 2>&1 | grep "$EXPECTED_VERSION" ; then echo upgrade appeared to fail exit 1 fi + +if ! diff letsencrypt-auto letsencrypt-auto-source/letsencrypt-auto ; then + echo letsencrypt-auto and letsencrypt-auto-source/letsencrypt-auto differ + exit 1 +fi + +if [ "$RUN_PYTHON3_TESTS" = 1 ]; then + if ! command -v python3; then + echo "Python3 wasn't properly installed" + exit 1 + fi + if [ "$(/opt/eff.org/certbot/venv/bin/python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1)" != 3 ]; then + echo "Python3 wasn't used in venv!" + exit 1 + fi +fi echo upgrade appeared to be successful if [ "$(tools/readlink.py ${XDG_DATA_HOME:-~/.local/share}/letsencrypt)" != "/opt/eff.org/certbot/venv" ]; then diff --git a/tests/letstest/scripts/test_sdists.sh b/tests/letstest/scripts/test_sdists.sh index ced21e338..f18a64065 100755 --- a/tests/letstest/scripts/test_sdists.sh +++ b/tests/letstest/scripts/test_sdists.sh @@ -7,13 +7,11 @@ PLUGINS="certbot-apache certbot-nginx" PYTHON=$(command -v python2.7 || command -v python27 || command -v python2 || command -v python) TEMP_DIR=$(mktemp -d) VERSION=$(letsencrypt-auto-source/version.py) +export VENV_ARGS="-p $PYTHON" # setup venv -virtualenv --no-site-packages -p $PYTHON --setuptools venv +tools/_venv_common.sh --requirement letsencrypt-auto-source/pieces/dependency-requirements.txt . ./venv/bin/activate -pip install -U pip -pip install -U setuptools -pip install --requirement letsencrypt-auto-source/pieces/dependency-requirements.txt # build sdists for pkg_dir in acme . $PLUGINS; do diff --git a/tests/letstest/scripts/test_tests.sh b/tests/letstest/scripts/test_tests.sh index 6d67bdf2e..e6ab836b8 100755 --- a/tests/letstest/scripts/test_tests.sh +++ b/tests/letstest/scripts/test_tests.sh @@ -1,18 +1,20 @@ #!/bin/sh -xe +LE_AUTO="letsencrypt/letsencrypt-auto-source/letsencrypt-auto" +LE_AUTO="$LE_AUTO --debug --no-self-upgrade --non-interactive" MODULES="acme certbot certbot_apache certbot_nginx" VENV_NAME=venv # *-auto respects VENV_PATH -letsencrypt/certbot-auto --debug --os-packages-only --non-interactive -LE_AUTO_SUDO="" VENV_PATH=$VENV_NAME letsencrypt/certbot-auto --debug --no-bootstrap --non-interactive --version +$LE_AUTO --os-packages-only +LE_AUTO_SUDO="" VENV_PATH="$VENV_NAME" $LE_AUTO --no-bootstrap --version . $VENV_NAME/bin/activate # change to an empty directory to ensure CWD doesn't affect tests cd $(mktemp -d) -pip install nose +pip install pytest==3.2.5 for module in $MODULES ; do echo testing $module - nosetests -v $module + pytest -v --pyargs $module done diff --git a/tests/tox-boulder-integration.sh b/tests/tox-boulder-integration.sh new file mode 100755 index 000000000..8c8a967fd --- /dev/null +++ b/tests/tox-boulder-integration.sh @@ -0,0 +1,12 @@ +#!/bin/bash -e +# A simple wrapper around tests/boulder-integration.sh that activates the tox +# virtual environment defined by the environment variable TOXENV before running +# integration tests. + +if [ -z "${TOXENV+x}" ]; then + echo "The environment variable TOXENV must be set to use this script!" >&2 + exit 1 +fi + +source .tox/$TOXENV/bin/activate +tests/boulder-integration.sh diff --git a/tests/travis-integration.sh b/tests/travis-integration.sh deleted file mode 100755 index b42617400..000000000 --- a/tests/travis-integration.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -set -o errexit - -./tests/boulder-fetch.sh - -source .tox/$TOXENV/bin/activate - -until curl http://boulder:4000/directory 2>/dev/null; do - echo waiting for boulder - sleep 1 -done - -./tests/boulder-integration.sh diff --git a/tools/_venv_common.sh b/tools/_venv_common.sh index 20ed4c034..0f0ff7e28 100755 --- a/tools/_venv_common.sh +++ b/tools/_venv_common.sh @@ -15,10 +15,10 @@ mv $VENV_NAME "$VENV_NAME.$(date +%s).bak" || true virtualenv --no-site-packages --setuptools $VENV_NAME $VENV_ARGS . ./$VENV_NAME/bin/activate -# Separately install setuptools and pip to make sure following -# invocations use latest -pip install -U pip -pip install -U setuptools +# Use pipstrap to update Python packaging tools to only update to a well tested +# version and to work around https://github.com/pypa/pip/issues/4817 on older +# systems. +python letsencrypt-auto-source/pieces/pipstrap.py ./tools/pip_install.sh "$@" set +x diff --git a/tools/deactivate.py b/tools/deactivate.py index 5facc8436..d43b84552 100644 --- a/tools/deactivate.py +++ b/tools/deactivate.py @@ -18,10 +18,10 @@ import sys from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization +import josepy as jose from acme import client as acme_client from acme import errors as acme_errors -from acme import jose from acme import messages DIRECTORY = os.getenv('DIRECTORY', 'http://localhost:4000/directory') diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt new file mode 100644 index 000000000..47440241d --- /dev/null +++ b/tools/dev_constraints.txt @@ -0,0 +1,71 @@ +# Specifies Python package versions for packages not specified in +# letsencrypt-auto's requirements file. +alabaster==0.7.10 +apipkg==1.4 +asn1crypto==0.22.0 +astroid==1.3.5 +attrs==17.3.0 +Babel==2.5.1 +backports.shutil-get-terminal-size==1.0.0 +boto3==1.4.7 +botocore==1.7.41 +cloudflare==1.5.1 +coverage==4.4.2 +decorator==4.1.2 +dns-lexicon==2.1.14 +dnspython==1.15.0 +docutils==0.14 +execnet==1.5.0 +future==0.16.0 +futures==3.1.1 +google-api-python-client==1.5 +httplib2==0.10.3 +imagesize==0.7.1 +ipdb==0.10.2 +ipython==5.5.0 +ipython-genutils==0.2.0 +Jinja2==2.9.6 +jmespath==0.9.3 +josepy==1.0.1 +logger==1.4 +logilab-common==1.4.1 +MarkupSafe==1.0 +ndg-httpsclient==0.3.2 +oauth2client==2.0.0 +pathlib2==2.3.0 +pexpect==4.2.1 +pickleshare==0.7.4 +pkginfo==1.4.1 +pluggy==0.5.2 +prompt-toolkit==1.0.15 +ptyprocess==0.5.2 +py==1.4.34 +pyasn1==0.1.9 +pyasn1-modules==0.0.10 +Pygments==2.2.0 +pylint==1.4.2 +pytest==3.2.5 +pytest-cov==2.5.1 +pytest-forked==0.2 +pytest-xdist==1.20.1 +python-dateutil==2.6.1 +python-digitalocean==1.11 +PyYAML==3.12 +repoze.sphinx.autointerface==0.8 +requests-file==1.4.2 +requests-toolbelt==0.8.0 +rsa==3.4.2 +s3transfer==0.1.11 +scandir==1.6 +simplegeneric==0.8.1 +snowballstemmer==1.2.1 +Sphinx==1.5.6 +sphinx-rtd-theme==0.2.4 +tldextract==2.2.0 +tox==2.9.1 +tqdm==4.19.4 +traitlets==4.3.2 +twine==1.9.1 +uritemplate==0.6 +virtualenv==15.1.0 +wcwidth==0.1.7 diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh index 0de2ea3f8..0d39e0594 100755 --- a/tools/install_and_test.sh +++ b/tools/install_and_test.sh @@ -2,20 +2,26 @@ # pip installs the requested packages in editable mode and runs unit tests on # them. Each package is installed and tested in the order they are provided # before the script moves on to the next package. If CERTBOT_NO_PIN is set not -# set to 1, packages are installed using certbot-auto's requirements file as -# constraints. +# set to 1, packages are installed using pinned versions of all of our +# dependencies. See pip_install.sh for more information on the versions pinned +# to. if [ "$CERTBOT_NO_PIN" = 1 ]; then - pip_install="pip install -e" + pip_install="pip install -q -e" else pip_install="$(dirname $0)/pip_install_editable.sh" fi +set -x for requirement in "$@" ; do $pip_install $requirement pkg=$(echo $requirement | cut -f1 -d\[) # remove any extras such as [dev] if [ $pkg = "." ]; then pkg="certbot" + else + # Work around a bug in pytest/importlib for the deprecated Python 3.3. + # See https://travis-ci.org/certbot/certbot/jobs/308774157#L1333. + pkg=$(echo "$pkg" | tr - _) fi - nosetests -v $pkg --processes=-1 --process-timeout=100 + "$(dirname $0)/pytest.sh" --pyargs $pkg done diff --git a/tools/merge_requirements.py b/tools/merge_requirements.py new file mode 100755 index 000000000..c8fb95351 --- /dev/null +++ b/tools/merge_requirements.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +"""Merges multiple Python requirements files into one file. + +Requirements files specified later take precedence over earlier ones. Only +simple SomeProject==1.2.3 format is currently supported. + +""" + +from __future__ import print_function + +import sys + + +def read_file(file_path): + """Reads in a Python requirements file. + + :param str file_path: path to requirements file + + :returns: mapping from a project to its pinned version + :rtype: dict + + """ + d = {} + with open(file_path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + project, version = line.split('==') + if not version: + raise ValueError("Unexpected syntax '{0}'".format(line)) + d[project] = version + return d + + +def print_requirements(requirements): + """Prints requirements to stdout. + + :param dict requirements: mapping from a project to its pinned version + + """ + print('\n'.join('{0}=={1}'.format(k, v) + for k, v in sorted(requirements.items()))) + + +def merge_requirements_files(*files): + """Merges multiple requirements files together and prints the result. + + Requirement files specified later in the list take precedence over earlier + files. + + :param tuple files: paths to requirements files + + """ + d = {} + for f in files: + d.update(read_file(f)) + print_requirements(d) + + +if __name__ == '__main__': + merge_requirements_files(*sys.argv[1:]) diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt new file mode 100644 index 000000000..de2b83ad8 --- /dev/null +++ b/tools/oldest_constraints.txt @@ -0,0 +1,51 @@ +# This file contains the oldest versions of our dependencies we say we require +# in our packages or versions we need to support to maintain compatibility with +# the versions included in the various Linux distros where we are packaged. + +# CentOS/RHEL 7 EPEL constraints +cffi==1.6.0 +chardet==2.2.1 +configobj==4.7.2 +ipaddress==1.0.16 +mock==1.0.1 +ndg-httpsclient==0.3.2 +ply==3.4 +pyasn1==0.1.9 +pycparser==2.14 +pyOpenSSL==0.13.1 +pyparsing==1.5.6 +pyRFC3339==1.0 +python-augeas==0.5.0 +six==1.9.0 +# setuptools 0.9.8 is the actual version packaged, but some other dependencies +# in this file require setuptools>=1.0 and there are no relevant changes for us +# between these versions. +setuptools==1.0.0 +urllib3==1.10.2 +zope.component==4.1.0 +zope.event==4.0.3 +zope.interface==4.0.5 + +# Debian Jessie Backports constraints +PyICU==1.8 +colorama==0.3.2 +enum34==1.0.3 +html5lib==0.999 +idna==2.0 +pbr==1.8.0 +pytz==2012rc0 + +# Our setup.py constraints +cloudflare==1.5.1 +cryptography==1.2.0 +google-api-python-client==1.5 +oauth2client==2.0 +parsedatetime==1.3 +pyparsing==1.5.5 +python-digitalocean==1.11 +requests[security]==2.4.1 + +# Ubuntu Xenial constraints +ConfigArgParse==0.10.0 +funcsigs==0.4 +zope.hookable==4.0.4 diff --git a/tools/pip_install.sh b/tools/pip_install.sh index 438e567e4..d2aae4a43 100755 --- a/tools/pip_install.sh +++ b/tools/pip_install.sh @@ -1,14 +1,26 @@ -#!/bin/sh -e -# pip installs packages using Certbot's requirements file as constraints +#!/bin/bash -e +# pip installs packages using pinned package versions. If CERTBOT_OLDEST is set +# to 1, a combination of tools/oldest_constraints.txt and +# tools/dev_constraints.txt is used, otherwise, a combination of certbot-auto's +# requirements file and tools/dev_constraints.txt is used. The other file +# always takes precedence over tools/dev_constraints.txt. # get the root of the Certbot repo -my_path=$("$(dirname $0)/readlink.py" $0) -repo_root=$(dirname $(dirname $my_path)) -requirements="$repo_root/letsencrypt-auto-source/pieces/dependency-requirements.txt" -constraints=$(mktemp) -trap "rm -f $constraints" EXIT -# extract pinned requirements without hashes -sed -n -e 's/^\([^[:space:]]*==[^[:space:]]*\).*$/\1/p' $requirements > $constraints +tools_dir=$(dirname $("$(dirname $0)/readlink.py" $0)) +dev_constraints="$tools_dir/dev_constraints.txt" +merge_reqs="$tools_dir/merge_requirements.py" +test_constraints=$(mktemp) +trap "rm -f $test_constraints" EXIT + +if [ "$CERTBOT_OLDEST" = 1 ]; then + cp "$tools_dir/oldest_constraints.txt" "$test_constraints" +else + repo_root=$(dirname "$tools_dir") + certbot_requirements="$repo_root/letsencrypt-auto-source/pieces/dependency-requirements.txt" + sed -n -e 's/^\([^[:space:]]*==[^[:space:]]*\).*$/\1/p' "$certbot_requirements" > "$test_constraints" +fi + +set -x # install the requested packages using the pinned requirements as constraints -pip install --constraint $constraints "$@" +pip install -q --constraint <("$merge_reqs" "$dev_constraints" "$test_constraints") "$@" diff --git a/tools/pytest.sh b/tools/pytest.sh new file mode 100755 index 000000000..8e3619d5d --- /dev/null +++ b/tools/pytest.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Runs pytest with the provided arguments, adding --numprocesses to the command +# line. This argument is set to "auto" if the environmnent variable TRAVIS is +# not set, otherwise, it is set to 2. This works around +# https://github.com/pytest-dev/pytest-xdist/issues/9. Currently every Travis +# environnment provides two cores. See +# https://docs.travis-ci.com/user/reference/overview/#Virtualization-environments. + +if ${TRAVIS:-false}; then + NUMPROCESSES="2" +else + NUMPROCESSES="auto" +fi + +pytest --numprocesses "$NUMPROCESSES" "$@" diff --git a/tools/release.sh b/tools/release.sh index 2a8e00aa1..a8de208b5 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -55,7 +55,7 @@ SUBPKGS="$SUBPKGS_IN_AUTO $SUBPKGS_NOT_IN_AUTO" subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs -# - it causes problems when running nosetests - the latter tries to +# - it causes problems when running pytest - the latter tries to # run everything that matches test*, while there are no unittests # there @@ -166,10 +166,10 @@ fi mkdir kgs kgs="kgs/$version" pip freeze | tee $kgs -pip install nose +pip install pytest for module in $subpkgs_modules ; do echo testing $module - nosetests $module + pytest --pyargs $module done cd ~- diff --git a/tools/simple_http_server.py b/tools/simple_http_server.py new file mode 100755 index 000000000..26bf231b7 --- /dev/null +++ b/tools/simple_http_server.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +"""A version of Python 2.x's SimpleHTTPServer that flushes its output.""" +from BaseHTTPServer import HTTPServer +from SimpleHTTPServer import SimpleHTTPRequestHandler +import sys + +def serve_forever(port=0): + """Spins up an HTTP server on all interfaces and the given port. + + A message is printed to stdout specifying the address and port being used + by the server. + + :param int port: port number to use. + + """ + server = HTTPServer(('', port), SimpleHTTPRequestHandler) + print 'Serving HTTP on {0} port {1} ...'.format(*server.server_address) + sys.stdout.flush() + server.serve_forever() + + +if __name__ == '__main__': + kwargs = {} + if len(sys.argv) > 1: + kwargs['port'] = int(sys.argv[1]) + serve_forever(**kwargs) diff --git a/tox.cover.sh b/tox.cover.sh index 4f6ea2dab..2908d1f9a 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -40,7 +40,7 @@ cover () { elif [ "$1" = "certbot_dns_rfc2136" ]; then min=99 elif [ "$1" = "certbot_dns_route53" ]; then - min=99 + min=92 elif [ "$1" = "certbot_nginx" ]; then min=97 elif [ "$1" = "certbot_postfix" ]; then @@ -52,17 +52,13 @@ cover () { exit 1 fi - # "-c /dev/null" makes sure setup.cfg is not loaded (multiple - # --with-cover add up, --cover-erase must not be set for coveralls - # to get all the data); --with-cover scopes coverage to only - # specific package, positional argument scopes tests only to - # specific package directory; --cover-tests makes sure every tests - # is run (c.f. #403) - nosetests -c /dev/null --with-cover --cover-tests --cover-package \ - "$1" --cover-min-percentage="$min" "$1" + pkg_dir=$(echo "$1" | tr _ -) + pytest="$(dirname $0)/tools/pytest.sh" + "$pytest" --cov "$pkg_dir" --cov-append --cov-report= --pyargs "$1" + coverage report --fail-under="$min" --include="$pkg_dir/*" --show-missing } -rm -f .coverage # --cover-erase is off, make sure stats are correct +rm -f .coverage # --cov-append is on, make sure stats are correct for pkg in $pkgs do cover $pkg diff --git a/tox.ini b/tox.ini index 87eb77338..febb6f5ff 100644 --- a/tox.ini +++ b/tox.ini @@ -6,17 +6,13 @@ skipsdist = true envlist = modification,py{26,33,34,35,36},cover,lint -# nosetest -v => more verbose output, allows to detect busy waiting -# loops, especially on Travis - [base] # pip installs the requested packages in editable mode pip_install = {toxinidir}/tools/pip_install_editable.sh # pip installs the requested packages in editable mode and runs unit tests on # them. Each package is installed and tested in the order they are provided -# before the script moves on to the next package. If CERTBOT_NO_PIN is set not -# set to 1, packages are installed using certbot-auto's requirements file as -# constraints. +# before the script moves on to the next package. All dependencies are pinned +# to a specific version for increased stability for developers. install_and_test = {toxinidir}/tools/install_and_test.sh py26_packages = acme[dev] \ @@ -64,6 +60,10 @@ source_paths = commands = {[base]install_and_test} {[base]py26_packages} python tests/lock_test.py +deps = + setuptools==36.8.0 + wheel==0.29.0 +passenv = TRAVIS [testenv] commands = @@ -72,41 +72,25 @@ commands = setenv = PYTHONPATH = {toxinidir} PYTHONHASHSEED = 0 +passenv = + {[testenv:py26]passenv} + +[testenv:py33] +commands = + {[testenv]commands} +deps = + wheel==0.29.0 +passenv = + {[testenv]passenv} [testenv:py27-oldest] commands = {[testenv]commands} setenv = {[testenv]setenv} - CERTBOT_NO_PIN=1 -deps = - PyOpenSSL==0.13 - cffi==1.5.2 - configargparse==0.10.0 - configargparse==0.10.0 - configobj==4.7.2 - cryptography==1.2.3 - enum34==0.9.23 - google-api-python-client==1.5 - idna==2.0 - ipaddress==1.0.16 - mock==1.0.1 - ndg-httpsclient==0.3.2 - oauth2client==2.0 - parsedatetime==1.4 - pyasn1-modules==0.0.5 - pyasn1==0.1.9 - pyparsing==1.5.6 - pyrfc3339==1.0 - python-augeas==0.4.1 - pytz==2012c - requests[security]==2.6.0 - setuptools==0.9.8 - six==1.9.0 - urllib3==1.10 - zope.component==4.0.2 - zope.event==4.0.1 - zope.interface==4.0.5 + CERTBOT_OLDEST=1 +passenv = + {[testenv]passenv} [testenv:py27_install] basepython = python2.7 @@ -118,6 +102,8 @@ basepython = python2.7 commands = {[base]install_packages} ./tox.cover.sh +passenv = + {[testenv]passenv} [testenv:lint] # recent versions of pylint do not support Python 2.6 (#97, #187)