mirror of
https://github.com/certbot/certbot.git
synced 2026-06-09 08:42:57 -04:00
Merge master in to get up to date.
This commit is contained in:
commit
4abe7ab93d
78 changed files with 3193 additions and 674 deletions
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
156
README.rst
156
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
|
||||
<https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md>`_
|
||||
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
|
||||
<https://letsencrypt.readthedocs.org/en/latest/using.html#installation>`_.
|
||||
|
||||
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 <https://letsencrypt.readthedocs.org/en/latest/using.html#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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
# SLE12 dont have python-virtualenv
|
||||
# SLE12 don't have python-virtualenv
|
||||
|
||||
zypper -nq in -l git-core \
|
||||
python \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt_apache.dvsni`
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: letsencrypt_apache.dvsni
|
||||
:members:
|
||||
5
letsencrypt-apache/docs/api/tls_sni_01.rst
Normal file
5
letsencrypt-apache/docs/api/tls_sni_01.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt_apache.tls_sni_01`
|
||||
------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt_apache.tls_sni_01
|
||||
:members:
|
||||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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 <VirtualHost>
|
||||
# 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<IfModule mod_ssl.c>
|
||||
<VirtualHost _default_:443>
|
||||
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
|
||||
<FilesMatch "\.(cgi|shtml|phtml|php)$">
|
||||
SSLOptions +StdEnvVars
|
||||
</FilesMatch>
|
||||
<Directory /usr/lib/cgi-bin>
|
||||
SSLOptions +StdEnvVars
|
||||
</Directory>
|
||||
|
||||
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
|
||||
|
||||
</VirtualHost>
|
||||
</IfModule>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
|
|
@ -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__":
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = "<IfModule mod_ssl.c>\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
|
||||
BIN
letsencrypt-compatibility-test/letsencrypt_compatibility_test/testdata/configs.tar.gz
vendored
Normal file
BIN
letsencrypt-compatibility-test/letsencrypt_compatibility_test/testdata/configs.tar.gz
vendored
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt_nginx.dvsni`
|
||||
------------------------------
|
||||
|
||||
.. automodule:: letsencrypt_nginx.dvsni
|
||||
:members:
|
||||
5
letsencrypt-nginx/docs/api/tls_sni_01.rst
Normal file
5
letsencrypt-nginx/docs/api/tls_sni_01.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt_nginx.tls_sni_01`
|
||||
-----------------------------------
|
||||
|
||||
.. automodule:: letsencrypt_nginx.tls_sni_01
|
||||
:members:
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}(?<!-)\\.)+[A-Za-z]{2,6}$")
|
||||
fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,63}$")
|
||||
if any(True for d in domains if not fqdn.match(d)):
|
||||
raise errors.ConfigurationError("Requested domain is not a FQDN")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ CLI_DEFAULTS = dict(
|
|||
work_dir="/var/lib/letsencrypt",
|
||||
logs_dir="/var/log/letsencrypt",
|
||||
no_verify_ssl=False,
|
||||
http01_port=challenges.HTTP01Response.PORT,
|
||||
tls_sni_01_port=challenges.TLSSNI01Response.PORT,
|
||||
|
||||
auth_cert_path="./cert.pem",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,15 @@ def choose_plugin(prepared, question):
|
|||
question, opts, help_label="More Info")
|
||||
|
||||
if code == display_util.OK:
|
||||
return prepared[index]
|
||||
plugin_ep = prepared[index]
|
||||
if plugin_ep.misconfigured:
|
||||
util(interfaces.IDisplay).notification(
|
||||
"The selected plugin encountered an error while parsing "
|
||||
"your server configuration and cannot be used. The error "
|
||||
"was:\n\n{0}".format(plugin_ep.prepare()),
|
||||
height=display_util.HEIGHT, pause=False)
|
||||
else:
|
||||
return plugin_ep
|
||||
elif code == display_util.HELP:
|
||||
if prepared[index].misconfigured:
|
||||
msg = "Reported Error: %s" % prepared[index].prepare()
|
||||
|
|
@ -114,23 +122,36 @@ def pick_configurator(
|
|||
config, default, plugins, question,
|
||||
(interfaces.IAuthenticator, interfaces.IInstaller))
|
||||
|
||||
|
||||
def get_email():
|
||||
def get_email(more=False, invalid=False):
|
||||
"""Prompt for valid email address.
|
||||
|
||||
:param bool more: explain why the email is strongly advisable, but how to
|
||||
skip it
|
||||
:param bool invalid: true if the user just typed something, but it wasn't
|
||||
a valid-looking email
|
||||
|
||||
:returns: Email or ``None`` if cancelled by user.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
while True:
|
||||
code, email = zope.component.getUtility(interfaces.IDisplay).input(
|
||||
"Enter email address (used for urgent notices and lost key recovery)")
|
||||
msg = "Enter email address (used for urgent notices and lost key recovery)"
|
||||
if invalid:
|
||||
msg = "There seem to be problems with that address. " + msg
|
||||
if more:
|
||||
msg += ('\n\nIf you really want to skip this, you can run the client with '
|
||||
'--register-unsafely-without-email but make sure you backup your '
|
||||
'account key from /etc/letsencrypt/accounts\n\n')
|
||||
code, email = zope.component.getUtility(interfaces.IDisplay).input(msg)
|
||||
|
||||
if code == display_util.OK:
|
||||
if le_util.safe_email(email):
|
||||
return email
|
||||
if code == display_util.OK:
|
||||
if le_util.safe_email(email):
|
||||
return email
|
||||
else:
|
||||
return None
|
||||
# TODO catch the server's ACME invalid email address error, and
|
||||
# make a similar call when that happens
|
||||
return get_email(more=True, invalid=(email != ""))
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def choose_account(accounts):
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ class NcursesDisplay(object):
|
|||
|
||||
return code, int(tag) - 1
|
||||
|
||||
|
||||
def input(self, message):
|
||||
"""Display an input box to the user.
|
||||
|
||||
|
|
@ -114,7 +115,12 @@ class NcursesDisplay(object):
|
|||
`string` - input entered by the user
|
||||
|
||||
"""
|
||||
return self.dialog.inputbox(message, width=self.width)
|
||||
sections = message.split("\n")
|
||||
# each section takes at least one line, plus extras if it's longer than self.width
|
||||
wordlines = [1 + (len(section)/self.width) for section in sections]
|
||||
height = 6 + sum(wordlines) + len(sections)
|
||||
return self.dialog.inputbox(message, width=self.width, height=height)
|
||||
|
||||
|
||||
def yesno(self, message, yes_label="Yes", no_label="No"):
|
||||
"""Display a Yes/No dialog box.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ class PluginError(Error):
|
|||
"""Let's Encrypt Plugin error."""
|
||||
|
||||
|
||||
class PluginEnhancementAlreadyPresent(Error):
|
||||
""" Enhancement was already set """
|
||||
|
||||
|
||||
class PluginSelectionError(Error):
|
||||
"""A problem with plugin/configurator selection or setup"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
"""Utilities for all Let's Encrypt."""
|
||||
import argparse
|
||||
import collections
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from letsencrypt import errors
|
||||
|
||||
|
|
@ -255,3 +257,26 @@ def safe_email(email):
|
|||
else:
|
||||
logger.warn("Invalid email address: %s.", email)
|
||||
return False
|
||||
|
||||
|
||||
def add_deprecated_argument(add_argument, argument_name, nargs):
|
||||
"""Adds a deprecated argument with the name argument_name.
|
||||
|
||||
Deprecated arguments are not shown in the help. If they are used on
|
||||
the command line, a warning is shown stating that the argument is
|
||||
deprecated and no other action is taken.
|
||||
|
||||
:param callable add_argument: Function that adds arguments to an
|
||||
argument parser/group.
|
||||
:param str argument_name: Name of deprecated argument.
|
||||
:param nargs: Value for nargs when adding the argument to argparse.
|
||||
|
||||
"""
|
||||
class ShowWarning(argparse.Action):
|
||||
"""Action to log a warning when an argument is used."""
|
||||
def __call__(self, unused1, unused2, unused3, option_string=None):
|
||||
sys.stderr.write(
|
||||
"Use of {0} is deprecated.\n".format(option_string))
|
||||
|
||||
add_argument(argument_name, action=ShowWarning,
|
||||
help=argparse.SUPPRESS, nargs=nargs)
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class Addr(object):
|
|||
|
||||
|
||||
class TLSSNI01(object):
|
||||
"""Class that performs tls-sni-01 challenges."""
|
||||
"""Abstract base for TLS-SNI-01 challenge performers"""
|
||||
|
||||
def __init__(self, configurator):
|
||||
self.configurator = configurator
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ Make sure your web server displays the following content at
|
|||
|
||||
{validation}
|
||||
|
||||
Content-Type header MUST be set to {ct}.
|
||||
|
||||
If you don't have HTTP server configured, you can run the following
|
||||
command on the target server (as root):
|
||||
|
||||
|
|
@ -75,7 +73,6 @@ printf "%s" {validation} > {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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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::
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
<LocationMatch "/.well-known/acme-challenge/*">
|
||||
Header set Content-Type "text/plain"
|
||||
</LocationMatch>
|
||||
</IfModule>
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
tests/apache-conf-files/NEEDED.txt
Normal file
6
tests/apache-conf-files/NEEDED.txt
Normal file
|
|
@ -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
|
||||
|
||||
149
tests/apache-conf-files/failing/drupal-htaccess-1531.conf
Normal file
149
tests/apache-conf-files/failing/drupal-htaccess-1531.conf
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#
|
||||
# Apache/PHP/Drupal settings:
|
||||
#
|
||||
|
||||
# Protect files and directories from prying eyes.
|
||||
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\..*|Entries.*|Repository|Root|Tag|Template)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$">
|
||||
Order allow,deny
|
||||
</FilesMatch>
|
||||
|
||||
# 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.
|
||||
<IfModule mod_php5.c>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
# Requires mod_expires to be enabled.
|
||||
<IfModule mod_expires.c>
|
||||
# Enable expirations.
|
||||
ExpiresActive On
|
||||
|
||||
# Cache all files for 2 weeks after access (A).
|
||||
ExpiresDefault A1209600
|
||||
|
||||
<FilesMatch \.php$>
|
||||
# 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
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
# Various rewrite rules.
|
||||
<IfModule mod_rewrite.c>
|
||||
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
|
||||
# <DirectoryMatch> 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.
|
||||
<IfModule mod_headers.c>
|
||||
# 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]
|
||||
|
||||
<FilesMatch "(\.js\.gz|\.css\.gz)$">
|
||||
# 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
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
# Add headers to all responses.
|
||||
<IfModule mod_headers.c>
|
||||
# Disable content sniffing, since it's an attack vector.
|
||||
Header always set X-Content-Type-Options nosniff
|
||||
</IfModule>
|
||||
9
tests/apache-conf-files/failing/ipv6-1143.conf
Normal file
9
tests/apache-conf-files/failing/ipv6-1143.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<VirtualHost 173.192.30.7:80 [2607:f0d0:1005:99::3:1337]:80>
|
||||
DocumentRoot /xxxx/
|
||||
ServerName noodles.net.nz
|
||||
ServerAlias www.noodles.net.nz
|
||||
CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
|
||||
<Directory "/xxxx/">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
21
tests/apache-conf-files/failing/ipv6-1143b.conf
Normal file
21
tests/apache-conf-files/failing/ipv6-1143b.conf
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
<VirtualHost 173.192.30.7:443 [2607:f0d0:1005:99::3:1337]:443>
|
||||
DocumentRoot /xxxx/
|
||||
ServerName noodles.net.nz
|
||||
ServerAlias www.noodles.net.nz
|
||||
CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
|
||||
<Directory "xxxx">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
|
||||
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"
|
||||
</VirtualHost>
|
||||
295
tests/apache-conf-files/failing/multivhost-1093.conf
Normal file
295
tests/apache-conf-files/failing/multivhost-1093.conf
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
<Directory /var/www/sjau.ch>
|
||||
AllowOverride None
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
<VirtualHost *:80>
|
||||
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
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
</IfModule>
|
||||
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client1/web2/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<IfModule mod_ruby.c>
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
Options +ExecCGI
|
||||
</Directory>
|
||||
RubyRequire apache/ruby-run
|
||||
#RubySafeLevel 0
|
||||
AddType text/html .rb
|
||||
AddType text/html .rbx
|
||||
<Files *.rb>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
<Files *.rbx>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
</IfModule>
|
||||
|
||||
|
||||
<IfModule mod_python.c>
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
<FilesMatch "\.py$">
|
||||
SetHandler mod_python
|
||||
</FilesMatch>
|
||||
PythonHandler mod_python.publisher
|
||||
PythonDebug On
|
||||
</Directory>
|
||||
</IfModule>
|
||||
|
||||
# cgi enabled
|
||||
<Directory /var/www/clients/client1/web2/cgi-bin>
|
||||
Require all granted
|
||||
</Directory>
|
||||
ScriptAlias /cgi-bin/ /var/www/clients/client1/web2/cgi-bin/
|
||||
<FilesMatch "\.(cgi|pl)$">
|
||||
SetHandler cgi-script
|
||||
</FilesMatch>
|
||||
# suexec enabled
|
||||
<IfModule mod_suexec.c>
|
||||
SuexecUserGroup web2 client1
|
||||
</IfModule>
|
||||
# php as fast-cgi enabled
|
||||
# For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html
|
||||
<IfModule mod_fcgid.c>
|
||||
IdleTimeout 300
|
||||
ProcessLifeTime 3600
|
||||
# MaxProcessCount 1000
|
||||
DefaultMinClassProcessCount 0
|
||||
DefaultMaxClassProcessCount 100
|
||||
IPCConnectTimeout 3
|
||||
IPCCommTimeout 600
|
||||
BusyTimeout 3600
|
||||
</IfModule>
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client1/web2/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
|
||||
|
||||
# add support for apache mpm_itk
|
||||
<IfModule mpm_itk_module>
|
||||
AssignUserId web2 client1
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_dav_fs.c>
|
||||
# Do not execute PHP files in webdav directory
|
||||
<Directory /var/www/clients/client1/web2/webdav>
|
||||
<ifModule mod_security2.c>
|
||||
SecRuleRemoveById 960015
|
||||
SecRuleRemoveById 960032
|
||||
</ifModule>
|
||||
<FilesMatch "\.ph(p3?|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
|
||||
</VirtualHost>
|
||||
<VirtualHost [2a01:4f8:160:13a2::1002]:80>
|
||||
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
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
</IfModule>
|
||||
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client1/web2/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<IfModule mod_ruby.c>
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
Options +ExecCGI
|
||||
</Directory>
|
||||
RubyRequire apache/ruby-run
|
||||
#RubySafeLevel 0
|
||||
AddType text/html .rb
|
||||
AddType text/html .rbx
|
||||
<Files *.rb>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
<Files *.rbx>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
</IfModule>
|
||||
|
||||
|
||||
<IfModule mod_python.c>
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
<FilesMatch "\.py$">
|
||||
SetHandler mod_python
|
||||
</FilesMatch>
|
||||
PythonHandler mod_python.publisher
|
||||
PythonDebug On
|
||||
</Directory>
|
||||
</IfModule>
|
||||
|
||||
# cgi enabled
|
||||
<Directory /var/www/clients/client1/web2/cgi-bin>
|
||||
Require all granted
|
||||
</Directory>
|
||||
ScriptAlias /cgi-bin/ /var/www/clients/client1/web2/cgi-bin/
|
||||
<FilesMatch "\.(cgi|pl)$">
|
||||
SetHandler cgi-script
|
||||
</FilesMatch>
|
||||
# suexec enabled
|
||||
<IfModule mod_suexec.c>
|
||||
SuexecUserGroup web2 client1
|
||||
</IfModule>
|
||||
# php as fast-cgi enabled
|
||||
# For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html
|
||||
<IfModule mod_fcgid.c>
|
||||
IdleTimeout 300
|
||||
ProcessLifeTime 3600
|
||||
# MaxProcessCount 1000
|
||||
DefaultMinClassProcessCount 0
|
||||
DefaultMaxClassProcessCount 100
|
||||
IPCConnectTimeout 3
|
||||
IPCCommTimeout 600
|
||||
BusyTimeout 3600
|
||||
</IfModule>
|
||||
<Directory /var/www/sjau.ch/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client1/web2/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
|
||||
|
||||
# add support for apache mpm_itk
|
||||
<IfModule mpm_itk_module>
|
||||
AssignUserId web2 client1
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_dav_fs.c>
|
||||
# Do not execute PHP files in webdav directory
|
||||
<Directory /var/www/clients/client1/web2/webdav>
|
||||
<ifModule mod_security2.c>
|
||||
SecRuleRemoveById 960015
|
||||
SecRuleRemoveById 960032
|
||||
</ifModule>
|
||||
<FilesMatch "\.ph(p3?|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
|
||||
</VirtualHost>
|
||||
593
tests/apache-conf-files/failing/multivhost-1093b.conf
Normal file
593
tests/apache-conf-files/failing/multivhost-1093b.conf
Normal file
|
|
@ -0,0 +1,593 @@
|
|||
<Directory /var/www/ensemen.ch>
|
||||
AllowOverride None
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
<VirtualHost *:80>
|
||||
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
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
</IfModule>
|
||||
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<IfModule mod_ruby.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
Options +ExecCGI
|
||||
</Directory>
|
||||
RubyRequire apache/ruby-run
|
||||
#RubySafeLevel 0
|
||||
AddType text/html .rb
|
||||
AddType text/html .rbx
|
||||
<Files *.rb>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
<Files *.rbx>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
</IfModule>
|
||||
|
||||
|
||||
<IfModule mod_python.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.py$">
|
||||
SetHandler mod_python
|
||||
</FilesMatch>
|
||||
PythonHandler mod_python.publisher
|
||||
PythonDebug On
|
||||
</Directory>
|
||||
</IfModule>
|
||||
|
||||
# cgi enabled
|
||||
<Directory /var/www/clients/client4/web17/cgi-bin>
|
||||
Require all granted
|
||||
</Directory>
|
||||
ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/
|
||||
<FilesMatch "\.(cgi|pl)$">
|
||||
SetHandler cgi-script
|
||||
</FilesMatch>
|
||||
# suexec enabled
|
||||
<IfModule mod_suexec.c>
|
||||
SuexecUserGroup web17 client4
|
||||
</IfModule>
|
||||
# php as fast-cgi enabled
|
||||
# For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html
|
||||
<IfModule mod_fcgid.c>
|
||||
IdleTimeout 300
|
||||
ProcessLifeTime 3600
|
||||
# MaxProcessCount 1000
|
||||
DefaultMinClassProcessCount 0
|
||||
DefaultMaxClassProcessCount 100
|
||||
IPCConnectTimeout 3
|
||||
IPCCommTimeout 600
|
||||
BusyTimeout 3600
|
||||
</IfModule>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
|
||||
|
||||
# add support for apache mpm_itk
|
||||
<IfModule mpm_itk_module>
|
||||
AssignUserId web17 client4
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_dav_fs.c>
|
||||
# Do not execute PHP files in webdav directory
|
||||
<Directory /var/www/clients/client4/web17/webdav>
|
||||
<ifModule mod_security2.c>
|
||||
SecRuleRemoveById 960015
|
||||
SecRuleRemoveById 960032
|
||||
</ifModule>
|
||||
<FilesMatch "\.ph(p3?|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
|
||||
</VirtualHost>
|
||||
<VirtualHost *:443>
|
||||
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
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<IfModule mod_ruby.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
Options +ExecCGI
|
||||
</Directory>
|
||||
RubyRequire apache/ruby-run
|
||||
#RubySafeLevel 0
|
||||
AddType text/html .rb
|
||||
AddType text/html .rbx
|
||||
<Files *.rb>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
<Files *.rbx>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
</IfModule>
|
||||
|
||||
|
||||
<IfModule mod_python.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.py$">
|
||||
SetHandler mod_python
|
||||
</FilesMatch>
|
||||
PythonHandler mod_python.publisher
|
||||
PythonDebug On
|
||||
</Directory>
|
||||
</IfModule>
|
||||
|
||||
# cgi enabled
|
||||
<Directory /var/www/clients/client4/web17/cgi-bin>
|
||||
Require all granted
|
||||
</Directory>
|
||||
ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/
|
||||
<FilesMatch "\.(cgi|pl)$">
|
||||
SetHandler cgi-script
|
||||
</FilesMatch>
|
||||
# suexec enabled
|
||||
<IfModule mod_suexec.c>
|
||||
SuexecUserGroup web17 client4
|
||||
</IfModule>
|
||||
# php as fast-cgi enabled
|
||||
# For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html
|
||||
<IfModule mod_fcgid.c>
|
||||
IdleTimeout 300
|
||||
ProcessLifeTime 3600
|
||||
# MaxProcessCount 1000
|
||||
DefaultMinClassProcessCount 0
|
||||
DefaultMaxClassProcessCount 100
|
||||
IPCConnectTimeout 3
|
||||
IPCCommTimeout 600
|
||||
BusyTimeout 3600
|
||||
</IfModule>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
|
||||
|
||||
# add support for apache mpm_itk
|
||||
<IfModule mpm_itk_module>
|
||||
AssignUserId web17 client4
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_dav_fs.c>
|
||||
# Do not execute PHP files in webdav directory
|
||||
<Directory /var/www/clients/client4/web17/webdav>
|
||||
<ifModule mod_security2.c>
|
||||
SecRuleRemoveById 960015
|
||||
SecRuleRemoveById 960032
|
||||
</ifModule>
|
||||
<FilesMatch "\.ph(p3?|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
|
||||
</VirtualHost>
|
||||
<VirtualHost [2a01:4f8:160:13a2::1017]:80>
|
||||
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
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
</IfModule>
|
||||
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<IfModule mod_ruby.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
Options +ExecCGI
|
||||
</Directory>
|
||||
RubyRequire apache/ruby-run
|
||||
#RubySafeLevel 0
|
||||
AddType text/html .rb
|
||||
AddType text/html .rbx
|
||||
<Files *.rb>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
<Files *.rbx>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
</IfModule>
|
||||
|
||||
|
||||
<IfModule mod_python.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.py$">
|
||||
SetHandler mod_python
|
||||
</FilesMatch>
|
||||
PythonHandler mod_python.publisher
|
||||
PythonDebug On
|
||||
</Directory>
|
||||
</IfModule>
|
||||
|
||||
# cgi enabled
|
||||
<Directory /var/www/clients/client4/web17/cgi-bin>
|
||||
Require all granted
|
||||
</Directory>
|
||||
ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/
|
||||
<FilesMatch "\.(cgi|pl)$">
|
||||
SetHandler cgi-script
|
||||
</FilesMatch>
|
||||
# suexec enabled
|
||||
<IfModule mod_suexec.c>
|
||||
SuexecUserGroup web17 client4
|
||||
</IfModule>
|
||||
# php as fast-cgi enabled
|
||||
# For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html
|
||||
<IfModule mod_fcgid.c>
|
||||
IdleTimeout 300
|
||||
ProcessLifeTime 3600
|
||||
# MaxProcessCount 1000
|
||||
DefaultMinClassProcessCount 0
|
||||
DefaultMaxClassProcessCount 100
|
||||
IPCConnectTimeout 3
|
||||
IPCCommTimeout 600
|
||||
BusyTimeout 3600
|
||||
</IfModule>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
|
||||
|
||||
# add support for apache mpm_itk
|
||||
<IfModule mpm_itk_module>
|
||||
AssignUserId web17 client4
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_dav_fs.c>
|
||||
# Do not execute PHP files in webdav directory
|
||||
<Directory /var/www/clients/client4/web17/webdav>
|
||||
<ifModule mod_security2.c>
|
||||
SecRuleRemoveById 960015
|
||||
SecRuleRemoveById 960032
|
||||
</ifModule>
|
||||
<FilesMatch "\.ph(p3?|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
|
||||
</VirtualHost>
|
||||
<VirtualHost [2a01:4f8:160:13a2::1017]:443>
|
||||
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
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
# Clear PHP settings of this website
|
||||
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<IfModule mod_ruby.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
Options +ExecCGI
|
||||
</Directory>
|
||||
RubyRequire apache/ruby-run
|
||||
#RubySafeLevel 0
|
||||
AddType text/html .rb
|
||||
AddType text/html .rbx
|
||||
<Files *.rb>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
<Files *.rbx>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
</IfModule>
|
||||
|
||||
|
||||
<IfModule mod_python.c>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.py$">
|
||||
SetHandler mod_python
|
||||
</FilesMatch>
|
||||
PythonHandler mod_python.publisher
|
||||
PythonDebug On
|
||||
</Directory>
|
||||
</IfModule>
|
||||
|
||||
# cgi enabled
|
||||
<Directory /var/www/clients/client4/web17/cgi-bin>
|
||||
Require all granted
|
||||
</Directory>
|
||||
ScriptAlias /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/
|
||||
<FilesMatch "\.(cgi|pl)$">
|
||||
SetHandler cgi-script
|
||||
</FilesMatch>
|
||||
# suexec enabled
|
||||
<IfModule mod_suexec.c>
|
||||
SuexecUserGroup web17 client4
|
||||
</IfModule>
|
||||
# php as fast-cgi enabled
|
||||
# For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html
|
||||
<IfModule mod_fcgid.c>
|
||||
IdleTimeout 300
|
||||
ProcessLifeTime 3600
|
||||
# MaxProcessCount 1000
|
||||
DefaultMinClassProcessCount 0
|
||||
DefaultMaxClassProcessCount 100
|
||||
IPCConnectTimeout 3
|
||||
IPCCommTimeout 600
|
||||
BusyTimeout 3600
|
||||
</IfModule>
|
||||
<Directory /var/www/ensemen.ch/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
<Directory /var/www/clients/client4/web17/web>
|
||||
<FilesMatch "\.php[345]?$">
|
||||
SetHandler fcgid-script
|
||||
</FilesMatch>
|
||||
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
|
||||
</Directory>
|
||||
|
||||
|
||||
# add support for apache mpm_itk
|
||||
<IfModule mpm_itk_module>
|
||||
AssignUserId web17 client4
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_dav_fs.c>
|
||||
# Do not execute PHP files in webdav directory
|
||||
<Directory /var/www/clients/client4/web17/webdav>
|
||||
<ifModule mod_security2.c>
|
||||
SecRuleRemoveById 960015
|
||||
SecRuleRemoveById 960032
|
||||
</ifModule>
|
||||
<FilesMatch "\.ph(p3?|tml)$">
|
||||
SetHandler None
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
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
|
||||
</IfModule>
|
||||
|
||||
|
||||
</VirtualHost>
|
||||
5
tests/apache-conf-files/passing/README.modules
Normal file
5
tests/apache-conf-files/passing/README.modules
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Modules required to parse these conf files:
|
||||
|
||||
ssl
|
||||
rewrite
|
||||
macro
|
||||
14
tests/apache-conf-files/passing/anarcat-1531.conf
Normal file
14
tests/apache-conf-files/passing/anarcat-1531.conf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<VirtualHost *:80>
|
||||
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
|
||||
</VirtualHost>
|
||||
136
tests/apache-conf-files/passing/example-ssl.conf
Normal file
136
tests/apache-conf-files/passing/example-ssl.conf
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<VirtualHost *:443>
|
||||
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
|
||||
<FilesMatch "\.(cgi|shtml|phtml|php)$">
|
||||
SSLOptions +StdEnvVars
|
||||
</FilesMatch>
|
||||
<Directory /usr/lib/cgi-bin>
|
||||
SSLOptions +StdEnvVars
|
||||
</Directory>
|
||||
|
||||
# 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
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
32
tests/apache-conf-files/passing/example.conf
Normal file
32
tests/apache-conf-files/passing/example.conf
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<VirtualHost *:80>
|
||||
# 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
|
||||
</VirtualHost>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
222
tests/apache-conf-files/passing/finalize-1243.apache2.conf.txt
Normal file
222
tests/apache-conf-files/passing/finalize-1243.apache2.conf.txt
Normal file
|
|
@ -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 <URL:http://httpd.apache.org/docs/2.4/mod/core.html#mutex>);
|
||||
# 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 <VirtualHost>
|
||||
# container, error messages relating to that virtual host will be
|
||||
# logged here. If you *do* define an error logfile for a <VirtualHost>
|
||||
# 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.
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
<Directory /usr/share>
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /var/www/>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
#<Directory /srv/>
|
||||
# Options Indexes FollowSymLinks
|
||||
# AllowOverride None
|
||||
# Require all granted
|
||||
#</Directory>
|
||||
|
||||
# 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.
|
||||
#
|
||||
<FilesMatch "^\.ht">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
67
tests/apache-conf-files/passing/finalize-1243.conf
Normal file
67
tests/apache-conf-files/passing/finalize-1243.conf
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#LoadModule ssl_module modules/mod_ssl.so
|
||||
|
||||
Listen 443
|
||||
<VirtualHost *: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
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Order Deny,Allow
|
||||
#Deny from All
|
||||
</Directory>
|
||||
|
||||
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
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
# 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.
|
||||
#
|
||||
<Directory "/usr/local/awstats/wwwroot">
|
||||
Options None
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
33
tests/apache-conf-files/passing/modmacro-1385.conf
Normal file
33
tests/apache-conf-files/passing/modmacro-1385.conf
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<Macro Vhost $host $port $dir>
|
||||
<VirtualHost *:$port>
|
||||
# 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
|
||||
</VirtualHost>
|
||||
</Macro>
|
||||
Use Vhost goxogle.com 80 /var/www/goxogle/
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
13
tests/apache-conf-files/passing/owncloud-1264.conf
Normal file
13
tests/apache-conf-files/passing/owncloud-1264.conf
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Alias /owncloud /usr/share/owncloud
|
||||
|
||||
<Directory /usr/share/owncloud/>
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
<IfVersion < 2.3>
|
||||
order allow,deny
|
||||
allow from all
|
||||
</IfVersion>
|
||||
<IfVersion >= 2.3>
|
||||
Require all granted
|
||||
</IfVersion>
|
||||
</Directory>
|
||||
61
tests/apache-conf-files/passing/roundcube-1222.conf
Normal file
61
tests/apache-conf-files/passing/roundcube-1222.conf
Normal file
|
|
@ -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
|
||||
<Directory "/usr/share/tinymce/www/">
|
||||
Options Indexes MultiViews FollowSymLinks
|
||||
AllowOverride None
|
||||
<IfVersion >= 2.3>
|
||||
Require all granted
|
||||
</IfVersion>
|
||||
<IfVersion < 2.3>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</IfVersion>
|
||||
</Directory>
|
||||
|
||||
<Directory /var/lib/roundcube/>
|
||||
Options +FollowSymLinks
|
||||
# This is needed to parse /var/lib/roundcube/.htaccess. See its
|
||||
# content before setting AllowOverride to None.
|
||||
AllowOverride All
|
||||
<IfVersion >= 2.3>
|
||||
Require all granted
|
||||
</IfVersion>
|
||||
<IfVersion < 2.3>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</IfVersion>
|
||||
</Directory>
|
||||
|
||||
# Protecting basic directories:
|
||||
<Directory /var/lib/roundcube/config>
|
||||
Options -FollowSymLinks
|
||||
AllowOverride None
|
||||
</Directory>
|
||||
|
||||
<Directory /var/lib/roundcube/temp>
|
||||
Options -FollowSymLinks
|
||||
AllowOverride None
|
||||
<IfVersion >= 2.3>
|
||||
Require all denied
|
||||
</IfVersion>
|
||||
<IfVersion < 2.3>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</IfVersion>
|
||||
</Directory>
|
||||
|
||||
<Directory /var/lib/roundcube/logs>
|
||||
Options -FollowSymLinks
|
||||
AllowOverride None
|
||||
<IfVersion >= 2.3>
|
||||
Require all denied
|
||||
</IfVersion>
|
||||
<IfVersion < 2.3>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</IfVersion>
|
||||
</Directory>
|
||||
44
tests/apache-conf-files/passing/semacode-1598.conf
Normal file
44
tests/apache-conf-files/passing/semacode-1598.conf
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<VirtualHost *:80>
|
||||
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
|
||||
#<proxy>
|
||||
# Order allow,deny
|
||||
#Allow from all
|
||||
#</proxy>
|
||||
|
||||
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]
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
|
||||
<VirtualHost *:80>
|
||||
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
|
||||
<Directory "/home/simon/semacode-server/semacode/website/trunk/public">
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
40
tests/boulder-fetch.sh
Executable file
40
tests/boulder-fetch.sh
Executable file
|
|
@ -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 -
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ letsencrypt_test () {
|
|||
--no-redirect \
|
||||
--agree-dev-preview \
|
||||
--agree-tos \
|
||||
--email "" \
|
||||
--register-unsafely-without-email \
|
||||
--renew-by-default \
|
||||
--debug \
|
||||
-vvvvvvv \
|
||||
|
|
|
|||
19
tests/travis-integration.sh
Executable file
19
tests/travis-integration.sh
Executable file
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue