Merge master in to get up to date.

This commit is contained in:
Erik Rose 2015-12-02 00:53:09 -05:00
commit 4abe7ab93d
78 changed files with 3193 additions and 674 deletions

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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."""

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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,

View file

@ -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

View file

@ -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!"

View file

@ -1,6 +1,6 @@
#!/bin/sh
# SLE12 dont have python-virtualenv
# SLE12 don't have python-virtualenv
zypper -nq in -l git-core \
python \

View file

@ -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

View file

@ -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**

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt_apache.dvsni`
-------------------------------
.. automodule:: letsencrypt_apache.dvsni
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt_apache.tls_sni_01`
------------------------------------
.. automodule:: letsencrypt_apache.tls_sni_01
:members:

View file

@ -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/

View file

@ -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.

View file

@ -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}

View file

@ -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."""

View file

@ -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

View file

@ -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__":

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt_nginx.dvsni`
------------------------------
.. automodule:: letsencrypt_nginx.dvsni
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt_nginx.tls_sni_01`
-----------------------------------
.. automodule:: letsencrypt_nginx.tls_sni_01
:members:

View file

@ -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

View file

@ -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':

View file

@ -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")

View file

@ -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)

View file

@ -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(

View file

@ -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]

View file

@ -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:

View file

@ -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)

View file

@ -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")

View file

@ -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",

View file

@ -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):

View file

@ -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.

View file

@ -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"""

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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."""

View file

@ -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

View file

@ -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

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,5 @@
Modules required to parse these conf files:
ssl
rewrite
macro

View 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>

View 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

View 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

View 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

View 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>

View 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

View 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>

View 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>

View 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
View 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 -

View file

@ -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

View file

@ -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
View 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

View file

@ -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"