Compare commits

..

1 commit

Author SHA1 Message Date
Jeff Hodges
9ad7eedbfe revert the nginx port change
This was fixed in the boulder codebase with letsencrypt/boulder#482.

This is a partial reversion of #618.
2015-07-22 16:57:07 -07:00
203 changed files with 3719 additions and 9561 deletions

6
.gitignore vendored
View file

@ -2,9 +2,9 @@
*.egg-info/ *.egg-info/
.eggs/ .eggs/
build/ build/
dist*/ dist/
/venv*/ /venv/
/kgs/ /venv3/
/.tox/ /.tox/
letsencrypt.log letsencrypt.log

4
.pep8
View file

@ -1,4 +0,0 @@
[pep8]
# E265 block comment should start with '# '
# E501 line too long (X > 79 characters)
ignore = E265,E501

View file

@ -38,7 +38,7 @@ load-plugins=linter_plugin
# --enable=similarities". If you want to run only the classes checker, but have # --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes # no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W" # --disable=W"
disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name disable=fixme,locally-disabled,abstract-class-not-used
# abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) # abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1)
@ -101,7 +101,7 @@ function-rgx=[a-z_][a-z0-9_]{2,40}$
function-name-hint=[a-z_][a-z0-9_]{2,40}$ function-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression matching correct variable names # Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{1,30}$ variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names # Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$ variable-name-hint=[a-z_][a-z0-9_]{2,30}$
@ -218,7 +218,7 @@ ignore-long-lines=^\s*(# )?<?https?://\S+>?$
single-line-if-stmt=no single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled # List of optional constructs for which whitespace checking is disabled
no-space-check=trailing-comma no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module # Maximum number of lines in a module
max-module-lines=1250 max-module-lines=1250
@ -228,8 +228,7 @@ max-module-lines=1250
indent-string=' ' indent-string=' '
# Number of spaces of indent required inside a hanging or continued line. # Number of spaces of indent required inside a hanging or continued line.
# This does something silly/broken... indent-after-paren=4
#indent-after-paren=4
[TYPECHECK] [TYPECHECK]

View file

@ -2,12 +2,11 @@ language: python
services: services:
- rabbitmq - rabbitmq
- mysql
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS # http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
before_install: before_install:
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5)"' - travis_retry sudo ./bootstrap/ubuntu.sh
- travis_retry sudo apt-get install --no-install-recommends nginx-light openssl
# using separate envs with different TOXENVs creates 4x1 Travis build # using separate envs with different TOXENVs creates 4x1 Travis build
# matrix, which allows us to clearly distinguish which component under # matrix, which allows us to clearly distinguish which component under
@ -15,38 +14,16 @@ before_install:
env: env:
global: global:
- GOPATH=/tmp/go - GOPATH=/tmp/go
- PATH=$GOPATH/bin:$PATH
matrix: matrix:
- TOXENV=py26 BOULDER_INTEGRATION=1
- TOXENV=py27 BOULDER_INTEGRATION=1 - TOXENV=py27 BOULDER_INTEGRATION=1
- TOXENV=py33
- TOXENV=py34
- TOXENV=lint - TOXENV=lint
- TOXENV=cover - TOXENV=cover
sudo: false # containers
addons:
# make sure simplehttp simple verification works (custom /etc/hosts)
hosts:
- le.wtf
mariadb: "10.0"
apt:
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
- lsb-release
- python
- python-dev
- python-virtualenv
- gcc
- dialog
- libaugeas0
- libssl-dev
- libffi-dev
- ca-certificates
# For letsencrypt-nginx integration testing
- nginx-light
- openssl
# For Boulder integration testing
- rsyslog
install: "travis_retry pip install tox coveralls" install: "travis_retry pip install tox coveralls"
before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh' before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh amqp'
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" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))'
after_success: '[ "$TOXENV" == "cover" ] && coveralls' after_success: '[ "$TOXENV" == "cover" ] && coveralls'

View file

@ -62,5 +62,5 @@ RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \
# bash" and investigate, apply patches, etc. # bash" and investigate, apply patches, etc.
ENV PATH /opt/letsencrypt/venv/bin:$PATH ENV PATH /opt/letsencrypt/venv/bin:$PATH
# TODO: is --text really necessary?
ENTRYPOINT [ "letsencrypt" ] ENTRYPOINT [ "letsencrypt", "--text" ]

View file

@ -1,69 +0,0 @@
# This Dockerfile builds an image for development.
FROM ubuntu:trusty
MAINTAINER Jakub Warmuz <jakub@warmuz.org>
MAINTAINER William Budington <bill@eff.org>
MAINTAINER Yan <yan@eff.org>
# Note: this only exposes the port to other docker containers. You
# still have to bind to 443@host at runtime, as per the ACME spec.
EXPOSE 443
# TODO: make sure --config-dir and --work-dir cannot be changed
# through the CLI (letsencrypt-docker wrapper that uses standalone
# authenticator and text mode only?)
VOLUME /etc/letsencrypt /var/lib/letsencrypt
WORKDIR /opt/letsencrypt
# no need to mkdir anything:
# https://docs.docker.com/reference/builder/#copy
# If <dest> doesn't exist, it is created along with all missing
# directories in its path.
# TODO: Install non-default Python versions for tox.
# TODO: Install Apache/Nginx for plugin development.
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/
RUN /opt/letsencrypt/src/ubuntu.sh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
/var/tmp/*
# the above is not likely to change, so by putting it further up the
# Dockerfile we make sure we cache as much as possible
COPY setup.py README.rst CHANGES.rst MANIFEST.in requirements.txt EULA linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/letsencrypt/src/
# all above files are necessary for setup.py, however, package source
# code directory has to be copied separately to a subdirectory...
# https://docs.docker.com/reference/builder/#copy: "If <src> is a
# directory, the entire contents of the directory are copied,
# including filesystem metadata. Note: The directory itself is not
# copied, just its contents." Order again matters, three files are far
# more likely to be cached than the whole project directory
COPY letsencrypt /opt/letsencrypt/src/letsencrypt/
COPY acme /opt/letsencrypt/src/acme/
COPY letsencrypt-apache /opt/letsencrypt/src/letsencrypt-apache/
COPY letsencrypt-nginx /opt/letsencrypt/src/letsencrypt-nginx/
COPY letshelp-letsencrypt /opt/letsencrypt/src/letshelp-letsencrypt/
COPY letsencrypt-compatibility-test /opt/letsencrypt/src/letsencrypt-compatibility-test/
COPY tests /opt/letsencrypt/src/tests/
RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \
/opt/letsencrypt/venv/bin/pip install \
-r /opt/letsencrypt/src/requirements.txt \
-e /opt/letsencrypt/src/acme \
-e /opt/letsencrypt/src \
-e /opt/letsencrypt/src/letsencrypt-apache \
-e /opt/letsencrypt/src/letsencrypt-nginx \
-e /opt/letsencrypt/src/letshelp-letsencrypt \
-e /opt/letsencrypt/src/letsencrypt-compatibility-test \
-e /opt/letsencrypt/src[dev,docs,testing]
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/letsencrypt/src" (it's stays in the underlaying image);
# this might also help in debugging: you can "docker run --entrypoint
# bash" and investigate, apply patches, etc.
ENV PATH /opt/letsencrypt/venv/bin:$PATH

View file

@ -1,5 +1,5 @@
Let's Encrypt Python Client Let's Encrypt:
Copyright (c) Electronic Frontier Foundation and others Copyright (c) Internet Security Research Group
Licensed Apache Version 2.0 Licensed Apache Version 2.0
Incorporating code from nginxparser Incorporating code from nginxparser

View file

@ -2,8 +2,6 @@ include requirements.txt
include README.rst include README.rst
include CHANGES.rst include CHANGES.rst
include CONTRIBUTING.md include CONTRIBUTING.md
include LICENSE.txt
include linter_plugin.py include linter_plugin.py
include letsencrypt/EULA include letsencrypt/EULA
recursive-include docs *
recursive-include letsencrypt/tests/testdata * recursive-include letsencrypt/tests/testdata *

View file

@ -1,18 +1,12 @@
.. notice for github users .. notice for github users
Disclaimer Official **documentation**, including `installation instructions`_, is
========== available at https://letsencrypt.readthedocs.org.
This is a **DEVELOPER PREVIEW** intended for developers and testers only. Generic information about Let's Encrypt project can be found at
https://letsencrypt.org. Please read `Frequently Asked Questions (FAQ)
<https://letsencrypt.org/faq/>`_.
**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.
For more information regarding the status of the project, please see
https://letsencrypt.org. Be sure to checkout the
`Frequently Asked Questions (FAQ) <https://community.letsencrypt.org/t/frequently-asked-questions-faq/26#topic-title>`_.
About the Let's Encrypt Client About the Let's Encrypt Client
============================== ==============================
@ -24,7 +18,7 @@ In short: getting and installing SSL/TLS certificates made easy (`watch demo vid
The Let's Encrypt Client is a tool to automatically receive and install The Let's Encrypt Client is a tool to automatically receive and install
X.509 certificates to enable TLS on servers. The client will X.509 certificates to enable TLS on servers. The client will
interoperate with the Let's Encrypt CA which will be issuing browser-trusted interoperate with the Let's Encrypt CA which will be issuing browser-trusted
certificates for free. certificates for free beginning the summer of 2015.
It's all automated: It's all automated:
@ -38,7 +32,7 @@ All you need to do to sign a single domain is::
user@www:~$ sudo letsencrypt -d www.example.org auth user@www:~$ sudo letsencrypt -d www.example.org auth
For multiple domains (SAN) use:: For multiple domains (SAN) use::
user@www:~$ sudo letsencrypt -d www.example.org -d example.org auth user@www:~$ sudo letsencrypt -d www.example.org -d example.org auth
and if you have a compatible web server (Apache or Nginx), Let's Encrypt can and if you have a compatible web server (Apache or Nginx), Let's Encrypt can
@ -73,13 +67,22 @@ server automatically!::
.. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU
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.**
Current Features Current Features
---------------- ----------------
* web servers supported: * web servers supported:
- apache/2.x (tested and working on Ubuntu Linux) - apache/2.x (tested and working on Ubuntu Linux)
- nginx/0.8.48+ (under development) - nginx/0.8.48+ (tested and mostly working on Ubuntu Linux)
- standalone (runs its own webserver to prove you control the domain) - standalone (runs its own webserver to prove you control the domain)
* the private key is generated locally on your system * the private key is generated locally on your system
@ -96,13 +99,6 @@ Current Features
* Free and Open Source Software, made with Python. * Free and Open Source Software, made with Python.
Installation Instructions
-------------------------
Official **documentation**, including `installation instructions`_, is
available at https://letsencrypt.readthedocs.org.
Links Links
----- -----
@ -116,8 +112,6 @@ Main Website: https://letsencrypt.org/
IRC Channel: #letsencrypt on `Freenode`_ IRC Channel: #letsencrypt on `Freenode`_
Community: https://community.letsencrypt.org
Mailing list: `client-dev`_ (to subscribe without a Google account, send an Mailing list: `client-dev`_ (to subscribe without a Google account, send an
email to client-dev+subscribe@letsencrypt.org) email to client-dev+subscribe@letsencrypt.org)

View file

@ -1,190 +0,0 @@
Copyright 2015 Electronic Frontier Foundation and others
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -1,3 +1 @@
include LICENSE.txt
include README.rst
recursive-include acme/testdata * recursive-include acme/testdata *

View file

@ -1 +0,0 @@
ACME protocol implementation for Python

View file

@ -1,9 +1,13 @@
"""ACME Identifier Validation Challenges.""" """ACME Identifier Validation Challenges."""
import binascii
import functools import functools
import hashlib import hashlib
import logging import logging
import os
import socket import socket
from cryptography.hazmat.backends import default_backend
from cryptography import x509
import OpenSSL import OpenSSL
import requests import requests
@ -25,14 +29,6 @@ class Challenge(jose.TypedJSONObjectWithFields):
"""ACME challenge.""" """ACME challenge."""
TYPES = {} TYPES = {}
@classmethod
def from_json(cls, jobj):
try:
return super(Challenge, cls).from_json(jobj)
except jose.UnrecognizedTypeError as error:
logger.debug(error)
return UnrecognizedChallenge.from_json(jobj)
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
"""Client validation challenges.""" """Client validation challenges."""
@ -50,32 +46,6 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields):
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class UnrecognizedChallenge(Challenge):
"""Unrecognized challenge.
ACME specification defines a generic framework for challenges and
defines some standard challenges that are implemented in this
module. However, other implementations (including peers) might
define additional challenge types, which should be ignored if
unrecognized.
:ivar jobj: Original JSON decoded object.
"""
def __init__(self, jobj):
super(UnrecognizedChallenge, self).__init__()
object.__setattr__(self, "jobj", jobj)
def to_partial_json(self):
# pylint: disable=no-member
return self.jobj
@classmethod
def from_json(cls, jobj):
return cls(jobj)
@Challenge.register @Challenge.register
class SimpleHTTP(DVChallenge): class SimpleHTTP(DVChallenge):
"""ACME "simpleHttp" challenge. """ACME "simpleHttp" challenge.
@ -84,45 +54,43 @@ class SimpleHTTP(DVChallenge):
""" """
typ = "simpleHttp" typ = "simpleHttp"
token = jose.Field("token")
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
# TODO: acme-spec doesn't specify token as base64-encoded value
token = jose.Field(
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
@property
def good_token(self): # XXX: @token.decoder
"""Is `token` good?
.. todo:: acme-spec wants "It MUST NOT contain any non-ASCII
characters", but it should also warrant that it doesn't
contain ".." or "/"...
"""
# TODO: check that path combined with uri does not go above
# URI_ROOT_PATH!
return b'..' not in self.token and b'/' not in self.token
@ChallengeResponse.register @ChallengeResponse.register
class SimpleHTTPResponse(ChallengeResponse): class SimpleHTTPResponse(ChallengeResponse):
"""ACME "simpleHttp" challenge response. """ACME "simpleHttp" challenge response.
:ivar bool tls: :ivar unicode path:
:ivar unicode tls:
""" """
typ = "simpleHttp" typ = "simpleHttp"
path = jose.Field("path")
tls = jose.Field("tls", default=True, omitempty=True) tls = jose.Field("tls", default=True, omitempty=True)
URI_ROOT_PATH = ".well-known/acme-challenge" URI_ROOT_PATH = ".well-known/acme-challenge"
"""URI root path for the server provisioned resource.""" """URI root path for the server provisioned resource."""
_URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{token}" _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{path}"
CONTENT_TYPE = "application/jose+json" MAX_PATH_LEN = 25
"""Maximum allowed `path` length."""
CONTENT_TYPE = "text/plain"
@property
def good_path(self):
"""Is `path` good?
.. todo:: acme-spec: "The value MUST be comprised entirely of
characters from the URL-safe alphabet for Base64 encoding
[RFC4648]", base64.b64decode ignores those characters
"""
# TODO: check that path combined with uri does not go above
# URI_ROOT_PATH!
return len(self.path) <= 25
@property @property
def scheme(self): def scheme(self):
@ -134,91 +102,27 @@ class SimpleHTTPResponse(ChallengeResponse):
"""Port that the ACME client should be listening for validation.""" """Port that the ACME client should be listening for validation."""
return 443 if self.tls else 80 return 443 if self.tls else 80
def uri(self, domain, chall): def uri(self, domain):
"""Create an URI to the provisioned resource. """Create an URI to the provisioned resource.
Forms an URI to the HTTPS server provisioned resource Forms an URI to the HTTPS server provisioned resource
(containing :attr:`~SimpleHTTP.token`). (containing :attr:`~SimpleHTTP.token`).
:param unicode domain: Domain name being verified. :param unicode domain: Domain name being verified.
:param challenges.SimpleHTTP chall:
""" """
return self._URI_TEMPLATE.format( return self._URI_TEMPLATE.format(
scheme=self.scheme, domain=domain, token=chall.encode("token")) scheme=self.scheme, domain=domain, path=self.path)
def gen_resource(self, chall): def simple_verify(self, chall, domain, port=None):
"""Generate provisioned resource.
:param challenges.SimpleHTTP chall:
:rtype: SimpleHTTPProvisionedResource
"""
return SimpleHTTPProvisionedResource(token=chall.token, tls=self.tls)
def gen_validation(self, chall, account_key, alg=jose.RS256, **kwargs):
"""Generate validation.
:param challenges.SimpleHTTP chall:
:param .JWK account_key: Private account key.
:param .JWA alg:
:returns: `.SimpleHTTPProvisionedResource` signed in `.JWS`
:rtype: .JWS
"""
return jose.JWS.sign(
payload=self.gen_resource(chall).json_dumps(
sort_keys=True).encode('utf-8'),
key=account_key, alg=alg, **kwargs)
def check_validation(self, validation, chall, account_public_key):
"""Check validation.
:param .JWS validation:
:param challenges.SimpleHTTP chall:
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:rtype: bool
"""
if not validation.verify(key=account_public_key):
return False
try:
resource = SimpleHTTPProvisionedResource.json_loads(
validation.payload.decode('utf-8'))
except jose.DeserializationError as error:
logger.debug(error)
return False
return resource.token == chall.token and resource.tls == self.tls
def simple_verify(self, chall, domain, account_public_key, port=None):
"""Simple verify. """Simple verify.
According to the ACME specification, "the ACME server MUST According to the ACME specification, "the ACME server MUST
ignore the certificate provided by the HTTPS server", so ignore the certificate provided by the HTTPS server", so
``requests.get`` is called with ``verify=False``. ``requests.get`` is called with ``verify=False``.
:param challenges.SimpleHTTP chall: Corresponding challenge. :param .SimpleHTTP chall: Corresponding challenge.
:param unicode domain: Domain name being verified. :param unicode domain: Domain name being verified.
:param account_public_key: Public key for the key pair
being authorized. If ``None`` key verification is not
performed!
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:param int port: Port used in the validation. :param int port: Port used in the validation.
:returns: ``True`` iff validation is successful, ``False`` :returns: ``True`` iff validation is successful, ``False``
@ -234,67 +138,76 @@ class SimpleHTTPResponse(ChallengeResponse):
"Using non-standard port for SimpleHTTP verification: %s", port) "Using non-standard port for SimpleHTTP verification: %s", port)
domain += ":{0}".format(port) domain += ":{0}".format(port)
uri = self.uri(domain, chall) uri = self.uri(domain)
logger.debug("Verifying %s at %s...", chall.typ, uri) logger.debug("Verifying %s at %s...", chall.typ, uri)
try: try:
http_response = requests.get(uri, verify=False) http_response = requests.get(uri, verify=False)
except requests.exceptions.RequestException as error: except requests.exceptions.RequestException as error:
logger.error("Unable to reach %s: %s", uri, error) logger.error("Unable to reach %s: %s", uri, error)
return False return False
logger.debug("Received %s: %s. Headers: %s", http_response, logger.debug(
http_response.text, http_response.headers) "Received %s. Headers: %s", http_response, http_response.headers)
if self.CONTENT_TYPE != http_response.headers.get( good_token = http_response.text == chall.token
"Content-Type", self.CONTENT_TYPE): if not good_token:
return False logger.error(
"Unable to verify %s! Expected: %r, returned: %r.",
try: uri, chall.token, http_response.text)
validation = jose.JWS.json_loads(http_response.text) # TODO: spec contradicts itself, c.f.
except jose.DeserializationError as error: # https://github.com/letsencrypt/acme-spec/pull/156/files#r33136438
logger.debug(error) good_ct = self.CONTENT_TYPE == http_response.headers.get(
return False "Content-Type", self.CONTENT_TYPE)
return self.good_path and good_ct and good_token
return self.check_validation(validation, chall, account_public_key)
class SimpleHTTPProvisionedResource(jose.JSONObjectWithFields):
"""SimpleHTTP provisioned resource."""
typ = fields.Fixed("type", SimpleHTTP.typ)
token = SimpleHTTP._fields["token"]
# If the "tls" field is not included in the response, then
# validation object MUST have its "tls" field set to "true".
tls = jose.Field("tls", omitempty=False)
@Challenge.register @Challenge.register
class DVSNI(DVChallenge): class DVSNI(DVChallenge):
"""ACME "dvsni" challenge. """ACME "dvsni" challenge.
:ivar bytes token: Random data, **not** base64-encoded. :ivar bytes r: Random data, **not** base64-encoded.
:ivar bytes nonce: Random data, **not** hex-encoded.
""" """
typ = "dvsni" typ = "dvsni"
DOMAIN_SUFFIX = b".acme.invalid"
"""Domain name suffix."""
R_SIZE = 32
"""Required size of the :attr:`r` in bytes."""
NONCE_SIZE = 16
"""Required size of the :attr:`nonce` in bytes."""
PORT = 443 PORT = 443
"""Port to perform DVSNI challenge.""" """Port to perform DVSNI challenge."""
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec r = jose.Field("r", encoder=jose.encode_b64jose, # pylint: disable=invalid-name
"""Minimum size of the :attr:`token` in bytes.""" decoder=functools.partial(jose.decode_b64jose, size=R_SIZE))
nonce = jose.Field("nonce", encoder=jose.encode_hex16,
decoder=functools.partial(functools.partial(
jose.decode_hex16, size=NONCE_SIZE)))
token = jose.Field( @property
"token", encoder=jose.encode_b64jose, decoder=functools.partial( def nonce_domain(self):
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) """Domain name used in SNI.
def gen_response(self, account_key, alg=jose.RS256, **kwargs): :rtype: bytes
"""Generate response.
:param .JWK account_key: Private account key.
:rtype: .DVSNIResponse
""" """
return DVSNIResponse(validation=jose.JWS.sign( return binascii.hexlify(self.nonce) + self.DOMAIN_SUFFIX
payload=self.json_dumps(sort_keys=True).encode('utf-8'),
key=account_key, alg=alg, **kwargs)) def probe_cert(self, domain, **kwargs):
"""Probe DVSNI challenge certificate."""
host = socket.gethostbyname(domain)
logging.debug('%s resolved to %s', domain, host)
kwargs.setdefault("host", host)
kwargs.setdefault("port", self.PORT)
kwargs["name"] = self.nonce_domain
# TODO: try different methods?
# pylint: disable=protected-access
return crypto_util._probe_sni(**kwargs)
@ChallengeResponse.register @ChallengeResponse.register
@ -306,138 +219,105 @@ class DVSNIResponse(ChallengeResponse):
""" """
typ = "dvsni" typ = "dvsni"
DOMAIN_SUFFIX = b".acme.invalid" DOMAIN_SUFFIX = DVSNI.DOMAIN_SUFFIX
"""Domain name suffix.""" """Domain name suffix."""
PORT = DVSNI.PORT S_SIZE = 32
"""Port to perform DVSNI challenge.""" """Required size of the :attr:`s` in bytes."""
validation = jose.Field("validation", decoder=jose.JWS.from_json) s = jose.Field("s", encoder=jose.encode_b64jose, # pylint: disable=invalid-name
decoder=functools.partial(jose.decode_b64jose, size=S_SIZE))
@property def __init__(self, s=None, *args, **kwargs):
def z(self): # pylint: disable=invalid-name s = os.urandom(self.S_SIZE) if s is None else s
"""The ``z`` parameter. super(DVSNIResponse, self).__init__(s=s, *args, **kwargs)
def z(self, chall): # pylint: disable=invalid-name
"""Compute the parameter ``z``.
:param challenge: Corresponding challenge.
:type challenge: :class:`DVSNI`
:rtype: bytes :rtype: bytes
""" """
# Instance of 'Field' has no 'signature' member z = hashlib.new("sha256") # pylint: disable=invalid-name
# pylint: disable=no-member z.update(chall.r)
return hashlib.sha256(self.validation.signature.encode( z.update(self.s)
"signature").encode("utf-8")).hexdigest().encode() return z.hexdigest().encode()
@property def z_domain(self, chall):
def z_domain(self):
"""Domain name for certificate subjectAltName. """Domain name for certificate subjectAltName.
:rtype: bytes :rtype bytes:
""" """
z = self.z # pylint: disable=invalid-name return self.z(chall) + self.DOMAIN_SUFFIX
return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX
@property def gen_cert(self, chall, domain, key):
def chall(self):
"""Get challenge encoded in the `validation` payload.
:rtype: challenges.DVSNI
"""
# pylint: disable=no-member
return DVSNI.json_loads(self.validation.payload.decode('utf-8'))
def gen_cert(self, key=None, bits=2048):
"""Generate DVSNI certificate. """Generate DVSNI certificate.
:param OpenSSL.crypto.PKey key: Optional private key used in :param .DVSNI chall: Corresponding challenge.
certificate generation. If not provided (``None``), then
fresh key will be generated.
:param int bits: Number of bits for newly generated key.
:rtype: `tuple` of `OpenSSL.crypto.X509` and
`OpenSSL.crypto.PKey`
"""
if key is None:
key = OpenSSL.crypto.PKey()
key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
return crypto_util.gen_ss_cert(key, [
# z_domain is too big to fit into CN, hence first dummy domain
'dummy', self.z_domain.decode()], force_san=True), key
def probe_cert(self, domain, **kwargs):
"""Probe DVSNI challenge certificate.
:param unicode domain: :param unicode domain:
:param OpenSSL.crypto.PKey
""" """
if "host" not in kwargs: return crypto_util.gen_ss_cert(key, [
host = socket.gethostbyname(domain) domain, chall.nonce_domain.decode(), self.z_domain(chall).decode()])
logging.debug('%s resolved to %s', domain, host)
kwargs["host"] = host
kwargs.setdefault("port", self.PORT) def simple_verify(self, chall, domain, public_key, **kwargs):
kwargs["name"] = self.z_domain
# TODO: try different methods?
# pylint: disable=protected-access
return crypto_util.probe_sni(**kwargs)
def verify_cert(self, cert):
"""Verify DVSNI challenge certificate."""
# pylint: disable=protected-access
sans = crypto_util._pyopenssl_cert_or_req_san(cert)
logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)
return self.z_domain.decode() in sans
def simple_verify(self, chall, domain, account_public_key,
cert=None, **kwargs):
"""Simple verify. """Simple verify.
Verify ``validation`` using ``account_public_key``, optionally Probes DVSNI certificate and checks it using `verify_cert`;
probe DVSNI certificate and check using `verify_cert`. hence all arguments documented in `verify_cert`.
"""
try:
cert = chall.probe_cert(domain=domain, **kwargs)
except errors.Error as error:
logger.debug(error, exc_info=True)
return False
return self.verify_cert(chall, domain, public_key, cert)
def verify_cert(self, chall, domain, public_key, cert):
"""Verify DVSNI certificate.
:param .challenges.DVSNI chall: Corresponding challenge. :param .challenges.DVSNI chall: Corresponding challenge.
:param str domain: Domain name being validated. :param str domain: Domain name being validated.
:type account_public_key: :param public_key: Public key for the key pair
being authorized. If ``None`` key verification is not
performed!
:type public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey` wrapped in `.ComparableKey
:param OpenSSL.crypto.X509 cert: Optional certificate. If not :param OpenSSL.crypto.X509 cert:
provided (``None``) certificate will be retrieved using
`probe_cert`.
:returns: ``True`` iff client's control of the domain has been :returns: ``True`` iff client's control of the domain has been
verified, ``False`` otherwise. verified, ``False`` otherwise.
:rtype: bool :rtype: bool
""" """
# pylint: disable=no-member # TODO: check "It is a valid self-signed certificate" and
if not self.validation.verify(key=account_public_key): # return False if not
# pylint: disable=protected-access
sans = crypto_util._pyopenssl_cert_or_req_san(cert)
logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)
cert = x509.load_der_x509_certificate(
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert),
default_backend())
if public_key is None:
logging.warn('No key verification is performed')
elif public_key != jose.ComparableKey(cert.public_key()):
return False return False
# TODO: it's not checked that payload has exectly 2 fields! return domain in sans and self.z_domain(chall).decode() in sans
try:
decoded_chall = self.chall
except jose.DeserializationError as error:
logger.debug(error, exc_info=True)
return False
if decoded_chall.token != chall.token:
logger.debug("Wrong token: expected %r, found %r",
chall.token, decoded_chall.token)
return False
if cert is None:
try:
cert = self.probe_cert(domain=domain, **kwargs)
except errors.Error as error:
logger.debug(error, exc_info=True)
return False
return self.verify_cert(cert)
@Challenge.register @Challenge.register
@ -467,6 +347,23 @@ class RecoveryContactResponse(ChallengeResponse):
token = jose.Field("token", omitempty=True) token = jose.Field("token", omitempty=True)
@Challenge.register
class RecoveryToken(ContinuityChallenge):
"""ACME "recoveryToken" challenge."""
typ = "recoveryToken"
@ChallengeResponse.register
class RecoveryTokenResponse(ChallengeResponse):
"""ACME "recoveryToken" challenge response.
:ivar unicode token:
"""
typ = "recoveryToken"
token = jose.Field("token", omitempty=True)
@Challenge.register @Challenge.register
class ProofOfPossession(ContinuityChallenge): class ProofOfPossession(ContinuityChallenge):
"""ACME "proofOfPossession" challenge. """ACME "proofOfPossession" challenge.
@ -548,100 +445,10 @@ class DNS(DVChallenge):
""" """
typ = "dns" typ = "dns"
token = jose.Field("token")
LABEL = "_acme-challenge"
"""Label clients prepend to the domain name being validated."""
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
token = jose.Field(
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
def gen_validation(self, account_key, alg=jose.RS256, **kwargs):
"""Generate validation.
:param .JWK account_key: Private account key.
:param .JWA alg:
:returns: This challenge wrapped in `.JWS`
:rtype: .JWS
"""
return jose.JWS.sign(
payload=self.json_dumps(sort_keys=True).encode('utf-8'),
key=account_key, alg=alg, **kwargs)
def check_validation(self, validation, account_public_key):
"""Check validation.
:param JWS validation:
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:rtype: bool
"""
if not validation.verify(key=account_public_key):
return False
try:
return self == self.json_loads(
validation.payload.decode('utf-8'))
except jose.DeserializationError as error:
logger.debug("Checking validation for DNS failed: %s", error)
return False
def gen_response(self, account_key, **kwargs):
"""Generate response.
:param .JWK account_key: Private account key.
:param .JWA alg:
:rtype: DNSResponse
"""
return DNSResponse(validation=self.gen_validation(
self, account_key, **kwargs))
def validation_domain_name(self, name):
"""Domain name for TXT validation record.
:param unicode name: Domain name being validated.
"""
return "{0}.{1}".format(self.LABEL, name)
@ChallengeResponse.register @ChallengeResponse.register
class DNSResponse(ChallengeResponse): class DNSResponse(ChallengeResponse):
"""ACME "dns" challenge response. """ACME "dns" challenge response."""
:param JWS validation:
"""
typ = "dns" typ = "dns"
validation = jose.Field("validation", decoder=jose.JWS.from_json)
def check_validation(self, chall, account_public_key):
"""Check validation.
:param challenges.DNS chall:
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:rtype: bool
"""
return chall.check_validation(self.validation, account_public_key)

View file

@ -17,42 +17,15 @@ CERT = test_util.load_cert('cert.pem')
KEY = test_util.load_rsa_private_key('rsa512_key.pem') KEY = test_util.load_rsa_private_key('rsa512_key.pem')
class ChallengeTest(unittest.TestCase):
def test_from_json_unrecognized(self):
from acme.challenges import Challenge
from acme.challenges import UnrecognizedChallenge
chall = UnrecognizedChallenge({"type": "foo"})
# pylint: disable=no-member
self.assertEqual(chall, Challenge.from_json(chall.jobj))
class UnrecognizedChallengeTest(unittest.TestCase):
def setUp(self):
from acme.challenges import UnrecognizedChallenge
self.jobj = {"type": "foo"}
self.chall = UnrecognizedChallenge(self.jobj)
def test_to_partial_json(self):
self.assertEqual(self.jobj, self.chall.to_partial_json())
def test_from_json(self):
from acme.challenges import UnrecognizedChallenge
self.assertEqual(
self.chall, UnrecognizedChallenge.from_json(self.jobj))
class SimpleHTTPTest(unittest.TestCase): class SimpleHTTPTest(unittest.TestCase):
def setUp(self): def setUp(self):
from acme.challenges import SimpleHTTP from acme.challenges import SimpleHTTP
self.msg = SimpleHTTP( self.msg = SimpleHTTP(
token=jose.decode_b64jose( token='evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))
self.jmsg = { self.jmsg = {
'type': 'simpleHttp', 'type': 'simpleHttp',
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA',
} }
def test_to_partial_json(self): def test_to_partial_json(self):
@ -66,36 +39,56 @@ class SimpleHTTPTest(unittest.TestCase):
from acme.challenges import SimpleHTTP from acme.challenges import SimpleHTTP
hash(SimpleHTTP.from_json(self.jmsg)) hash(SimpleHTTP.from_json(self.jmsg))
def test_good_token(self):
self.assertTrue(self.msg.good_token)
self.assertFalse(
self.msg.update(token=b'..').good_token)
class SimpleHTTPResponseTest(unittest.TestCase): class SimpleHTTPResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def setUp(self): def setUp(self):
from acme.challenges import SimpleHTTPResponse from acme.challenges import SimpleHTTPResponse
self.msg_http = SimpleHTTPResponse(tls=False) self.msg_http = SimpleHTTPResponse(
self.msg_https = SimpleHTTPResponse(tls=True) path='6tbIMBC5Anhl5bOlWT5ZFA', tls=False)
self.msg_https = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA')
self.jmsg_http = { self.jmsg_http = {
'resource': 'challenge', 'resource': 'challenge',
'type': 'simpleHttp', 'type': 'simpleHttp',
'path': '6tbIMBC5Anhl5bOlWT5ZFA',
'tls': False, 'tls': False,
} }
self.jmsg_https = { self.jmsg_https = {
'resource': 'challenge', 'resource': 'challenge',
'type': 'simpleHttp', 'type': 'simpleHttp',
'path': '6tbIMBC5Anhl5bOlWT5ZFA',
'tls': True, 'tls': True,
} }
from acme.challenges import SimpleHTTP from acme.challenges import SimpleHTTP
self.chall = SimpleHTTP(token=(b"x" * 16)) self.chall = SimpleHTTP(token="foo")
self.resp_http = SimpleHTTPResponse(tls=False) self.resp_http = SimpleHTTPResponse(path="bar", tls=False)
self.resp_https = SimpleHTTPResponse(tls=True) self.resp_https = SimpleHTTPResponse(path="bar", tls=True)
self.good_headers = {'Content-Type': SimpleHTTPResponse.CONTENT_TYPE} self.good_headers = {'Content-Type': SimpleHTTPResponse.CONTENT_TYPE}
def test_good_path(self):
self.assertTrue(self.msg_http.good_path)
self.assertTrue(self.msg_https.good_path)
self.assertFalse(
self.msg_http.update(path=(self.msg_http.path * 10)).good_path)
def test_scheme(self):
self.assertEqual('http', self.msg_http.scheme)
self.assertEqual('https', self.msg_https.scheme)
def test_port(self):
self.assertEqual(80, self.msg_http.port)
self.assertEqual(443, self.msg_https.port)
def test_uri(self):
self.assertEqual(
'http://example.com/.well-known/acme-challenge/'
'6tbIMBC5Anhl5bOlWT5ZFA', self.msg_http.uri('example.com'))
self.assertEqual(
'https://example.com/.well-known/acme-challenge/'
'6tbIMBC5Anhl5bOlWT5ZFA', self.msg_https.uri('example.com'))
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jmsg_http, self.msg_http.to_partial_json()) self.assertEqual(self.jmsg_http, self.msg_http.to_partial_json())
self.assertEqual(self.jmsg_https, self.msg_https.to_partial_json()) self.assertEqual(self.jmsg_https, self.msg_https.to_partial_json())
@ -112,98 +105,34 @@ class SimpleHTTPResponseTest(unittest.TestCase):
hash(SimpleHTTPResponse.from_json(self.jmsg_http)) hash(SimpleHTTPResponse.from_json(self.jmsg_http))
hash(SimpleHTTPResponse.from_json(self.jmsg_https)) hash(SimpleHTTPResponse.from_json(self.jmsg_https))
def test_scheme(self):
self.assertEqual('http', self.msg_http.scheme)
self.assertEqual('https', self.msg_https.scheme)
def test_port(self):
self.assertEqual(80, self.msg_http.port)
self.assertEqual(443, self.msg_https.port)
def test_uri(self):
self.assertEqual(
'http://example.com/.well-known/acme-challenge/'
'eHh4eHh4eHh4eHh4eHh4eA', self.msg_http.uri(
'example.com', self.chall))
self.assertEqual(
'https://example.com/.well-known/acme-challenge/'
'eHh4eHh4eHh4eHh4eHh4eA', self.msg_https.uri(
'example.com', self.chall))
def test_gen_check_validation(self):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
self.assertTrue(self.resp_http.check_validation(
validation=self.resp_http.gen_validation(self.chall, account_key),
chall=self.chall, account_public_key=account_key.public_key()))
def test_gen_check_validation_wrong_key(self):
key1 = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem'))
self.assertFalse(self.resp_http.check_validation(
validation=self.resp_http.gen_validation(self.chall, key1),
chall=self.chall, account_public_key=key2.public_key()))
def test_check_validation_wrong_payload(self):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
validations = tuple(
jose.JWS.sign(payload=payload, alg=jose.RS256, key=account_key)
for payload in (b'', b'{}', self.chall.json_dumps().encode('utf-8'),
self.resp_http.json_dumps().encode('utf-8'))
)
for validation in validations:
self.assertFalse(self.resp_http.check_validation(
validation=validation, chall=self.chall,
account_public_key=account_key.public_key()))
def test_check_validation_wrong_fields(self):
resource = self.resp_http.gen_resource(self.chall)
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
validations = tuple(
jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'),
alg=jose.RS256, key=account_key)
for bad_resource in (resource.update(tls=True),
resource.update(token=(b'x' * 20)))
)
for validation in validations:
self.assertFalse(self.resp_http.check_validation(
validation=validation, chall=self.chall,
account_public_key=account_key.public_key()))
@mock.patch("acme.challenges.requests.get") @mock.patch("acme.challenges.requests.get")
def test_simple_verify_good_validation(self, mock_get): def test_simple_verify_good_token(self, mock_get):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
for resp in self.resp_http, self.resp_https: for resp in self.resp_http, self.resp_https:
mock_get.reset_mock() mock_get.reset_mock()
validation = resp.gen_validation(self.chall, account_key)
mock_get.return_value = mock.MagicMock( mock_get.return_value = mock.MagicMock(
text=validation.json_dumps(), headers=self.good_headers) text=self.chall.token, headers=self.good_headers)
self.assertTrue(resp.simple_verify(self.chall, "local", None)) self.assertTrue(resp.simple_verify(self.chall, "local"))
mock_get.assert_called_once_with(resp.uri( mock_get.assert_called_once_with(resp.uri("local"), verify=False)
"local", self.chall), verify=False)
@mock.patch("acme.challenges.requests.get") @mock.patch("acme.challenges.requests.get")
def test_simple_verify_bad_validation(self, mock_get): def test_simple_verify_bad_token(self, mock_get):
mock_get.return_value = mock.MagicMock( mock_get.return_value = mock.MagicMock(
text="!", headers=self.good_headers) text=self.chall.token + "!", headers=self.good_headers)
self.assertFalse(self.resp_http.simple_verify( self.assertFalse(self.resp_http.simple_verify(self.chall, "local"))
self.chall, "local", None))
@mock.patch("acme.challenges.requests.get") @mock.patch("acme.challenges.requests.get")
def test_simple_verify_bad_content_type(self, mock_get): def test_simple_verify_bad_content_type(self, mock_get):
mock_get().text = self.chall.token mock_get().text = self.chall.token
self.assertFalse(self.resp_http.simple_verify( self.assertFalse(self.resp_http.simple_verify(self.chall, "local"))
self.chall, "local", None))
@mock.patch("acme.challenges.requests.get") @mock.patch("acme.challenges.requests.get")
def test_simple_verify_connection_error(self, mock_get): def test_simple_verify_connection_error(self, mock_get):
mock_get.side_effect = requests.exceptions.RequestException mock_get.side_effect = requests.exceptions.RequestException
self.assertFalse(self.resp_http.simple_verify( self.assertFalse(self.resp_http.simple_verify(self.chall, "local"))
self.chall, "local", None))
@mock.patch("acme.challenges.requests.get") @mock.patch("acme.challenges.requests.get")
def test_simple_verify_port(self, mock_get): def test_simple_verify_port(self, mock_get):
self.resp_http.simple_verify( self.resp_http.simple_verify(self.chall, "local", 4430)
self.chall, domain="local", account_public_key=None, port=4430)
self.assertEqual("local:4430", urllib_parse.urlparse( self.assertEqual("local:4430", urllib_parse.urlparse(
mock_get.mock_calls[0][1][0]).netloc) mock_get.mock_calls[0][1][0]).netloc)
@ -213,12 +142,19 @@ class DVSNITest(unittest.TestCase):
def setUp(self): def setUp(self):
from acme.challenges import DVSNI from acme.challenges import DVSNI
self.msg = DVSNI( self.msg = DVSNI(
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) r=b"O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6"
b"\xbf'\xb3\xed\x9a9nX\x0f'\\m\xe7\x12",
nonce=b'\xa8-_\xf8\xeft\r\x12\x88\x1fm<"w\xab.')
self.jmsg = { self.jmsg = {
'type': 'dvsni', 'type': 'dvsni',
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', 'r': 'Tyq0La3slT7tqQ0wlOiXnCY2vyez7Zo5blgPJ1xt5xI',
'nonce': 'a82d5ff8ef740d12881f6d3c2277ab2e',
} }
def test_nonce_domain(self):
self.assertEqual(b'a82d5ff8ef740d12881f6d3c2277ab2e.acme.invalid',
self.msg.nonce_domain)
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json()) self.assertEqual(self.jmsg, self.msg.to_partial_json())
@ -230,76 +166,27 @@ class DVSNITest(unittest.TestCase):
from acme.challenges import DVSNI from acme.challenges import DVSNI
hash(DVSNI.from_json(self.jmsg)) hash(DVSNI.from_json(self.jmsg))
def test_from_json_invalid_token_length(self): def test_from_json_invalid_r_length(self):
from acme.challenges import DVSNI from acme.challenges import DVSNI
self.jmsg['token'] = jose.encode_b64jose(b'abcd') self.jmsg['r'] = 'abcd'
self.assertRaises( self.assertRaises(
jose.DeserializationError, DVSNI.from_json, self.jmsg) jose.DeserializationError, DVSNI.from_json, self.jmsg)
def test_gen_response(self): def test_from_json_invalid_nonce_length(self):
key = jose.JWKRSA(key=KEY)
from acme.challenges import DVSNI from acme.challenges import DVSNI
self.assertEqual(self.msg, DVSNI.json_loads( self.jmsg['nonce'] = 'abcd'
self.msg.gen_response(key).validation.payload.decode())) self.assertRaises(
jose.DeserializationError, DVSNI.from_json, self.jmsg)
class DVSNIResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
self.key = jose.JWKRSA(key=KEY)
from acme.challenges import DVSNI
self.chall = DVSNI(
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
from acme.challenges import DVSNIResponse
self.validation = jose.JWS.sign(
payload=self.chall.json_dumps(sort_keys=True).encode(),
key=self.key, alg=jose.RS256)
self.msg = DVSNIResponse(validation=self.validation)
self.jmsg_to = {
'resource': 'challenge',
'type': 'dvsni',
'validation': self.validation,
}
self.jmsg_from = {
'resource': 'challenge',
'type': 'dvsni',
'validation': self.validation.to_json(),
}
# pylint: disable=invalid-name
label1 = b'e2df3498860637c667fedadc5a8494ec'
label2 = b'09dcc75553c9b3bd73662b50e71b1e42'
self.z = label1 + label2
self.z_domain = label1 + b'.' + label2 + b'.acme.invalid'
self.domain = 'foo.com'
def test_z_and_domain(self):
self.assertEqual(self.z, self.msg.z)
self.assertEqual(self.z_domain, self.msg.z_domain)
def test_to_partial_json(self):
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import DVSNIResponse
self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import DVSNIResponse
hash(DVSNIResponse.from_json(self.jmsg_from))
@mock.patch('acme.challenges.socket.gethostbyname') @mock.patch('acme.challenges.socket.gethostbyname')
@mock.patch('acme.challenges.crypto_util.probe_sni') @mock.patch('acme.challenges.crypto_util._probe_sni')
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
mock_gethostbyname.return_value = '127.0.0.1' mock_gethostbyname.return_value = '127.0.0.1'
self.msg.probe_cert('foo.com') self.msg.probe_cert('foo.com')
mock_gethostbyname.assert_called_once_with('foo.com') mock_gethostbyname.assert_called_once_with('foo.com')
mock_probe_sni.assert_called_once_with( mock_probe_sni.assert_called_once_with(
host='127.0.0.1', port=self.msg.PORT, host='127.0.0.1', port=self.msg.PORT,
name=self.z_domain) name=b'a82d5ff8ef740d12881f6d3c2277ab2e.acme.invalid')
self.msg.probe_cert('foo.com', host='8.8.8.8') self.msg.probe_cert('foo.com', host='8.8.8.8')
mock_probe_sni.assert_called_with( mock_probe_sni.assert_called_with(
@ -316,54 +203,88 @@ class DVSNIResponseTest(unittest.TestCase):
self.msg.probe_cert('foo.com', name=b'xxx') self.msg.probe_cert('foo.com', name=b'xxx')
mock_probe_sni.assert_called_with( mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY, host=mock.ANY, port=mock.ANY,
name=self.z_domain) name=b'a82d5ff8ef740d12881f6d3c2277ab2e.acme.invalid')
def test_gen_verify_cert(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.msg.gen_cert(key1)
self.assertEqual(key1, key2)
self.assertTrue(self.msg.verify_cert(cert))
def test_gen_verify_cert_gen_key(self): class DVSNIResponseTest(unittest.TestCase):
cert, key = self.msg.gen_cert()
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
self.assertTrue(self.msg.verify_cert(cert))
def test_verify_bad_cert(self): def setUp(self):
self.assertFalse(self.msg.verify_cert(test_util.load_cert('cert.pem'))) from acme.challenges import DVSNIResponse
# pylint: disable=invalid-name
s = '9dbjsl3gTAtOnEtKFEmhS6Mj-ajNjDcOmRkp3Lfzm3c'
self.msg = DVSNIResponse(s=jose.decode_b64jose(s))
self.jmsg = {
'resource': 'challenge',
'type': 'dvsni',
's': s,
}
def test_simple_verify_wrong_account_key(self): from acme.challenges import DVSNI
self.assertFalse(self.msg.simple_verify( self.chall = DVSNI(
self.chall, self.domain, jose.JWKRSA.load( r=jose.decode_b64jose('Tyq0La3slT7tqQ0wlOiXnCY2vyez7Zo5blgPJ1xt5xI'),
test_util.load_vector('rsa256_key.pem')).public_key())) nonce=jose.decode_b64jose('a82d5ff8ef740d12881f6d3c2277ab2e'))
self.z = (b'38e612b0397cc2624a07d351d7ef50e4'
b'6134c0213d9ed52f7d7c611acaeed41b')
self.domain = 'foo.com'
self.key = test_util.load_pyopenssl_private_key('rsa512_key.pem')
self.public_key = test_util.load_rsa_private_key(
'rsa512_key.pem').public_key()
def test_simple_verify_wrong_payload(self): def test_z_and_domain(self):
for payload in b'', b'{}': # pylint: disable=invalid-name
msg = self.msg.update(validation=jose.JWS.sign( self.assertEqual(self.z, self.msg.z(self.chall))
payload=payload, key=self.key, alg=jose.RS256)) self.assertEqual(
self.assertFalse(msg.simple_verify( self.z + b'.acme.invalid', self.msg.z_domain(self.chall))
self.chall, self.domain, self.key.public_key()))
def test_simple_verify_wrong_token(self): def test_to_partial_json(self):
msg = self.msg.update(validation=jose.JWS.sign( self.assertEqual(self.jmsg, self.msg.to_partial_json())
payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(),
key=self.key, alg=jose.RS256))
self.assertFalse(msg.simple_verify(
self.chall, self.domain, self.key.public_key()))
@mock.patch('acme.challenges.DVSNIResponse.verify_cert', autospec=True) def test_from_json(self):
from acme.challenges import DVSNIResponse
self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import DVSNIResponse
hash(DVSNIResponse.from_json(self.jmsg))
@mock.patch('acme.challenges.DVSNIResponse.verify_cert')
def test_simple_verify(self, mock_verify_cert): def test_simple_verify(self, mock_verify_cert):
mock_verify_cert.return_value = mock.sentinel.verification chall = mock.Mock()
self.assertEqual(mock.sentinel.verification, self.msg.simple_verify( chall.probe_cert.return_value = mock.sentinel.cert
self.chall, self.domain, self.key.public_key(), mock_verify_cert.return_value = 'x'
cert=mock.sentinel.cert)) self.assertEqual('x', self.msg.simple_verify(
mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) chall, mock.sentinel.domain, mock.sentinel.key))
chall.probe_cert.assert_called_once_with(domain=mock.sentinel.domain)
self.msg.verify_cert.assert_called_once_with(
chall, mock.sentinel.domain, mock.sentinel.key,
mock.sentinel.cert)
def test_simple_verify_false_on_probe_error(self): def test_simple_verify_false_on_probe_error(self):
chall = mock.Mock() chall = mock.Mock()
chall.probe_cert.side_effect = errors.Error chall.probe_cert.side_effect = errors.Error
self.assertFalse(self.msg.simple_verify( self.assertFalse(self.msg.simple_verify(
self.chall, self.domain, self.key.public_key())) chall=chall, domain=None, public_key=None))
def test_gen_verify_cert_postive_no_key(self):
cert = self.msg.gen_cert(self.chall, self.domain, self.key)
self.assertTrue(self.msg.verify_cert(
self.chall, self.domain, public_key=None, cert=cert))
def test_gen_verify_cert_postive_with_key(self):
cert = self.msg.gen_cert(self.chall, self.domain, self.key)
self.assertTrue(self.msg.verify_cert(
self.chall, self.domain, public_key=self.public_key, cert=cert))
def test_gen_verify_cert_negative_with_wrong_key(self):
cert = self.msg.gen_cert(self.chall, self.domain, self.key)
key = test_util.load_rsa_private_key('rsa256_key.pem').public_key()
self.assertFalse(self.msg.verify_cert(
self.chall, self.domain, public_key=key, cert=cert))
def test_gen_verify_cert_negative(self):
cert = self.msg.gen_cert(self.chall, self.domain + 'x', self.key)
self.assertFalse(self.msg.verify_cert(
self.chall, self.domain, public_key=None, cert=cert))
class RecoveryContactTest(unittest.TestCase): class RecoveryContactTest(unittest.TestCase):
@ -376,9 +297,9 @@ class RecoveryContactTest(unittest.TestCase):
contact='c********n@example.com') contact='c********n@example.com')
self.jmsg = { self.jmsg = {
'type': 'recoveryContact', 'type': 'recoveryContact',
'activationURL': 'https://example.ca/sendrecovery/a5bd99383fb0', 'activationURL' : 'https://example.ca/sendrecovery/a5bd99383fb0',
'successURL': 'https://example.ca/confirmrecovery/bb1b9928932', 'successURL' : 'https://example.ca/confirmrecovery/bb1b9928932',
'contact': 'c********n@example.com', 'contact' : 'c********n@example.com',
} }
def test_to_partial_json(self): def test_to_partial_json(self):
@ -439,6 +360,58 @@ class RecoveryContactResponseTest(unittest.TestCase):
self.assertEqual(self.jmsg, msg.to_partial_json()) self.assertEqual(self.jmsg, msg.to_partial_json())
class RecoveryTokenTest(unittest.TestCase):
def setUp(self):
from acme.challenges import RecoveryToken
self.msg = RecoveryToken()
self.jmsg = {'type': 'recoveryToken'}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import RecoveryToken
self.assertEqual(self.msg, RecoveryToken.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import RecoveryToken
hash(RecoveryToken.from_json(self.jmsg))
class RecoveryTokenResponseTest(unittest.TestCase):
def setUp(self):
from acme.challenges import RecoveryTokenResponse
self.msg = RecoveryTokenResponse(token='23029d88d9e123e')
self.jmsg = {
'resource': 'challenge',
'type': 'recoveryToken',
'token': '23029d88d9e123e'
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import RecoveryTokenResponse
self.assertEqual(
self.msg, RecoveryTokenResponse.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import RecoveryTokenResponse
hash(RecoveryTokenResponse.from_json(self.jmsg))
def test_json_without_optionals(self):
del self.jmsg['token']
from acme.challenges import RecoveryTokenResponse
msg = RecoveryTokenResponse.from_json(self.jmsg)
self.assertTrue(msg.token is None)
self.assertEqual(self.jmsg, msg.to_partial_json())
class ProofOfPossessionHintsTest(unittest.TestCase): class ProofOfPossessionHintsTest(unittest.TestCase):
def setUp(self): def setUp(self):
@ -596,15 +569,9 @@ class ProofOfPossessionResponseTest(unittest.TestCase):
class DNSTest(unittest.TestCase): class DNSTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.account_key = jose.JWKRSA.load(
test_util.load_vector('rsa512_key.pem'))
from acme.challenges import DNS from acme.challenges import DNS
self.msg = DNS(token=jose.b64decode( self.msg = DNS(token='17817c66b60ce2e4012dfad92657527a')
b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) self.jmsg = {'type': 'dns', 'token': '17817c66b60ce2e4012dfad92657527a'}
self.jmsg = {
'type': 'dns',
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
}
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json()) self.assertEqual(self.jmsg, self.msg.to_partial_json())
@ -617,84 +584,27 @@ class DNSTest(unittest.TestCase):
from acme.challenges import DNS from acme.challenges import DNS
hash(DNS.from_json(self.jmsg)) hash(DNS.from_json(self.jmsg))
def test_gen_check_validation(self):
self.assertTrue(self.msg.check_validation(
self.msg.gen_validation(self.account_key),
self.account_key.public_key()))
def test_gen_check_validation_wrong_key(self):
key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem'))
self.assertFalse(self.msg.check_validation(
self.msg.gen_validation(self.account_key), key2.public_key()))
def test_check_validation_wrong_payload(self):
validations = tuple(
jose.JWS.sign(payload=payload, alg=jose.RS256, key=self.account_key)
for payload in (b'', b'{}')
)
for validation in validations:
self.assertFalse(self.msg.check_validation(
validation, self.account_key.public_key()))
def test_check_validation_wrong_fields(self):
bad_validation = jose.JWS.sign(
payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'),
alg=jose.RS256, key=self.account_key)
self.assertFalse(self.msg.check_validation(
bad_validation, self.account_key.public_key()))
def test_gen_response(self):
with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen:
mock_gen.return_value = mock.sentinel.validation
response = self.msg.gen_response(self.account_key)
from acme.challenges import DNSResponse
self.assertTrue(isinstance(response, DNSResponse))
self.assertEqual(response.validation, mock.sentinel.validation)
def test_validation_domain_name(self):
self.assertEqual(
'_acme-challenge.le.wtf', self.msg.validation_domain_name('le.wtf'))
class DNSResponseTest(unittest.TestCase): class DNSResponseTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.key = jose.JWKRSA(key=KEY)
from acme.challenges import DNS
self.chall = DNS(token=jose.b64decode(
b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"))
self.validation = jose.JWS.sign(
payload=self.chall.json_dumps(sort_keys=True).encode(),
key=self.key, alg=jose.RS256)
from acme.challenges import DNSResponse from acme.challenges import DNSResponse
self.msg = DNSResponse(validation=self.validation) self.msg = DNSResponse()
self.jmsg_to = { self.jmsg = {
'resource': 'challenge', 'resource': 'challenge',
'type': 'dns', 'type': 'dns',
'validation': self.validation,
}
self.jmsg_from = {
'resource': 'challenge',
'type': 'dns',
'validation': self.validation.to_json(),
} }
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self): def test_from_json(self):
from acme.challenges import DNSResponse from acme.challenges import DNSResponse
self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg_from)) self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg))
def test_from_json_hashable(self): def test_from_json_hashable(self):
from acme.challenges import DNSResponse from acme.challenges import DNSResponse
hash(DNSResponse.from_json(self.jmsg_from)) hash(DNSResponse.from_json(self.jmsg))
def test_check_validation(self):
self.assertTrue(
self.msg.check_validation(self.chall, self.key.public_key()))
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -4,12 +4,11 @@ import heapq
import logging import logging
import time import time
import six
from six.moves import http_client # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error
import OpenSSL import OpenSSL
import requests import requests
import sys import six
import werkzeug import werkzeug
from acme import errors from acme import errors
@ -20,9 +19,8 @@ from acme import messages
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Python does not validate certificates by default before version 2.7.9
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if sys.version_info < (2, 7, 9): # pragma: no cover if six.PY2:
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
@ -33,7 +31,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
Clean up raised error types hierarchy, document, and handle (wrap) Clean up raised error types hierarchy, document, and handle (wrap)
instances of `.DeserializationError` raised in `from_json()`. instances of `.DeserializationError` raised in `from_json()`.
:ivar messages.Directory directory: :ivar str new_reg_uri: Location of new-reg
:ivar key: `.JWK` (private) :ivar key: `.JWK` (private)
:ivar alg: `.JWASignature` :ivar alg: `.JWASignature`
:ivar bool verify_ssl: Verify SSL certificates? :ivar bool verify_ssl: Verify SSL certificates?
@ -44,23 +42,12 @@ class Client(object): # pylint: disable=too-many-instance-attributes
""" """
DER_CONTENT_TYPE = 'application/pkix-cert' DER_CONTENT_TYPE = 'application/pkix-cert'
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, def __init__(self, new_reg_uri, key, alg=jose.RS256,
net=None): verify_ssl=True, net=None):
"""Initialize. self.new_reg_uri = new_reg_uri
:param directory: Directory Resource (`.messages.Directory`) or
URI from which the resource will be downloaded.
"""
self.key = key self.key = key
self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net
if isinstance(directory, six.string_types):
self.directory = messages.Directory.from_json(
self.net.get(directory).json())
else:
self.directory = directory
@classmethod @classmethod
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
terms_of_service=None): terms_of_service=None):
@ -94,7 +81,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
new_reg = messages.NewRegistration() if new_reg is None else new_reg new_reg = messages.NewRegistration() if new_reg is None else new_reg
assert isinstance(new_reg, messages.NewRegistration) assert isinstance(new_reg, messages.NewRegistration)
response = self.net.post(self.directory[new_reg], new_reg) response = self.net.post(self.new_reg_uri, new_reg)
# TODO: handle errors # TODO: handle errors
assert response.status_code == http_client.CREATED assert response.status_code == http_client.CREATED
@ -107,8 +94,18 @@ class Client(object): # pylint: disable=too-many-instance-attributes
return regr return regr
def _send_recv_regr(self, regr, body): def update_registration(self, regr):
response = self.net.post(regr.uri, body) """Update registration.
:pram regr: Registration Resource.
:type regr: `.RegistrationResource`
:returns: Updated Registration Resource.
:rtype: `.RegistrationResource`
"""
response = self.net.post(
regr.uri, messages.UpdateRegistration(**dict(regr.body)))
# TODO: Boulder returns httplib.ACCEPTED # TODO: Boulder returns httplib.ACCEPTED
#assert response.status_code == httplib.OK #assert response.status_code == httplib.OK
@ -116,37 +113,13 @@ class Client(object): # pylint: disable=too-many-instance-attributes
# TODO: Boulder does not set Location or Link on update # TODO: Boulder does not set Location or Link on update
# (c.f. acme-spec #94) # (c.f. acme-spec #94)
return self._regr_from_response( updated_regr = self._regr_from_response(
response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri,
terms_of_service=regr.terms_of_service) terms_of_service=regr.terms_of_service)
def update_registration(self, regr, update=None):
"""Update registration.
:param messages.RegistrationResource regr: Registration Resource.
:param messages.Registration update: Updated body of the
resource. If not provided, body will be taken from `regr`.
:returns: Updated Registration Resource.
:rtype: `.RegistrationResource`
"""
update = regr.body if update is None else update
updated_regr = self._send_recv_regr(
regr, body=messages.UpdateRegistration(**dict(update)))
if updated_regr != regr: if updated_regr != regr:
raise errors.UnexpectedUpdate(regr) raise errors.UnexpectedUpdate(regr)
return updated_regr return updated_regr
def query_registration(self, regr):
"""Query server about registration.
:param messages.RegistrationResource: Existing Registration
Resource.
"""
return self._send_recv_regr(regr, messages.UpdateRegistration())
def agree_to_tos(self, regr): def agree_to_tos(self, regr):
"""Agree to the terms-of-service. """Agree to the terms-of-service.
@ -302,7 +275,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
logger.debug("Requesting issuance...") logger.debug("Requesting issuance...")
# TODO: assert len(authzrs) == number of SANs # TODO: assert len(authzrs) == number of SANs
req = messages.CertificateRequest(csr=csr) req = messages.CertificateRequest(
csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs))
content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
response = self.net.post( response = self.net.post(
@ -429,34 +403,20 @@ class Client(object): # pylint: disable=too-many-instance-attributes
# respond with status code 403 (Forbidden) # respond with status code 403 (Forbidden)
return self.check_cert(certr) return self.check_cert(certr)
def fetch_chain(self, certr, max_length=10): def fetch_chain(self, certr):
"""Fetch chain for certificate. """Fetch chain for certificate.
:param .CertificateResource certr: Certificate Resource :param certr: Certificate Resource
:param int max_length: Maximum allowed length of the chain. :type certr: `.CertificateResource`
Note that each element in the certificate requires new
``HTTP GET`` request, and the length of the chain is
controlled by the ACME CA.
:raises errors.Error: if recursion exceeds `max_length` :returns: Certificate chain, or `None` if no "up" Link was provided.
:rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
:returns: Certificate chain for the Certificate Resource. It is
a list ordered so that the first element is a signer of the
certificate from Certificate Resource. Will be empty if
``cert_chain_uri`` is ``None``.
:rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
""" """
chain = [] if certr.cert_chain_uri is not None:
uri = certr.cert_chain_uri return self._get_cert(certr.cert_chain_uri)[1]
while uri is not None and len(chain) < max_length: else:
response, cert = self._get_cert(uri) return None
uri = response.links.get('up', {}).get('url')
chain.append(cert)
if uri is not None:
raise errors.Error(
"Recursion limit reached. Didn't get {0}".format(uri))
return chain
def revoke(self, cert): def revoke(self, cert):
"""Revoke certificate. """Revoke certificate.
@ -467,9 +427,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:raises .ClientError: If revocation is unsuccessful. :raises .ClientError: If revocation is unsuccessful.
""" """
response = self.net.post(self.directory[messages.Revocation], response = self.net.post(messages.Revocation.url(self.new_reg_uri),
messages.Revocation(certificate=cert), messages.Revocation(certificate=cert))
content_type=None)
if response.status_code != http_client.OK: if response.status_code != http_client.OK:
raise errors.ClientError( raise errors.ClientError(
'Successful revocation must return HTTP OK status') 'Successful revocation must return HTTP OK status')
@ -575,8 +534,7 @@ class ClientNetwork(object):
""" """
logging.debug('Sending %s request to %s. args: %r, kwargs: %r', logging.debug('Sending %s request to %s', method, url)
method, url, args, kwargs)
kwargs['verify'] = self.verify_ssl kwargs['verify'] = self.verify_ssl
response = requests.request(method, url, *args, **kwargs) response = requests.request(method, url, *args, **kwargs)
logging.debug('Received %s. Headers: %s. Content: %r', logging.debug('Received %s. Headers: %s. Content: %r',
@ -587,7 +545,7 @@ class ClientNetwork(object):
"""Send HEAD request without checking the response. """Send HEAD request without checking the response.
Note, that `_check_response` is not called, as it is expected Note, that `_check_response` is not called, as it is expected
that status code other than successfully 2xx will be returned, or that status code other than successfuly 2xx will be returned, or
messages2.Error will be raised by the server. messages2.Error will be raised by the server.
""" """

View file

@ -33,14 +33,10 @@ class ClientTest(unittest.TestCase):
self.net.post.return_value = self.response self.net.post.return_value = self.response
self.net.get.return_value = self.response self.net.get.return_value = self.response
self.directory = messages.Directory({
messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg',
messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert',
})
from acme.client import Client from acme.client import Client
self.client = Client( self.client = Client(
directory=self.directory, key=KEY, alg=jose.RS256, net=self.net) new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
key=KEY, alg=jose.RS256, net=self.net)
self.identifier = messages.Identifier( self.identifier = messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value='example.com') typ=messages.IDENTIFIER_FQDN, value='example.com')
@ -48,7 +44,7 @@ class ClientTest(unittest.TestCase):
# Registration # Registration
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages.Registration( reg = messages.Registration(
contact=self.contact, key=KEY.public_key()) contact=self.contact, key=KEY.public_key(), recovery_token='t')
self.new_reg = messages.NewRegistration(**dict(reg)) self.new_reg = messages.NewRegistration(**dict(reg))
self.regr = messages.RegistrationResource( self.regr = messages.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
@ -59,8 +55,7 @@ class ClientTest(unittest.TestCase):
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
challb = messages.ChallengeBody( challb = messages.ChallengeBody(
uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, uri=(authzr_uri + '/1'), status=messages.STATUS_VALID,
chall=challenges.DNS(token=jose.b64decode( chall=challenges.DNS(token='foo'))
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')))
self.challr = messages.ChallengeResource( self.challr = messages.ChallengeResource(
body=challb, authzr_uri=authzr_uri) body=challb, authzr_uri=authzr_uri)
self.authz = messages.Authorization( self.authz = messages.Authorization(
@ -77,13 +72,6 @@ class ClientTest(unittest.TestCase):
uri='https://www.letsencrypt-demo.org/acme/cert/1', uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca') cert_chain_uri='https://www.letsencrypt-demo.org/ca')
def test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import Client
self.client = Client(
directory=uri, key=KEY, alg=jose.RS256, net=self.net)
self.net.get.assert_called_once_with(uri)
def test_register(self): def test_register(self):
# "Instance of 'Field' has no to_json/update member" bug: # "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member # pylint: disable=no-member
@ -123,10 +111,6 @@ class ClientTest(unittest.TestCase):
self.assertRaises( self.assertRaises(
errors.UnexpectedUpdate, self.client.update_registration, self.regr) errors.UnexpectedUpdate, self.client.update_registration, self.regr)
def test_query_registration(self):
self.response.json.return_value = self.regr.body.to_json()
self.assertEqual(self.regr, self.client.query_registration(self.regr))
def test_agree_to_tos(self): def test_agree_to_tos(self):
self.client.update_registration = mock.Mock() self.client.update_registration = mock.Mock()
self.client.agree_to_tos(self.regr) self.client.agree_to_tos(self.regr)
@ -167,7 +151,7 @@ class ClientTest(unittest.TestCase):
self.response.links['up'] = {'url': self.challr.authzr_uri} self.response.links['up'] = {'url': self.challr.authzr_uri}
self.response.json.return_value = self.challr.body.to_json() self.response.json.return_value = self.challr.body.to_json()
chall_response = challenges.DNSResponse(validation=None) chall_response = challenges.DNSResponse()
self.client.answer_challenge(self.challr.body, chall_response) self.client.answer_challenge(self.challr.body, chall_response)
@ -176,9 +160,8 @@ class ClientTest(unittest.TestCase):
self.challr.body.update(uri='foo'), chall_response) self.challr.body.update(uri='foo'), chall_response)
def test_answer_challenge_missing_next(self): def test_answer_challenge_missing_next(self):
self.assertRaises( self.assertRaises(errors.ClientError, self.client.answer_challenge,
errors.ClientError, self.client.answer_challenge, self.challr.body, challenges.DNSResponse())
self.challr.body, challenges.DNSResponse(validation=None))
def test_retry_after_date(self): def test_retry_after_date(self):
self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT'
@ -348,39 +331,21 @@ class ClientTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
self.client.check_cert(self.certr), self.client.refresh(self.certr)) self.client.check_cert(self.certr), self.client.refresh(self.certr))
def test_fetch_chain_no_up_link(self): def test_fetch_chain(self):
self.assertEqual([], self.client.fetch_chain(self.certr.update(
cert_chain_uri=None)))
def test_fetch_chain_single(self):
# pylint: disable=protected-access # pylint: disable=protected-access
self.client._get_cert = mock.MagicMock() self.client._get_cert = mock.MagicMock()
self.client._get_cert.return_value = ( self.client._get_cert.return_value = ("response", "certificate")
mock.MagicMock(links={}), "certificate") self.assertEqual(self.client._get_cert(self.certr.cert_chain_uri)[1],
self.assertEqual([self.client._get_cert(self.certr.cert_chain_uri)[1]],
self.client.fetch_chain(self.certr)) self.client.fetch_chain(self.certr))
def test_fetch_chain_max(self): def test_fetch_chain_no_up_link(self):
# pylint: disable=protected-access self.assertTrue(self.client.fetch_chain(self.certr.update(
up_response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) cert_chain_uri=None)) is None)
noup_response = mock.MagicMock(links={})
self.client._get_cert = mock.MagicMock()
self.client._get_cert.side_effect = [
(up_response, "cert")] * 9 + [(noup_response, "last_cert")]
chain = self.client.fetch_chain(self.certr, max_length=10)
self.assertEqual(chain, ["cert"] * 9 + ["last_cert"])
def test_fetch_chain_too_many(self): # recursive
# pylint: disable=protected-access
response = mock.MagicMock(links={'up': {'url': 'http://cert'}})
self.client._get_cert = mock.MagicMock()
self.client._get_cert.return_value = (response, "certificate")
self.assertRaises(errors.Error, self.client.fetch_chain, self.certr)
def test_revoke(self): def test_revoke(self):
self.client.revoke(self.certr.body) self.client.revoke(self.certr.body)
self.net.post.assert_called_once_with( self.net.post.assert_called_once_with(messages.Revocation.url(
self.directory[messages.Revocation], mock.ANY, content_type=None) self.client.new_reg_uri), mock.ANY)
def test_revoke_bad_status_raises_error(self): def test_revoke_bad_status_raises_error(self):
self.response.status_code = http_client.METHOD_NOT_ALLOWED self.response.status_code = http_client.METHOD_NOT_ALLOWED
@ -410,14 +375,11 @@ class ClientNetworkTest(unittest.TestCase):
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
def to_partial_json(self): def to_partial_json(self):
return {'foo': self.value} return {'foo': self.value}
@classmethod @classmethod
def from_json(cls, value): def from_json(cls, value):
pass # pragma: no cover pass # pragma: no cover
# pylint: disable=protected-access # pylint: disable=protected-access
jws_dump = self.net._wrap_in_jws( jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce=b'Tg') MockJSONDeSerializable('foo'), nonce=b'Tg')
@ -521,7 +483,6 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')] self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')]
self.available_nonces = self.all_nonces[:] self.available_nonces = self.all_nonces[:]
def send_request(*args, **kwargs): def send_request(*args, **kwargs):
# pylint: disable=unused-argument,missing-docstring # pylint: disable=unused-argument,missing-docstring
if self.available_nonces: if self.available_nonces:

View file

@ -69,8 +69,8 @@ def _serve_sni(certs, sock, reuseaddr=True, method=_DEFAULT_DVSNI_SSL_METHOD,
raise errors.Error(error) raise errors.Error(error)
def probe_sni(name, host, port=443, timeout=300, def _probe_sni(name, host, port=443, timeout=300,
method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)): method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)):
"""Probe SNI server for SSL certificate. """Probe SNI server for SSL certificate.
:param bytes name: Byte string to send as the server name in the :param bytes name: Byte string to send as the server name in the
@ -155,18 +155,13 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
for part in parts if part.startswith(prefix)] for part in parts if part.startswith(prefix)]
def gen_ss_cert(key, domains, not_before=None, def gen_ss_cert(key, domains, not_before=None, validity=(7 * 24 * 60 * 60)):
validity=(7 * 24 * 60 * 60), force_san=True):
"""Generate new self-signed certificate. """Generate new self-signed certificate.
:type domains: `list` of `unicode` :type domains: `list` of `unicode`
:param OpenSSL.crypto.PKey key: :param OpenSSL.crypto.PKey key:
:param bool force_san:
If more than one domain is provided, all of the domains are put into Uses key and contains all domains.
``subjectAltName`` X.509 extension and first domain is set as the
subject CN. If only one domain is provided no ``subjectAltName``
extension is used, unless `force_san` is ``True``.
""" """
assert domains, "Must provide one or more hostnames for the cert." assert domains, "Must provide one or more hostnames for the cert."
@ -183,7 +178,7 @@ def gen_ss_cert(key, domains, not_before=None,
# TODO: what to put into cert.get_subject()? # TODO: what to put into cert.get_subject()?
cert.set_issuer(cert.get_subject()) cert.set_issuer(cert.get_subject())
if force_san or len(domains) > 1: if len(domains) > 1:
extensions.append(OpenSSL.crypto.X509Extension( extensions.append(OpenSSL.crypto.X509Extension(
b"subjectAltName", b"subjectAltName",
critical=False, critical=False,

View file

@ -13,7 +13,7 @@ from acme import test_util
class ServeProbeSNITest(unittest.TestCase): class ServeProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util._serve_sni/probe_sni.""" """Tests for acme.crypto_util._serve_sni/_probe_sni."""
def setUp(self): def setUp(self):
self.cert = test_util.load_cert('cert.pem') self.cert = test_util.load_cert('cert.pem')
@ -45,8 +45,8 @@ class ServeProbeSNITest(unittest.TestCase):
self.server.join() self.server.join()
def _probe(self, name): def _probe(self, name):
from acme.crypto_util import probe_sni from acme.crypto_util import _probe_sni
return jose.ComparableX509(probe_sni( return jose.ComparableX509(_probe_sni(
name, host='127.0.0.1', port=self.port)) name, host='127.0.0.1', port=self.port))
def test_probe_ok(self): def test_probe_ok(self):
@ -55,11 +55,10 @@ class ServeProbeSNITest(unittest.TestCase):
def test_probe_not_recognized_name(self): def test_probe_not_recognized_name(self):
self.assertRaises(errors.Error, self._probe, b'bar') self.assertRaises(errors.Error, self._probe, b'bar')
# TODO: py33/py34 tox hangs forever on do_hendshake in second probe def test_probe_connection_error(self):
#def probe_connection_error(self): self._probe(b'foo')
# self._probe(b'foo') time.sleep(1) # TODO: avoid race conditions in other way
# #time.sleep(1) # TODO: avoid race conditions in other way self.assertRaises(errors.Error, self._probe, b'bar')
# self.assertRaises(errors.Error, self._probe, b'bar')
class PyOpenSSLCertOrReqSANTest(unittest.TestCase): class PyOpenSSLCertOrReqSANTest(unittest.TestCase):

View file

@ -1,34 +1,9 @@
"""ACME JSON fields.""" """ACME JSON fields."""
import logging
import pyrfc3339 import pyrfc3339
from acme import jose from acme import jose
logger = logging.getLogger(__name__)
class Fixed(jose.Field):
"""Fixed field."""
def __init__(self, json_name, value):
self.value = value
super(Fixed, self).__init__(
json_name=json_name, default=value, omitempty=False)
def decode(self, value):
if value != self.value:
raise jose.DeserializationError('Expected {0!r}'.format(self.value))
return self.value
def encode(self, value):
if value != self.value:
logger.warn(
'Overriding fixed field (%s) with %r', self.json_name, value)
return value
class RFC3339Field(jose.Field): class RFC3339Field(jose.Field):
"""RFC3339 field encoder/decoder. """RFC3339 field encoder/decoder.
@ -56,6 +31,8 @@ class Resource(jose.Field):
def __init__(self, resource_type, *args, **kwargs): def __init__(self, resource_type, *args, **kwargs):
self.resource_type = resource_type self.resource_type = resource_type
super(Resource, self).__init__( super(Resource, self).__init__(
# TODO: omitempty used only to trick
# JSONObjectWithFieldsMeta._defaults..., server implementation
'resource', default=resource_type, *args, **kwargs) 'resource', default=resource_type, *args, **kwargs)
def decode(self, value): def decode(self, value):

View file

@ -7,26 +7,6 @@ import pytz
from acme import jose from acme import jose
class FixedTest(unittest.TestCase):
"""Tests for acme.fields.Fixed."""
def setUp(self):
from acme.fields import Fixed
self.field = Fixed('name', 'x')
def test_decode(self):
self.assertEqual('x', self.field.decode('x'))
def test_decode_bad(self):
self.assertRaises(jose.DeserializationError, self.field.decode, 'y')
def test_encode(self):
self.assertEqual('x', self.field.encode('x'))
def test_encode_override(self):
self.assertEqual('y', self.field.encode('y'))
class RFC3339FieldTest(unittest.TestCase): class RFC3339FieldTest(unittest.TestCase):
"""Tests for acme.fields.RFC3339Field.""" """Tests for acme.fields.RFC3339Field."""

View file

@ -8,10 +8,6 @@ class Error(Exception):
class DeserializationError(Error): class DeserializationError(Error):
"""JSON deserialization error.""" """JSON deserialization error."""
def __str__(self):
return "Deserialization error: {0}".format(
super(DeserializationError, self).__str__())
class SerializationError(Error): class SerializationError(Error):
"""JSON serialization error.""" """JSON serialization error."""

View file

@ -5,7 +5,6 @@ import json
import six import six
from acme.jose import errors
from acme.jose import util from acme.jose import util
# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class # pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class
@ -41,7 +40,7 @@ class JSONDeSerializable(object):
be encoded into a JSON document. **Full serialization** produces be encoded into a JSON document. **Full serialization** produces
a Python object composed of only basic types as required by the a Python object composed of only basic types as required by the
:ref:`conversion table <conversion-table>`. **Partial :ref:`conversion table <conversion-table>`. **Partial
serialization** (accomplished by :meth:`to_partial_json`) serialization** (acomplished by :meth:`to_partial_json`)
produces a Python object that might also be built from other produces a Python object that might also be built from other
:class:`JSONDeSerializable` objects. :class:`JSONDeSerializable` objects.
@ -173,11 +172,7 @@ class JSONDeSerializable(object):
@classmethod @classmethod
def json_loads(cls, json_string): def json_loads(cls, json_string):
"""Deserialize from JSON document string.""" """Deserialize from JSON document string."""
try: return cls.from_json(json.loads(json_string))
loads = json.loads(json_string)
except ValueError as error:
raise errors.DeserializationError(error)
return cls.from_json(loads)
def json_dumps(self, **kwargs): def json_dumps(self, **kwargs):
"""Dump to JSON string using proper serializer. """Dump to JSON string using proper serializer.
@ -194,7 +189,7 @@ class JSONDeSerializable(object):
:rtype: str :rtype: str
""" """
return self.json_dumps(sort_keys=True, indent=4) return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
@classmethod @classmethod
def json_dump_default(cls, python_object): def json_dump_default(cls, python_object):

View file

@ -91,7 +91,7 @@ class JSONDeSerializableTest(unittest.TestCase):
def test_json_dumps_pretty(self): def test_json_dumps_pretty(self):
self.assertEqual( self.assertEqual(
self.seq.json_dumps_pretty(), '[\n "foo1", \n "foo2"\n]') self.seq.json_dumps_pretty(), '[\n "foo1",\n "foo2"\n]')
def test_json_dump_default(self): def test_json_dump_default(self):
from acme.jose.interfaces import JSONDeSerializable from acme.jose.interfaces import JSONDeSerializable

View file

@ -221,22 +221,6 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
super(JSONObjectWithFields, self).__init__( super(JSONObjectWithFields, self).__init__(
**(dict(self._defaults(), **kwargs))) **(dict(self._defaults(), **kwargs)))
def encode(self, name):
"""Encode a single field.
:param str name: Name of the field to be encoded.
:raises erors.SerializationError: if field cannot be serialized
:raises errors.Error: if field could not be found
"""
try:
field = self._fields[name]
except KeyError:
raise errors.Error("Field not found: {0}".format(name))
return field.encode(getattr(self, name))
def fields_to_partial_json(self): def fields_to_partial_json(self):
"""Serialize fields to JSON.""" """Serialize fields to JSON."""
jobj = {} jobj = {}
@ -307,7 +291,6 @@ def encode_b64jose(data):
# b64encode produces ASCII characters only # b64encode produces ASCII characters only
return b64.b64encode(data).decode('ascii') return b64.b64encode(data).decode('ascii')
def decode_b64jose(data, size=None, minimum=False): def decode_b64jose(data, size=None, minimum=False):
"""Decode JOSE Base-64 field. """Decode JOSE Base-64 field.
@ -325,14 +308,12 @@ def decode_b64jose(data, size=None, minimum=False):
except error_cls as error: except error_cls as error:
raise errors.DeserializationError(error) raise errors.DeserializationError(error)
if size is not None and ((not minimum and len(decoded) != size) or if size is not None and ((not minimum and len(decoded) != size)
(minimum and len(decoded) < size)): or (minimum and len(decoded) < size)):
raise errors.DeserializationError( raise errors.DeserializationError()
"Expected at least or exactly {0} bytes".format(size))
return decoded return decoded
def encode_hex16(value): def encode_hex16(value):
"""Hexlify. """Hexlify.
@ -342,7 +323,6 @@ def encode_hex16(value):
""" """
return binascii.hexlify(value).decode() return binascii.hexlify(value).decode()
def decode_hex16(value, size=None, minimum=False): def decode_hex16(value, size=None, minimum=False):
"""Decode hexlified field. """Decode hexlified field.
@ -355,8 +335,8 @@ def decode_hex16(value, size=None, minimum=False):
""" """
value = value.encode() value = value.encode()
if size is not None and ((not minimum and len(value) != size * 2) or if size is not None and ((not minimum and len(value) != size * 2)
(minimum and len(value) < size * 2)): or (minimum and len(value) < size * 2)):
raise errors.DeserializationError() raise errors.DeserializationError()
error_cls = TypeError if six.PY2 else binascii.Error error_cls = TypeError if six.PY2 else binascii.Error
try: try:
@ -364,7 +344,6 @@ def decode_hex16(value, size=None, minimum=False):
except error_cls as error: except error_cls as error:
raise errors.DeserializationError(error) raise errors.DeserializationError(error)
def encode_cert(cert): def encode_cert(cert):
"""Encode certificate as JOSE Base-64 DER. """Encode certificate as JOSE Base-64 DER.
@ -375,7 +354,6 @@ def encode_cert(cert):
return encode_b64jose(OpenSSL.crypto.dump_certificate( return encode_b64jose(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, cert)) OpenSSL.crypto.FILETYPE_ASN1, cert))
def decode_cert(b64der): def decode_cert(b64der):
"""Decode JOSE Base-64 DER-encoded certificate. """Decode JOSE Base-64 DER-encoded certificate.
@ -389,7 +367,6 @@ def decode_cert(b64der):
except OpenSSL.crypto.Error as error: except OpenSSL.crypto.Error as error:
raise errors.DeserializationError(error) raise errors.DeserializationError(error)
def encode_csr(csr): def encode_csr(csr):
"""Encode CSR as JOSE Base-64 DER. """Encode CSR as JOSE Base-64 DER.
@ -400,7 +377,6 @@ def encode_csr(csr):
return encode_b64jose(OpenSSL.crypto.dump_certificate_request( return encode_b64jose(OpenSSL.crypto.dump_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, csr)) OpenSSL.crypto.FILETYPE_ASN1, csr))
def decode_csr(b64der): def decode_csr(b64der):
"""Decode JOSE Base-64 DER-encoded CSR. """Decode JOSE Base-64 DER-encoded CSR.
@ -442,9 +418,7 @@ class TypedJSONObjectWithFields(JSONObjectWithFields):
def get_type_cls(cls, jobj): def get_type_cls(cls, jobj):
"""Get the registered class for ``jobj``.""" """Get the registered class for ``jobj``."""
if cls in six.itervalues(cls.TYPES): if cls in six.itervalues(cls.TYPES):
if cls.type_field_name not in jobj: assert jobj[cls.type_field_name]
raise errors.DeserializationError(
"Missing type field ({0})".format(cls.type_field_name))
# cls is already registered type_cls, force to use it # cls is already registered type_cls, force to use it
# so that, e.g Revocation.from_json(jobj) fails if # so that, e.g Revocation.from_json(jobj) fails if
# jobj["type"] != "revocation". # jobj["type"] != "revocation".

View file

@ -52,7 +52,6 @@ class FieldTest(unittest.TestCase):
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
def to_partial_json(self): def to_partial_json(self):
return 'foo' # pragma: no cover return 'foo' # pragma: no cover
@classmethod @classmethod
def from_json(cls, jobj): def from_json(cls, jobj):
pass # pragma: no cover pass # pragma: no cover
@ -94,18 +93,14 @@ class JSONObjectWithFieldsMetaTest(unittest.TestCase):
self.field2 = Field('Baz2') self.field2 = Field('Baz2')
# pylint: disable=invalid-name,missing-docstring,too-few-public-methods # pylint: disable=invalid-name,missing-docstring,too-few-public-methods
# pylint: disable=blacklisted-name # pylint: disable=blacklisted-name
@six.add_metaclass(JSONObjectWithFieldsMeta) @six.add_metaclass(JSONObjectWithFieldsMeta)
class A(object): class A(object):
__slots__ = ('bar',) __slots__ = ('bar',)
baz = self.field baz = self.field
class B(A): class B(A):
pass pass
class C(A): class C(A):
baz = self.field2 baz = self.field2
self.a_cls = A self.a_cls = A
self.b_cls = B self.b_cls = B
self.c_cls = C self.c_cls = C
@ -165,18 +160,6 @@ class JSONObjectWithFieldsTest(unittest.TestCase):
def test_init_defaults(self): def test_init_defaults(self):
self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3)) self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3))
def test_encode(self):
self.assertEqual(10, self.MockJSONObjectWithFields(
x=5, y=0, z=0).encode("x"))
def test_encode_wrong_field(self):
self.assertRaises(errors.Error, self.mock.encode, 'foo')
def test_encode_serialization_error_passthrough(self):
self.assertRaises(
errors.SerializationError,
self.MockJSONObjectWithFields(y=500, z=None).encode, "y")
def test_fields_to_partial_json_omits_empty(self): def test_fields_to_partial_json_omits_empty(self):
self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3}) self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3})

View file

@ -21,7 +21,7 @@ from acme.jose import jwk
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class JWA(interfaces.JSONDeSerializable): # pylint: disable=abstract-method class JWA(interfaces.JSONDeSerializable): # pylint: disable=abstract-method
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
# for some reason disable=abstract-method has to be on the line # for some reason disable=abstract-method has to be on the line
# above... # above...
@ -159,7 +159,7 @@ class _JWAES(JWASignature): # pylint: disable=abstract-class-not-used
def sign(self, key, msg): # pragma: no cover def sign(self, key, msg): # pragma: no cover
raise NotImplementedError() raise NotImplementedError()
def verify(self, key, msg, sig): # pragma: no cover def verify(self, key, msg, sig): # pragma: no cover
raise NotImplementedError() raise NotImplementedError()

View file

@ -1,12 +1,10 @@
"""JSON Web Key.""" """JSON Web Key."""
import abc import abc
import binascii import binascii
import json
import logging import logging
import cryptography.exceptions import cryptography.exceptions
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
@ -29,32 +27,6 @@ class JWK(json_util.TypedJSONObjectWithFields):
cryptography_key_types = () cryptography_key_types = ()
"""Subclasses should override.""" """Subclasses should override."""
required = NotImplemented
"""Required members of public key's representation as defined by JWK/JWA."""
_thumbprint_json_dumps_params = {
# "no whitespace or line breaks before or after any syntactic
# elements"
'indent': 0,
'separators': (',', ':'),
# "members ordered lexicographically by the Unicode [UNICODE]
# code points of the member names"
'sort_keys': True,
}
def thumbprint(self, hash_function=hashes.SHA256):
"""Compute JWK Thumbprint.
https://tools.ietf.org/html/rfc7638
"""
digest = hashes.Hash(hash_function(), backend=default_backend())
digest.update(json.dumps(
dict((k, v) for k, v in six.iteritems(self.to_json())
if k in self.required),
**self._thumbprint_json_dumps_params).encode())
return digest.finalize()
@abc.abstractmethod @abc.abstractmethod
def public_key(self): # pragma: no cover def public_key(self): # pragma: no cover
"""Generate JWK with public key. """Generate JWK with public key.
@ -88,7 +60,7 @@ class JWK(json_util.TypedJSONObjectWithFields):
exceptions[loader] = error exceptions[loader] = error
# no luck # no luck
raise errors.Error('Unable to deserialize key: {0}'.format(exceptions)) raise errors.Error("Unable to deserialize key: {0}".format(exceptions))
@classmethod @classmethod
def load(cls, data, password=None, backend=None): def load(cls, data, password=None, backend=None):
@ -109,17 +81,17 @@ class JWK(json_util.TypedJSONObjectWithFields):
try: try:
key = cls._load_cryptography_key(data, password, backend) key = cls._load_cryptography_key(data, password, backend)
except errors.Error as error: except errors.Error as error:
logger.debug('Loading symmetric key, assymentric failed: %s', error) logger.debug("Loading symmetric key, assymentric failed: %s", error)
return JWKOct(key=data) return JWKOct(key=data)
if cls.typ is not NotImplemented and not isinstance( if cls.typ is not NotImplemented and not isinstance(
key, cls.cryptography_key_types): key, cls.cryptography_key_types):
raise errors.Error('Unable to deserialize {0} into {1}'.format( raise errors.Error("Unable to deserialize {0} into {1}".format(
key.__class__, cls.__class__)) key.__class__, cls.__class__))
for jwk_cls in six.itervalues(cls.TYPES): for jwk_cls in six.itervalues(cls.TYPES):
if isinstance(key, jwk_cls.cryptography_key_types): if isinstance(key, jwk_cls.cryptography_key_types):
return jwk_cls(key=key) return jwk_cls(key=key)
raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__)) raise errors.Error("Unsupported algorithm: {0}".format(key.__class__))
@JWK.register @JWK.register
@ -133,7 +105,6 @@ class JWKES(JWK): # pragma: no cover
typ = 'ES' typ = 'ES'
cryptography_key_types = ( cryptography_key_types = (
ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)
required = ('crv', JWK.type_field_name, 'x', 'y')
def fields_to_partial_json(self): def fields_to_partial_json(self):
raise NotImplementedError() raise NotImplementedError()
@ -151,7 +122,6 @@ class JWKOct(JWK):
"""Symmetric JWK.""" """Symmetric JWK."""
typ = 'oct' typ = 'oct'
__slots__ = ('key',) __slots__ = ('key',)
required = ('k', JWK.type_field_name)
def fields_to_partial_json(self): def fields_to_partial_json(self):
# TODO: An "alg" member SHOULD also be present to identify the # TODO: An "alg" member SHOULD also be present to identify the
@ -180,7 +150,6 @@ class JWKRSA(JWK):
typ = 'RSA' typ = 'RSA'
cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey)
__slots__ = ('key',) __slots__ = ('key',)
required = ('e', JWK.type_field_name, 'n')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'key' in kwargs and not isinstance( if 'key' in kwargs and not isinstance(
@ -235,7 +204,7 @@ class JWKRSA(JWK):
jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi')) jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi'))
if tuple(param for param in all_params if param is None): if tuple(param for param in all_params if param is None):
raise errors.Error( raise errors.Error(
'Some private parameters are missing: {0}'.format( "Some private parameters are missing: {0}".format(
all_params)) all_params))
p, q, dp, dq, qi = tuple( p, q, dp, dq, qi = tuple(
cls._decode_param(x) for x in all_params) cls._decode_param(x) for x in all_params)
@ -262,7 +231,7 @@ class JWKRSA(JWK):
'n': numbers.n, 'n': numbers.n,
'e': numbers.e, 'e': numbers.e,
} }
else: # rsa.RSAPrivateKey else: # rsa.RSAPrivateKey
private = self.key.private_numbers() private = self.key.private_numbers()
public = self.key.public_key().public_numbers() public = self.key.public_key().public_numbers()
params = { params = {

View file

@ -25,24 +25,9 @@ class JWKTest(unittest.TestCase):
self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM) self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM)
class JWKTestBaseMixin(object): class JWKOctTest(unittest.TestCase):
"""Mixin test for JWK subclass tests."""
thumbprint = NotImplemented
def test_thumbprint_private(self):
self.assertEqual(self.thumbprint, self.jwk.thumbprint())
def test_thumbprint_public(self):
self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint())
class JWKOctTest(unittest.TestCase, JWKTestBaseMixin):
"""Tests for acme.jose.jwk.JWKOct.""" """Tests for acme.jose.jwk.JWKOct."""
thumbprint = (b"=,\xdd;I\x1a+i\x02x\x8a\x12?06IM\xc2\x80"
b"\xe4\xc3\x1a\xfc\x89\xf3)'\xce\xccm\xfd5")
def setUp(self): def setUp(self):
from acme.jose.jwk import JWKOct from acme.jose.jwk import JWKOct
self.jwk = JWKOct(key=b'foo') self.jwk = JWKOct(key=b'foo')
@ -67,13 +52,10 @@ class JWKOctTest(unittest.TestCase, JWKTestBaseMixin):
self.assertTrue(self.jwk.public_key() is self.jwk) self.assertTrue(self.jwk.public_key() is self.jwk)
class JWKRSATest(unittest.TestCase, JWKTestBaseMixin): class JWKRSATest(unittest.TestCase):
"""Tests for acme.jose.jwk.JWKRSA.""" """Tests for acme.jose.jwk.JWKRSA."""
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
thumbprint = (b'\x08\xfa1\x87\x1d\x9b6H/*\x1eW\xc2\xe3\xf6P'
b'\xefs\x0cKB\x87\xcf\x85yO\x045\x0e\x91\x80\x0b')
def setUp(self): def setUp(self):
from acme.jose.jwk import JWKRSA from acme.jose.jwk import JWKRSA
self.jwk256 = JWKRSA(key=RSA256_KEY.public_key()) self.jwk256 = JWKRSA(key=RSA256_KEY.public_key())
@ -105,7 +87,6 @@ class JWKRSATest(unittest.TestCase, JWKTestBaseMixin):
'dq': 'bHh2u7etM8LKKCF2pY2UdQ', 'dq': 'bHh2u7etM8LKKCF2pY2UdQ',
'qi': 'oi45cEkbVoJjAbnQpFY87Q', 'qi': 'oi45cEkbVoJjAbnQpFY87Q',
}) })
self.jwk = self.private
def test_init_auto_comparable(self): def test_init_auto_comparable(self):
self.assertTrue(isinstance( self.assertTrue(isinstance(

View file

@ -53,8 +53,8 @@ class Header(json_util.JSONObjectWithFields):
.. warning:: This class does not support any extensions through .. warning:: This class does not support any extensions through
the "crit" (Critical) Header Parameter (4.1.11) and as a the "crit" (Critical) Header Parameter (4.1.11) and as a
conforming implementation, :meth:`from_json` treats its conforming implementation, :meth:`from_json` treats its
occurrence as an error. Please subclass if you seek for occurence as an error. Please subclass if you seek for
a different behaviour. a diferent behaviour.
:ivar x5tS256: "x5t#S256" :ivar x5tS256: "x5t#S256"
:ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`. :ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`.
@ -294,10 +294,10 @@ class JWS(json_util.JSONObjectWithFields):
# ... it must be in protected # ... it must be in protected
return ( return (
b64.b64encode(self.signature.protected.encode('utf-8')) + b64.b64encode(self.signature.protected.encode('utf-8'))
b'.' + + b'.' +
b64.b64encode(self.payload) + b64.b64encode(self.payload)
b'.' + + b'.' +
b64.b64encode(self.signature.signature)) b64.b64encode(self.signature.signature))
@classmethod @classmethod
@ -345,7 +345,6 @@ class JWS(json_util.JSONObjectWithFields):
signatures=tuple(cls.signature_cls.from_json(sig) signatures=tuple(cls.signature_cls.from_json(sig)
for sig in jobj['signatures'])) for sig in jobj['signatures']))
class CLI(object): class CLI(object):
"""JWS CLI.""" """JWS CLI."""

View file

@ -107,8 +107,8 @@ class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods
"""Wrapper for `cryptography` RSA keys. """Wrapper for `cryptography` RSA keys.
Wraps around: Wraps around:
- `cryptography.hazmat.primitives.asymmetric.RSAPrivateKey` - `cryptography.hazmat.primitives.assymetric.RSAPrivateKey`
- `cryptography.hazmat.primitives.asymmetric.RSAPublicKey` - `cryptography.hazmat.primitives.assymetric.RSAPublicKey`
""" """

View file

@ -1,10 +1,11 @@
"""ACME protocol messages.""" """ACME protocol messages."""
import collections import collections
from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error
from acme import challenges from acme import challenges
from acme import fields from acme import fields
from acme import jose from acme import jose
from acme import util
class Error(jose.JSONObjectWithFields, Exception): class Error(jose.JSONObjectWithFields, Exception):
@ -127,56 +128,6 @@ class Identifier(jose.JSONObjectWithFields):
value = jose.Field('value') value = jose.Field('value')
class Directory(jose.JSONDeSerializable):
"""Directory."""
_REGISTERED_TYPES = {}
@classmethod
def _canon_key(cls, key):
return getattr(key, 'resource_type', key)
@classmethod
def register(cls, resource_body_cls):
"""Register resource."""
assert resource_body_cls.resource_type not in cls._REGISTERED_TYPES
cls._REGISTERED_TYPES[resource_body_cls.resource_type] = resource_body_cls
return resource_body_cls
def __init__(self, jobj):
canon_jobj = util.map_keys(jobj, self._canon_key)
if not set(canon_jobj).issubset(self._REGISTERED_TYPES):
# TODO: acme-spec is not clear about this: 'It is a JSON
# dictionary, whose keys are the "resource" values listed
# in {{https-requests}}'z
raise ValueError('Wrong directory fields')
# TODO: check that everything is an absolute URL; acme-spec is
# not clear on that
self._jobj = canon_jobj
def __getattr__(self, name):
try:
return self[name.replace('_', '-')]
except KeyError as error:
raise AttributeError(str(error))
def __getitem__(self, name):
try:
return self._jobj[self._canon_key(name)]
except KeyError:
raise KeyError('Directory field not found')
def to_partial_json(self):
return self._jobj
@classmethod
def from_json(cls, jobj):
try:
return cls(jobj)
except ValueError as error:
raise jose.DeserializationError(str(error))
class Resource(jose.JSONObjectWithFields): class Resource(jose.JSONObjectWithFields):
"""ACME Resource. """ACME Resource.
@ -205,36 +156,16 @@ class Registration(ResourceBody):
:ivar acme.jose.jwk.JWK key: Public key. :ivar acme.jose.jwk.JWK key: Public key.
:ivar tuple contact: Contact information following ACME spec, :ivar tuple contact: Contact information following ACME spec,
`tuple` of `unicode`. `tuple` of `unicode`.
:ivar unicode recovery_token:
:ivar unicode agreement: :ivar unicode agreement:
:ivar unicode authorizations: URI where
`messages.Registration.Authorizations` can be found.
:ivar unicode certificates: URI where
`messages.Registration.Certificates` can be found.
""" """
# on new-reg key server ignores 'key' and populates it based on # on new-reg key server ignores 'key' and populates it based on
# JWS.signature.combined.jwk # JWS.signature.combined.jwk
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
contact = jose.Field('contact', omitempty=True, default=()) contact = jose.Field('contact', omitempty=True, default=())
recovery_token = jose.Field('recoveryToken', omitempty=True)
agreement = jose.Field('agreement', omitempty=True) agreement = jose.Field('agreement', omitempty=True)
authorizations = jose.Field('authorizations', omitempty=True)
certificates = jose.Field('certificates', omitempty=True)
class Authorizations(jose.JSONObjectWithFields):
"""Authorizations granted to Account in the process of registration.
:ivar tuple authorizations: URIs to Authorization Resources.
"""
authorizations = jose.Field('authorizations')
class Certificates(jose.JSONObjectWithFields):
"""Certificates granted to Account in the process of registration.
:ivar tuple certificates: URIs to Certificate Resources.
"""
certificates = jose.Field('certificates')
phone_prefix = 'tel:' phone_prefix = 'tel:'
email_prefix = 'mailto:' email_prefix = 'mailto:'
@ -265,20 +196,16 @@ class Registration(ResourceBody):
"""All emails found in the ``contact`` field.""" """All emails found in the ``contact`` field."""
return self._filter_contact(self.email_prefix) return self._filter_contact(self.email_prefix)
@Directory.register
class NewRegistration(Registration): class NewRegistration(Registration):
"""New registration.""" """New registration."""
resource_type = 'new-reg' resource_type = 'new-reg'
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class UpdateRegistration(Registration): class UpdateRegistration(Registration):
"""Update registration.""" """Update registration."""
resource_type = 'reg' resource_type = 'reg'
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class RegistrationResource(ResourceWithURI): class RegistrationResource(ResourceWithURI):
"""Registration Resource. """Registration Resource.
@ -306,7 +233,7 @@ class ChallengeBody(ResourceBody):
call ``challb.x`` to get ``challb.chall.x`` contents. call ``challb.x`` to get ``challb.chall.x`` contents.
:ivar acme.messages.Status status: :ivar acme.messages.Status status:
:ivar datetime.datetime validated: :ivar datetime.datetime validated:
:ivar messages.Error error: :ivar Error error:
""" """
__slots__ = ('chall',) __slots__ = ('chall',)
@ -381,14 +308,11 @@ class Authorization(ResourceBody):
return tuple(tuple(self.challenges[idx] for idx in combo) return tuple(tuple(self.challenges[idx] for idx in combo)
for combo in self.combinations) for combo in self.combinations)
@Directory.register
class NewAuthorization(Authorization): class NewAuthorization(Authorization):
"""New authorization.""" """New authorization."""
resource_type = 'new-authz' resource_type = 'new-authz'
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class AuthorizationResource(ResourceWithURI): class AuthorizationResource(ResourceWithURI):
"""Authorization Resource. """Authorization Resource.
@ -400,17 +324,18 @@ class AuthorizationResource(ResourceWithURI):
new_cert_uri = jose.Field('new_cert_uri') new_cert_uri = jose.Field('new_cert_uri')
@Directory.register
class CertificateRequest(jose.JSONObjectWithFields): class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request. """ACME new-cert request.
:ivar acme.jose.util.ComparableX509 csr: :ivar acme.jose.util.ComparableX509 csr:
`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
:ivar tuple authorizations: `tuple` of URIs (`str`)
""" """
resource_type = 'new-cert' resource_type = 'new-cert'
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
authorizations = jose.Field('authorizations', decoder=tuple)
class CertificateResource(ResourceWithURI): class CertificateResource(ResourceWithURI):
@ -426,7 +351,6 @@ class CertificateResource(ResourceWithURI):
authzrs = jose.Field('authzrs') authzrs = jose.Field('authzrs')
@Directory.register
class Revocation(jose.JSONObjectWithFields): class Revocation(jose.JSONObjectWithFields):
"""Revocation message. """Revocation message.
@ -438,3 +362,16 @@ class Revocation(jose.JSONObjectWithFields):
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
certificate = jose.Field( certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
# TODO: acme-spec#138, this allows only one ACME server instance per domain
PATH = '/acme/revoke-cert'
"""Path to revocation URL, see `url`"""
@classmethod
def url(cls, base):
"""Get revocation URL.
:param str base: New Registration Resource or server (root) URL.
"""
return urllib_parse.urljoin(base, cls.PATH)

View file

@ -60,7 +60,6 @@ class ConstantTest(unittest.TestCase):
def setUp(self): def setUp(self):
from acme.messages import _Constant from acme.messages import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES = {} POSSIBLE_NAMES = {}
@ -93,45 +92,6 @@ class ConstantTest(unittest.TestCase):
self.assertFalse(self.const_a != const_a_prime) self.assertFalse(self.const_a != const_a_prime)
class DirectoryTest(unittest.TestCase):
"""Tests for acme.messages.Directory."""
def setUp(self):
from acme.messages import Directory
self.dir = Directory({
'new-reg': 'reg',
mock.MagicMock(resource_type='new-cert'): 'cert',
})
def test_init_wrong_key_value_error(self):
from acme.messages import Directory
self.assertRaises(ValueError, Directory, {'foo': 'bar'})
def test_getitem(self):
self.assertEqual('reg', self.dir['new-reg'])
from acme.messages import NewRegistration
self.assertEqual('reg', self.dir[NewRegistration])
self.assertEqual('reg', self.dir[NewRegistration()])
def test_getitem_fails_with_key_error(self):
self.assertRaises(KeyError, self.dir.__getitem__, 'foo')
def test_getattr(self):
self.assertEqual('reg', self.dir.new_reg)
def test_getattr_fails_with_attribute_error(self):
self.assertRaises(AttributeError, self.dir.__getattr__, 'foo')
def test_to_partial_json(self):
self.assertEqual(
self.dir.to_partial_json(), {'new-reg': 'reg', 'new-cert': 'cert'})
def test_from_json_deserialization_error_on_wrong_key(self):
from acme.messages import Directory
self.assertRaises(
jose.DeserializationError, Directory.from_json, {'foo': 'bar'})
class RegistrationTest(unittest.TestCase): class RegistrationTest(unittest.TestCase):
"""Tests for acme.messages.Registration.""" """Tests for acme.messages.Registration."""
@ -141,15 +101,18 @@ class RegistrationTest(unittest.TestCase):
'mailto:admin@foo.com', 'mailto:admin@foo.com',
'tel:1234', 'tel:1234',
) )
recovery_token = 'XYZ'
agreement = 'https://letsencrypt.org/terms' agreement = 'https://letsencrypt.org/terms'
from acme.messages import Registration from acme.messages import Registration
self.reg = Registration(key=key, contact=contact, agreement=agreement) self.reg = Registration(
self.reg_none = Registration(authorizations='uri/authorizations', key=key, contact=contact, recovery_token=recovery_token,
certificates='uri/certificates') agreement=agreement)
self.reg_none = Registration()
self.jobj_to = { self.jobj_to = {
'contact': contact, 'contact': contact,
'recoveryToken': recovery_token,
'agreement': agreement, 'agreement': agreement,
'key': key, 'key': key,
} }
@ -182,17 +145,6 @@ class RegistrationTest(unittest.TestCase):
hash(Registration.from_json(self.jobj_from)) hash(Registration.from_json(self.jobj_from))
class UpdateRegistrationTest(unittest.TestCase):
"""Tests for acme.messages.UpdateRegistration."""
def test_empty(self):
from acme.messages import UpdateRegistration
jstring = '{"resource": "reg"}'
self.assertEqual(jstring, UpdateRegistration().json_dumps())
self.assertEqual(
UpdateRegistration(), UpdateRegistration.json_loads(jstring))
class RegistrationResourceTest(unittest.TestCase): class RegistrationResourceTest(unittest.TestCase):
"""Tests for acme.messages.RegistrationResource.""" """Tests for acme.messages.RegistrationResource."""
@ -225,8 +177,7 @@ class ChallengeBodyTest(unittest.TestCase):
"""Tests for acme.messages.ChallengeBody.""" """Tests for acme.messages.ChallengeBody."""
def setUp(self): def setUp(self):
self.chall = challenges.DNS(token=jose.b64decode( self.chall = challenges.DNS(token='foo')
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))
from acme.messages import ChallengeBody from acme.messages import ChallengeBody
from acme.messages import Error from acme.messages import Error
@ -242,7 +193,7 @@ class ChallengeBodyTest(unittest.TestCase):
'uri': 'http://challb', 'uri': 'http://challb',
'status': self.status, 'status': self.status,
'type': 'dns', 'type': 'dns',
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 'token': 'foo',
'error': error, 'error': error,
} }
self.jobj_from = self.jobj_to.copy() self.jobj_from = self.jobj_to.copy()
@ -252,6 +203,7 @@ class ChallengeBodyTest(unittest.TestCase):
'detail': 'Unable to communicate with DNS server', 'detail': 'Unable to communicate with DNS server',
} }
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.challb.to_partial_json()) self.assertEqual(self.jobj_to, self.challb.to_partial_json())
@ -264,8 +216,7 @@ class ChallengeBodyTest(unittest.TestCase):
hash(ChallengeBody.from_json(self.jobj_from)) hash(ChallengeBody.from_json(self.jobj_from))
def test_proxy(self): def test_proxy(self):
self.assertEqual(jose.b64decode( self.assertEqual('foo', self.challb.token)
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'), self.challb.token)
class AuthorizationTest(unittest.TestCase): class AuthorizationTest(unittest.TestCase):
@ -274,16 +225,14 @@ class AuthorizationTest(unittest.TestCase):
def setUp(self): def setUp(self):
from acme.messages import ChallengeBody from acme.messages import ChallengeBody
from acme.messages import STATUS_VALID from acme.messages import STATUS_VALID
self.challbs = ( self.challbs = (
ChallengeBody( ChallengeBody(
uri='http://challb1', status=STATUS_VALID, uri='http://challb1', status=STATUS_VALID,
chall=challenges.SimpleHTTP(token=b'IlirfxKKXAsHtmzK29Pj8A')), chall=challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A')),
ChallengeBody(uri='http://challb2', status=STATUS_VALID, ChallengeBody(uri='http://challb2', status=STATUS_VALID,
chall=challenges.DNS( chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
token=b'DGyRejmCefe7v4NfDGDKfA')),
ChallengeBody(uri='http://challb3', status=STATUS_VALID, ChallengeBody(uri='http://challb3', status=STATUS_VALID,
chall=challenges.RecoveryContact()), chall=challenges.RecoveryToken()),
) )
combinations = ((0, 2), (1, 2)) combinations = ((0, 2), (1, 2))
@ -334,7 +283,7 @@ class CertificateRequestTest(unittest.TestCase):
def setUp(self): def setUp(self):
from acme.messages import CertificateRequest from acme.messages import CertificateRequest
self.req = CertificateRequest(csr=CSR) self.req = CertificateRequest(csr=CSR, authorizations=('foo',))
def test_json_de_serializable(self): def test_json_de_serializable(self):
self.assertTrue(isinstance(self.req, jose.JSONDeSerializable)) self.assertTrue(isinstance(self.req, jose.JSONDeSerializable))
@ -362,6 +311,13 @@ class CertificateResourceTest(unittest.TestCase):
class RevocationTest(unittest.TestCase): class RevocationTest(unittest.TestCase):
"""Tests for acme.messages.RevocationTest.""" """Tests for acme.messages.RevocationTest."""
def test_url(self):
from acme.messages import Revocation
url = 'https://letsencrypt-demo.org/acme/revoke-cert'
self.assertEqual(url, Revocation.url('https://letsencrypt-demo.org'))
self.assertEqual(
url, Revocation.url('https://letsencrypt-demo.org/acme/new-reg'))
def setUp(self): def setUp(self):
from acme.messages import Revocation from acme.messages import Revocation
self.rev = Revocation(certificate=CERT) self.rev = Revocation(certificate=CERT)

View file

@ -36,7 +36,7 @@ class Signature(jose.JSONObjectWithFields):
:param bytes msg: Message to be signed. :param bytes msg: Message to be signed.
:param key: Key used for signing. :param key: Key used for signing.
:type key: `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` :type key: `cryptography.hazmat.primitives.assymetric.rsa.RSAPrivateKey`
(optionally wrapped in `.ComparableRSAKey`). (optionally wrapped in `.ComparableRSAKey`).
:param bytes nonce: Nonce to be used. If None, nonce of :param bytes nonce: Nonce to be used. If None, nonce of

View file

@ -1,3 +1,5 @@
# Symlinked in letsencrypt/tests/test_util.py, casues duplicate-code
# warning that cannot be disabled locally.
"""Test utilities. """Test utilities.
.. warning:: This module is not part of the public API. .. warning:: This module is not part of the public API.
@ -18,14 +20,12 @@ def vector_path(*names):
return pkg_resources.resource_filename( return pkg_resources.resource_filename(
__name__, os.path.join('testdata', *names)) __name__, os.path.join('testdata', *names))
def load_vector(*names): def load_vector(*names):
"""Load contents of a test vector.""" """Load contents of a test vector."""
# luckily, resource_string opens file in binary mode # luckily, resource_string opens file in binary mode
return pkg_resources.resource_string( return pkg_resources.resource_string(
__name__, os.path.join('testdata', *names)) __name__, os.path.join('testdata', *names))
def _guess_loader(filename, loader_pem, loader_der): def _guess_loader(filename, loader_pem, loader_der):
_, ext = os.path.splitext(filename) _, ext = os.path.splitext(filename)
if ext.lower() == '.pem': if ext.lower() == '.pem':
@ -35,7 +35,6 @@ def _guess_loader(filename, loader_pem, loader_der):
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Loader could not be recognized based on extension") raise ValueError("Loader could not be recognized based on extension")
def load_cert(*names): def load_cert(*names):
"""Load certificate.""" """Load certificate."""
loader = _guess_loader( loader = _guess_loader(
@ -43,7 +42,6 @@ def load_cert(*names):
return jose.ComparableX509(OpenSSL.crypto.load_certificate( return jose.ComparableX509(OpenSSL.crypto.load_certificate(
loader, load_vector(*names))) loader, load_vector(*names)))
def load_csr(*names): def load_csr(*names):
"""Load certificate request.""" """Load certificate request."""
loader = _guess_loader( loader = _guess_loader(
@ -51,7 +49,6 @@ def load_csr(*names):
return jose.ComparableX509(OpenSSL.crypto.load_certificate_request( return jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
loader, load_vector(*names))) loader, load_vector(*names)))
def load_rsa_private_key(*names): def load_rsa_private_key(*names):
"""Load RSA private key.""" """Load RSA private key."""
loader = _guess_loader(names[-1], serialization.load_pem_private_key, loader = _guess_loader(names[-1], serialization.load_pem_private_key,
@ -59,7 +56,6 @@ def load_rsa_private_key(*names):
return jose.ComparableRSAKey(loader( return jose.ComparableRSAKey(loader(
load_vector(*names), password=None, backend=default_backend())) load_vector(*names), password=None, backend=default_backend()))
def load_pyopenssl_private_key(*names): def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key.""" """Load pyOpenSSL private key."""
loader = _guess_loader( loader = _guess_loader(

View file

@ -1,7 +0,0 @@
"""ACME utilities."""
import six
def map_keys(dikt, func):
"""Map dictionary keys."""
return dict((func(key), value) for key, value in six.iteritems(dikt))

View file

@ -1,16 +0,0 @@
"""Tests for acme.util."""
import unittest
class MapKeysTest(unittest.TestCase):
"""Tests for acme.util.map_keys."""
def test_it(self):
from acme.util import map_keys
self.assertEqual({'a': 'b', 'c': 'd'},
map_keys({'a': 'b', 'c': 'd'}, lambda key: key))
self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1))
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -4,33 +4,27 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.1.0.dev0'
install_requires = [ install_requires = [
'argparse',
# load_pem_private/public_key (>=0.6) # load_pem_private/public_key (>=0.6)
# rsa_recover_prime_factors (>=0.8) # rsa_recover_prime_factors (>=0.8)
'cryptography>=0.8', 'cryptography>=0.8',
'mock<1.1.0', # py26
'pyrfc3339',
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'pyasn1', # urllib3 InsecurePlatformWarning (#304) 'pyasn1', # urllib3 InsecurePlatformWarning (#304)
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15) # Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
'PyOpenSSL>=0.15', 'PyOpenSSL>=0.15',
'pyrfc3339',
'pytz', 'pytz',
'requests', 'requests',
'setuptools', # pkg_resources
'six', 'six',
'werkzeug', 'werkzeug',
] ]
# env markers in extras_require cause problems with older pip: #517 # env markers in extras_require cause problems with older pip: #517
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
install_requires.extend([ # only some distros recognize stdlib argparse as already satisfying
# only some distros recognize stdlib argparse as already satisfying install_requires.append('argparse')
'argparse',
'mock<1.1.0',
])
else:
install_requires.append('mock')
testing_extras = [ testing_extras = [
'nose', 'nose',
@ -40,25 +34,7 @@ testing_extras = [
setup( setup(
name='acme', name='acme',
version=version,
description='ACME protocol implementation',
url='https://github.com/letsencrypt/letsencrypt',
author="Let's Encrypt Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],
packages=find_packages(), packages=find_packages(),
include_package_data=True,
install_requires=install_requires, install_requires=install_requires,
extras_require={ extras_require={
'testing': testing_extras, 'testing': testing_extras,

View file

@ -4,19 +4,8 @@
# - Fedora 22 (x64) # - Fedora 22 (x64)
# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet) # - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet)
if type yum 2>/dev/null
then
tool=yum
elif type dnf 2>/dev/null
then
tool=dnf
else
echo "Neither yum nor dnf found. Aborting bootstrap!"
exit 1
fi
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails) # "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
$tool install -y \ yum install -y \
git-core \ git-core \
python \ python \
python-devel \ python-devel \

View file

@ -1,15 +0,0 @@
#!/bin/sh
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
# only "virtualenv2" binary, not "virtualenv" necessary in
# ./bootstrap/dev/_common_venv.sh
pacman -S \
git \
python2 \
python-virtualenv \
gcc \
dialog \
augeas \
openssl \
libffi \
ca-certificates \

View file

@ -1 +0,0 @@
This directory contains developer setup.

View file

@ -1,25 +0,0 @@
#!/bin/sh -xe
VENV_NAME=${VENV_NAME:-venv}
# .egg-info directories tend to cause bizzaire problems (e.g. `pip -e
# .` might unexpectedly install letshelp-letsencrypt only, in case
# `python letshelp-letsencrypt/setup.py build` has been called
# earlier)
rm -rf *.egg-info
# virtualenv setup is NOT idempotent: shutil.Error:
# `/home/jakub/dev/letsencrypt/letsencrypt/venv/bin/python2` and
# `venv/bin/python2` are the same file
mv $VENV_NAME "$VENV_NAME.$(date +%s).bak" || true
virtualenv --no-site-packages $VENV_NAME $VENV_ARGS
. ./$VENV_NAME/bin/activate
# Separately install setuptools and pip to make sure following
# invocations use latest
pip install -U setuptools
pip install -U pip
pip install "$@"
echo "Please run the following command to activate developer environment:"
echo "source $VENV_NAME/bin/activate"

View file

@ -1,13 +0,0 @@
#!/bin/sh -xe
# Developer virtualenv setup for Let's Encrypt client
export VENV_ARGS="--python python2"
./bootstrap/dev/_venv_common.sh \
-r requirements.txt \
-e acme[testing] \
-e .[dev,docs,testing] \
-e letsencrypt-apache \
-e letsencrypt-nginx \
-e letshelp-letsencrypt \
-e letsencrypt-compatibility-test

View file

@ -1,8 +0,0 @@
#!/bin/sh -xe
# Developer Python3 virtualenv setup for Let's Encrypt
export VENV_NAME="${VENV_NAME:-venv3}"
export VENV_ARGS="--python python3"
./bootstrap/dev/_venv_common.sh \
-e acme[testing] \

View file

@ -1,8 +0,0 @@
#!/bin/sh -xe
pkg install -Ay \
git \
python \
py27-virtualenv \
augeas \
libffi \

View file

@ -1,8 +1,2 @@
#!/bin/sh #!/bin/sh
if ! hash brew 2>/dev/null; then
echo "Homebrew Not Installed\nDownloading..."
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
fi
brew install augeas brew install augeas
brew install dialog

View file

@ -21,3 +21,9 @@
.. automodule:: letsencrypt.display.enhancements .. automodule:: letsencrypt.display.enhancements
:members: :members:
:mod:`letsencrypt.display.revocation`
=====================================
.. automodule:: letsencrypt.display.revocation
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.recovery_token`
--------------------------------------------------
.. automodule:: letsencrypt.recovery_token
:members:

5
docs/api/revoker.rst Normal file
View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.revoker`
--------------------------
.. automodule:: letsencrypt.revoker
:members:

View file

@ -30,7 +30,7 @@ here = os.path.abspath(os.path.dirname(__file__))
# read version number (and other metadata) from package init # read version number (and other metadata) from package init
init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py') init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py')
with codecs.open(init_fn, encoding='utf8') as fd: with codecs.open(init_fn, encoding='utf8') as fd:
meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", fd.read())) meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", fd.read()))
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
@ -54,7 +54,6 @@ extensions = [
'sphinx.ext.coverage', 'sphinx.ext.coverage',
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
'repoze.sphinx.autointerface', 'repoze.sphinx.autointerface',
'sphinxcontrib.programoutput',
] ]
autodoc_member_order = 'bysource' autodoc_member_order = 'bysource'
@ -284,12 +283,7 @@ latex_documents = [
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'letsencrypt', u'Let\'s Encrypt Documentation', ('index', 'letsencrypt', u'Let\'s Encrypt Documentation',
[project], 7), [u'Let\'s Encrypt Project'], 1)
('man/letsencrypt', 'letsencrypt', u'letsencrypt script documentation',
[project], 1),
('man/letsencrypt-renewer', 'letsencrypt-renewer',
u'letsencrypt-renewer script documentation', [project], 1),
('man/jws', 'jws', u'jws script documentation', [project], 1),
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.

View file

@ -7,37 +7,38 @@ Contributing
Hacking Hacking
======= =======
All changes in your pull request **must** have 100% unit test coverage, pass Start by :doc:`installing dependencies and setting up Let's Encrypt
our `integration`_ tests, **and** be compliant with the <using>`.
:ref:`coding style <coding-style>`.
When you're done activate the virtualenv:
Bootstrap
---------
Start by :ref:`installing Let's Encrypt prerequisites
<prerequisites>`. Then run:
.. code-block:: shell .. code-block:: shell
./bootstrap/dev/venv.sh source ./venv/bin/activate
Activate the virtualenv: This step should prepend you prompt with ``(venv)`` and save you from
typing ``./venv/bin/...``. It is also required to run some of the
.. code-block:: shell `testing`_ tools. Virtualenv can be disabled at any time by typing
``deactivate``. More information can be found in `virtualenv
source ./$VENV_NAME/bin/activate
This step should prepend you prompt with ``($VENV_NAME)`` and save you
from typing ``./$VENV_NAME/bin/...``. It is also required to run some
of the `testing`_ tools. Virtualenv can be disabled at any time by
typing ``deactivate``. More information can be found in `virtualenv
documentation`_. documentation`_.
Note that packages are installed in so called *editable mode*, in Install the development packages:
which any source code changes in the current working directory are
"live" and no further ``./bootstrap/dev/venv.sh`` or ``pip install .. code-block:: shell
...`` invocations are necessary while developing.
pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx
.. note:: `-e` (short for `--editable`) turns on *editable mode* in
which any source code changes in the current working
directory are "live" and no further `pip install ...`
invocations are necessary while developing.
This is roughly equivalent to `python setup.py develop`. For
more info see `man pip`.
The code base, including your pull requests, **must** have 100% unit
test coverage, pass our `integration`_ tests **and** be compliant with
the :ref:`coding style <coding-style>`.
.. _`virtualenv documentation`: https://virtualenv.pypa.io .. _`virtualenv documentation`: https://virtualenv.pypa.io
@ -51,8 +52,7 @@ The following tools are there to help you:
before submitting a new pull request. before submitting a new pull request.
- ``tox -e cover`` checks the test coverage only. Calling the - ``tox -e cover`` checks the test coverage only. Calling the
``./tox.cover.sh`` script directly (or even ``./tox.cover.sh $pkg1 ``./tox-cover.sh`` script directly might be a bit quicker, though.
$pkg2 ...`` for any subpackages) might be a bit quicker, though.
- ``tox -e lint`` checks the style of the whole project, while - ``tox -e lint`` checks the style of the whole project, while
``pylint --rcfile=.pylintrc path`` will check a single file or ``pylint --rcfile=.pylintrc path`` will check a single file or
@ -60,33 +60,29 @@ The following tools are there to help you:
- For debugging, we recommend ``pip install ipdb`` and putting - For debugging, we recommend ``pip install ipdb`` and putting
``import ipdb; ipdb.set_trace()`` statement inside the source ``import ipdb; ipdb.set_trace()`` statement inside the source
code. Alternatively, you can use Python's standard library `pdb`, code. Alternatively, you can use Python'd standard library `pdb`,
but you won't get TAB completion... but you won't get TAB completion...
Integration Integration
~~~~~~~~~~~ ~~~~~~~~~~~
Mac OS X users: Run `./tests/mac-bootstrap.sh` instead of `boulder-start.sh` to
install dependencies, configure the environment, and start boulder.
Otherwise, install `Go`_ 1.5, libtool-ltdl, mariadb-server and First, install `Go`_ 1.4 and start Boulder_, an ACME CA server::
rabbitmq-server and then start Boulder_, an ACME CA server::
./tests/boulder-start.sh ./tests/boulder-start.sh
The script will download, compile and run the executable; please be The script will download, compile and run the executable; please be
patient - it will take some time... Once its ready, you will see patient - it will take some time... Once its ready, you will see
``Server running, listening on 127.0.0.1:4000...``. Add an ``Server running, listening on 127.0.0.1:4000...``. You may now run
``/etc/hosts`` entry pointing ``le.wtf`` to 127.0.0.1. You may now (in a separate terminal)::
run (in a separate terminal)::
./tests/boulder-integration.sh && echo OK || echo FAIL ./tests/boulder-integration.sh && echo OK || echo FAIL
If you would like to test `letsencrypt_nginx` plugin (highly If you would like to test `lesencrypt_nginx` plugin (highly
encouraged) make sure to install prerequisites as listed in encouraged) make sure to install prerequisites as listed in
``letsencrypt-nginx/tests/boulder-integration.sh``: ``tests/integration/nginx.sh``:
.. include:: ../letsencrypt-nginx/tests/boulder-integration.sh .. include:: ../tests/integration/nginx.sh
:start-line: 1 :start-line: 1
:end-line: 2 :end-line: 2
:code: shell :code: shell
@ -125,27 +121,6 @@ Support for other Linux distributions coming soon.
.. _related issue: https://github.com/ClusterHQ/flocker/issues/516 .. _related issue: https://github.com/ClusterHQ/flocker/issues/516
Docker
------
OSX users will probably find it easiest to set up a Docker container for
development. Let's Encrypt comes with a Dockerfile (``Dockerfile-dev``)
for doing so. To use Docker on OSX, install and setup docker-machine using the
instructions at https://docs.docker.com/installation/mac/.
To build the development Docker image::
docker build -t letsencrypt -f Dockerfile-dev .
Now run tests inside the Docker image:
.. code-block:: shell
docker run -it letsencrypt bash
cd src
tox -e py27
Code components and layout Code components and layout
========================== ==========================

View file

@ -1 +0,0 @@
.. program-output:: jws --help all

View file

@ -1 +0,0 @@
.. program-output:: letsencrypt-renewer --help

View file

@ -1 +0,0 @@
.. program-output:: letsencrypt --help all

View file

@ -1,53 +0,0 @@
:mod:`letsencrypt_compatibility_test`
-------------------------------------
.. automodule:: letsencrypt_compatibility_test
:members:
:mod:`letsencrypt_compatibility_test.errors`
============================================
.. automodule:: letsencrypt_compatibility_test.errors
:members:
:mod:`letsencrypt_compatibility_test.interfaces`
================================================
.. automodule:: letsencrypt_compatibility_test.interfaces
:members:
:mod:`letsencrypt_compatibility_test.test_driver`
=================================================
.. automodule:: letsencrypt_compatibility_test.test_driver
:members:
:mod:`letsencrypt_compatibility_test.util`
==========================================
.. automodule:: letsencrypt_compatibility_test.util
:members:
:mod:`letsencrypt_compatibility_test.configurators`
===================================================
.. automodule:: letsencrypt_compatibility_test.configurators
:members:
:mod:`letsencrypt_compatibility_test.configurators.apache`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: letsencrypt_compatibility_test.configurators.apache
:members:
:mod:`letsencrypt_compatibility_test.configurators.apache.apache24`
-------------------------------------------------------------------
.. automodule:: letsencrypt_compatibility_test.configurators.apache.apache24
:members:
:mod:`letsencrypt_compatibility_test.configurators.apache.common`
-------------------------------------------------------------------
.. automodule:: letsencrypt_compatibility_test.configurators.apache.common
:members:

View file

@ -1,11 +0,0 @@
:mod:`letshelp_letsencrypt`
---------------------------
.. automodule:: letshelp_letsencrypt
:members:
:mod:`letshelp_letsencrypt.apache`
==================================
.. automodule:: letshelp_letsencrypt.apache
:members:

View file

@ -42,8 +42,6 @@ above method instead.
https://github.com/letsencrypt/letsencrypt/archive/master.zip https://github.com/letsencrypt/letsencrypt/archive/master.zip
.. _prerequisites:
Prerequisites Prerequisites
============= =============
@ -85,7 +83,7 @@ Mac OSX
.. code-block:: shell .. code-block:: shell
./bootstrap/mac.sh sudo ./bootstrap/mac.sh
Fedora Fedora
@ -104,32 +102,15 @@ Centos 7
sudo ./bootstrap/centos.sh sudo ./bootstrap/centos.sh
FreeBSD
-------
.. code-block:: shell
sudo ./bootstrap/freebsd.sh
Bootstrap script for FreeBSD uses ``pkg`` for package installation,
i.e. it does not use ports.
FreeBSD by default uses ``tcsh``. In order to activate virtulenv (see
below), you will need a compatbile shell, e.g. ``pkg install bash &&
bash``.
Installation Installation
============ ============
.. "pip install acme" doesn't search for "acme" in cwd, just like "pip .. "pip install acme" doesn't search for "acme" in cwd, just like "pip
install -e acme" does; `-U setuptools pip` necessary for #722 install -e acme" does
.. code-block:: shell .. code-block:: shell
virtualenv --no-site-packages -p python2 venv virtualenv --no-site-packages -p python2 venv
./venv/bin/pip install -U setuptools
./venv/bin/pip install -U pip
./venv/bin/pip install -r requirements.txt acme/ . letsencrypt-apache/ letsencrypt-nginx/ ./venv/bin/pip install -r requirements.txt acme/ . letsencrypt-apache/ letsencrypt-nginx/
.. warning:: Please do **not** use ``python setup.py install``. Please .. warning:: Please do **not** use ``python setup.py install``. Please
@ -148,7 +129,7 @@ To get a new certificate run:
.. code-block:: shell .. code-block:: shell
sudo ./venv/bin/letsencrypt auth ./venv/bin/letsencrypt auth
The ``letsencrypt`` commandline tool has a builtin help: The ``letsencrypt`` commandline tool has a builtin help:

View file

@ -9,7 +9,6 @@ domains = example.com
text = True text = True
agree-eula = True agree-eula = True
agree-tos = True
debug = True debug = True
# Unfortunately, it's not possible to specify "verbose" multiple times # Unfortunately, it's not possible to specify "verbose" multiple times
# (correspondingly to -vvvvvv) # (correspondingly to -vvvvvv)

View file

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# This script generates a simple SAN CSR to be used with Let's Encrypt # This script generates a simple SAN CSR to be used with Let's Encrypt
# CA. Mostly intended for "auth --csr" testing, but, since it's easily # CA. Mostly intedened for "auth --csr" testing, but, since its easily
# auditable, feel free to adjust it and use it on your production web # auditable, feel free to adjust it and use on you production web
# server. # server.
if [ "$#" -lt 1 ] if [ "$#" -lt 1 ]

View file

@ -1,190 +0,0 @@
Copyright 2015 Electronic Frontier Foundation and others
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -1,4 +1,2 @@
include LICENSE.txt
include README.rst
recursive-include letsencrypt_apache/tests/testdata * recursive-include letsencrypt_apache/tests/testdata *
include letsencrypt_apache/options-ssl-apache.conf include letsencrypt_apache/options-ssl-apache.conf

View file

@ -1 +0,0 @@
Apache plugin for Let's Encrypt client

View file

@ -3,7 +3,6 @@ import logging
import augeas import augeas
from letsencrypt import errors
from letsencrypt import reverter from letsencrypt import reverter
from letsencrypt.plugins import common from letsencrypt.plugins import common
@ -24,6 +23,7 @@ class AugeasConfigurator(common.Plugin):
:type reverter: :class:`letsencrypt.reverter.Reverter` :type reverter: :class:`letsencrypt.reverter.Reverter`
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AugeasConfigurator, self).__init__(*args, **kwargs) super(AugeasConfigurator, self).__init__(*args, **kwargs)
@ -38,16 +38,13 @@ class AugeasConfigurator(common.Plugin):
# because this will change the underlying configuration and potential # because this will change the underlying configuration and potential
# vhosts # vhosts
self.reverter = reverter.Reverter(self.config) self.reverter = reverter.Reverter(self.config)
self.recovery_routine() self.reverter.recovery_routine()
def check_parsing_errors(self, lens): def check_parsing_errors(self, lens):
"""Verify Augeas can parse all of the lens files. """Verify Augeas can parse all of the lens files.
:param str lens: lens to check for errors :param str lens: lens to check for errors
:raises .errors.PluginError: If there has been an error in parsing with
the specified lens.
""" """
error_files = self.aug.match("/augeas//error") error_files = self.aug.match("/augeas//error")
@ -57,13 +54,11 @@ class AugeasConfigurator(common.Plugin):
lens_path = self.aug.get(path + "/lens") lens_path = self.aug.get(path + "/lens")
# As aug.get may return null # As aug.get may return null
if lens_path and lens in lens_path: if lens_path and lens in lens_path:
msg = ( logger.error(
"There has been an error in parsing the file (%s): %s", "There has been an error in parsing the file (%s): %s",
# Strip off /augeas/files and /error # Strip off /augeas/files and /error
path[13:len(path) - 6], self.aug.get(path + "/message")) path[13:len(path) - 6], self.aug.get(path + "/message"))
raise errors.PluginError(msg)
# TODO: Cleanup this function
def save(self, title=None, temporary=False): def save(self, title=None, temporary=False):
"""Saves all changes to the configuration files. """Saves all changes to the configuration files.
@ -78,9 +73,6 @@ class AugeasConfigurator(common.Plugin):
:param bool temporary: Indicates whether the changes made will :param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (ie. challenges) be quickly reversed in the future (ie. challenges)
:raises .errors.PluginError: If there was an error in Augeas, in an
attempt to save the configuration, or an error creating a checkpoint
""" """
save_state = self.aug.get("/augeas/save") save_state = self.aug.get("/augeas/save")
self.aug.set("/augeas/save", "noop") self.aug.set("/augeas/save", "noop")
@ -93,8 +85,7 @@ class AugeasConfigurator(common.Plugin):
self._log_save_errors(ex_errs) self._log_save_errors(ex_errs)
# Erase Save Notes # Erase Save Notes
self.save_notes = "" self.save_notes = ""
raise errors.PluginError( return False
"Error saving files, check logs for more info.")
# Retrieve list of modified files # Retrieve list of modified files
# Note: Noop saves can cause the file to be listed twice, I used a # Note: Noop saves can cause the file to be listed twice, I used a
@ -108,26 +99,22 @@ class AugeasConfigurator(common.Plugin):
for path in save_paths: for path in save_paths:
save_files.add(self.aug.get(path)[6:]) save_files.add(self.aug.get(path)[6:])
try: # Create Checkpoint
# Create Checkpoint if temporary:
if temporary: self.reverter.add_to_temp_checkpoint(
self.reverter.add_to_temp_checkpoint( save_files, self.save_notes)
save_files, self.save_notes) else:
else: self.reverter.add_to_checkpoint(save_files, self.save_notes)
self.reverter.add_to_checkpoint(save_files, self.save_notes)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
if title and not temporary: if title and not temporary:
try: self.reverter.finalize_checkpoint(title)
self.reverter.finalize_checkpoint(title)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.aug.set("/augeas/save", save_state) self.aug.set("/augeas/save", save_state)
self.save_notes = "" self.save_notes = ""
self.aug.save() self.aug.save()
return True
def _log_save_errors(self, ex_errs): def _log_save_errors(self, ex_errs):
"""Log errors due to bad Augeas save. """Log errors due to bad Augeas save.
@ -148,26 +135,14 @@ class AugeasConfigurator(common.Plugin):
Reverts all modified files that have not been saved as a checkpoint Reverts all modified files that have not been saved as a checkpoint
:raises .errors.PluginError: If unable to recover the configuration
""" """
try: self.reverter.recovery_routine()
self.reverter.recovery_routine()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
# Need to reload configuration after these changes take effect # Need to reload configuration after these changes take effect
self.aug.load() self.aug.load()
def revert_challenge_config(self): def revert_challenge_config(self):
"""Used to cleanup challenge configurations. """Used to cleanup challenge configurations."""
self.reverter.revert_temporary_config()
:raises .errors.PluginError: If unable to revert the challenge config.
"""
try:
self.reverter.revert_temporary_config()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.aug.load() self.aug.load()
def rollback_checkpoints(self, rollback=1): def rollback_checkpoints(self, rollback=1):
@ -175,24 +150,10 @@ class AugeasConfigurator(common.Plugin):
:param int rollback: Number of checkpoints to revert :param int rollback: Number of checkpoints to revert
:raises .errors.PluginError: If there is a problem with the input or
the function is unable to correctly revert the configuration
""" """
try: self.reverter.rollback_checkpoints(rollback)
self.reverter.rollback_checkpoints(rollback)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.aug.load() self.aug.load()
def view_config_changes(self): def view_config_changes(self):
"""Show all of the configuration changes that have taken place. """Show all of the configuration changes that have taken place."""
self.reverter.view_config_changes()
:raises .errors.PluginError: If there is a problem while processing
the checkpoints directories.
"""
try:
self.reverter.view_config_changes()
except errors.ReverterError as err:
raise errors.PluginError(str(err))

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,6 @@ CLI_DEFAULTS = dict(
server_root="/etc/apache2", server_root="/etc/apache2",
ctl="apache2ctl", ctl="apache2ctl",
enmod="a2enmod", enmod="a2enmod",
dismod="a2dismod",
init_script="/etc/init.d/apache2", init_script="/etc/init.d/apache2",
le_vhost_ext="-le-ssl.conf", le_vhost_ext="-le-ssl.conf",
) )
@ -21,5 +20,5 @@ MOD_SSL_CONF_SRC = pkg_resources.resource_filename(
distribution.""" distribution."""
REWRITE_HTTPS_ARGS = [ REWRITE_HTTPS_ARGS = [
"^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"] "^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
"""Apache rewrite rule arguments used for redirections to https vhost""" """Apache rewrite rule arguments used for redirections to https vhost"""

View file

@ -60,9 +60,9 @@ def _vhost_menu(domain, vhosts):
choices = [] choices = []
for vhost in vhosts: for vhost in vhosts:
if len(vhost.get_names()) == 1: if len(vhost.names) == 1:
disp_name = next(iter(vhost.get_names())) disp_name = next(iter(vhost.names))
elif len(vhost.get_names()) == 0: elif len(vhost.names) == 0:
disp_name = "" disp_name = ""
else: else:
disp_name = "Multiple Names" disp_name = "Multiple Names"

View file

@ -3,7 +3,6 @@ import os
from letsencrypt.plugins import common from letsencrypt.plugins import common
from letsencrypt_apache import obj
from letsencrypt_apache import parser from letsencrypt_apache import parser
@ -45,24 +44,28 @@ class ApacheDvsni(common.Dvsni):
""" """
def __init__(self, *args, **kwargs):
super(ApacheDvsni, self).__init__(*args, **kwargs)
self.challenge_conf = os.path.join(
self.configurator.conf("server-root"),
"le_dvsni_cert_challenge.conf")
def perform(self): def perform(self):
"""Perform a DVSNI challenge.""" """Peform a DVSNI challenge."""
if not self.achalls: if not self.achalls:
return [] return []
# Save any changes to the configuration as a precaution # Save any changes to the configuration as a precaution
# About to make temporary changes to the config # About to make temporary changes to the config
self.configurator.save() self.configurator.save()
# Prepare the server for HTTPS addresses = []
self.configurator.prepare_server_https( default_addr = "*:443"
str(self.configurator.config.dvsni_port), True) for achall in self.achalls:
vhost = self.configurator.choose_vhost(achall.domain)
# TODO - @jdkasten review this code to make sure it makes sense
self.configurator.make_server_sni_ready(vhost, default_addr)
for addr in vhost.addrs:
if "_default_" == addr.get_addr():
addresses.append([default_addr])
break
else:
addresses.append(list(vhost.addrs))
responses = [] responses = []
@ -71,32 +74,25 @@ class ApacheDvsni(common.Dvsni):
responses.append(self._setup_challenge_cert(achall)) responses.append(self._setup_challenge_cert(achall))
# Setup the configuration # Setup the configuration
dvsni_addrs = self._mod_config() self._mod_config(addresses)
self.configurator.make_addrs_sni_ready(dvsni_addrs)
# Save reversible changes # Save reversible changes
self.configurator.save("SNI Challenge", True) self.configurator.save("SNI Challenge", True)
return responses return responses
def _mod_config(self): def _mod_config(self, ll_addrs):
"""Modifies Apache config files to include challenge vhosts. """Modifies Apache config files to include challenge vhosts.
Result: Apache config includes virtual servers for issued challs Result: Apache config includes virtual servers for issued challs
:returns: All DVSNI addresses used :param list ll_addrs: list of list of `~.common.Addr` to apply
:rtype: set
""" """
dvsni_addrs = set() # TODO: Use ip address of existing vhost instead of relying on FQDN
config_text = "<IfModule mod_ssl.c>\n" config_text = "<IfModule mod_ssl.c>\n"
for idx, lis in enumerate(ll_addrs):
for achall in self.achalls: config_text += self._get_config_text(self.achalls[idx], lis)
achall_addrs = self.get_dvsni_addrs(achall)
dvsni_addrs.update(achall_addrs)
config_text += self._get_config_text(achall, achall_addrs)
config_text += "</IfModule>\n" config_text += "</IfModule>\n"
self._conf_include_check(self.configurator.parser.loc["default"]) self._conf_include_check(self.configurator.parser.loc["default"])
@ -106,25 +102,6 @@ class ApacheDvsni(common.Dvsni):
with open(self.challenge_conf, "w") as new_conf: with open(self.challenge_conf, "w") as new_conf:
new_conf.write(config_text) 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)
# TODO: Checkout _default_ rules.
dvsni_addrs = set()
default_addr = obj.Addr(("*", str(self.configurator.config.dvsni_port)))
for addr in vhost.addrs:
if "_default_" == addr.get_addr():
dvsni_addrs.add(default_addr)
else:
dvsni_addrs.add(
addr.get_sni_addr(self.configurator.config.dvsni_port))
return dvsni_addrs
def _conf_include_check(self, main_config): def _conf_include_check(self, main_config):
"""Adds DVSNI challenge conf file into configuration. """Adds DVSNI challenge conf file into configuration.
@ -148,7 +125,7 @@ class ApacheDvsni(common.Dvsni):
:type achall: :class:`letsencrypt.achallenges.DVSNI` :type achall: :class:`letsencrypt.achallenges.DVSNI`
:param list ip_addrs: addresses of challenged domain :param list ip_addrs: addresses of challenged domain
:class:`list` of type `~.obj.Addr` :class:`list` of type `~.common.Addr`
:returns: virtual host configuration text :returns: virtual host configuration text
:rtype: str :rtype: str
@ -163,9 +140,8 @@ class ApacheDvsni(common.Dvsni):
# parses it as "\n"... c.f.: # parses it as "\n"... c.f.:
# https://docs.python.org/2.7/reference/lexical_analysis.html # https://docs.python.org/2.7/reference/lexical_analysis.html
return self.VHOST_TEMPLATE.format( return self.VHOST_TEMPLATE.format(
vhost=ips, vhost=ips, server_name=achall.nonce_domain,
server_name=achall.gen_response(achall.account_key).z_domain, ssl_options_conf_path=self.configurator.parser.loc["ssl_options"],
ssl_options_conf_path=self.configurator.mod_ssl_conf,
cert_path=self.get_cert_path(achall), cert_path=self.get_cert_path(achall),
key_path=self.get_key_path(achall), key_path=self.get_key_path(achall),
document_root=document_root).replace("\n", os.linesep) document_root=document_root).replace("\n", os.linesep)

View file

@ -1,92 +1,4 @@
"""Module contains classes used by the Apache Configurator.""" """Module contains classes used by the Apache Configurator."""
import re
from letsencrypt.plugins import common
class Addr(common.Addr):
"""Represents an Apache address."""
def __eq__(self, other):
"""This is defined as equalivalent within Apache.
ip_addr:* == ip_addr
"""
if isinstance(other, self.__class__):
return ((self.tup == other.tup) or
(self.tup[0] == other.tup[0] and
self.is_wildcard() and other.is_wildcard()))
return False
def __ne__(self, other):
return not self.__eq__(other)
def _addr_less_specific(self, addr):
"""Returns if addr.get_addr() is more specific than self.get_addr()."""
# pylint: disable=protected-access
return addr._rank_specific_addr() > self._rank_specific_addr()
def _rank_specific_addr(self):
"""Returns numerical rank for get_addr()
:returns: 2 - FQ, 1 - wildcard, 0 - _default_
:rtype: int
"""
if self.get_addr() == "_default_":
return 0
elif self.get_addr() == "*":
return 1
else:
return 2
def conflicts(self, addr):
r"""Returns if address could conflict with correct function of self.
Could addr take away service provided by self within Apache?
.. note::IP Address is more important than wildcard.
Connection from 127.0.0.1:80 with choices of *:80 and 127.0.0.1:*
chooses 127.0.0.1:\*
.. todo:: Handle domain name addrs...
Examples:
========================================= =====
``127.0.0.1:\*.conflicts(127.0.0.1:443)`` True
``127.0.0.1:443.conflicts(127.0.0.1:\*)`` False
``\*:443.conflicts(\*:80)`` False
``_default_:443.conflicts(\*:443)`` True
========================================= =====
"""
if self._addr_less_specific(addr):
return True
elif self.get_addr() == addr.get_addr():
if self.is_wildcard() or self.get_port() == addr.get_port():
return True
return False
def is_wildcard(self):
"""Returns if address has a wildcard port."""
return self.tup[1] == "*" or not self.tup[1]
def get_sni_addr(self, port):
"""Returns the least specific address that resolves on the port.
Examples:
- ``1.2.3.4:443`` -> ``1.2.3.4:<port>``
- ``1.2.3.4:*`` -> ``1.2.3.4:*``
:param str port: Desired port
"""
if self.is_wildcard():
return self
return self.get_addr_obj(port)
class VirtualHost(object): # pylint: disable=too-few-public-methods class VirtualHost(object): # pylint: disable=too-few-public-methods
@ -96,57 +8,39 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
:ivar str path: Augeas path to virtual host :ivar str path: Augeas path to virtual host
:ivar set addrs: Virtual Host addresses (:class:`set` of :ivar set addrs: Virtual Host addresses (:class:`set` of
:class:`common.Addr`) :class:`common.Addr`)
:ivar str name: ServerName of VHost :ivar set names: Server names/aliases of vhost
:ivar list aliases: Server aliases of vhost
(:class:`list` of :class:`str`) (:class:`list` of :class:`str`)
:ivar bool ssl: SSLEngine on in vhost :ivar bool ssl: SSLEngine on in vhost
:ivar bool enabled: Virtual host is enabled :ivar bool enabled: Virtual host is enabled
https://httpd.apache.org/docs/2.4/vhosts/details.html
.. todo:: Any vhost that includes the magic _default_ wildcard is given the
same ServerName as the main server.
""" """
# ?: is used for not returning enclosed characters def __init__(self, filep, path, addrs, ssl, enabled, names=None):
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
def __init__(self, filep, path, addrs, ssl, enabled, name=None, aliases=None):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
"""Initialize a VH.""" """Initialize a VH."""
self.filep = filep self.filep = filep
self.path = path self.path = path
self.addrs = addrs self.addrs = addrs
self.name = name self.names = set() if names is None else set(names)
self.aliases = aliases if aliases is not None else set()
self.ssl = ssl self.ssl = ssl
self.enabled = enabled self.enabled = enabled
def get_names(self): def add_name(self, name):
"""Return a set of all names.""" """Add name to vhost."""
all_names = set() self.names.add(name)
all_names.update(self.aliases)
# Strip out any scheme:// and <port> field from servername
if self.name is not None:
all_names.add(VirtualHost.strip_name.findall(self.name)[0])
return all_names
def __str__(self): def __str__(self):
return ( return (
"File: {filename}\n" "File: {filename}\n"
"Vhost path: {vhpath}\n" "Vhost path: {vhpath}\n"
"Addresses: {addrs}\n" "Addresses: {addrs}\n"
"Name: {name}\n" "Names: {names}\n"
"Aliases: {aliases}\n"
"TLS Enabled: {tls}\n" "TLS Enabled: {tls}\n"
"Site Enabled: {active}".format( "Site Enabled: {active}".format(
filename=self.filep, filename=self.filep,
vhpath=self.path, vhpath=self.path,
addrs=", ".join(str(addr) for addr in self.addrs), addrs=", ".join(str(addr) for addr in self.addrs),
name=self.name if self.name is not None else "", names=", ".join(name for name in self.names),
aliases=", ".join(name for name in self.aliases),
tls="Yes" if self.ssl else "No", tls="Yes" if self.ssl else "No",
active="Yes" if self.enabled else "No")) active="Yes" if self.enabled else "No"))
@ -154,73 +48,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
return (self.filep == other.filep and self.path == other.path and return (self.filep == other.filep and self.path == other.path and
self.addrs == other.addrs and self.addrs == other.addrs and
self.get_names() == other.get_names() and self.names == other.names and
self.ssl == other.ssl and self.enabled == other.enabled) self.ssl == other.ssl and self.enabled == other.enabled)
return False return False
def __ne__(self, other):
return not self.__eq__(other)
def conflicts(self, addrs):
"""See if vhost conflicts with any of the addrs.
This determines whether or not these addresses would/could overwrite
the vhost addresses.
:param addrs: Iterable Addresses
:type addrs: Iterable :class:~obj.Addr
:returns: If addresses conflicts with vhost
:rtype: bool
"""
for pot_addr in addrs:
for addr in self.addrs:
if addr.conflicts(pot_addr):
return True
return False
def same_server(self, vhost):
"""Determines if the vhost is the same 'server'.
Used in redirection - indicates whether or not the two virtual hosts
serve on the exact same IP combinations, but different ports.
.. todo:: Handle _default_
"""
if vhost.get_names() != self.get_names():
return False
# If equal and set is not empty... assume same server
if self.name is not None or self.aliases:
return True
# Both sets of names are empty.
# Make conservative educated guess... this is very restrictive
# Consider adding more safety checks.
if len(vhost.addrs) != len(self.addrs):
return False
# already_found acts to keep everything very conservative.
# Don't allow multiple ip:ports in same set.
already_found = set()
for addr in vhost.addrs:
for local_addr in self.addrs:
if (local_addr.get_addr() == addr.get_addr() and
local_addr != addr and
local_addr.get_addr() not in already_found):
# This intends to make sure we aren't double counting...
# e.g. 127.0.0.1:* - We require same number of addrs
# currently
already_found.add(local_addr.get_addr())
break
else:
return False
return True

View file

@ -1,177 +1,33 @@
"""ApacheParser is a member object of the ApacheConfigurator class.""" """ApacheParser is a member object of the ApacheConfigurator class."""
import fnmatch
import itertools
import logging
import os import os
import re import re
import subprocess
from letsencrypt import errors from letsencrypt import errors
logger = logging.getLogger(__name__)
class ApacheParser(object): class ApacheParser(object):
"""Class handles the fine details of parsing the Apache Configuration. """Class handles the fine details of parsing the Apache Configuration.
.. todo:: Make parsing general... remove sites-available etc... :ivar str root: Normalized abosulte path to the server root
:ivar str root: Normalized absolute path to the server root
directory. Without trailing slash. directory. Without trailing slash.
:ivar str root: Server root
:ivar set modules: All module names that are currently enabled.
:ivar dict loc: Location to place directives, root - configuration origin,
default - user config file, name - NameVirtualHost,
""" """
arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
fnmatch_chars = set(["*", "?", "\\", "[", "]"])
def __init__(self, aug, root, ctl): def __init__(self, aug, root, ssl_options):
# Note: Order is important here.
# This uses the binary, so it can be done first.
# https://httpd.apache.org/docs/2.4/mod/core.html#define
# https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine
# This only handles invocation parameters and Define directives!
self.variables = {}
self.update_runtime_variables(ctl)
self.aug = aug
# Find configuration root and make sure augeas can parse it. # Find configuration root and make sure augeas can parse it.
self.aug = aug
self.root = os.path.abspath(root) self.root = os.path.abspath(root)
self.loc = {"root": self._find_config_root()} self.loc = self._set_locations(ssl_options)
self._parse_file(self.loc["root"]) self._parse_file(self.loc["root"])
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
# Temporarily set modules to be empty, so that find_dirs can work
# https://httpd.apache.org/docs/2.4/mod/core.html#ifmodule
# This needs to come before locations are set.
self.modules = set()
self.init_modules()
# Set up rest of locations
self.loc.update(self._set_locations())
# Must also attempt to parse sites-available or equivalent # Must also attempt to parse sites-available or equivalent
# Sites-available is not included naturally in configuration # Sites-available is not included naturally in configuration
self._parse_file(os.path.join(self.root, "sites-available") + "/*") self._parse_file(os.path.join(self.root, "sites-available") + "/*")
def init_modules(self): # This problem has been fixed in Augeas 1.0
"""Iterates on the configuration until no new modules are loaded. self.standardize_excl()
..todo:: This should be attempted to be done with a binary to avoid def add_dir_to_ifmodssl(self, aug_conf_path, directive, val):
the iteration issue. Else... parse and enable mods at same time.
"""
# Since modules are being initiated... clear existing set.
self.modules = set()
matches = self.find_dir("LoadModule")
iterator = iter(matches)
# Make sure prev_size != cur_size for do: while: iteration
prev_size = -1
while len(self.modules) != prev_size:
prev_size = len(self.modules)
for match_name, match_filename in itertools.izip(
iterator, iterator):
self.modules.add(self.get_arg(match_name))
self.modules.add(
os.path.basename(self.get_arg(match_filename))[:-2] + "c")
def update_runtime_variables(self, ctl):
""""
.. note:: Compile time variables (apache2ctl -V) are not used within the
dynamic configuration files. These should not be parsed or
interpreted.
.. todo:: Create separate compile time variables... simply for arg_get()
"""
stdout = self._get_runtime_cfg(ctl)
variables = dict()
matches = re.compile(r"Define: ([^ \n]*)").findall(stdout)
try:
matches.remove("DUMP_RUN_CFG")
except ValueError:
raise errors.PluginError("Unable to parse runtime variables")
for match in matches:
if match.count("=") > 1:
logger.error("Unexpected number of equal signs in "
"apache2ctl -D DUMP_RUN_CFG")
raise errors.PluginError(
"Error parsing Apache runtime variables")
parts = match.partition("=")
variables[parts[0]] = parts[2]
self.variables = variables
def _get_runtime_cfg(self, ctl): # pylint: disable=no-self-use
"""Get runtime configuration info.
:returns: stdout from DUMP_RUN_CFG
"""
try:
proc = subprocess.Popen(
[ctl, "-D", "DUMP_RUN_CFG"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logger.error(
"Error accessing %s for runtime parameters!%s", ctl, os.linesep)
raise errors.MisconfigurationError(
"Error accessing loaded Apache parameters: %s", ctl)
# Small errors that do not impede
if proc.returncode != 0:
logger.warn("Error in checking parameter list: %s", stderr)
raise errors.MisconfigurationError(
"Apache is unable to check whether or not the module is "
"loaded because Apache is misconfigured.")
return stdout
def filter_args_num(self, matches, args): # pylint: disable=no-self-use
"""Filter out directives with specific number of arguments.
This function makes the assumption that all related arguments are given
in order. Thus /files/apache/directive[5]/arg[2] must come immediately
after /files/apache/directive[5]/arg[1]. Runs in 1 linear pass.
:param string matches: Matches of all directives with arg nodes
:param int args: Number of args you would like to filter
:returns: List of directives that contain # of arguments.
(arg is stripped off)
"""
filtered = []
if args == 1:
for i in range(len(matches)):
if matches[i].endswith("/arg"):
filtered.append(matches[i][:-4])
else:
for i in range(len(matches)):
if matches[i].endswith("/arg[%d]" % args):
# Make sure we don't cause an IndexError (end of list)
# Check to make sure arg + 1 doesn't exist
if (i == (len(matches) - 1) or
not matches[i + 1].endswith("/arg[%d]" % (args + 1))):
filtered.append(matches[i][:-len("/arg[%d]" % args)])
return filtered
def add_dir_to_ifmodssl(self, aug_conf_path, directive, args):
"""Adds directive and value to IfMod ssl block. """Adds directive and value to IfMod ssl block.
Adds given directive and value along configuration path within Adds given directive and value along configuration path within
@ -179,9 +35,8 @@ class ApacheParser(object):
the file, it is created. the file, it is created.
:param str aug_conf_path: Desired Augeas config path to add directive :param str aug_conf_path: Desired Augeas config path to add directive
:param str directive: Directive you would like to add, e.g. Listen :param str directive: Directive you would like to add
:param args: Values of the directive; str "443" or list of str :param str val: Value of directive ie. Listen 443, 443 is the value
:type args: list
""" """
# TODO: Add error checking code... does the path given even exist? # TODO: Add error checking code... does the path given even exist?
@ -191,11 +46,7 @@ class ApacheParser(object):
self.aug.insert(if_mod_path + "arg", "directive", False) self.aug.insert(if_mod_path + "arg", "directive", False)
nvh_path = if_mod_path + "directive[1]" nvh_path = if_mod_path + "directive[1]"
self.aug.set(nvh_path, directive) self.aug.set(nvh_path, directive)
if len(args) == 1: self.aug.set(nvh_path + "/arg", val)
self.aug.set(nvh_path + "/arg", args[0])
else:
for i, arg in enumerate(args):
self.aug.set("%s/arg[%d]" % (nvh_path, i + 1), arg)
def _get_ifmod(self, aug_conf_path, mod): def _get_ifmod(self, aug_conf_path, mod):
"""Returns the path to <IfMod mod> and creates one if it doesn't exist. """Returns the path to <IfMod mod> and creates one if it doesn't exist.
@ -214,7 +65,7 @@ class ApacheParser(object):
# Strip off "arg" at end of first ifmod path # Strip off "arg" at end of first ifmod path
return if_mods[0][:len(if_mods[0]) - 3] return if_mods[0][:len(if_mods[0]) - 3]
def add_dir(self, aug_conf_path, directive, args): def add_dir(self, aug_conf_path, directive, arg):
"""Appends directive to the end fo the file given by aug_conf_path. """Appends directive to the end fo the file given by aug_conf_path.
.. note:: Not added to AugeasConfigurator because it may depend .. note:: Not added to AugeasConfigurator because it may depend
@ -222,29 +73,25 @@ class ApacheParser(object):
:param str aug_conf_path: Augeas configuration path to add directive :param str aug_conf_path: Augeas configuration path to add directive
:param str directive: Directive to add :param str directive: Directive to add
:param args: Value of the directive. ie. Listen 443, 443 is arg :param str arg: Value of the directive. ie. Listen 443, 443 is arg
:type args: list or str
""" """
self.aug.set(aug_conf_path + "/directive[last() + 1]", directive) self.aug.set(aug_conf_path + "/directive[last() + 1]", directive)
if isinstance(args, list): if isinstance(arg, list):
for i, value in enumerate(args, 1): for i, value in enumerate(arg, 1):
self.aug.set( self.aug.set(
"%s/directive[last()]/arg[%d]" % (aug_conf_path, i), value) "%s/directive[last()]/arg[%d]" % (aug_conf_path, i), value)
else: else:
self.aug.set(aug_conf_path + "/directive[last()]/arg", args) self.aug.set(aug_conf_path + "/directive[last()]/arg", arg)
def find_dir(self, directive, arg=None, start=None, exclude=True): def find_dir(self, directive, arg=None, start=None):
"""Finds directive in the configuration. """Finds directive in the configuration.
Recursively searches through config files to find directives Recursively searches through config files to find directives
Directives should be in the form of a case insensitive regex currently Directives should be in the form of a case insensitive regex currently
.. todo:: Add order to directives returned. Last directive comes last..
.. todo:: arg should probably be a list .. todo:: arg should probably be a list
.. todo:: arg search currently only supports direct matching. It does
not handle the case of variables or quoted arguments. This should
be adapted to use a generic search for the directive and then do a
case-insensitive self.get_arg filter
Note: Augeas is inherently case sensitive while Apache is case Note: Augeas is inherently case sensitive while Apache is case
insensitive. Augeas 1.0 allows case insensitive regexes like insensitive. Augeas 1.0 allows case insensitive regexes like
@ -254,19 +101,20 @@ class ApacheParser(object):
compatibility. compatibility.
:param str directive: Directive to look for :param str directive: Directive to look for
:param arg: Specific value directive must have, None if all should :param arg: Specific value directive must have, None if all should
be considered be considered
:type arg: str or None :type arg: str or None
:param str start: Beginning Augeas path to begin looking :param str start: Beginning Augeas path to begin looking
:param bool exclude: Whether or not to exclude directives based on
variables and enabled modules
""" """
# Cannot place member variable in the definition of the function so... # Cannot place member variable in the definition of the function so...
if not start: if not start:
start = get_aug_path(self.loc["root"]) start = get_aug_path(self.loc["root"])
# Debug code
# print "find_dir:", directive, "arg:", arg, " | Looking in:", start
# No regexp code # No regexp code
# if arg is None: # if arg is None:
# matches = self.aug.match(start + # matches = self.aug.match(start +
@ -279,109 +127,32 @@ class ApacheParser(object):
# includes = self.aug.match(start + # includes = self.aug.match(start +
# "//* [self::directive='Include']/* [label()='arg']") # "//* [self::directive='Include']/* [label()='arg']")
regex = "(%s)|(%s)|(%s)" % (case_i(directive),
case_i("Include"),
case_i("IncludeOptional"))
matches = self.aug.match(
"%s//*[self::directive=~regexp('%s')]" % (start, regex))
if exclude:
matches = self._exclude_dirs(matches)
if arg is None: if arg is None:
arg_suffix = "/arg" matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/arg"
% (start, directive)))
else: else:
arg_suffix = "/*[self::arg=~regexp('%s')]" % case_i(arg) matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/*"
"[self::arg=~regexp('%s')]" %
(start, directive, arg)))
ordered_matches = [] incl_regex = "(%s)|(%s)" % (case_i('Include'),
case_i('IncludeOptional'))
# TODO: Wildcards should be included in alphabetical order includes = self.aug.match(("%s//* [self::directive=~regexp('%s')]/* "
# https://httpd.apache.org/docs/2.4/mod/core.html#include "[label()='arg']" % (start, incl_regex)))
for match in matches:
dir_ = self.aug.get(match).lower()
if dir_ == "include" or dir_ == "includeoptional":
# start[6:] to strip off /files
#print self._get_include_path(self.get_arg(match +"/arg")), directive, arg
ordered_matches.extend(self.find_dir(
directive, arg,
self._get_include_path(self.get_arg(match + "/arg")),
exclude))
# This additionally allows Include
if dir_ == directive.lower():
ordered_matches.extend(self.aug.match(match + arg_suffix))
return ordered_matches # for inc in includes:
# print inc, self.aug.get(inc)
def get_arg(self, match): for include in includes:
"""Uses augeas.get to get argument value and interprets result. # start[6:] to strip off /files
matches.extend(self.find_dir(
directive, arg, self._get_include_path(
strip_dir(start[6:]), self.aug.get(include))))
This also converts all variables and parameters appropriately. return matches
""" def _get_include_path(self, cur_dir, arg):
value = self.aug.get(match)
# No need to strip quotes for variables, as apache2ctl already does this
# but we do need to strip quotes for all normal arguments.
# Note: normal argument may be a quoted variable
# e.g. strip now, not later
value = value.strip("'\"")
variables = ApacheParser.arg_var_interpreter.findall(value)
for var in variables:
# Strip off ${ and }
try:
value = value.replace(var, self.variables[var[2:-1]])
except KeyError:
raise errors.PluginError("Error Parsing variable: %s" % var)
return value
def _exclude_dirs(self, matches):
"""Exclude directives that are not loaded into the configuration."""
filters = [("ifmodule", self.modules), ("ifdefine", self.variables)]
valid_matches = []
for match in matches:
for filter_ in filters:
if not self._pass_filter(match, filter_):
break
else:
valid_matches.append(match)
return valid_matches
def _pass_filter(self, match, filter_):
"""Determine if directive passes a filter.
:param str match: Augeas path
:param list filter: list of tuples of form
[("lowercase if directive", set of relevant parameters)]
"""
match_l = match.lower()
last_match_idx = match_l.find(filter_[0])
while last_match_idx != -1:
# Check args
end_of_if = match_l.find("/", last_match_idx)
# This should be aug.get (vars are not used e.g. parser.aug_get)
expression = self.aug.get(match[:end_of_if] + "/arg")
if expression.startswith("!"):
# Strip off "!"
if expression[1:] in filter_[1]:
return False
else:
if expression not in filter_[1]:
return False
last_match_idx = match_l.find(filter_[0], end_of_if)
return True
def _get_include_path(self, arg):
"""Converts an Apache Include directive into Augeas path. """Converts an Apache Include directive into Augeas path.
Converts an Apache Include directive argument into an Augeas Converts an Apache Include directive argument into an Augeas
@ -389,12 +160,29 @@ class ApacheParser(object):
.. todo:: convert to use os.path.join() .. todo:: convert to use os.path.join()
:param str cur_dir: current working directory
:param str arg: Argument of Include directive :param str arg: Argument of Include directive
:returns: Augeas path string :returns: Augeas path string
:rtype: str :rtype: str
""" """
# Sanity check argument - maybe
# Question: what can the attacker do with control over this string
# Effect parse file... maybe exploit unknown errors in Augeas
# If the attacker can Include anything though... and this function
# only operates on Apache real config data... then the attacker has
# already won.
# Perhaps it is better to simply check the permissions on all
# included files?
# check_config to validate apache config doesn't work because it
# would create a race condition between the check and this input
# TODO: Maybe... although I am convinced we have lost if
# Apache files can't be trusted. The augeas include path
# should be made to be exact.
# Check to make sure only expected characters are used <- maybe remove # Check to make sure only expected characters are used <- maybe remove
# validChars = re.compile("[a-zA-Z0-9.*?_-/]*") # validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# matchObj = validChars.match(arg) # matchObj = validChars.match(arg)
@ -402,55 +190,60 @@ class ApacheParser(object):
# logger.error("Error: Invalid regexp characters in %s", arg) # logger.error("Error: Invalid regexp characters in %s", arg)
# return [] # return []
# Remove beginning and ending quotes
arg = arg.strip("'\"")
# Standardize the include argument based on server root # Standardize the include argument based on server root
if not arg.startswith("/"): if not arg.startswith("/"):
# Normpath will condense ../ arg = cur_dir + arg
arg = os.path.normpath(os.path.join(self.root, arg)) # conf/ is a special variable for ServerRoot in Apache
else: elif arg.startswith("conf/"):
arg = os.path.normpath(arg) arg = self.root + arg[4:]
# TODO: Test if Apache allows ../ or ~/ for Includes
# Attempts to add a transform to the file if one does not already exist # Attempts to add a transform to the file if one does not already exist
if os.path.isdir(arg): self._parse_file(arg)
self._parse_file(os.path.join(arg, "*"))
else:
self._parse_file(arg)
# Argument represents an fnmatch regular expression, convert it # Argument represents an fnmatch regular expression, convert it
# Split up the path and convert each into an Augeas accepted regex # Split up the path and convert each into an Augeas accepted regex
# then reassemble # then reassemble
split_arg = arg.split("/") if "*" in arg or "?" in arg:
for idx, split in enumerate(split_arg): split_arg = arg.split("/")
if any(char in ApacheParser.fnmatch_chars for char in split): for idx, split in enumerate(split_arg):
# Turn it into a augeas regex # * and ? are the two special fnmatch characters
# TODO: Can this instead be an augeas glob instead of regex if "*" in split or "?" in split:
split_arg[idx] = ("* [label()=~regexp('%s')]" % # Turn it into a augeas regex
self.fnmatch_to_re(split)) # TODO: Can this instead be an augeas glob instead of regex
# Reassemble the argument split_arg[idx] = ("* [label()=~regexp('%s')]" %
# Note: This also normalizes the argument /serverroot/ -> /serverroot self.fnmatch_to_re(split))
arg = "/".join(split_arg) # Reassemble the argument
arg = "/".join(split_arg)
# If the include is a directory, just return the directory as a file
if arg.endswith("/"):
return get_aug_path(arg[:len(arg)-1])
return get_aug_path(arg) return get_aug_path(arg)
def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use
"""Method converts Apache's basic fnmatch to regular expression. """Method converts Apache's basic fnmatch to regular expression.
Assumption - Configs are assumed to be well-formed and only writable by
privileged users.
https://apr.apache.org/docs/apr/2.0/apr__fnmatch_8h_source.html
http://apache2.sourcearchive.com/documentation/2.2.16-6/apr__fnmatch_8h_source.html
:param str clean_fn_match: Apache style filename match, similar to globs :param str clean_fn_match: Apache style filename match, similar to globs
:returns: regex suitable for augeas :returns: regex suitable for augeas
:rtype: str :rtype: str
""" """
# This strips off final /Z(?ms) # Checkout fnmatch.py in venv/local/lib/python2.7/fnmatch.py
return fnmatch.translate(clean_fn_match)[:-7] regex = ""
for letter in clean_fn_match:
if letter == '.':
regex = regex + r"\."
elif letter == '*':
regex = regex + ".*"
# According to apache.org ? shouldn't appear
# but in case it is valid...
elif letter == '?':
regex = regex + "."
else:
regex = regex + letter
return regex
def _parse_file(self, filepath): def _parse_file(self, filepath):
"""Parse file with Augeas """Parse file with Augeas
@ -525,14 +318,15 @@ class ApacheParser(object):
self.aug.load() self.aug.load()
def _set_locations(self): def _set_locations(self, ssl_options):
"""Set default location for directives. """Set default location for directives.
Locations are given as file_paths Locations are given as file_paths
.. todo:: Make sure that files are included .. todo:: Make sure that files are included
""" """
default = self._set_user_config_file() root = self._find_config_root()
default = self._set_user_config_file(root)
temp = os.path.join(self.root, "ports.conf") temp = os.path.join(self.root, "ports.conf")
if os.path.isfile(temp): if os.path.isfile(temp):
@ -542,7 +336,8 @@ class ApacheParser(object):
listen = default listen = default
name = default name = default
return {"default": default, "listen": listen, "name": name} return {"root": root, "default": default, "listen": listen,
"name": name, "ssl_options": ssl_options}
def _find_config_root(self): def _find_config_root(self):
"""Find the Apache Configuration Root file.""" """Find the Apache Configuration Root file."""
@ -554,7 +349,7 @@ class ApacheParser(object):
raise errors.NoInstallationError("Could not find configuration root") raise errors.NoInstallationError("Could not find configuration root")
def _set_user_config_file(self): def _set_user_config_file(self, root):
"""Set the appropriate user configuration file """Set the appropriate user configuration file
.. todo:: This will have to be updated for other distros versions .. todo:: This will have to be updated for other distros versions
@ -565,11 +360,12 @@ class ApacheParser(object):
# Basic check to see if httpd.conf exists and # Basic check to see if httpd.conf exists and
# in hierarchy via direct include # in hierarchy via direct include
# httpd.conf was very common as a user file in Apache 2.2 # httpd.conf was very common as a user file in Apache 2.2
if (os.path.isfile(os.path.join(self.root, "httpd.conf")) and if (os.path.isfile(os.path.join(self.root, 'httpd.conf')) and
self.find_dir("Include", "httpd.conf", self.loc["root"])): self.find_dir(
return os.path.join(self.root, "httpd.conf") case_i("Include"), case_i("httpd.conf"), root)):
return os.path.join(self.root, 'httpd.conf')
else: else:
return os.path.join(self.root, "apache2.conf") return os.path.join(self.root, 'apache2.conf')
def case_i(string): def case_i(string):
@ -584,7 +380,7 @@ def case_i(string):
:param str string: string to make case i regex :param str string: string to make case i regex
""" """
return "".join(["[" + c.upper() + c.lower() + "]" return "".join(["["+c.upper()+c.lower()+"]"
if c.isalpha() else c for c in re.escape(string)]) if c.isalpha() else c for c in re.escape(string)])
@ -595,3 +391,22 @@ def get_aug_path(file_path):
""" """
return "/files%s" % file_path return "/files%s" % file_path
def strip_dir(path):
"""Returns directory of file path.
.. todo:: Replace this with Python standard function
:param str path: path is a file path. not an augeas section or
directive path
:returns: directory
:rtype: str
"""
index = path.rfind("/")
if index > 0:
return path[:index+1]
# No directory
return ""

View file

@ -1,115 +0,0 @@
"""Test for letsencrypt_apache.augeas_configurator."""
import os
import shutil
import unittest
import mock
from letsencrypt import errors
from letsencrypt_apache.tests import util
class AugeasConfiguratorTest(util.ApacheTest):
"""Test for Augeas Configurator base class."""
def setUp(self): # pylint: disable=arguments-differ
super(AugeasConfiguratorTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/two_vhost_80")
def tearDown(self):
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
shutil.rmtree(self.temp_dir)
def test_bad_parse(self):
# pylint: disable=protected-access
self.config.parser._parse_file(os.path.join(
self.config.parser.root, "conf-available", "bad_conf_file.conf"))
self.assertRaises(
errors.PluginError, self.config.check_parsing_errors, "httpd.aug")
def test_bad_save(self):
mock_save = mock.Mock()
mock_save.side_effect = IOError
self.config.aug.save = mock_save
self.assertRaises(errors.PluginError, self.config.save)
def test_bad_save_checkpoint(self):
self.config.reverter.add_to_checkpoint = mock.Mock(
side_effect=errors.ReverterError)
self.config.parser.add_dir(
self.vh_truth[0].path, "Test", "bad_save_ckpt")
self.assertRaises(errors.PluginError, self.config.save)
def test_bad_save_finalize_checkpoint(self):
self.config.reverter.finalize_checkpoint = mock.Mock(
side_effect=errors.ReverterError)
self.config.parser.add_dir(
self.vh_truth[0].path, "Test", "bad_save_ckpt")
self.assertRaises(errors.PluginError, self.config.save, "Title")
def test_finalize_save(self):
mock_finalize = mock.Mock()
self.config.reverter = mock_finalize
self.config.save("Example Title")
self.assertTrue(mock_finalize.is_called)
def test_recovery_routine(self):
mock_load = mock.Mock()
self.config.aug.load = mock_load
self.config.recovery_routine()
self.assertEqual(mock_load.call_count, 1)
def test_recovery_routine_error(self):
self.config.reverter.recovery_routine = mock.Mock(
side_effect=errors.ReverterError)
self.assertRaises(
errors.PluginError, self.config.recovery_routine)
def test_revert_challenge_config(self):
mock_load = mock.Mock()
self.config.aug.load = mock_load
self.config.revert_challenge_config()
self.assertEqual(mock_load.call_count, 1)
def test_revert_challenge_config_error(self):
self.config.reverter.revert_temporary_config = mock.Mock(
side_effect=errors.ReverterError)
self.assertRaises(
errors.PluginError, self.config.revert_challenge_config)
def test_rollback_checkpoints(self):
mock_load = mock.Mock()
self.config.aug.load = mock_load
self.config.rollback_checkpoints()
self.assertEqual(mock_load.call_count, 1)
def test_rollback_error(self):
self.config.reverter.rollback_checkpoints = mock.Mock(
side_effect=errors.ReverterError)
self.assertRaises(errors.PluginError, self.config.rollback_checkpoints)
def test_view_config_changes(self):
self.config.view_config_changes()
def test_view_config_changes_error(self):
self.config.reverter.view_config_changes = mock.Mock(
side_effect=errors.ReverterError)
self.assertRaises(errors.PluginError, self.config.view_config_changes)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View file

@ -1,127 +0,0 @@
"""Tests for letsencrypt_apache.parser."""
import os
import shutil
import unittest
from letsencrypt import errors
from letsencrypt_apache.tests import util
class ComplexParserTest(util.ParserTest):
"""Apache Parser Test."""
def setUp(self): # pylint: disable=arguments-differ
super(ComplexParserTest, self).setUp(
"complex_parsing", "complex_parsing")
self.setup_variables()
# This needs to happen after due to setup_variables not being run
# until after
self.parser.init_modules() # pylint: disable=protected-access
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
def setup_variables(self):
"""Set up variables for parser."""
self.parser.variables.update(
{
"COMPLEX": "",
"tls_port": "1234",
"fnmatch_filename": "test_fnmatch.conf",
"tls_port_str": "1234"
}
)
def test_filter_args_num(self):
"""Note: This may also fail do to Include conf-enabled/ syntax."""
matches = self.parser.find_dir("TestArgsDirective")
self.assertEqual(len(self.parser.filter_args_num(matches, 1)), 3)
self.assertEqual(len(self.parser.filter_args_num(matches, 2)), 2)
self.assertEqual(len(self.parser.filter_args_num(matches, 3)), 1)
def test_basic_variable_parsing(self):
matches = self.parser.find_dir("TestVariablePort")
self.assertEqual(len(matches), 1)
self.assertEqual(self.parser.get_arg(matches[0]), "1234")
def test_basic_variable_parsing_quotes(self):
matches = self.parser.find_dir("TestVariablePortStr")
self.assertEqual(len(matches), 1)
self.assertEqual(self.parser.get_arg(matches[0]), "1234")
def test_invalid_variable_parsing(self):
del self.parser.variables["tls_port"]
matches = self.parser.find_dir("TestVariablePort")
self.assertRaises(
errors.PluginError, self.parser.get_arg, matches[0])
def test_basic_ifdefine(self):
self.assertEqual(len(self.parser.find_dir("VAR_DIRECTIVE")), 2)
self.assertEqual(len(self.parser.find_dir("INVALID_VAR_DIRECTIVE")), 0)
def test_basic_ifmodule(self):
self.assertEqual(len(self.parser.find_dir("MOD_DIRECTIVE")), 2)
self.assertEqual(
len(self.parser.find_dir("INVALID_MOD_DIRECTIVE")), 0)
def test_nested(self):
self.assertEqual(len(self.parser.find_dir("NESTED_DIRECTIVE")), 3)
self.assertEqual(
len(self.parser.find_dir("INVALID_NESTED_DIRECTIVE")), 0)
def test_load_modules(self):
"""If only first is found, there is bad variable parsing."""
self.assertTrue("status_module" in self.parser.modules)
self.assertTrue("mod_status.c" in self.parser.modules)
# This is in an IfDefine
self.assertTrue("ssl_module" in self.parser.modules)
self.assertTrue("mod_ssl.c" in self.parser.modules)
def verify_fnmatch(self, arg, hit=True):
"""Test if Include was correctly parsed."""
from letsencrypt_apache import parser
self.parser.add_dir(parser.get_aug_path(self.parser.loc["default"]),
"Include", [arg])
if hit:
self.assertTrue(self.parser.find_dir("FNMATCH_DIRECTIVE"))
else:
self.assertFalse(self.parser.find_dir("FNMATCH_DIRECTIVE"))
# NOTE: Only run one test per function otherwise you will have inf recursion
def test_include(self):
self.verify_fnmatch("test_fnmatch.?onf")
def test_include_complex(self):
self.verify_fnmatch("../complex_parsing/[te][te]st_*.?onf")
def test_include_fullpath(self):
self.verify_fnmatch(os.path.join(self.config_path, "test_fnmatch.conf"))
def test_include_fullpath_trailing_slash(self):
self.verify_fnmatch(self.config_path + "//")
def test_include_single_quotes(self):
self.verify_fnmatch("'" + self.config_path + "'")
def test_include_double_quotes(self):
self.verify_fnmatch('"' + self.config_path + '"')
def test_include_variable(self):
self.verify_fnmatch("../complex_parsing/${fnmatch_filename}")
def test_include_missing(self):
# This should miss
self.verify_fnmatch("test_*.onf", False)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View file

@ -1,8 +1,7 @@
# pylint: disable=too-many-public-methods
"""Test for letsencrypt_apache.configurator.""" """Test for letsencrypt_apache.configurator."""
import os import os
import re
import shutil import shutil
import socket
import unittest import unittest
import mock import mock
@ -11,23 +10,29 @@ from acme import challenges
from letsencrypt import achallenges from letsencrypt import achallenges
from letsencrypt import errors from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.plugins import common
from letsencrypt.tests import acme_util from letsencrypt.tests import acme_util
from letsencrypt_apache import configurator from letsencrypt_apache import configurator
from letsencrypt_apache import obj from letsencrypt_apache import parser
from letsencrypt_apache.tests import util from letsencrypt_apache.tests import util
class TwoVhost80Test(util.ApacheTest): class TwoVhost80Test(util.ApacheTest):
"""Test two standard well-configured HTTP vhosts.""" """Test two standard well configured HTTP vhosts."""
def setUp(self): # pylint: disable=arguments-differ def setUp(self):
super(TwoVhost80Test, self).setUp() super(TwoVhost80Test, self).setUp()
self.config = util.get_apache_configurator( with mock.patch("letsencrypt_apache.configurator.ApacheConfigurator."
self.config_path, self.config_dir, self.work_dir) "mod_loaded") as mock_load:
mock_load.return_value = True
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir)
self.vh_truth = util.get_vh_truth( self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/two_vhost_80") self.temp_dir, "debian_apache_2_4/two_vhost_80")
@ -37,63 +42,16 @@ class TwoVhost80Test(util.ApacheTest):
shutil.rmtree(self.config_dir) shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir) shutil.rmtree(self.work_dir)
@mock.patch("letsencrypt_apache.configurator.le_util.exe_exists")
def test_prepare_no_install(self, mock_exe_exists):
mock_exe_exists.return_value = False
self.assertRaises(
errors.NoInstallationError, self.config.prepare)
@mock.patch("letsencrypt_apache.parser.ApacheParser")
@mock.patch("letsencrypt_apache.configurator.le_util.exe_exists")
def test_prepare_version(self, mock_exe_exists, _):
mock_exe_exists.return_value = True
self.config.version = None
self.config.config_test = mock.Mock()
self.config.get_version = mock.Mock(return_value=(1, 1))
self.assertRaises(
errors.NotSupportedError, self.config.prepare)
def test_add_parser_arguments(self): # pylint: disable=no-self-use
from letsencrypt_apache.configurator import ApacheConfigurator
# Weak test..
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
def test_get_all_names(self): def test_get_all_names(self):
names = self.config.get_all_names() names = self.config.get_all_names()
self.assertEqual(names, set( self.assertEqual(names, set(
["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"])) ["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"]))
@mock.patch("letsencrypt_apache.configurator.socket.gethostbyaddr")
def test_get_all_names_addrs(self, mock_gethost):
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
vhost = obj.VirtualHost(
"fp", "ap",
set([obj.Addr(("8.8.8.8", "443")),
obj.Addr(("zombo.com",)),
obj.Addr(("192.168.1.2"))]),
True, False)
self.config.vhosts.append(vhost)
names = self.config.get_all_names()
self.assertEqual(len(names), 5)
self.assertTrue("zombo.com" in names)
self.assertTrue("google.com" in names)
self.assertTrue("letsencrypt.demo" in names)
def test_add_servernames_alias(self):
self.config.parser.add_dir(
self.vh_truth[2].path, "ServerAlias", ["*.le.co"])
self.config._add_servernames(self.vh_truth[2]) # pylint: disable=protected-access
self.assertEqual(
self.vh_truth[2].get_names(), set(["*.le.co", "ip-172-30-0-17"]))
def test_get_virtual_hosts(self): def test_get_virtual_hosts(self):
"""Make sure all vhosts are being properly found. """Make sure all vhosts are being properly found.
.. note:: If test fails, only finding 1 Vhost... it is likely that .. note:: If test fails, only finding 1 Vhost... it is likely that
it is a problem with is_enabled. If finding only 3, likely is_ssl it is a problem with is_enabled.
""" """
vhs = self.config.get_virtual_hosts() vhs = self.config.get_virtual_hosts()
@ -105,77 +63,9 @@ class TwoVhost80Test(util.ApacheTest):
if vhost == truth: if vhost == truth:
found += 1 found += 1
break break
else:
raise Exception("Missed: %s" % vhost) # pragma: no cover
self.assertEqual(found, 4) self.assertEqual(found, 4)
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
def test_choose_vhost_none_avail(self, mock_select):
mock_select.return_value = None
self.assertRaises(
errors.PluginError, self.config.choose_vhost, "none.com")
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
def test_choose_vhost_select_vhost_ssl(self, mock_select):
mock_select.return_value = self.vh_truth[1]
self.assertEqual(
self.vh_truth[1], self.config.choose_vhost("none.com"))
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
def test_choose_vhost_select_vhost_non_ssl(self, mock_select):
mock_select.return_value = self.vh_truth[0]
chosen_vhost = self.config.choose_vhost("none.com")
self.assertEqual(
self.vh_truth[0].get_names(), chosen_vhost.get_names())
# Make sure we go from HTTP -> HTTPS
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_conflicting_non_ssl(self, mock_select):
mock_select.return_value = self.vh_truth[3]
conflicting_vhost = obj.VirtualHost(
"path", "aug_path", set([obj.Addr.fromstring("*:443")]), True, True)
self.config.vhosts.append(conflicting_vhost)
self.assertRaises(
errors.PluginError, self.config.choose_vhost, "none.com")
def test_find_best_vhost(self):
# pylint: disable=protected-access
self.assertEqual(
self.vh_truth[3], self.config._find_best_vhost("letsencrypt.demo"))
self.assertEqual(
self.vh_truth[0],
self.config._find_best_vhost("encryption-example.demo"))
self.assertTrue(
self.config._find_best_vhost("does-not-exist.com") is None)
def test_find_best_vhost_variety(self):
# pylint: disable=protected-access
ssl_vh = obj.VirtualHost(
"fp", "ap", set([obj.Addr(("*", "443")), obj.Addr(("zombo.com",))]),
True, False)
self.config.vhosts.append(ssl_vh)
self.assertEqual(self.config._find_best_vhost("zombo.com"), ssl_vh)
def test_find_best_vhost_default(self):
# pylint: disable=protected-access
# Assume only the two default vhosts.
self.config.vhosts = [
vh for vh in self.config.vhosts
if vh.name not in ["letsencrypt.demo", "encryption-example.demo"]
]
self.assertEqual(
self.config._find_best_vhost("example.demo"), self.vh_truth[2])
def test_non_default_vhosts(self):
# pylint: disable=protected-access
self.assertEqual(len(self.config._non_default_vhosts()), 3)
def test_is_site_enabled(self): def test_is_site_enabled(self):
"""Test if site is enabled. """Test if site is enabled.
@ -190,50 +80,7 @@ class TwoVhost80Test(util.ApacheTest):
self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep)) self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep)) self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep))
@mock.patch("letsencrypt.le_util.run_script")
@mock.patch("letsencrypt.le_util.exe_exists")
@mock.patch("letsencrypt_apache.parser.subprocess.Popen")
def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script):
mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "")
mock_popen().returncode = 0
mock_exe_exists.return_value = True
self.config.enable_mod("ssl")
self.assertTrue("ssl_module" in self.config.parser.modules)
self.assertTrue("mod_ssl.c" in self.config.parser.modules)
self.assertTrue(mock_run_script.called)
def test_enable_mod_unsupported_dirs(self):
shutil.rmtree(os.path.join(self.config.parser.root, "mods-enabled"))
self.assertRaises(
errors.NotSupportedError, self.config.enable_mod, "ssl")
@mock.patch("letsencrypt.le_util.exe_exists")
def test_enable_mod_no_disable(self, mock_exe_exists):
mock_exe_exists.return_value = False
self.assertRaises(
errors.MisconfigurationError, self.config.enable_mod, "ssl")
def test_enable_site(self):
# Default 443 vhost
self.assertFalse(self.vh_truth[1].enabled)
self.config.enable_site(self.vh_truth[1])
self.assertTrue(self.vh_truth[1].enabled)
# Go again to make sure nothing fails
self.config.enable_site(self.vh_truth[1])
def test_enable_site_failure(self):
self.assertRaises(
errors.NotSupportedError,
self.config.enable_site,
obj.VirtualHost("asdf", "afsaf", set(), False, False))
def test_deploy_cert(self): def test_deploy_cert(self):
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
# Get the default 443 vhost # Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1] self.config.assoc["random.demo"] = self.vh_truth[1]
self.config.deploy_cert( self.config.deploy_cert(
@ -241,17 +88,15 @@ class TwoVhost80Test(util.ApacheTest):
"example/cert.pem", "example/key.pem", "example/cert_chain.pem") "example/cert.pem", "example/key.pem", "example/cert_chain.pem")
self.config.save() 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( loc_cert = self.config.parser.find_dir(
"sslcertificatefile", "example/cert.pem", self.vh_truth[1].path) parser.case_i("sslcertificatefile"),
re.escape("example/cert.pem"), self.vh_truth[1].path)
loc_key = self.config.parser.find_dir( loc_key = self.config.parser.find_dir(
"sslcertificateKeyfile", "example/key.pem", self.vh_truth[1].path) parser.case_i("sslcertificateKeyfile"),
re.escape("example/key.pem"), self.vh_truth[1].path)
loc_chain = self.config.parser.find_dir( loc_chain = self.config.parser.find_dir(
"SSLCertificateChainFile", "example/cert_chain.pem", parser.case_i("SSLCertificateChainFile"),
self.vh_truth[1].path) re.escape("example/cert_chain.pem"), self.vh_truth[1].path)
# Verify one directive was found in the correct file # Verify one directive was found in the correct file
self.assertEqual(len(loc_cert), 1) self.assertEqual(len(loc_cert), 1)
@ -266,60 +111,16 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(configurator.get_file_path(loc_chain[0]), self.assertEqual(configurator.get_file_path(loc_chain[0]),
self.vh_truth[1].filep) self.vh_truth[1].filep)
# One more time for chain directive setting
self.config.deploy_cert(
"random.demo",
"two/cert.pem", "two/key.pem", "two/cert_chain.pem")
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateChainFile", "two/cert_chain.pem",
self.vh_truth[1].path))
def test_deploy_cert_invalid_vhost(self):
self.config.parser.modules.add("ssl_module")
mock_find = mock.MagicMock()
mock_find.return_value = []
self.config.parser.find_dir = mock_find
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
self.assertRaises(
errors.PluginError, self.config.deploy_cert, "random.demo",
"example/cert.pem", "example/key.pem", "example/cert_chain.pem")
def test_is_name_vhost(self): def test_is_name_vhost(self):
addr = obj.Addr.fromstring("*:80") addr = common.Addr.fromstring("*:80")
self.assertTrue(self.config.is_name_vhost(addr)) self.assertTrue(self.config.is_name_vhost(addr))
self.config.version = (2, 2) self.config.version = (2, 2)
self.assertFalse(self.config.is_name_vhost(addr)) self.assertFalse(self.config.is_name_vhost(addr))
def test_add_name_vhost(self): def test_add_name_vhost(self):
self.config.add_name_vhost(obj.Addr.fromstring("*:443")) self.config.add_name_vhost("*:443")
self.config.add_name_vhost(obj.Addr.fromstring("*:80"))
self.assertTrue(self.config.parser.find_dir( self.assertTrue(self.config.parser.find_dir(
"NameVirtualHost", "*:443", exclude=False)) "NameVirtualHost", re.escape("*:443")))
self.assertTrue(self.config.parser.find_dir(
"NameVirtualHost", "*:80"))
def test_prepare_server_https(self):
mock_enable = mock.Mock()
self.config.enable_mod = mock_enable
mock_find = mock.Mock()
mock_add_dir = mock.Mock()
mock_find.return_value = []
# This will test the Add listen
self.config.parser.find_dir = mock_find
self.config.parser.add_dir_to_ifmodssl = mock_add_dir
self.config.prepare_server_https("443")
self.assertEqual(mock_enable.call_args[1], {"temp": False})
self.config.prepare_server_https("8080", temp=True)
# Enable mod is temporary
self.assertEqual(mock_enable.call_args[1], {"temp": True})
self.assertEqual(mock_add_dir.call_count, 2)
def test_make_vhost_ssl(self): def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
@ -332,58 +133,47 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(ssl_vhost.path, self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost") "/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
self.assertEqual(len(ssl_vhost.addrs), 1) self.assertEqual(len(ssl_vhost.addrs), 1)
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) self.assertEqual(set([common.Addr.fromstring("*:443")]), ssl_vhost.addrs)
self.assertEqual(ssl_vhost.name, "encryption-example.demo") self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"]))
self.assertTrue(ssl_vhost.ssl) self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled) self.assertFalse(ssl_vhost.enabled)
self.assertTrue(self.config.parser.find_dir( self.assertTrue(self.config.parser.find_dir(
"SSLCertificateFile", None, ssl_vhost.path, False)) "SSLCertificateFile", None, ssl_vhost.path))
self.assertTrue(self.config.parser.find_dir( self.assertTrue(self.config.parser.find_dir(
"SSLCertificateKeyFile", None, ssl_vhost.path, False)) "SSLCertificateKeyFile", None, ssl_vhost.path))
self.assertTrue(self.config.parser.find_dir(
"Include", self.ssl_options, ssl_vhost.path))
self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]),
self.config.is_name_vhost(ssl_vhost)) self.config.is_name_vhost(ssl_vhost))
self.assertEqual(len(self.config.vhosts), 5) self.assertEqual(len(self.config.vhosts), 5)
def test_make_vhost_ssl_extra_vhs(self):
self.config.aug.match = mock.Mock(return_value=["p1", "p2"])
self.assertRaises(
errors.PluginError, self.config.make_vhost_ssl, self.vh_truth[0])
def test_make_vhost_ssl_bad_write(self):
mock_open = mock.mock_open()
# This calls open
self.config.reverter.register_file_creation = mock.Mock()
mock_open.side_effect = IOError
with mock.patch("__builtin__.open", mock_open):
self.assertRaises(
errors.PluginError,
self.config.make_vhost_ssl, self.vh_truth[0])
def test_get_ssl_vhost_path(self):
# pylint: disable=protected-access
self.assertTrue(
self.config._get_ssl_vhost_path("example_path").endswith(".conf"))
def test_add_name_vhost_if_necessary(self):
# pylint: disable=protected-access
self.config.save = mock.Mock()
self.config.version = (2, 2)
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.dvsni.ApacheDvsni.perform")
@mock.patch("letsencrypt_apache.configurator.ApacheConfigurator.restart") @mock.patch("letsencrypt_apache.configurator.ApacheConfigurator.restart")
def test_perform(self, mock_restart, mock_dvsni_perform): def test_perform(self, mock_restart, mock_dvsni_perform):
# Only tests functionality specific to configurator.perform # Only tests functionality specific to configurator.perform
# Note: As more challenges are offered this will have to be expanded # Note: As more challenges are offered this will have to be expanded
account_key, achall1, achall2 = self.get_achalls() auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem)
achall1 = achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
r="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q",
nonce="37bc5eb75d3e00a19b4f6355845e5a18"),
"pending"),
domain="encryption-example.demo", key=auth_key)
achall2 = achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
r="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU",
nonce="59ed014cac95f77057b1d7a1b2c596ba"),
"pending"),
domain="letsencrypt.demo", key=auth_key)
dvsni_ret_val = [ dvsni_ret_val = [
achall1.gen_response(account_key), challenges.DVSNIResponse(s="randomS1"),
achall2.gen_response(account_key), challenges.DVSNIResponse(s="randomS2"),
] ]
mock_dvsni_perform.return_value = dvsni_ret_val mock_dvsni_perform.return_value = dvsni_ret_val
@ -394,228 +184,27 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(mock_restart.call_count, 1) self.assertEqual(mock_restart.call_count, 1)
@mock.patch("letsencrypt_apache.configurator.ApacheConfigurator.restart") @mock.patch("letsencrypt_apache.configurator.subprocess.Popen")
def test_cleanup(self, mock_restart): def test_get_version(self, mock_popen):
_, achall1, achall2 = self.get_achalls() mock_popen().communicate.return_value = (
self.config._chall_out.add(achall1) # pylint: disable=protected-access
self.config._chall_out.add(achall2) # pylint: disable=protected-access
self.config.cleanup([achall1])
self.assertFalse(mock_restart.called)
self.config.cleanup([achall2])
self.assertTrue(mock_restart.called)
@mock.patch("letsencrypt_apache.configurator.ApacheConfigurator.restart")
def test_cleanup_no_errors(self, mock_restart):
_, achall1, achall2 = self.get_achalls()
self.config._chall_out.add(achall1) # pylint: disable=protected-access
self.config.cleanup([achall2])
self.assertFalse(mock_restart.called)
self.config.cleanup([achall1, achall2])
self.assertTrue(mock_restart.called)
@mock.patch("letsencrypt.le_util.run_script")
def test_get_version(self, mock_script):
mock_script.return_value = (
"Server Version: Apache/2.4.2 (Debian)", "") "Server Version: Apache/2.4.2 (Debian)", "")
self.assertEqual(self.config.get_version(), (2, 4, 2)) self.assertEqual(self.config.get_version(), (2, 4, 2))
mock_script.return_value = ( mock_popen().communicate.return_value = (
"Server Version: Apache/2 (Linux)", "") "Server Version: Apache/2 (Linux)", "")
self.assertEqual(self.config.get_version(), (2,)) self.assertEqual(self.config.get_version(), (2,))
mock_script.return_value = ( mock_popen().communicate.return_value = (
"Server Version: Apache (Debian)", "") "Server Version: Apache (Debian)", "")
self.assertRaises(errors.PluginError, self.config.get_version) self.assertRaises(errors.PluginError, self.config.get_version)
mock_script.return_value = ( mock_popen().communicate.return_value = (
"Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "") "Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "")
self.assertRaises(errors.PluginError, self.config.get_version) self.assertRaises(errors.PluginError, self.config.get_version)
mock_script.side_effect = errors.SubprocessError("Can't find program") mock_popen.side_effect = OSError("Can't find program")
self.assertRaises(errors.PluginError, self.config.get_version) 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 = ("", "")
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
self.assertRaises(errors.MisconfigurationError, self.config.restart)
@mock.patch("letsencrypt.le_util.run_script")
def test_config_test(self, _):
self.config.config_test()
@mock.patch("letsencrypt.le_util.run_script")
def test_config_test_bad_process(self, mock_run_script):
mock_run_script.side_effect = errors.SubprocessError
self.assertRaises(errors.MisconfigurationError, self.config.config_test)
def test_get_all_certs_keys(self):
c_k = self.config.get_all_certs_keys()
self.assertEqual(len(c_k), 1)
cert, key, path = next(iter(c_k))
self.assertTrue("cert" in cert)
self.assertTrue("key" in key)
self.assertTrue("default-ssl.conf" in path)
def test_get_all_certs_keys_malformed_conf(self):
self.config.parser.find_dir = mock.Mock(side_effect=[["path"], []])
c_k = self.config.get_all_certs_keys()
self.assertFalse(c_k)
def test_more_info(self):
self.assertTrue(self.config.more_info())
def test_get_chall_pref(self):
self.assertTrue(isinstance(self.config.get_chall_pref(""), list))
def test_temp_install(self):
from letsencrypt_apache.configurator import temp_install
path = os.path.join(self.work_dir, "test_it")
temp_install(path)
self.assertTrue(os.path.isfile(path))
# TEST ENHANCEMENTS
def test_supported_enhancements(self):
self.assertTrue(isinstance(self.config.supported_enhancements(), list))
def test_enhance_unknown_enhancement(self):
self.assertRaises(
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_redirect_well_formed_http(self, mock_exe, _):
self.config.parser.update_runtime_variables = mock.Mock()
mock_exe.return_value = True
# This will create an ssl vhost for letsencrypt.demo
self.config.enhance("letsencrypt.demo", "redirect")
# These are not immediately available in find_dir even with save() and
# load(). They must be found in sites-available
rw_engine = self.config.parser.find_dir(
"RewriteEngine", "on", self.vh_truth[3].path)
rw_rule = self.config.parser.find_dir(
"RewriteRule", None, self.vh_truth[3].path)
self.assertEqual(len(rw_engine), 1)
# three args to rw_rule
self.assertEqual(len(rw_rule), 3)
self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path))
self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path))
self.assertTrue("rewrite_module" in self.config.parser.modules)
def test_redirect_with_conflict(self):
self.config.parser.modules.add("rewrite_module")
ssl_vh = obj.VirtualHost(
"fp", "ap", set([obj.Addr(("*", "443")), obj.Addr(("zombo.com",))]),
True, False)
# No names ^ this guy should conflict.
# pylint: disable=protected-access
self.assertRaises(
errors.PluginError, self.config._enable_redirect, ssl_vh, "")
def test_redirect_twice(self):
# Skip the enable mod
self.config.parser.modules.add("rewrite_module")
self.config.enhance("encryption-example.demo", "redirect")
self.assertRaises(
errors.PluginError,
self.config.enhance, "encryption-example.demo", "redirect")
def test_unknown_rewrite(self):
# Skip the enable mod
self.config.parser.modules.add("rewrite_module")
self.config.parser.add_dir(
self.vh_truth[3].path, "RewriteRule", ["Unknown"])
self.config.save()
self.assertRaises(
errors.PluginError,
self.config.enhance, "letsencrypt.demo", "redirect")
def test_unknown_rewrite2(self):
# Skip the enable mod
self.config.parser.modules.add("rewrite_module")
self.config.parser.add_dir(
self.vh_truth[3].path, "RewriteRule", ["Unknown", "2", "3"])
self.config.save()
self.assertRaises(
errors.PluginError,
self.config.enhance, "letsencrypt.demo", "redirect")
def test_unknown_redirect(self):
# Skip the enable mod
self.config.parser.modules.add("rewrite_module")
self.config.parser.add_dir(
self.vh_truth[3].path, "Redirect", ["Unknown"])
self.config.save()
self.assertRaises(
errors.PluginError,
self.config.enhance, "letsencrypt.demo", "redirect")
def test_create_own_redirect(self):
self.config.parser.modules.add("rewrite_module")
# For full testing... give names...
self.vh_truth[1].name = "default.com"
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), 5)
def get_achalls(self):
"""Return testing achallenges."""
account_key = self.rsa512jwk
achall1 = achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
token="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q"),
"pending"),
domain="encryption-example.demo", account_key=account_key)
achall2 = achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
token="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"),
"pending"),
domain="letsencrypt.demo", account_key=account_key)
return account_key, achall1, achall2
def test_make_addrs_sni_ready(self):
self.config.version = (2, 2)
self.config.make_addrs_sni_ready(
set([obj.Addr.fromstring("*:443"), obj.Addr.fromstring("*:80")]))
self.assertTrue(self.config.parser.find_dir(
"NameVirtualHost", "*:80", exclude=False))
self.assertTrue(self.config.parser.find_dir(
"NameVirtualHost", "*:443", exclude=False))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() # pragma: no cover unittest.main() # pragma: no cover

View file

@ -5,12 +5,10 @@ import unittest
import mock import mock
import zope.component import zope.component
from letsencrypt.display import util as display_util
from letsencrypt_apache import obj
from letsencrypt_apache.tests import util from letsencrypt_apache.tests import util
from letsencrypt.display import util as display_util
class SelectVhostTest(unittest.TestCase): class SelectVhostTest(unittest.TestCase):
"""Tests for letsencrypt_apache.display_ops.select_vhost.""" """Tests for letsencrypt_apache.display_ops.select_vhost."""
@ -55,18 +53,6 @@ class SelectVhostTest(unittest.TestCase):
self.assertEqual(mock_logger.debug.call_count, 1) self.assertEqual(mock_logger.debug.call_count, 1)
@mock.patch("letsencrypt_apache.display_ops.zope.component.getUtility")
def test_multiple_names(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 4)
self.vhosts.append(
obj.VirtualHost(
"path", "aug_path", set([obj.Addr.fromstring("*:80")]),
False, False,
"wildcard.com", set(["*.wildcard.com"])))
self.assertEqual(self.vhosts[4], self._call(self.vhosts))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() # pragma: no cover unittest.main() # pragma: no cover

View file

@ -4,24 +4,27 @@ import shutil
import mock import mock
from acme import challenges
from letsencrypt.plugins import common
from letsencrypt.plugins import common_test from letsencrypt.plugins import common_test
from letsencrypt_apache import obj
from letsencrypt_apache.tests import util from letsencrypt_apache.tests import util
class DvsniPerformTest(util.ApacheTest): class DvsniPerformTest(util.ApacheTest):
"""Test the ApacheDVSNI challenge.""" """Test the ApacheDVSNI challenge."""
auth_key = common_test.DvsniTest.auth_key
achalls = common_test.DvsniTest.achalls achalls = common_test.DvsniTest.achalls
def setUp(self): # pylint: disable=arguments-differ def setUp(self):
super(DvsniPerformTest, self).setUp() super(DvsniPerformTest, self).setUp()
config = util.get_apache_configurator( with mock.patch("letsencrypt_apache.configurator.ApacheConfigurator."
self.config_path, self.config_dir, self.work_dir) "mod_loaded") as mock_load:
config.config.dvsni_port = 443 mock_load.return_value = True
config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir)
from letsencrypt_apache import dvsni from letsencrypt_apache import dvsni
self.sni = dvsni.ApacheDvsni(config) self.sni = dvsni.ApacheDvsni(config)
@ -35,50 +38,37 @@ class DvsniPerformTest(util.ApacheTest):
resp = self.sni.perform() resp = self.sni.perform()
self.assertEqual(len(resp), 0) self.assertEqual(len(resp), 0)
@mock.patch("letsencrypt.le_util.exe_exists") def test_perform1(self):
@mock.patch("letsencrypt.le_util.run_script")
def test_perform1(self, _, mock_exists):
mock_register = mock.Mock()
self.sni.configurator.reverter.register_undo_command = mock_register
mock_exists.return_value = True
self.sni.configurator.parser.update_runtime_variables = mock.Mock()
achall = self.achalls[0] achall = self.achalls[0]
self.sni.add_chall(achall) self.sni.add_chall(achall)
response = self.achalls[0].gen_response(self.auth_key) mock_setup_cert = mock.MagicMock(
mock_setup_cert = mock.MagicMock(return_value=response) return_value=challenges.DVSNIResponse(s="randomS1"))
# pylint: disable=protected-access # pylint: disable=protected-access
self.sni._setup_challenge_cert = mock_setup_cert self.sni._setup_challenge_cert = mock_setup_cert
responses = self.sni.perform() responses = self.sni.perform()
# Make sure that register_undo_command was called into temp directory.
self.assertEqual(True, mock_register.call_args[0][0])
mock_setup_cert.assert_called_once_with(achall) mock_setup_cert.assert_called_once_with(achall)
# Check to make sure challenge config path is included in apache config. # Check to make sure challenge config path is included in apache config.
self.assertEqual( self.assertEqual(
len(self.sni.configurator.parser.find_dir( len(self.sni.configurator.parser.find_dir(
"Include", self.sni.challenge_conf)), 1) "Include", self.sni.challenge_conf)),
1)
self.assertEqual(len(responses), 1) self.assertEqual(len(responses), 1)
self.assertEqual(responses[0], response) self.assertEqual(responses[0].s, "randomS1")
def test_perform2(self): def test_perform2(self):
# Avoid load module
self.sni.configurator.parser.modules.add("ssl_module")
acme_responses = []
for achall in self.achalls: for achall in self.achalls:
self.sni.add_chall(achall) self.sni.add_chall(achall)
acme_responses.append(achall.gen_response(self.auth_key))
mock_setup_cert = mock.MagicMock(side_effect=acme_responses) mock_setup_cert = mock.MagicMock(side_effect=[
challenges.DVSNIResponse(s="randomS0"),
challenges.DVSNIResponse(s="randomS1")])
# pylint: disable=protected-access # pylint: disable=protected-access
self.sni._setup_challenge_cert = mock_setup_cert self.sni._setup_challenge_cert = mock_setup_cert
sni_responses = self.sni.perform() responses = self.sni.perform()
self.assertEqual(mock_setup_cert.call_count, 2) self.assertEqual(mock_setup_cert.call_count, 2)
@ -92,18 +82,20 @@ class DvsniPerformTest(util.ApacheTest):
len(self.sni.configurator.parser.find_dir( len(self.sni.configurator.parser.find_dir(
"Include", self.sni.challenge_conf)), "Include", self.sni.challenge_conf)),
1) 1)
self.assertEqual(len(sni_responses), 2) self.assertEqual(len(responses), 2)
for i in xrange(2): for i in xrange(2):
self.assertEqual(sni_responses[i], acme_responses[i]) self.assertEqual(responses[i].s, "randomS%d" % i)
def test_mod_config(self): def test_mod_config(self):
z_domains = []
for achall in self.achalls: for achall in self.achalls:
self.sni.add_chall(achall) self.sni.add_chall(achall)
z_domain = achall.gen_response(self.auth_key).z_domain v_addr1 = [common.Addr(("1.2.3.4", "443")),
z_domains.append(set([z_domain])) common.Addr(("5.6.7.8", "443"))]
v_addr2 = [common.Addr(("127.0.0.1", "443"))]
self.sni._mod_config() # pylint: disable=protected-access ll_addr = []
ll_addr.append(v_addr1)
ll_addr.append(v_addr2)
self.sni._mod_config(ll_addr) # pylint: disable=protected-access
self.sni.configurator.save() self.sni.configurator.save()
self.sni.configurator.parser.find_dir( self.sni.configurator.parser.find_dir(
@ -117,20 +109,15 @@ class DvsniPerformTest(util.ApacheTest):
vhs.append(self.sni.configurator._create_vhost(match)) vhs.append(self.sni.configurator._create_vhost(match))
self.assertEqual(len(vhs), 2) self.assertEqual(len(vhs), 2)
for vhost in vhs: for vhost in vhs:
self.assertEqual(vhost.addrs, set([obj.Addr.fromstring("*:443")])) if vhost.addrs == set(v_addr1):
names = vhost.get_names() self.assertEqual(
self.assertTrue(names in z_domains) vhost.names,
set([self.achalls[0].nonce_domain]))
def test_get_dvsni_addrs_default(self): else:
self.sni.configurator.choose_vhost = mock.Mock( self.assertEqual(vhost.addrs, set(v_addr2))
return_value=obj.VirtualHost( self.assertEqual(
"path", "aug_path", set([obj.Addr.fromstring("_default_:443")]), vhost.names,
False, False) set([self.achalls[1].nonce_domain]))
)
self.assertEqual(
set([obj.Addr.fromstring("*:443")]),
self.sni.get_dvsni_addrs(self.achalls[0]))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,135 +1,27 @@
"""Tests for letsencrypt_apache.obj.""" """Tests for letsencrypt_apache.obj."""
import unittest import unittest
from letsencrypt.plugins import common
class VirtualHostTest(unittest.TestCase): class VirtualHostTest(unittest.TestCase):
"""Test the VirtualHost class.""" """Test the VirtualHost class."""
def setUp(self): def setUp(self):
from letsencrypt_apache.obj import Addr
from letsencrypt_apache.obj import VirtualHost from letsencrypt_apache.obj import VirtualHost
self.addr1 = Addr.fromstring("127.0.0.1")
self.addr2 = Addr.fromstring("127.0.0.1:443")
self.addr_default = Addr.fromstring("_default_:443")
self.vhost1 = VirtualHost( self.vhost1 = VirtualHost(
"filep", "vh_path", set([self.addr1]), False, False, "localhost") "filep", "vh_path",
set([common.Addr.fromstring("localhost")]), False, False)
self.vhost1b = VirtualHost(
"filep", "vh_path", set([self.addr1]), False, False, "localhost")
self.vhost2 = VirtualHost(
"fp", "vhp", set([self.addr2]), False, False, "localhost")
def test_eq(self): def test_eq(self):
self.assertTrue(self.vhost1b == self.vhost1)
self.assertFalse(self.vhost1 == self.vhost2)
self.assertEqual(str(self.vhost1b), str(self.vhost1))
self.assertFalse(self.vhost1b == 1234)
def test_ne(self):
self.assertTrue(self.vhost1 != self.vhost2)
self.assertFalse(self.vhost1 != self.vhost1b)
def test_conflicts(self):
from letsencrypt_apache.obj import Addr
from letsencrypt_apache.obj import VirtualHost from letsencrypt_apache.obj import VirtualHost
vhost1b = VirtualHost(
"filep", "vh_path",
set([common.Addr.fromstring("localhost")]), False, False)
complex_vh = VirtualHost( self.assertEqual(vhost1b, self.vhost1)
"fp", "vhp", self.assertEqual(str(vhost1b), str(self.vhost1))
set([Addr.fromstring("*:443"), Addr.fromstring("1.2.3.4:443")]), self.assertFalse(vhost1b == 1234)
False, False)
self.assertTrue(complex_vh.conflicts([self.addr1]))
self.assertTrue(complex_vh.conflicts([self.addr2]))
self.assertFalse(complex_vh.conflicts([self.addr_default]))
self.assertTrue(self.vhost1.conflicts([self.addr2]))
self.assertFalse(self.vhost1.conflicts([self.addr_default]))
self.assertFalse(self.vhost2.conflicts([self.addr1, self.addr_default]))
def test_same_server(self):
from letsencrypt_apache.obj import VirtualHost
no_name1 = VirtualHost(
"fp", "vhp", set([self.addr1]), False, False, None)
no_name2 = VirtualHost(
"fp", "vhp", set([self.addr2]), False, False, None)
no_name3 = VirtualHost(
"fp", "vhp", set([self.addr_default]),
False, False, None)
no_name4 = VirtualHost(
"fp", "vhp", set([self.addr2, self.addr_default]),
False, False, None)
self.assertTrue(self.vhost1.same_server(self.vhost2))
self.assertTrue(no_name1.same_server(no_name2))
self.assertFalse(self.vhost1.same_server(no_name1))
self.assertFalse(no_name1.same_server(no_name3))
self.assertFalse(no_name1.same_server(no_name4))
class AddrTest(unittest.TestCase):
"""Test obj.Addr."""
def setUp(self):
from letsencrypt_apache.obj import Addr
self.addr = Addr.fromstring("*:443")
self.addr1 = Addr.fromstring("127.0.0.1")
self.addr2 = Addr.fromstring("127.0.0.1:*")
self.addr_defined = Addr.fromstring("127.0.0.1:443")
self.addr_default = Addr.fromstring("_default_:443")
def test_wildcard(self):
self.assertFalse(self.addr.is_wildcard())
self.assertTrue(self.addr1.is_wildcard())
self.assertTrue(self.addr2.is_wildcard())
def test_get_sni_addr(self):
from letsencrypt_apache.obj import Addr
self.assertEqual(
self.addr.get_sni_addr("443"), Addr.fromstring("*:443"))
self.assertEqual(
self.addr.get_sni_addr("225"), Addr.fromstring("*:225"))
self.assertEqual(
self.addr1.get_sni_addr("443"), Addr.fromstring("127.0.0.1"))
def test_conflicts(self):
# Note: Defined IP is more important than defined port in match
self.assertTrue(self.addr.conflicts(self.addr1))
self.assertTrue(self.addr.conflicts(self.addr2))
self.assertTrue(self.addr.conflicts(self.addr_defined))
self.assertFalse(self.addr.conflicts(self.addr_default))
self.assertFalse(self.addr1.conflicts(self.addr))
self.assertTrue(self.addr1.conflicts(self.addr_defined))
self.assertFalse(self.addr1.conflicts(self.addr_default))
self.assertFalse(self.addr_defined.conflicts(self.addr1))
self.assertFalse(self.addr_defined.conflicts(self.addr2))
self.assertFalse(self.addr_defined.conflicts(self.addr))
self.assertFalse(self.addr_defined.conflicts(self.addr_default))
self.assertTrue(self.addr_default.conflicts(self.addr))
self.assertTrue(self.addr_default.conflicts(self.addr1))
self.assertTrue(self.addr_default.conflicts(self.addr_defined))
# Self test
self.assertTrue(self.addr.conflicts(self.addr))
self.assertTrue(self.addr1.conflicts(self.addr1))
# This is a tricky one...
self.assertTrue(self.addr1.conflicts(self.addr2))
def test_equal(self):
self.assertTrue(self.addr1 == self.addr2)
self.assertFalse(self.addr == self.addr1)
self.assertFalse(self.addr == 123)
def test_not_equal(self):
self.assertFalse(self.addr1 != self.addr2)
self.assertTrue(self.addr != self.addr1)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,32 +1,52 @@
"""Tests for letsencrypt_apache.parser.""" """Tests for letsencrypt_apache.parser."""
import os import os
import shutil import shutil
import sys
import unittest import unittest
import augeas import augeas
import mock import mock
import zope.component
from letsencrypt import errors from letsencrypt import errors
from letsencrypt.display import util as display_util
from letsencrypt_apache.tests import util from letsencrypt_apache.tests import util
class BasicParserTest(util.ParserTest): class ApacheParserTest(util.ApacheTest):
"""Apache Parser Test.""" """Apache Parser Test."""
def setUp(self): # pylint: disable=arguments-differ def setUp(self):
super(BasicParserTest, self).setUp() super(ApacheParserTest, self).setUp()
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
from letsencrypt_apache.parser import ApacheParser
self.aug = augeas.Augeas(flags=augeas.Augeas.NONE)
self.parser = ApacheParser(self.aug, self.config_path, self.ssl_options)
def tearDown(self): def tearDown(self):
shutil.rmtree(self.temp_dir) shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir) shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir) shutil.rmtree(self.work_dir)
def test_find_config_root_no_root(self): def test_root_normalized(self):
# pylint: disable=protected-access from letsencrypt_apache.parser import ApacheParser
os.remove(self.parser.loc["root"]) path = os.path.join(self.temp_dir, "debian_apache_2_4/////"
self.assertRaises( "two_vhost_80/../two_vhost_80/apache2")
errors.NoInstallationError, self.parser._find_config_root) parser = ApacheParser(self.aug, path, None)
self.assertEqual(parser.root, self.config_path)
def test_root_absolute(self):
from letsencrypt_apache.parser import ApacheParser
parser = ApacheParser(self.aug, os.path.relpath(self.config_path), None)
self.assertEqual(parser.root, self.config_path)
def test_root_no_trailing_slash(self):
from letsencrypt_apache.parser import ApacheParser
parser = ApacheParser(self.aug, self.config_path + os.path.sep, None)
self.assertEqual(parser.root, self.config_path)
def test_parse_file(self): def test_parse_file(self):
"""Test parse_file. """Test parse_file.
@ -47,11 +67,11 @@ class BasicParserTest(util.ParserTest):
self.assertTrue(matches) self.assertTrue(matches)
def test_find_dir(self): def test_find_dir(self):
test = self.parser.find_dir("Listen", "80") from letsencrypt_apache.parser import case_i
test = self.parser.find_dir(case_i("Listen"), "443")
# This will only look in enabled hosts # This will only look in enabled hosts
test2 = self.parser.find_dir("documentroot") test2 = self.parser.find_dir(case_i("documentroot"))
self.assertEqual(len(test), 2)
self.assertEqual(len(test), 1)
self.assertEqual(len(test2), 3) self.assertEqual(len(test2), 3)
def test_add_dir(self): def test_add_dir(self):
@ -73,32 +93,15 @@ class BasicParserTest(util.ParserTest):
""" """
from letsencrypt_apache.parser import get_aug_path from letsencrypt_apache.parser import get_aug_path
# This makes sure that find_dir will work
self.parser.modules.add("mod_ssl.c")
self.parser.add_dir_to_ifmodssl( self.parser.add_dir_to_ifmodssl(
get_aug_path(self.parser.loc["default"]), get_aug_path(self.parser.loc["default"]),
"FakeDirective", ["123"]) "FakeDirective", "123")
matches = self.parser.find_dir("FakeDirective", "123") matches = self.parser.find_dir("FakeDirective", "123")
self.assertEqual(len(matches), 1) self.assertEqual(len(matches), 1)
self.assertTrue("IfModule" in matches[0]) self.assertTrue("IfModule" in matches[0])
def test_add_dir_to_ifmodssl_multiple(self):
from letsencrypt_apache.parser import get_aug_path
# This makes sure that find_dir will work
self.parser.modules.add("mod_ssl.c")
self.parser.add_dir_to_ifmodssl(
get_aug_path(self.parser.loc["default"]),
"FakeDirective", ["123", "456", "789"])
matches = self.parser.find_dir("FakeDirective")
self.assertEqual(len(matches), 3)
self.assertTrue("IfModule" in matches[0])
def test_get_aug_path(self): def test_get_aug_path(self):
from letsencrypt_apache.parser import get_aug_path from letsencrypt_apache.parser import get_aug_path
self.assertEqual("/files/etc/apache", get_aug_path("/etc/apache")) self.assertEqual("/files/etc/apache", get_aug_path("/etc/apache"))
@ -106,114 +109,20 @@ class BasicParserTest(util.ParserTest):
def test_set_locations(self): def test_set_locations(self):
with mock.patch("letsencrypt_apache.parser.os.path") as mock_path: with mock.patch("letsencrypt_apache.parser.os.path") as mock_path:
mock_path.isfile.return_value = False
# pylint: disable=protected-access
self.assertRaises(errors.PluginError,
self.parser._set_locations, self.ssl_options)
mock_path.isfile.side_effect = [True, False, False] mock_path.isfile.side_effect = [True, False, False]
# pylint: disable=protected-access # pylint: disable=protected-access
results = self.parser._set_locations() results = self.parser._set_locations(self.ssl_options)
self.assertEqual(results["default"], results["listen"]) self.assertEqual(results["default"], results["listen"])
self.assertEqual(results["default"], results["name"]) self.assertEqual(results["default"], results["name"])
def test_set_user_config_file(self):
# pylint: disable=protected-access
path = os.path.join(self.parser.root, "httpd.conf")
open(path, 'w').close()
self.parser.add_dir(self.parser.loc["default"], "Include", "httpd.conf")
self.assertEqual(
path, self.parser._set_user_config_file())
@mock.patch("letsencrypt_apache.parser.ApacheParser._get_runtime_cfg")
def test_update_runtime_variables(self, mock_cfg):
mock_cfg.return_value = (
'ServerRoot: "/etc/apache2"\n'
'Main DocumentRoot: "/var/www"\n'
'Main ErrorLog: "/var/log/apache2/error.log"\n'
'Mutex ssl-stapling: using_defaults\n'
'Mutex ssl-cache: using_defaults\n'
'Mutex default: dir="/var/lock/apache2" mechanism=fcntl\n'
'Mutex watchdog-callback: using_defaults\n'
'PidFile: "/var/run/apache2/apache2.pid"\n'
'Define: TEST\n'
'Define: DUMP_RUN_CFG\n'
'Define: U_MICH\n'
'Define: TLS=443\n'
'Define: example_path=Documents/path\n'
'User: name="www-data" id=33 not_used\n'
'Group: name="www-data" id=33 not_used\n'
)
expected_vars = {"TEST": "", "U_MICH": "", "TLS": "443",
"example_path": "Documents/path"}
self.parser.update_runtime_variables("ctl")
self.assertEqual(self.parser.variables, expected_vars)
@mock.patch("letsencrypt_apache.parser.ApacheParser._get_runtime_cfg")
def test_update_runtime_vars_bad_output(self, mock_cfg):
mock_cfg.return_value = "Define: TLS=443=24"
self.assertRaises(
errors.PluginError, self.parser.update_runtime_variables, "ctl")
mock_cfg.return_value = "Define: DUMP_RUN_CFG\nDefine: TLS=443=24"
self.assertRaises(
errors.PluginError, self.parser.update_runtime_variables, "ctl")
@mock.patch("letsencrypt_apache.parser.subprocess.Popen")
def test_update_runtime_vars_bad_ctl(self, mock_popen):
mock_popen.side_effect = OSError
self.assertRaises(
errors.MisconfigurationError,
self.parser.update_runtime_variables, "ctl")
@mock.patch("letsencrypt_apache.parser.subprocess.Popen")
def test_update_runtime_vars_bad_exit(self, mock_popen):
mock_popen().communicate.return_value = ("", "")
mock_popen.returncode = -1
self.assertRaises(
errors.MisconfigurationError,
self.parser.update_runtime_variables, "ctl")
class ParserInitTest(util.ApacheTest):
def setUp(self): # pylint: disable=arguments-differ
super(ParserInitTest, self).setUp()
self.aug = augeas.Augeas(
flags=augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
def test_root_normalized(self):
from letsencrypt_apache.parser import ApacheParser
with mock.patch("letsencrypt_apache.parser.ApacheParser."
"update_runtime_variables"):
path = os.path.join(
self.temp_dir,
"debian_apache_2_4/////two_vhost_80/../two_vhost_80/apache2")
parser = ApacheParser(self.aug, path, "dummy_ctl")
self.assertEqual(parser.root, self.config_path)
def test_root_absolute(self):
from letsencrypt_apache.parser import ApacheParser
with mock.patch("letsencrypt_apache.parser.ApacheParser."
"update_runtime_variables"):
parser = ApacheParser(
self.aug, os.path.relpath(self.config_path), "dummy_ctl")
self.assertEqual(parser.root, self.config_path)
def test_root_no_trailing_slash(self):
from letsencrypt_apache.parser import ApacheParser
with mock.patch("letsencrypt_apache.parser.ApacheParser."
"update_runtime_variables"):
parser = ApacheParser(
self.aug, self.config_path + os.path.sep, "dummy_ctl")
self.assertEqual(parser.root, self.config_path)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() # pragma: no cover unittest.main() # pragma: no cover

View file

@ -1,55 +0,0 @@
# Global configuration
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
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
# Include module configuration:
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Include generic snippets of statements
IncludeOptional conf-enabled/
# Include the virtual host configurations:
IncludeOptional sites-enabled/*.conf
Define COMPLEX
Define tls_port 1234
Define tls_port_str "1234"
Define fnmatch_filename test_fnmatch.conf
Include test_variables.conf
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View file

@ -1,9 +0,0 @@
# 3 - one arg directives
# 2 - two arg directives
# 1 - three arg directives
TestArgsDirective one_arg
TestArgsDirective one_arg two_arg
TestArgsDirective one_arg
TestArgsDirective one_arg two_arg
TestArgsDirective one_arg two_arg three_arg
TestArgsDirective one_arg

View file

@ -1 +0,0 @@
FNMATCH_DIRECTIVE Success

View file

@ -1,66 +0,0 @@
TestVariablePort ${tls_port}
TestVariablePortStr "${tls_port_str}"
LoadModule status_module modules/mod_status.so
# Basic IfDefine
<IfDefine COMPLEX>
VAR_DIRECTIVE success
LoadModule ssl_module modules/mod_ssl.so
</IfDefine>
<IfDefine !COMPLEX>
INVALID_VAR_DIRECTIVE failure
</IfDefine>
<IfDefine NOT_COMPLEX>
INVALID_VAR_DIRECTIVE failure
</IfDefine>
<IfDefine !NOT_COMPLEX>
VAR_DIRECTIVE failure
</IfDefine>
# Basic IfModule
<IfModule ssl_module>
MOD_DIRECTIVE Success
</IfModule>
<IfModule !ssl_module>
INVALID_MOD_DIRECTIVE failure
</IfModule>
<IfModule fake_module>
INVALID_MOD_DIRECTIVE failure
</IfModule>
<IfModule !fake_module>
MOD_DIRECTIVE Success
</IfModule>
# Nested Tests
<IfModule status_module>
<IfDefine COMPLEX>
NESTED_DIRECTIVE success
<IfModule mod_ssl.c>
NESTED_DIRECTIVE success
</IfModule>
<IfModule !mod_ssl.c>
INVALID_NESTED_DIRECTIVE failure
</IfModule>
</IfDefine>
<IfDefine !COMPLEX>
INVALID_NESTED_DIRECTIVE failure
<IfModule ssl_module>
INVALID_NESTED_DIRECTIVE failure
</IfModule>
</IfDefine>
NESTED_DIRECTIVE success
</IfModule>

View file

@ -1,5 +0,0 @@
<VirtualHost 1.1.1.1>
ServerName invalid.net
</virtualHost>

View file

@ -1 +0,0 @@
LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so

View file

@ -1,20 +1,12 @@
"""Common utilities for letsencrypt_apache.""" """Common utilities for letsencrypt_apache."""
import os import os
import sys import pkg_resources
import unittest import unittest
import augeas
import mock import mock
import zope.component
from acme import jose
from letsencrypt.display import util as display_util
from letsencrypt.plugins import common from letsencrypt.plugins import common
from letsencrypt.tests import test_util
from letsencrypt_apache import configurator from letsencrypt_apache import configurator
from letsencrypt_apache import constants from letsencrypt_apache import constants
from letsencrypt_apache import obj from letsencrypt_apache import obj
@ -22,78 +14,49 @@ from letsencrypt_apache import obj
class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
def setUp(self, test_dir="debian_apache_2_4/two_vhost_80", def setUp(self):
config_root="debian_apache_2_4/two_vhost_80/apache2"):
# pylint: disable=arguments-differ
super(ApacheTest, self).setUp() super(ApacheTest, self).setUp()
self.temp_dir, self.config_dir, self.work_dir = common.dir_setup( self.temp_dir, self.config_dir, self.work_dir = common.dir_setup(
test_dir=test_dir, test_dir="debian_apache_2_4/two_vhost_80",
pkg="letsencrypt_apache.tests") pkg="letsencrypt_apache.tests")
self.ssl_options = common.setup_ssl_options( self.ssl_options = common.setup_ssl_options(
self.config_dir, constants.MOD_SSL_CONF_SRC, self.config_dir, constants.MOD_SSL_CONF_SRC,
constants.MOD_SSL_CONF_DEST) constants.MOD_SSL_CONF_DEST)
self.config_path = os.path.join(self.temp_dir, config_root) self.config_path = os.path.join(
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2")
self.rsa512jwk = jose.JWKRSA.load(test_util.load_vector( self.rsa256_file = pkg_resources.resource_filename(
"rsa512_key.pem")) "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem"))
self.rsa256_pem = pkg_resources.resource_string(
"letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem"))
class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods
def setUp(self, test_dir="debian_apache_2_4/two_vhost_80",
config_root="debian_apache_2_4/two_vhost_80/apache2"):
super(ParserTest, self).setUp(test_dir, config_root)
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
from letsencrypt_apache.parser import ApacheParser
self.aug = augeas.Augeas(
flags=augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)
with mock.patch("letsencrypt_apache.parser.ApacheParser."
"update_runtime_variables"):
self.parser = ApacheParser(
self.aug, self.config_path, "dummy_ctl_path")
def get_apache_configurator( def get_apache_configurator(
config_path, config_dir, work_dir, version=(2, 4, 7), conf=None): config_path, config_dir, work_dir, version=(2, 4, 7)):
"""Create an Apache Configurator with the specified options. """Create an Apache Configurator with the specified options."""
:param conf: Function that returns binary paths. self.conf in Configurator
"""
backups = os.path.join(work_dir, "backups") backups = os.path.join(work_dir, "backups")
mock_le_config = mock.MagicMock(
apache_server_root=config_path,
apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"],
backup_dir=backups,
config_dir=config_dir,
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
work_dir=work_dir)
with mock.patch("letsencrypt_apache.configurator." with mock.patch("letsencrypt_apache.configurator."
"subprocess.Popen") as mock_popen: "subprocess.Popen") as mock_popen:
# This indicates config_test passes # This just states that the ssl module is already loaded
mock_popen().communicate.return_value = ("Fine output", "No problems") mock_popen().communicate.return_value = ("ssl_module", "")
mock_popen().returncode = 0 config = configurator.ApacheConfigurator(
with mock.patch("letsencrypt_apache.configurator.le_util." config=mock.MagicMock(
"exe_exists") as mock_exe_exists: apache_server_root=config_path,
mock_exe_exists.return_value = True apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"],
with mock.patch("letsencrypt_apache.parser.ApacheParser." backup_dir=backups,
"update_runtime_variables"): config_dir=config_dir,
config = configurator.ApacheConfigurator( temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
config=mock_le_config, in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
name="apache", work_dir=work_dir),
version=version) name="apache",
# This allows testing scripts to set it a bit more quickly version=version)
if conf is not None:
config.conf = conf # pragma: no cover
config.prepare() config.prepare()
return config return config
@ -108,23 +71,23 @@ def get_vh_truth(temp_dir, config_name):
obj.VirtualHost( obj.VirtualHost(
os.path.join(prefix, "encryption-example.conf"), os.path.join(prefix, "encryption-example.conf"),
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"), os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), set([common.Addr.fromstring("*:80")]),
False, True, "encryption-example.demo"), False, True, set(["encryption-example.demo"])),
obj.VirtualHost( obj.VirtualHost(
os.path.join(prefix, "default-ssl.conf"), os.path.join(prefix, "default-ssl.conf"),
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"), os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
set([obj.Addr.fromstring("_default_:443")]), True, False), set([common.Addr.fromstring("_default_:443")]), True, False),
obj.VirtualHost( obj.VirtualHost(
os.path.join(prefix, "000-default.conf"), os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"), os.path.join(aug_pre, "000-default.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True, set([common.Addr.fromstring("*:80")]), False, True,
"ip-172-30-0-17"), set(["ip-172-30-0-17"])),
obj.VirtualHost( obj.VirtualHost(
os.path.join(prefix, "letsencrypt.conf"), os.path.join(prefix, "letsencrypt.conf"),
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"), os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True, set([common.Addr.fromstring("*:80")]), False, True,
"letsencrypt.demo"), set(["letsencrypt.demo"])),
] ]
return vh_truth return vh_truth
return None # pragma: no cover return None

View file

@ -1,56 +1,23 @@
import sys
from setuptools import setup from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.1.0.dev0'
install_requires = [ install_requires = [
'acme=={0}'.format(version), 'acme',
'letsencrypt=={0}'.format(version), 'letsencrypt',
'mock<1.1.0', # py26
'python-augeas', 'python-augeas',
'setuptools', # pkg_resources
'zope.component', 'zope.component',
'zope.interface', 'zope.interface',
] ]
if sys.version_info < (2, 7):
install_requires.append('mock<1.1.0')
else:
install_requires.append('mock')
setup( setup(
name='letsencrypt-apache', name='letsencrypt-apache',
version=version,
description="Apache plugin for Let's Encrypt client",
url='https://github.com/letsencrypt/letsencrypt',
author="Let's Encrypt Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',
'Topic :: System :: Networking',
'Topic :: System :: Systems Administration',
'Topic :: Utilities',
],
packages=find_packages(), packages=find_packages(),
include_package_data=True,
install_requires=install_requires, install_requires=install_requires,
entry_points={ entry_points={
'letsencrypt.plugins': [ 'letsencrypt.plugins': [
'apache = letsencrypt_apache.configurator:ApacheConfigurator', 'apache = letsencrypt_apache.configurator:ApacheConfigurator',
], ],
}, },
) )

View file

@ -1,190 +0,0 @@
Copyright 2015 Electronic Frontier Foundation and others
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -1,6 +0,0 @@
include LICENSE.txt
include README.rst
include letsencrypt_compatibility_test/configurators/apache/a2enmod.sh
include letsencrypt_compatibility_test/configurators/apache/a2dismod.sh
include letsencrypt_compatibility_test/configurators/apache/Dockerfile
recursive-include letsencrypt_compatibility_test/testdata *

View file

@ -1 +0,0 @@
Compatibility tests for Let's Encrypt client

View file

@ -1 +0,0 @@
"""Let's Encrypt compatibility test"""

View file

@ -1 +0,0 @@
"""Let's Encrypt compatibility test configurators"""

View file

@ -1,20 +0,0 @@
FROM httpd
MAINTAINER Brad Warren <bradmw@umich.edu>
RUN mkdir /var/run/apache2
ENV APACHE_RUN_USER=daemon \
APACHE_RUN_GROUP=daemon \
APACHE_PID_FILE=/usr/local/apache2/logs/httpd.pid \
APACHE_RUN_DIR=/var/run/apache2 \
APACHE_LOCK_DIR=/var/lock \
APACHE_LOG_DIR=/usr/local/apache2/logs
COPY letsencrypt-compatibility-test/letsencrypt_compatibility_test/configurators/apache/a2enmod.sh /usr/local/bin/
COPY letsencrypt-compatibility-test/letsencrypt_compatibility_test/configurators/apache/a2dismod.sh /usr/local/bin/
COPY letsencrypt-compatibility-test/letsencrypt_compatibility_test/testdata/rsa1024_key2.pem /usr/local/apache2/conf/
COPY letsencrypt-compatibility-test/letsencrypt_compatibility_test/testdata/empty_cert.pem /usr/local/apache2/conf/
# Note: this only exposes the port to other docker containers. You
# still have to bind to 443@host at runtime.
EXPOSE 443

View file

@ -1 +0,0 @@
"""Let's Encrypt compatibility test Apache configurators"""

View file

@ -1,14 +0,0 @@
#!/bin/bash
# An extremely simplified version of `a2enmod` for disabling modules in the
# httpd docker image. First argument is the server_root and the second is the
# module to be disabled.
apache_confdir=$1
module=$2
sed -i "/.*"$module".*/d" "$apache_confdir/test.conf"
enabled_conf="$apache_confdir/mods-enabled/"$module".conf"
if [ -e "$enabled_conf" ]
then
rm $enabled_conf
fi

View file

@ -1,33 +0,0 @@
#!/bin/bash
# An extremely simplified version of `a2enmod` for enabling modules in the
# httpd docker image. First argument is the server_root and the second is the
# module to be enabled.
APACHE_CONFDIR=$1
enable () {
echo "LoadModule "$1"_module /usr/local/apache2/modules/mod_"$1".so" >> \
$APACHE_CONFDIR"/test.conf"
available_base="/mods-available/"$1".conf"
available_conf=$APACHE_CONFDIR$available_base
enabled_dir=$APACHE_CONFDIR"/mods-enabled"
enabled_conf=$enabled_dir"/"$1".conf"
if [ -e "$available_conf" -a -d "$enabled_dir" -a ! -e "$enabled_conf" ]
then
ln -s "..$available_base" $enabled_conf
fi
}
if [ $2 == "ssl" ]
then
# Enables ssl and all its dependencies
enable "setenvif"
enable "mime"
enable "socache_shmcb"
enable "ssl"
elif [ $2 == "rewrite" ]
then
enable "rewrite"
else
exit 1
fi

View file

@ -1,64 +0,0 @@
"""Proxies ApacheConfigurator for Apache 2.4 tests"""
import zope.interface
from letsencrypt_compatibility_test import errors
from letsencrypt_compatibility_test import interfaces
from letsencrypt_compatibility_test.configurators.apache import common as apache_common
# The docker image doesn't actually have the watchdog module, but unless the
# config uses mod_heartbeat or mod_heartmonitor (which aren't installed and
# therefore the config won't be loaded), I believe this isn't a problem
# http://httpd.apache.org/docs/2.4/mod/mod_watchdog.html
STATIC_MODULES = set(["core", "so", "http", "mpm_event", "watchdog"])
SHARED_MODULES = {
"log_config", "logio", "version", "unixd", "access_compat", "actions",
"alias", "allowmethods", "auth_basic", "auth_digest", "auth_form",
"authn_anon", "authn_core", "authn_dbd", "authn_dbm", "authn_file",
"authn_socache", "authnz_ldap", "authz_core", "authz_dbd", "authz_dbm",
"authz_groupfile", "authz_host", "authz_owner", "authz_user", "autoindex",
"buffer", "cache", "cache_disk", "cache_socache", "cgid", "dav", "dav_fs",
"dbd", "deflate", "dir", "dumpio", "env", "expires", "ext_filter",
"file_cache", "filter", "headers", "include", "info", "lbmethod_bybusyness",
"lbmethod_byrequests", "lbmethod_bytraffic", "lbmethod_heartbeat", "ldap",
"log_debug", "macro", "mime", "negotiation", "proxy", "proxy_ajp",
"proxy_balancer", "proxy_connect", "proxy_express", "proxy_fcgi",
"proxy_ftp", "proxy_http", "proxy_scgi", "proxy_wstunnel", "ratelimit",
"remoteip", "reqtimeout", "request", "rewrite", "sed", "session",
"session_cookie", "session_crypto", "session_dbd", "setenvif",
"slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb",
"speling", "ssl", "status", "substitute", "unique_id", "userdir",
"vhost_alias"}
class Proxy(apache_common.Proxy):
"""Wraps the ApacheConfigurator for Apache 2.4 tests"""
zope.interface.implements(interfaces.IConfiguratorProxy)
def __init__(self, args):
"""Initializes the plugin with the given command line args"""
super(Proxy, self).__init__(args)
# Running init isn't ideal, but the Docker container needs to survive
# Apache restarts
self.start_docker("bradmw/apache2.4", "init")
def preprocess_config(self, server_root):
"""Prepares the configuration for use in the Docker"""
super(Proxy, self).preprocess_config(server_root)
if self.version[1] != 4:
raise errors.Error("Apache version not 2.4")
with open(self.test_conf, "a") as f:
for module in self.modules:
if module not in STATIC_MODULES:
if module in SHARED_MODULES:
f.write(
"LoadModule {0}_module /usr/local/apache2/modules/"
"mod_{0}.so\n".format(module))
else:
raise errors.Error(
"Unsupported module {0}".format(module))

View file

@ -1,285 +0,0 @@
"""Provides a common base for Apache proxies"""
import re
import os
import subprocess
import mock
import zope.interface
from letsencrypt import configuration
from letsencrypt import errors as le_errors
from letsencrypt_apache import configurator
from letsencrypt_compatibility_test import errors
from letsencrypt_compatibility_test import interfaces
from letsencrypt_compatibility_test import util
from letsencrypt_compatibility_test.configurators import common as configurators_common
APACHE_VERSION_REGEX = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
APACHE_COMMANDS = ["apachectl", "a2enmod", "a2dismod"]
class Proxy(configurators_common.Proxy):
# pylint: disable=too-many-instance-attributes
"""A common base for Apache test configurators"""
zope.interface.implements(interfaces.IConfiguratorProxy)
def __init__(self, args):
"""Initializes the plugin with the given command line args"""
super(Proxy, self).__init__(args)
self.le_config.apache_le_vhost_ext = "-le-ssl.conf"
self._setup_mock()
self.modules = self.server_root = self.test_conf = self.version = None
self._apache_configurator = self._all_names = self._test_names = None
def _setup_mock(self):
"""Replaces specific modules with mock.MagicMock"""
mock_subprocess = mock.MagicMock()
mock_subprocess.check_call = self.check_call
mock_subprocess.Popen = self.popen
mock.patch(
"letsencrypt_apache.configurator.subprocess",
mock_subprocess).start()
mock.patch(
"letsencrypt_apache.parser.subprocess",
mock_subprocess).start()
mock.patch(
"letsencrypt.le_util.subprocess",
mock_subprocess).start()
mock.patch(
"letsencrypt_apache.configurator.le_util.exe_exists",
_is_apache_command).start()
patch = mock.patch(
"letsencrypt_apache.configurator.display_ops.select_vhost")
mock_display = patch.start()
mock_display.side_effect = le_errors.PluginError(
"Unable to determine vhost")
def check_call(self, command, *args, **kwargs):
"""If command is an Apache command, command is executed in the
running docker image. Otherwise, subprocess.check_call is used.
"""
if _is_apache_command(command):
command = _modify_command(command)
return super(Proxy, self).check_call(command, *args, **kwargs)
else:
return subprocess.check_call(command, *args, **kwargs)
def popen(self, command, *args, **kwargs):
"""If command is an Apache command, command is executed in the
running docker image. Otherwise, subprocess.Popen is used.
"""
if _is_apache_command(command):
command = _modify_command(command)
return super(Proxy, self).popen(command, *args, **kwargs)
else:
return subprocess.Popen(command, *args, **kwargs)
def __getattr__(self, name):
"""Wraps the Apache Configurator methods"""
method = getattr(self._apache_configurator, name, None)
if callable(method):
return method
else:
raise AttributeError()
def load_config(self):
"""Loads the next configuration for the plugin to test"""
if hasattr(self.le_config, "apache_init_script"):
try:
self.check_call([self.le_config.apache_init_script, "stop"])
except errors.Error:
raise errors.Error(
"Failed to stop previous apache config from running")
config = super(Proxy, self).load_config()
self.modules = _get_modules(config)
self.version = _get_version(config)
self._all_names, self._test_names = _get_names(config)
server_root = _get_server_root(config)
with open(os.path.join(config, "config_file")) as f:
config_file = os.path.join(server_root, f.readline().rstrip())
self.test_conf = _create_test_conf(server_root, config_file)
self.preprocess_config(server_root)
self._prepare_configurator(server_root, config_file)
try:
self.check_call("apachectl -d {0} -f {1} -k start".format(
server_root, config_file))
except errors.Error:
raise errors.Error(
"Apache failed to load {0} before tests started".format(
config))
return config
def preprocess_config(self, server_root):
# pylint: disable=anomalous-backslash-in-string, no-self-use
"""Prepares the configuration for use in the Docker"""
find = subprocess.Popen(
["find", server_root, "-type", "f"],
stdout=subprocess.PIPE)
subprocess.check_call([
"xargs", "sed", "-e", "s/DocumentRoot.*/DocumentRoot "
"\/usr\/local\/apache2\/htdocs/I",
"-e", "s/SSLPassPhraseDialog.*/SSLPassPhraseDialog builtin/I",
"-e", "s/TypesConfig.*/TypesConfig "
"\/usr\/local\/apache2\/conf\/mime.types/I",
"-e", "s/LoadModule/#LoadModule/I",
"-e", "s/SSLCertificateFile.*/SSLCertificateFile "
"\/usr\/local\/apache2\/conf\/empty_cert.pem/I",
"-e", "s/SSLCertificateKeyFile.*/SSLCertificateKeyFile "
"\/usr\/local\/apache2\/conf\/rsa1024_key2.pem/I",
"-i"], stdin=find.stdout)
def _prepare_configurator(self, server_root, config_file):
"""Prepares the Apache plugin for testing"""
self.le_config.apache_server_root = server_root
self.le_config.apache_ctl = "apachectl -d {0} -f {1}".format(
server_root, config_file)
self.le_config.apache_enmod = "a2enmod.sh {0}".format(server_root)
self.le_config.apache_dismod = "a2dismod.sh {0}".format(server_root)
self.le_config.apache_init_script = self.le_config.apache_ctl + " -k"
self._apache_configurator = configurator.ApacheConfigurator(
config=configuration.NamespaceConfig(self.le_config),
name="apache")
self._apache_configurator.prepare()
def cleanup_from_tests(self):
"""Performs any necessary cleanup from running plugin tests"""
super(Proxy, self).cleanup_from_tests()
mock.patch.stopall()
def get_all_names_answer(self):
"""Returns the set of domain names that the plugin should find"""
if self._all_names:
return self._all_names
else:
raise errors.Error("No configuration file loaded")
def get_testable_domain_names(self):
"""Returns the set of domain names that can be tested against"""
if self._test_names:
return self._test_names
else:
return {"example.com"}
def deploy_cert(self, domain, cert_path, key_path, chain_path=None,
fullchain_path=None):
"""Installs cert"""
cert_path, key_path, chain_path = self.copy_certs_and_keys(
cert_path, key_path, chain_path)
self._apache_configurator.deploy_cert(
domain, cert_path, key_path, chain_path, fullchain_path)
def _is_apache_command(command):
"""Returns true if command is an Apache command"""
if isinstance(command, list):
command = command[0]
for apache_command in APACHE_COMMANDS:
if command.startswith(apache_command):
return True
return False
def _modify_command(command):
"""Modifies command so configtest works inside the docker image"""
if isinstance(command, list):
for i in xrange(len(command)):
if command[i] == "configtest":
command[i] = "-t"
else:
command = command.replace("configtest", "-t")
return command
def _create_test_conf(server_root, apache_config):
"""Creates a test config file and adds it to the Apache config"""
test_conf = os.path.join(server_root, "test.conf")
open(test_conf, "w").close()
subprocess.check_call(
["sed", "-i", "1iInclude test.conf", apache_config])
return test_conf
def _get_server_root(config):
"""Returns the server root directory in config"""
subdirs = [
name for name in os.listdir(config)
if os.path.isdir(os.path.join(config, name))]
if len(subdirs) != 1:
errors.Error("Malformed configuration directiory {0}".format(config))
return os.path.join(config, subdirs[0].rstrip())
def _get_names(config):
"""Returns all and testable domain names in config"""
all_names = set()
non_ip_names = set()
with open(os.path.join(config, "vhosts")) as f:
for line in f:
# If parsing a specific vhost
if line[0].isspace():
words = line.split()
if words[0] == "alias":
all_names.add(words[1])
non_ip_names.add(words[1])
# If for port 80 and not IP vhost
elif words[1] == "80" and not util.IP_REGEX.match(words[3]):
all_names.add(words[3])
non_ip_names.add(words[3])
elif "NameVirtualHost" not in line:
words = line.split()
if (words[0].endswith("*") or words[0].endswith("80") and
not util.IP_REGEX.match(words[1]) and
words[1].find(".") != -1):
all_names.add(words[1])
return all_names, non_ip_names
def _get_modules(config):
"""Returns the list of modules found in module_list"""
modules = []
with open(os.path.join(config, "modules")) as f:
for line in f:
# Modules list is indented, everything else is headers/footers
if line[0].isspace():
words = line.split()
# Modules redundantly end in "_module" which we can discard
modules.append(words[0][:-7])
return modules
def _get_version(config):
"""Return version of Apache Server.
Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)). Code taken from
the Apache plugin.
"""
with open(os.path.join(config, "version")) as f:
# Should be on first line of input
matches = APACHE_VERSION_REGEX.findall(f.readline())
if len(matches) != 1:
raise errors.Error("Unable to find Apache version")
return tuple([int(i) for i in matches[0].split(".")])

View file

@ -1,145 +0,0 @@
"""Provides a common base for configurator proxies"""
import logging
import os
import shutil
import tempfile
import docker
from letsencrypt import constants
from letsencrypt_compatibility_test import errors
from letsencrypt_compatibility_test import util
logger = logging.getLogger(__name__)
class Proxy(object):
# pylint: disable=too-many-instance-attributes
"""A common base for compatibility test configurators"""
_NOT_ADDED_ARGS = True
@classmethod
def add_parser_arguments(cls, parser):
"""Adds command line arguments needed by the plugin"""
if Proxy._NOT_ADDED_ARGS:
group = parser.add_argument_group("docker")
group.add_argument(
"--docker-url", default="unix://var/run/docker.sock",
help="URL of the docker server")
group.add_argument(
"--no-remove", action="store_true",
help="do not delete container on program exit")
Proxy._NOT_ADDED_ARGS = False
def __init__(self, args):
"""Initializes the plugin with the given command line args"""
self._temp_dir = tempfile.mkdtemp()
self.le_config = util.create_le_config(self._temp_dir)
config_dir = util.extract_configs(args.configs, self._temp_dir)
self._configs = [
os.path.join(config_dir, config)
for config in os.listdir(config_dir)]
self.args = args
self._docker_client = docker.Client(
base_url=self.args.docker_url, version="auto")
self.http_port, self.https_port = util.get_two_free_ports()
self._container_id = None
def has_more_configs(self):
"""Returns true if there are more configs to test"""
return bool(self._configs)
def cleanup_from_tests(self):
"""Performs any necessary cleanup from running plugin tests"""
self._docker_client.stop(self._container_id, 0)
if not self.args.no_remove:
self._docker_client.remove_container(self._container_id)
def load_config(self):
"""Returns the next config directory to be tested"""
shutil.rmtree(self.le_config.work_dir, ignore_errors=True)
backup = os.path.join(self.le_config.work_dir, constants.BACKUP_DIR)
os.makedirs(backup)
return self._configs.pop()
def start_docker(self, image_name, command):
"""Creates and runs a Docker container with the specified image"""
logger.warning("Pulling Docker image. This may take a minute.")
for line in self._docker_client.pull(image_name, stream=True):
logger.debug(line)
host_config = docker.utils.create_host_config(
binds={self._temp_dir: {"bind": self._temp_dir, "mode": "rw"}},
port_bindings={
80: ("127.0.0.1", self.http_port),
443: ("127.0.0.1", self.https_port)},)
container = self._docker_client.create_container(
image_name, command, ports=[80, 443], volumes=self._temp_dir,
host_config=host_config)
if container["Warnings"]:
logger.warning(container["Warnings"])
self._container_id = container["Id"]
self._docker_client.start(self._container_id)
def check_call(self, command, *args, **kwargs):
# pylint: disable=unused-argument
"""Simulates a call to check_call but executes the command in the
running docker image
"""
if self.popen(command).returncode:
raise errors.Error(
"{0} exited with a nonzero value".format(command))
def popen(self, command, *args, **kwargs):
# pylint: disable=unused-argument
"""Simulates a call to Popen but executes the command in the
running docker image
"""
class SimplePopen(object):
# pylint: disable=too-few-public-methods
"""Simplified Popen object"""
def __init__(self, returncode, output):
self.returncode = returncode
self._stdout = output
self._stderr = output
def communicate(self):
"""Returns stdout and stderr"""
return self._stdout, self._stderr
if isinstance(command, list):
command = " ".join(command)
returncode, output = self.execute_in_docker(command)
return SimplePopen(returncode, output)
def execute_in_docker(self, command):
"""Executes command inside the running docker image"""
logger.debug("Executing '%s'", command)
exec_id = self._docker_client.exec_create(self._container_id, command)
output = self._docker_client.exec_start(exec_id)
returncode = self._docker_client.exec_inspect(exec_id)["ExitCode"]
return returncode, output
def copy_certs_and_keys(self, cert_path, key_path, chain_path=None):
"""Copies certs and keys into the temporary directory"""
cert_and_key_dir = os.path.join(self._temp_dir, "certs_and_keys")
if not os.path.isdir(cert_and_key_dir):
os.mkdir(cert_and_key_dir)
cert = os.path.join(cert_and_key_dir, "cert")
shutil.copy(cert_path, cert)
key = os.path.join(cert_and_key_dir, "key")
shutil.copy(key_path, key)
if chain_path:
chain = os.path.join(cert_and_key_dir, "chain")
shutil.copy(chain_path, chain)
else:
chain = None
return cert, key, chain

View file

@ -1,5 +0,0 @@
"""Let's Encrypt compatibility test errors"""
class Error(Exception):
"""Generic Let's Encrypt compatibility test error"""

View file

@ -1,52 +0,0 @@
"""Let's Encrypt compatibility test interfaces"""
import zope.interface
import letsencrypt.interfaces
# pylint: disable=no-self-argument,no-method-argument
class IPluginProxy(zope.interface.Interface):
"""Wraps a Let's Encrypt plugin"""
http_port = zope.interface.Attribute(
"The port to connect to on localhost for HTTP traffic")
https_port = zope.interface.Attribute(
"The port to connect to on localhost for HTTPS traffic")
def add_parser_arguments(cls, parser):
"""Adds command line arguments needed by the parser"""
def __init__(args):
"""Initializes the plugin with the given command line args"""
def cleanup_from_tests():
"""Performs any necessary cleanup from running plugin tests.
This is guaranteed to be called before the program exits.
"""
def has_more_configs():
"""Returns True if there are more configs to test"""
def load_config():
"""Loads the next config and returns its name"""
def get_testable_domain_names():
"""Returns the domain names that can be used in testing"""
class IAuthenticatorProxy(IPluginProxy, letsencrypt.interfaces.IAuthenticator):
"""Wraps a Let's Encrypt authenticator"""
class IInstallerProxy(IPluginProxy, letsencrypt.interfaces.IInstaller):
"""Wraps a Let's Encrypt installer"""
def get_all_names_answer():
"""Returns all names that should be found by the installer"""
class IConfiguratorProxy(IAuthenticatorProxy, IInstallerProxy):
"""Wraps a Let's Encrypt configurator"""

View file

@ -1,368 +0,0 @@
"""Tests Let's Encrypt plugins against different server configurations."""
import argparse
import filecmp
import functools
import logging
import os
import shutil
import tempfile
import time
import OpenSSL
from acme import challenges
from acme import crypto_util
from acme import messages
from letsencrypt import achallenges
from letsencrypt import errors as le_errors
from letsencrypt import validator
from letsencrypt.tests import acme_util
from letsencrypt_compatibility_test import errors
from letsencrypt_compatibility_test import util
from letsencrypt_compatibility_test.configurators.apache import apache24
DESCRIPTION = """
Tests Let's Encrypt plugins against different server configuratons. It is
assumed that Docker is already installed. If no test types is specified, all
tests that the plugin supports are performed.
"""
PLUGINS = {"apache": apache24.Proxy}
logger = logging.getLogger(__name__)
def test_authenticator(plugin, config, temp_dir):
"""Tests authenticator, returning True if the tests are successful"""
backup = _create_backup(config, temp_dir)
achalls = _create_achalls(plugin)
if not achalls:
logger.error("The plugin and this program support no common "
"challenge types")
return False
try:
responses = plugin.perform(achalls)
except le_errors.Error as error:
logger.error("Performing challenges on %s caused an error:", config)
logger.exception(error)
return False
success = True
for i in xrange(len(responses)):
if not responses[i]:
logger.error(
"Plugin failed to complete %s for %s in %s",
type(achalls[i]), achalls[i].domain, config)
success = False
elif isinstance(responses[i], challenges.DVSNIResponse):
verify = functools.partial(responses[i].simple_verify, achalls[i],
achalls[i].domain,
util.JWK.public_key(),
host="127.0.0.1",
port=plugin.https_port)
if _try_until_true(verify):
logger.info(
"DVSNI verification for %s succeeded", achalls[i].domain)
else:
logger.error(
"DVSNI verification for %s in %s failed",
achalls[i].domain, config)
success = False
if success:
try:
plugin.cleanup(achalls)
except le_errors.Error as error:
logger.error("Challenge cleanup for %s caused an error:", config)
logger.exception(error)
success = False
if _dirs_are_unequal(config, backup):
logger.error("Challenge cleanup failed for %s", config)
return False
else:
logger.info("Challenge cleanup succeeded")
return success
def _create_achalls(plugin):
"""Returns a list of annotated challenges to test on plugin"""
achalls = list()
names = plugin.get_testable_domain_names()
for domain in names:
prefs = plugin.get_chall_pref(domain)
for chall_type in prefs:
if chall_type == challenges.DVSNI:
chall = challenges.DVSNI(
token=os.urandom(challenges.DVSNI.TOKEN_SIZE))
challb = acme_util.chall_to_challb(
chall, messages.STATUS_PENDING)
achall = achallenges.DVSNI(
challb=challb, domain=domain, account_key=util.JWK)
achalls.append(achall)
return achalls
def test_installer(args, plugin, config, temp_dir):
"""Tests plugin as an installer"""
backup = _create_backup(config, temp_dir)
names_match = plugin.get_all_names() == plugin.get_all_names_answer()
if names_match:
logger.info("get_all_names test succeeded")
else:
logger.error("get_all_names test failed for config %s", config)
domains = list(plugin.get_testable_domain_names())
success = test_deploy_cert(plugin, temp_dir, domains)
if success and args.enhance:
success = test_enhancements(plugin, domains)
good_rollback = test_rollback(plugin, config, backup)
return names_match and success and good_rollback
def test_deploy_cert(plugin, temp_dir, domains):
"""Tests deploy_cert returning True if the tests are successful"""
cert = crypto_util.gen_ss_cert(util.KEY, domains)
cert_path = os.path.join(temp_dir, "cert.pem")
with open(cert_path, "w") as f:
f.write(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert))
for domain in domains:
try:
plugin.deploy_cert(domain, cert_path, util.KEY_PATH)
except le_errors.Error as error:
logger.error("Plugin failed to deploy ceritificate for %s:", domain)
logger.exception(error)
return False
if not _save_and_restart(plugin, "deployed"):
return False
success = True
for domain in domains:
verify = functools.partial(validator.Validator().certificate, cert,
domain, "127.0.0.1", plugin.https_port)
if not _try_until_true(verify):
logger.error("Could not verify certificate for domain %s", domain)
success = False
if success:
logger.info("HTTPS validation succeeded")
return success
def test_enhancements(plugin, domains):
"""Tests supported enhancements returning True if successful"""
supported = plugin.supported_enhancements()
if "redirect" not in supported:
logger.error("The plugin and this program support no common "
"enhancements")
return False
for domain in domains:
try:
plugin.enhance(domain, "redirect")
except le_errors.PluginError as error:
# Don't immediately fail because a redirect may already be enabled
logger.warning("Plugin failed to enable redirect for %s:", domain)
logger.warning("%s", error)
except le_errors.Error as error:
logger.error("An error occurred while enabling redirect for %s:",
domain)
logger.exception(error)
if not _save_and_restart(plugin, "enhanced"):
return False
success = True
for domain in domains:
verify = functools.partial(validator.Validator().redirect, "localhost",
plugin.http_port, headers={"Host": domain})
if not _try_until_true(verify):
logger.error("Improper redirect for domain %s", domain)
success = False
if success:
logger.info("Enhancments test succeeded")
return success
def _try_until_true(func, max_tries=5, sleep_time=0.5):
"""Calls func up to max_tries times until it returns True"""
for _ in xrange(0, max_tries):
if func():
return True
else:
time.sleep(sleep_time)
return False
def _save_and_restart(plugin, title=None):
"""Saves and restart the plugin, returning True if no errors occurred"""
try:
plugin.save(title)
plugin.restart()
return True
except le_errors.Error as error:
logger.error("Plugin failed to save and restart server:")
logger.exception(error)
return False
def test_rollback(plugin, config, backup):
"""Tests the rollback checkpoints function"""
try:
plugin.rollback_checkpoints(1337)
except le_errors.Error as error:
logger.error("Plugin raised an exception during rollback:")
logger.exception(error)
return False
if _dirs_are_unequal(config, backup):
logger.error("Rollback failed for config `%s`", config)
return False
else:
logger.info("Rollback succeeded")
return True
def _create_backup(config, temp_dir):
"""Creates a backup of config in temp_dir"""
backup = os.path.join(temp_dir, "backup")
shutil.rmtree(backup, ignore_errors=True)
shutil.copytree(config, backup, symlinks=True)
return backup
def _dirs_are_unequal(dir1, dir2):
"""Returns True if dir1 and dir2 are unequal"""
dircmps = [filecmp.dircmp(dir1, dir2)]
while len(dircmps):
dircmp = dircmps.pop()
if dircmp.left_only or dircmp.right_only:
logger.error("The following files and directories are only "
"present in one directory")
if dircmp.left_only:
logger.error(dircmp.left_only)
else:
logger.error(dircmp.right_only)
return True
elif dircmp.common_funny or dircmp.funny_files:
logger.error("The following files and directories could not be "
"compared:")
if dircmp.common_funny:
logger.error(dircmp.common_funny)
else:
logger.error(dircmp.funny_files)
return True
elif dircmp.diff_files:
logger.error("The following files differ:")
logger.error(dircmp.diff_files)
return True
for subdir in dircmp.subdirs.itervalues():
dircmps.append(subdir)
return False
def get_args():
"""Returns parsed command line arguments."""
parser = argparse.ArgumentParser(
description=DESCRIPTION,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
group = parser.add_argument_group("general")
group.add_argument(
"-c", "--configs", default="configs.tar.gz",
help="a directory or tarball containing server configurations")
group.add_argument(
"-p", "--plugin", default="apache", help="the plugin to be tested")
group.add_argument(
"-v", "--verbose", dest="verbose_count", action="count",
default=0, help="you know how to use this")
group.add_argument(
"-a", "--auth", action="store_true",
help="tests the challenges the plugin supports")
group.add_argument(
"-i", "--install", action="store_true",
help="tests the plugin as an installer")
group.add_argument(
"-e", "--enhance", action="store_true", help="tests the enhancements "
"the plugin supports (implicitly includes installer tests)")
for plugin in PLUGINS.itervalues():
plugin.add_parser_arguments(parser)
args = parser.parse_args()
if args.enhance:
args.install = True
elif not (args.auth or args.install):
args.auth = args.install = args.enhance = True
return args
def setup_logging(args):
"""Prepares logging for the program"""
handler = logging.StreamHandler()
root_logger = logging.getLogger()
root_logger.setLevel(logging.ERROR - args.verbose_count * 10)
root_logger.addHandler(handler)
def main():
"""Main test script execution."""
args = get_args()
setup_logging(args)
if args.plugin not in PLUGINS:
raise errors.Error("Unknown plugin {0}".format(args.plugin))
temp_dir = tempfile.mkdtemp()
plugin = PLUGINS[args.plugin](args)
try:
plugin.execute_in_docker("mkdir -p /var/log/apache2")
while plugin.has_more_configs():
success = True
try:
config = plugin.load_config()
logger.info("Loaded configuration: %s", config)
if args.auth:
success = test_authenticator(plugin, config, temp_dir)
if success and args.install:
success = test_installer(args, plugin, config, temp_dir)
except errors.Error as error:
logger.error("Tests on %s raised:", config)
logger.exception(error)
success = False
if success:
logger.info("All tests on %s succeeded", config)
else:
logger.error("Tests on %s failed", config)
finally:
plugin.cleanup_from_tests()
if __name__ == "__main__":
main()

View file

@ -1,13 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICATCCAWoCCQCvMbKu4FHZ6zANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMB4XDTE1MDcyMzIzMjc1MFoXDTE2MDcyMjIzMjc1MFowRTELMAkG
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAws3o
y46PMLM9Gr68pbex0MhdPr7Cq4rRe9BBpnOuHFdF35Ak0aPrzFwVzLlGOir94U11
e5JYJDWJi+4FwLBRkOAfanjJ5GJ9BnEHSOdbtO+sv9uhbt+7iYOOUOngKSiJyUrM
i1THAE+B1CenxZ1KHRQCke708zkK8jVuxLeIAOMCAwEAATANBgkqhkiG9w0BAQsF
AAOBgQCC3LUP3MHk+IBmwHHZAZCX+6p4lop9SP6y6rDpWgnqEEeb9oFleHi2Rvzq
7gxl6nS5AsaSzfAygJ3zWKTwVAZyU4GOQ8QTK+nHk3+LO1X4cDbUlQfm5+YuwKDa
4LFKeovmrK6BiMLIc1J+MxUjLfCeVHYSdkZULTVXue0zif0BUA==
-----END CERTIFICATE-----

Some files were not shown because too many files have changed in this diff Show more