diff --git a/.travis.yml b/.travis.yml index 96e28b1b0..8dde06ceb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,8 +60,7 @@ addons: - rsyslog install: "travis_retry pip install tox coveralls" -before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh' -script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))' +script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)' after_success: '[ "$TOXENV" == "cover" ] && coveralls' diff --git a/LICENSE.txt b/LICENSE.txt index 2ed752521..5965ec2ef 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -2,7 +2,7 @@ Let's Encrypt Python Client Copyright (c) Electronic Frontier Foundation and others Licensed Apache Version 2.0 -Incorporating code from nginxparser +The nginx plugin incorporates code from nginxparser Copyright (c) 2014 Fatih Erikli Licensed MIT diff --git a/README.rst b/README.rst index ce0d1b686..f38b09cfd 100644 --- a/README.rst +++ b/README.rst @@ -3,12 +3,9 @@ Disclaimer ========== -This is a **DEVELOPER PREVIEW** intended for developers and testers only. - -**DO NOT RUN THIS CODE ON A PRODUCTION SERVER. IT WILL INSTALL CERTIFICATES -SIGNED BY A TEST CA, AND WILL CAUSE CERT WARNINGS FOR USERS.** - -Browser-trusted certificates will be available in the coming months. +The Let's Encrypt Client is **BETA SOFTWARE**. It contains plenty of bugs and +rough edges, and should be tested thoroughly in staging environments before use +on production systems. For more information regarding the status of the project, please see https://letsencrypt.org. Be sure to checkout the @@ -17,38 +14,87 @@ https://letsencrypt.org. Be sure to checkout the About the Let's Encrypt Client ============================== +The Let's Encrypt Client is a fully-featured, extensible client for the Let's +Encrypt CA (or any other CA that speaks the `ACME +`_ +protocol) that can automate the tasks of obtaining certificates and +configuring webservers to use them. + +Installation +------------ + +If ``letsencrypt`` is packaged for your OS, you can install it from there, and +run it by typing ``letsencrypt``. Because not all operating systems have +packages yet, we provide a temporary solution via the ``letsencrypt-auto`` +wrapper script, which obtains some dependencies from your OS and puts others +in an python virtual environment:: + + user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt + user@webserver:~$ cd letsencrypt + user@webserver:~/letsencrypt$ ./letsencrypt-auto --help + +Or for full command line help, type:: + + ./letsencrypt-auto --help all + +``letsencrypt-auto`` updates to the latest client release automatically. And +since ``letsencrypt-auto`` is a wrapper to ``letsencrypt``, it accepts exactly +the same command line flags and arguments. More details about this script and +other installation methods can be found `in the User Guide +`_. + +How to run the client +--------------------- + +In many cases, you can just run ``letsencrypt-auto`` or ``letsencrypt``, and the +client will guide you through the process of obtaining and installing certs +interactively. + +You can also tell it exactly what you want it to do from the command line. +For instance, if you want to obtain a cert for ``thing.com``, +``www.thing.com``, and ``otherthing.net``, using the Apache plugin to both +obtain and install the certs, you could do this:: + + ./letsencrypt-auto --apache -d thing.com -d www.thing.com -d otherthing.net + +(The first time you run the command, it will make an account, and ask for an +email and agreement to the Let's Encrypt Subscriber Agreement; you can +automate those with ``--email`` and ``--agree-tos``) + +If you want to use a webserver that doesn't have full plugin support yet, you +can still use "standalone" or "webroot" plugins to obtain a certificate:: + + ./letsencrypt-auto certonly --standalone --email admin@thing.com -d thing.com -d www.thing.com -d otherthing.net + + +Understanding the client in more depth +-------------------------------------- + +To understand what the client is doing in detail, it's important to +understand the way it uses plugins. Please see the `explanation of +plugins `_ in +the User Guide. + +Links +===== + +Documentation: https://letsencrypt.readthedocs.org + +Software project: https://github.com/letsencrypt/letsencrypt + +Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html + +Main Website: https://letsencrypt.org/ + +IRC Channel: #letsencrypt on `Freenode`_ + +Community: https://community.letsencrypt.org + +Mailing list: `client-dev`_ (to subscribe without a Google account, send an +email to client-dev+subscribe@letsencrypt.org) + |build-status| |coverage| |docs| |container| -In short: getting and installing SSL/TLS certificates made easy (`watch demo video`_). - -The Let's Encrypt Client is a tool to automatically receive and install -X.509 certificates to enable TLS on servers. The client will -interoperate with the Let's Encrypt CA which will be issuing browser-trusted -certificates for free. - -It's all automated: - -* The tool will prove domain control to the CA and submit a CSR (Certificate - Signing Request). -* If domain control has been proven, a certificate will get issued and the tool - will automatically install it. - -All you need to do to sign a single domain is:: - - user@www:~$ sudo letsencrypt -d www.example.org certonly - -For multiple domains (SAN) use:: - - user@www:~$ sudo letsencrypt -d www.example.org -d example.org certonly - -and if you have a compatible web server (Apache or Nginx), Let's Encrypt can -not only get a new certificate, but also deploy it and configure your -server automatically!:: - - user@www:~$ sudo letsencrypt -d www.example.org run - - -**Encrypt ALL the things!** .. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master @@ -74,16 +120,18 @@ server automatically!:: Current Features ----------------- +================ * Supports multiple web servers: - - apache/2.x (tested and working on Ubuntu Linux) - - nginx/0.8.48+ (under development) + - apache/2.x (working on Debian 8+ and Ubuntu 12.04+) - standalone (runs its own simple webserver to prove you control a domain) + - webroot (adds files to webroot directories in order to prove control of + domains and obtain certs) + - nginx/0.8.48+ (highly experimental, not included in letsencrypt-auto) * The private key is generated locally on your system. -* Can talk to the Let's Encrypt (demo) CA or optionally to other ACME +* Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. @@ -92,34 +140,10 @@ Current Features runs https only (Apache only) * Fully automated. * Configuration changes are logged and can be reverted. -* Text and ncurses UI. +* Supports ncurses and text (-t) UI, or can be driven entirely from the + command line. * Free and Open Source Software, made with Python. -Installation Instructions -------------------------- - -Official **documentation**, including `installation instructions`_, is -available at https://letsencrypt.readthedocs.org. - - -Links ------ - -Documentation: https://letsencrypt.readthedocs.org - -Software project: https://github.com/letsencrypt/letsencrypt - -Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html - -Main Website: https://letsencrypt.org/ - -IRC Channel: #letsencrypt on `Freenode`_ - -Community: https://community.letsencrypt.org - -Mailing list: `client-dev`_ (to subscribe without a Google account, send an -email to client-dev+subscribe@letsencrypt.org) - .. _Freenode: https://freenode.net .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 976d7ab12..1e456d325 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -228,6 +228,9 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): """ + WHITESPACE_CUTSET = "\n\r\t " + """Whitespace characters which should be ignored at the end of the body.""" + def simple_verify(self, chall, domain, account_public_key, port=None): """Simple verify. @@ -266,17 +269,11 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): logger.debug("Received %s: %s. Headers: %s", http_response, http_response.text, http_response.headers) - found_ct = http_response.headers.get( - "Content-Type", chall.CONTENT_TYPE) - if found_ct != chall.CONTENT_TYPE: - logger.debug("Wrong Content-Type: found %r, expected %r", - found_ct, chall.CONTENT_TYPE) - return False - - if self.key_authorization != http_response.text: + challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET) + if self.key_authorization != challenge_response: logger.debug("Key authorization from response (%r) doesn't match " "HTTP response (%r)", self.key_authorization, - http_response.text) + challenge_response) return False return True @@ -288,9 +285,6 @@ class HTTP01(KeyAuthorizationChallenge): response_cls = HTTP01Response typ = response_cls.typ - CONTENT_TYPE = "text/plain" - """Only valid value for Content-Type if the header is included.""" - URI_ROOT_PATH = ".well-known/acme-challenge" """URI root path for the server provisioned resource.""" diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index c4f3d6c61..a4e78ebe9 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -92,7 +92,6 @@ class HTTP01ResponseTest(unittest.TestCase): from acme.challenges import HTTP01 self.chall = HTTP01(token=(b'x' * 16)) self.response = self.chall.response(KEY) - self.good_headers = {'Content-Type': HTTP01.CONTENT_TYPE} def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -113,24 +112,26 @@ class HTTP01ResponseTest(unittest.TestCase): @mock.patch("acme.challenges.requests.get") def test_simple_verify_good_validation(self, mock_get): validation = self.chall.validation(KEY) - mock_get.return_value = mock.MagicMock( - text=validation, headers=self.good_headers) + mock_get.return_value = mock.MagicMock(text=validation) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_get.assert_called_once_with(self.chall.uri("local")) @mock.patch("acme.challenges.requests.get") def test_simple_verify_bad_validation(self, mock_get): - mock_get.return_value = mock.MagicMock( - text="!", headers=self.good_headers) + mock_get.return_value = mock.MagicMock(text="!") self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) @mock.patch("acme.challenges.requests.get") - def test_simple_verify_bad_content_type(self, mock_get): - mock_get().text = self.chall.token - self.assertFalse(self.response.simple_verify( + def test_simple_verify_whitespace_validation(self, mock_get): + from acme.challenges import HTTP01Response + mock_get.return_value = mock.MagicMock( + text=(self.chall.validation(KEY) + + HTTP01Response.WHITESPACE_CUTSET)) + self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) + mock_get.assert_called_once_with(self.chall.uri("local")) @mock.patch("acme.challenges.requests.get") def test_simple_verify_connection_error(self, mock_get): diff --git a/acme/acme/client.py b/acme/acme/client.py index 0e9319f9c..08d476783 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -246,9 +246,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes def retry_after(cls, response, default): """Compute next `poll` time based on response ``Retry-After`` header. - :param response: Response from `poll`. - :type response: `requests.Response` - + :param requests.Response response: Response from `poll`. :param int default: Default value (in seconds), used when ``Retry-After`` header is not present or invalid. @@ -323,22 +321,21 @@ class Client(object): # pylint: disable=too-many-instance-attributes body=jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content))) - def poll_and_request_issuance(self, csr, authzrs, mintime=5): + def poll_and_request_issuance( + self, csr, authzrs, mintime=5, max_attempts=10): """Poll and request issuance. This function polls all provided Authorization Resource URIs until all challenges are valid, respecting ``Retry-After`` HTTP headers, and then calls `request_issuance`. - .. todo:: add `max_attempts` or `timeout` - - :param csr: CSR. - :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - + :param .ComparableX509 csr: CSR (`OpenSSL.crypto.X509Req` + wrapped in `.ComparableX509`) :param authzrs: `list` of `.AuthorizationResource` - :param int mintime: Minimum time before next attempt, used if ``Retry-After`` is not present in the response. + :param int max_attempts: Maximum number of attempts before + `PollError` with non-empty ``waiting`` is raised. :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is the issued certificate (`.messages.CertificateResource.), @@ -348,6 +345,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes as the input ``authzrs``. :rtype: `tuple` + :raises PollError: in case of timeout or if some authorization + was marked by the CA as invalid + """ # priority queue with datetime (based on Retry-After) as key, # and original Authorization Resource as value @@ -356,7 +356,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes # recently updated one updated = dict((authzr, authzr) for authzr in authzrs) - while waiting: + while waiting and max_attempts: + max_attempts -= 1 # find the smallest Retry-After, and sleep if necessary when, authzr = heapq.heappop(waiting) now = datetime.datetime.now() @@ -371,11 +372,16 @@ class Client(object): # pylint: disable=too-many-instance-attributes updated[authzr] = updated_authzr # pylint: disable=no-member - if updated_authzr.body.status != messages.STATUS_VALID: + if updated_authzr.body.status not in ( + messages.STATUS_VALID, messages.STATUS_INVALID): # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( response, default=mintime), authzr)) + if not max_attempts or any(authzr.body.status == messages.STATUS_INVALID + for authzr in six.itervalues(updated)): + raise errors.PollError(waiting, updated) + updated_authzrs = tuple(updated[authzr] for authzr in authzrs) return self.request_issuance(csr, updated_authzrs), updated_authzrs diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 2df7b5313..58f55b293 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -271,9 +271,9 @@ class ClientTest(unittest.TestCase): # result, increment clock clock.dt += datetime.timedelta(seconds=2) - if not authzr.retries: # no more retries + if len(authzr.retries) == 1: # no more retries done = mock.MagicMock(uri=authzr.uri, times=authzr.times) - done.body.status = messages.STATUS_VALID + done.body.status = authzr.retries[0] return done, [] # response (2nd result tuple element) is reduced to only @@ -289,7 +289,8 @@ class ClientTest(unittest.TestCase): mintime = 7 - def retry_after(response, default): # pylint: disable=missing-docstring + def retry_after(response, default): + # pylint: disable=missing-docstring # check that poll_and_request_issuance correctly passes mintime self.assertEqual(default, mintime) return clock.dt + datetime.timedelta(seconds=response) @@ -302,8 +303,10 @@ class ClientTest(unittest.TestCase): csr = mock.MagicMock() authzrs = ( - mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)), - mock.MagicMock(uri='b', times=[], retries=(5,)), + mock.MagicMock(uri='a', times=[], retries=( + 8, 20, 30, messages.STATUS_VALID)), + mock.MagicMock(uri='b', times=[], retries=( + 5, messages.STATUS_VALID)), ) cert, updated_authzrs = self.client.poll_and_request_issuance( @@ -327,6 +330,17 @@ class ClientTest(unittest.TestCase): ]) self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) + # CA sets invalid | TODO: move to a separate test + invalid_authzr = mock.MagicMock(times=[], retries=[messages.STATUS_INVALID]) + self.assertRaises( + errors.PollError, self.client.poll_and_request_issuance, + csr, authzrs=(invalid_authzr,), mintime=mintime) + + # exceeded max_attemps | TODO: move to a separate test + self.assertRaises( + errors.PollError, self.client.poll_and_request_issuance, + csr, authzrs, mintime=mintime, max_attempts=2) + def test_check_cert(self): self.response.headers['Location'] = self.certr.uri self.response.content = CERT_DER diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 9a96ec43a..0385667c7 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -51,3 +51,31 @@ class MissingNonce(NonceError): return ('Server {0} response did not include a replay ' 'nonce, headers: {1}'.format( self.response.request.method, self.response.headers)) + + +class PollError(ClientError): + """Generic error when polling for authorization fails. + + This might be caused by either timeout (`waiting` will be non-empty) + or by some authorization being invalid. + + :ivar waiting: Priority queue with `datetime.datatime` (based on + ``Retry-After``) as key, and original `.AuthorizationResource` + as value. + :ivar updated: Mapping from original `.AuthorizationResource` + to the most recently updated one + + """ + def __init__(self, waiting, updated): + self.waiting = waiting + self.updated = updated + super(PollError, self).__init__() + + @property + def timeout(self): + """Was the error caused by timeout?""" + return bool(self.waiting) + + def __repr__(self): + return '{0}(waiting={1!r}, updated={2!r})'.format( + self.__class__.__name__, self.waiting, self.updated) diff --git a/acme/acme/errors_test.py b/acme/acme/errors_test.py index 3790d91ed..45b269a0b 100644 --- a/acme/acme/errors_test.py +++ b/acme/acme/errors_test.py @@ -1,4 +1,5 @@ """Tests for acme.errors.""" +import datetime import unittest import mock @@ -29,5 +30,25 @@ class MissingNonceTest(unittest.TestCase): self.assertTrue("{}" in str(self.error)) +class PollErrorTest(unittest.TestCase): + """Tests for acme.errors.PollError.""" + + def setUp(self): + from acme.errors import PollError + self.timeout = PollError( + waiting=[(datetime.datetime(2015, 11, 29), mock.sentinel.AR)], + updated={}) + self.invalid = PollError(waiting=[], updated={ + mock.sentinel.AR: mock.sentinel.AR2}) + + def test_timeout(self): + self.assertTrue(self.timeout.timeout) + self.assertFalse(self.invalid.timeout) + + def test_repr(self): + self.assertEqual('PollError(waiting=[], updated={sentinel.AR: ' + 'sentinel.AR2})', repr(self.invalid)) + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 9d4dcbf30..0b9ea8105 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -2,12 +2,13 @@ import collections from acme import challenges +from acme import errors from acme import fields from acme import jose from acme import util -class Error(jose.JSONObjectWithFields, Exception): +class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 @@ -17,55 +18,40 @@ class Error(jose.JSONObjectWithFields, Exception): :ivar unicode detail: """ - ERROR_TYPE_NAMESPACE = 'urn:acme:error:' - ERROR_TYPE_DESCRIPTIONS = { - 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', - 'badNonce': 'The client sent an unacceptable anti-replay nonce', - 'connection': 'The server could not connect to the client for DV', - 'dnssec': 'The server could not validate a DNSSEC signed domain', - 'malformed': 'The request message was malformed', - 'rateLimited': 'There were too many requests of a given type', - 'serverInternal': 'The server experienced an internal error', - 'tls': 'The server experienced a TLS error during DV', - 'unauthorized': 'The client lacks sufficient authorization', - 'unknownHost': 'The server could not resolve a domain name', - } + ERROR_TYPE_DESCRIPTIONS = dict( + ('urn:acme:error:' + name, description) for name, description in ( + ('badCSR', 'The CSR is unacceptable (e.g., due to a short key)'), + ('badNonce', 'The client sent an unacceptable anti-replay nonce'), + ('connection', 'The server could not connect to the client for DV'), + ('dnssec', 'The server could not validate a DNSSEC signed domain'), + ('malformed', 'The request message was malformed'), + ('rateLimited', 'There were too many requests of a given type'), + ('serverInternal', 'The server experienced an internal error'), + ('tls', 'The server experienced a TLS error during DV'), + ('unauthorized', 'The client lacks sufficient authorization'), + ('unknownHost', 'The server could not resolve a domain name'), + ) + ) typ = jose.Field('type') title = jose.Field('title', omitempty=True) detail = jose.Field('detail') - @typ.encoder - def typ(value): # pylint: disable=missing-docstring,no-self-argument - return Error.ERROR_TYPE_NAMESPACE + value - - @typ.decoder - def typ(value): # pylint: disable=missing-docstring,no-self-argument - # pylint thinks isinstance(value, Error), so startswith is not found - # pylint: disable=no-member - if not value.startswith(Error.ERROR_TYPE_NAMESPACE): - raise jose.DeserializationError('Missing error type prefix') - - without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):] - if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS: - raise jose.DeserializationError('Error type not recognized') - - return without_prefix - @property def description(self): """Hardcoded error description based on its type. + :returns: Description if standard ACME error or ``None``. :rtype: unicode """ - return self.ERROR_TYPE_DESCRIPTIONS[self.typ] + return self.ERROR_TYPE_DESCRIPTIONS.get(self.typ) def __str__(self): - if self.typ is not None: - return ' :: '.join([self.typ, self.description, self.detail]) - else: - return str(self.detail) + return ' :: '.join( + part for part in + (self.typ, self.description, self.detail, self.title) + if part is not None) class _Constant(jose.JSONDeSerializable, collections.Hashable): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 6c1c4f596..5a7a71299 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -18,41 +18,30 @@ class ErrorTest(unittest.TestCase): def setUp(self): from acme.messages import Error - self.error = Error(detail='foo', typ='malformed', title='title') - self.jobj = {'detail': 'foo', 'title': 'some title'} - - def test_typ_prefix(self): - self.assertEqual('malformed', self.error.typ) - self.assertEqual( - 'urn:acme:error:malformed', self.error.to_partial_json()['type']) - self.assertEqual( - 'malformed', self.error.from_json(self.error.to_partial_json()).typ) - - def test_typ_decoder_missing_prefix(self): - from acme.messages import Error - self.jobj['type'] = 'malformed' - self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) - self.jobj['type'] = 'not valid bare type' - self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) - - def test_typ_decoder_not_recognized(self): - from acme.messages import Error - self.jobj['type'] = 'urn:acme:error:baz' - self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) - - def test_description(self): - self.assertEqual( - 'The request message was malformed', self.error.description) + self.error = Error( + detail='foo', typ='urn:acme:error:malformed', title='title') + self.jobj = { + 'detail': 'foo', + 'title': 'some title', + 'type': 'urn:acme:error:malformed', + } + self.error_custom = Error(typ='custom', detail='bar') + self.jobj_cusom = {'type': 'custom', 'detail': 'bar'} def test_from_json_hashable(self): from acme.messages import Error hash(Error.from_json(self.error.to_json())) + def test_description(self): + self.assertEqual( + 'The request message was malformed', self.error.description) + self.assertTrue(self.error_custom.description is None) + def test_str(self): self.assertEqual( - 'malformed :: The request message was malformed :: foo', - str(self.error)) - self.assertEqual('foo', str(self.error.update(typ=None))) + 'urn:acme:error:malformed :: The request message was ' + 'malformed :: foo :: title', str(self.error)) + self.assertEqual('custom :: bar', str(self.error_custom)) class ConstantTest(unittest.TestCase): @@ -232,7 +221,7 @@ class ChallengeBodyTest(unittest.TestCase): from acme.messages import Error from acme.messages import STATUS_INVALID self.status = STATUS_INVALID - error = Error(typ='serverInternal', + error = Error(typ='urn:acme:error:serverInternal', detail='Unable to communicate with DNS server') self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status, diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 3ddb21beb..02cc2daf5 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -133,7 +133,6 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.log_message("Serving HTTP01 with token %r", resource.chall.encode("token")) self.send_response(http_client.OK) - self.send_header("Content-type", resource.chall.CONTENT_TYPE) self.end_headers() self.wfile.write(resource.validation.encode()) return diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh index 5aca13cd4..b975da444 100755 --- a/bootstrap/_rpm_common.sh +++ b/bootstrap/_rpm_common.sh @@ -4,12 +4,13 @@ # - Fedora 22, 23 (x64) # - Centos 7 (x64: onD igitalOcean droplet) -if type yum 2>/dev/null -then - tool=yum -elif type dnf 2>/dev/null +if type dnf 2>/dev/null then tool=dnf +elif type yum 2>/dev/null +then + tool=yum + else echo "Neither yum nor dnf found. Aborting bootstrap!" exit 1 @@ -40,6 +41,7 @@ if ! $tool install -y \ augeas-libs \ openssl-devel \ libffi-devel \ + redhat-rpm-config \ ca-certificates then echo "Could not install additional dependencies. Aborting bootstrap!" diff --git a/bootstrap/_suse_common.sh b/bootstrap/_suse_common.sh index 4b41bac36..46f9d693b 100755 --- a/bootstrap/_suse_common.sh +++ b/bootstrap/_suse_common.sh @@ -1,6 +1,6 @@ #!/bin/sh -# SLE12 dont have python-virtualenv +# SLE12 don't have python-virtualenv zypper -nq in -l git-core \ python \ diff --git a/docs/ciphers.rst b/docs/ciphers.rst index 12c403d09..49c0824a3 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -105,7 +105,7 @@ https://wiki.mozilla.org/Security/Server_Side_TLS and the version implemented by the Let's Encrypt client will be the version that was most current as of the release date of each client -version. Mozilla offers three seperate sets of cryptographic options, +version. Mozilla offers three separate sets of cryptographic options, which trade off security and compatibility differently. These are referred to as as the "Modern", "Intermediate", and "Old" configurations (in order from most secure to least secure, and least-backwards compatible diff --git a/docs/using.rst b/docs/using.rst index f6fb82f52..d6ae2c5ee 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -108,8 +108,7 @@ Operating System Packages .. code-block:: shell - sudo pacman -S letsencrypt letsencrypt-nginx letsencrypt-apache \ - letshelp-letsencrypt + sudo pacman -S letsencrypt letsencrypt-apache **Other Operating Systems** diff --git a/letsencrypt-apache/docs/api/dvsni.rst b/letsencrypt-apache/docs/api/dvsni.rst deleted file mode 100644 index 945771db8..000000000 --- a/letsencrypt-apache/docs/api/dvsni.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt_apache.dvsni` -------------------------------- - -.. automodule:: letsencrypt_apache.dvsni - :members: diff --git a/letsencrypt-apache/docs/api/tls_sni_01.rst b/letsencrypt-apache/docs/api/tls_sni_01.rst new file mode 100644 index 000000000..2c11a3394 --- /dev/null +++ b/letsencrypt-apache/docs/api/tls_sni_01.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt_apache.tls_sni_01` +------------------------------------ + +.. automodule:: letsencrypt_apache.tls_sni_01 + :members: diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug b/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug index 9b50a8f0e..30d8ca501 100644 --- a/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug +++ b/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug @@ -59,7 +59,7 @@ let empty = Util.empty_dos let indent = Util.indent (* borrowed from shellvars.aug *) -let char_arg_dir = /[^\\ '"\t\r\n]|\\\\"|\\\\'/ +let char_arg_dir = /([^\\ '"\t\r\n]|[^\\ '"\t\r\n][^ '"\t\r\n]*[^\\ '"\t\r\n])|\\\\"|\\\\'/ let char_arg_sec = /[^ '"\t\r\n>]|\\\\"|\\\\'/ let cdot = /\\\\./ let cl = /\\\\\n/ diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index f10f0c241..98b0b8820 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -7,7 +7,7 @@ import os import re import shutil import socket -import subprocess +import time import zope.interface @@ -22,7 +22,7 @@ from letsencrypt.plugins import common from letsencrypt_apache import augeas_configurator from letsencrypt_apache import constants from letsencrypt_apache import display_ops -from letsencrypt_apache import dvsni +from letsencrypt_apache import tls_sni_01 from letsencrypt_apache import obj from letsencrypt_apache import parser @@ -94,13 +94,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): help="Path to the Apache 'a2enmod' binary.") add("dismod", default=constants.CLI_DEFAULTS["dismod"], help="Path to the Apache 'a2enmod' binary.") - add("init-script", default=constants.CLI_DEFAULTS["init_script"], - help="Path to the Apache init script (used for server " - "reload/restart).") add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"], help="SSL vhost configuration extension.") add("server-root", default=constants.CLI_DEFAULTS["server_root"], help="Apache server root directory.") + le_util.add_deprecated_argument(add, "init-script", 1) def __init__(self, *args, **kwargs): """Initialize an Apache Configurator. @@ -121,7 +119,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser = None self.version = version self.vhosts = None - self._enhance_func = {"redirect": self._enable_redirect} + self._enhance_func = {"redirect": self._enable_redirect, + "ensure-http-header": self._set_http_header} @property def mod_ssl_conf(self): @@ -138,8 +137,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ # Verify Apache is installed - for exe in (self.conf("ctl"), self.conf("enmod"), - self.conf("dismod"), self.conf("init-script")): + for exe in (self.conf("ctl"), self.conf("enmod"), self.conf("dismod")): if not le_util.exe_exists(exe): raise errors.NoInstallationError @@ -182,17 +180,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ vhost = self.choose_vhost(domain) + self._clean_vhost(vhost) # This is done first so that ssl module is enabled and cert_path, # cert_key... can all be parsed appropriately self.prepare_server_https("443") - path = {} - - path["cert_path"] = self.parser.find_dir( - "SSLCertificateFile", None, vhost.path) - path["cert_key"] = self.parser.find_dir( - "SSLCertificateKeyFile", None, vhost.path) + path = {"cert_path": self.parser.find_dir("SSLCertificateFile", None, vhost.path), + "cert_key": self.parser.find_dir("SSLCertificateKeyFile", None, vhost.path)} # Only include if a certificate chain is specified if chain_path is not None: @@ -209,16 +204,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "Unable to find cert and/or key directives") logger.info("Deploying Certificate to VirtualHost %s", vhost.filep) + logger.debug("Apache version is %s", + ".".join(str(i) for i in self.version)) - # Assign the final directives; order is maintained in find_dir - self.aug.set(path["cert_path"][-1], cert_path) - self.aug.set(path["cert_key"][-1], key_path) - if chain_path is not None: - if not path["chain_path"]: - self.parser.add_dir( - vhost.path, "SSLCertificateChainFile", chain_path) + if self.version < (2, 4, 8) or (chain_path and not fullchain_path): + # install SSLCertificateFile, SSLCertificateKeyFile, + # and SSLCertificateChainFile directives + set_cert_path = cert_path + self.aug.set(path["cert_path"][-1], cert_path) + self.aug.set(path["cert_key"][-1], key_path) + if chain_path is not None: + self.parser.add_dir(vhost.path, + "SSLCertificateChainFile", chain_path) else: - self.aug.set(path["chain_path"][-1], chain_path) + raise errors.PluginError("--chain-path is required for your version of Apache") + else: + if not fullchain_path: + raise errors.PluginError("Please provide the --fullchain-path\ + option pointing to your full chain file") + set_cert_path = fullchain_path + self.aug.set(path["cert_path"][-1], fullchain_path) + self.aug.set(path["cert_key"][-1], key_path) # Save notes about the transaction that took place self.save_notes += ("Changed vhost at %s with addresses of %s\n" @@ -226,7 +232,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "\tSSLCertificateKeyFile %s\n" % (vhost.filep, ", ".join(str(addr) for addr in vhost.addrs), - cert_path, key_path)) + set_cert_path, key_path)) if chain_path is not None: self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path @@ -234,13 +240,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not vhost.enabled: self.enable_site(vhost) - def choose_vhost(self, target_name): + def choose_vhost(self, target_name, temp=False): """Chooses a virtual host based on the given domain name. If there is no clear virtual host to be selected, the user is prompted with all available choices. + The returned vhost is guaranteed to have TLS enabled unless temp is + True. If temp is True, there is no such guarantee and the result is + not cached. + :param str target_name: domain name + :param bool temp: whether the vhost is only used temporarily :returns: ssl vhost associated with name :rtype: :class:`~letsencrypt_apache.obj.VirtualHost` @@ -255,15 +266,17 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Try to find a reasonable vhost vhost = self._find_best_vhost(target_name) if vhost is not None: + if temp: + return vhost if not vhost.ssl: vhost = self.make_vhost_ssl(vhost) self.assoc[target_name] = vhost return vhost - return self._choose_vhost_from_list(target_name) + return self._choose_vhost_from_list(target_name, temp) - def _choose_vhost_from_list(self, target_name): + def _choose_vhost_from_list(self, target_name, temp=False): # Select a vhost from a list vhost = display_ops.select_vhost(target_name, self.vhosts) if vhost is None: @@ -272,7 +285,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "No vhost was selected. Please specify servernames " "in the Apache config", target_name) raise errors.PluginError("No vhost selected") - + elif temp: + return vhost elif not vhost.ssl: addrs = self._get_proposed_addrs(vhost, "443") # TODO: Conflicts is too conservative @@ -437,6 +451,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if self.parser.find_dir("SSLEngine", "on", start=path, exclude=False): is_ssl = True + # "SSLEngine on" might be set outside of + # Treat vhosts with port 443 as ssl vhosts + for addr in addrs: + if addr.get_port() == "443": + is_ssl = True + filename = get_file_path(path) is_enabled = self.is_site_enabled(filename) @@ -590,7 +610,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): (ssl_fp, parser.case_i("VirtualHost"))) if len(vh_p) != 1: logger.error("Error: should only be one vhost in %s", avail_fp) - raise errors.PluginError("Only one vhost per file is allowed") + raise errors.PluginError("Currently, we only support " + "configurations with one vhost per file") else: # This simplifies the process vh_p = vh_p[0] @@ -666,6 +687,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return ssl_addrs + def _clean_vhost(self, vhost): + # remove duplicated or conflicting ssl directives + self._deduplicate_directives(vhost.path, + ["SSLCertificateFile", "SSLCertificateKeyFile"]) + # remove all problematic directives + self._remove_directives(vhost.path, ["SSLCertificateChainFile"]) + + def _deduplicate_directives(self, vh_path, directives): + for directive in directives: + while len(self.parser.find_dir(directive, None, vh_path, False)) > 1: + directive_path = self.parser.find_dir(directive, None, vh_path, False) + self.aug.remove(re.sub(r"/\w*$", "", directive_path[0])) + + def _remove_directives(self, vh_path, directives): + for directive in directives: + while len(self.parser.find_dir(directive, None, vh_path, False)) > 0: + directive_path = self.parser.find_dir(directive, None, vh_path, False) + self.aug.remove(re.sub(r"/\w*$", "", directive_path[0])) + def _add_dummy_ssl_directives(self, vh_path): self.parser.add_dir(vh_path, "SSLCertificateFile", "insert_cert_file_path") @@ -704,7 +744,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ############################################################################ def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" - return ["redirect"] + return ["redirect", "ensure-http-header"] def enhance(self, domain, enhancement, options=None): """Enhance configuration. @@ -731,6 +771,73 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): logger.warn("Failed %s for %s", enhancement, domain) raise + def _set_http_header(self, ssl_vhost, header_substring): + """Enables header that is identified by header_substring on ssl_vhost. + + If the header identified by header_substring is not already set, + a new Header directive is placed in ssl_vhost's configuration with + arguments from: constants.HTTP_HEADER[header_substring] + + .. note:: This function saves the configuration + + :param ssl_vhost: Destination of traffic, an ssl enabled vhost + :type ssl_vhost: :class:`~letsencrypt_apache.obj.VirtualHost` + + :param header_substring: string that uniquely identifies a header. + e.g: Strict-Transport-Security, Upgrade-Insecure-Requests. + :type str + + :returns: Success, general_vhost (HTTP vhost) + :rtype: (bool, :class:`~letsencrypt_apache.obj.VirtualHost`) + + :raises .errors.PluginError: If no viable HTTP host can be created or + set with header header_substring. + + """ + if "headers_module" not in self.parser.modules: + self.enable_mod("headers") + + # Check if selected header is already set + self._verify_no_matching_http_header(ssl_vhost, header_substring) + + # Add directives to server + self.parser.add_dir(ssl_vhost.path, "Header", + constants.HEADER_ARGS[header_substring]) + + self.save_notes += ("Adding %s header to ssl vhost in %s\n" % + (header_substring, ssl_vhost.filep)) + + self.save() + logger.info("Adding %s header to ssl vhost in %s", header_substring, + ssl_vhost.filep) + + def _verify_no_matching_http_header(self, ssl_vhost, header_substring): + """Checks to see if an there is an existing Header directive that + contains the string header_substring. + + :param ssl_vhost: vhost to check + :type vhost: :class:`~letsencrypt_apache.obj.VirtualHost` + + :param header_substring: string that uniquely identifies a header. + e.g: Strict-Transport-Security, Upgrade-Insecure-Requests. + :type str + + :returns: boolean + :rtype: (bool) + + :raises errors.PluginEnhancementAlreadyPresent When header + header_substring exists + + """ + header_path = self.parser.find_dir("Header", None, start=ssl_vhost.path) + if header_path: + # "Existing Header directive for virtualhost" + pat = '(?:[ "]|^)(%s)(?:[ "]|$)' % (header_substring.lower()) + for match in header_path: + if re.search(pat, self.aug.get(match).lower()): + raise errors.PluginEnhancementAlreadyPresent( + "Existing %s header" % (header_substring)) + def _enable_redirect(self, ssl_vhost, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. @@ -800,8 +907,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param vhost: vhost to check :type vhost: :class:`~letsencrypt_apache.obj.VirtualHost` - :raises errors.PluginError: When another redirection exists + :raises errors.PluginEnhancementAlreadyPresent: When the exact + letsencrypt redirection WriteRule exists in virtual host. + errors.PluginError: When there exists directives that may hint + other redirection. (TODO: We should not throw a PluginError, + but that's for an other PR.) """ rewrite_path = self.parser.find_dir( "RewriteRule", None, start=vhost.path) @@ -818,7 +929,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): rewrite_path, constants.REWRITE_HTTPS_ARGS): if self.aug.get(match) != arg: raise errors.PluginError("Unknown Existing RewriteRule") - raise errors.PluginError( + + raise errors.PluginEnhancementAlreadyPresent( "Let's Encrypt has already enabled redirection") def _create_redirect_vhost(self, ssl_vhost): @@ -978,7 +1090,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return False def enable_site(self, vhost): - """Enables an available site, Apache restart required. + """Enables an available site, Apache reload required. .. note:: Does not make sure that the site correctly works or that all modules are enabled appropriately. @@ -1013,7 +1125,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): def enable_mod(self, mod_name, temp=False): """Enables module in Apache. - Both enables and restarts Apache so module is active. + 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. @@ -1055,7 +1167,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Modules can enable additional config files. Variables may be defined # within these new configuration sections. - # Restart is not necessary as DUMP_RUN_CFG uses latest config. + # Reload is not necessary as DUMP_RUN_CFG uses latest config. self.parser.update_runtime_variables(self.conf("ctl")) def _add_parser_mod(self, mod_name): @@ -1078,16 +1190,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): le_util.run_script([self.conf("enmod"), mod_name]) def restart(self): - """Restarts apache server. + """Runs a config test and reloads the Apache server. - .. todo:: This function will be converted to using reload - - :raises .errors.MisconfigurationError: If unable to restart due - to a configuration problem, or if the restart subprocess - cannot be run. + :raises .errors.MisconfigurationError: If either the config test + or reload fails. """ - return apache_restart(self.conf("init-script")) + self.config_test() + self._reload() + + def _reload(self): + """Reloads the Apache server. + + :raises .errors.MisconfigurationError: If reload fails + + """ + try: + le_util.run_script([self.conf("ctl"), "-k", "graceful"]) + except errors.SubprocessError as err: + raise errors.MisconfigurationError(str(err)) def config_test(self): # pylint: disable=no-self-use """Check the configuration of Apache for errors. @@ -1152,26 +1273,30 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ self._chall_out.update(achalls) responses = [None] * len(achalls) - apache_dvsni = dvsni.ApacheDvsni(self) + chall_doer = tls_sni_01.ApacheTlsSni01(self) for i, achall in enumerate(achalls): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - apache_dvsni.add_chall(achall, i) + # 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) - sni_response = apache_dvsni.perform() + sni_response = chall_doer.perform() if sni_response: - # Must restart in order to activate the challenges. + # Must reload in order to activate the challenges. # Handled here because we may be able to load up other challenge # types self.restart() + # TODO: Remove this dirty hack. We need to determine a reliable way + # 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[apache_dvsni.indices[i]] = resp + responses[chall_doer.indices[i]] = resp return responses @@ -1205,44 +1330,6 @@ def _get_mod_deps(mod_name): return deps.get(mod_name, []) -def apache_restart(apache_init_script): - """Restarts the Apache Server. - - :param str apache_init_script: Path to the Apache init script. - - .. todo:: Try to use reload instead. (This caused timing problems before) - - .. todo:: On failure, this should be a recovery_routine call with another - restart. This will confuse and inhibit developers from testing code - though. This change should happen after - the ApacheConfigurator has been thoroughly tested. The function will - need to be moved into the class again. Perhaps - this version can live on... for testing purposes. - - :raises .errors.MisconfigurationError: If unable to restart due to a - configuration problem, or if the restart subprocess cannot be run. - - """ - try: - proc = subprocess.Popen([apache_init_script, "restart"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - except (OSError, ValueError): - logger.fatal( - "Unable to restart the Apache process with %s", apache_init_script) - raise errors.MisconfigurationError( - "Unable to restart Apache process with %s" % apache_init_script) - - stdout, stderr = proc.communicate() - - if proc.returncode != 0: - # Enter recovery routine... - logger.error("Apache Restart Failed!\n%s\n%s", stdout, stderr) - raise errors.MisconfigurationError( - "Error while restarting Apache:\n%s\n%s" % (stdout, stderr)) - - def get_file_path(vhost_path): """Get file path from augeas_vhost_path. diff --git a/letsencrypt-apache/letsencrypt_apache/constants.py b/letsencrypt-apache/letsencrypt_apache/constants.py index 1c17eacc3..202fc3e21 100644 --- a/letsencrypt-apache/letsencrypt_apache/constants.py +++ b/letsencrypt-apache/letsencrypt_apache/constants.py @@ -7,7 +7,6 @@ CLI_DEFAULTS = dict( ctl="apache2ctl", enmod="a2enmod", dismod="a2dismod", - init_script="/etc/init.d/apache2", le_vhost_ext="-le-ssl.conf", ) """CLI defaults.""" @@ -27,3 +26,15 @@ AUGEAS_LENS_DIR = pkg_resources.resource_filename( REWRITE_HTTPS_ARGS = [ "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"] """Apache rewrite rule arguments used for redirections to https vhost""" + + +HSTS_ARGS = ["always", "set", "Strict-Transport-Security", + "\"max-age=31536000; includeSubDomains\""] +"""Apache header arguments for HSTS""" + +UIR_ARGS = ["always", "set", "Content-Security-Policy", + "upgrade-insecure-requests"] + +HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS, + "Upgrade-Insecure-Requests": UIR_ARGS} + diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 0350a32ec..fcccfaae2 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -103,7 +103,7 @@ class TwoVhost80Test(util.ApacheTest): """ vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 5) + self.assertEqual(len(vhs), 6) found = 0 for vhost in vhs: @@ -114,7 +114,7 @@ class TwoVhost80Test(util.ApacheTest): else: raise Exception("Missed: %s" % vhost) # pragma: no cover - self.assertEqual(found, 5) + self.assertEqual(found, 6) @mock.patch("letsencrypt_apache.display_ops.select_vhost") def test_choose_vhost_none_avail(self, mock_select): @@ -139,6 +139,12 @@ class TwoVhost80Test(util.ApacheTest): self.assertFalse(self.vh_truth[0].ssl) self.assertTrue(chosen_vhost.ssl) + @mock.patch("letsencrypt_apache.display_ops.select_vhost") + def test_choose_vhost_select_vhost_with_temp(self, mock_select): + mock_select.return_value = self.vh_truth[0] + chosen_vhost = self.config.choose_vhost("none.com", temp=True) + self.assertEqual(self.vh_truth[0], chosen_vhost) + @mock.patch("letsencrypt_apache.display_ops.select_vhost") def test_choose_vhost_select_vhost_conflicting_non_ssl(self, mock_select): mock_select.return_value = self.vh_truth[3] @@ -236,6 +242,64 @@ class TwoVhost80Test(util.ApacheTest): 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.config_dir, self.work_dir, version=(2, 4, 16)) + + 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(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.config_dir, self.work_dir, version=(2, 4, 16)) + + 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.config_dir, self.work_dir, version=(2, 4, 7)) + + 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(self): self.config.parser.modules.add("ssl_module") self.config.parser.modules.add("mod_ssl.c") @@ -351,7 +415,66 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), self.config.is_name_vhost(ssl_vhost)) - self.assertEqual(len(self.config.vhosts), 6) + self.assertEqual(len(self.config.vhosts), 7) + + def test_clean_vhost_ssl(self): + # pylint: disable=protected-access + for directive in ["SSLCertificateFile", "SSLCertificateKeyFile", + "SSLCertificateChainFile", "SSLCACertificatePath"]: + for _ in range(10): + self.config.parser.add_dir(self.vh_truth[1].path, directive, ["bogus"]) + self.config.save() + + self.config._clean_vhost(self.vh_truth[1]) + self.config.save() + + loc_cert = self.config.parser.find_dir( + 'SSLCertificateFile', None, self.vh_truth[1].path, False) + loc_key = self.config.parser.find_dir( + 'SSLCertificateKeyFile', None, self.vh_truth[1].path, False) + loc_chain = self.config.parser.find_dir( + 'SSLCertificateChainFile', None, self.vh_truth[1].path, False) + loc_cacert = self.config.parser.find_dir( + 'SSLCACertificatePath', None, self.vh_truth[1].path, False) + + self.assertEqual(len(loc_cert), 1) + self.assertEqual(len(loc_key), 1) + + self.assertEqual(len(loc_chain), 0) + + self.assertEqual(len(loc_cacert), 10) + + def test_deduplicate_directives(self): + # pylint: disable=protected-access + DIRECTIVE = "Foo" + for _ in range(10): + self.config.parser.add_dir(self.vh_truth[1].path, DIRECTIVE, ["bar"]) + self.config.save() + + self.config._deduplicate_directives(self.vh_truth[1].path, [DIRECTIVE]) + self.config.save() + + self.assertEqual( + len(self.config.parser.find_dir( + DIRECTIVE, None, self.vh_truth[1].path, False)), + 1) + + def test_remove_directives(self): + # pylint: disable=protected-access + DIRECTIVES = ["Foo", "Bar"] + for directive in DIRECTIVES: + for _ in range(10): + self.config.parser.add_dir(self.vh_truth[1].path, directive, ["baz"]) + self.config.save() + + self.config._remove_directives(self.vh_truth[1].path, DIRECTIVES) + self.config.save() + + for directive in DIRECTIVES: + self.assertEqual( + len(self.config.parser.find_dir( + directive, None, self.vh_truth[1].path, False)), + 0) def test_make_vhost_ssl_extra_vhs(self): self.config.aug.match = mock.Mock(return_value=["p1", "p2"]) @@ -380,23 +503,23 @@ class TwoVhost80Test(util.ApacheTest): self.config._add_name_vhost_if_necessary(self.vh_truth[0]) self.assertTrue(self.config.save.called) - @mock.patch("letsencrypt_apache.configurator.dvsni.ApacheDvsni.perform") + @mock.patch("letsencrypt_apache.configurator.tls_sni_01.ApacheTlsSni01.perform") @mock.patch("letsencrypt_apache.configurator.ApacheConfigurator.restart") - def test_perform(self, mock_restart, mock_dvsni_perform): + def test_perform(self, mock_restart, mock_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() - dvsni_ret_val = [ + expected = [ achall1.response(account_key), achall2.response(account_key), ] - mock_dvsni_perform.return_value = dvsni_ret_val + mock_perform.return_value = expected responses = self.config.perform([achall1, achall2]) - self.assertEqual(mock_dvsni_perform.call_count, 1) - self.assertEqual(responses, dvsni_ret_val) + self.assertEqual(mock_perform.call_count, 1) + self.assertEqual(responses, expected) self.assertEqual(mock_restart.call_count, 1) @@ -446,24 +569,13 @@ class TwoVhost80Test(util.ApacheTest): mock_script.side_effect = errors.SubprocessError("Can't find program") self.assertRaises(errors.PluginError, self.config.get_version) - @mock.patch("letsencrypt_apache.configurator.subprocess.Popen") - def test_restart(self, mock_popen): - """These will be changed soon enough with reload.""" - mock_popen().returncode = 0 - mock_popen().communicate.return_value = ("", "") - + @mock.patch("letsencrypt_apache.configurator.le_util.run_script") + def test_restart(self, _): self.config.restart() - @mock.patch("letsencrypt_apache.configurator.subprocess.Popen") - def test_restart_bad_process(self, mock_popen): - mock_popen.side_effect = OSError - - self.assertRaises(errors.MisconfigurationError, self.config.restart) - - @mock.patch("letsencrypt_apache.configurator.subprocess.Popen") - def test_restart_failure(self, mock_popen): - mock_popen().communicate.return_value = ("", "") - mock_popen().returncode = 1 + @mock.patch("letsencrypt_apache.configurator.le_util.run_script") + def test_restart_bad_process(self, mock_run_script): + mock_run_script.side_effect = [None, errors.SubprocessError] self.assertRaises(errors.MisconfigurationError, self.config.restart) @@ -480,14 +592,14 @@ class TwoVhost80Test(util.ApacheTest): def test_get_all_certs_keys(self): c_k = self.config.get_all_certs_keys() - self.assertEqual(len(c_k), 1) + self.assertEqual(len(c_k), 2) cert, key, path = next(iter(c_k)) self.assertTrue("cert" in cert) self.assertTrue("key" in key) - self.assertTrue("default-ssl.conf" in path) + self.assertTrue("default-ssl" in path) def test_get_all_certs_keys_malformed_conf(self): - self.config.parser.find_dir = mock.Mock(side_effect=[["path"], []]) + self.config.parser.find_dir = mock.Mock(side_effect=[["path"], [], ["path"], []]) c_k = self.config.get_all_certs_keys() self.assertFalse(c_k) @@ -513,6 +625,84 @@ class TwoVhost80Test(util.ApacheTest): errors.PluginError, self.config.enhance, "letsencrypt.demo", "unknown_enhancement") + @mock.patch("letsencrypt.le_util.run_script") + @mock.patch("letsencrypt.le_util.exe_exists") + def test_http_header_hsts(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 letsencrypt.demo + self.config.enhance("letsencrypt.demo", "ensure-http-header", + "Strict-Transport-Security") + + self.assertTrue("headers_module" in self.config.parser.modules) + + # Get the ssl vhost for letsencrypt.demo + ssl_vhost = self.config.assoc["letsencrypt.demo"] + + # These are not immediately available in find_dir even with save() and + # load(). They must be found in sites-available + hsts_header = self.config.parser.find_dir( + "Header", None, ssl_vhost.path) + + # four args to HSTS header + self.assertEqual(len(hsts_header), 4) + + def test_http_header_hsts_twice(self): + self.config.parser.modules.add("mod_ssl.c") + # skip the enable mod + self.config.parser.modules.add("headers_module") + + # This will create an ssl vhost for letsencrypt.demo + self.config.enhance("encryption-example.demo", "ensure-http-header", + "Strict-Transport-Security") + + self.assertRaises( + errors.PluginEnhancementAlreadyPresent, + self.config.enhance, "encryption-example.demo", "ensure-http-header", + "Strict-Transport-Security") + + @mock.patch("letsencrypt.le_util.run_script") + @mock.patch("letsencrypt.le_util.exe_exists") + def test_http_header_uir(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 letsencrypt.demo + self.config.enhance("letsencrypt.demo", "ensure-http-header", + "Upgrade-Insecure-Requests") + + self.assertTrue("headers_module" in self.config.parser.modules) + + # Get the ssl vhost for letsencrypt.demo + ssl_vhost = self.config.assoc["letsencrypt.demo"] + + # These are not immediately available in find_dir even with save() and + # load(). They must be found in sites-available + uir_header = self.config.parser.find_dir( + "Header", None, ssl_vhost.path) + + # four args to HSTS header + self.assertEqual(len(uir_header), 4) + + def test_http_header_uir_twice(self): + self.config.parser.modules.add("mod_ssl.c") + # skip the enable mod + self.config.parser.modules.add("headers_module") + + # This will create an ssl vhost for letsencrypt.demo + self.config.enhance("encryption-example.demo", "ensure-http-header", + "Upgrade-Insecure-Requests") + + self.assertRaises( + errors.PluginEnhancementAlreadyPresent, + self.config.enhance, "encryption-example.demo", "ensure-http-header", + "Upgrade-Insecure-Requests") + + + @mock.patch("letsencrypt.le_util.run_script") @mock.patch("letsencrypt.le_util.exe_exists") def test_redirect_well_formed_http(self, mock_exe, _): @@ -553,7 +743,7 @@ class TwoVhost80Test(util.ApacheTest): self.config.parser.modules.add("rewrite_module") self.config.enhance("encryption-example.demo", "redirect") self.assertRaises( - errors.PluginError, + errors.PluginEnhancementAlreadyPresent, self.config.enhance, "encryption-example.demo", "redirect") def test_unknown_rewrite(self): @@ -593,7 +783,7 @@ class TwoVhost80Test(util.ApacheTest): self.vh_truth[1].aliases = set(["yes.default.com"]) self.config._enable_redirect(self.vh_truth[1], "") # pylint: disable=protected-access - self.assertEqual(len(self.config.vhosts), 6) + self.assertEqual(len(self.config.vhosts), 7) def get_achalls(self): """Return testing achallenges.""" diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl-port-only.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl-port-only.conf new file mode 100644 index 000000000..5a50c536e --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl-port-only.conf @@ -0,0 +1,36 @@ + + + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # A self-signed (snakeoil) certificate can be created by installing + # the ssl-cert package. See + # /usr/share/doc/apache2/README.Debian.gz for more info. + # If both key and certificate are stored in the same file, only the + # SSLCertificateFile directive is needed. + SSLCertificateFile /etc/apache2/certs/letsencrypt-cert_5.pem + SSLCertificateKeyFile /etc/apache2/ssl/key-letsencrypt_15.pem + + + #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + + BrowserMatch "MSIE [2-6]" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + # MSIE 7 and newer should be able to use keepalive + BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown + + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt-apache/letsencrypt_apache/tests/tls_sni_01_test.py similarity index 91% rename from letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py rename to letsencrypt-apache/letsencrypt_apache/tests/tls_sni_01_test.py index 911c2a36b..f4dff7734 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/tls_sni_01_test.py @@ -1,4 +1,4 @@ -"""Test for letsencrypt_apache.dvsni.""" +"""Test for letsencrypt_apache.tls_sni_01.""" import unittest import shutil @@ -10,21 +10,21 @@ from letsencrypt_apache import obj from letsencrypt_apache.tests import util -class DvsniPerformTest(util.ApacheTest): - """Test the ApacheDVSNI challenge.""" +class TlsSniPerformTest(util.ApacheTest): + """Test the ApacheTlsSni01 challenge.""" auth_key = common_test.TLSSNI01Test.auth_key achalls = common_test.TLSSNI01Test.achalls def setUp(self): # pylint: disable=arguments-differ - super(DvsniPerformTest, self).setUp() + super(TlsSniPerformTest, self).setUp() config = util.get_apache_configurator( self.config_path, self.config_dir, self.work_dir) config.config.tls_sni_01_port = 443 - from letsencrypt_apache import dvsni - self.sni = dvsni.ApacheDvsni(config) + from letsencrypt_apache import tls_sni_01 + self.sni = tls_sni_01.ApacheTlsSni01(config) def tearDown(self): shutil.rmtree(self.temp_dir) @@ -121,7 +121,7 @@ class DvsniPerformTest(util.ApacheTest): names = vhost.get_names() self.assertTrue(names in z_domains) - def test_get_dvsni_addrs_default(self): + def test_get_addrs_default(self): self.sni.configurator.choose_vhost = mock.Mock( return_value=obj.VirtualHost( "path", "aug_path", set([obj.Addr.fromstring("_default_:443")]), @@ -130,7 +130,7 @@ class DvsniPerformTest(util.ApacheTest): self.assertEqual( set([obj.Addr.fromstring("*:443")]), - self.sni.get_dvsni_addrs(self.achalls[0])) + self.sni._get_addrs(self.achalls[0])) # pylint: disable=protected-access if __name__ == "__main__": diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py index a8bfe0e4b..0c60373f2 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/util.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py @@ -75,11 +75,7 @@ def get_apache_configurator( in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) - with mock.patch("letsencrypt_apache.configurator." - "subprocess.Popen") as mock_popen: - # This indicates config_test passes - mock_popen().communicate.return_value = ("Fine output", "No problems") - mock_popen().returncode = 0 + with mock.patch("letsencrypt_apache.configurator.le_util.run_script"): with mock.patch("letsencrypt_apache.configurator.le_util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True @@ -128,7 +124,11 @@ def get_vh_truth(temp_dir, config_name): os.path.join(prefix, "mod_macro-example.conf"), os.path.join(aug_pre, "mod_macro-example.conf/Macro/VirtualHost"), - set([obj.Addr.fromstring("*:80")]), False, True, modmacro=True) + set([obj.Addr.fromstring("*:80")]), False, True, modmacro=True), + obj.VirtualHost( + os.path.join(prefix, "default-ssl-port-only.conf"), + os.path.join(aug_pre, "default-ssl-port-only.conf/IfModule/VirtualHost"), + set([obj.Addr.fromstring("_default_:443")]), True, False), ] return vh_truth diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/tls_sni_01.py similarity index 76% rename from letsencrypt-apache/letsencrypt_apache/dvsni.py rename to letsencrypt-apache/letsencrypt_apache/tls_sni_01.py index 2f9e9ed18..4284e240c 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/tls_sni_01.py @@ -1,4 +1,5 @@ -"""ApacheDVSNI""" +"""A class that performs TLS-SNI-01 challenges for Apache""" + import os from letsencrypt.plugins import common @@ -7,22 +8,22 @@ from letsencrypt_apache import obj from letsencrypt_apache import parser -class ApacheDvsni(common.TLSSNI01): - """Class performs DVSNI challenges within the Apache configurator. +class ApacheTlsSni01(common.TLSSNI01): + """Class that performs TLS-SNI-01 challenges within the Apache configurator :ivar configurator: ApacheConfigurator object :type configurator: :class:`~apache.configurator.ApacheConfigurator` - :ivar list achalls: Annotated tls-sni-01 + :ivar list achalls: Annotated TLS-SNI-01 (`.KeyAuthorizationAnnotatedChallenge`) challenges. :param list indices: Meant to hold indices of challenges in a - larger array. ApacheDvsni is capable of solving many challenges + larger array. ApacheTlsSni01 is capable of solving many challenges at once which causes an indexing issue within ApacheConfigurator who must return all responses in order. Imagine ApacheConfigurator maintaining state about where all of the http-01 Challenges, - Dvsni Challenges belong in the response array. This is an optional - utility. + TLS-SNI-01 Challenges belong in the response array. This is an + optional utility. :param str challenge_conf: location of the challenge config file @@ -46,14 +47,14 @@ class ApacheDvsni(common.TLSSNI01): """ def __init__(self, *args, **kwargs): - super(ApacheDvsni, self).__init__(*args, **kwargs) + super(ApacheTlsSni01, self).__init__(*args, **kwargs) self.challenge_conf = os.path.join( self.configurator.conf("server-root"), - "le_dvsni_cert_challenge.conf") + "le_tls_sni_01_cert_challenge.conf") def perform(self): - """Perform a DVSNI challenge.""" + """Perform a TLS-SNI-01 challenge.""" if not self.achalls: return [] # Save any changes to the configuration as a precaution @@ -71,8 +72,8 @@ class ApacheDvsni(common.TLSSNI01): responses.append(self._setup_challenge_cert(achall)) # Setup the configuration - dvsni_addrs = self._mod_config() - self.configurator.make_addrs_sni_ready(dvsni_addrs) + addrs = self._mod_config() + self.configurator.make_addrs_sni_ready(addrs) # Save reversible changes self.configurator.save("SNI Challenge", True) @@ -84,16 +85,16 @@ class ApacheDvsni(common.TLSSNI01): Result: Apache config includes virtual servers for issued challs - :returns: All DVSNI addresses used + :returns: All TLS-SNI-01 addresses used :rtype: set """ - dvsni_addrs = set() + addrs = set() config_text = "\n" for achall in self.achalls: - achall_addrs = self.get_dvsni_addrs(achall) - dvsni_addrs.update(achall_addrs) + achall_addrs = self._get_addrs(achall) + addrs.update(achall_addrs) config_text += self._get_config_text(achall, achall_addrs) @@ -106,30 +107,29 @@ class ApacheDvsni(common.TLSSNI01): with open(self.challenge_conf, "w") as new_conf: new_conf.write(config_text) - return dvsni_addrs - - def get_dvsni_addrs(self, achall): - """Return the Apache addresses needed for DVSNI.""" - vhost = self.configurator.choose_vhost(achall.domain) + return addrs + def _get_addrs(self, achall): + """Return the Apache addresses needed for TLS-SNI-01.""" + vhost = self.configurator.choose_vhost(achall.domain, temp=True) # TODO: Checkout _default_ rules. - dvsni_addrs = set() + addrs = set() default_addr = obj.Addr(("*", str( self.configurator.config.tls_sni_01_port))) for addr in vhost.addrs: if "_default_" == addr.get_addr(): - dvsni_addrs.add(default_addr) + addrs.add(default_addr) else: - dvsni_addrs.add( + addrs.add( addr.get_sni_addr(self.configurator.config.tls_sni_01_port)) - return dvsni_addrs + return addrs def _conf_include_check(self, main_config): - """Adds DVSNI challenge conf file into configuration. + """Add TLS-SNI-01 challenge conf file into configuration. - Adds DVSNI challenge include file if it does not already exist + Adds TLS-SNI-01 challenge include file if it does not already exist within mainConfig :param str main_config: file path to main user apache config file @@ -146,7 +146,7 @@ class ApacheDvsni(common.TLSSNI01): """Chocolate virtual server configuration text :param .KeyAuthorizationAnnotatedChallenge achall: Annotated - DVSNI challenge. + TLS-SNI-01 challenge. :param list ip_addrs: addresses of challenged domain :class:`list` of type `~.obj.Addr` @@ -157,7 +157,7 @@ class ApacheDvsni(common.TLSSNI01): """ ips = " ".join(str(i) for i in ip_addrs) document_root = os.path.join( - self.configurator.config.work_dir, "dvsni_page/") + self.configurator.config.work_dir, "tls_sni_01_page/") # TODO: Python docs is not clear how mutliline string literal # newlines are parsed on different platforms. At least on # Linux (Debian sid), when source file uses CRLF, Python still diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/testdata/configs.tar.gz b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/testdata/configs.tar.gz new file mode 100644 index 000000000..7323acf74 Binary files /dev/null and b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/testdata/configs.tar.gz differ diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py index 43070cf03..b635ee539 100644 --- a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py +++ b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py @@ -34,6 +34,8 @@ def create_le_config(parent_dir): os.mkdir(config["work_dir"]) os.mkdir(config["logs_dir"]) + config["domains"] = None + return argparse.Namespace(**config) # pylint: disable=star-args diff --git a/letsencrypt-nginx/LICENSE.txt b/letsencrypt-nginx/LICENSE.txt index 981c46c9f..02a1459be 100644 --- a/letsencrypt-nginx/LICENSE.txt +++ b/letsencrypt-nginx/LICENSE.txt @@ -12,6 +12,13 @@ See the License for the specific language governing permissions and limitations under the License. + Incorporating code from nginxparser + Copyright 2014 Fatih Erikli + Licensed MIT + + +Text of Apache License +====================== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -188,3 +195,22 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + +Text of MIT License +=================== +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/letsencrypt-nginx/docs/api/dvsni.rst b/letsencrypt-nginx/docs/api/dvsni.rst deleted file mode 100644 index 4f5f9d7e3..000000000 --- a/letsencrypt-nginx/docs/api/dvsni.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt_nginx.dvsni` ------------------------------- - -.. automodule:: letsencrypt_nginx.dvsni - :members: diff --git a/letsencrypt-nginx/docs/api/tls_sni_01.rst b/letsencrypt-nginx/docs/api/tls_sni_01.rst new file mode 100644 index 000000000..f9f584b0c --- /dev/null +++ b/letsencrypt-nginx/docs/api/tls_sni_01.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt_nginx.tls_sni_01` +----------------------------------- + +.. automodule:: letsencrypt_nginx.tls_sni_01 + :members: diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 29445a9d4..aaaf43c5f 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -24,7 +24,7 @@ from letsencrypt import reverter from letsencrypt.plugins import common from letsencrypt_nginx import constants -from letsencrypt_nginx import dvsni +from letsencrypt_nginx import tls_sni_01 from letsencrypt_nginx import obj from letsencrypt_nginx import parser @@ -379,11 +379,12 @@ class NginxConfigurator(common.Plugin): :param unused_options: Not currently used :type unused_options: Not Available """ - redirect_block = [[['if', '($scheme != "https")'], + redirect_block = [[ + ['if', '($scheme != "https")'], [['return', '301 https://$host$request_uri']] ]] - self.parser.add_server_directives(vhost.filep, vhost.names, - redirect_block) + self.parser.add_server_directives( + vhost.filep, vhost.names, redirect_block) logger.info("Redirecting all traffic to ssl in %s", vhost.filep) ###################################### @@ -573,15 +574,15 @@ class NginxConfigurator(common.Plugin): """ self._chall_out += len(achalls) responses = [None] * len(achalls) - nginx_dvsni = dvsni.NginxDvsni(self) + chall_doer = tls_sni_01.NginxTlsSni01(self) for i, achall in enumerate(achalls): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - nginx_dvsni.add_chall(achall, i) + # 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) - sni_response = nginx_dvsni.perform() + sni_response = chall_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() @@ -590,7 +591,7 @@ class NginxConfigurator(common.Plugin): # 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[nginx_dvsni.indices[i]] = resp + responses[chall_doer.indices[i]] = resp return responses diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 705257c16..14db2f8b7 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -413,7 +413,7 @@ def _regex_match(target_name, name): return True else: return False - except re.error: # pragma: no cover + except re.error: # pragma: no cover # perl-compatible regexes are sometimes not recognized by python return False @@ -450,10 +450,9 @@ def _parse_server(server): :rtype: dict """ - parsed_server = {} - parsed_server['addrs'] = set() - parsed_server['ssl'] = False - parsed_server['names'] = set() + parsed_server = {'addrs': set(), + 'ssl': False, + 'names': set()} for directive in server: if directive[0] == 'listen': diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index ff720ea85..56ad5110c 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -212,9 +212,9 @@ class NginxConfiguratorTest(util.NginxTest): ('/etc/nginx/fullchain.pem', '/etc/nginx/key.pem', nginx_conf), ]), self.config.get_all_certs_keys()) - @mock.patch("letsencrypt_nginx.configurator.dvsni.NginxDvsni.perform") + @mock.patch("letsencrypt_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") @mock.patch("letsencrypt_nginx.configurator.NginxConfigurator.restart") - def test_perform(self, mock_restart, mock_dvsni_perform): + def test_perform(self, mock_restart, mock_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( @@ -230,16 +230,16 @@ class NginxConfiguratorTest(util.NginxTest): status=messages.Status("pending"), ), domain="example.com", account_key=self.rsa512jwk) - dvsni_ret_val = [ + expected = [ achall1.response(self.rsa512jwk), achall2.response(self.rsa512jwk), ] - mock_dvsni_perform.return_value = dvsni_ret_val + mock_perform.return_value = expected responses = self.config.perform([achall1, achall2]) - self.assertEqual(mock_dvsni_perform.call_count, 1) - self.assertEqual(responses, dvsni_ret_val) + self.assertEqual(mock_perform.call_count, 1) + self.assertEqual(responses, expected) self.assertEqual(mock_restart.call_count, 1) @mock.patch("letsencrypt_nginx.configurator.subprocess.Popen") diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/tls_sni_01_test.py similarity index 95% rename from letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py rename to letsencrypt-nginx/letsencrypt_nginx/tests/tls_sni_01_test.py index d32e3d98f..04fe01bc4 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/tls_sni_01_test.py @@ -1,4 +1,4 @@ -"""Test for letsencrypt_nginx.dvsni.""" +"""Tests for letsencrypt_nginx.tls_sni_01""" import unittest import shutil @@ -16,8 +16,8 @@ from letsencrypt_nginx import obj from letsencrypt_nginx.tests import util -class DvsniPerformTest(util.NginxTest): - """Test the NginxDVSNI challenge.""" +class TlsSniPerformTest(util.NginxTest): + """Test the NginxTlsSni01 challenge.""" account_key = common_test.TLSSNI01Test.auth_key achalls = [ @@ -42,13 +42,13 @@ class DvsniPerformTest(util.NginxTest): ] def setUp(self): - super(DvsniPerformTest, self).setUp() + super(TlsSniPerformTest, self).setUp() config = util.get_nginx_configurator( self.config_path, self.config_dir, self.work_dir) - from letsencrypt_nginx import dvsni - self.sni = dvsni.NginxDvsni(config) + from letsencrypt_nginx import tls_sni_01 + self.sni = tls_sni_01.NginxTlsSni01(config) def tearDown(self): shutil.rmtree(self.temp_dir) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py index e60feb3d3..3d70f7ac7 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py @@ -50,7 +50,7 @@ def get_nginx_configurator( backups = os.path.join(work_dir, "backups") with mock.patch("letsencrypt_nginx.configurator.le_util." - "exe_exists") as mock_exe_exists: + "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True config = configurator.NginxConfigurator( diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py similarity index 82% rename from letsencrypt-nginx/letsencrypt_nginx/dvsni.py rename to letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py index 8fd705f08..e59281c4c 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py @@ -1,4 +1,5 @@ -"""NginxDVSNI""" +"""A class that performs TLS-SNI-01 challenges for Nginx""" + import itertools import logging import os @@ -13,31 +14,32 @@ from letsencrypt_nginx import nginxparser logger = logging.getLogger(__name__) -class NginxDvsni(common.TLSSNI01): - """Class performs DVSNI challenges within the Nginx configurator. +class NginxTlsSni01(common.TLSSNI01): + """TLS-SNI-01 authenticator for Nginx :ivar configurator: NginxConfigurator object :type configurator: :class:`~nginx.configurator.NginxConfigurator` - :ivar list achalls: Annotated :class:`~letsencrypt.achallenges.DVSNI` - challenges. + :ivar list achalls: Annotated + class:`~letsencrypt.achallenges.KeyAuthorizationAnnotatedChallenge` + challenges :param list indices: Meant to hold indices of challenges in a - larger array. NginxDvsni is capable of solving many challenges + larger array. NginxTlsSni01 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, - Dvsni Challenges belong in the response array. This is an optional - utility. + TLS-SNI-01 Challenges belong in the response array. This is an + optional utility. :param str challenge_conf: location of the challenge config file """ def perform(self): - """Perform a DVSNI challenge on Nginx. + """Perform a challenge on Nginx. - :returns: list of :class:`letsencrypt.acme.challenges.DVSNIResponse` + :returns: list of :class:`letsencrypt.acme.challenges.TLSSNI01Response` :rtype: list """ @@ -84,7 +86,8 @@ class NginxDvsni(common.TLSSNI01): :class:`letsencrypt_nginx.obj.Addr` to apply :raises .MisconfigurationError: - Unable to find a suitable HTTP block to include DVSNI hosts. + Unable to find a suitable HTTP block in which to include + authenticator hosts. """ # Add the 'include' statement for the challenges if it doesn't exist @@ -110,8 +113,8 @@ class NginxDvsni(common.TLSSNI01): break if not included: raise errors.MisconfigurationError( - 'LetsEncrypt could not find an HTTP block to include DVSNI ' - 'challenges in %s.' % root) + 'LetsEncrypt could not find an HTTP block to include ' + 'TLS-SNI-01 challenges in %s.' % root) config = [self._make_server_block(pair[0], pair[1]) for pair in itertools.izip(self.achalls, ll_addrs)] @@ -123,10 +126,11 @@ class NginxDvsni(common.TLSSNI01): nginxparser.dump(config, new_conf) def _make_server_block(self, achall, addrs): - """Creates a server block for a DVSNI challenge. + """Creates a server block for a challenge. - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` + :param achall: Annotated TLS-SNI-01 challenge + :type achall: + :class:`letsencrypt.achallenges.KeyAuthorizationAnnotatedChallenge` :param list addrs: addresses of challenged domain :class:`list` of type :class:`~nginx.obj.Addr` @@ -136,7 +140,7 @@ class NginxDvsni(common.TLSSNI01): """ document_root = os.path.join( - self.configurator.config.work_dir, "dvsni_page") + self.configurator.config.work_dir, "tls_sni_01_page") block = [['listen', str(addr)] for addr in addrs] diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index d641578ed..0498d66c4 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1,8 +1,11 @@ """Let's Encrypt CLI.""" # TODO: Sanity check all input. Be sure to avoid shell code etc... +# pylint: disable=too-many-lines +# (TODO: split this file into main.py and cli.py) import argparse import atexit import functools +import json import logging import logging.handlers import os @@ -59,6 +62,7 @@ the cert. Major SUBCOMMANDS are: revoke Revoke a previously obtained certificate rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation + plugins Display information about installed plugins """ @@ -71,7 +75,7 @@ USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing c %s --webroot Place files in a server's webroot folder for authentication -OR use different servers to obtain (authenticate) the cert and then install it: +OR use different plugins to obtain (authenticate) the cert and then install it: --authenticator standalone --installer apache @@ -99,7 +103,7 @@ def usage_strings(plugins): def _find_domains(args, installer): - if args.domains is None: + if not args.domains: domains = display_ops.choose_names(installer) else: domains = args.domains @@ -140,10 +144,8 @@ def _determine_account(args, config): elif len(accounts) == 1: acc = accounts[0] else: # no account registered yet - if args.email is None: + if args.email is None and not args.register_unsafely_without_email: args.email = display_ops.get_email() - if not args.email: # get_email might return "" - args.email = None def _tos_cb(regr): if args.tos: @@ -381,10 +383,9 @@ def diagnose_configurator_problem(cfg_type, requested, plugins): raise errors.PluginSelectionError(msg) -def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable=too-many-branches +def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable=too-many-branches """ Figure out which configurator we're going to use - :raises error.PluginSelectionError if there was a problem """ @@ -465,7 +466,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo domains, lineage.privkey, lineage.cert, lineage.chain, lineage.fullchain) - le_client.enhance_config(domains, args.redirect) + le_client.enhance_config(domains, config) if len(lineage.available_versions("cert")) == 1: display_ops.success_installation(domains) @@ -476,7 +477,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo def obtain_cert(args, config, plugins): """Authenticate & obtain cert, but do not install it.""" - if args.domains is not None and args.csr is not None: + if args.domains and args.csr is not None: # TODO: --csr could have a priority, when --domains is # supplied, check if CSR matches given domains? return "--domains and --csr are mutually exclusive" @@ -519,7 +520,7 @@ def install(args, config, plugins): le_client.deploy_certificate( domains, args.key_path, args.cert_path, args.chain_path, args.fullchain_path) - le_client.enhance_config(domains, args.redirect) + le_client.enhance_config(domains, config) def revoke(args, config, unused_plugins): # TODO: coop with renewal config @@ -672,7 +673,6 @@ class HelpfulArgumentParser(object): print usage sys.exit(0) self.visible_topics = self.determine_help_topics(self.help_arg) - #print self.visible_topics self.groups = {} # elements are added by .add_group() def parse_args(self): @@ -685,31 +685,8 @@ class HelpfulArgumentParser(object): parsed_args = self.parser.parse_args(self.args) parsed_args.func = self.VERBS[self.verb] - parsed_args.domains = self._parse_domains(parsed_args.domains) return parsed_args - def _parse_domains(self, domains): - """Helper function for parse_args() that parses domains from a - (possibly) comma separated list and returns list of unique domains. - - :param domains: List of domain flags - :type domains: `list` of `string` - - :returns: List of unique domains - :rtype: `list` of `string` - - """ - - uniqd = None - - if domains: - dlist = [] - for domain in domains: - dlist.extend([d.strip() for d in domain.split(",")]) - # Make sure we don't have duplicates - uniqd = [d for i, d in enumerate(dlist) if d not in dlist[:i]] - - return uniqd def determine_verb(self): """Determines the verb/subcommand provided by the user. @@ -847,15 +824,25 @@ def prepare_and_parse_args(plugins, args): helpful.add( None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") + helpful.add( + None, "--register-unsafely-without-email", action="store_true", + help="Specifying this flag enables registering an account with no " + "email address. This is strongly discouraged, because in the " + "event of key loss or account compromise you will irrevocably " + "lose access to your account. You will also be unable to receive " + "notice about impending expiration of revocation of your " + "certificates. Updates to the Subscriber Agreement will still " + "affect you, and will be effective 14 days after posting an " + "update to the web site.") helpful.add(None, "-m", "--email", help=config_help("email")) # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: # subparser.add_argument("domains", nargs="*", metavar="domain") - helpful.add(None, "-d", "--domains", dest="domains", - metavar="DOMAIN", action="append", + helpful.add(None, "-d", "--domains", "--domain", dest="domains", + metavar="DOMAIN", action=DomainFlagProcessor, default=[], help="Domain names to apply. For multiple domains you can use " - "multiple -d flags or enter a comma separated list of domains" + "multiple -d flags or enter a comma separated list of domains " "as a parameter.") helpful.add( None, "--duplicate", dest="duplicate", action="store_true", @@ -898,8 +885,9 @@ def prepare_and_parse_args(plugins, args): "testing", "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), help=config_help("tls_sni_01_port")) - helpful.add("testing", "--http-01-port", dest="http01_port", type=int, - help=config_help("http01_port")) + helpful.add( + "testing", "--http-01-port", type=int, dest="http01_port", + default=flag_default("http01_port"), help=config_help("http01_port")) helpful.add_group( "security", description="Security parameters & server settings") @@ -914,6 +902,25 @@ def prepare_and_parse_args(plugins, args): "security", "--no-redirect", action="store_false", help="Do not automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.", dest="redirect", default=None) + helpful.add( + "security", "--hsts", action="store_true", + help="Add the Strict-Transport-Security header to every HTTP response." + " Forcing browser to use always use SSL for the domain." + " Defends against SSL Stripping.", dest="hsts", default=False) + helpful.add( + "security", "--no-hsts", action="store_false", + help="Do not automatically add the Strict-Transport-Security header" + " to every HTTP response.", dest="hsts", default=False) + helpful.add( + "security", "--uir", action="store_true", + help="Add the \"Content-Security-Policy: upgrade-insecure-requests\"" + " header to every HTTP response. Forcing the browser to use" + " https:// for every http:// resource.", dest="uir", default=None) + helpful.add( + "security", "--no-uir", action="store_false", + help=" Do not automatically set the \"Content-Security-Policy:" + " upgrade-insecure-requests\" header to every HTTP response.", + dest="uir", default=None) helpful.add( "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " @@ -939,8 +946,7 @@ def _create_subparsers(helpful): help="Set a custom user agent string for the client. User agent strings allow " "the CA to collect high level statistics about success rates by OS and " "plugin. If you wish to hide your server OS version from the Let's " - 'Encrypt server, set this to "".' - ) + 'Encrypt server, set this to "".') helpful.add("certonly", "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER" @@ -1013,7 +1019,7 @@ def _plugins_parsing(helpful, plugins): helpful.add_group( "plugins", description="Let's Encrypt client supports an " "extensible plugins architecture. See '%(prog)s plugins' for a " - "list of all available plugins and their names. You can force " + "list of all installed plugins and their names. You can force " "a particular plugin by setting options provided below. Further " "down this help message you will find plugin-specific options " "(prefixed by --{plugin_name}).") @@ -1042,6 +1048,61 @@ def _plugins_parsing(helpful, plugins): helpful.add_plugin_args(plugins) + # These would normally be a flag within the webroot plugin, but because + # they are parsed in conjunction with --domains, they live here for + # legibiility. helpful.add_plugin_ags must be called first to add the + # "webroot" topic + helpful.add("webroot", "-w", "--webroot-path", action=WebrootPathProcessor, + help="public_html / webroot path. This can be specified multiple times to " + "handle different domains; each domain will have the webroot path that" + " precededed it. For instance: `-w /var/www/example -d example.com -d " + "www.example.com -w /var/www/thing -d thing.net -d m.thing.net`") + parse_dict = lambda s: dict(json.loads(s)) + # --webroot-map still has some awkward properties, so it is undocumented + helpful.add("webroot", "--webroot-map", default={}, type=parse_dict, + help=argparse.SUPPRESS) + + +class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring + def __init__(self, *args, **kwargs): + self.domain_before_webroot = False + argparse.Action.__init__(self, *args, **kwargs) + + def __call__(self, parser, config, webroot, option_string=None): + """ + Keep a record of --webroot-path / -w flags during processing, so that + we know which apply to which -d flags + """ + if config.webroot_path is None: # first -w flag encountered + config.webroot_path = [] + # if any --domain flags preceded the first --webroot-path flag, + # apply that webroot path to those; subsequent entries in + # config.webroot_map are filled in by cli.DomainFlagProcessor + if config.domains: + self.domain_before_webroot = True + for d in config.domains: + config.webroot_map.setdefault(d, webroot) + elif self.domain_before_webroot: + # FIXME if you set domains in a config file, you should get a different error + # here, pointing you to --webroot-map + raise errors.Error("If you specify multiple webroot paths, one of " + "them must precede all domain flags") + config.webroot_path.append(webroot) + + +class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring + def __call__(self, parser, config, domain_arg, option_string=None): + """ + Process a new -d flag, helping the webroot plugin construct a map of + {domain : webrootpath} if -w / --webroot-path is in use + """ + for domain in (d.strip() for d in domain_arg.split(",")): + if domain not in config.domains: + config.domains.append(domain) + # Each domain has a webroot_path of the most recent -w flag + if config.webroot_path: + config.webroot_map[domain] = config.webroot_path[-1] + def setup_log_file_handler(args, logfile, fmt): """Setup file debug logging.""" @@ -1120,11 +1181,19 @@ def _handle_exception(exc_type, exc_value, trace, args): if issubclass(exc_type, errors.Error): sys.exit(exc_value) else: + # Here we're passing a client or ACME error out to the client at the shell # Tell the user a bit about what happened, without overwhelming # them with a full traceback - msg = ("An unexpected error occurred.\n" + - traceback.format_exception_only(exc_type, exc_value)[0] + - "Please see the ") + err = traceback.format_exception_only(exc_type, exc_value)[0] + # Typical error from the ACME module: + # acme.messages.Error: urn:acme:error:malformed :: The request message was + # malformed :: Error creating new registration :: Validation of contact + # mailto:none@longrandomstring.biz failed: Server failure at resolver + if ("urn:acme" in err and ":: " in err + and args.verbose_count <= flag_default("verbose_count")): + # prune ACME error code, we have a human description + _code, _sep, err = err.partition(":: ") + msg = "An unexpected error occurred:\n" + err + "Please see the " if args is None: msg += "logfile '{0}' for more details.".format(logfile) else: diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 8e053e926..f7010e09d 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -100,6 +100,11 @@ def register(config, account_storage, tos_cb=None): if account_storage.find_all(): logger.info("There are already existing accounts for %s", config.server) if config.email is None: + if not config.register_unsafely_without_email: + msg = ("No email was provided and " + "--register-unsafely-without-email was not present.") + logger.warn(msg) + raise errors.Error(msg) logger.warn("Registering without email!") # Each new registration shall use a fresh new key @@ -110,7 +115,7 @@ def register(config, account_storage, tos_cb=None): backend=default_backend()))) acme = acme_from_config_key(config, key) # TODO: add phone? - regr = acme.register(messages.NewRegistration.from_data(email=config.email)) + regr = perform_registration(acme, config) if regr.terms_of_service is not None: if tos_cb is not None and not tos_cb(regr): @@ -126,6 +131,30 @@ def register(config, account_storage, tos_cb=None): return acc, acme +def perform_registration(acme, config): + """ + Actually register new account, trying repeatedly if there are email + problems + + :param .IConfig config: Client configuration. + :param acme.client.Client client: ACME client object. + + :returns: Registration Resource. + :rtype: `acme.messages.RegistrationResource` + + :raises .UnexpectedUpdate: + """ + try: + return acme.register(messages.NewRegistration.from_data(email=config.email)) + except messages.Error, e: + err = repr(e) + if "MX record" in err or "Validation of contact mailto" in err: + config.namespace.email = display_ops.get_email(more=True, invalid=True) + return perform_registration(acme, config) + else: + raise + + class Client(object): """ACME protocol client. @@ -354,57 +383,86 @@ class Client(object): with error_handler.ErrorHandler(self._rollback_and_restart, msg): # sites may have been enabled / final cleanup self.installer.restart() - - def enhance_config(self, domains, redirect=None): + def enhance_config(self, domains, config): """Enhance the configuration. - .. todo:: This needs to handle the specific enhancements offered by the - installer. We will also have to find a method to pass in the chosen - values efficiently. - :param list domains: list of domains to configure - :param redirect: If traffic should be forwarded from HTTP to HTTPS. - :type redirect: bool or None + :ivar config: Namespace typically produced by + :meth:`argparse.ArgumentParser.parse_args`. + it must have the redirect, hsts and uir attributes. + :type namespace: :class:`argparse.Namespace` :raises .errors.Error: if no installer is specified in the client. """ + if self.installer is None: logger.warning("No installer is specified, there isn't any " "configuration to enhance.") raise errors.Error("No installer available") + if config is None: + logger.warning("No config is specified.") + raise errors.Error("No config available") + + redirect = config.redirect + hsts = config.hsts + uir = config.uir # Upgrade Insecure Requests + if redirect is None: redirect = enhancements.ask("redirect") - # When support for more enhancements are added, the call to the - # plugin's `enhance` function should be wrapped by an ErrorHandler if redirect: - self.redirect_to_ssl(domains) + self.apply_enhancement(domains, "redirect") - def redirect_to_ssl(self, domains): - """Redirect all traffic from HTTP to HTTPS + if hsts: + self.apply_enhancement(domains, "ensure-http-header", + "Strict-Transport-Security") + if uir: + self.apply_enhancement(domains, "ensure-http-header", + "Upgrade-Insecure-Requests") + + msg = ("We were unable to restart web server") + if redirect or hsts or uir: + with error_handler.ErrorHandler(self._rollback_and_restart, msg): + self.installer.restart() + + def apply_enhancement(self, domains, enhancement, options=None): + """Applies an enhacement on all domains. + + :param domains: list of ssl_vhosts + :type list of str + + :param enhancement: name of enhancement, e.g. ensure-http-header + :type str + + .. note:: when more options are need make options a list. + :param options: options to enhancement, e.g. Strict-Transport-Security + :type str + + :raises .errors.PluginError: If Enhancement is not supported, or if + there is any other problem with the enhancement. - :param vhost: list of ssl_vhosts - :type vhost: :class:`letsencrypt.interfaces.IInstaller` """ - msg = ("We were unable to set up a redirect for your server, " - "however, we successfully installed your certificate.") + msg = ("We were unable to set up enhancement %s for your server, " + "however, we successfully installed your certificate." + % (enhancement)) with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg): for dom in domains: try: - self.installer.enhance(dom, "redirect") + self.installer.enhance(dom, enhancement, options) + except errors.PluginEnhancementAlreadyPresent: + logger.warn("Enhancement %s was already set.", + enhancement) except errors.PluginError: - logger.warn("Unable to perform redirect for %s", dom) + logger.warn("Unable to set enhancement %s for %s", + enhancement, dom) raise - self.installer.save("Add Redirects") - - with error_handler.ErrorHandler(self._rollback_and_restart, msg): - self.installer.restart() + self.installer.save("Add enhancement %s" % (enhancement)) def _recovery_routine_with_msg(self, success_msg): """Calls the installer's recovery routine and prints success_msg @@ -430,7 +488,7 @@ class Client(object): except: # TODO: suggest letshelp-letsencypt here reporter.add_message( - "An error occured and we failed to restore your config and " + "An error occurred and we failed to restore your config and " "restart your server. Please submit a bug report to " "https://github.com/letsencrypt/letsencrypt", reporter.HIGH_PRIORITY) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 4955655f3..a2a54d2d0 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -5,8 +5,6 @@ import re import zope.interface -from acme import challenges - from letsencrypt import constants from letsencrypt import errors from letsencrypt import interfaces @@ -80,13 +78,6 @@ class NamespaceConfig(object): return os.path.join( self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) - @property - def http01_port(self): # pylint: disable=missing-docstring - if self.namespace.http01_port is not None: - return self.namespace.http01_port - else: - return challenges.HTTP01Response.PORT - class RenewerConfiguration(object): """Configuration wrapper for renewer.""" @@ -155,8 +146,8 @@ def _check_config_domain_sanity(domains): "Punycode domains are not supported") # FQDN checks from # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/ - # Characters used, domain parts < 63 chars, tld > 1 < 7 chars + # Characters used, domain parts < 63 chars, tld > 1 < 64 chars # first and last char is not "-" - fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? {achall.URI_ROOT_PATH}/{encoded_token} # run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ "import BaseHTTPServer, SimpleHTTPServer; \\ -SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\ s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ s.serve_forever()" """ """Command template.""" @@ -90,6 +87,8 @@ s.serve_forever()" """ def add_parser_arguments(cls, add): add("test-mode", action="store_true", help="Test mode. Executes the manual command in subprocess.") + add("public-ip-logging-ok", action="store_true", + help="Automatically allows public IP logging.") def prepare(self): # pylint: disable=missing-docstring,no-self-use pass # pragma: no cover @@ -140,7 +139,7 @@ s.serve_forever()" """ # TODO(kuba): pipes still necessary? validation=pipes.quote(validation), encoded_token=achall.chall.encode("token"), - ct=achall.CONTENT_TYPE, port=port) + port=port) if self.conf("test-mode"): logger.debug("Test mode. Executing the manual command: %s", command) # sh shipped with OS X does't support echo -n, but supports printf @@ -164,26 +163,22 @@ s.serve_forever()" """ if self._httpd.poll() is not None: raise errors.Error("Couldn't execute manual command") else: - if not zope.component.getUtility(interfaces.IDisplay).yesno( - self.IP_DISCLAIMER, "Yes", "No"): - raise errors.PluginError("Must agree to IP logging to proceed") + if not self.conf("public-ip-logging-ok"): + if not zope.component.getUtility(interfaces.IDisplay).yesno( + self.IP_DISCLAIMER, "Yes", "No"): + raise errors.PluginError("Must agree to IP logging to proceed") self._notify_and_wait(self.MESSAGE_TEMPLATE.format( validation=validation, response=response, uri=achall.chall.uri(achall.domain), - ct=achall.CONTENT_TYPE, command=command)) + command=command)) - if response.simple_verify( + if not response.simple_verify( achall.chall, achall.domain, achall.account_key.public_key(), self.config.http01_port): - return response - else: - logger.error( - "Self-verify of challenge failed, authorization abandoned.") - if self.conf("test-mode") and self._httpd.poll() is not None: - # simply verify cause command failure... - return False - return None + logger.warning("Self-verify of challenge failed.") + + return response def _notify_and_wait(self, message): # pylint: disable=no-self-use # TODO: IDisplay wraps messages, breaking the command diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index a9281902f..e16fadd13 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -23,7 +23,7 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( - http01_port=8080, manual_test_mode=False) + http01_port=8080, manual_test_mode=False, manual_public_ip_logging_ok=False) self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] @@ -61,7 +61,9 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(self.achalls[0].chall.encode("token") in message) mock_verify.return_value = False - self.assertEqual([None], self.auth.perform(self.achalls)) + with mock.patch("letsencrypt.plugins.manual.logger") as mock_logger: + self.auth.perform(self.achalls) + mock_logger.warning.assert_called_once_with(mock.ANY) @mock.patch("letsencrypt.plugins.manual.zope.component.getUtility") @mock.patch("letsencrypt.plugins.manual.Authenticator._notify_and_wait") @@ -87,20 +89,6 @@ class AuthenticatorTest(unittest.TestCase): self.assertRaises( errors.Error, self.auth_test_mode.perform, self.achalls) - @mock.patch("letsencrypt.plugins.manual.socket.socket") - @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True) - @mock.patch("acme.challenges.HTTP01Response.simple_verify", - autospec=True) - @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True) - def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep, - mock_socket): - mock_popen.return_value.poll.side_effect = [None, 10] - mock_popen.return_value.pid = 1234 - mock_verify.return_value = False - self.assertEqual([False], self.auth_test_mode.perform(self.achalls)) - self.assertEqual(1, mock_sleep.call_count) - self.assertEqual(1, mock_socket.call_count) - def test_cleanup_test_mode_already_terminated(self): # pylint: disable=protected-access self.auth_test_mode._httpd = httpd = mock.Mock() diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index 42bfe312b..0b81d45b5 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -1,46 +1,8 @@ -"""Webroot plugin. - -Content-Type ------------- - -This plugin requires your webserver to use a specific `Content-Type` -header in the HTTP response. - -Apache2 -~~~~~~~ - -.. note:: Instructions written and tested for Debian Jessie. Other - operating systems might use something very similar, but you might - still need to readjust some commands. - -Create ``/etc/apache2/conf-available/letsencrypt.conf``, with -the following contents:: - - - - Header set Content-Type "text/plain" - - - -and then run ``a2enmod headers; a2enconf letsencrypt``; depending on the -output you will have to either ``service apache2 restart`` or ``service -apache2 reload``. - -nginx -~~~~~ - -Use the following snippet in your ``server{...}`` stanza:: - - location ~ /.well-known/acme-challenge/(.*) { - default_type text/plain; - } - -and reload your daemon. - -""" +"""Webroot plugin.""" import errno import logging import os +import stat import zope.interface @@ -72,7 +34,9 @@ to serve all files under specified web root ({0}).""" @classmethod def add_parser_arguments(cls, add): - add("path", help="public_html / webroot path") + # --webroot-path and --webroot-map are added in cli.py because they + # are parsed in conjunction with --domains + pass def get_chall_pref(self, domain): # pragma: no cover # pylint: disable=missing-docstring,no-self-use,unused-argument @@ -80,34 +44,54 @@ to serve all files under specified web root ({0}).""" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - self.full_root = None + self.full_roots = {} def prepare(self): # pylint: disable=missing-docstring - path = self.conf("path") - if path is None: + path_map = self.conf("map") + + if not path_map: raise errors.PluginError("--{0} must be set".format( self.option_name("path"))) - if not os.path.isdir(path): - raise errors.PluginError( - path + " does not exist or is not a directory") - self.full_root = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH) + for name, path in path_map.items(): + if not os.path.isdir(path): + raise errors.PluginError(path + " does not exist or is not a directory") + self.full_roots[name] = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH) - logger.debug("Creating root challenges validation dir at %s", - self.full_root) - try: - os.makedirs(self.full_root) - except OSError as exception: - if exception.errno != errno.EEXIST: - raise errors.PluginError( - "Couldn't create root for http-01 " - "challenge responses: {0}", exception) + logger.debug("Creating root challenges validation dir at %s", + self.full_roots[name]) + try: + os.makedirs(self.full_roots[name]) + # Set permissions as parent directory (GH #1389) + # We don't use the parameters in makedirs because it + # may not always work + # https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python + stat_path = os.stat(path) + filemode = stat.S_IMODE(stat_path.st_mode) + os.chmod(self.full_roots[name], filemode) + # Set owner and group, too + os.chown(self.full_roots[name], stat_path.st_uid, + stat_path.st_gid) + + except OSError as exception: + if exception.errno != errno.EEXIST: + raise errors.PluginError( + "Couldn't create root for {0} http-01 " + "challenge responses: {1}", name, exception) def perform(self, achalls): # pylint: disable=missing-docstring - assert self.full_root is not None + assert self.full_roots, "Webroot plugin appears to be missing webroot map" return [self._perform_single(achall) for achall in achalls] def _path_for_achall(self, achall): - return os.path.join(self.full_root, achall.chall.encode("token")) + try: + path = self.full_roots[achall.domain] + except IndexError: + raise errors.PluginError("Missing --webroot-path for domain: {1}" + .format(achall.domain)) + if not os.path.exists(path): + raise errors.PluginError("Mysteriously missing path {0} for domain: {1}" + .format(path, achall.domain)) + return os.path.join(path, achall.chall.encode("token")) def _perform_single(self, achall): response, validation = achall.response_and_validation() @@ -115,6 +99,15 @@ to serve all files under specified web root ({0}).""" logger.debug("Attempting to save validation to %s", path) with open(path, "w") as validation_file: validation_file.write(validation.encode()) + + # Set permissions as parent directory (GH #1389) + parent_path = self.full_roots[achall.domain] + stat_parent_path = os.stat(parent_path) + filemode = stat.S_IMODE(stat_parent_path.st_mode) + # Remove execution bit (not needed for this file) + os.chmod(path, filemode & ~stat.S_IEXEC) + os.chown(path, stat_parent_path.st_uid, stat_parent_path.st_gid) + return response def cleanup(self, achalls): # pylint: disable=missing-docstring diff --git a/letsencrypt/plugins/webroot_test.py b/letsencrypt/plugins/webroot_test.py index aa8f16e38..e7f96b50d 100644 --- a/letsencrypt/plugins/webroot_test.py +++ b/letsencrypt/plugins/webroot_test.py @@ -3,6 +3,7 @@ import os import shutil import tempfile import unittest +import stat import mock @@ -23,7 +24,7 @@ class AuthenticatorTest(unittest.TestCase): """Tests for letsencrypt.plugins.webroot.Authenticator.""" achall = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.HTTP01_P, domain=None, account_key=KEY) + challb=acme_util.HTTP01_P, domain="thing.com", account_key=KEY) def setUp(self): from letsencrypt.plugins.webroot import Authenticator @@ -31,7 +32,8 @@ class AuthenticatorTest(unittest.TestCase): self.validation_path = os.path.join( self.path, ".well-known", "acme-challenge", "ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ") - self.config = mock.MagicMock(webroot_path=self.path) + self.config = mock.MagicMock(webroot_path=self.path, + webroot_map={"thing.com": self.path}) self.auth = Authenticator(self.config, "webroot") self.auth.prepare() @@ -46,14 +48,16 @@ class AuthenticatorTest(unittest.TestCase): def test_add_parser_arguments(self): add = mock.MagicMock() self.auth.add_parser_arguments(add) - self.assertEqual(1, add.call_count) + self.assertEqual(0, add.call_count) # became 0 when we moved the args to cli.py! def test_prepare_bad_root(self): self.config.webroot_path = os.path.join(self.path, "null") + self.config.webroot_map["thing.com"] = self.config.webroot_path self.assertRaises(errors.PluginError, self.auth.prepare) def test_prepare_missing_root(self): self.config.webroot_path = None + self.config.webroot_map = {} self.assertRaises(errors.PluginError, self.auth.prepare) def test_prepare_full_root_exists(self): @@ -66,6 +70,23 @@ class AuthenticatorTest(unittest.TestCase): self.assertRaises(errors.PluginError, self.auth.prepare) os.chmod(self.path, 0o700) + def test_prepare_permissions(self): + + # Remove exec bit from permission check, so that it + # matches the file + self.auth.perform([self.achall]) + parent_permissions = (stat.S_IMODE(os.stat(self.path).st_mode) & + ~stat.S_IEXEC) + + actual_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode) + + self.assertEqual(parent_permissions, actual_permissions) + parent_gid = os.stat(self.path).st_gid + parent_uid = os.stat(self.path).st_uid + + self.assertEqual(os.stat(self.validation_path).st_gid, parent_gid) + self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid) + def test_perform_cleanup(self): responses = self.auth.perform([self.achall]) self.assertEqual(1, len(responses)) diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 52be94f68..7e2802b14 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -1,5 +1,6 @@ """Renewable certificates storage.""" import datetime +import logging import os import re @@ -13,6 +14,8 @@ from letsencrypt import errors from letsencrypt import error_handler from letsencrypt import le_util +logger = logging.getLogger(__name__) + ALL_FOUR = ("cert", "privkey", "chain", "fullchain") @@ -136,14 +139,17 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes """ # Each element must be referenced with an absolute path - if any(not os.path.isabs(x) for x in - (self.cert, self.privkey, self.chain, self.fullchain)): - return False + for x in (self.cert, self.privkey, self.chain, self.fullchain): + if not os.path.isabs(x): + logger.debug("Element %s is not referenced with an " + "absolute path.", x) + return False # Each element must exist and be a symbolic link - if any(not os.path.islink(x) for x in - (self.cert, self.privkey, self.chain, self.fullchain)): - return False + for x in (self.cert, self.privkey, self.chain, self.fullchain): + if not os.path.islink(x): + logger.debug("Element %s is not a symbolic link.", x) + return False for kind in ALL_FOUR: link = getattr(self, kind) where = os.path.dirname(link) @@ -157,16 +163,26 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes self.cli_config.archive_dir, self.lineagename) if not os.path.samefile(os.path.dirname(target), desired_directory): + logger.debug("Element's link does not point within the " + "cert lineage's directory within the " + "official archive directory. Link: %s, " + "target directory: %s, " + "archive directory: %s.", + link, os.path.dirname(target), desired_directory) return False # The link must point to a file that exists if not os.path.exists(target): + logger.debug("Link %s points to file %s that does not exist.", + link, target) return False # The link must point to a file that follows the archive # naming convention pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) if not pattern.match(os.path.basename(target)): + logger.debug("%s does not follow the archive naming " + "convention.", target) return False # It is NOT required that the link's target be a regular @@ -251,6 +267,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes raise errors.CertStorageError("unknown kind of item") link = getattr(self, kind) if not os.path.exists(link): + logger.debug("Expected symlink %s for %s does not exist.", + link, kind) return None target = os.readlink(link) if not os.path.isabs(target): @@ -275,11 +293,14 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) target = self.current_target(kind) if target is None or not os.path.exists(target): + logger.debug("Current-version target for %s " + "does not exist at %s.", kind, target) target = "" matches = pattern.match(os.path.basename(target)) if matches: return int(matches.groups()[0]) else: + logger.debug("No matches for target %s.", kind) return None def version(self, kind, version): @@ -529,6 +550,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Renewals on the basis of revocation if self.ocsp_revoked(self.latest_common_version()): + logger.debug("Should renew, certificate is revoked.") return True # Renewals on the basis of expiry time @@ -537,6 +559,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes "cert", self.latest_common_version())) now = pytz.UTC.fromutc(datetime.datetime.utcnow()) if expiry < add_time_interval(now, interval): + logger.debug("Should renew, certificate " + "has been expired since %s.", + expiry.strftime("%Y-%m-%d %H:%M:%S %Z")) return True return False @@ -588,6 +613,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes cli_config.live_dir): if not os.path.exists(i): os.makedirs(i, 0700) + logger.debug("Creating directory %s.", i) config_file, config_filename = le_util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) if not config_filename.endswith(".conf"): @@ -608,6 +634,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes "live directory exists for " + lineagename) os.mkdir(archive) os.mkdir(live_dir) + logger.debug("Archive directory %s and live " + "directory %s created.", archive, live_dir) relative_archive = os.path.join("..", "..", "archive", lineagename) # Put the data into the appropriate files on disk @@ -617,15 +645,19 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes os.symlink(os.path.join(relative_archive, kind + "1.pem"), target[kind]) with open(target["cert"], "w") as f: + logger.debug("Writing certificate to %s.", target["cert"]) f.write(cert) with open(target["privkey"], "w") as f: + logger.debug("Writing private key to %s.", target["privkey"]) f.write(privkey) # XXX: Let's make sure to get the file permissions right here with open(target["chain"], "w") as f: + logger.debug("Writing chain to %s.", target["chain"]) f.write(chain) with open(target["fullchain"], "w") as f: # assumes that OpenSSL.crypto.dump_certificate includes # ending newline character + logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(cert + chain) # Document what we've done in a new renewal config file @@ -640,6 +672,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes " in the renewal process"] # TODO: add human-readable comments explaining other available # parameters + logger.debug("Writing new config %s.", config_filename) new_config.write() return cls(new_config.filename, cli_config) @@ -690,16 +723,21 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes old_privkey = os.readlink(old_privkey) else: old_privkey = "privkey{0}.pem".format(prior_version) + logger.debug("Writing symlink to old private key, %s.", old_privkey) os.symlink(old_privkey, target["privkey"]) else: with open(target["privkey"], "w") as f: + logger.debug("Writing new private key to %s.", target["privkey"]) f.write(new_privkey) # Save everything else with open(target["cert"], "w") as f: + logger.debug("Writing certificate to %s.", target["cert"]) f.write(new_cert) with open(target["chain"], "w") as f: + logger.debug("Writing chain to %s.", target["chain"]) f.write(new_chain) with open(target["fullchain"], "w") as f: + logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(new_cert + new_chain) return target_version diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 71b580cf0..e0fd145e4 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -40,8 +40,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.work_dir = os.path.join(self.tmp_dir, 'work') self.logs_dir = os.path.join(self.tmp_dir, 'logs') self.standard_args = ['--text', '--config-dir', self.config_dir, - '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, - '--agree-dev-preview'] + '--work-dir', self.work_dir, '--logs-dir', + self.logs_dir, '--agree-dev-preview'] def tearDown(self): shutil.rmtree(self.tmp_dir) @@ -57,7 +57,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: - ret = cli.main(args[:]) # NOTE: parser can alter its args! + ret = cli.main(args[:]) # NOTE: parser can alter its args! return ret, stdout, stderr def _call_stdout(self, args): @@ -236,7 +236,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call(['plugins'] + list(args)) @mock.patch('letsencrypt.cli.plugins_disco') - def test_plugins_no_args(self, mock_disco): + @mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics') + def test_plugins_no_args(self, _det, mock_disco): ifaces = [] plugins = mock_disco.PluginsRegistry.find_all() @@ -247,7 +248,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods stdout.write.called_once_with(str(filtered)) @mock.patch('letsencrypt.cli.plugins_disco') - def test_plugins_init(self, mock_disco): + @mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics') + def test_plugins_init(self, _det, mock_disco): ifaces = [] plugins = mock_disco.PluginsRegistry.find_all() @@ -261,10 +263,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods stdout.write.called_once_with(str(verified)) @mock.patch('letsencrypt.cli.plugins_disco') - def test_plugins_prepare(self, mock_disco): + @mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics') + def test_plugins_prepare(self, _det, mock_disco): ifaces = [] plugins = mock_disco.PluginsRegistry.find_all() - _, stdout, _, _ = self._call(['plugins', '--init', '--prepare']) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) @@ -339,6 +341,25 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods namespace = cli.prepare_and_parse_args(plugins, long_args) self.assertEqual(namespace.domains, ['example.com', 'another.net']) + def test_parse_webroot(self): + plugins = disco.PluginsRegistry.find_all() + webroot_args = ['--webroot', '-w', '/var/www/example', + '-d', 'example.com,www.example.com', '-w', '/var/www/superfluous', + '-d', 'superfluo.us', '-d', 'www.superfluo.us'] + namespace = cli.prepare_and_parse_args(plugins, webroot_args) + self.assertEqual(namespace.webroot_map, { + 'example.com': '/var/www/example', + 'www.example.com': '/var/www/example', + 'www.superfluo.us': '/var/www/superfluous', + 'superfluo.us': '/var/www/superfluous'}) + + webroot_args = ['-d', 'stray.example.com'] + webroot_args + self.assertRaises(errors.Error, cli.prepare_and_parse_args, plugins, webroot_args) + + webroot_map_args = ['--webroot-map', '{"eg.com" : "/tmp"}'] + namespace = cli.prepare_and_parse_args(plugins, webroot_map_args) + self.assertEqual(namespace.webroot_map, {u"eg.com": u"/tmp"}) + @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.zope.component.getUtility') def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): @@ -450,9 +471,14 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): # pylint: disable=protected-access + from acme import messages + + args = mock.MagicMock() mock_open = mock.mock_open() + with mock.patch('letsencrypt.cli.open', mock_open, create=True): exception = Exception('detail') + args.verbose_count = 1 cli._handle_exception( Exception, exc_value=exception, trace=None, args=None) mock_open().write.assert_called_once_with(''.join( @@ -469,11 +495,23 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_sys.exit.assert_any_call(''.join( traceback.format_exception_only(errors.Error, error))) - args = mock.MagicMock(debug=False) + exception = messages.Error(detail='alpha', typ='urn:acme:error:triffid', + title='beta') + args = mock.MagicMock(debug=False, verbose_count=-3) cli._handle_exception( - Exception, exc_value=Exception('detail'), trace=None, args=args) + messages.Error, exc_value=exception, trace=None, args=args) error_msg = mock_sys.exit.call_args_list[-1][0][0] self.assertTrue('unexpected error' in error_msg) + self.assertTrue('acme:error' not in error_msg) + self.assertTrue('alpha' in error_msg) + self.assertTrue('beta' in error_msg) + args = mock.MagicMock(debug=False, verbose_count=1) + cli._handle_exception( + messages.Error, exc_value=exception, trace=None, args=args) + error_msg = mock_sys.exit.call_args_list[-1][0][0] + self.assertTrue('unexpected error' in error_msg) + self.assertTrue('acme:error' in error_msg) + self.assertTrue('alpha' in error_msg) interrupt = KeyboardInterrupt('detail') cli._handle_exception( @@ -499,7 +537,8 @@ class DetermineAccountTest(unittest.TestCase): """Tests for letsencrypt.cli._determine_account.""" def setUp(self): - self.args = mock.MagicMock(account=None, email=None) + self.args = mock.MagicMock(account=None, email=None, + register_unsafely_without_email=False) self.config = configuration.NamespaceConfig(self.args) self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] self.account_storage = account.AccountMemoryStorage() diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index d396e25bc..578cd77ab 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -20,11 +20,20 @@ KEY = test_util.load_vector("rsa512_key.pem") CSR_SAN = test_util.load_vector("csr-san.der") +class ConfigHelper(object): + """Creates a dummy object to imitate a namespace object + + Example: cfg = ConfigHelper(redirect=True, hsts=False, uir=False) + will result in: cfg.redirect=True, cfg.hsts=False, etc. + """ + def __init__(self, **kwds): + self.__dict__.update(kwds) + class RegisterTest(unittest.TestCase): """Tests for letsencrypt.client.register.""" def setUp(self): - self.config = mock.MagicMock(rsa_key_size=1024) + self.config = mock.MagicMock(rsa_key_size=1024, register_unsafely_without_email=False) self.account_storage = account.AccountMemoryStorage() self.tos_cb = mock.MagicMock() @@ -47,10 +56,23 @@ class RegisterTest(unittest.TestCase): def test_it(self): with mock.patch("letsencrypt.client.acme_client.Client"): - with mock.patch("letsencrypt.account." - "report_new_account"): + with mock.patch("letsencrypt.account.report_new_account"): self._call() + @mock.patch("letsencrypt.account.report_new_account") + @mock.patch("letsencrypt.client.display_ops.get_email") + def test_email_retry(self, _rep, mock_get_email): + from acme import messages + msg = "Validation of contact mailto:sousaphone@improbablylongggstring.tld failed" + mx_err = messages.Error(detail=msg, typ="malformed", title="title") + with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: + mock_client().register.side_effect = [mx_err, mock.MagicMock()] + self._call() + self.assertEqual(mock_get_email.call_count, 1) + + def test_needs_email(self): + self.config.email = None + self.assertRaises(errors.Error, self._call) class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" @@ -211,21 +233,50 @@ class ClientTest(unittest.TestCase): @mock.patch("letsencrypt.client.enhancements") def test_enhance_config(self, mock_enhancements): + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, - self.client.enhance_config, ["foo.bar"]) + self.client.enhance_config, ["foo.bar"], config) mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer - self.client.enhance_config(["foo.bar"]) - installer.enhance.assert_called_once_with("foo.bar", "redirect") + self.client.enhance_config(["foo.bar"], config) + installer.enhance.assert_called_once_with("foo.bar", "redirect", None) self.assertEqual(installer.save.call_count, 1) installer.restart.assert_called_once_with() - def test_enhance_config_no_installer(self): + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_no_ask(self, mock_enhancements): + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, - self.client.enhance_config, ["foo.bar"]) + self.client.enhance_config, ["foo.bar"], config) + + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + + config = ConfigHelper(redirect=True, hsts=False, uir=False) + self.client.enhance_config(["foo.bar"], config) + installer.enhance.assert_called_with("foo.bar", "redirect", None) + + config = ConfigHelper(redirect=False, hsts=True, uir=False) + self.client.enhance_config(["foo.bar"], config) + installer.enhance.assert_called_with("foo.bar", "ensure-http-header", + "Strict-Transport-Security") + + config = ConfigHelper(redirect=False, hsts=False, uir=True) + self.client.enhance_config(["foo.bar"], config) + installer.enhance.assert_called_with("foo.bar", "ensure-http-header", + "Upgrade-Insecure-Requests") + + self.assertEqual(installer.save.call_count, 3) + self.assertEqual(installer.restart.call_count, 3) + + def test_enhance_config_no_installer(self): + config = ConfigHelper(redirect=True, hsts=False, uir=False) + self.assertRaises(errors.Error, + self.client.enhance_config, ["foo.bar"], config) @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") @@ -236,8 +287,10 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.enhance.side_effect = errors.PluginError + config = ConfigHelper(redirect=True, hsts=False, uir=False) + self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], True) + self.client.enhance_config, ["foo.bar"], config) installer.recovery_routine.assert_called_once_with() self.assertEqual(mock_get_utility().add_message.call_count, 1) @@ -250,8 +303,10 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.save.side_effect = errors.PluginError + config = ConfigHelper(redirect=True, hsts=False, uir=False) + self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], True) + self.client.enhance_config, ["foo.bar"], config) installer.recovery_routine.assert_called_once_with() self.assertEqual(mock_get_utility().add_message.call_count, 1) @@ -264,8 +319,11 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.restart.side_effect = [errors.PluginError, None] + config = ConfigHelper(redirect=True, hsts=False, uir=False) + self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], True) + self.client.enhance_config, ["foo.bar"], config) + self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 2) @@ -280,8 +338,10 @@ class ClientTest(unittest.TestCase): installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError + config = ConfigHelper(redirect=True, hsts=False, uir=False) + self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], True) + self.client.enhance_config, ["foo.bar"], config) self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index c42b99081..a4f881d34 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -54,11 +54,6 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual(self.config.key_dir, '/tmp/config/keys') self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') - def test_http01_port(self): - self.assertEqual(4321, self.config.http01_port) - self.namespace.http01_port = None - self.assertEqual(80, self.config.http01_port) - def test_absolute_paths(self): from letsencrypt.configuration import NamespaceConfig diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 9d4a3a933..b0b905c33 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -41,9 +41,11 @@ class ChoosePluginTest(unittest.TestCase): return choose_plugin(self.plugins, "Question?") @mock.patch("letsencrypt.display.ops.util") - def test_successful_choice(self, mock_util): - mock_util().menu.return_value = (display_util.OK, 0) - self.assertEqual(self.mock_apache, self._call()) + def test_selection(self, mock_util): + mock_util().menu.side_effect = [(display_util.OK, 0), + (display_util.OK, 1)] + self.assertEqual(self.mock_stand, self._call()) + self.assertEqual(mock_util().notification.call_count, 1) @mock.patch("letsencrypt.display.ops.util") def test_more_info(self, mock_util): @@ -168,9 +170,9 @@ class GetEmailTest(unittest.TestCase): zope.component.provideUtility(mock_display, interfaces.IDisplay) @classmethod - def _call(cls): + def _call(cls, **kwargs): from letsencrypt.display.ops import get_email - return get_email() + return get_email(**kwargs) def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") @@ -178,18 +180,38 @@ class GetEmailTest(unittest.TestCase): def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("letsencrypt.display.ops.le_util" - ".safe_email") as mock_safe_email: + with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self.assertTrue(self._call() is "foo@bar.baz") def test_ok_not_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("letsencrypt.display.ops.le_util" - ".safe_email") as mock_safe_email: + with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") + def test_more_and_invalid_flags(self): + more_txt = "--register-unsafely-without-email" + invalid_txt = "There seem to be problems" + base_txt = "Enter email" + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: + mock_safe_email.return_value = True + self._call() + msg = self.input.call_args[0][0] + self.assertTrue(more_txt not in msg) + self.assertTrue(invalid_txt not in msg) + self.assertTrue(base_txt in msg) + self._call(more=True) + msg = self.input.call_args[0][0] + self.assertTrue(more_txt in msg) + self.assertTrue(invalid_txt not in msg) + self._call(more=True, invalid=True) + msg = self.input.call_args[0][0] + self.assertTrue(more_txt in msg) + self.assertTrue(invalid_txt in msg) + self.assertTrue(base_txt in msg) + class ChooseAccountTest(unittest.TestCase): """Tests for letsencrypt.display.ops.choose_account.""" diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index ed976f72d..87894f837 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -1,8 +1,10 @@ """Tests for letsencrypt.le_util.""" +import argparse import errno import os import shutil import stat +import StringIO import tempfile import unittest @@ -284,5 +286,42 @@ class SafeEmailTest(unittest.TestCase): self.assertFalse(self._call(addr), "%s failed." % addr) +class AddDeprecatedArgumentTest(unittest.TestCase): + """Test add_deprecated_argument.""" + def setUp(self): + self.parser = argparse.ArgumentParser() + + def _call(self, argument_name, nargs): + from letsencrypt.le_util import add_deprecated_argument + + add_deprecated_argument(self.parser.add_argument, argument_name, nargs) + + def test_warning_no_arg(self): + self._call("--old-option", 0) + stderr = self._get_argparse_warnings(["--old-option"]) + self.assertTrue("--old-option is deprecated" in stderr) + + def test_warning_with_arg(self): + self._call("--old-option", 1) + stderr = self._get_argparse_warnings(["--old-option", "42"]) + self.assertTrue("--old-option is deprecated" in stderr) + + def _get_argparse_warnings(self, args): + stderr = StringIO.StringIO() + with mock.patch("letsencrypt.le_util.sys.stderr", new=stderr): + self.parser.parse_args(args) + return stderr.getvalue() + + def test_help(self): + self._call("--old-option", 2) + stdout = StringIO.StringIO() + with mock.patch("letsencrypt.le_util.sys.stdout", new=stdout): + try: + self.parser.parse_args(["-h"]) + except SystemExit: + pass + self.assertTrue("--old-option" not in stdout.getvalue()) + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt_auto/letsencrypt-auto.template b/letsencrypt_auto/letsencrypt-auto.template index b67f87e54..99f0f25d8 100755 --- a/letsencrypt_auto/letsencrypt-auto.template +++ b/letsencrypt_auto/letsencrypt-auto.template @@ -44,7 +44,7 @@ DeterminePythonVersion() { ExperimentalBootstrap "Python 2.6" elif [ $PYVER -lt 26 ] ; then echo "You have an ancient version of Python entombed in your operating system..." - echo "This isn't going to work." + echo "This isn't going to work; you'll need at least version 2.6." exit 1 fi } @@ -61,8 +61,17 @@ Bootstrap() { echo "Bootstrapping dependencies for openSUSE-based OSes..." BootstrapSuseCommon elif [ -f /etc/arch-release ] ; then - echo "Bootstrapping dependencies for Archlinux..." - BootstrapArchLinux + if [ "$DEBUG" = 1 ] ; then + echo "Bootstrapping dependencies for Archlinux..." + BootstrapArchLinux + else + echo "Please use pacman to install letsencrypt packages:" + echo "# pacman -S letsencrypt letsencrypt-apache" + echo + echo "If you would like to use the virtualenv way, please run the script again with the" + echo "--debug flag." + exit 1 + fi elif [ -f /etc/manjaro-release ] ; then ExperimentalBootstrap "Manjaro Linux" BootstrapManjaro elif [ -f /etc/gentoo-release ] ; then diff --git a/tests/apache-conf-files/NEEDED.txt b/tests/apache-conf-files/NEEDED.txt new file mode 100644 index 000000000..b51956b0c --- /dev/null +++ b/tests/apache-conf-files/NEEDED.txt @@ -0,0 +1,6 @@ +Issues for which some kind of test case should be constructable, but we do not +currently have one: + +https://github.com/letsencrypt/letsencrypt/issues/1213 +https://github.com/letsencrypt/letsencrypt/issues/1602 + diff --git a/tests/apache-conf-files/failing/drupal-htaccess-1531.conf b/tests/apache-conf-files/failing/drupal-htaccess-1531.conf new file mode 100644 index 000000000..a1aab7a39 --- /dev/null +++ b/tests/apache-conf-files/failing/drupal-htaccess-1531.conf @@ -0,0 +1,149 @@ +# +# Apache/PHP/Drupal settings: +# + +# Protect files and directories from prying eyes. + + Order allow,deny + + +# Don't show directory listings for URLs which map to a directory. +Options -Indexes + +# Follow symbolic links in this directory. +Options +FollowSymLinks + +# Make Drupal handle any 404 errors. +ErrorDocument 404 /index.php + +# Set the default handler. +DirectoryIndex index.php index.html index.htm + +# Override PHP settings that cannot be changed at runtime. See +# sites/default/default.settings.php and drupal_environment_initialize() in +# includes/bootstrap.inc for settings that can be changed at runtime. + +# PHP 5, Apache 1 and 2. + + php_flag magic_quotes_gpc off + php_flag magic_quotes_sybase off + php_flag register_globals off + php_flag session.auto_start off + php_value mbstring.http_input pass + php_value mbstring.http_output pass + php_flag mbstring.encoding_translation off + + +# Requires mod_expires to be enabled. + + # Enable expirations. + ExpiresActive On + + # Cache all files for 2 weeks after access (A). + ExpiresDefault A1209600 + + + # Do not allow PHP scripts to be cached unless they explicitly send cache + # headers themselves. Otherwise all scripts would have to overwrite the + # headers set by mod_expires if they want another caching behavior. This may + # fail if an error occurs early in the bootstrap process, and it may cause + # problems if a non-Drupal PHP file is installed in a subdirectory. + ExpiresActive Off + + + +# Various rewrite rules. + + RewriteEngine on + + # Set "protossl" to "s" if we were accessed via https://. This is used later + # if you enable "www." stripping or enforcement, in order to ensure that + # you don't bounce between http and https. + RewriteRule ^ - [E=protossl] + RewriteCond %{HTTPS} on + RewriteRule ^ - [E=protossl:s] + + # Make sure Authorization HTTP header is available to PHP + # even when running as CGI or FastCGI. + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Block access to "hidden" directories whose names begin with a period. This + # includes directories used by version control systems such as Subversion or + # Git to store control files. Files whose names begin with a period, as well + # as the control files used by CVS, are protected by the FilesMatch directive + # above. + # + # NOTE: This only works when mod_rewrite is loaded. Without mod_rewrite, it is + # not possible to block access to entire directories from .htaccess, because + # is not allowed here. + # + # If you do not have mod_rewrite installed, you should remove these + # directories from your webroot or otherwise protect them from being + # downloaded. + RewriteRule "(^|/)\." - [F] + + # If your site can be accessed both with and without the 'www.' prefix, you + # can use one of the following settings to redirect users to your preferred + # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option: + # + # To redirect all users to access the site WITH the 'www.' prefix, + # (http://example.com/... will be redirected to http://www.example.com/...) + # uncomment the following: + # RewriteCond %{HTTP_HOST} . + # RewriteCond %{HTTP_HOST} !^www\. [NC] + # RewriteRule ^ http%{ENV:protossl}://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + # + # To redirect all users to access the site WITHOUT the 'www.' prefix, + # (http://www.example.com/... will be redirected to http://example.com/...) + # uncomment the following: + # RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + # RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301] + + # Modify the RewriteBase if you are using Drupal in a subdirectory or in a + # VirtualDocumentRoot and the rewrite rules are not working properly. + # For example if your site is at http://example.com/drupal uncomment and + # modify the following line: + # RewriteBase /drupal + # + # If your site is running in a VirtualDocumentRoot at http://example.com/, + # uncomment the following line: + # RewriteBase / + + # Pass all requests not referring directly to files in the filesystem to + # index.php. Clean URLs are handled in drupal_environment_initialize(). + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} !=/favicon.ico + RewriteRule ^ index.php [L] + + # Rules to correctly serve gzip compressed CSS and JS files. + # Requires both mod_rewrite and mod_headers to be enabled. + + # Serve gzip compressed CSS files if they exist and the client accepts gzip. + RewriteCond %{HTTP:Accept-encoding} gzip + RewriteCond %{REQUEST_FILENAME}\.gz -s + RewriteRule ^(.*)\.css $1\.css\.gz [QSA] + + # Serve gzip compressed JS files if they exist and the client accepts gzip. + RewriteCond %{HTTP:Accept-encoding} gzip + RewriteCond %{REQUEST_FILENAME}\.gz -s + RewriteRule ^(.*)\.js $1\.js\.gz [QSA] + + # Serve correct content types, and prevent mod_deflate double gzip. + RewriteRule .css.gz$ - [T=text/css,E=no-gzip:1] + RewriteRule .js.gz$ - [T=text/javascript,E=no-gzip:1] + + + # Serve correct encoding type. + Header set Content-Encoding gzip + # Force proxies to cache gzipped & non-gzipped css/js files separately. + Header append Vary Accept-Encoding + + + + +# Add headers to all responses. + + # Disable content sniffing, since it's an attack vector. + Header always set X-Content-Type-Options nosniff + diff --git a/tests/apache-conf-files/failing/ipv6-1143.conf b/tests/apache-conf-files/failing/ipv6-1143.conf new file mode 100644 index 000000000..ab4ed412e --- /dev/null +++ b/tests/apache-conf-files/failing/ipv6-1143.conf @@ -0,0 +1,9 @@ + +DocumentRoot /xxxx/ +ServerName noodles.net.nz +ServerAlias www.noodles.net.nz +CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined + + AllowOverride All + + diff --git a/tests/apache-conf-files/failing/ipv6-1143b.conf b/tests/apache-conf-files/failing/ipv6-1143b.conf new file mode 100644 index 000000000..25655a07c --- /dev/null +++ b/tests/apache-conf-files/failing/ipv6-1143b.conf @@ -0,0 +1,21 @@ + + +DocumentRoot /xxxx/ +ServerName noodles.net.nz +ServerAlias www.noodles.net.nz +CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined + + AllowOverride All + + + SSLEngine on + + SSLHonorCipherOrder On + SSLProtocol all -SSLv2 -SSLv3 + SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS" + + SSLCertificateFile /xxxx/noodles.net.nz.crt + SSLCertificateKeyFile /xxxx/noodles.net.nz.key + + Header set Strict-Transport-Security "max-age=31536000; preload" + diff --git a/tests/apache-conf-files/failing/multivhost-1093.conf b/tests/apache-conf-files/failing/multivhost-1093.conf new file mode 100644 index 000000000..444f0dade --- /dev/null +++ b/tests/apache-conf-files/failing/multivhost-1093.conf @@ -0,0 +1,295 @@ + + AllowOverride None + Require all denied + + + + DocumentRoot /var/www/sjau.ch/web + + ServerName sjau.ch + ServerAlias www.sjau.ch + ServerAdmin webmaster@sjau.ch + + ErrorLog /var/log/ispconfig/httpd/sjau.ch/error.log + + Alias /error/ "/var/www/sjau.ch/web/error/" + ErrorDocument 400 /error/400.html + ErrorDocument 401 /error/401.html + ErrorDocument 403 /error/403.html + ErrorDocument 404 /error/404.html + ErrorDocument 405 /error/405.html + ErrorDocument 500 /error/500.html + ErrorDocument 502 /error/502.html + ErrorDocument 503 /error/503.html + + + + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + + + Options +ExecCGI + + RubyRequire apache/ruby-run + #RubySafeLevel 0 + AddType text/html .rb + AddType text/html .rbx + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + + + + + + SetHandler mod_python + + PythonHandler mod_python.publisher + PythonDebug On + + + + # cgi enabled + + Require all granted + + ScriptAlias /cgi-bin/ /var/www/clients/client1/web2/cgi-bin/ + + SetHandler cgi-script + + # suexec enabled + + SuexecUserGroup web2 client1 + + # php as fast-cgi enabled + # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html + + IdleTimeout 300 + ProcessLifeTime 3600 + # MaxProcessCount 1000 + DefaultMinClassProcessCount 0 + DefaultMaxClassProcessCount 100 + IPCConnectTimeout 3 + IPCCommTimeout 600 + BusyTimeout 3600 + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + # add support for apache mpm_itk + + AssignUserId web2 client1 + + + + # Do not execute PHP files in webdav directory + + + SecRuleRemoveById 960015 + SecRuleRemoveById 960032 + + + SetHandler None + + + DavLockDB /var/www/clients/client1/web2/tmp/DavLock + # DO NOT REMOVE THE COMMENTS! + # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE! + # WEBDAV BEGIN + # WEBDAV END + + + + + + DocumentRoot /var/www/sjau.ch/web + + ServerName sjau.ch + ServerAlias www.sjau.ch + ServerAdmin webmaster@sjau.ch + + ErrorLog /var/log/ispconfig/httpd/sjau.ch/error.log + + Alias /error/ "/var/www/sjau.ch/web/error/" + ErrorDocument 400 /error/400.html + ErrorDocument 401 /error/401.html + ErrorDocument 403 /error/403.html + ErrorDocument 404 /error/404.html + ErrorDocument 405 /error/405.html + ErrorDocument 500 /error/500.html + ErrorDocument 502 /error/502.html + ErrorDocument 503 /error/503.html + + + + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + + + Options +ExecCGI + + RubyRequire apache/ruby-run + #RubySafeLevel 0 + AddType text/html .rb + AddType text/html .rbx + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + + + + + + SetHandler mod_python + + PythonHandler mod_python.publisher + PythonDebug On + + + + # cgi enabled + + Require all granted + + ScriptAlias /cgi-bin/ /var/www/clients/client1/web2/cgi-bin/ + + SetHandler cgi-script + + # suexec enabled + + SuexecUserGroup web2 client1 + + # php as fast-cgi enabled + # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html + + IdleTimeout 300 + ProcessLifeTime 3600 + # MaxProcessCount 1000 + DefaultMinClassProcessCount 0 + DefaultMaxClassProcessCount 100 + IPCConnectTimeout 3 + IPCCommTimeout 600 + BusyTimeout 3600 + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + # add support for apache mpm_itk + + AssignUserId web2 client1 + + + + # Do not execute PHP files in webdav directory + + + SecRuleRemoveById 960015 + SecRuleRemoveById 960032 + + + SetHandler None + + + DavLockDB /var/www/clients/client1/web2/tmp/DavLock + # DO NOT REMOVE THE COMMENTS! + # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE! + # WEBDAV BEGIN + # WEBDAV END + + + + diff --git a/tests/apache-conf-files/failing/multivhost-1093b.conf b/tests/apache-conf-files/failing/multivhost-1093b.conf new file mode 100644 index 000000000..0388abc2c --- /dev/null +++ b/tests/apache-conf-files/failing/multivhost-1093b.conf @@ -0,0 +1,593 @@ + + AllowOverride None + Require all denied + + + + DocumentRoot /var/www/ensemen.ch/web + + ServerName ensemen.ch + ServerAlias www.ensemen.ch + ServerAdmin webmaster@ensemen.ch + + ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log + + Alias /error/ "/var/www/ensemen.ch/web/error/" + ErrorDocument 400 /error/400.html + ErrorDocument 401 /error/401.html + ErrorDocument 403 /error/403.html + ErrorDocument 404 /error/404.html + ErrorDocument 405 /error/405.html + ErrorDocument 500 /error/500.html + ErrorDocument 502 /error/502.html + ErrorDocument 503 /error/503.html + + + + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + + + Options +ExecCGI + + RubyRequire apache/ruby-run + #RubySafeLevel 0 + AddType text/html .rb + AddType text/html .rbx + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + + + + + + SetHandler mod_python + + PythonHandler mod_python.publisher + PythonDebug On + + + + # cgi enabled + + Require all granted + + ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/ + + SetHandler cgi-script + + # suexec enabled + + SuexecUserGroup web17 client4 + + # php as fast-cgi enabled + # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html + + IdleTimeout 300 + ProcessLifeTime 3600 + # MaxProcessCount 1000 + DefaultMinClassProcessCount 0 + DefaultMaxClassProcessCount 100 + IPCConnectTimeout 3 + IPCCommTimeout 600 + BusyTimeout 3600 + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + # add support for apache mpm_itk + + AssignUserId web17 client4 + + + + # Do not execute PHP files in webdav directory + + + SecRuleRemoveById 960015 + SecRuleRemoveById 960032 + + + SetHandler None + + + DavLockDB /var/www/clients/client4/web17/tmp/DavLock + # DO NOT REMOVE THE COMMENTS! + # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE! + # WEBDAV BEGIN + # WEBDAV END + + + + + + DocumentRoot /var/www/ensemen.ch/web + + ServerName ensemen.ch + ServerAlias www.ensemen.ch + ServerAdmin webmaster@ensemen.ch + + ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log + + Alias /error/ "/var/www/ensemen.ch/web/error/" + ErrorDocument 400 /error/400.html + ErrorDocument 401 /error/401.html + ErrorDocument 403 /error/403.html + ErrorDocument 404 /error/404.html + ErrorDocument 405 /error/405.html + ErrorDocument 500 /error/500.html + ErrorDocument 502 /error/502.html + ErrorDocument 503 /error/503.html + + + SSLEngine on + SSLProtocol All -SSLv2 -SSLv3 + SSLCertificateFile /var/www/clients/client4/web17/ssl/ensemen.ch.crt + SSLCertificateKeyFile /var/www/clients/client4/web17/ssl/ensemen.ch.key + + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + + + Options +ExecCGI + + RubyRequire apache/ruby-run + #RubySafeLevel 0 + AddType text/html .rb + AddType text/html .rbx + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + + + + + + SetHandler mod_python + + PythonHandler mod_python.publisher + PythonDebug On + + + + # cgi enabled + + Require all granted + + ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/ + + SetHandler cgi-script + + # suexec enabled + + SuexecUserGroup web17 client4 + + # php as fast-cgi enabled + # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html + + IdleTimeout 300 + ProcessLifeTime 3600 + # MaxProcessCount 1000 + DefaultMinClassProcessCount 0 + DefaultMaxClassProcessCount 100 + IPCConnectTimeout 3 + IPCCommTimeout 600 + BusyTimeout 3600 + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + # add support for apache mpm_itk + + AssignUserId web17 client4 + + + + # Do not execute PHP files in webdav directory + + + SecRuleRemoveById 960015 + SecRuleRemoveById 960032 + + + SetHandler None + + + DavLockDB /var/www/clients/client4/web17/tmp/DavLock + # DO NOT REMOVE THE COMMENTS! + # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE! + # WEBDAV BEGIN + # WEBDAV END + + + + + + DocumentRoot /var/www/ensemen.ch/web + + ServerName ensemen.ch + ServerAlias www.ensemen.ch + ServerAdmin webmaster@ensemen.ch + + ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log + + Alias /error/ "/var/www/ensemen.ch/web/error/" + ErrorDocument 400 /error/400.html + ErrorDocument 401 /error/401.html + ErrorDocument 403 /error/403.html + ErrorDocument 404 /error/404.html + ErrorDocument 405 /error/405.html + ErrorDocument 500 /error/500.html + ErrorDocument 502 /error/502.html + ErrorDocument 503 /error/503.html + + + + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + + + Options +ExecCGI + + RubyRequire apache/ruby-run + #RubySafeLevel 0 + AddType text/html .rb + AddType text/html .rbx + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + + + + + + SetHandler mod_python + + PythonHandler mod_python.publisher + PythonDebug On + + + + # cgi enabled + + Require all granted + + ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/ + + SetHandler cgi-script + + # suexec enabled + + SuexecUserGroup web17 client4 + + # php as fast-cgi enabled + # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html + + IdleTimeout 300 + ProcessLifeTime 3600 + # MaxProcessCount 1000 + DefaultMinClassProcessCount 0 + DefaultMaxClassProcessCount 100 + IPCConnectTimeout 3 + IPCCommTimeout 600 + BusyTimeout 3600 + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + # add support for apache mpm_itk + + AssignUserId web17 client4 + + + + # Do not execute PHP files in webdav directory + + + SecRuleRemoveById 960015 + SecRuleRemoveById 960032 + + + SetHandler None + + + DavLockDB /var/www/clients/client4/web17/tmp/DavLock + # DO NOT REMOVE THE COMMENTS! + # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE! + # WEBDAV BEGIN + # WEBDAV END + + + + + + DocumentRoot /var/www/ensemen.ch/web + + ServerName ensemen.ch + ServerAlias www.ensemen.ch + ServerAdmin webmaster@ensemen.ch + + ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log + + Alias /error/ "/var/www/ensemen.ch/web/error/" + ErrorDocument 400 /error/400.html + ErrorDocument 401 /error/401.html + ErrorDocument 403 /error/403.html + ErrorDocument 404 /error/404.html + ErrorDocument 405 /error/405.html + ErrorDocument 500 /error/500.html + ErrorDocument 502 /error/502.html + ErrorDocument 503 /error/503.html + + + SSLEngine on + SSLProtocol All -SSLv2 -SSLv3 + SSLCertificateFile /var/www/clients/client4/web17/ssl/ensemen.ch.crt + SSLCertificateKeyFile /var/www/clients/client4/web17/ssl/ensemen.ch.key + + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + # Clear PHP settings of this website + + SetHandler None + + Options +FollowSymLinks + AllowOverride All + Require all granted + + + + + Options +ExecCGI + + RubyRequire apache/ruby-run + #RubySafeLevel 0 + AddType text/html .rb + AddType text/html .rbx + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + SetHandler ruby-object + RubyHandler Apache::RubyRun.instance + + + + + + + + SetHandler mod_python + + PythonHandler mod_python.publisher + PythonDebug On + + + + # cgi enabled + + Require all granted + + ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/ + + SetHandler cgi-script + + # suexec enabled + + SuexecUserGroup web17 client4 + + # php as fast-cgi enabled + # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html + + IdleTimeout 300 + ProcessLifeTime 3600 + # MaxProcessCount 1000 + DefaultMinClassProcessCount 0 + DefaultMaxClassProcessCount 100 + IPCConnectTimeout 3 + IPCCommTimeout 600 + BusyTimeout 3600 + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + SetHandler fcgid-script + + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4 + FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5 + Options +ExecCGI + AllowOverride All + Require all granted + + + + # add support for apache mpm_itk + + AssignUserId web17 client4 + + + + # Do not execute PHP files in webdav directory + + + SecRuleRemoveById 960015 + SecRuleRemoveById 960032 + + + SetHandler None + + + DavLockDB /var/www/clients/client4/web17/tmp/DavLock + # DO NOT REMOVE THE COMMENTS! + # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE! + # WEBDAV BEGIN + # WEBDAV END + + + + diff --git a/tests/apache-conf-files/passing/README.modules b/tests/apache-conf-files/passing/README.modules new file mode 100644 index 000000000..9c5853061 --- /dev/null +++ b/tests/apache-conf-files/passing/README.modules @@ -0,0 +1,5 @@ +Modules required to parse these conf files: + +ssl +rewrite +macro diff --git a/tests/apache-conf-files/passing/anarcat-1531.conf b/tests/apache-conf-files/passing/anarcat-1531.conf new file mode 100644 index 000000000..73a9b746c --- /dev/null +++ b/tests/apache-conf-files/passing/anarcat-1531.conf @@ -0,0 +1,14 @@ + + ServerAdmin root@localhost + ServerName anarcat.wiki.orangeseeds.org:80 + + + UserDir disabled + + RewriteEngine On + RewriteRule ^/(.*) http\:\/\/anarc\.at\/$1 [L,R,NE] + + ErrorLog /var/log/apache2/1531error.log + LogLevel warn + CustomLog /var/log/apache2/1531access.log combined + diff --git a/tests/apache-conf-files/passing/example-ssl.conf b/tests/apache-conf-files/passing/example-ssl.conf new file mode 100644 index 000000000..466ac9ce3 --- /dev/null +++ b/tests/apache-conf-files/passing/example-ssl.conf @@ -0,0 +1,136 @@ + + ServerName example.com + ServerAlias www.example.com + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf + + # SSL Engine Switch: + # Enable/Disable SSL for this virtual host. + SSLEngine on + + # A self-signed (snakeoil) certificate can be created by installing + # the ssl-cert package. See + # /usr/share/doc/apache2/README.Debian.gz for more info. + # If both key and certificate are stored in the same file, only the + # SSLCertificateFile directive is needed. + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.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/apache2/ssl.crt/server-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/certs/ + #SSLCACertificateFile /etc/apache2/ssl.crt/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/apache2/ssl.crl/ + #SSLCARevocationFile /etc/apache2/ssl.crl/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 + + # 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 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-6]" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + # MSIE 7 and newer should be able to use keepalive + BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/tests/apache-conf-files/passing/example.conf b/tests/apache-conf-files/passing/example.conf new file mode 100644 index 000000000..60bdeead6 --- /dev/null +++ b/tests/apache-conf-files/passing/example.conf @@ -0,0 +1,32 @@ + + # The ServerName directive sets the request scheme, hostname and port that + # the server uses to identify itself. This is used when creating + # redirection URLs. In the context of virtual hosts, the ServerName + # specifies what hostname must appear in the request's Host: header to + # match this virtual host. For the default virtual host (this file) this + # value is not decisive as it is used as a last resort host regardless. + # However, you must set it for any further virtual host explicitly. + ServerName www.example.com + ServerAlias example.com + + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/tests/apache-conf-files/passing/finalize-1243.apache2.conf.txt b/tests/apache-conf-files/passing/finalize-1243.apache2.conf.txt new file mode 100644 index 000000000..73dc64223 --- /dev/null +++ b/tests/apache-conf-files/passing/finalize-1243.apache2.conf.txt @@ -0,0 +1,222 @@ +# This is the main Apache server configuration file. It contains the +# configuration directives that give the server its instructions. +# See http://httpd.apache.org/docs/2.4/ for detailed information about +# the directives and /usr/share/doc/apache2/README.Debian about Debian specific +# hints. +# +# +# Summary of how the Apache 2 configuration works in Debian: +# The Apache 2 web server configuration in Debian is quite different to +# upstream's suggested way to configure the web server. This is because Debian's +# default Apache2 installation attempts to make adding and removing modules, +# virtual hosts, and extra configuration directives as flexible as possible, in +# order to make automating the changes and administering the server as easy as +# possible. + +# It is split into several files forming the configuration hierarchy outlined +# below, all located in the /etc/apache2/ directory: +# +# /etc/apache2/ +# |-- apache2.conf +# | `-- ports.conf +# |-- mods-enabled +# | |-- *.load +# | `-- *.conf +# |-- conf-enabled +# | `-- *.conf +# `-- sites-enabled +# `-- *.conf +# +# +# * apache2.conf is the main configuration file (this file). It puts the pieces +# together by including all remaining configuration files when starting up the +# web server. +# +# * ports.conf is always included from the main configuration file. It is +# supposed to determine listening ports for incoming connections which can be +# customized anytime. +# +# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/ +# directories contain particular configuration snippets which manage modules, +# global configuration fragments, or virtual host configurations, +# respectively. +# +# They are activated by symlinking available configuration files from their +# respective *-available/ counterparts. These should be managed by using our +# helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See +# their respective man pages for detailed information. +# +# * The binary is called apache2. Due to the use of environment variables, in +# the default configuration, apache2 needs to be started/stopped with +# /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not +# work with the default configuration. + + +# Global configuration +# + +# +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# NOTE! If you intend to place this on an NFS (or otherwise network) +# mounted filesystem then please read the Mutex documentation (available +# at ); +# you will save yourself a lot of trouble. +# +# Do NOT add a slash at the end of the directory path. +# +#ServerRoot "/etc/apache2" + +# +# The accept serialization lock file MUST BE STORED ON A LOCAL DISK. +# +Mutex file:${APACHE_LOCK_DIR} default + +# +# PidFile: The file in which the server should record its process +# identification number when it starts. +# This needs to be set in /etc/apache2/envvars +# +PidFile ${APACHE_PID_FILE} + +# +# 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 5 + + +# These need to be set in /etc/apache2/envvars +User ${APACHE_RUN_USER} +Group ${APACHE_RUN_GROUP} + +# +# 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 + +# 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 ${APACHE_LOG_DIR}/error.log + +# +# LogLevel: Control the severity of messages logged to the error_log. +# Available values: trace8, ..., trace1, debug, info, notice, warn, +# error, crit, alert, emerg. +# It is also possible to configure the log level for particular modules, e.g. +# "LogLevel info ssl:warn" +# +LogLevel warn + +# Include module configuration: +IncludeOptional mods-enabled/*.load +IncludeOptional mods-enabled/*.conf + +# Include list of ports to listen on +Include ports.conf + + +# Sets the default security model of the Apache2 HTTPD server. It does +# not allow access to the root filesystem outside of /usr/share and /var/www. +# The former is used by web applications packaged in Debian, +# the latter may be used for local directories served by the web server. If +# your system is serving content from a sub-directory in /srv you must allow +# access here, or in any related virtual host. + + Options FollowSymLinks + AllowOverride None + Require all denied + + + + AllowOverride None + Require all granted + + + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + +# +# Options Indexes FollowSymLinks +# AllowOverride None +# Require all granted +# + +# AccessFileName: The name of the file to look for in each directory +# for additional configuration directives. See also the AllowOverride +# directive. +# +AccessFileName .htaccess + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + + +# +# The following directives define some format nicknames for use with +# a CustomLog directive. +# +# These deviate from the Common Log Format definitions in that they use %O +# (the actual bytes sent including headers) instead of %b (the size of the +# requested file), because the latter makes it impossible to detect partial +# requests. +# +# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended. +# Use mod_remoteip instead. +# +#LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined +LogFormat "%t \"%r\" %>s %O \"%{User-Agent}i\"" vhost_combined + +#LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined +#LogFormat "%h %l %u %t \"%r\" %>s %O" common +LogFormat "- %t \"%r\" %>s %b" noip + +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-agent}i" agent + +# Include of directories ignores editors' and dpkg's backup files, +# see README.Debian for details. + +# Include generic snippets of statements +IncludeOptional conf-enabled/*.conf + +# Include the virtual host configurations: +#IncludeOptional sites-enabled/*.conf + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/tests/apache-conf-files/passing/finalize-1243.conf b/tests/apache-conf-files/passing/finalize-1243.conf new file mode 100644 index 000000000..0918e5669 --- /dev/null +++ b/tests/apache-conf-files/passing/finalize-1243.conf @@ -0,0 +1,67 @@ +#LoadModule ssl_module modules/mod_ssl.so + +Listen 443 + + # The ServerName directive sets the request scheme, hostname and port that + # the server uses to identify itself. This is used when creating + # redirection URLs. In the context of virtual hosts, the ServerName + # specifies what hostname must appear in the request's Host: header to + # match this virtual host. For the default virtual host (this file) this + # value is not decisive as it is used as a last resort host regardless. + # However, you must set it for any further virtual host explicitly. + ServerName www.eiserneketten.de + + SSLEngine on + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log noip + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf + + Options FollowSymLinks + AllowOverride None + Order Deny,Allow + #Deny from All + + + Alias / /eiserneketten/pages/eiserneketten.html +SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem +SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key +SSLCertificateChainFile /etc/ssl/certs/ssl-cert-snakeoil.pem +Include /etc/letsencrypt/options-ssl-apache.conf + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet + +# +# Directives to allow use of AWStats as a CGI +# +Alias /awstatsclasses "/usr/local/awstats/wwwroot/classes/" +Alias /awstatscss "/usr/local/awstats/wwwroot/css/" +Alias /awstatsicons "/usr/local/awstats/wwwroot/icon/" +ScriptAlias /awstats/ "/usr/local/awstats/wwwroot/cgi-bin/" + +# +# This is to permit URL access to scripts/files in AWStats directory. +# + + Options None + AllowOverride None + Order allow,deny + Allow from all + + diff --git a/tests/apache-conf-files/passing/modmacro-1385.conf b/tests/apache-conf-files/passing/modmacro-1385.conf new file mode 100644 index 000000000..d327c9421 --- /dev/null +++ b/tests/apache-conf-files/passing/modmacro-1385.conf @@ -0,0 +1,33 @@ + + + # The ServerName directive sets the request scheme, hostname and port that + # the server uses to identify itself. This is used when creating + # redirection URLs. In the context of virtual hosts, the ServerName + # specifies what hostname must appear in the request's Host: header to + # match this virtual host. For the default virtual host (this file) this + # value is not decisive as it is used as a last resort host regardless. + # However, you must set it for any further virtual host explicitly. + ServerName $host + + ServerAdmin webmaster@localhost + DocumentRoot $dir + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf + + +Use Vhost goxogle.com 80 /var/www/goxogle/ +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/tests/apache-conf-files/passing/owncloud-1264.conf b/tests/apache-conf-files/passing/owncloud-1264.conf new file mode 100644 index 000000000..d0ac81fa3 --- /dev/null +++ b/tests/apache-conf-files/passing/owncloud-1264.conf @@ -0,0 +1,13 @@ +Alias /owncloud /usr/share/owncloud + + + Options +FollowSymLinks + AllowOverride All + + order allow,deny + allow from all + + = 2.3> + Require all granted + + diff --git a/tests/apache-conf-files/passing/roundcube-1222.conf b/tests/apache-conf-files/passing/roundcube-1222.conf new file mode 100644 index 000000000..72ced7fb3 --- /dev/null +++ b/tests/apache-conf-files/passing/roundcube-1222.conf @@ -0,0 +1,61 @@ +# Those aliases do not work properly with several hosts on your apache server +# Uncomment them to use it or adapt them to your configuration +# Alias /roundcube/program/js/tiny_mce/ /usr/share/tinymce/www/ +# Alias /roundcube /var/lib/roundcube + +# Access to tinymce files + + Options Indexes MultiViews FollowSymLinks + AllowOverride None + = 2.3> + Require all granted + + + Order allow,deny + Allow from all + + + + + Options +FollowSymLinks + # This is needed to parse /var/lib/roundcube/.htaccess. See its + # content before setting AllowOverride to None. + AllowOverride All + = 2.3> + Require all granted + + + Order allow,deny + Allow from all + + + +# Protecting basic directories: + + Options -FollowSymLinks + AllowOverride None + + + + Options -FollowSymLinks + AllowOverride None + = 2.3> + Require all denied + + + Order allow,deny + Deny from all + + + + + Options -FollowSymLinks + AllowOverride None + = 2.3> + Require all denied + + + Order allow,deny + Deny from all + + diff --git a/tests/apache-conf-files/passing/semacode-1598.conf b/tests/apache-conf-files/passing/semacode-1598.conf new file mode 100644 index 000000000..89e2fb25c --- /dev/null +++ b/tests/apache-conf-files/passing/semacode-1598.conf @@ -0,0 +1,44 @@ + + ServerName semacode.com + ServerAlias www.semacode.com + DocumentRoot /tmp/ + TransferLog /tmp/access + ErrorLog /tmp/error + Redirect /posts/rss http://semacode.com/feed + Redirect permanent /weblog http://semacode.com/blog + +#ProxyPreserveHost On +# ProxyPass /past http://old.semacode.com + #ProxyPassReverse /past http://old.semacode.com +# + # Order allow,deny + #Allow from all +# + + Redirect /stylesheets/inside.css http://old.semacode.com/stylesheets/inside.css + RedirectMatch /images/portal/(.*) http://old.semacode.com/images/portal/$1 + Redirect /images/invisible.gif http://old.semacode.com/images/invisible.gif + RedirectMatch /javascripts/(.*) http://old.semacode.com/javascripts/$1 + + RewriteEngine on + RewriteRule ^/past/(.*) http://old.semacode.com/past/$1 [L,P] + RewriteCond %{HTTP_HOST} !^semacode\.com$ [NC] + RewriteCond %{HTTP_HOST} !^$ + RewriteRule ^/(.*) http://semacode.com/$1 [L,R] + + + + + + ServerName old.semacode.com + ServerAlias www.old.semacode.com + DocumentRoot /home/simon/semacode-server/semacode/website/trunk/public + TransferLog /tmp/access-old + ErrorLog /tmp/error-old + + Options FollowSymLinks + AllowOverride None + Order allow,deny + Allow from all + + diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh new file mode 100755 index 000000000..0d8a3de38 --- /dev/null +++ b/tests/boulder-fetch.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Download and run Boulder instance for integration testing + +# ugh, go version output is like: +# go version go1.4.2 linux/amd64 +GOVER=`go version | cut -d" " -f3 | cut -do -f2` + +# version comparison +function verlte { + #OS X doesn't support version sorting; emulate with sed + if [ `uname` == 'Darwin' ]; then + [ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \ + | sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ] + else + [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ] + fi +} + +if ! verlte 1.5 "$GOVER" ; then + echo "We require go version 1.5 or later; you have... $GOVER" + exit 1 +fi + +set -xe + +# `/...` avoids `no buildable Go source files` errors, for more info +# see `go help packages` +go get -d github.com/letsencrypt/boulder/... +cd $GOPATH/src/github.com/letsencrypt/boulder +# goose is needed for ./test/create_db.sh +wget https://github.com/jsha/boulder-tools/raw/master/goose.gz && \ + mkdir $GOPATH/bin && \ + zcat goose.gz > $GOPATH/bin/goose && \ + chmod +x $GOPATH/bin/goose +./test/create_db.sh +go run cmd/rabbitmq-setup/main.go -server amqp://localhost +# listenbuddy is needed for ./start.py +go get github.com/jsha/listenbuddy +cd - + diff --git a/tests/boulder-start.sh b/tests/boulder-start.sh index 47c1b6278..acf8f0bbf 100755 --- a/tests/boulder-start.sh +++ b/tests/boulder-start.sh @@ -1,43 +1,9 @@ #!/bin/bash -# Download and run Boulder instance for integration testing - - -# ugh, go version output is like: -# go version go1.4.2 linux/amd64 -GOVER=`go version | cut -d" " -f3 | cut -do -f2` - -# version comparison -function verlte { - #OS X doesn't support version sorting; emulate with sed - if [ `uname` == 'Darwin' ]; then - [ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \ - | sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ] - else - [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ] - fi -} - -if ! verlte 1.5 "$GOVER" ; then - echo "We require go version 1.5 or later; you have... $GOVER" - exit 1 -fi - -set -xe export GOPATH="${GOPATH:-/tmp/go}" export PATH="$GOPATH/bin:$PATH" -# `/...` avoids `no buildable Go source files` errors, for more info -# see `go help packages` -go get -d github.com/letsencrypt/boulder/... +./tests/boulder-fetch.sh + cd $GOPATH/src/github.com/letsencrypt/boulder -# goose is needed for ./test/create_db.sh -wget https://github.com/jsha/boulder-tools/raw/master/goose.gz && \ - mkdir $GOPATH/bin && \ - zcat goose.gz > $GOPATH/bin/goose && \ - chmod +x $GOPATH/bin/goose -./test/create_db.sh -# listenbuddy is needed for ./start.py -go get github.com/jsha/listenbuddy -./start.py & -# Hopefully start.py bootstraps before integration test is started... +./start.py diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index dbc473728..71d745d93 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -23,7 +23,7 @@ letsencrypt_test () { --no-redirect \ --agree-dev-preview \ --agree-tos \ - --email "" \ + --register-unsafely-without-email \ --renew-by-default \ --debug \ -vvvvvvv \ diff --git a/tests/travis-integration.sh b/tests/travis-integration.sh new file mode 100755 index 000000000..3b507bb86 --- /dev/null +++ b/tests/travis-integration.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -o errexit + +./tests/boulder-fetch.sh + +source .tox/$TOXENV/bin/activate + +export LETSENCRYPT_PATH=`pwd` + +cd $GOPATH/src/github.com/letsencrypt/boulder/ + +# boulder's integration-test.py has code that knows to start and wait for the +# boulder processes to start reliably and then will run the letsencrypt +# boulder-interation.sh on its own. The --letsencrypt flag says to run only the +# letsencrypt tests (instead of any other client tests it might run). We're +# going to want to define a more robust interaction point between the boulder +# and letsencrypt tests, but that will be better built off of this. +python test/integration-test.py --letsencrypt diff --git a/tools/dev-release.sh b/tools/dev-release.sh index 6bbc6ced4..d8c720559 100755 --- a/tools/dev-release.sh +++ b/tools/dev-release.sh @@ -1,10 +1,12 @@ #!/bin/sh -xe # Release dev packages to PyPI +# Needed to fix problems with git signatures and pinentry +export GPG_TTY=$(tty) + version="0.0.0.dev$(date +%Y%m%d)" DEV_RELEASE_BRANCH="dev-release" -# TODO: create a real release key instead of using Kuba's personal one -RELEASE_GPG_KEY="${RELEASE_GPG_KEY:-148C30F6F7E429337A72D992B00B9CC82D7ADF2C}" +RELEASE_GPG_KEY=A2CFB51FA275A7286234E7B24D17C995CD9775F2 # port for a local Python Package Index (used in testing) PORT=${PORT:-1234} @@ -23,7 +25,7 @@ mv "dist.$version" "dist.$version.$(date +%s).bak" || true git tag --delete "$tag" || true tmpvenv=$(mktemp -d) -virtualenv --no-site-packages $tmpvenv +virtualenv --no-site-packages -p python2 $tmpvenv . $tmpvenv/bin/activate # update setuptools/pip just like in other places in the repo pip install -U setuptools @@ -49,7 +51,7 @@ done sed -i "s/^__version.*/__version__ = '$version'/" letsencrypt/__init__.py git add -p # interactive user input -git -c commit.gpgsign=true commit -m "Release $version" +git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version" git tag --local-user "$RELEASE_GPG_KEY" \ --sign --message "Release $version" "$tag"