mirror of
https://github.com/certbot/certbot.git
synced 2026-04-28 17:51:04 -04:00
merge github/letsencrypt/master
This commit is contained in:
commit
b0fe02f732
86 changed files with 5586 additions and 353 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
*.pyc
|
||||
*.egg-info
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
venv/
|
||||
|
|
@ -9,3 +10,4 @@ m3
|
|||
*~
|
||||
.vagrant
|
||||
*.swp
|
||||
\#*#
|
||||
15
.travis.yml
15
.travis.yml
|
|
@ -1,10 +1,7 @@
|
|||
language: python
|
||||
|
||||
# please keep this in sync with docs/using.rst (Ubuntu section, apt-get)
|
||||
before_install: >
|
||||
travis_retry sudo apt-get install python python-setuptools
|
||||
python-virtualenv python-dev gcc swig dialog libaugeas0 libssl-dev
|
||||
libffi-dev ca-certificates
|
||||
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
|
||||
before_install: travis_retry sudo ./bootstrap/ubuntu.sh
|
||||
|
||||
install: "travis_retry pip install tox coveralls"
|
||||
script: "travis_retry tox"
|
||||
|
|
@ -22,4 +19,10 @@ env:
|
|||
|
||||
notifications:
|
||||
email: false
|
||||
irc: "chat.freenode.net#letsencrypt"
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#letsencrypt"
|
||||
on_success: never
|
||||
on_failure: always
|
||||
use_notice: true
|
||||
skip_join: true
|
||||
|
|
|
|||
30
LICENSE.txt
30
LICENSE.txt
|
|
@ -1,4 +1,14 @@
|
|||
Let's Encrypt Preview:
|
||||
Copyright (c) Internet Security Research Group
|
||||
Licensed Apache Version 2.0
|
||||
|
||||
Incorporating code from nginxparser
|
||||
Copyright (c) 2014 Fatih Erikli
|
||||
Licensed MIT
|
||||
|
||||
|
||||
Text of Apache License
|
||||
======================
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
|
@ -173,3 +183,23 @@
|
|||
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.
|
||||
|
||||
|
||||
Text of MIT License
|
||||
===================
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
|
|||
4
Vagrantfile
vendored
4
Vagrantfile
vendored
|
|
@ -6,10 +6,8 @@ VAGRANTFILE_API_VERSION = "2"
|
|||
|
||||
# Setup instructions from docs/using.rst
|
||||
$ubuntu_setup_script = <<SETUP_SCRIPT
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python python-setuptools python-virtualenv python-dev gcc swig dialog libaugeas0 libssl-dev libffi-dev ca-certificates
|
||||
|
||||
cd /vagrant
|
||||
sudo ./bootstrap/ubuntu.sh
|
||||
if [ ! -d "venv" ]; then
|
||||
virtualenv --no-site-packages -p python2 venv
|
||||
./venv/bin/python setup.py dev
|
||||
|
|
|
|||
2
bootstrap/README
Normal file
2
bootstrap/README
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
This directory contains scripts that install necessary OS-specific
|
||||
prerequisite dependencies (see docs/using.rst).
|
||||
35
bootstrap/_deb_common.sh
Executable file
35
bootstrap/_deb_common.sh
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Tested with:
|
||||
# - Ubuntu:
|
||||
# - 12.04 (x64, Travis)
|
||||
# - 14.04 (x64, Vagrant)
|
||||
# - 14.10 (x64)
|
||||
# - Debian:
|
||||
# - 6.0.10 "squeeze" (x64)
|
||||
# - 7.8 "wheezy" (x64)
|
||||
# - 8.0 "jessie" (x64)
|
||||
|
||||
# virtualenv binary can be found in different packages depending on
|
||||
# distro version (#346)
|
||||
distro=$(lsb_release -si)
|
||||
# 6.0.10 => 60, 14.04 => 1404
|
||||
version=$(lsb_release -sr | awk -F '.' '{print $1 $2}')
|
||||
if [ "$distro" = "Ubuntu" -a "$version" -ge 1410 ]
|
||||
then
|
||||
virtualenv="virtualenv"
|
||||
elif [ "$distro" = "Debian" -a "$version" -ge 80 ]
|
||||
then
|
||||
virtualenv="virtualenv"
|
||||
else
|
||||
virtualenv="python-virtualenv"
|
||||
fi
|
||||
|
||||
# dpkg-dev: dpkg-architecture binary necessary to compile M2Crypto, c.f.
|
||||
# #276, https://github.com/martinpaljak/M2Crypto/issues/62,
|
||||
# M2Crypto setup.py:add_multiarch_paths
|
||||
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
python python-setuptools "$virtualenv" python-dev gcc swig \
|
||||
dialog libaugeas0 libssl-dev libffi-dev ca-certificates dpkg-dev
|
||||
1
bootstrap/debian.sh
Symbolic link
1
bootstrap/debian.sh
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
_deb_common.sh
|
||||
2
bootstrap/mac.sh
Executable file
2
bootstrap/mac.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
brew install augeas swig
|
||||
1
bootstrap/ubuntu.sh
Symbolic link
1
bootstrap/ubuntu.sh
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
_deb_common.sh
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
:mod:`letsencrypt.acme`
|
||||
=======================
|
||||
|
||||
.. contents::
|
||||
|
||||
.. automodule:: letsencrypt.acme
|
||||
:members:
|
||||
|
||||
|
|
@ -8,9 +10,18 @@
|
|||
Messages
|
||||
--------
|
||||
|
||||
v00
|
||||
~~~
|
||||
|
||||
.. automodule:: letsencrypt.acme.messages
|
||||
:members:
|
||||
|
||||
v02
|
||||
~~~
|
||||
|
||||
.. automodule:: letsencrypt.acme.messages2
|
||||
:members:
|
||||
|
||||
|
||||
Challenges
|
||||
----------
|
||||
|
|
@ -21,10 +32,18 @@ Challenges
|
|||
|
||||
Other ACME objects
|
||||
------------------
|
||||
|
||||
.. automodule:: letsencrypt.acme.other
|
||||
:members:
|
||||
|
||||
|
||||
Fields
|
||||
------
|
||||
|
||||
.. automodule:: letsencrypt.acme.fields
|
||||
:members:
|
||||
|
||||
|
||||
Errors
|
||||
------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.client.client_authenticator`
|
||||
----------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.client_authenticator
|
||||
:members:
|
||||
5
docs/api/client/continuity_auth.rst
Normal file
5
docs/api/client/continuity_auth.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.continuity_auth`
|
||||
-----------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.continuity_auth
|
||||
:members:
|
||||
5
docs/api/client/network2.rst
Normal file
5
docs/api/client/network2.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.network2`
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.network2
|
||||
:members:
|
||||
|
|
@ -80,6 +80,8 @@ Plugin-architecture
|
|||
|
||||
Let's Encrypt has a plugin architecture to facilitate support for
|
||||
different webservers, other TLS servers, and operating systems.
|
||||
The interfaces available for plugins to implement are defined in
|
||||
`interfaces.py`_.
|
||||
|
||||
The most common kind of plugin is a "Configurator", which is likely to
|
||||
implement the `~letsencrypt.client.interfaces.IAuthenticator` and
|
||||
|
|
@ -89,6 +91,8 @@ Configurators may implement just one of those).
|
|||
There are also `~letsencrypt.client.interfaces.IDisplay` plugins,
|
||||
which implement bindings to alternative UI libraries.
|
||||
|
||||
.. _interfaces.py: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/letsencrypt/client/interfaces.py
|
||||
|
||||
|
||||
Authenticators
|
||||
--------------
|
||||
|
|
@ -98,15 +102,16 @@ the ACME server. From the protocol, there are essentially two
|
|||
different types of challenges. Challenges that must be solved by
|
||||
individual plugins in order to satisfy domain validation (subclasses
|
||||
of `~.DVChallenge`, i.e. `~.challenges.DVSNI`,
|
||||
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and client specific
|
||||
challenges (subclasses of `~.ClientChallenge`,
|
||||
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific
|
||||
challenges (subclasses of `~.ContinuityChallenge`,
|
||||
i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`,
|
||||
`~.challenges.ProofOfPossession`). Client specific challenges are
|
||||
always handled by the `~.ClientAuthenticator`. Right now we have two
|
||||
DV Authenticators, `~.ApacheConfigurator` and the
|
||||
`~.StandaloneAuthenticator`. The Standalone and Apache authenticators
|
||||
only solve the `~.challenges.DVSNI` challenge currently. (You can set
|
||||
which challenges your authenticator can handle through the
|
||||
`~.challenges.ProofOfPossession`). Continuity challenges are
|
||||
always handled by the `~.ContinuityAuthenticator`, while plugins are
|
||||
expected to handle `~.DVChallenge` types.
|
||||
Right now, we have two authenticator plugins, the `~.ApacheConfigurator`
|
||||
and the `~.StandaloneAuthenticator`. The Standalone and Apache
|
||||
authenticators only solve the `~.challenges.DVSNI` challenge currently.
|
||||
(You can set which challenges your authenticator can handle through the
|
||||
:meth:`~.IAuthenticator.get_chall_pref`.
|
||||
|
||||
(FYI: We also have a partial implementation for a `~.DNSAuthenticator`
|
||||
|
|
@ -126,26 +131,27 @@ Installers and Authenticators will oftentimes be the same
|
|||
class/object. Installers and Authenticators are kept separate because
|
||||
it should be possible to use the `~.StandaloneAuthenticator` (it sets
|
||||
up its own Python server to perform challenges) with a program that
|
||||
cannot solve challenges itself. (I am imagining MTA installers).
|
||||
cannot solve challenges itself. (Imagine MTA installers).
|
||||
|
||||
|
||||
Installer Development
|
||||
---------------------
|
||||
|
||||
There are a few existing classes that may be beneficial while
|
||||
developing a new `~letsencrypt.client.interfaces.IInstaller`.
|
||||
Installers aimed to reconfigure UNIX servers may use Augeas for
|
||||
configuration parsing and can inherit from `~.AugeasConfigurator` class
|
||||
to handle much of the interface. Installers that are unable to use
|
||||
Augeas may still find the `~.Reverter` class helpful in handling
|
||||
configuration checkpoints and rollback.
|
||||
|
||||
|
||||
Display
|
||||
~~~~~~~
|
||||
|
||||
We currently offer a pythondialog and "text" mode for displays. I have
|
||||
rewritten the interface which should be merged within the next day
|
||||
(the rewrite is in the revoker branch of the repo and should be merged
|
||||
within the next day). Display plugins implement
|
||||
`~letsencrypt.client.interfaces.IDisplay` interface.
|
||||
|
||||
|
||||
Augeas
|
||||
------
|
||||
|
||||
Some plugins, especially those designed to reconfigure UNIX servers,
|
||||
can take inherit from `~.AugeasConfigurator` class in order to more
|
||||
efficiently handle common operations on UNIX server configuration
|
||||
files.
|
||||
We currently offer a pythondialog and "text" mode for displays. Display
|
||||
plugins implement the `~letsencrypt.client.interfaces.IDisplay`
|
||||
interface.
|
||||
|
||||
|
||||
.. _coding-style:
|
||||
|
|
|
|||
|
|
@ -5,36 +5,46 @@ Using the Let's Encrypt client
|
|||
Prerequisites
|
||||
=============
|
||||
|
||||
The demo code is supported and known to work on **Ubuntu only** (even
|
||||
closely related `Debian is known to fail`_).
|
||||
|
||||
Therefore, prerequisites for other platforms listed below are provided
|
||||
mainly for the :ref:`developers <hacking>` reference.
|
||||
The demo code is supported and known to work on **Ubuntu and
|
||||
Debian**. Therefore, prerequisites for other platforms listed below
|
||||
are provided mainly for the :ref:`developers <hacking>` reference.
|
||||
|
||||
In general:
|
||||
|
||||
* ``sudo`` is required as a suggested way of running privileged process
|
||||
* `swig`_ is required for compiling `m2crypto`_
|
||||
* `augeas`_ is required for the ``python-augeas`` bindings
|
||||
|
||||
.. _Debian is known to fail: https://github.com/letsencrypt/lets-encrypt-preview/issues/68
|
||||
|
||||
Ubuntu
|
||||
------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo apt-get install python python-setuptools python-virtualenv python-dev \
|
||||
gcc swig dialog libaugeas0 libssl-dev libffi-dev \
|
||||
ca-certificates
|
||||
sudo ./bootstrap/ubuntu.sh
|
||||
|
||||
|
||||
Debian
|
||||
------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo ./bootstrap/debian.sh
|
||||
|
||||
For squezze you will need to:
|
||||
|
||||
- Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``.
|
||||
|
||||
|
||||
.. _`#280`: https://github.com/letsencrypt/lets-encrypt-preview/issues/280
|
||||
|
||||
.. Please keep the above command in sync with .travis.yml (before_install)
|
||||
|
||||
Mac OSX
|
||||
-------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo brew install augeas swig
|
||||
sudo ./bootstrap/mac.sh
|
||||
|
||||
|
||||
Installation
|
||||
|
|
|
|||
42
examples/restified.py
Normal file
42
examples/restified.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import logging
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
import M2Crypto
|
||||
|
||||
from letsencrypt.acme import messages2
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
from letsencrypt.client import network2
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
|
||||
|
||||
key = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
|
||||
net = network2.Network(NEW_REG_URL, key)
|
||||
|
||||
regr = net.register(contact=(
|
||||
'mailto:cert-admin@example.com', 'tel:+12025551212'))
|
||||
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
|
||||
net.update_registration(regr.update(
|
||||
body=regr.body.update(agreement=regr.terms_of_service)))
|
||||
logging.debug(regr)
|
||||
|
||||
authzr = net.request_challenges(
|
||||
identifier=messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example1.com'),
|
||||
regr=regr)
|
||||
logging.debug(authzr)
|
||||
|
||||
authzr, authzr_response = net.poll(authzr)
|
||||
|
||||
csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem')))
|
||||
try:
|
||||
net.request_issuance(csr, (authzr,))
|
||||
except messages2.Error as error:
|
||||
print error.detail
|
||||
|
|
@ -13,12 +13,12 @@ from letsencrypt.acme import other
|
|||
|
||||
|
||||
class Challenge(jose.TypedJSONObjectWithFields):
|
||||
# _fields_to_json | pylint: disable=abstract-method
|
||||
# _fields_to_partial_json | pylint: disable=abstract-method
|
||||
"""ACME challenge."""
|
||||
TYPES = {}
|
||||
|
||||
|
||||
class ClientChallenge(Challenge): # pylint: disable=abstract-method
|
||||
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
|
||||
"""Client validation challenges."""
|
||||
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method
|
|||
|
||||
|
||||
class ChallengeResponse(jose.TypedJSONObjectWithFields):
|
||||
# _fields_to_json | pylint: disable=abstract-method
|
||||
# _fields_to_partial_json | pylint: disable=abstract-method
|
||||
"""ACME challenge response."""
|
||||
TYPES = {}
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ class DVSNIResponse(ChallengeResponse):
|
|||
return self.z(chall) + self.DOMAIN_SUFFIX
|
||||
|
||||
@Challenge.register
|
||||
class RecoveryContact(ClientChallenge):
|
||||
class RecoveryContact(ContinuityChallenge):
|
||||
"""ACME "recoveryContact" challenge."""
|
||||
typ = "recoveryContact"
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ class RecoveryContactResponse(ChallengeResponse):
|
|||
|
||||
|
||||
@Challenge.register
|
||||
class RecoveryToken(ClientChallenge):
|
||||
class RecoveryToken(ContinuityChallenge):
|
||||
"""ACME "recoveryToken" challenge."""
|
||||
typ = "recoveryToken"
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ class RecoveryTokenResponse(ChallengeResponse):
|
|||
|
||||
|
||||
@Challenge.register
|
||||
class ProofOfPossession(ClientChallenge):
|
||||
class ProofOfPossession(ContinuityChallenge):
|
||||
"""ACME "proofOfPossession" challenge.
|
||||
|
||||
:ivar str nonce: Random data, **not** base64-encoded.
|
||||
|
|
@ -184,7 +184,8 @@ class ProofOfPossession(ClientChallenge):
|
|||
"""Hints for "proofOfPossession" challenge.
|
||||
|
||||
:ivar jwk: JSON Web Key (:class:`letsencrypt.acme.jose.JWK`)
|
||||
:ivar list certs: List of :class:`M2Crypto.X509.X509` cetificates.
|
||||
:ivar list certs: List of :class:`letsencrypt.acme.jose.ComparableX509`
|
||||
certificates.
|
||||
|
||||
"""
|
||||
jwk = jose.Field("jwk", decoder=jose.JWK.from_json)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ from letsencrypt.acme import other
|
|||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
|
||||
pkg_resources.resource_filename(
|
||||
'letsencrypt.client.tests', 'testdata/cert.pem')))
|
||||
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', os.path.join('testdata', 'rsa256_key.pem')))
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests',
|
||||
os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
|
||||
|
||||
class SimpleHTTPSTest(unittest.TestCase):
|
||||
|
|
@ -28,13 +30,17 @@ class SimpleHTTPSTest(unittest.TestCase):
|
|||
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA',
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import SimpleHTTPS
|
||||
self.assertEqual(self.msg, SimpleHTTPS.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import SimpleHTTPS
|
||||
hash(SimpleHTTPS.from_json(self.jmsg))
|
||||
|
||||
|
||||
class SimpleHTTPSResponseTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -50,14 +56,18 @@ class SimpleHTTPSResponseTest(unittest.TestCase):
|
|||
self.assertEqual('https://example.com/.well-known/acme-challenge/'
|
||||
'6tbIMBC5Anhl5bOlWT5ZFA', self.msg.uri('example.com'))
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import SimpleHTTPSResponse
|
||||
self.assertEqual(
|
||||
self.msg, SimpleHTTPSResponse.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import SimpleHTTPSResponse
|
||||
hash(SimpleHTTPSResponse.from_json(self.jmsg))
|
||||
|
||||
|
||||
class DVSNITest(unittest.TestCase):
|
||||
|
||||
|
|
@ -77,13 +87,17 @@ class DVSNITest(unittest.TestCase):
|
|||
self.assertEqual('a82d5ff8ef740d12881f6d3c2277ab2e.acme.invalid',
|
||||
self.msg.nonce_domain)
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import DVSNI
|
||||
self.assertEqual(self.msg, DVSNI.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import DVSNI
|
||||
hash(DVSNI.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_invalid_r_length(self):
|
||||
from letsencrypt.acme.challenges import DVSNI
|
||||
self.jmsg['r'] = 'abcd'
|
||||
|
|
@ -122,13 +136,17 @@ class DVSNIResponseTest(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
'{0}.acme.invalid'.format(z), self.msg.z_domain(challenge))
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import DVSNIResponse
|
||||
self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import DVSNIResponse
|
||||
hash(DVSNIResponse.from_json(self.jmsg))
|
||||
|
||||
|
||||
class RecoveryContactTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -145,13 +163,17 @@ class RecoveryContactTest(unittest.TestCase):
|
|||
'contact' : 'c********n@example.com',
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import RecoveryContact
|
||||
self.assertEqual(self.msg, RecoveryContact.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import RecoveryContact
|
||||
hash(RecoveryContact.from_json(self.jmsg))
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg['activationURL']
|
||||
del self.jmsg['successURL']
|
||||
|
|
@ -163,7 +185,7 @@ class RecoveryContactTest(unittest.TestCase):
|
|||
self.assertTrue(msg.activation_url is None)
|
||||
self.assertTrue(msg.success_url is None)
|
||||
self.assertTrue(msg.contact is None)
|
||||
self.assertEqual(self.jmsg, msg.to_json())
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
|
||||
|
||||
class RecoveryContactResponseTest(unittest.TestCase):
|
||||
|
|
@ -173,14 +195,18 @@ class RecoveryContactResponseTest(unittest.TestCase):
|
|||
self.msg = RecoveryContactResponse(token='23029d88d9e123e')
|
||||
self.jmsg = {'type': 'recoveryContact', 'token': '23029d88d9e123e'}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import RecoveryContactResponse
|
||||
self.assertEqual(
|
||||
self.msg, RecoveryContactResponse.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import RecoveryContactResponse
|
||||
hash(RecoveryContactResponse.from_json(self.jmsg))
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg['token']
|
||||
|
||||
|
|
@ -188,7 +214,7 @@ class RecoveryContactResponseTest(unittest.TestCase):
|
|||
msg = RecoveryContactResponse.from_json(self.jmsg)
|
||||
|
||||
self.assertTrue(msg.token is None)
|
||||
self.assertEqual(self.jmsg, msg.to_json())
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
|
||||
|
||||
class RecoveryTokenTest(unittest.TestCase):
|
||||
|
|
@ -198,13 +224,17 @@ class RecoveryTokenTest(unittest.TestCase):
|
|||
self.msg = RecoveryToken()
|
||||
self.jmsg = {'type': 'recoveryToken'}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import RecoveryToken
|
||||
self.assertEqual(self.msg, RecoveryToken.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import RecoveryToken
|
||||
hash(RecoveryToken.from_json(self.jmsg))
|
||||
|
||||
|
||||
class RecoveryTokenResponseTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -213,14 +243,18 @@ class RecoveryTokenResponseTest(unittest.TestCase):
|
|||
self.msg = RecoveryTokenResponse(token='23029d88d9e123e')
|
||||
self.jmsg = {'type': 'recoveryToken', 'token': '23029d88d9e123e'}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import RecoveryTokenResponse
|
||||
self.assertEqual(
|
||||
self.msg, RecoveryTokenResponse.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import RecoveryTokenResponse
|
||||
hash(RecoveryTokenResponse.from_json(self.jmsg))
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg['token']
|
||||
|
||||
|
|
@ -228,7 +262,7 @@ class RecoveryTokenResponseTest(unittest.TestCase):
|
|||
msg = RecoveryTokenResponse.from_json(self.jmsg)
|
||||
|
||||
self.assertTrue(msg.token is None)
|
||||
self.assertEqual(self.jmsg, msg.to_json())
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
|
||||
|
||||
class ProofOfPossessionHintsTest(unittest.TestCase):
|
||||
|
|
@ -264,16 +298,20 @@ class ProofOfPossessionHintsTest(unittest.TestCase):
|
|||
'authorizedFor': authorized_for,
|
||||
}
|
||||
self.jmsg_from = self.jmsg_to.copy()
|
||||
self.jmsg_from.update({'jwk': jwk.fully_serialize()})
|
||||
self.jmsg_from.update({'jwk': jwk.to_json()})
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg_to, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import ProofOfPossession
|
||||
self.assertEqual(
|
||||
self.msg, ProofOfPossession.Hints.from_json(self.jmsg_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import ProofOfPossession
|
||||
hash(ProofOfPossession.Hints.from_json(self.jmsg_from))
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
for optional in ['certFingerprints', 'certs', 'subjectKeyIdentifiers',
|
||||
'serialNumbers', 'issuers', 'authorizedFor']:
|
||||
|
|
@ -290,7 +328,7 @@ class ProofOfPossessionHintsTest(unittest.TestCase):
|
|||
self.assertEqual(msg.issuers, ())
|
||||
self.assertEqual(msg.authorized_for, ())
|
||||
|
||||
self.assertEqual(self.jmsg_to, msg.to_json())
|
||||
self.assertEqual(self.jmsg_to, msg.to_partial_json())
|
||||
|
||||
|
||||
class ProofOfPossessionTest(unittest.TestCase):
|
||||
|
|
@ -313,19 +351,23 @@ class ProofOfPossessionTest(unittest.TestCase):
|
|||
}
|
||||
self.jmsg_from = {
|
||||
'type': 'proofOfPossession',
|
||||
'alg': jose.RS256.fully_serialize(),
|
||||
'alg': jose.RS256.to_json(),
|
||||
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
|
||||
'hints': hints.fully_serialize(),
|
||||
'hints': hints.to_json(),
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg_to, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import ProofOfPossession
|
||||
self.assertEqual(
|
||||
self.msg, ProofOfPossession.from_json(self.jmsg_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import ProofOfPossession
|
||||
hash(ProofOfPossession.from_json(self.jmsg_from))
|
||||
|
||||
|
||||
class ProofOfPossessionResponseTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -355,20 +397,24 @@ class ProofOfPossessionResponseTest(unittest.TestCase):
|
|||
self.jmsg_from = {
|
||||
'type': 'proofOfPossession',
|
||||
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
|
||||
'signature': signature.fully_serialize(),
|
||||
'signature': signature.to_json(),
|
||||
}
|
||||
|
||||
def test_verify(self):
|
||||
self.assertTrue(self.msg.verify())
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg_to, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import ProofOfPossessionResponse
|
||||
self.assertEqual(
|
||||
self.msg, ProofOfPossessionResponse.from_json(self.jmsg_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import ProofOfPossessionResponse
|
||||
hash(ProofOfPossessionResponse.from_json(self.jmsg_from))
|
||||
|
||||
|
||||
class DNSTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -377,13 +423,17 @@ class DNSTest(unittest.TestCase):
|
|||
self.msg = DNS(token='17817c66b60ce2e4012dfad92657527a')
|
||||
self.jmsg = {'type': 'dns', 'token': '17817c66b60ce2e4012dfad92657527a'}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import DNS
|
||||
self.assertEqual(self.msg, DNS.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import DNS
|
||||
hash(DNS.from_json(self.jmsg))
|
||||
|
||||
|
||||
class DNSResponseTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -392,13 +442,17 @@ class DNSResponseTest(unittest.TestCase):
|
|||
self.msg = DNSResponse()
|
||||
self.jmsg = {'type': 'dns'}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.challenges import DNSResponse
|
||||
self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.challenges import DNSResponse
|
||||
hash(DNSResponse.from_json(self.jmsg))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
25
letsencrypt/acme/fields.py
Normal file
25
letsencrypt/acme/fields.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""ACME JSON fields."""
|
||||
import pyrfc3339
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class RFC3339Field(jose.Field):
|
||||
"""RFC3339 field encoder/decoder.
|
||||
|
||||
Handles decoding/encoding between RFC3339 strings and aware (not
|
||||
naive) `datetime.datetime` objects
|
||||
(e.g. ``datetime.datetime.now(pytz.utc)``).
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def default_encoder(cls, value):
|
||||
return pyrfc3339.generate(value)
|
||||
|
||||
@classmethod
|
||||
def default_decoder(cls, value):
|
||||
try:
|
||||
return pyrfc3339.parse(value)
|
||||
except ValueError as error:
|
||||
raise jose.DeserializationError(error)
|
||||
35
letsencrypt/acme/fields_test.py
Normal file
35
letsencrypt/acme/fields_test.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""Tests for letsencrypt.acme.fields."""
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import pytz
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class RFC3339FieldTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.fields.RFC3339Field."""
|
||||
|
||||
def setUp(self):
|
||||
self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.utc)
|
||||
self.encoded = '2015-03-27T00:00:00Z'
|
||||
|
||||
def test_default_encoder(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertEqual(
|
||||
self.encoded, RFC3339Field.default_encoder(self.decoded))
|
||||
|
||||
def test_default_encoder_naive_fails(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertRaises(
|
||||
ValueError, RFC3339Field.default_encoder, datetime.datetime.now())
|
||||
|
||||
def test_default_decoder(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertEqual(
|
||||
self.decoded, RFC3339Field.default_decoder(self.encoded))
|
||||
|
||||
def test_default_decoder_raises_deserialization_error(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, RFC3339Field.default_decoder, '')
|
||||
|
|
@ -70,5 +70,6 @@ from letsencrypt.acme.jose.jws import JWS
|
|||
|
||||
from letsencrypt.acme.jose.util import (
|
||||
ComparableX509,
|
||||
HashableRSAKey,
|
||||
ImmutableMap,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ class JSONDeSerializable(object):
|
|||
Turning an arbitrary Python object into Python object that can
|
||||
be encoded into a JSON document. **Full serialization** produces
|
||||
a Python object composed of only basic types as required by the
|
||||
:ref:`conversion table <conversion-table>`.
|
||||
**Partial serialization** (acomplished by :meth:`to_json`)
|
||||
:ref:`conversion table <conversion-table>`. **Partial
|
||||
serialization** (acomplished by :meth:`to_partial_json`)
|
||||
produces a Python object that might also be built from other
|
||||
:class:`JSONDeSerializable` objects.
|
||||
|
||||
|
|
@ -71,15 +71,16 @@ class JSONDeSerializable(object):
|
|||
|
||||
Interestingly, ``default`` is required to perform only partial
|
||||
serialization, as :func:`json.dumps` applies ``default``
|
||||
recursively. This is the idea behind making :meth:`to_json` produce
|
||||
only partial serialization, while providing custom :meth:`json_dumps`
|
||||
that dumps with ``default`` set to :meth:`json_dump_default`.
|
||||
recursively. This is the idea behind making :meth:`to_partial_json`
|
||||
produce only partial serialization, while providing custom
|
||||
:meth:`json_dumps` that dumps with ``default`` set to
|
||||
:meth:`json_dump_default`.
|
||||
|
||||
To make further documentation a bit more concrete, please, consider
|
||||
the following imaginatory implementation example::
|
||||
|
||||
class Foo(JSONDeSerializable):
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
return 'foo'
|
||||
|
||||
@classmethod
|
||||
|
|
@ -87,7 +88,7 @@ class JSONDeSerializable(object):
|
|||
return Foo()
|
||||
|
||||
class Bar(JSONDeSerializable):
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
return [Foo(), Foo()]
|
||||
|
||||
@classmethod
|
||||
|
|
@ -98,16 +99,16 @@ class JSONDeSerializable(object):
|
|||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def to_json(self): # pragma: no cover
|
||||
def to_partial_json(self): # pragma: no cover
|
||||
"""Partially serialize.
|
||||
|
||||
Following the example, **partial serialization** means the following::
|
||||
|
||||
assert isinstance(Bar().to_json()[0], Foo)
|
||||
assert isinstance(Bar().to_json()[1], Foo)
|
||||
assert isinstance(Bar().to_partial_json()[0], Foo)
|
||||
assert isinstance(Bar().to_partial_json()[1], Foo)
|
||||
|
||||
# in particular...
|
||||
assert Bar().to_json() != ['foo', 'foo']
|
||||
assert Bar().to_partial_json() != ['foo', 'foo']
|
||||
|
||||
:raises letsencrypt.acme.jose.errors.SerializationError:
|
||||
in case of any serialization error.
|
||||
|
|
@ -116,31 +117,37 @@ class JSONDeSerializable(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def fully_serialize(self):
|
||||
def to_json(self):
|
||||
"""Fully serialize.
|
||||
|
||||
Again, following the example from before, **full serialization**
|
||||
means the following::
|
||||
|
||||
assert Bar().fully_serialize() == ['foo', 'foo']
|
||||
assert Bar().to_json() == ['foo', 'foo']
|
||||
|
||||
:raises letsencrypt.acme.jose.errors.SerializationError:
|
||||
in case of any serialization error.
|
||||
:returns: Fully serialized object.
|
||||
|
||||
"""
|
||||
partial = self.to_json()
|
||||
try_serialize = (lambda x: x.fully_serialize()
|
||||
if isinstance(x, JSONDeSerializable) else x)
|
||||
if isinstance(partial, basestring): # strings are sequences
|
||||
return partial
|
||||
if isinstance(partial, collections.Sequence):
|
||||
return [try_serialize(elem) for elem in partial]
|
||||
elif isinstance(partial, collections.Mapping):
|
||||
return dict([(try_serialize(key), try_serialize(value))
|
||||
for key, value in partial.iteritems()])
|
||||
else:
|
||||
return partial
|
||||
def _serialize(obj):
|
||||
if isinstance(obj, JSONDeSerializable):
|
||||
return _serialize(obj.to_partial_json())
|
||||
if isinstance(obj, basestring): # strings are sequence
|
||||
return obj
|
||||
elif isinstance(obj, list):
|
||||
return [_serialize(subobj) for subobj in obj]
|
||||
elif isinstance(obj, collections.Sequence):
|
||||
# default to tuple, otherwise Mapping could get
|
||||
# unhashable list
|
||||
return tuple(_serialize(subobj) for subobj in obj)
|
||||
elif isinstance(obj, collections.Mapping):
|
||||
return dict((_serialize(key), _serialize(value))
|
||||
for key, value in obj.iteritems())
|
||||
else:
|
||||
return obj
|
||||
|
||||
return _serialize(self)
|
||||
|
||||
@util.abstractclassmethod
|
||||
def from_json(cls, unused_jobj):
|
||||
|
|
@ -157,7 +164,7 @@ class JSONDeSerializable(object):
|
|||
|
||||
"""
|
||||
# TypeError: Can't instantiate abstract class <cls> with
|
||||
# abstract methods from_json, to_json
|
||||
# abstract methods from_json, to_partial_json
|
||||
return cls() # pylint: disable=abstract-class-instantiated
|
||||
|
||||
@classmethod
|
||||
|
|
@ -193,6 +200,6 @@ class JSONDeSerializable(object):
|
|||
|
||||
"""
|
||||
if isinstance(python_object, JSONDeSerializable):
|
||||
return python_object.to_json()
|
||||
return python_object.to_partial_json()
|
||||
else: # this branch is necessary, cannot just "return"
|
||||
raise TypeError(repr(python_object) + ' is not JSON serializable')
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import unittest
|
|||
|
||||
|
||||
class JSONDeSerializableTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.jose.interfaces import JSONDeSerializable
|
||||
|
|
@ -13,7 +14,7 @@ class JSONDeSerializableTest(unittest.TestCase):
|
|||
def __init__(self, v):
|
||||
self.v = v
|
||||
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
return self.v
|
||||
|
||||
@classmethod
|
||||
|
|
@ -25,7 +26,7 @@ class JSONDeSerializableTest(unittest.TestCase):
|
|||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
return [self.x, self.y]
|
||||
|
||||
@classmethod
|
||||
|
|
@ -38,7 +39,7 @@ class JSONDeSerializableTest(unittest.TestCase):
|
|||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
return {self.x: self.y}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -50,21 +51,29 @@ class JSONDeSerializableTest(unittest.TestCase):
|
|||
self.basic2 = Basic('foo2')
|
||||
self.seq = Sequence(self.basic1, self.basic2)
|
||||
self.mapping = Mapping(self.basic1, self.basic2)
|
||||
self.nested = Basic([[self.basic1]])
|
||||
self.tuple = Basic(('foo',))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
self.Basic = Basic
|
||||
self.Sequence = Sequence
|
||||
self.Mapping = Mapping
|
||||
|
||||
def test_fully_serialize_sequence(self):
|
||||
self.assertEqual(self.seq.fully_serialize(), ['foo1', 'foo2'])
|
||||
def test_to_json_sequence(self):
|
||||
self.assertEqual(self.seq.to_json(), ['foo1', 'foo2'])
|
||||
|
||||
def test_fully_serialize_mapping(self):
|
||||
self.assertEqual(self.mapping.fully_serialize(), {'foo1': 'foo2'})
|
||||
def test_to_json_mapping(self):
|
||||
self.assertEqual(self.mapping.to_json(), {'foo1': 'foo2'})
|
||||
|
||||
def test_fully_serialize_other(self):
|
||||
def test_to_json_other(self):
|
||||
mock_value = object()
|
||||
self.assertTrue(self.Basic(mock_value).fully_serialize() is mock_value)
|
||||
self.assertTrue(self.Basic(mock_value).to_json() is mock_value)
|
||||
|
||||
def test_to_json_nested(self):
|
||||
self.assertEqual(self.nested.to_json(), [['foo1']])
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.tuple.to_json(), (('foo', )))
|
||||
|
||||
def test_from_json_not_implemented(self):
|
||||
from letsencrypt.acme.jose.interfaces import JSONDeSerializable
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class Field(object):
|
|||
@classmethod
|
||||
def default_encoder(cls, value):
|
||||
"""Default (passthrough) encoder."""
|
||||
# field.to_json() is no good as encoder has to do partial
|
||||
# field.to_partial_json() is no good as encoder has to do partial
|
||||
# serialization only
|
||||
return value
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
|||
raise errors.DeserializationError('No bar suffix!')
|
||||
return value[:-3]
|
||||
|
||||
assert Foo(bar='baz').to_json() == {'Bar': 'bazbar'}
|
||||
assert Foo(bar='baz').to_partial_json() == {'Bar': 'bazbar'}
|
||||
assert Foo.from_json({'Bar': 'bazbar'}) == Foo(bar='baz')
|
||||
assert (Foo.from_json({'Bar': 'bazbar', 'Empty': '!'})
|
||||
== Foo(bar='baz', empty='!'))
|
||||
|
|
@ -209,7 +209,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
|||
super(JSONObjectWithFields, self).__init__(
|
||||
**(dict(self._defaults(), **kwargs)))
|
||||
|
||||
def fields_to_json(self):
|
||||
def fields_to_partial_json(self):
|
||||
"""Serialize fields to JSON."""
|
||||
jobj = {}
|
||||
for slot, field in self._fields.iteritems():
|
||||
|
|
@ -226,8 +226,8 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
|||
slot, value, error))
|
||||
return jobj
|
||||
|
||||
def to_json(self):
|
||||
return self.fields_to_json()
|
||||
def to_partial_json(self):
|
||||
return self.fields_to_partial_json()
|
||||
|
||||
@classmethod
|
||||
def _check_required(cls, jobj):
|
||||
|
|
@ -378,7 +378,7 @@ class TypedJSONObjectWithFields(JSONObjectWithFields):
|
|||
|
||||
return type_cls
|
||||
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
"""Get JSON serializable object.
|
||||
|
||||
:returns: Serializable JSON object representing ACME typed object.
|
||||
|
|
@ -387,7 +387,7 @@ class TypedJSONObjectWithFields(JSONObjectWithFields):
|
|||
:rtype: dict
|
||||
|
||||
"""
|
||||
jobj = self.fields_to_json()
|
||||
jobj = self.fields_to_partial_json()
|
||||
jobj[self.type_field_name] = self.typ
|
||||
return jobj
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class FieldTest(unittest.TestCase):
|
|||
def test_default_encoder_is_partial(self):
|
||||
class MockField(interfaces.JSONDeSerializable):
|
||||
# pylint: disable=missing-docstring
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
return 'foo'
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
|
|
@ -113,8 +113,8 @@ class JSONObjectWithFieldsTest(unittest.TestCase):
|
|||
def test_init_defaults(self):
|
||||
self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3))
|
||||
|
||||
def test_fields_to_json_omits_empty(self):
|
||||
self.assertEqual(self.mock.fields_to_json(), {'y': 2, 'Z': 3})
|
||||
def test_fields_to_partial_json_omits_empty(self):
|
||||
self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3})
|
||||
|
||||
def test_fields_from_json_fills_default_for_empty(self):
|
||||
self.assertEqual(
|
||||
|
|
@ -135,9 +135,10 @@ class JSONObjectWithFieldsTest(unittest.TestCase):
|
|||
errors.DeserializationError,
|
||||
self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'Z': 0})
|
||||
|
||||
def test_fields_to_json_encoder(self):
|
||||
self.assertEqual(self.MockJSONObjectWithFields(x=1, y=2, z=3).to_json(),
|
||||
{'x': 2, 'y': 2, 'Z': 3})
|
||||
def test_fields_to_partial_json_encoder(self):
|
||||
self.assertEqual(
|
||||
self.MockJSONObjectWithFields(x=1, y=2, z=3).to_partial_json(),
|
||||
{'x': 2, 'y': 2, 'Z': 3})
|
||||
|
||||
def test_fields_from_json_decoder(self):
|
||||
self.assertEqual(
|
||||
|
|
@ -145,10 +146,10 @@ class JSONObjectWithFieldsTest(unittest.TestCase):
|
|||
self.MockJSONObjectWithFields.fields_from_json(
|
||||
{'x': 4, 'y': 2, 'Z': 3}))
|
||||
|
||||
def test_fields_to_json_error_passthrough(self):
|
||||
def test_fields_to_partial_json_error_passthrough(self):
|
||||
self.assertRaises(
|
||||
errors.SerializationError, self.MockJSONObjectWithFields(
|
||||
x=1, y=500, z=3).to_json)
|
||||
x=1, y=500, z=3).to_partial_json)
|
||||
|
||||
def test_fields_from_json_error_passthrough(self):
|
||||
self.assertRaises(
|
||||
|
|
@ -262,14 +263,14 @@ class TypedJSONObjectWithFieldsTest(unittest.TestCase):
|
|||
def fields_from_json(cls, jobj):
|
||||
return {'foo': jobj['foo']}
|
||||
|
||||
def fields_to_json(self):
|
||||
def fields_to_partial_json(self):
|
||||
return {'foo': self.foo}
|
||||
|
||||
self.parent_cls = MockParentTypedJSONObjectWithFields
|
||||
self.msg = MockTypedJSONObjectWithFields(foo='bar')
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), {
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), {
|
||||
'type': 'test',
|
||||
'foo': 'bar',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class JWASignature(JWA):
|
|||
cls.SIGNATURES[signature_cls.name] = signature_cls
|
||||
return signature_cls
|
||||
|
||||
def to_json(self):
|
||||
def to_partial_json(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ class JWASignatureTest(unittest.TestCase):
|
|||
self.assertEqual('Sig1', repr(self.Sig1))
|
||||
self.assertEqual('Sig2', repr(self.Sig2))
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.Sig1.to_json(), 'Sig1')
|
||||
self.assertEqual(self.Sig2.to_json(), 'Sig2')
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.Sig1.to_partial_json(), 'Sig1')
|
||||
self.assertEqual(self.Sig2.to_partial_json(), 'Sig2')
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.jose.jwa import JWASignature
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class JWKES(JWK): # pragma: no cover
|
|||
"""
|
||||
typ = 'ES'
|
||||
|
||||
def fields_to_json(self):
|
||||
def fields_to_partial_json(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
|
|
@ -62,7 +62,7 @@ class JWKOct(JWK):
|
|||
typ = 'oct'
|
||||
__slots__ = ('key',)
|
||||
|
||||
def fields_to_json(self):
|
||||
def fields_to_partial_json(self):
|
||||
# TODO: An "alg" member SHOULD also be present to identify the
|
||||
# algorithm intended to be used with the key, unless the
|
||||
# application uses another means or convention to determine
|
||||
|
|
@ -83,7 +83,11 @@ class JWKOct(JWK):
|
|||
|
||||
@JWK.register
|
||||
class JWKRSA(JWK):
|
||||
"""RSA JWK."""
|
||||
"""RSA JWK.
|
||||
|
||||
:ivar key: `Crypto.PublicKey.RSA` wrapped in `.HashableRSAKey`
|
||||
|
||||
"""
|
||||
typ = 'RSA'
|
||||
__slots__ = ('key',)
|
||||
|
||||
|
|
@ -114,18 +118,20 @@ class JWKRSA(JWK):
|
|||
:rtype: :class:`JWKRSA`
|
||||
|
||||
"""
|
||||
return cls(key=Crypto.PublicKey.RSA.importKey(string))
|
||||
return cls(key=util.HashableRSAKey(
|
||||
Crypto.PublicKey.RSA.importKey(string)))
|
||||
|
||||
def public(self):
|
||||
return type(self)(key=self.key.publickey())
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
return cls(key=Crypto.PublicKey.RSA.construct(
|
||||
(cls._decode_param(jobj['n']),
|
||||
cls._decode_param(jobj['e']))))
|
||||
return cls(key=util.HashableRSAKey(
|
||||
Crypto.PublicKey.RSA.construct(
|
||||
(cls._decode_param(jobj['n']),
|
||||
cls._decode_param(jobj['e'])))))
|
||||
|
||||
def fields_to_json(self):
|
||||
def fields_to_partial_json(self):
|
||||
return {
|
||||
'n': self._encode_param(self.key.n),
|
||||
'e': self._encode_param(self.key.e),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import unittest
|
|||
from Crypto.PublicKey import RSA
|
||||
|
||||
from letsencrypt.acme.jose import errors
|
||||
from letsencrypt.acme.jose import util
|
||||
|
||||
|
||||
RSA256_KEY = RSA.importKey(pkg_resources.resource_string(
|
||||
|
|
@ -22,13 +23,17 @@ class JWKOctTest(unittest.TestCase):
|
|||
self.jwk = JWKOct(key='foo')
|
||||
self.jobj = {'kty': 'oct', 'k': 'foo'}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jwk.to_json(), self.jobj)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jwk.to_partial_json(), self.jobj)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.jose.jwk import JWKOct
|
||||
self.assertEqual(self.jwk, JWKOct.from_json(self.jobj))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.jose.jwk import JWKOct
|
||||
hash(JWKOct.from_json(self.jobj))
|
||||
|
||||
def test_load(self):
|
||||
from letsencrypt.acme.jose.jwk import JWKOct
|
||||
self.assertEqual(self.jwk, JWKOct.load('foo'))
|
||||
|
|
@ -42,15 +47,15 @@ class JWKRSATest(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.jose.jwk import JWKRSA
|
||||
self.jwk256 = JWKRSA(key=RSA256_KEY.publickey())
|
||||
self.jwk256_private = JWKRSA(key=RSA256_KEY)
|
||||
self.jwk256 = JWKRSA(key=util.HashableRSAKey(RSA256_KEY.publickey()))
|
||||
self.jwk256_private = JWKRSA(key=util.HashableRSAKey(RSA256_KEY))
|
||||
self.jwk256json = {
|
||||
'kty': 'RSA',
|
||||
'e': 'AQAB',
|
||||
'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5'
|
||||
'80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q',
|
||||
}
|
||||
self.jwk512 = JWKRSA(key=RSA512_KEY.publickey())
|
||||
self.jwk512 = JWKRSA(key=util.HashableRSAKey(RSA512_KEY.publickey()))
|
||||
self.jwk512json = {
|
||||
'kty': 'RSA',
|
||||
'e': 'AQAB',
|
||||
|
|
@ -68,17 +73,18 @@ class JWKRSATest(unittest.TestCase):
|
|||
|
||||
def test_load(self):
|
||||
from letsencrypt.acme.jose.jwk import JWKRSA
|
||||
self.assertEqual(JWKRSA(key=RSA256_KEY), JWKRSA.load(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests',
|
||||
os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
self.assertEqual(
|
||||
JWKRSA(key=util.HashableRSAKey(RSA256_KEY)), JWKRSA.load(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests',
|
||||
os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
|
||||
def test_public(self):
|
||||
self.assertEqual(self.jwk256, self.jwk256_private.public())
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jwk256.to_json(), self.jwk256json)
|
||||
self.assertEqual(self.jwk512.to_json(), self.jwk512json)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jwk256.to_partial_json(), self.jwk256json)
|
||||
self.assertEqual(self.jwk512.to_partial_json(), self.jwk512json)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.jose.jwk import JWK
|
||||
|
|
@ -86,6 +92,10 @@ class JWKRSATest(unittest.TestCase):
|
|||
# TODO: fix schemata to allow RSA512
|
||||
#self.assertEqual(self.jwk512, JWK.from_json(self.jwk512json))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.jose.jwk import JWK
|
||||
hash(JWK.from_json(self.jwk256json))
|
||||
|
||||
def test_from_json_non_schema_errors(self):
|
||||
# valid against schema, but still failing
|
||||
from letsencrypt.acme.jose.jwk import JWK
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class Header(json_util.JSONObjectWithFields):
|
|||
Parameter Names (as defined in section 4.1 of the
|
||||
protocol). If you need Public Header Parameter Names (4.2)
|
||||
or Private Header Parameter Names (4.3), you must subclass
|
||||
and override :meth:`from_json` and :meth:`to_json`
|
||||
and override :meth:`from_json` and :meth:`to_partial_json`
|
||||
appropriately.
|
||||
|
||||
.. warning:: This class does not support any extensions through
|
||||
|
|
@ -223,8 +223,8 @@ class Signature(json_util.JSONObjectWithFields):
|
|||
|
||||
return cls(protected=protected, header=header, signature=signature)
|
||||
|
||||
def fields_to_json(self):
|
||||
fields = super(Signature, self).fields_to_json()
|
||||
def fields_to_partial_json(self):
|
||||
fields = super(Signature, self).fields_to_partial_json()
|
||||
if not fields['header'].not_omitted():
|
||||
del fields['header']
|
||||
return fields
|
||||
|
|
@ -294,12 +294,12 @@ class JWS(json_util.JSONObjectWithFields):
|
|||
signature=json_util.decode_b64jose(signature))
|
||||
return cls(payload=json_util.decode_b64jose(payload), signatures=(sig,))
|
||||
|
||||
def to_json(self, flat=True): # pylint: disable=arguments-differ
|
||||
def to_partial_json(self, flat=True): # pylint: disable=arguments-differ
|
||||
assert self.signatures
|
||||
payload = b64.b64encode(self.payload)
|
||||
|
||||
if flat and len(self.signatures) == 1:
|
||||
ret = self.signatures[0].to_json()
|
||||
ret = self.signatures[0].to_partial_json()
|
||||
ret['payload'] = payload
|
||||
return ret
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class HeaderTest(unittest.TestCase):
|
|||
def test_x5c_decoding(self):
|
||||
from letsencrypt.acme.jose.jws import Header
|
||||
header = Header(x5c=(CERT, CERT))
|
||||
jobj = header.to_json()
|
||||
jobj = header.to_partial_json()
|
||||
cert_b64 = base64.b64encode(CERT.as_der())
|
||||
self.assertEqual(jobj, {'x5c': [cert_b64, cert_b64]})
|
||||
self.assertEqual(header, Header.from_json(jobj))
|
||||
|
|
@ -152,14 +152,13 @@ class JWSTest(unittest.TestCase):
|
|||
self.assertRaises(errors.DeserializationError, JWS.from_compact, '.')
|
||||
|
||||
def test_json_omitempty(self):
|
||||
protected_jobj = self.protected.to_json(flat=True)
|
||||
unprotected_jobj = self.unprotected.to_json(flat=True)
|
||||
protected_jobj = self.protected.to_partial_json(flat=True)
|
||||
unprotected_jobj = self.unprotected.to_partial_json(flat=True)
|
||||
|
||||
self.assertTrue('protected' not in unprotected_jobj)
|
||||
self.assertTrue('header' not in protected_jobj)
|
||||
|
||||
unprotected_jobj['header'] = unprotected_jobj[
|
||||
'header'].fully_serialize()
|
||||
unprotected_jobj['header'] = unprotected_jobj['header'].to_json()
|
||||
|
||||
from letsencrypt.acme.jose.jws import JWS
|
||||
self.assertEqual(JWS.from_json(protected_jobj), self.protected)
|
||||
|
|
@ -173,9 +172,9 @@ class JWSTest(unittest.TestCase):
|
|||
'protected': b64.b64encode(self.mixed.signature.protected),
|
||||
}
|
||||
jobj_from = jobj_to.copy()
|
||||
jobj_from['header'] = jobj_from['header'].fully_serialize()
|
||||
jobj_from['header'] = jobj_from['header'].to_json()
|
||||
|
||||
self.assertEqual(self.mixed.to_json(flat=True), jobj_to)
|
||||
self.assertEqual(self.mixed.to_partial_json(flat=True), jobj_to)
|
||||
from letsencrypt.acme.jose.jws import JWS
|
||||
self.assertEqual(self.mixed, JWS.from_json(jobj_from))
|
||||
|
||||
|
|
@ -185,9 +184,9 @@ class JWSTest(unittest.TestCase):
|
|||
'payload': b64.b64encode('foo'),
|
||||
}
|
||||
jobj_from = jobj_to.copy()
|
||||
jobj_from['signatures'] = [jobj_to['signatures'][0].fully_serialize()]
|
||||
jobj_from['signatures'] = [jobj_to['signatures'][0].to_json()]
|
||||
|
||||
self.assertEqual(self.mixed.to_json(flat=False), jobj_to)
|
||||
self.assertEqual(self.mixed.to_partial_json(flat=False), jobj_to)
|
||||
from letsencrypt.acme.jose.jws import JWS
|
||||
self.assertEqual(self.mixed, JWS.from_json(jobj_from))
|
||||
|
||||
|
|
@ -196,6 +195,10 @@ class JWSTest(unittest.TestCase):
|
|||
self.assertRaises(errors.DeserializationError, JWS.from_json,
|
||||
{'signatures': (), 'signature': 'foo'})
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.jose.jws import JWS
|
||||
hash(JWS.from_json(self.mixed.to_json()))
|
||||
|
||||
|
||||
class CLITest(unittest.TestCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,26 @@ class ComparableX509(object): # pylint: disable=too-few-public-methods
|
|||
return self.as_der() == other.as_der()
|
||||
|
||||
|
||||
class HashableRSAKey(object): # pylint: disable=too-few-public-methods
|
||||
"""Wrapper for `Crypto.PublicKey.RSA` objects that supports hashing."""
|
||||
|
||||
def __init__(self, wrapped):
|
||||
self._wrapped = wrapped
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._wrapped, name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._wrapped == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((type(self), self.exportKey(format='DER')))
|
||||
|
||||
def publickey(self):
|
||||
"""Get wrapped public key."""
|
||||
return type(self)(self._wrapped.publickey())
|
||||
|
||||
|
||||
class ImmutableMap(collections.Mapping, collections.Hashable):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Immutable key to value mapping with attribute access."""
|
||||
|
|
@ -57,6 +77,12 @@ class ImmutableMap(collections.Mapping, collections.Hashable):
|
|||
for slot in self.__slots__:
|
||||
object.__setattr__(self, slot, kwargs.pop(slot))
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Return updated map."""
|
||||
items = dict(self)
|
||||
items.update(kwargs)
|
||||
return type(self)(**items) # pylint: disable=star-args
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return getattr(self, key)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,36 @@
|
|||
"""Tests for letsencrypt.acme.jose.util."""
|
||||
import functools
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import Crypto.PublicKey.RSA
|
||||
|
||||
|
||||
class HashableRSAKeyTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.jose.util.HashableRSAKey."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.jose.util import HashableRSAKey
|
||||
self.key = HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
self.key_same = HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
|
||||
def test_eq(self):
|
||||
# if __eq__ is not defined, then two HashableRSAKeys with same
|
||||
# _wrapped do not equate
|
||||
self.assertEqual(self.key, self.key_same)
|
||||
|
||||
def test_hash(self):
|
||||
self.assertTrue(isinstance(hash(self.key), int))
|
||||
|
||||
def test_publickey(self):
|
||||
from letsencrypt.acme.jose.util import HashableRSAKey
|
||||
self.assertTrue(isinstance(self.key.publickey(), HashableRSAKey))
|
||||
|
||||
|
||||
class ImmutableMapTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.jose.util.ImmutableMap."""
|
||||
|
|
@ -25,6 +54,10 @@ class ImmutableMapTest(unittest.TestCase):
|
|||
self.a2 = self.A(x=3, y=4)
|
||||
self.b = self.B(x=1, y=2)
|
||||
|
||||
def test_update(self):
|
||||
self.assertEqual(self.A(x=2, y=2), self.a1.update(x=2))
|
||||
self.assertEqual(self.a2, self.a1.update(x=3, y=4))
|
||||
|
||||
def test_get_missing_item_raises_key_error(self):
|
||||
self.assertRaises(KeyError, self.a1.__getitem__, 'z')
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from letsencrypt.acme import util
|
|||
|
||||
|
||||
class Message(jose.TypedJSONObjectWithFields):
|
||||
# _fields_to_json | pylint: disable=abstract-method
|
||||
# _fields_to_partial_json | pylint: disable=abstract-method
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""ACME message."""
|
||||
TYPES = {}
|
||||
|
|
|
|||
298
letsencrypt/acme/messages2.py
Normal file
298
letsencrypt/acme/messages2.py
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
"""ACME protocol v02 messages."""
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import fields
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class Error(jose.JSONObjectWithFields, Exception):
|
||||
"""ACME error.
|
||||
|
||||
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||
|
||||
"""
|
||||
|
||||
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
|
||||
ERROR_TYPE_DESCRIPTIONS = {
|
||||
'malformed': 'The request message was malformed',
|
||||
'unauthorized': 'The client lacks sufficient authorization',
|
||||
'serverInternal': 'The server experienced an internal error',
|
||||
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
|
||||
}
|
||||
|
||||
# TODO: Boulder omits 'type' and 'instance', spec requires
|
||||
typ = jose.Field('type', omitempty=True)
|
||||
title = jose.Field('title', omitempty=True)
|
||||
detail = jose.Field('detail')
|
||||
instance = jose.Field('instance', omitempty=True)
|
||||
|
||||
@typ.encoder
|
||||
def typ(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return Error.ERROR_TYPE_NAMESPACE + value
|
||||
|
||||
@typ.decoder
|
||||
def typ(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
# pylint thinks isinstance(value, Error), so startswith is not found
|
||||
# pylint: disable=no-member
|
||||
if not value.startswith(Error.ERROR_TYPE_NAMESPACE):
|
||||
raise jose.DeserializationError('Missing error type prefix')
|
||||
|
||||
without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):]
|
||||
if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS:
|
||||
raise jose.DeserializationError('Error type not recognized')
|
||||
|
||||
return without_prefix
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Hardcoded error description based on its type."""
|
||||
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
|
||||
|
||||
|
||||
class _Constant(jose.JSONDeSerializable):
|
||||
"""ACME constant."""
|
||||
__slots__ = ('name',)
|
||||
POSSIBLE_NAMES = NotImplemented
|
||||
|
||||
def __init__(self, name):
|
||||
self.POSSIBLE_NAMES[name] = self
|
||||
self.name = name
|
||||
|
||||
def to_partial_json(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, value):
|
||||
if value not in cls.POSSIBLE_NAMES:
|
||||
raise jose.DeserializationError(
|
||||
'{0} not recognized'.format(cls.__name__))
|
||||
return cls.POSSIBLE_NAMES[value]
|
||||
|
||||
def __repr__(self):
|
||||
return '{0}({1})'.format(self.__class__.__name__, self.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, type(self)) and other.name == self.name
|
||||
|
||||
|
||||
class Status(_Constant):
|
||||
"""ACME "status" field."""
|
||||
POSSIBLE_NAMES = {}
|
||||
STATUS_UNKNOWN = Status('unknown')
|
||||
STATUS_PENDING = Status('pending')
|
||||
STATUS_PROCESSING = Status('processing')
|
||||
STATUS_VALID = Status('valid')
|
||||
STATUS_INVALID = Status('invalid')
|
||||
STATUS_REVOKED = Status('revoked')
|
||||
|
||||
|
||||
class IdentifierType(_Constant):
|
||||
"""ACME identifier type."""
|
||||
POSSIBLE_NAMES = {}
|
||||
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
|
||||
|
||||
|
||||
class Identifier(jose.JSONObjectWithFields):
|
||||
"""ACME identifier.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.IdentifierType typ:
|
||||
|
||||
"""
|
||||
typ = jose.Field('type', decoder=IdentifierType.from_json)
|
||||
value = jose.Field('value')
|
||||
|
||||
|
||||
class Resource(jose.ImmutableMap):
|
||||
"""ACME Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.ResourceBody body: Resource body.
|
||||
:ivar str uri: Location of the resource.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri')
|
||||
|
||||
|
||||
class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body."""
|
||||
|
||||
|
||||
class RegistrationResource(Resource):
|
||||
"""Registration Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Registration body:
|
||||
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
|
||||
:ivar str terms_of_service: URL for the CA TOS.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service')
|
||||
|
||||
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact:
|
||||
|
||||
"""
|
||||
|
||||
# on new-reg key server ignores 'key' and populates it based on
|
||||
# JWS.signature.combined.jwk
|
||||
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
contact = jose.Field('contact', omitempty=True, default=())
|
||||
recovery_token = jose.Field('recoveryToken', omitempty=True)
|
||||
agreement = jose.Field('agreement', omitempty=True)
|
||||
|
||||
|
||||
class ChallengeResource(Resource, jose.JSONObjectWithFields):
|
||||
"""Challenge Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.ChallengeBody body:
|
||||
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'authzr_uri')
|
||||
|
||||
@property
|
||||
def uri(self): # pylint: disable=missing-docstring,no-self-argument
|
||||
# bug? 'method already defined line None'
|
||||
# pylint: disable=function-redefined
|
||||
return self.body.uri
|
||||
|
||||
|
||||
class ChallengeBody(ResourceBody):
|
||||
"""Challenge Resource Body.
|
||||
|
||||
.. todo::
|
||||
Confusingly, this has a similar name to `.challenges.Challenge`,
|
||||
as well as `.achallenges.AnnotatedChallenge` or
|
||||
`.achallenges.Indexed`... Once `messages2` and `network2` is
|
||||
integrated with the rest of the client, this class functionality
|
||||
will be merged with `.challenges.Challenge`. Meanwhile,
|
||||
separation allows the ``master`` to be still interoperable with
|
||||
Node.js server (protocol v00). For the time being use names such
|
||||
as ``challb`` to distinguish instances of this class from
|
||||
``achall`` or ``ichall``.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Status status:
|
||||
:ivar datetime.datetime validated:
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ('chall',)
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
def to_partial_json(self):
|
||||
jobj = super(ChallengeBody, self).to_partial_json()
|
||||
jobj.update(self.chall.to_partial_json())
|
||||
return jobj
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
|
||||
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
|
||||
return jobj_fields
|
||||
|
||||
|
||||
class AuthorizationResource(Resource):
|
||||
"""Authorization Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Authorization body:
|
||||
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'new_cert_uri')
|
||||
|
||||
|
||||
class Authorization(ResourceBody):
|
||||
"""Authorization Resource Body.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Identifier identifier:
|
||||
:ivar list challenges: `list` of `Challenge`
|
||||
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
|
||||
of `int`, as opposed to `list` of `list` from the spec).
|
||||
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact:
|
||||
:ivar letsencrypt.acme.messages2.Status status:
|
||||
:ivar datetime.datetime expires:
|
||||
|
||||
"""
|
||||
|
||||
identifier = jose.Field('identifier', decoder=Identifier.from_json)
|
||||
challenges = jose.Field('challenges', omitempty=True)
|
||||
combinations = jose.Field('combinations', omitempty=True)
|
||||
|
||||
# TODO: acme-spec #92, #98
|
||||
key = Registration._fields['key']
|
||||
contact = Registration._fields['contact']
|
||||
|
||||
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
|
||||
# TODO: 'expires' is allowed for Authorization Resources in
|
||||
# general, but for Key Authorization '[t]he "expires" field MUST
|
||||
# be absent'... then acme-spec gives example with 'expires'
|
||||
# present... That's confusing!
|
||||
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||
|
||||
@challenges.decoder
|
||||
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(ChallengeBody.from_json(chall) for chall in value)
|
||||
|
||||
@property
|
||||
def resolved_combinations(self):
|
||||
"""Combinations with challenges instead of indices."""
|
||||
return tuple(tuple(self.challenges[idx] for idx in combo)
|
||||
for combo in self.combinations)
|
||||
|
||||
|
||||
class CertificateRequest(jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
:ivar letsencrypt.acme.jose.util.ComparableX509 csr:
|
||||
`M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
:ivar tuple authorizations: `tuple` of URIs (`str`)
|
||||
|
||||
"""
|
||||
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
|
||||
authorizations = jose.Field('authorizations', decoder=tuple)
|
||||
|
||||
|
||||
class CertificateResource(Resource):
|
||||
"""Certificate Resource.
|
||||
|
||||
:ivar letsencrypt.acme.jose.util.ComparableX509 body:
|
||||
`M2Crypto.X509.X509` wrapped in `.ComparableX509`
|
||||
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
|
||||
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs')
|
||||
|
||||
|
||||
class Revocation(jose.JSONObjectWithFields):
|
||||
"""Revocation message.
|
||||
|
||||
:ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`.
|
||||
:ivar tuple authorizations: Same as `CertificateRequest.authorizations`
|
||||
|
||||
"""
|
||||
|
||||
NOW = 'now'
|
||||
"""A possible value for `revoke`, denoting that certificate should
|
||||
be revoked now."""
|
||||
|
||||
revoke = jose.Field('revoke')
|
||||
authorizations = CertificateRequest._fields['authorizations']
|
||||
|
||||
@revoke.decoder
|
||||
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
if value == Revocation.NOW:
|
||||
return value
|
||||
else:
|
||||
return fields.RFC3339Field.default_decoder(value)
|
||||
|
||||
@revoke.encoder
|
||||
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
if value == Revocation.NOW:
|
||||
return value
|
||||
else:
|
||||
return fields.RFC3339Field.default_encoder(value)
|
||||
232
letsencrypt/acme/messages2_test.py
Normal file
232
letsencrypt/acme/messages2_test.py
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
"""Tests for letsencrypt.acme.messages2."""
|
||||
import datetime
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import pytz
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class ErrorTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.Error."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import Error
|
||||
self.error = Error(detail='foo', typ='malformed')
|
||||
|
||||
def test_typ_prefix(self):
|
||||
self.assertEqual('malformed', self.error.typ)
|
||||
self.assertEqual(
|
||||
'urn:acme:error:malformed', self.error.to_partial_json()['type'])
|
||||
self.assertEqual(
|
||||
'malformed', self.error.from_json(self.error.to_partial_json()).typ)
|
||||
|
||||
def test_typ_decoder_missing_prefix(self):
|
||||
from letsencrypt.acme.messages2 import Error
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json,
|
||||
{'detail': 'foo', 'type': 'malformed'})
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json,
|
||||
{'detail': 'foo', 'type': 'not valid bare type'})
|
||||
|
||||
def test_typ_decoder_not_recognized(self):
|
||||
from letsencrypt.acme.messages2 import Error
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json,
|
||||
{'detail': 'foo', 'type': 'urn:acme:error:baz'})
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual(
|
||||
'The request message was malformed', self.error.description)
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.messages2 import Error
|
||||
hash(Error.from_json(self.error.to_json()))
|
||||
|
||||
|
||||
class ConstantTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2._Constant."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import _Constant
|
||||
class MockConstant(_Constant): # pylint: disable=missing-docstring
|
||||
POSSIBLE_NAMES = {}
|
||||
|
||||
self.MockConstant = MockConstant # pylint: disable=invalid-name
|
||||
self.const_a = MockConstant('a')
|
||||
self.const_b = MockConstant('b')
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual('a', self.const_a.to_partial_json())
|
||||
self.assertEqual('b', self.const_b.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, self.MockConstant.from_json, 'c')
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
hash(self.MockConstant.from_json('a'))
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual('MockConstant(a)', repr(self.const_a))
|
||||
self.assertEqual('MockConstant(b)', repr(self.const_b))
|
||||
|
||||
|
||||
class RegistrationTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.Registration."""
|
||||
|
||||
def setUp(self):
|
||||
key = jose.jwk.JWKRSA(key=jose.util.HashableRSAKey(
|
||||
RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', os.path.join(
|
||||
'testdata', 'rsa256_key.pem'))).publickey()))
|
||||
contact = ('mailto:letsencrypt-client@letsencrypt.org',)
|
||||
recovery_token = 'XYZ'
|
||||
agreement = 'https://letsencrypt.org/terms'
|
||||
|
||||
from letsencrypt.acme.messages2 import Registration
|
||||
self.reg = Registration(
|
||||
key=key, contact=contact, recovery_token=recovery_token,
|
||||
agreement=agreement)
|
||||
|
||||
self.jobj_to = {
|
||||
'contact': contact,
|
||||
'recoveryToken': recovery_token,
|
||||
'agreement': agreement,
|
||||
'key': key,
|
||||
}
|
||||
self.jobj_from = self.jobj_to.copy()
|
||||
self.jobj_from['key'] = key.to_json()
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jobj_to, self.reg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages2 import Registration
|
||||
self.assertEqual(self.reg, Registration.from_json(self.jobj_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.messages2 import Registration
|
||||
hash(Registration.from_json(self.jobj_from))
|
||||
|
||||
|
||||
class ChallengeResourceTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.ChallengeResource."""
|
||||
|
||||
def test_uri(self):
|
||||
from letsencrypt.acme.messages2 import ChallengeResource
|
||||
self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock(
|
||||
uri='http://challb'), authzr_uri='http://authz').uri)
|
||||
|
||||
|
||||
class ChallengeBodyTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.ChallengeBody."""
|
||||
|
||||
def setUp(self):
|
||||
self.chall = challenges.DNS(token='foo')
|
||||
|
||||
from letsencrypt.acme.messages2 import ChallengeBody
|
||||
from letsencrypt.acme.messages2 import STATUS_VALID
|
||||
self.status = STATUS_VALID
|
||||
self.challb = ChallengeBody(
|
||||
uri='http://challb', chall=self.chall, status=self.status)
|
||||
|
||||
self.jobj_to = {
|
||||
'uri': 'http://challb',
|
||||
'status': self.status,
|
||||
'type': 'dns',
|
||||
'token': 'foo',
|
||||
}
|
||||
self.jobj_from = self.jobj_to.copy()
|
||||
self.jobj_from['status'] = 'valid'
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jobj_to, self.challb.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages2 import ChallengeBody
|
||||
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.messages2 import ChallengeBody
|
||||
hash(ChallengeBody.from_json(self.jobj_from))
|
||||
|
||||
|
||||
class AuthorizationTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.Authorization."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import ChallengeBody
|
||||
from letsencrypt.acme.messages2 import STATUS_VALID
|
||||
self.challbs = (
|
||||
ChallengeBody(
|
||||
uri='http://challb1', status=STATUS_VALID,
|
||||
chall=challenges.SimpleHTTPS(token='IlirfxKKXAsHtmzK29Pj8A')),
|
||||
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
|
||||
chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
|
||||
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
|
||||
chall=challenges.RecoveryToken()),
|
||||
)
|
||||
combinations = ((0, 2), (1, 2))
|
||||
|
||||
from letsencrypt.acme.messages2 import Authorization
|
||||
from letsencrypt.acme.messages2 import Identifier
|
||||
from letsencrypt.acme.messages2 import IDENTIFIER_FQDN
|
||||
identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')
|
||||
self.authz = Authorization(
|
||||
identifier=identifier, combinations=combinations,
|
||||
challenges=self.challbs)
|
||||
|
||||
self.jobj_from = {
|
||||
'identifier': identifier.to_json(),
|
||||
'challenges': [challb.to_json() for challb in self.challbs],
|
||||
'combinations': combinations,
|
||||
}
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages2 import Authorization
|
||||
Authorization.from_json(self.jobj_from)
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.messages2 import Authorization
|
||||
hash(Authorization.from_json(self.jobj_from))
|
||||
|
||||
def test_resolved_combinations(self):
|
||||
self.assertEqual(self.authz.resolved_combinations, (
|
||||
(self.challbs[0], self.challbs[2]),
|
||||
(self.challbs[1], self.challbs[2]),
|
||||
))
|
||||
|
||||
|
||||
class RevocationTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.RevocationTest."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import Revocation
|
||||
self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW)
|
||||
self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime(
|
||||
2015, 3, 27, tzinfo=pytz.utc))
|
||||
self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW}
|
||||
self.jobj_date = {'authorizations': (),
|
||||
'revoke': '2015-03-27T00:00:00Z'}
|
||||
|
||||
def test_revoke_decoder(self):
|
||||
from letsencrypt.acme.messages2 import Revocation
|
||||
self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now))
|
||||
self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date))
|
||||
|
||||
def test_revoke_encoder(self):
|
||||
self.assertEqual(self.jobj_now, self.rev_now.to_partial_json())
|
||||
self.assertEqual(self.jobj_date, self.rev_date.to_partial_json())
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from letsencrypt.acme.messages2 import Revocation
|
||||
hash(Revocation.from_json(self.rev_now.to_json()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -11,8 +11,9 @@ from letsencrypt.acme import jose
|
|||
from letsencrypt.acme import other
|
||||
|
||||
|
||||
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem')))
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
|
||||
pkg_resources.resource_filename(
|
||||
'letsencrypt.client.tests', 'testdata/cert.pem')))
|
||||
|
|
@ -85,7 +86,7 @@ class ChallengeTest(unittest.TestCase):
|
|||
'type': 'challenge',
|
||||
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
|
||||
'challenges': [chall.fully_serialize() for chall in challs],
|
||||
'challenges': [chall.to_json() for chall in challs],
|
||||
'combinations': [[0, 2], [1, 2]], # TODO array tuples
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +102,8 @@ class ChallengeTest(unittest.TestCase):
|
|||
)
|
||||
))
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg_to)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import Challenge
|
||||
|
|
@ -116,7 +117,7 @@ class ChallengeTest(unittest.TestCase):
|
|||
msg = Challenge.from_json(self.jmsg_from)
|
||||
|
||||
self.assertEqual(msg.combinations, ())
|
||||
self.assertEqual(msg.to_json(), self.jmsg_to)
|
||||
self.assertEqual(msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
|
||||
class ChallengeRequestTest(unittest.TestCase):
|
||||
|
|
@ -130,8 +131,8 @@ class ChallengeRequestTest(unittest.TestCase):
|
|||
'identifier': 'example.com',
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import ChallengeRequest
|
||||
|
|
@ -154,11 +155,11 @@ class AuthorizationTest(unittest.TestCase):
|
|||
'jwk': jwk,
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
self.jmsg['jwk'] = self.jmsg['jwk'].to_json()
|
||||
self.jmsg['jwk'] = self.jmsg['jwk'].to_partial_json()
|
||||
|
||||
from letsencrypt.acme.messages import Authorization
|
||||
self.assertEqual(Authorization.from_json(self.jmsg), self.msg)
|
||||
|
|
@ -174,7 +175,7 @@ class AuthorizationTest(unittest.TestCase):
|
|||
self.assertTrue(msg.recovery_token is None)
|
||||
self.assertTrue(msg.identifier is None)
|
||||
self.assertTrue(msg.jwk is None)
|
||||
self.assertEqual(self.jmsg, msg.to_json())
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
|
||||
|
||||
class AuthorizationRequestTest(unittest.TestCase):
|
||||
|
|
@ -215,10 +216,9 @@ class AuthorizationRequestTest(unittest.TestCase):
|
|||
'type': 'authorizationRequest',
|
||||
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
|
||||
'responses': [None if response is None
|
||||
else response.fully_serialize()
|
||||
'responses': [None if response is None else response.to_json()
|
||||
for response in self.responses],
|
||||
'signature': signature.fully_serialize(),
|
||||
'signature': signature.to_json(),
|
||||
# TODO: schema validation doesn't recognize tuples as
|
||||
# arrays :(
|
||||
'contact': list(self.contact),
|
||||
|
|
@ -236,8 +236,8 @@ class AuthorizationRequestTest(unittest.TestCase):
|
|||
def test_verify(self):
|
||||
self.assertTrue(self.msg.verify('example.com'))
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg_to)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import AuthorizationRequest
|
||||
|
|
@ -252,7 +252,7 @@ class AuthorizationRequestTest(unittest.TestCase):
|
|||
msg = AuthorizationRequest.from_json(self.jmsg_from)
|
||||
|
||||
self.assertEqual(msg.contact, ())
|
||||
self.assertEqual(self.jmsg_to, msg.to_json())
|
||||
self.assertEqual(self.jmsg_to, msg.to_partial_json())
|
||||
|
||||
|
||||
class CertificateTest(unittest.TestCase):
|
||||
|
|
@ -274,8 +274,8 @@ class CertificateTest(unittest.TestCase):
|
|||
# TODO: schema validation array tuples
|
||||
self.jmsg_from['chain'] = list(self.jmsg_from['chain'])
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg_to)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import Certificate
|
||||
|
|
@ -292,7 +292,7 @@ class CertificateTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(msg.chain, ())
|
||||
self.assertTrue(msg.refresh is None)
|
||||
self.assertEqual(self.jmsg_to, msg.to_json())
|
||||
self.assertEqual(self.jmsg_to, msg.to_partial_json())
|
||||
|
||||
|
||||
class CertificateRequestTest(unittest.TestCase):
|
||||
|
|
@ -315,8 +315,7 @@ class CertificateRequestTest(unittest.TestCase):
|
|||
'signature': signature,
|
||||
}
|
||||
self.jmsg_from = self.jmsg_to.copy()
|
||||
self.jmsg_from['signature'] = self.jmsg_from[
|
||||
'signature'].fully_serialize()
|
||||
self.jmsg_from['signature'] = self.jmsg_from['signature'].to_json()
|
||||
|
||||
def test_create(self):
|
||||
from letsencrypt.acme.messages import CertificateRequest
|
||||
|
|
@ -327,8 +326,8 @@ class CertificateRequestTest(unittest.TestCase):
|
|||
def test_verify(self):
|
||||
self.assertTrue(self.msg.verify())
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg_to)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import CertificateRequest
|
||||
|
|
@ -350,8 +349,8 @@ class DeferTest(unittest.TestCase):
|
|||
'message': 'Warming up the HSM',
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import Defer
|
||||
|
|
@ -366,7 +365,7 @@ class DeferTest(unittest.TestCase):
|
|||
|
||||
self.assertTrue(msg.interval is None)
|
||||
self.assertTrue(msg.message is None)
|
||||
self.assertEqual(self.jmsg, msg.to_json())
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
|
||||
|
||||
class ErrorTest(unittest.TestCase):
|
||||
|
|
@ -384,8 +383,8 @@ class ErrorTest(unittest.TestCase):
|
|||
'moreInfo': 'https://ca.example.com/documentation/csr-requirements',
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import Error
|
||||
|
|
@ -400,7 +399,7 @@ class ErrorTest(unittest.TestCase):
|
|||
|
||||
self.assertTrue(msg.message is None)
|
||||
self.assertTrue(msg.more_info is None)
|
||||
self.assertEqual(self.jmsg, msg.to_json())
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
|
||||
|
||||
class RevocationTest(unittest.TestCase):
|
||||
|
|
@ -410,8 +409,8 @@ class RevocationTest(unittest.TestCase):
|
|||
self.msg = Revocation()
|
||||
self.jmsg = {'type': 'revocation'}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import Revocation
|
||||
|
|
@ -440,8 +439,7 @@ class RevocationRequestTest(unittest.TestCase):
|
|||
'signature': signature,
|
||||
}
|
||||
self.jmsg_from = self.jmsg_to.copy()
|
||||
self.jmsg_from['signature'] = self.jmsg_from[
|
||||
'signature'].fully_serialize()
|
||||
self.jmsg_from['signature'] = self.jmsg_from['signature'].to_json()
|
||||
|
||||
def test_create(self):
|
||||
from letsencrypt.acme.messages import RevocationRequest
|
||||
|
|
@ -451,8 +449,8 @@ class RevocationRequestTest(unittest.TestCase):
|
|||
def test_verify(self):
|
||||
self.assertTrue(self.msg.verify())
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg_to)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import RevocationRequest
|
||||
|
|
@ -469,8 +467,8 @@ class StatusRequestTest(unittest.TestCase):
|
|||
'token': u'O7-s9MNq1siZHlgrMzi9_A',
|
||||
}
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.msg.to_json(), self.jmsg)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages import StatusRequest
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import Crypto.PublicKey.RSA
|
|||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
RSA256_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))
|
||||
RSA512_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa512_key.pem'))
|
||||
RSA256_KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem')))
|
||||
RSA512_KEY = jose.HashableRSAKey(
|
||||
Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa512_key.pem')))
|
||||
|
||||
|
||||
class SignatureTest(unittest.TestCase):
|
||||
|
|
@ -40,8 +42,8 @@ class SignatureTest(unittest.TestCase):
|
|||
|
||||
self.jsig_from = {
|
||||
'nonce': b64nonce,
|
||||
'alg': self.alg.to_json(),
|
||||
'jwk': self.jwk.to_json(),
|
||||
'alg': self.alg.to_partial_json(),
|
||||
'jwk': self.jwk.to_partial_json(),
|
||||
'sig': b64sig,
|
||||
}
|
||||
|
||||
|
|
@ -76,8 +78,8 @@ class SignatureTest(unittest.TestCase):
|
|||
self.assertEqual(signature.jwk, self.jwk)
|
||||
self.assertTrue(signature.verify(self.msg))
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.signature.to_json(), self.jsig_to)
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.signature.to_partial_json(), self.jsig_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.other import Signature
|
||||
|
|
@ -86,7 +88,7 @@ class SignatureTest(unittest.TestCase):
|
|||
|
||||
def test_from_json_non_schema_errors(self):
|
||||
from letsencrypt.acme.other import Signature
|
||||
jwk = self.jwk.to_json()
|
||||
jwk = self.jwk.to_partial_json()
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, Signature.from_json, {
|
||||
'alg': 'RS256', 'sig': 'x', 'nonce': '', 'jwk': jwk})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import sys
|
|||
import Crypto.PublicKey.RSA
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
|
|
@ -16,12 +17,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
"""ACME Authorization Handler for a client.
|
||||
|
||||
:ivar dv_auth: Authenticator capable of solving
|
||||
:const:`~letsencrypt.client.constants.DV_CHALLENGES`
|
||||
:class:`~letsencrypt.acme.challenges.DVChallenge` types
|
||||
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
:ivar client_auth: Authenticator capable of solving
|
||||
:const:`~letsencrypt.client_auth.constants.CLIENT_CHALLENGES`
|
||||
:type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
:ivar cont_auth: Authenticator capable of solving
|
||||
:class:`~letsencrypt.acme.challenges.ContinuityChallenge` types
|
||||
:type cont_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
:ivar network: Network object for sending and receiving authorization
|
||||
messages
|
||||
|
|
@ -36,13 +37,13 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
:ivar dict paths: optimal path for authorization. eg. paths[domain]
|
||||
:ivar dict dv_c: Keys - domain, Values are DV challenges in the form of
|
||||
:class:`letsencrypt.client.achallenges.Indexed`
|
||||
:ivar dict client_c: Keys - domain, Values are Client challenges in the form
|
||||
of :class:`letsencrypt.client.achallenges.Indexed`
|
||||
:ivar dict cont_c: Keys - domain, Values are Continuity challenges in the
|
||||
form of :class:`letsencrypt.client.achallenges.Indexed`
|
||||
|
||||
"""
|
||||
def __init__(self, dv_auth, client_auth, network):
|
||||
def __init__(self, dv_auth, cont_auth, network):
|
||||
self.dv_auth = dv_auth
|
||||
self.client_auth = client_auth
|
||||
self.cont_auth = cont_auth
|
||||
self.network = network
|
||||
|
||||
self.domains = []
|
||||
|
|
@ -52,7 +53,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
self.paths = dict()
|
||||
|
||||
self.dv_c = dict()
|
||||
self.client_c = dict()
|
||||
self.cont_c = dict()
|
||||
|
||||
def add_chall_msg(self, domain, msg, authkey):
|
||||
"""Add a challenge message to the AuthHandler.
|
||||
|
|
@ -76,7 +77,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
self.authkey[domain] = authkey
|
||||
|
||||
def get_authorizations(self):
|
||||
"""Retreive all authorizations for challenges.
|
||||
"""Retrieve all authorizations for challenges.
|
||||
|
||||
:raises LetsEncryptAuthHandlerError: If unable to retrieve all
|
||||
authorizations
|
||||
|
|
@ -119,8 +120,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
nonce=self.msgs[domain].nonce,
|
||||
responses=self.responses[domain],
|
||||
name=domain,
|
||||
key=Crypto.PublicKey.RSA.importKey(
|
||||
self.authkey[domain].pem)),
|
||||
key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
self.authkey[domain].pem))),
|
||||
messages.Authorization)
|
||||
logging.info("Received Authorization for %s", domain)
|
||||
return auth
|
||||
|
|
@ -147,24 +148,24 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
self._get_chall_pref(dom),
|
||||
self.msgs[dom].combinations)
|
||||
|
||||
self.dv_c[dom], self.client_c[dom] = self._challenge_factory(
|
||||
self.dv_c[dom], self.cont_c[dom] = self._challenge_factory(
|
||||
dom, self.paths[dom])
|
||||
|
||||
# Flatten challs for authenticator functions and remove index
|
||||
# Order is important here as we will not expose the outside
|
||||
# Authenticator to our own indices.
|
||||
flat_client = []
|
||||
flat_cont = []
|
||||
flat_dv = []
|
||||
|
||||
for dom in self.domains:
|
||||
flat_client.extend(ichall.achall for ichall in self.client_c[dom])
|
||||
flat_cont.extend(ichall.achall for ichall in self.cont_c[dom])
|
||||
flat_dv.extend(ichall.achall for ichall in self.dv_c[dom])
|
||||
|
||||
client_resp = []
|
||||
cont_resp = []
|
||||
dv_resp = []
|
||||
try:
|
||||
if flat_client:
|
||||
client_resp = self.client_auth.perform(flat_client)
|
||||
if flat_cont:
|
||||
cont_resp = self.cont_auth.perform(flat_cont)
|
||||
if flat_dv:
|
||||
dv_resp = self.dv_auth.perform(flat_dv)
|
||||
# This will catch both specific types of errors.
|
||||
|
|
@ -181,8 +182,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
logging.info("Ready for verification...")
|
||||
|
||||
# Assemble Responses
|
||||
if client_resp:
|
||||
self._assign_responses(client_resp, self.client_c)
|
||||
if cont_resp:
|
||||
self._assign_responses(cont_resp, self.cont_c)
|
||||
if dv_resp:
|
||||
self._assign_responses(dv_resp, self.dv_c)
|
||||
|
||||
|
|
@ -191,7 +192,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
:param list flat_list: flat_list of responses from an IAuthenticator
|
||||
:param dict ichall_dict: Master dict mapping all domains to a list of
|
||||
their associated 'client' and 'dv' Indexed challenges, or their
|
||||
their associated 'continuity' and 'dv' Indexed challenges, or their
|
||||
:class:`letsencrypt.client.achallenges.Indexed` list
|
||||
|
||||
"""
|
||||
|
|
@ -213,7 +214,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
"""
|
||||
chall_prefs = []
|
||||
chall_prefs.extend(self.client_auth.get_chall_pref(domain))
|
||||
chall_prefs.extend(self.cont_auth.get_chall_pref(domain))
|
||||
chall_prefs.extend(self.dv_auth.get_chall_pref(domain))
|
||||
return chall_prefs
|
||||
|
||||
|
|
@ -228,11 +229,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
# Chose to make these lists instead of a generator to make it easier to
|
||||
# work with...
|
||||
dv_list = [ichall.achall for ichall in self.dv_c[domain]]
|
||||
client_list = [ichall.achall for ichall in self.client_c[domain]]
|
||||
cont_list = [ichall.achall for ichall in self.cont_c[domain]]
|
||||
if dv_list:
|
||||
self.dv_auth.cleanup(dv_list)
|
||||
if client_list:
|
||||
self.client_auth.cleanup(client_list)
|
||||
if cont_list:
|
||||
self.cont_auth.cleanup(cont_list)
|
||||
|
||||
def _cleanup_state(self, delete_list):
|
||||
"""Cleanup state after an authorization is received.
|
||||
|
|
@ -247,7 +248,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
del self.authkey[domain]
|
||||
|
||||
del self.client_c[domain]
|
||||
del self.cont_c[domain]
|
||||
del self.dv_c[domain]
|
||||
|
||||
self.domains.remove(domain)
|
||||
|
|
@ -259,9 +260,9 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
:param list path: List of indices from `challenges`.
|
||||
|
||||
:returns: dv_chall, list of
|
||||
:returns: dv_chall, list of DVChallenge type
|
||||
:class:`letsencrypt.client.achallenges.Indexed`
|
||||
client_chall, list of
|
||||
cont_chall, list of ContinuityChallenge type
|
||||
:class:`letsencrypt.client.achallenges.Indexed`
|
||||
:rtype: tuple
|
||||
|
||||
|
|
@ -270,7 +271,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
"""
|
||||
dv_chall = []
|
||||
client_chall = []
|
||||
cont_chall = []
|
||||
|
||||
for index in path:
|
||||
chall = self.msgs[domain].challenges[index]
|
||||
|
|
@ -304,35 +305,38 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
ichall = achallenges.Indexed(achall=achall, index=index)
|
||||
|
||||
if isinstance(chall, challenges.ClientChallenge):
|
||||
client_chall.append(ichall)
|
||||
if isinstance(chall, challenges.ContinuityChallenge):
|
||||
cont_chall.append(ichall)
|
||||
elif isinstance(chall, challenges.DVChallenge):
|
||||
dv_chall.append(ichall)
|
||||
|
||||
return dv_chall, client_chall
|
||||
return dv_chall, cont_chall
|
||||
|
||||
|
||||
def gen_challenge_path(challs, preferences, combinations):
|
||||
"""Generate a plan to get authority over the identity.
|
||||
|
||||
.. todo:: Make sure that the challenges are feasible...
|
||||
Example: Do you have the recovery key?
|
||||
.. todo:: This can be possibly be rewritten to use resolved_combinations.
|
||||
|
||||
:param list challs: A list of challenges
|
||||
:param tuple challs: A tuple of challenges
|
||||
(:class:`letsencrypt.acme.challenges.Challenge`) from
|
||||
:class:`letsencrypt.acme.messages.Challenge` server message to
|
||||
be fulfilled by the client in order to prove possession of the
|
||||
identifier.
|
||||
|
||||
:param list preferences: List of challenge preferences for domain
|
||||
(:class:`letsencrypt.acme.challenges.Challege` subclasses)
|
||||
(:class:`letsencrypt.acme.challenges.Challenge` subclasses)
|
||||
|
||||
:param list combinations: A collection of sets of challenges from
|
||||
:param tuple combinations: A collection of sets of challenges from
|
||||
:class:`letsencrypt.acme.messages.Challenge`, each of which would
|
||||
be sufficient to prove possession of the identifier.
|
||||
|
||||
:returns: List of indices from ``challenges``.
|
||||
:rtype: list
|
||||
:returns: tuple of indices from ``challenges``.
|
||||
:rtype: tuple
|
||||
|
||||
:raises letsencrypt.client.errors.LetsEncryptAuthHandlerError: If a
|
||||
path cannot be created that satisfies the CA given the preferences and
|
||||
combinations.
|
||||
|
||||
"""
|
||||
if combinations:
|
||||
|
|
@ -349,29 +353,34 @@ def _find_smart_path(challs, preferences, combinations):
|
|||
|
||||
"""
|
||||
chall_cost = {}
|
||||
max_cost = 0
|
||||
max_cost = 1
|
||||
for i, chall_cls in enumerate(preferences):
|
||||
chall_cost[chall_cls] = i
|
||||
max_cost += i
|
||||
|
||||
# max_cost is now equal to sum(indices) + 1
|
||||
|
||||
best_combo = []
|
||||
# Set above completing all of the available challenges
|
||||
best_combo_cost = max_cost + 1
|
||||
best_combo_cost = max_cost
|
||||
|
||||
combo_total = 0
|
||||
for combo in combinations:
|
||||
for challenge_index in combo:
|
||||
combo_total += chall_cost.get(challs[
|
||||
challenge_index].__class__, max_cost)
|
||||
|
||||
if combo_total < best_combo_cost:
|
||||
best_combo = combo
|
||||
best_combo_cost = combo_total
|
||||
combo_total = 0
|
||||
|
||||
combo_total = 0
|
||||
|
||||
if not best_combo:
|
||||
logging.fatal("Client does not support any combination of "
|
||||
"challenges to satisfy ACME server")
|
||||
sys.exit(22)
|
||||
msg = ("Client does not support any combination of challenges that "
|
||||
"will satisfy the CA.")
|
||||
logging.fatal(msg)
|
||||
raise errors.LetsEncryptAuthHandlerError(msg)
|
||||
|
||||
return best_combo
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from letsencrypt.client.display import ops as display_ops
|
|||
from letsencrypt.client.plugins import disco as plugins_disco
|
||||
|
||||
from letsencrypt.client.plugins.apache import configurator as apache_configurator
|
||||
from letsencrypt.client.plugins.nginx import configurator as nginx_configurator
|
||||
|
||||
|
||||
def _common_run(args, config, authenticator, installer):
|
||||
|
|
@ -303,6 +304,10 @@ def create_parser():
|
|||
plugin_parser(
|
||||
parser.add_argument_group("apache"), prefix="apache",
|
||||
plugin_cls=apache_configurator.ApacheConfigurator)
|
||||
plugin_parser(
|
||||
parser.add_argument_group("nginx"), prefix="nginx",
|
||||
plugin_cls=nginx_configurator.NginxConfigurator)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import sys
|
|||
import Crypto.PublicKey.RSA
|
||||
import M2Crypto
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages
|
||||
from letsencrypt.acme.jose import util as jose_util
|
||||
|
||||
from letsencrypt.client import auth_handler
|
||||
from letsencrypt.client import client_authenticator
|
||||
from letsencrypt.client import continuity_auth
|
||||
from letsencrypt.client import crypto_util
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
|
|
@ -33,7 +33,8 @@ class Client(object):
|
|||
:type authkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:ivar auth_handler: Object that supports the IAuthenticator interface.
|
||||
auth_handler contains both a dv_authenticator and a client_authenticator
|
||||
auth_handler contains both a dv_authenticator and a
|
||||
continuity_authenticator
|
||||
:type auth_handler: :class:`letsencrypt.client.auth_handler.AuthHandler`
|
||||
|
||||
:ivar installer: Object supporting the IInstaller interface.
|
||||
|
|
@ -60,9 +61,9 @@ class Client(object):
|
|||
self.config = config
|
||||
|
||||
if dv_auth is not None:
|
||||
client_auth = client_authenticator.ClientAuthenticator(config)
|
||||
cont_auth = continuity_auth.ContinuityAuthenticator(config)
|
||||
self.auth_handler = auth_handler.AuthHandler(
|
||||
dv_auth, client_auth, self.network)
|
||||
dv_auth, cont_auth, self.network)
|
||||
else:
|
||||
self.auth_handler = None
|
||||
|
||||
|
|
@ -130,9 +131,10 @@ class Client(object):
|
|||
logging.info("Preparing and sending CSR...")
|
||||
return self.network.send_and_receive_expected(
|
||||
messages.CertificateRequest.create(
|
||||
csr=jose_util.ComparableX509(
|
||||
csr=jose.ComparableX509(
|
||||
M2Crypto.X509.load_request_der_string(csr_der)),
|
||||
key=Crypto.PublicKey.RSA.importKey(self.authkey.pem)),
|
||||
key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
self.authkey.pem))),
|
||||
messages.Certificate)
|
||||
|
||||
def save_certificate(self, certificate_msg, cert_path, chain_path):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Client Authenticator"""
|
||||
"""Continuity Authenticator"""
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
|
|
@ -9,9 +9,9 @@ from letsencrypt.client import interfaces
|
|||
from letsencrypt.client import recovery_token
|
||||
|
||||
|
||||
class ClientAuthenticator(object):
|
||||
class ContinuityAuthenticator(object):
|
||||
"""IAuthenticator for
|
||||
:const:`~letsencrypt.client.constants.CLIENT_CHALLENGES`.
|
||||
:const:`~letsencrypt.acme.challenges.ContinuityChallenge` class challenges.
|
||||
|
||||
:ivar rec_token: Performs "recoveryToken" challenges
|
||||
:type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken`
|
||||
|
|
@ -41,7 +41,7 @@ class ClientAuthenticator(object):
|
|||
if isinstance(achall, achallenges.RecoveryToken):
|
||||
responses.append(self.rec_token.perform(achall))
|
||||
else:
|
||||
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
|
||||
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
|
||||
return responses
|
||||
|
||||
def cleanup(self, achalls):
|
||||
|
|
@ -50,4 +50,4 @@ class ClientAuthenticator(object):
|
|||
if isinstance(achall, achallenges.RecoveryToken):
|
||||
self.rec_token.cleanup(achall)
|
||||
else:
|
||||
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
|
||||
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
|
||||
|
|
@ -5,6 +5,14 @@ class LetsEncryptClientError(Exception):
|
|||
"""Generic Let's Encrypt client error."""
|
||||
|
||||
|
||||
class NetworkError(LetsEncryptClientError):
|
||||
"""Network error."""
|
||||
|
||||
|
||||
class UnexpectedUpdate(NetworkError):
|
||||
"""Unexpected update."""
|
||||
|
||||
|
||||
class LetsEncryptReverterError(LetsEncryptClientError):
|
||||
"""Let's Encrypt Reverter error."""
|
||||
|
||||
|
|
@ -14,7 +22,7 @@ class LetsEncryptAuthHandlerError(LetsEncryptClientError):
|
|||
"""Let's Encrypt Auth Handler error."""
|
||||
|
||||
|
||||
class LetsEncryptClientAuthError(LetsEncryptAuthHandlerError):
|
||||
class LetsEncryptContAuthError(LetsEncryptAuthHandlerError):
|
||||
"""Let's Encrypt Client Authenticator error."""
|
||||
|
||||
|
||||
|
|
|
|||
506
letsencrypt/client/network2.py
Normal file
506
letsencrypt/client/network2.py
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
"""Networking for ACME protocol v02."""
|
||||
import datetime
|
||||
import heapq
|
||||
import httplib
|
||||
import itertools
|
||||
import logging
|
||||
import time
|
||||
|
||||
import M2Crypto
|
||||
import requests
|
||||
import werkzeug
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages2
|
||||
|
||||
from letsencrypt.client import errors
|
||||
|
||||
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
|
||||
class Network(object):
|
||||
"""ACME networking.
|
||||
|
||||
.. todo::
|
||||
Clean up raised error types hierarchy, document, and handle (wrap)
|
||||
instances of `.DeserializationError` raised in `from_json()``.
|
||||
|
||||
:ivar str new_reg_uri: Location of new-reg
|
||||
:ivar key: `.JWK` (private)
|
||||
:ivar alg: `.JWASignature`
|
||||
|
||||
"""
|
||||
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
|
||||
|
||||
def __init__(self, new_reg_uri, key, alg=jose.RS256):
|
||||
self.new_reg_uri = new_reg_uri
|
||||
self.key = key
|
||||
self.alg = alg
|
||||
|
||||
def _wrap_in_jws(self, obj):
|
||||
"""Wrap `JSONDeSerializable` object in JWS.
|
||||
|
||||
:rtype: `.JWS`
|
||||
|
||||
"""
|
||||
dumps = obj.json_dumps()
|
||||
logging.debug('Serialized JSON: %s', dumps)
|
||||
return jose.JWS.sign(
|
||||
payload=dumps, key=self.key, alg=self.alg).json_dumps()
|
||||
|
||||
@classmethod
|
||||
def _check_response(cls, response, content_type=None):
|
||||
"""Check response content and its type.
|
||||
|
||||
.. note::
|
||||
Checking is not strict: wrong server response ``Content-Type``
|
||||
HTTP header is ignored if response is an expected JSON object
|
||||
(c.f. Boulder #56).
|
||||
|
||||
:param str content_type: Expected Content-Type response header.
|
||||
If JSON is expected and not present in server response, this
|
||||
function will raise an error. Otherwise, wrong Content-Type
|
||||
is ignored, but logged.
|
||||
|
||||
:raises letsencrypt.messages2.Error: If server response body
|
||||
carries HTTP Problem (draft-ietf-appsawg-http-problem-00).
|
||||
:raises letsencrypt.errors.NetworkError: In case of other
|
||||
networking errors.
|
||||
|
||||
"""
|
||||
response_ct = response.headers.get('Content-Type')
|
||||
|
||||
try:
|
||||
# TODO: response.json() is called twice, once here, and
|
||||
# once in _get and _post clients
|
||||
jobj = response.json()
|
||||
except ValueError as error:
|
||||
jobj = None
|
||||
|
||||
if not response.ok:
|
||||
if jobj is not None:
|
||||
if response_ct != cls.JSON_ERROR_CONTENT_TYPE:
|
||||
logging.debug(
|
||||
'Ignoring wrong Content-Type (%r) for JSON Error',
|
||||
response_ct)
|
||||
|
||||
try:
|
||||
raise messages2.Error.from_json(jobj)
|
||||
except jose.DeserializationError as error:
|
||||
# Couldn't deserialize JSON object
|
||||
raise errors.NetworkError((response, error))
|
||||
else:
|
||||
# response is not JSON object
|
||||
raise errors.NetworkError(response)
|
||||
else:
|
||||
if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE:
|
||||
logging.debug(
|
||||
'Ignoring wrong Content-Type (%r) for JSON decodable '
|
||||
'response', response_ct)
|
||||
|
||||
if content_type == cls.JSON_CONTENT_TYPE and jobj is None:
|
||||
raise errors.NetworkError(
|
||||
'Unexpected response Content-Type: {0}'.format(response_ct))
|
||||
|
||||
def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs):
|
||||
"""Send GET request.
|
||||
|
||||
:raises letsencrypt.client.errors.NetworkError:
|
||||
|
||||
:returns: HTTP Response
|
||||
:rtype: `requests.Response`
|
||||
|
||||
"""
|
||||
try:
|
||||
response = requests.get(uri, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise errors.NetworkError(error)
|
||||
self._check_response(response, content_type=content_type)
|
||||
return response
|
||||
|
||||
def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs):
|
||||
"""Send POST data.
|
||||
|
||||
:param str content_type: Expected ``Content-Type``, fails if not set.
|
||||
|
||||
:raises letsencrypt.acme.messages2.NetworkError:
|
||||
|
||||
:returns: HTTP Response
|
||||
:rtype: `requests.Response`
|
||||
|
||||
"""
|
||||
logging.debug('Sending POST data: %s', data)
|
||||
try:
|
||||
response = requests.post(uri, data=data, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise errors.NetworkError(error)
|
||||
logging.debug('Received response %s: %s', response, response.text)
|
||||
|
||||
self._check_response(response, content_type=content_type)
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
|
||||
terms_of_service=None):
|
||||
terms_of_service = (
|
||||
response.links['terms-of-service']['url']
|
||||
if 'terms-of-service' in response.links else terms_of_service)
|
||||
|
||||
if new_authzr_uri is None:
|
||||
try:
|
||||
new_authzr_uri = response.links['next']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"next" link missing')
|
||||
|
||||
return messages2.RegistrationResource(
|
||||
body=messages2.Registration.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri),
|
||||
new_authzr_uri=new_authzr_uri,
|
||||
terms_of_service=terms_of_service)
|
||||
|
||||
def register(self, contact=messages2.Registration._fields[
|
||||
'contact'].default):
|
||||
"""Register.
|
||||
|
||||
:param contact: Contact list, as accpeted by `.RegistrationResource`
|
||||
:type contact: `tuple`
|
||||
|
||||
:returns: Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
:raises letsencrypt.client.errors.UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
new_reg = messages2.Registration(contact=contact)
|
||||
|
||||
response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg))
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
|
||||
regr = self._regr_from_response(response)
|
||||
if regr.body.key != self.key.public() or regr.body.contact != contact:
|
||||
raise errors.UnexpectedUpdate(regr)
|
||||
|
||||
return regr
|
||||
|
||||
def update_registration(self, regr):
|
||||
"""Update registration.
|
||||
|
||||
:pram regr: Registration Resource.
|
||||
:type regr: `.RegistrationResource`
|
||||
|
||||
:returns: Updated Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
response = self._post(regr.uri, self._wrap_in_jws(regr.body))
|
||||
|
||||
# TODO: Boulder returns httplib.ACCEPTED
|
||||
#assert response.status_code == httplib.OK
|
||||
|
||||
# TODO: Boulder does not set Location or Link on update
|
||||
# (c.f. acme-spec #94)
|
||||
|
||||
updated_regr = self._regr_from_response(
|
||||
response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri,
|
||||
terms_of_service=regr.terms_of_service)
|
||||
if updated_regr != regr:
|
||||
# TODO: Boulder reregisters with new recoveryToken and new URI
|
||||
raise errors.UnexpectedUpdate(regr)
|
||||
return updated_regr
|
||||
|
||||
def _authzr_from_response(self, response, identifier,
|
||||
uri=None, new_cert_uri=None):
|
||||
if new_cert_uri is None:
|
||||
try:
|
||||
new_cert_uri = response.links['next']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"next" link missing')
|
||||
|
||||
authzr = messages2.AuthorizationResource(
|
||||
body=messages2.Authorization.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri),
|
||||
new_cert_uri=new_cert_uri)
|
||||
if (authzr.body.key != self.key.public()
|
||||
or authzr.body.identifier != identifier):
|
||||
raise errors.UnexpectedUpdate(authzr)
|
||||
return authzr
|
||||
|
||||
def request_challenges(self, identifier, regr):
|
||||
"""Request challenges.
|
||||
|
||||
:param identifier: Identifier to be challenged.
|
||||
:type identifier: `.messages2.Identifier`
|
||||
|
||||
:param regr: Registration Resource.
|
||||
:type regr: `.RegistrationResource`
|
||||
|
||||
:returns: Authorization Resource.
|
||||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
new_authz = messages2.Authorization(identifier=identifier)
|
||||
response = self._post(regr.new_authzr_uri, self._wrap_in_jws(new_authz))
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
return self._authzr_from_response(response, identifier)
|
||||
|
||||
def request_domain_challenges(self, domain, regr):
|
||||
"""Request challenges for domain names.
|
||||
|
||||
This is simply a convenience function that wraps around
|
||||
`request_challenges`, but works with domain names instead of
|
||||
generic identifiers.
|
||||
|
||||
:param str domain: Domain name to be challenged.
|
||||
|
||||
"""
|
||||
return self.request_challenges(messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value=domain), regr)
|
||||
|
||||
def answer_challenge(self, challb, response):
|
||||
"""Answer challenge.
|
||||
|
||||
:param challb: Challenge Resource body.
|
||||
:type challb: `.ChallengeBody`
|
||||
|
||||
:param response: Corresponding Challenge response
|
||||
:type response: `.challenges.ChallengeResponse`
|
||||
|
||||
:returns: Challenge Resource with updated body.
|
||||
:rtype: `.ChallengeResource`
|
||||
|
||||
:raises errors.UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
response = self._post(challb.uri, self._wrap_in_jws(response))
|
||||
try:
|
||||
authzr_uri = response.links['up']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"up" Link header missing')
|
||||
challr = messages2.ChallengeResource(
|
||||
authzr_uri=authzr_uri,
|
||||
body=messages2.ChallengeBody.from_json(response.json()))
|
||||
# TODO: check that challr.uri == response.headers['Location']?
|
||||
if challr.uri != challb.uri:
|
||||
raise errors.UnexpectedUpdate(challr.uri)
|
||||
return challr
|
||||
|
||||
def answer_challenges(self, challbs, responses):
|
||||
"""Answer multiple challenges.
|
||||
|
||||
.. note:: This is a convenience function to make integration
|
||||
with old proto code easier and shall probably be removed
|
||||
once restification is over.
|
||||
|
||||
"""
|
||||
return [self.answer_challenge(challb, response)
|
||||
for challb, response in itertools.izip(challbs, responses)]
|
||||
|
||||
@classmethod
|
||||
def retry_after(cls, response, default):
|
||||
"""Compute next `poll` time based on response ``Retry-After`` header.
|
||||
|
||||
:param response: Response from `poll`.
|
||||
:type response: `requests.Response`
|
||||
|
||||
:param int default: Default value (in seconds), used when
|
||||
``Retry-After`` header is not present or invalid.
|
||||
|
||||
:returns: Time point when next `poll` should be performed.
|
||||
:rtype: `datetime.datetime`
|
||||
|
||||
"""
|
||||
retry_after = response.headers.get('Retry-After', str(default))
|
||||
try:
|
||||
seconds = int(retry_after)
|
||||
except ValueError:
|
||||
# pylint: disable=no-member
|
||||
decoded = werkzeug.parse_date(retry_after) # RFC1123
|
||||
if decoded is None:
|
||||
seconds = default
|
||||
else:
|
||||
return decoded
|
||||
|
||||
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
|
||||
|
||||
def poll(self, authzr):
|
||||
"""Poll Authorization Resource for status.
|
||||
|
||||
:param authzr: Authorization Resource
|
||||
:type authzr: `.AuthorizationResource`
|
||||
|
||||
:returns: Updated Authorization Resource and HTTP response.
|
||||
|
||||
:rtype: (`.AuthorizationResource`, `requests.Response`)
|
||||
|
||||
"""
|
||||
response = self._get(authzr.uri)
|
||||
updated_authzr = self._authzr_from_response(
|
||||
response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri)
|
||||
# TODO: check and raise UnexpectedUpdate
|
||||
|
||||
return updated_authzr, response
|
||||
|
||||
def request_issuance(self, csr, authzrs):
|
||||
"""Request issuance.
|
||||
|
||||
:param csr: CSR
|
||||
:type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
|
||||
:param authzrs: `list` of `.AuthorizationResource`
|
||||
|
||||
:returns: Issued certificate
|
||||
:rtype: `.messages2.CertificateResource`
|
||||
|
||||
"""
|
||||
assert authzrs, "Authorizations list is empty"
|
||||
|
||||
# TODO: assert len(authzrs) == number of SANs
|
||||
req = messages2.CertificateRequest(
|
||||
csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs))
|
||||
|
||||
content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
|
||||
response = self._post(
|
||||
authzrs[0].new_cert_uri, # TODO: acme-spec #90
|
||||
self._wrap_in_jws(req),
|
||||
content_type=content_type,
|
||||
headers={'Accept': content_type})
|
||||
|
||||
cert_chain_uri = response.links.get('up', {}).get('url')
|
||||
|
||||
try:
|
||||
uri = response.headers['Location']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"Location" Header missing')
|
||||
|
||||
return messages2.CertificateResource(
|
||||
uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri,
|
||||
body=jose.ComparableX509(
|
||||
M2Crypto.X509.load_cert_der_string(response.content)))
|
||||
|
||||
def poll_and_request_issuance(self, csr, authzrs, mintime=5):
|
||||
"""Poll and request issuance.
|
||||
|
||||
This function polls all provided Authorization Resource URIs
|
||||
until all challenges are valid, respecting ``Retry-After`` HTTP
|
||||
headers, and then calls `request_issuance`.
|
||||
|
||||
.. todo:: add `max_attempts` or `timeout`
|
||||
|
||||
:param csr: CSR.
|
||||
:type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
|
||||
:param authzrs: `list` of `.AuthorizationResource`
|
||||
|
||||
:param int mintime: Minimum time before next attempt, used if
|
||||
``Retry-After`` is not present in the response.
|
||||
|
||||
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
|
||||
the issued certificate (`.messages2.CertificateResource.),
|
||||
and ``updated_authzrs`` is a `tuple` consisting of updated
|
||||
Authorization Resources (`.AuthorizationResource`) as
|
||||
present in the responses from server, and in the same order
|
||||
as the input ``authzrs``.
|
||||
:rtype: `tuple`
|
||||
|
||||
"""
|
||||
# priority queue with datetime (based od Retry-After) as key,
|
||||
# and original Authorization Resource as value
|
||||
waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs]
|
||||
# mapping between original Authorization Resource and the most
|
||||
# recently updated one
|
||||
updated = dict((authzr, authzr) for authzr in authzrs)
|
||||
|
||||
while waiting:
|
||||
# find the smallest Retry-After, and sleep if necessary
|
||||
when, authzr = heapq.heappop(waiting)
|
||||
now = datetime.datetime.now()
|
||||
if when > now:
|
||||
seconds = (when - now).seconds
|
||||
logging.debug('Sleeping for %d seconds', seconds)
|
||||
time.sleep(seconds)
|
||||
|
||||
# Note that we poll with the latest updated Authorization
|
||||
# URI, which might have a different URI than initial one
|
||||
updated_authzr, response = self.poll(updated[authzr])
|
||||
updated[authzr] = updated_authzr
|
||||
|
||||
if updated_authzr.body.status != messages2.STATUS_VALID:
|
||||
# push back to the priority queue, with updated retry_after
|
||||
heapq.heappush(waiting, (self.retry_after(
|
||||
response, default=mintime), authzr))
|
||||
|
||||
updated_authzrs = tuple(updated[authzr] for authzr in authzrs)
|
||||
return self.request_issuance(csr, updated_authzrs), updated_authzrs
|
||||
|
||||
def _get_cert(self, uri):
|
||||
content_type = self.DER_CONTENT_TYPE # TODO: make it a param
|
||||
response = self._get(uri, headers={'Accept': content_type},
|
||||
content_type=content_type)
|
||||
return response, jose.ComparableX509(
|
||||
M2Crypto.X509.load_cert_der_string(response.content))
|
||||
|
||||
def check_cert(self, certr):
|
||||
"""Check for new cert.
|
||||
|
||||
:param certr: Certificate Resource
|
||||
:type certr: `.CertificateResource`
|
||||
|
||||
:returns: Updated Certificate Resource.
|
||||
:rtype: `.CertificateResource`
|
||||
|
||||
"""
|
||||
# TODO: acme-spec 5.1 table action should be renamed to
|
||||
# "refresh cert", and this method integrated with self.refresh
|
||||
response, cert = self._get_cert(certr.uri)
|
||||
if 'Location' not in response.headers:
|
||||
raise errors.NetworkError('Location header missing')
|
||||
if response.headers['Location'] != certr.uri:
|
||||
raise errors.UnexpectedUpdate(response.text)
|
||||
return certr.update(body=cert)
|
||||
|
||||
def refresh(self, certr):
|
||||
"""Refresh certificate.
|
||||
|
||||
:param certr: Certificate Resource
|
||||
:type certr: `.CertificateResource`
|
||||
|
||||
:returns: Updated Certificate Resource.
|
||||
:rtype: `.CertificateResource`
|
||||
|
||||
"""
|
||||
# TODO: If a client sends a refresh request and the server is
|
||||
# not willing to refresh the certificate, the server MUST
|
||||
# respond with status code 403 (Forbidden)
|
||||
return self.check_cert(certr)
|
||||
|
||||
def fetch_chain(self, certr):
|
||||
"""Fetch chain for certificate.
|
||||
|
||||
:param certr: Certificate Resource
|
||||
:type certr: `.CertificateResource`
|
||||
|
||||
:returns: Certificate chain, or `None` if no "up" Link was provided.
|
||||
:rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509`
|
||||
|
||||
"""
|
||||
if certr.cert_chain_uri is not None:
|
||||
return self._get_cert(certr.cert_chain_uri)
|
||||
|
||||
def revoke(self, certr, when=messages2.Revocation.NOW):
|
||||
"""Revoke certificate.
|
||||
|
||||
:param when: When should the revocation take place? Takes
|
||||
the same values as `.messages2.Revocation.revoke`.
|
||||
|
||||
"""
|
||||
rev = messages2.Revocation(revoke=when, authorizations=tuple(
|
||||
authzr.uri for authzr in certr.authzrs))
|
||||
response = self._post(certr.uri, self._wrap_in_jws(rev))
|
||||
if response.status_code != httplib.OK:
|
||||
raise errors.NetworkError(
|
||||
'Successful revocation must return HTTP OK status')
|
||||
1
letsencrypt/client/plugins/nginx/__init__.py
Normal file
1
letsencrypt/client/plugins/nginx/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Let's Encrypt client.plugins.nginx."""
|
||||
570
letsencrypt/client/plugins/nginx/configurator.py
Normal file
570
letsencrypt/client/plugins/nginx/configurator.py
Normal file
|
|
@ -0,0 +1,570 @@
|
|||
"""Nginx Configuration"""
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import reverter
|
||||
|
||||
from letsencrypt.client.plugins.nginx import constants
|
||||
from letsencrypt.client.plugins.nginx import dvsni
|
||||
from letsencrypt.client.plugins.nginx import parser
|
||||
|
||||
|
||||
class NginxConfigurator(object):
|
||||
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||
"""Nginx configurator.
|
||||
|
||||
.. todo:: Add proper support for comments in the config. Currently,
|
||||
config files modified by the configurator will lose all their comments.
|
||||
|
||||
:ivar config: Configuration.
|
||||
:type config: :class:`~letsencrypt.client.interfaces.IConfig`
|
||||
|
||||
:ivar parser: Handles low level parsing
|
||||
:type parser: :class:`~letsencrypt.client.plugins.nginx.parser`
|
||||
|
||||
:ivar str save_notes: Human-readable config change notes
|
||||
|
||||
:ivar reverter: saves and reverts checkpoints
|
||||
:type reverter: :class:`letsencrypt.client.reverter.Reverter`
|
||||
|
||||
:ivar tup version: version of Nginx
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Nginx Web Server"
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("server-root", default=constants.DEFAULT_SERVER_ROOT,
|
||||
help="Nginx server root directory.")
|
||||
add("mod-ssl-conf", default=constants.DEFAULT_MOD_SSL_CONF,
|
||||
help="Contains standard nginx SSL directives.")
|
||||
add("ctl", default=constants.DEFAULT_CTL, help="Path to the "
|
||||
"'nginx' binary, used for 'configtest' and retrieving nginx "
|
||||
"version number.")
|
||||
|
||||
def __init__(self, config, version=None):
|
||||
"""Initialize an Nginx Configurator.
|
||||
|
||||
:param tup version: version of Nginx as a tuple (1, 4, 7)
|
||||
(used mostly for unittesting)
|
||||
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# Verify that all directories and files exist with proper permissions
|
||||
if os.geteuid() == 0:
|
||||
self._verify_setup()
|
||||
|
||||
# Files to save
|
||||
self.save_notes = ""
|
||||
|
||||
# Add number of outstanding challenges
|
||||
self._chall_out = 0
|
||||
|
||||
# These will be set in the prepare function
|
||||
self.parser = None
|
||||
self.version = version
|
||||
self._enhance_func = {} # TODO: Support at least redirects
|
||||
|
||||
# Set up reverter
|
||||
self.reverter = reverter.Reverter(config)
|
||||
self.reverter.recovery_routine()
|
||||
|
||||
# This is called in determine_authenticator and determine_installer
|
||||
def prepare(self):
|
||||
"""Prepare the authenticator/installer."""
|
||||
self.parser = parser.NginxParser(
|
||||
self.config.nginx_server_root,
|
||||
self.config.nginx_mod_ssl_conf)
|
||||
|
||||
# Set Version
|
||||
if self.version is None:
|
||||
self.version = self.get_version()
|
||||
|
||||
temp_install(self.config.nginx_mod_ssl_conf)
|
||||
|
||||
# Entry point in main.py for installing cert
|
||||
def deploy_cert(self, domain, cert, key, cert_chain=None):
|
||||
# pylint: disable=unused-argument
|
||||
"""Deploys certificate to specified virtual host.
|
||||
|
||||
.. note:: Aborts if the vhost is missing ssl_certificate or
|
||||
ssl_certificate_key.
|
||||
|
||||
.. note:: Nginx doesn't have a cert chain directive, so the last
|
||||
parameter is always ignored. It expects the cert file to have
|
||||
the concatenated chain.
|
||||
|
||||
.. note:: This doesn't save the config files!
|
||||
|
||||
:param str domain: domain to deploy certificate
|
||||
:param str cert: certificate filename
|
||||
:param str key: private key filename
|
||||
:param str cert_chain: certificate chain filename
|
||||
|
||||
"""
|
||||
vhost = self.choose_vhost(domain)
|
||||
directives = [['ssl_certificate', cert], ['ssl_certificate_key', key]]
|
||||
|
||||
try:
|
||||
self.parser.add_server_directives(vhost.filep, vhost.names,
|
||||
directives, True)
|
||||
logging.info("Deployed Certificate to VirtualHost %s for %s",
|
||||
vhost.filep, vhost.names)
|
||||
except errors.LetsEncryptMisconfigurationError:
|
||||
logging.warn(
|
||||
"Cannot find a cert or key directive in %s for %s",
|
||||
vhost.filep, vhost.names)
|
||||
logging.warn("VirtualHost was not modified")
|
||||
# Presumably break here so that the virtualhost is not modified
|
||||
return False
|
||||
|
||||
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
|
||||
(vhost.filep,
|
||||
", ".join(str(addr) for addr in vhost.addrs)))
|
||||
self.save_notes += "\tssl_certificate %s\n" % cert
|
||||
self.save_notes += "\tssl_certificate_key %s\n" % key
|
||||
|
||||
#######################
|
||||
# Vhost parsing methods
|
||||
#######################
|
||||
def choose_vhost(self, target_name):
|
||||
"""Chooses a virtual host based on the given domain name.
|
||||
|
||||
.. note:: This makes the vhost SSL-enabled if it isn't already. Follows
|
||||
Nginx's server block selection rules preferring blocks that are
|
||||
already SSL.
|
||||
|
||||
.. todo:: This should maybe return list if no obvious answer
|
||||
is presented.
|
||||
|
||||
.. todo:: The special name "$hostname" corresponds to the machine's
|
||||
hostname. Currently we just ignore this.
|
||||
|
||||
:param str target_name: domain name
|
||||
|
||||
:returns: ssl vhost associated with name
|
||||
:rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
vhost = None
|
||||
|
||||
matches = self._get_ranked_matches(target_name)
|
||||
if not matches:
|
||||
# No matches at all :'(
|
||||
pass
|
||||
elif matches[0]['rank'] in xrange(2, 6):
|
||||
# Wildcard match - need to find the longest one
|
||||
rank = matches[0]['rank']
|
||||
wildcards = [x for x in matches if x['rank'] == rank]
|
||||
vhost = max(wildcards, key=lambda x: len(x['name']))['vhost']
|
||||
else:
|
||||
vhost = matches[0]['vhost']
|
||||
|
||||
if vhost is not None:
|
||||
if not vhost.ssl:
|
||||
self._make_server_ssl(vhost.filep, vhost.names)
|
||||
|
||||
return vhost
|
||||
|
||||
def _get_ranked_matches(self, target_name):
|
||||
"""Returns a ranked list of vhosts that match target_name.
|
||||
|
||||
:param str target_name: The name to match
|
||||
:returns: list of dicts containing the vhost, the matching name, and
|
||||
the numerical rank
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
# Nginx chooses a matching server name for a request with precedence:
|
||||
# 1. exact name match
|
||||
# 2. longest wildcard name starting with *
|
||||
# 3. longest wildcard name ending with *
|
||||
# 4. first matching regex in order of appearance in the file
|
||||
matches = []
|
||||
for vhost in self.parser.get_vhosts():
|
||||
name_type, name = parser.get_best_match(target_name, vhost.names)
|
||||
if name_type == 'exact':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 0 if vhost.ssl else 1})
|
||||
elif name_type == 'wildcard_start':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 2 if vhost.ssl else 3})
|
||||
elif name_type == 'wildcard_end':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 4 if vhost.ssl else 5})
|
||||
elif name_type == 'regex':
|
||||
matches.append({'vhost': vhost,
|
||||
'name': name,
|
||||
'rank': 6 if vhost.ssl else 7})
|
||||
return sorted(matches, key=lambda x: x['rank'])
|
||||
|
||||
def get_all_names(self):
|
||||
"""Returns all names found in the Nginx Configuration.
|
||||
|
||||
:returns: All ServerNames, ServerAliases, and reverse DNS entries for
|
||||
virtual host addresses
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
all_names = set()
|
||||
|
||||
# Kept in same function to avoid multiple compilations of the regex
|
||||
priv_ip_regex = (r"(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|"
|
||||
r"(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)")
|
||||
private_ips = re.compile(priv_ip_regex)
|
||||
hostname_regex = r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$"
|
||||
hostnames = re.compile(hostname_regex, re.IGNORECASE)
|
||||
|
||||
for vhost in self.parser.get_vhosts():
|
||||
all_names.update(vhost.names)
|
||||
|
||||
for addr in vhost.addrs:
|
||||
host = addr.get_addr()
|
||||
if hostnames.match(host):
|
||||
# If it's a hostname, add it to the names.
|
||||
all_names.add(host)
|
||||
elif not private_ips.match(host):
|
||||
# If it isn't a private IP, do a reverse DNS lookup
|
||||
# TODO: IPv6 support
|
||||
try:
|
||||
socket.inet_aton(host)
|
||||
all_names.add(socket.gethostbyaddr(host)[0])
|
||||
except (socket.error, socket.herror, socket.timeout):
|
||||
continue
|
||||
|
||||
return all_names
|
||||
|
||||
def _make_server_ssl(self, filename, names):
|
||||
"""Makes a server SSL based on server_name and filename by adding
|
||||
a 'listen 443 ssl' directive to the server block.
|
||||
|
||||
.. todo:: Maybe this should create a new block instead of modifying
|
||||
the existing one?
|
||||
|
||||
:param str filename: The absolute filename of the config file.
|
||||
:param set names: The server names of the block to add SSL in
|
||||
|
||||
"""
|
||||
self.parser.add_server_directives(
|
||||
filename, names,
|
||||
[['listen', '443 ssl'],
|
||||
['ssl_certificate', '/etc/ssl/certs/ssl-cert-snakeoil.pem'],
|
||||
['ssl_certificate_key', '/etc/ssl/private/ssl-cert-snakeoil.key'],
|
||||
['include', self.parser.loc["ssl_options"]]])
|
||||
|
||||
def get_all_certs_keys(self):
|
||||
"""Find all existing keys, certs from configuration.
|
||||
|
||||
:returns: list of tuples with form [(cert, key, path)]
|
||||
cert - str path to certificate file
|
||||
key - str path to associated key file
|
||||
path - File path to configuration file.
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
return self.parser.get_all_certs_keys()
|
||||
|
||||
##################################
|
||||
# enhancement methods (IInstaller)
|
||||
##################################
|
||||
def supported_enhancements(self): # pylint: disable=no-self-use
|
||||
"""Returns currently supported enhancements."""
|
||||
return []
|
||||
|
||||
def enhance(self, domain, enhancement, options=None):
|
||||
"""Enhance configuration.
|
||||
|
||||
:param str domain: domain to enhance
|
||||
:param str enhancement: enhancement type defined in
|
||||
:const:`~letsencrypt.client.constants.ENHANCEMENTS`
|
||||
:param options: options for the enhancement
|
||||
See :const:`~letsencrypt.client.constants.ENHANCEMENTS`
|
||||
documentation for appropriate parameter.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self._enhance_func[enhancement](
|
||||
self.choose_vhost(domain), options)
|
||||
except (KeyError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unsupported enhancement: {0}".format(enhancement))
|
||||
except errors.LetsEncryptConfiguratorError:
|
||||
logging.warn("Failed %s for %s", enhancement, domain)
|
||||
|
||||
######################################
|
||||
# Nginx server management (IInstaller)
|
||||
######################################
|
||||
def restart(self):
|
||||
"""Restarts nginx server.
|
||||
|
||||
:returns: Success
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return nginx_restart(self.config.nginx_ctl)
|
||||
|
||||
def config_test(self): # pylint: disable=no-self-use
|
||||
"""Check the configuration of Nginx for errors.
|
||||
|
||||
:returns: Success
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[self.config.nginx_ctl, "-t"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
except (OSError, ValueError):
|
||||
logging.fatal("Unable to run nginx config test")
|
||||
sys.exit(1)
|
||||
|
||||
if proc.returncode != 0:
|
||||
# Enter recovery routine...
|
||||
logging.error("Config test failed")
|
||||
logging.error(stdout)
|
||||
logging.error(stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _verify_setup(self):
|
||||
"""Verify the setup to ensure safe operating environment.
|
||||
|
||||
Make sure that files/directories are setup with appropriate permissions
|
||||
Aim for defensive coding... make sure all input files
|
||||
have permissions of root.
|
||||
|
||||
"""
|
||||
uid = os.geteuid()
|
||||
le_util.make_or_verify_dir(self.config.work_dir, 0o755, uid)
|
||||
le_util.make_or_verify_dir(self.config.backup_dir, 0o755, uid)
|
||||
le_util.make_or_verify_dir(self.config.config_dir, 0o755, uid)
|
||||
|
||||
def get_version(self):
|
||||
"""Return version of Nginx Server.
|
||||
|
||||
Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))
|
||||
|
||||
:returns: version
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.LetsEncryptConfiguratorError:
|
||||
Unable to find Nginx version or version is unsupported
|
||||
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[self.config.nginx_ctl, "-V"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[1] # nginx prints output to stderr
|
||||
except (OSError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to run %s -V" % self.config.nginx_ctl)
|
||||
|
||||
version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE)
|
||||
version_matches = version_regex.findall(text)
|
||||
|
||||
sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE)
|
||||
sni_matches = sni_regex.findall(text)
|
||||
|
||||
ssl_regex = re.compile(r" --with-http_ssl_module")
|
||||
ssl_matches = ssl_regex.findall(text)
|
||||
|
||||
if not version_matches:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to find Nginx version")
|
||||
if not ssl_matches:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Nginx build is missing SSL module (--with-http_ssl_module).")
|
||||
if not sni_matches:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Nginx build doesn't support SNI")
|
||||
|
||||
nginx_version = tuple([int(i) for i in version_matches[0].split(".")])
|
||||
|
||||
# nginx < 0.8.21 doesn't use default_server
|
||||
if nginx_version < (0, 8, 21):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Nginx version must be 0.8.21+")
|
||||
|
||||
return nginx_version
|
||||
|
||||
def more_info(self):
|
||||
"""Human-readable string to help understand the module"""
|
||||
return (
|
||||
"Configures Nginx to authenticate and install HTTPS.{0}"
|
||||
"Server root: {root}{0}"
|
||||
"Version: {version}".format(
|
||||
os.linesep, root=self.parser.loc["root"],
|
||||
version=".".join(str(i) for i in self.version))
|
||||
)
|
||||
|
||||
###################################################
|
||||
# Wrapper functions for Reverter class (IInstaller)
|
||||
###################################################
|
||||
def save(self, title=None, temporary=False):
|
||||
"""Saves all changes to the configuration files.
|
||||
|
||||
:param str title: The title of the save. If a title is given, the
|
||||
configuration will be saved as a new checkpoint and put in a
|
||||
timestamped directory.
|
||||
|
||||
:param bool temporary: Indicates whether the changes made will
|
||||
be quickly reversed in the future (ie. challenges)
|
||||
|
||||
"""
|
||||
save_files = set(self.parser.parsed.keys())
|
||||
|
||||
# Create Checkpoint
|
||||
if temporary:
|
||||
self.reverter.add_to_temp_checkpoint(
|
||||
save_files, self.save_notes)
|
||||
else:
|
||||
self.reverter.add_to_checkpoint(save_files,
|
||||
self.save_notes)
|
||||
|
||||
# Change 'ext' to something else to not override existing conf files
|
||||
self.parser.filedump(ext='')
|
||||
if title and not temporary:
|
||||
self.reverter.finalize_checkpoint(title)
|
||||
|
||||
return True
|
||||
|
||||
def recovery_routine(self):
|
||||
"""Revert all previously modified files.
|
||||
|
||||
Reverts all modified files that have not been saved as a checkpoint
|
||||
|
||||
"""
|
||||
self.reverter.recovery_routine()
|
||||
self.parser.load()
|
||||
|
||||
def revert_challenge_config(self):
|
||||
"""Used to cleanup challenge configurations."""
|
||||
self.reverter.revert_temporary_config()
|
||||
self.parser.load()
|
||||
|
||||
def rollback_checkpoints(self, rollback=1):
|
||||
"""Rollback saved checkpoints.
|
||||
|
||||
:param int rollback: Number of checkpoints to revert
|
||||
|
||||
"""
|
||||
self.reverter.rollback_checkpoints(rollback)
|
||||
self.parser.load()
|
||||
|
||||
def view_config_changes(self):
|
||||
"""Show all of the configuration changes that have taken place."""
|
||||
self.reverter.view_config_changes()
|
||||
|
||||
###########################################################################
|
||||
# Challenges Section for IAuthenticator
|
||||
###########################################################################
|
||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||
"""Return list of challenge preferences."""
|
||||
return [challenges.DVSNI]
|
||||
|
||||
# Entry point in main.py for performing challenges
|
||||
def perform(self, achalls):
|
||||
"""Perform the configuration related challenge.
|
||||
|
||||
This function currently assumes all challenges will be fulfilled.
|
||||
If this turns out not to be the case in the future. Cleanup and
|
||||
outstanding challenges will have to be designed better.
|
||||
|
||||
"""
|
||||
self._chall_out += len(achalls)
|
||||
responses = [None] * len(achalls)
|
||||
nginx_dvsni = dvsni.NginxDvsni(self)
|
||||
|
||||
for i, achall in enumerate(achalls):
|
||||
if isinstance(achall, achallenges.DVSNI):
|
||||
# Currently also have dvsni hold associated index
|
||||
# of the challenge. This helps to put all of the responses back
|
||||
# together when they are all complete.
|
||||
nginx_dvsni.add_chall(achall, i)
|
||||
|
||||
sni_response = nginx_dvsni.perform()
|
||||
# Must restart in order to activate the challenges.
|
||||
# Handled here because we may be able to load up other challenge types
|
||||
self.restart()
|
||||
|
||||
# Go through all of the challenges and assign them to the proper place
|
||||
# in the responses return value. All responses must be in the same order
|
||||
# as the original challenges.
|
||||
for i, resp in enumerate(sni_response):
|
||||
responses[nginx_dvsni.indices[i]] = resp
|
||||
|
||||
return responses
|
||||
|
||||
# called after challenges are performed
|
||||
def cleanup(self, achalls):
|
||||
"""Revert all challenges."""
|
||||
self._chall_out -= len(achalls)
|
||||
|
||||
# If all of the challenges have been finished, clean up everything
|
||||
if self._chall_out <= 0:
|
||||
self.revert_challenge_config()
|
||||
self.restart()
|
||||
|
||||
|
||||
def nginx_restart(nginx_ctl):
|
||||
"""Restarts the Nginx Server.
|
||||
|
||||
:param str nginx_ctl: Path to the Nginx binary.
|
||||
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen([nginx_ctl, "-s", "reload"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
# Enter recovery routine...
|
||||
logging.error("Nginx Restart Failed!")
|
||||
logging.error(stdout)
|
||||
logging.error(stderr)
|
||||
return False
|
||||
|
||||
except (OSError, ValueError):
|
||||
logging.fatal(
|
||||
"Nginx Restart Failed - Please Check the Configuration")
|
||||
sys.exit(1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def temp_install(options_ssl):
|
||||
"""Temporary install for convenience."""
|
||||
# WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY
|
||||
# THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER
|
||||
# AND TAKEN OUT BEFORE RELEASE, INSTEAD
|
||||
# SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM.
|
||||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
if not os.path.isfile(options_ssl):
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, options_ssl)
|
||||
13
letsencrypt/client/plugins/nginx/constants.py
Normal file
13
letsencrypt/client/plugins/nginx/constants.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""nginx plugin constants."""
|
||||
import pkg_resources
|
||||
|
||||
|
||||
DEFAULT_SERVER_ROOT = "/etc/nginx"
|
||||
DEFAULT_MOD_SSL_CONF = "/etc/letsencrypt/options-ssl-nginx.conf"
|
||||
DEFAULT_CTL = "nginx"
|
||||
|
||||
|
||||
MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.plugins.nginx", "options-ssl.conf")
|
||||
"""Path to the Nginx mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
63
letsencrypt/client/plugins/nginx/dvsni.py
Normal file
63
letsencrypt/client/plugins/nginx/dvsni.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""NginxDVSNI"""
|
||||
import logging
|
||||
|
||||
from letsencrypt.client.plugins.apache.dvsni import ApacheDvsni
|
||||
|
||||
|
||||
class NginxDvsni(ApacheDvsni):
|
||||
"""Class performs DVSNI challenges within the Nginx configurator.
|
||||
|
||||
.. todo:: This is basically copied-and-pasted from the Apache equivalent.
|
||||
It doesn't actually work yet.
|
||||
|
||||
:ivar configurator: NginxConfigurator object
|
||||
:type configurator: :class:`~nginx.configurator.NginxConfigurator`
|
||||
|
||||
:ivar list achalls: Annotated :class:`~letsencrypt.client.achallenges.DVSNI`
|
||||
challenges.
|
||||
|
||||
:param list indices: Meant to hold indices of challenges in a
|
||||
larger array. NginxDvsni is capable of solving many challenges
|
||||
at once which causes an indexing issue within NginxConfigurator
|
||||
who must return all responses in order. Imagine NginxConfigurator
|
||||
maintaining state about where all of the SimpleHTTPS Challenges,
|
||||
Dvsni Challenges belong in the response array. This is an optional
|
||||
utility.
|
||||
|
||||
:param str challenge_conf: location of the challenge config file
|
||||
|
||||
"""
|
||||
|
||||
def perform(self):
|
||||
"""Perform a DVSNI challenge on Nginx."""
|
||||
if not self.achalls:
|
||||
return []
|
||||
|
||||
self.configurator.save()
|
||||
|
||||
addresses = []
|
||||
for achall in self.achalls:
|
||||
vhost = self.configurator.choose_vhost(achall.domain)
|
||||
if vhost is None:
|
||||
logging.error(
|
||||
"No nginx vhost exists with servername or alias of: %s",
|
||||
achall.domain)
|
||||
logging.error("No default 443 nginx vhost exists")
|
||||
logging.error("Please specify servernames in the Nginx config")
|
||||
return None
|
||||
else:
|
||||
addresses.append(list(vhost.addrs))
|
||||
|
||||
responses = []
|
||||
|
||||
# Create all of the challenge certs
|
||||
# for achall in self.achalls:
|
||||
# responses.append(self._setup_challenge_cert(achall))
|
||||
|
||||
# Setup the configuration
|
||||
# self._mod_config(addresses)
|
||||
|
||||
# Save reversible changes
|
||||
self.configurator.save("SNI Challenge", True)
|
||||
|
||||
return responses
|
||||
130
letsencrypt/client/plugins/nginx/nginxparser.py
Normal file
130
letsencrypt/client/plugins/nginx/nginxparser.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""Very low-level nginx config parser based on pyparsing."""
|
||||
import string
|
||||
|
||||
from pyparsing import (
|
||||
Literal, White, Word, alphanums, CharsNotIn, Forward, Group,
|
||||
Optional, OneOrMore, ZeroOrMore, pythonStyleComment)
|
||||
|
||||
|
||||
class RawNginxParser(object):
|
||||
# pylint: disable=expression-not-assigned
|
||||
"""A class that parses nginx configuration with pyparsing."""
|
||||
|
||||
# constants
|
||||
left_bracket = Literal("{").suppress()
|
||||
right_bracket = Literal("}").suppress()
|
||||
semicolon = Literal(";").suppress()
|
||||
space = White().suppress()
|
||||
key = Word(alphanums + "_/")
|
||||
value = CharsNotIn("{};,")
|
||||
location = CharsNotIn("{};," + string.whitespace)
|
||||
# modifier for location uri [ = | ~ | ~* | ^~ ]
|
||||
modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~")
|
||||
|
||||
# rules
|
||||
assignment = (key + Optional(space + value) + semicolon)
|
||||
block = Forward()
|
||||
|
||||
block << Group(
|
||||
Group(key + Optional(space + modifier) + Optional(space + location))
|
||||
+ left_bracket
|
||||
+ Group(ZeroOrMore(Group(assignment) | block))
|
||||
+ right_bracket)
|
||||
|
||||
script = OneOrMore(Group(assignment) | block).ignore(pythonStyleComment)
|
||||
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
|
||||
def parse(self):
|
||||
"""Returns the parsed tree."""
|
||||
return self.script.parseString(self.source)
|
||||
|
||||
def as_list(self):
|
||||
"""Returns the parsed tree as a list."""
|
||||
return self.parse().asList()
|
||||
|
||||
|
||||
class RawNginxDumper(object):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""A class that dumps nginx configuration from the provided tree."""
|
||||
def __init__(self, blocks, indentation=4):
|
||||
self.blocks = blocks
|
||||
self.indentation = indentation
|
||||
|
||||
def __iter__(self, blocks=None, current_indent=0, spacer=' '):
|
||||
"""Iterates the dumped nginx content."""
|
||||
blocks = blocks or self.blocks
|
||||
for key, values in blocks:
|
||||
if current_indent:
|
||||
yield spacer
|
||||
indentation = spacer * current_indent
|
||||
if isinstance(key, list):
|
||||
yield indentation + spacer.join(key) + ' {'
|
||||
for parameter in values:
|
||||
if isinstance(parameter[0], list):
|
||||
dumped = self.__iter__(
|
||||
[parameter],
|
||||
current_indent + self.indentation)
|
||||
for line in dumped:
|
||||
yield line
|
||||
else:
|
||||
dumped = spacer.join(parameter) + ';'
|
||||
yield spacer * (
|
||||
current_indent + self.indentation) + dumped
|
||||
|
||||
yield indentation + '}'
|
||||
else:
|
||||
yield spacer * current_indent + key + spacer + values + ';'
|
||||
|
||||
def as_string(self):
|
||||
"""Return the parsed block as a string."""
|
||||
return '\n'.join(self)
|
||||
|
||||
|
||||
# Shortcut functions to respect Python's serialization interface
|
||||
# (like pyyaml, picker or json)
|
||||
|
||||
def loads(source):
|
||||
"""Parses from a string.
|
||||
|
||||
:param str souce: The string to parse
|
||||
:returns: The parsed tree
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
return RawNginxParser(source).as_list()
|
||||
|
||||
|
||||
def load(_file):
|
||||
"""Parses from a file.
|
||||
|
||||
:param file _file: The file to parse
|
||||
:returns: The parsed tree
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
return loads(_file.read())
|
||||
|
||||
|
||||
def dumps(blocks, indentation=4):
|
||||
"""Dump to a string.
|
||||
|
||||
:param list block: The parsed tree
|
||||
:param int indentation: The number of spaces to indent
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return RawNginxDumper(blocks, indentation).as_string()
|
||||
|
||||
|
||||
def dump(blocks, _file, indentation=4):
|
||||
"""Dump to a file.
|
||||
|
||||
:param list block: The parsed tree
|
||||
:param file _file: The file to dump to
|
||||
:param int indentation: The number of spaces to indent
|
||||
:rtype: NoneType
|
||||
|
||||
"""
|
||||
return _file.write(dumps(blocks, indentation))
|
||||
125
letsencrypt/client/plugins/nginx/obj.py
Normal file
125
letsencrypt/client/plugins/nginx/obj.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""Module contains classes used by the Nginx Configurator."""
|
||||
import re
|
||||
|
||||
from letsencrypt.client.plugins.apache.obj import Addr as ApacheAddr
|
||||
|
||||
|
||||
class Addr(ApacheAddr):
|
||||
"""Represents an Nginx address, i.e. what comes after the 'listen'
|
||||
directive.
|
||||
|
||||
According to http://nginx.org/en/docs/http/ngx_http_core_module.html#listen,
|
||||
this may be address[:port], port, or unix:path. The latter is ignored here.
|
||||
|
||||
The default value if no directive is specified is *:80 (superuser) or
|
||||
*:8000 (otherwise). If no port is specified, the default is 80. If no
|
||||
address is specified, listen on all addresses.
|
||||
|
||||
.. todo:: Old-style nginx configs define SSL vhosts in a separate block
|
||||
instead of using 'ssl' in the listen directive
|
||||
|
||||
:param str addr: addr part of vhost address, may be hostname, IPv4, IPv6,
|
||||
"", or "*"
|
||||
:param str port: port number or "*" or ""
|
||||
:param bool ssl: Whether the directive includes 'ssl'
|
||||
:param bool default: Whether the directive includes 'default_server'
|
||||
|
||||
"""
|
||||
def __init__(self, host, port, ssl, default):
|
||||
super(Addr, self).__init__((host, port))
|
||||
self.ssl = ssl
|
||||
self.default = default
|
||||
|
||||
@classmethod
|
||||
def fromstring(cls, str_addr):
|
||||
"""Initialize Addr from string."""
|
||||
parts = str_addr.split(' ')
|
||||
ssl = False
|
||||
default = False
|
||||
host = ''
|
||||
port = ''
|
||||
|
||||
# The first part must be the address
|
||||
addr = parts.pop(0)
|
||||
|
||||
# Ignore UNIX-domain sockets
|
||||
if addr.startswith('unix:'):
|
||||
return None
|
||||
|
||||
tup = addr.partition(':')
|
||||
if re.match(r'^\d+$', tup[0]):
|
||||
# This is a bare port, not a hostname. E.g. listen 80
|
||||
host = ''
|
||||
port = tup[0]
|
||||
else:
|
||||
# This is a host-port tuple. E.g. listen 127.0.0.1:*
|
||||
host = tup[0]
|
||||
port = tup[2]
|
||||
|
||||
# The rest of the parts are options; we only care about ssl and default
|
||||
while len(parts) > 0:
|
||||
nextpart = parts.pop()
|
||||
if nextpart == 'ssl':
|
||||
ssl = True
|
||||
elif nextpart == 'default_server':
|
||||
default = True
|
||||
|
||||
return cls(host, port, ssl, default)
|
||||
|
||||
def __str__(self):
|
||||
if self.tup[0] and self.tup[1]:
|
||||
return "%s:%s" % self.tup
|
||||
elif self.tup[0]:
|
||||
return self.tup[0]
|
||||
else:
|
||||
return self.tup[1]
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return (self.tup == other.tup and
|
||||
self.ssl == other.ssl and
|
||||
self.default == other.default)
|
||||
return False
|
||||
|
||||
|
||||
class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
"""Represents an Nginx Virtualhost.
|
||||
|
||||
:ivar str filep: file path of VH
|
||||
:ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`)
|
||||
:ivar set names: Server names/aliases of vhost
|
||||
(:class:`list` of :class:`str`)
|
||||
:ivar array raw: The raw form of the parsed server block
|
||||
|
||||
:ivar bool ssl: SSLEngine on in vhost
|
||||
:ivar bool enabled: Virtual host is enabled
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, filep, addrs, ssl, enabled, names, raw):
|
||||
# pylint: disable=too-many-arguments
|
||||
"""Initialize a VH."""
|
||||
self.filep = filep
|
||||
self.addrs = addrs
|
||||
self.names = names
|
||||
self.ssl = ssl
|
||||
self.enabled = enabled
|
||||
self.raw = raw
|
||||
|
||||
def __str__(self):
|
||||
addr_str = ", ".join(str(addr) for addr in self.addrs)
|
||||
return ("file: %s\n"
|
||||
"addrs: %s\n"
|
||||
"names: %s\n"
|
||||
"ssl: %s\n"
|
||||
"enabled: %s" % (self.filep, addr_str,
|
||||
self.names, self.ssl, self.enabled))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return (self.filep == other.filep and
|
||||
list(self.addrs) == list(other.addrs) and
|
||||
self.names == other.names and
|
||||
self.ssl == other.ssl and self.enabled == other.enabled)
|
||||
|
||||
return False
|
||||
8
letsencrypt/client/plugins/nginx/options-ssl.conf
Normal file
8
letsencrypt/client/plugins/nginx/options-ssl.conf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 1440m;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# Using list of ciphers from "Bulletproof SSL and TLS"
|
||||
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA";
|
||||
484
letsencrypt/client/plugins/nginx/parser.py
Normal file
484
letsencrypt/client/plugins/nginx/parser.py
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
"""NginxParser is a member object of the NginxConfigurator class."""
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import pyparsing
|
||||
import re
|
||||
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client.plugins.nginx import obj
|
||||
from letsencrypt.client.plugins.nginx.nginxparser import dump, load
|
||||
|
||||
|
||||
class NginxParser(object):
|
||||
"""Class handles the fine details of parsing the Nginx Configuration.
|
||||
|
||||
:ivar str root: Normalized abosulte path to the server root
|
||||
directory. Without trailing slash.
|
||||
:ivar dict parsed: Mapping of file paths to parsed trees
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, root, ssl_options):
|
||||
self.parsed = {}
|
||||
self.root = os.path.abspath(root)
|
||||
self.loc = self._set_locations(ssl_options)
|
||||
|
||||
# Parse nginx.conf and included files.
|
||||
# TODO: Check sites-available/ as well. For now, the configurator does
|
||||
# not enable sites from there.
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
"""Loads Nginx files into a parsed tree.
|
||||
|
||||
"""
|
||||
self._parse_recursively(self.loc["root"])
|
||||
|
||||
def _parse_recursively(self, filepath):
|
||||
"""Parses nginx config files recursively by looking at 'include'
|
||||
directives inside 'http' and 'server' blocks. Note that this only
|
||||
reads Nginx files that potentially declare a virtual host.
|
||||
|
||||
.. todo:: Can Nginx 'virtual hosts' be defined somewhere other than in
|
||||
the server context?
|
||||
|
||||
:param str filepath: The path to the files to parse, as a glob
|
||||
|
||||
"""
|
||||
filepath = self.abs_path(filepath)
|
||||
trees = self._parse_files(filepath)
|
||||
for tree in trees:
|
||||
for entry in tree:
|
||||
if _is_include_directive(entry):
|
||||
# Parse the top-level included file
|
||||
self._parse_recursively(entry[1])
|
||||
elif entry[0] == ['http'] or entry[0] == ['server']:
|
||||
# Look for includes in the top-level 'http'/'server' context
|
||||
for subentry in entry[1]:
|
||||
if _is_include_directive(subentry):
|
||||
self._parse_recursively(subentry[1])
|
||||
elif entry[0] == ['http'] and subentry[0] == ['server']:
|
||||
# Look for includes in a 'server' context within
|
||||
# an 'http' context
|
||||
for server_entry in subentry[1]:
|
||||
if _is_include_directive(server_entry):
|
||||
self._parse_recursively(server_entry[1])
|
||||
|
||||
def abs_path(self, path):
|
||||
"""Converts a relative path to an absolute path relative to the root.
|
||||
Does nothing for paths that are already absolute.
|
||||
|
||||
:param str path: The path
|
||||
:returns: The absolute path
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if not os.path.isabs(path):
|
||||
return os.path.join(self.root, path)
|
||||
else:
|
||||
return path
|
||||
|
||||
def get_vhosts(self):
|
||||
# pylint: disable=cell-var-from-loop
|
||||
"""Gets list of all 'virtual hosts' found in Nginx configuration.
|
||||
Technically this is a misnomer because Nginx does not have virtual
|
||||
hosts, it has 'server blocks'.
|
||||
|
||||
:returns: List of
|
||||
:class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` objects
|
||||
found in configuration
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
enabled = True # We only look at enabled vhosts for now
|
||||
vhosts = []
|
||||
servers = {}
|
||||
|
||||
for filename in self.parsed:
|
||||
tree = self.parsed[filename]
|
||||
servers[filename] = []
|
||||
srv = servers[filename] # workaround undefined loop var in lambdas
|
||||
|
||||
# Find all the server blocks
|
||||
_do_for_subarray(tree, lambda x: x[0] == ['server'],
|
||||
lambda x: srv.append(x[1]))
|
||||
|
||||
# Find 'include' statements in server blocks and append their trees
|
||||
for i, server in enumerate(servers[filename]):
|
||||
new_server = self._get_included_directives(server)
|
||||
servers[filename][i] = new_server
|
||||
|
||||
for filename in servers:
|
||||
for server in servers[filename]:
|
||||
# Parse the server block into a VirtualHost object
|
||||
parsed_server = _parse_server(server)
|
||||
vhost = obj.VirtualHost(filename,
|
||||
parsed_server['addrs'],
|
||||
parsed_server['ssl'],
|
||||
enabled,
|
||||
parsed_server['names'],
|
||||
server)
|
||||
vhosts.append(vhost)
|
||||
|
||||
return vhosts
|
||||
|
||||
def _get_included_directives(self, block):
|
||||
"""Returns array with the "include" directives expanded out by
|
||||
concatenating the contents of the included file to the block.
|
||||
|
||||
:param list block:
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
result = list(block) # Copy the list to keep self.parsed idempotent
|
||||
for directive in block:
|
||||
if _is_include_directive(directive):
|
||||
included_files = glob.glob(
|
||||
self.abs_path(directive[1]))
|
||||
for incl in included_files:
|
||||
try:
|
||||
result.extend(self.parsed[incl])
|
||||
except KeyError:
|
||||
pass
|
||||
return result
|
||||
|
||||
def _parse_files(self, filepath, override=False):
|
||||
"""Parse files from a glob
|
||||
|
||||
:param str filepath: Nginx config file path
|
||||
:param bool override: Whether to parse a file that has been parsed
|
||||
:returns: list of parsed tree structures
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
files = glob.glob(filepath)
|
||||
trees = []
|
||||
for item in files:
|
||||
if item in self.parsed and not override:
|
||||
continue
|
||||
try:
|
||||
with open(item) as _file:
|
||||
parsed = load(_file)
|
||||
self.parsed[item] = parsed
|
||||
trees.append(parsed)
|
||||
except IOError:
|
||||
logging.warn("Could not open file: %s", item)
|
||||
except pyparsing.ParseException:
|
||||
logging.warn("Could not parse file: %s", item)
|
||||
return trees
|
||||
|
||||
def _set_locations(self, ssl_options):
|
||||
"""Set default location for directives.
|
||||
|
||||
Locations are given as file_paths
|
||||
.. todo:: Make sure that files are included
|
||||
|
||||
"""
|
||||
root = self._find_config_root()
|
||||
default = root
|
||||
|
||||
nginx_temp = os.path.join(self.root, "nginx_ports.conf")
|
||||
if os.path.isfile(nginx_temp):
|
||||
listen = nginx_temp
|
||||
name = nginx_temp
|
||||
else:
|
||||
listen = default
|
||||
name = default
|
||||
|
||||
return {"root": root, "default": default, "listen": listen,
|
||||
"name": name, "ssl_options": ssl_options}
|
||||
|
||||
def _find_config_root(self):
|
||||
"""Find the Nginx Configuration Root file."""
|
||||
location = ['nginx.conf']
|
||||
|
||||
for name in location:
|
||||
if os.path.isfile(os.path.join(self.root, name)):
|
||||
return os.path.join(self.root, name)
|
||||
|
||||
raise errors.LetsEncryptNoInstallationError(
|
||||
"Could not find configuration root")
|
||||
|
||||
def filedump(self, ext='tmp'):
|
||||
"""Dumps parsed configurations into files.
|
||||
|
||||
:param str ext: The file extension to use for the dumped files. If
|
||||
empty, this overrides the existing conf files.
|
||||
|
||||
"""
|
||||
for filename in self.parsed:
|
||||
tree = self.parsed[filename]
|
||||
if ext:
|
||||
filename = filename + os.path.extsep + ext
|
||||
try:
|
||||
with open(filename, 'w') as _file:
|
||||
dump(tree, _file)
|
||||
except IOError:
|
||||
logging.error("Could not open file for writing: %s", filename)
|
||||
|
||||
def _has_server_names(self, entry, names):
|
||||
"""Checks if a server block has the given set of server_names. This
|
||||
is the primary way of identifying server blocks in the configurator.
|
||||
Returns false if 'entry' doesn't look like a server block at all.
|
||||
|
||||
..todo :: Doesn't match server blocks whose server_name directives are
|
||||
split across multiple conf files.
|
||||
|
||||
:param list entry: The block to search
|
||||
:param set names: The names to match
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if len(names) == 0:
|
||||
# Nothing to identify blocks with
|
||||
return False
|
||||
|
||||
if not isinstance(entry, list):
|
||||
# Can't be a server block
|
||||
return False
|
||||
|
||||
new_entry = self._get_included_directives(entry)
|
||||
server_names = set()
|
||||
for item in new_entry:
|
||||
if not isinstance(item, list):
|
||||
# Can't be a server block
|
||||
return False
|
||||
|
||||
if item[0] == 'server_name':
|
||||
server_names.update(_get_servernames(item[1]))
|
||||
|
||||
return server_names == names
|
||||
|
||||
def add_server_directives(self, filename, names, directives,
|
||||
replace=False):
|
||||
"""Add or replace directives in server blocks whose server_name set
|
||||
is 'names'. If replace is True, this raises a misconfiguration error
|
||||
if the directive does not already exist.
|
||||
|
||||
..todo :: Doesn't match server blocks whose server_name directives are
|
||||
split across multiple conf files.
|
||||
|
||||
:param str filename: The absolute filename of the config file
|
||||
:param set names: The server_name to match
|
||||
:param list directives: The directives to add
|
||||
:param bool replace: Whether to only replace existing directives
|
||||
|
||||
"""
|
||||
if replace:
|
||||
_do_for_subarray(self.parsed[filename],
|
||||
lambda x: self._has_server_names(x, names),
|
||||
lambda x: _replace_directives(x, directives))
|
||||
else:
|
||||
_do_for_subarray(self.parsed[filename],
|
||||
lambda x: self._has_server_names(x, names),
|
||||
lambda x: x.extend(directives))
|
||||
|
||||
def get_all_certs_keys(self):
|
||||
"""Gets all certs and keys in the nginx config.
|
||||
|
||||
:returns: list of tuples with form [(cert, key, path)]
|
||||
cert - str path to certificate file
|
||||
key - str path to associated key file
|
||||
path - File path to configuration file.
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
c_k = set()
|
||||
vhosts = self.get_vhosts()
|
||||
for vhost in vhosts:
|
||||
tup = [None, None, vhost.filep]
|
||||
if vhost.ssl:
|
||||
for directive in vhost.raw:
|
||||
if directive[0] == 'ssl_certificate':
|
||||
tup[0] = directive[1]
|
||||
elif directive[0] == 'ssl_certificate_key':
|
||||
tup[1] = directive[1]
|
||||
if tup[0] is not None and tup[1] is not None:
|
||||
c_k.add(tuple(tup))
|
||||
return c_k
|
||||
|
||||
|
||||
def _do_for_subarray(entry, condition, func):
|
||||
"""Executes a function for a subarray of a nested array if it matches
|
||||
the given condition.
|
||||
|
||||
:param list entry: The list to iterate over
|
||||
:param function condition: Returns true iff func should be executed on item
|
||||
:param function func: The function to call for each matching item
|
||||
|
||||
"""
|
||||
if isinstance(entry, list):
|
||||
if condition(entry):
|
||||
func(entry)
|
||||
else:
|
||||
for item in entry:
|
||||
_do_for_subarray(item, condition, func)
|
||||
|
||||
|
||||
def get_best_match(target_name, names):
|
||||
"""Finds the best match for target_name out of names using the Nginx
|
||||
name-matching rules (exact > longest wildcard starting with * >
|
||||
longest wildcard ending with * > regex).
|
||||
|
||||
:param str target_name: The name to match
|
||||
:param set names: The candidate server names
|
||||
:returns: Tuple of (type of match, the name that matched)
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
exact = []
|
||||
wildcard_start = []
|
||||
wildcard_end = []
|
||||
regex = []
|
||||
|
||||
for name in names:
|
||||
if _exact_match(target_name, name):
|
||||
exact.append(name)
|
||||
elif _wildcard_match(target_name, name, True):
|
||||
wildcard_start.append(name)
|
||||
elif _wildcard_match(target_name, name, False):
|
||||
wildcard_end.append(name)
|
||||
elif _regex_match(target_name, name):
|
||||
regex.append(name)
|
||||
|
||||
if len(exact) > 0:
|
||||
# There can be more than one exact match; e.g. eff.org, .eff.org
|
||||
match = min(exact, key=len)
|
||||
return ('exact', match)
|
||||
if len(wildcard_start) > 0:
|
||||
# Return the longest wildcard
|
||||
match = max(wildcard_start, key=len)
|
||||
return ('wildcard_start', match)
|
||||
if len(wildcard_end) > 0:
|
||||
# Return the longest wildcard
|
||||
match = max(wildcard_end, key=len)
|
||||
return ('wildcard_end', match)
|
||||
if len(regex) > 0:
|
||||
# Just return the first one for now
|
||||
match = regex[0]
|
||||
return ('regex', match)
|
||||
|
||||
return (None, None)
|
||||
|
||||
|
||||
def _exact_match(target_name, name):
|
||||
return target_name == name or '.' + target_name == name
|
||||
|
||||
|
||||
def _wildcard_match(target_name, name, start):
|
||||
# Degenerate case
|
||||
if name == '*':
|
||||
return True
|
||||
|
||||
parts = target_name.split('.')
|
||||
match_parts = name.split('.')
|
||||
|
||||
# If the domain ends in a wildcard, do the match procedure in reverse
|
||||
if not start:
|
||||
parts.reverse()
|
||||
match_parts.reverse()
|
||||
|
||||
if len(match_parts) == 0:
|
||||
return False
|
||||
|
||||
# The first part must be a wildcard or blank, e.g. '.eff.org'
|
||||
first = match_parts.pop(0)
|
||||
if first != '*' and first != '':
|
||||
return False
|
||||
|
||||
target_name = '.'.join(parts)
|
||||
name = '.'.join(match_parts)
|
||||
|
||||
# Ex: www.eff.org matches *.eff.org, eff.org does not match *.eff.org
|
||||
return target_name.endswith('.' + name)
|
||||
|
||||
|
||||
def _regex_match(target_name, name):
|
||||
# Must start with a tilde
|
||||
if len(name) < 2 or name[0] != '~':
|
||||
return False
|
||||
|
||||
# After tilde is a perl-compatible regex
|
||||
try:
|
||||
regex = re.compile(name[1:])
|
||||
if re.match(regex, target_name):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except re.error:
|
||||
# perl-compatible regexes are sometimes not recognized by python
|
||||
return False
|
||||
|
||||
|
||||
def _is_include_directive(entry):
|
||||
"""Checks if an nginx parsed entry is an 'include' directive.
|
||||
|
||||
:param list entry: the parsed entry
|
||||
:returns: Whether it's an 'include' directive
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return (isinstance(entry, list) and
|
||||
entry[0] == 'include' and len(entry) == 2 and
|
||||
isinstance(entry[1], str))
|
||||
|
||||
|
||||
def _get_servernames(names):
|
||||
"""Turns a server_name string into a list of server names
|
||||
|
||||
:param str names: server names
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
whitespace_re = re.compile(r'\s+')
|
||||
names = re.sub(whitespace_re, ' ', names)
|
||||
return names.split(' ')
|
||||
|
||||
|
||||
def _parse_server(server):
|
||||
"""Parses a list of server directives.
|
||||
|
||||
:param list server: list of directives in a server block
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
parsed_server = {}
|
||||
parsed_server['addrs'] = set()
|
||||
parsed_server['ssl'] = False
|
||||
parsed_server['names'] = set()
|
||||
|
||||
for directive in server:
|
||||
if directive[0] == 'listen':
|
||||
addr = obj.Addr.fromstring(directive[1])
|
||||
parsed_server['addrs'].add(addr)
|
||||
if not parsed_server['ssl'] and addr.ssl:
|
||||
parsed_server['ssl'] = True
|
||||
elif directive[0] == 'server_name':
|
||||
parsed_server['names'].update(
|
||||
_get_servernames(directive[1]))
|
||||
|
||||
return parsed_server
|
||||
|
||||
|
||||
def _replace_directives(block, directives):
|
||||
"""Replaces directives in a block. If the directive doesn't exist in
|
||||
the entry already, raises a misconfiguration error.
|
||||
|
||||
..todo :: Find directives that are in included files.
|
||||
|
||||
:param list block: The block to replace in
|
||||
:param list directives: The new directives.
|
||||
"""
|
||||
for directive in directives:
|
||||
changed = False
|
||||
if len(directive) == 0:
|
||||
continue
|
||||
for index, line in enumerate(block):
|
||||
if len(line) > 0 and line[0] == directive[0]:
|
||||
block[index] = directive
|
||||
changed = True
|
||||
if not changed:
|
||||
raise errors.LetsEncryptMisconfigurationError(
|
||||
'LetsEncrypt expected directive for %s in the Nginx config '
|
||||
'but did not find it.' % directive[0])
|
||||
1
letsencrypt/client/plugins/nginx/tests/__init__.py
Normal file
1
letsencrypt/client/plugins/nginx/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Let's Encrypt Nginx Tests"""
|
||||
264
letsencrypt/client/plugins/nginx/tests/configurator_test.py
Normal file
264
letsencrypt/client/plugins/nginx/tests/configurator_test.py
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
"""Test for letsencrypt.client.plugins.nginx.configurator."""
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.plugins.nginx.tests import util
|
||||
|
||||
|
||||
class NginxConfiguratorTest(util.NginxTest):
|
||||
"""Test a semi complex vhost configuration."""
|
||||
|
||||
def setUp(self):
|
||||
super(NginxConfiguratorTest, self).setUp()
|
||||
|
||||
self.config = util.get_nginx_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir,
|
||||
self.ssl_options)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
def test_prepare(self):
|
||||
self.assertEquals((1, 6, 2), self.config.version)
|
||||
self.assertEquals(5, len(self.config.parser.parsed))
|
||||
|
||||
def test_get_all_names(self):
|
||||
names = self.config.get_all_names()
|
||||
self.assertEqual(names, set(
|
||||
["*.www.foo.com", "somename", "another.alias",
|
||||
"alias", "localhost", ".example.com", r"~^(www\.)?(example|bar)\.",
|
||||
"155.225.50.69.nephoscale.net", "*.www.example.com",
|
||||
"example.*", "www.example.org", "myhost"]))
|
||||
|
||||
def test_supported_enhancements(self):
|
||||
self.assertEqual([], self.config.supported_enhancements())
|
||||
|
||||
def test_enhance(self):
|
||||
self.assertRaises(errors.LetsEncryptConfiguratorError,
|
||||
self.config.enhance,
|
||||
'myhost',
|
||||
'redirect')
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertEqual([challenges.DVSNI],
|
||||
self.config.get_chall_pref('myhost'))
|
||||
|
||||
def test_save(self):
|
||||
filep = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
self.config.parser.add_server_directives(
|
||||
filep, set(['.example.com', 'example.*']),
|
||||
[['listen', '443 ssl']])
|
||||
self.config.save()
|
||||
|
||||
# pylint: disable=protected-access
|
||||
parsed = self.config.parser._parse_files(filep, override=True)
|
||||
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
['server_name', '.example.com'],
|
||||
['server_name', 'example.*'],
|
||||
['listen', '443 ssl']]]],
|
||||
parsed[0])
|
||||
|
||||
def test_choose_vhost(self):
|
||||
localhost_conf = set(['localhost', r'~^(www\.)?(example|bar)\.'])
|
||||
server_conf = set(['somename', 'another.alias', 'alias'])
|
||||
example_conf = set(['.example.com', 'example.*'])
|
||||
foo_conf = set(['*.www.foo.com', '*.www.example.com'])
|
||||
|
||||
results = {'localhost': localhost_conf,
|
||||
'alias': server_conf,
|
||||
'example.com': example_conf,
|
||||
'example.com.uk.test': example_conf,
|
||||
'www.example.com': example_conf,
|
||||
'test.www.example.com': foo_conf,
|
||||
'abc.www.foo.com': foo_conf,
|
||||
'www.bar.co.uk': localhost_conf}
|
||||
bad_results = ['www.foo.com', 'example', 't.www.bar.co',
|
||||
'69.255.225.155']
|
||||
|
||||
for name in results:
|
||||
self.assertEqual(results[name],
|
||||
self.config.choose_vhost(name).names)
|
||||
for name in bad_results:
|
||||
self.assertEqual(None, self.config.choose_vhost(name))
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue('nginx.conf' in self.config.more_info())
|
||||
|
||||
def test_deploy_cert(self):
|
||||
server_conf = self.config.parser.abs_path('server.conf')
|
||||
nginx_conf = self.config.parser.abs_path('nginx.conf')
|
||||
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
|
||||
# Get the default 443 vhost
|
||||
self.config.deploy_cert(
|
||||
"www.example.com",
|
||||
"example/cert.pem", "example/key.pem")
|
||||
self.config.deploy_cert(
|
||||
"another.alias",
|
||||
"/etc/nginx/cert.pem", "/etc/nginx/key.pem")
|
||||
self.config.save()
|
||||
|
||||
self.config.parser.load()
|
||||
|
||||
self.assertEqual([[['server'],
|
||||
[['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
['server_name', '.example.com'],
|
||||
['server_name', 'example.*'],
|
||||
['listen', '443 ssl'],
|
||||
['ssl_certificate', 'example/cert.pem'],
|
||||
['ssl_certificate_key', 'example/key.pem'],
|
||||
['include',
|
||||
self.config.parser.loc["ssl_options"]]]]],
|
||||
self.config.parser.parsed[example_conf])
|
||||
self.assertEqual([['server_name', 'somename alias another.alias']],
|
||||
self.config.parser.parsed[server_conf])
|
||||
self.assertEqual([['server'],
|
||||
[['listen', '8000'],
|
||||
['listen', 'somename:8080'],
|
||||
['include', 'server.conf'],
|
||||
[['location', '/'],
|
||||
[['root', 'html'],
|
||||
['index', 'index.html index.htm']]],
|
||||
['listen', '443 ssl'],
|
||||
['ssl_certificate', '/etc/nginx/cert.pem'],
|
||||
['ssl_certificate_key', '/etc/nginx/key.pem'],
|
||||
['include',
|
||||
self.config.parser.loc["ssl_options"]]]],
|
||||
self.config.parser.parsed[nginx_conf][-1][-1][-1])
|
||||
|
||||
def test_get_all_certs_keys(self):
|
||||
nginx_conf = self.config.parser.abs_path('nginx.conf')
|
||||
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
|
||||
# Get the default 443 vhost
|
||||
self.config.deploy_cert(
|
||||
"www.example.com",
|
||||
"example/cert.pem", "example/key.pem")
|
||||
self.config.deploy_cert(
|
||||
"another.alias",
|
||||
"/etc/nginx/cert.pem", "/etc/nginx/key.pem")
|
||||
self.config.save()
|
||||
|
||||
self.config.parser.load()
|
||||
self.assertEqual(set([
|
||||
('example/cert.pem', 'example/key.pem', example_conf),
|
||||
('/etc/nginx/cert.pem', '/etc/nginx/key.pem', nginx_conf),
|
||||
]), self.config.get_all_certs_keys())
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.nginx.configurator."
|
||||
"dvsni.NginxDvsni.perform")
|
||||
@mock.patch("letsencrypt.client.plugins.nginx.configurator."
|
||||
"NginxConfigurator.restart")
|
||||
def test_perform(self, mock_restart, mock_dvsni_perform):
|
||||
# Only tests functionality specific to configurator.perform
|
||||
# Note: As more challenges are offered this will have to be expanded
|
||||
auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem)
|
||||
achall1 = achallenges.DVSNI(
|
||||
chall=challenges.DVSNI(
|
||||
r="foo",
|
||||
nonce="bar"),
|
||||
domain="localhost", key=auth_key)
|
||||
achall2 = achallenges.DVSNI(
|
||||
chall=challenges.DVSNI(
|
||||
r="abc",
|
||||
nonce="def"),
|
||||
domain="example.com", key=auth_key)
|
||||
|
||||
dvsni_ret_val = [
|
||||
challenges.DVSNIResponse(s="irrelevant"),
|
||||
challenges.DVSNIResponse(s="arbitrary"),
|
||||
]
|
||||
|
||||
mock_dvsni_perform.return_value = dvsni_ret_val
|
||||
responses = self.config.perform([achall1, achall2])
|
||||
|
||||
self.assertEqual(mock_dvsni_perform.call_count, 1)
|
||||
self.assertEqual(responses, dvsni_ret_val)
|
||||
self.assertEqual(mock_restart.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.nginx.configurator."
|
||||
"subprocess.Popen")
|
||||
def test_get_version(self, mock_popen):
|
||||
mock_popen().communicate.return_value = (
|
||||
"", "\n".join(["nginx version: nginx/1.4.2",
|
||||
"built by clang 6.0 (clang-600.0.56)"
|
||||
" (based on LLVM 3.5svn)",
|
||||
"TLS SNI support enabled",
|
||||
"configure arguments: --prefix=/usr/local/Cellar/"
|
||||
"nginx/1.6.2 --with-http_ssl_module"]))
|
||||
self.assertEqual(self.config.get_version(), (1, 4, 2))
|
||||
|
||||
mock_popen().communicate.return_value = (
|
||||
"", "\n".join(["nginx version: nginx/0.9",
|
||||
"built by clang 6.0 (clang-600.0.56)"
|
||||
" (based on LLVM 3.5svn)",
|
||||
"TLS SNI support enabled",
|
||||
"configure arguments: --with-http_ssl_module"]))
|
||||
self.assertEqual(self.config.get_version(), (0, 9))
|
||||
|
||||
mock_popen().communicate.return_value = (
|
||||
"", "\n".join(["blah 0.0.1",
|
||||
"built by clang 6.0 (clang-600.0.56)"
|
||||
" (based on LLVM 3.5svn)",
|
||||
"TLS SNI support enabled",
|
||||
"configure arguments: --with-http_ssl_module"]))
|
||||
self.assertRaises(errors.LetsEncryptConfiguratorError,
|
||||
self.config.get_version)
|
||||
|
||||
mock_popen().communicate.return_value = (
|
||||
"", "\n".join(["nginx version: nginx/1.4.2",
|
||||
"TLS SNI support enabled"]))
|
||||
self.assertRaises(errors.LetsEncryptConfiguratorError,
|
||||
self.config.get_version)
|
||||
|
||||
mock_popen().communicate.return_value = (
|
||||
"", "\n".join(["nginx version: nginx/1.4.2",
|
||||
"built by clang 6.0 (clang-600.0.56)"
|
||||
" (based on LLVM 3.5svn)",
|
||||
"configure arguments: --with-http_ssl_module"]))
|
||||
self.assertRaises(errors.LetsEncryptConfiguratorError,
|
||||
self.config.get_version)
|
||||
|
||||
mock_popen().communicate.return_value = (
|
||||
"", "\n".join(["nginx version: nginx/0.8.1",
|
||||
"built by clang 6.0 (clang-600.0.56)"
|
||||
" (based on LLVM 3.5svn)",
|
||||
"TLS SNI support enabled",
|
||||
"configure arguments: --with-http_ssl_module"]))
|
||||
self.assertRaises(errors.LetsEncryptConfiguratorError,
|
||||
self.config.get_version)
|
||||
|
||||
mock_popen.side_effect = OSError("Can't find program")
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.nginx.configurator."
|
||||
"subprocess.Popen")
|
||||
def test_nginx_restart(self, mock_popen):
|
||||
mocked = mock_popen()
|
||||
mocked.communicate.return_value = ('', '')
|
||||
mocked.returncode = 0
|
||||
self.assertTrue(self.config.restart())
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.nginx.configurator."
|
||||
"subprocess.Popen")
|
||||
def test_config_test(self, mock_popen):
|
||||
mocked = mock_popen()
|
||||
mocked.communicate.return_value = ('', '')
|
||||
mocked.returncode = 0
|
||||
self.assertTrue(self.config.config_test())
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
85
letsencrypt/client/plugins/nginx/tests/dvsni_test.py
Normal file
85
letsencrypt/client/plugins/nginx/tests/dvsni_test.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Test for letsencrypt.client.plugins.nginx.dvsni."""
|
||||
import pkg_resources
|
||||
import unittest
|
||||
import shutil
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.plugins.nginx.tests import util
|
||||
|
||||
|
||||
class DvsniPerformTest(util.NginxTest):
|
||||
"""Test the NginxDVSNI challenge."""
|
||||
|
||||
def setUp(self):
|
||||
super(DvsniPerformTest, self).setUp()
|
||||
|
||||
config = util.get_nginx_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir,
|
||||
self.ssl_options)
|
||||
|
||||
rsa256_file = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
rsa256_pem = pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
|
||||
auth_key = le_util.Key(rsa256_file, rsa256_pem)
|
||||
|
||||
from letsencrypt.client.plugins.nginx import dvsni
|
||||
self.sni = dvsni.NginxDvsni(config)
|
||||
|
||||
self.achalls = [
|
||||
achallenges.DVSNI(
|
||||
chall=challenges.DVSNI(
|
||||
r="foo",
|
||||
nonce="bar",
|
||||
), domain="www.example.com", key=auth_key),
|
||||
achallenges.DVSNI(
|
||||
chall=challenges.DVSNI(
|
||||
r="\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y\x80"
|
||||
"\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945",
|
||||
nonce="Y\xed\x01L\xac\x95\xf7pW\xb1\xd7"
|
||||
"\xa1\xb2\xc5\x96\xba",
|
||||
), domain="blah", key=auth_key),
|
||||
]
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
def test_add_chall(self):
|
||||
self.sni.add_chall(self.achalls[0], 0)
|
||||
self.assertEqual(1, len(self.sni.achalls))
|
||||
self.assertEqual([0], self.sni.indices)
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.nginx.configurator."
|
||||
"NginxConfigurator.save")
|
||||
def test_perform0(self, mock_save):
|
||||
self.sni.add_chall(self.achalls[0])
|
||||
responses = self.sni.perform()
|
||||
self.assertEqual([], responses)
|
||||
self.assertEqual(mock_save.call_count, 2)
|
||||
|
||||
def test_setup_challenge_cert(self):
|
||||
# This is a helper function that can be used for handling
|
||||
# open context managers more elegantly. It avoids dealing with
|
||||
# __enter__ and __exit__ calls.
|
||||
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
|
||||
pass
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.nginx.configurator."
|
||||
"NginxConfigurator.save")
|
||||
def test_perform1(self, mock_save):
|
||||
self.sni.add_chall(self.achalls[1])
|
||||
responses = self.sni.perform()
|
||||
self.assertEqual(None, responses)
|
||||
self.assertEqual(mock_save.call_count, 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
108
letsencrypt/client/plugins/nginx/tests/nginxparser_test.py
Normal file
108
letsencrypt/client/plugins/nginx/tests/nginxparser_test.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""Test for letsencrypt.client.plugins.nginx.nginxparser."""
|
||||
import operator
|
||||
import unittest
|
||||
|
||||
from letsencrypt.client.plugins.nginx.nginxparser import (RawNginxParser,
|
||||
load, dumps, dump)
|
||||
from letsencrypt.client.plugins.nginx.tests import util
|
||||
|
||||
|
||||
FIRST = operator.itemgetter(0)
|
||||
|
||||
|
||||
class TestRawNginxParser(unittest.TestCase):
|
||||
"""Test the raw low-level Nginx config parser."""
|
||||
|
||||
def test_assignments(self):
|
||||
parsed = RawNginxParser.assignment.parseString('root /test;').asList()
|
||||
self.assertEqual(parsed, ['root', '/test'])
|
||||
parsed = RawNginxParser.assignment.parseString('root /test;'
|
||||
'foo bar;').asList()
|
||||
self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar'])
|
||||
|
||||
def test_blocks(self):
|
||||
parsed = RawNginxParser.block.parseString('foo {}').asList()
|
||||
self.assertEqual(parsed, [[['foo'], []]])
|
||||
parsed = RawNginxParser.block.parseString('location /foo{}').asList()
|
||||
self.assertEqual(parsed, [[['location', '/foo'], []]])
|
||||
parsed = RawNginxParser.block.parseString('foo { bar foo; }').asList()
|
||||
self.assertEqual(parsed, [[['foo'], [['bar', 'foo']]]])
|
||||
|
||||
def test_nested_blocks(self):
|
||||
parsed = RawNginxParser.block.parseString('foo { bar {} }').asList()
|
||||
block, content = FIRST(parsed)
|
||||
self.assertEqual(FIRST(content), [['bar'], []])
|
||||
self.assertEqual(FIRST(block), 'foo')
|
||||
|
||||
def test_dump_as_string(self):
|
||||
dumped = dumps([
|
||||
['user', 'www-data'],
|
||||
[['server'], [
|
||||
['listen', '80'],
|
||||
['server_name', 'foo.com'],
|
||||
['root', '/home/ubuntu/sites/foo/'],
|
||||
[['location', '/status'], [
|
||||
['check_status'],
|
||||
[['types'], [['image/jpeg', 'jpg']]],
|
||||
]]
|
||||
]]])
|
||||
|
||||
self.assertEqual(dumped,
|
||||
'user www-data;\n'
|
||||
'server {\n'
|
||||
' listen 80;\n'
|
||||
' server_name foo.com;\n'
|
||||
' root /home/ubuntu/sites/foo/;\n \n'
|
||||
' location /status {\n'
|
||||
' check_status;\n \n'
|
||||
' types {\n'
|
||||
' image/jpeg jpg;\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
'}')
|
||||
|
||||
def test_parse_from_file(self):
|
||||
parsed = load(open(util.get_data_filename('foo.conf')))
|
||||
self.assertEqual(
|
||||
parsed,
|
||||
[['user', 'www-data'],
|
||||
[['http'],
|
||||
[[['server'], [
|
||||
['listen', '*:80 default_server ssl'],
|
||||
['server_name', '*.www.foo.com *.www.example.com'],
|
||||
['root', '/home/ubuntu/sites/foo/'],
|
||||
[['location', '/status'], [
|
||||
[['types'], [['image/jpeg', 'jpg']]],
|
||||
]],
|
||||
[['location', '~', r'case_sensitive\.php$'], [
|
||||
['index', 'index.php'],
|
||||
['root', '/var/root'],
|
||||
]],
|
||||
[['location', '~*', r'case_insensitive\.php$'], []],
|
||||
[['location', '=', r'exact_match\.php$'], []],
|
||||
[['location', '^~', r'ignore_regex\.php$'], []]
|
||||
]]]]]
|
||||
)
|
||||
|
||||
def test_dump_as_file(self):
|
||||
parsed = load(open(util.get_data_filename('nginx.conf')))
|
||||
parsed[-1][-1].append([['server'],
|
||||
[['listen', '443 ssl'],
|
||||
['server_name', 'localhost'],
|
||||
['ssl_certificate', 'cert.pem'],
|
||||
['ssl_certificate_key', 'cert.key'],
|
||||
['ssl_session_cache', 'shared:SSL:1m'],
|
||||
['ssl_session_timeout', '5m'],
|
||||
['ssl_ciphers', 'HIGH:!aNULL:!MD5'],
|
||||
[['location', '/'],
|
||||
[['root', 'html'],
|
||||
['index', 'index.html index.htm']]]]])
|
||||
_file = open(util.get_data_filename('nginx.new.conf'), 'w')
|
||||
dump(parsed, _file)
|
||||
_file.close()
|
||||
parsed_new = load(open(util.get_data_filename('nginx.new.conf')))
|
||||
self.assertEquals(parsed, parsed_new)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
105
letsencrypt/client/plugins/nginx/tests/obj_test.py
Normal file
105
letsencrypt/client/plugins/nginx/tests/obj_test.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""Test the helper objects in letsencrypt.client.plugins.nginx.obj."""
|
||||
import unittest
|
||||
|
||||
|
||||
class AddrTest(unittest.TestCase):
|
||||
"""Test the Addr class."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.nginx.obj import Addr
|
||||
self.addr1 = Addr.fromstring("192.168.1.1")
|
||||
self.addr2 = Addr.fromstring("192.168.1.1:* ssl")
|
||||
self.addr3 = Addr.fromstring("192.168.1.1:80")
|
||||
self.addr4 = Addr.fromstring("*:80 default_server ssl")
|
||||
self.addr5 = Addr.fromstring("myhost")
|
||||
self.addr6 = Addr.fromstring("80 default_server spdy")
|
||||
self.addr7 = Addr.fromstring("unix:/var/run/nginx.sock")
|
||||
|
||||
def test_fromstring(self):
|
||||
self.assertEqual(self.addr1.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr1.get_port(), "")
|
||||
self.assertFalse(self.addr1.ssl)
|
||||
self.assertFalse(self.addr1.default)
|
||||
|
||||
self.assertEqual(self.addr2.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr2.get_port(), "*")
|
||||
self.assertTrue(self.addr2.ssl)
|
||||
self.assertFalse(self.addr2.default)
|
||||
|
||||
self.assertEqual(self.addr3.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr3.get_port(), "80")
|
||||
self.assertFalse(self.addr3.ssl)
|
||||
self.assertFalse(self.addr3.default)
|
||||
|
||||
self.assertEqual(self.addr4.get_addr(), "*")
|
||||
self.assertEqual(self.addr4.get_port(), "80")
|
||||
self.assertTrue(self.addr4.ssl)
|
||||
self.assertTrue(self.addr4.default)
|
||||
|
||||
self.assertEqual(self.addr5.get_addr(), "myhost")
|
||||
self.assertEqual(self.addr5.get_port(), "")
|
||||
self.assertFalse(self.addr5.ssl)
|
||||
self.assertFalse(self.addr5.default)
|
||||
|
||||
self.assertEqual(self.addr6.get_addr(), "")
|
||||
self.assertEqual(self.addr6.get_port(), "80")
|
||||
self.assertFalse(self.addr6.ssl)
|
||||
self.assertTrue(self.addr6.default)
|
||||
|
||||
self.assertEqual(None, self.addr7)
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.addr1), "192.168.1.1")
|
||||
self.assertEqual(str(self.addr2), "192.168.1.1:*")
|
||||
self.assertEqual(str(self.addr3), "192.168.1.1:80")
|
||||
self.assertEqual(str(self.addr4), "*:80")
|
||||
self.assertEqual(str(self.addr5), "myhost")
|
||||
self.assertEqual(str(self.addr6), "80")
|
||||
|
||||
def test_eq(self):
|
||||
from letsencrypt.client.plugins.nginx.obj import Addr
|
||||
new_addr1 = Addr.fromstring("192.168.1.1 spdy")
|
||||
self.assertEqual(self.addr1, new_addr1)
|
||||
self.assertNotEqual(self.addr1, self.addr2)
|
||||
self.assertFalse(self.addr1 == 3333)
|
||||
|
||||
def test_set_inclusion(self):
|
||||
from letsencrypt.client.plugins.nginx.obj import Addr
|
||||
set_a = set([self.addr1, self.addr2])
|
||||
addr1b = Addr.fromstring("192.168.1.1")
|
||||
addr2b = Addr.fromstring("192.168.1.1:* ssl")
|
||||
set_b = set([addr1b, addr2b])
|
||||
|
||||
self.assertEqual(set_a, set_b)
|
||||
|
||||
|
||||
class VirtualHostTest(unittest.TestCase):
|
||||
"""Test the VirtualHost class."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.nginx.obj import VirtualHost
|
||||
from letsencrypt.client.plugins.nginx.obj import Addr
|
||||
self.vhost1 = VirtualHost(
|
||||
"filep",
|
||||
set([Addr.fromstring("localhost")]), False, False,
|
||||
set(['localhost']), [])
|
||||
|
||||
def test_eq(self):
|
||||
from letsencrypt.client.plugins.nginx.obj import Addr
|
||||
from letsencrypt.client.plugins.nginx.obj import VirtualHost
|
||||
vhost1b = VirtualHost(
|
||||
"filep",
|
||||
set([Addr.fromstring("localhost blah")]), False, False,
|
||||
set(['localhost']), [])
|
||||
|
||||
self.assertEqual(vhost1b, self.vhost1)
|
||||
self.assertEqual(str(vhost1b), str(self.vhost1))
|
||||
self.assertFalse(vhost1b == 1234)
|
||||
|
||||
def test_str(self):
|
||||
stringified = '\n'.join(['file: filep', 'addrs: localhost',
|
||||
"names: set(['localhost'])", 'ssl: False',
|
||||
'enabled: False'])
|
||||
self.assertEqual(stringified, str(self.vhost1))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
206
letsencrypt/client/plugins/nginx/tests/parser_test.py
Normal file
206
letsencrypt/client/plugins/nginx/tests/parser_test.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"""Tests for letsencrypt.client.plugins.nginx.parser."""
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
from letsencrypt.client.errors import LetsEncryptMisconfigurationError
|
||||
from letsencrypt.client.plugins.nginx import nginxparser
|
||||
from letsencrypt.client.plugins.nginx import obj
|
||||
from letsencrypt.client.plugins.nginx import parser
|
||||
from letsencrypt.client.plugins.nginx.tests import util
|
||||
|
||||
|
||||
class NginxParserTest(util.NginxTest):
|
||||
"""Nginx Parser Test."""
|
||||
|
||||
def setUp(self):
|
||||
super(NginxParserTest, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
def test_root_normalized(self):
|
||||
path = os.path.join(self.temp_dir, "foo/////"
|
||||
"bar/../../testdata")
|
||||
nparser = parser.NginxParser(path, None)
|
||||
self.assertEqual(nparser.root, self.config_path)
|
||||
|
||||
def test_root_absolute(self):
|
||||
nparser = parser.NginxParser(os.path.relpath(self.config_path), None)
|
||||
self.assertEqual(nparser.root, self.config_path)
|
||||
|
||||
def test_root_no_trailing_slash(self):
|
||||
nparser = parser.NginxParser(self.config_path + os.path.sep, None)
|
||||
self.assertEqual(nparser.root, self.config_path)
|
||||
|
||||
def test_load(self):
|
||||
"""Test recursive conf file parsing.
|
||||
|
||||
"""
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
nparser.load()
|
||||
self.assertEqual(set([nparser.abs_path(x) for x in
|
||||
['foo.conf', 'nginx.conf', 'server.conf',
|
||||
'sites-enabled/default',
|
||||
'sites-enabled/example.com']]),
|
||||
set(nparser.parsed.keys()))
|
||||
self.assertEqual([['server_name', 'somename alias another.alias']],
|
||||
nparser.parsed[nparser.abs_path('server.conf')])
|
||||
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
['server_name', '.example.com'],
|
||||
['server_name', 'example.*']]]],
|
||||
nparser.parsed[nparser.abs_path(
|
||||
'sites-enabled/example.com')])
|
||||
|
||||
def test_abs_path(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
self.assertEqual('/etc/nginx/*', nparser.abs_path('/etc/nginx/*'))
|
||||
self.assertEqual(os.path.join(self.config_path, 'foo/bar/'),
|
||||
nparser.abs_path('foo/bar/'))
|
||||
|
||||
def test_filedump(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
nparser.filedump('test')
|
||||
# pylint: disable=protected-access
|
||||
parsed = nparser._parse_files(nparser.abs_path(
|
||||
'sites-enabled/example.com.test'))
|
||||
self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test'))))
|
||||
self.assertEqual(2, len(
|
||||
glob.glob(nparser.abs_path('sites-enabled/*.test'))))
|
||||
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
['server_name', '.example.com'],
|
||||
['server_name', 'example.*']]]],
|
||||
parsed[0])
|
||||
|
||||
def test_get_vhosts(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
vhosts = nparser.get_vhosts()
|
||||
|
||||
vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
||||
[obj.Addr('', '8080', False, False)],
|
||||
False, True,
|
||||
set(['localhost',
|
||||
r'~^(www\.)?(example|bar)\.']),
|
||||
[])
|
||||
vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
||||
[obj.Addr('somename', '8080', False, False),
|
||||
obj.Addr('', '8000', False, False)],
|
||||
False, True,
|
||||
set(['somename', 'another.alias', 'alias']),
|
||||
[])
|
||||
vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'),
|
||||
[obj.Addr('69.50.225.155', '9000',
|
||||
False, False),
|
||||
obj.Addr('127.0.0.1', '', False, False)],
|
||||
False, True,
|
||||
set(['.example.com', 'example.*']), [])
|
||||
vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'),
|
||||
[obj.Addr('myhost', '', False, True)],
|
||||
False, True, set(['www.example.org']), [])
|
||||
vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'),
|
||||
[obj.Addr('*', '80', True, True)],
|
||||
True, True, set(['*.www.foo.com',
|
||||
'*.www.example.com']), [])
|
||||
|
||||
self.assertEqual(5, len(vhosts))
|
||||
example_com = [x for x in vhosts if 'example.com' in x.filep][0]
|
||||
self.assertEqual(vhost3, example_com)
|
||||
default = [x for x in vhosts if 'default' in x.filep][0]
|
||||
self.assertEqual(vhost4, default)
|
||||
fooconf = [x for x in vhosts if 'foo.conf' in x.filep][0]
|
||||
self.assertEqual(vhost5, fooconf)
|
||||
localhost = [x for x in vhosts if 'localhost' in x.names][0]
|
||||
self.assertEquals(vhost1, localhost)
|
||||
somename = [x for x in vhosts if 'somename' in x.names][0]
|
||||
self.assertEquals(vhost2, somename)
|
||||
|
||||
def test_add_server_directives(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
nparser.add_server_directives(nparser.abs_path('nginx.conf'),
|
||||
set(['localhost',
|
||||
r'~^(www\.)?(example|bar)\.']),
|
||||
[['foo', 'bar'], ['ssl_certificate',
|
||||
'/etc/ssl/cert.pem']])
|
||||
ssl_re = re.compile(r'foo bar;\n\s+ssl_certificate /etc/ssl/cert.pem')
|
||||
self.assertEqual(1, len(re.findall(ssl_re, nginxparser.dumps(
|
||||
nparser.parsed[nparser.abs_path('nginx.conf')]))))
|
||||
nparser.add_server_directives(nparser.abs_path('server.conf'),
|
||||
set(['alias', 'another.alias',
|
||||
'somename']),
|
||||
[['foo', 'bar'], ['ssl_certificate',
|
||||
'/etc/ssl/cert2.pem']])
|
||||
self.assertEqual(nparser.parsed[nparser.abs_path('server.conf')],
|
||||
[['server_name', 'somename alias another.alias'],
|
||||
['foo', 'bar'],
|
||||
['ssl_certificate', '/etc/ssl/cert2.pem']])
|
||||
|
||||
def test_replace_server_directives(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
target = set(['.example.com', 'example.*'])
|
||||
filep = nparser.abs_path('sites-enabled/example.com')
|
||||
nparser.add_server_directives(
|
||||
filep, target, [['server_name', 'foo bar']], True)
|
||||
self.assertEqual(
|
||||
nparser.parsed[filep],
|
||||
[[['server'], [['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
['server_name', 'foo bar'],
|
||||
['server_name', 'foo bar']]]])
|
||||
self.assertRaises(LetsEncryptMisconfigurationError,
|
||||
nparser.add_server_directives,
|
||||
filep, set(['foo', 'bar']),
|
||||
[['ssl_certificate', 'cert.pem']], True)
|
||||
|
||||
def test_get_best_match(self):
|
||||
target_name = 'www.eff.org'
|
||||
names = [set(['www.eff.org', 'irrelevant.long.name.eff.org', '*.org']),
|
||||
set(['eff.org', 'ww2.eff.org', 'test.www.eff.org']),
|
||||
set(['*.eff.org', '.www.eff.org']),
|
||||
set(['.eff.org', '*.org']),
|
||||
set(['www.eff.', 'www.eff.*', '*.www.eff.org']),
|
||||
set(['example.com', r'~^(www\.)?(eff.+)', '*.eff.*']),
|
||||
set(['*', r'~^(www\.)?(eff.+)']),
|
||||
set(['www.*', r'~^(www\.)?(eff.+)', '.test.eff.org']),
|
||||
set(['*.org', r'*.eff.org', 'www.eff.*']),
|
||||
set(['*.www.eff.org', 'www.*']),
|
||||
set(['*.org']),
|
||||
set([]),
|
||||
set(['example.com'])]
|
||||
winners = [('exact', 'www.eff.org'),
|
||||
(None, None),
|
||||
('exact', '.www.eff.org'),
|
||||
('wildcard_start', '.eff.org'),
|
||||
('wildcard_end', 'www.eff.*'),
|
||||
('regex', r'~^(www\.)?(eff.+)'),
|
||||
('wildcard_start', '*'),
|
||||
('wildcard_end', 'www.*'),
|
||||
('wildcard_start', '*.eff.org'),
|
||||
('wildcard_end', 'www.*'),
|
||||
('wildcard_start', '*.org'),
|
||||
(None, None),
|
||||
(None, None)]
|
||||
|
||||
for i, winner in enumerate(winners):
|
||||
self.assertEqual(winner,
|
||||
parser.get_best_match(target_name, names[i]))
|
||||
|
||||
def test_get_all_certs_keys(self):
|
||||
nparser = parser.NginxParser(self.config_path, self.ssl_options)
|
||||
filep = nparser.abs_path('sites-enabled/example.com')
|
||||
nparser.add_server_directives(filep,
|
||||
set(['.example.com', 'example.*']),
|
||||
[['ssl_certificate', 'foo.pem'],
|
||||
['ssl_certificate_key', 'bar.key'],
|
||||
['listen', '443 ssl']])
|
||||
c_k = nparser.get_all_certs_keys()
|
||||
self.assertEqual(set([('foo.pem', 'bar.key', filep)]), c_k)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
25
letsencrypt/client/plugins/nginx/tests/testdata/foo.conf
vendored
Normal file
25
letsencrypt/client/plugins/nginx/tests/testdata/foo.conf
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# a test nginx conf
|
||||
user www-data;
|
||||
|
||||
http {
|
||||
server {
|
||||
listen *:80 default_server ssl;
|
||||
server_name *.www.foo.com *.www.example.com;
|
||||
root /home/ubuntu/sites/foo/;
|
||||
|
||||
location /status {
|
||||
types {
|
||||
image/jpeg jpg;
|
||||
}
|
||||
}
|
||||
|
||||
location ~ case_sensitive\.php$ {
|
||||
index index.php;
|
||||
root /var/root;
|
||||
}
|
||||
location ~* case_insensitive\.php$ {}
|
||||
location = exact_match\.php$ {}
|
||||
location ^~ ignore_regex\.php$ {}
|
||||
|
||||
}
|
||||
}
|
||||
0
letsencrypt/client/plugins/nginx/tests/testdata/mime.types
vendored
Normal file
0
letsencrypt/client/plugins/nginx/tests/testdata/mime.types
vendored
Normal file
119
letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf
vendored
Normal file
119
letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# standard default nginx config
|
||||
|
||||
user nobody;
|
||||
worker_processes 1;
|
||||
|
||||
error_log logs/error.log;
|
||||
error_log logs/error.log notice;
|
||||
error_log logs/error.log info;
|
||||
|
||||
pid logs/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
include foo.conf;
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
include sites-enabled/*;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log logs/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
|
||||
keepalive_timeout 0;
|
||||
|
||||
gzip on;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_name ~^(www\.)?(example|bar)\.;
|
||||
|
||||
charset koi8-r;
|
||||
|
||||
access_log logs/host.access.log main;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
|
||||
# proxy the PHP scripts to Nginx listening on 127.0.0.1:80
|
||||
#
|
||||
location ~ \.php$ {
|
||||
proxy_pass http://127.0.0.1;
|
||||
}
|
||||
|
||||
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
|
||||
#
|
||||
location ~ \.php$ {
|
||||
root html;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
|
||||
}
|
||||
|
||||
# deny access to .htaccess files, if Nginx's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# another virtual host using mix of IP-, name-, and port-based configuration
|
||||
#
|
||||
server {
|
||||
listen 8000;
|
||||
listen somename:8080;
|
||||
include server.conf;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# HTTPS server
|
||||
#
|
||||
#server {
|
||||
# listen 443 ssl;
|
||||
# server_name localhost;
|
||||
|
||||
# ssl_certificate cert.pem;
|
||||
# ssl_certificate_key cert.key;
|
||||
|
||||
# ssl_session_cache shared:SSL:1m;
|
||||
# ssl_session_timeout 5m;
|
||||
|
||||
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
|
||||
# location / {
|
||||
# root html;
|
||||
# index index.html index.htm;
|
||||
# }
|
||||
#}
|
||||
|
||||
#include conf.d/test.conf;
|
||||
}
|
||||
83
letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf
vendored
Normal file
83
letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
user nobody;
|
||||
worker_processes 1;
|
||||
error_log logs/error.log;
|
||||
error_log logs/error.log notice;
|
||||
error_log logs/error.log info;
|
||||
pid logs/nginx.pid;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
include foo.conf;
|
||||
http {
|
||||
include mime.types;
|
||||
include sites-enabled/*;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log logs/access.log main;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 0;
|
||||
gzip on;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_name ~^(www\.)?(example|bar)\.;
|
||||
charset koi8-r;
|
||||
access_log logs/host.access.log main;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
proxy_pass http://127.0.0.1;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
root html;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8000;
|
||||
listen somename:8080;
|
||||
include server.conf;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name localhost;
|
||||
ssl_certificate cert.pem;
|
||||
ssl_certificate_key cert.key;
|
||||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
letsencrypt/client/plugins/nginx/tests/testdata/server.conf
vendored
Normal file
1
letsencrypt/client/plugins/nginx/tests/testdata/server.conf
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
server_name somename alias another.alias;
|
||||
9
letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default
vendored
Normal file
9
letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
server {
|
||||
listen myhost default_server;
|
||||
server_name www.example.org;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
6
letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com
vendored
Normal file
6
letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
server {
|
||||
listen 69.50.225.155:9000;
|
||||
listen 127.0.0.1;
|
||||
server_name .example.com;
|
||||
server_name example.*;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
fastcgi_param QUERY_STRING $query_string;
|
||||
fastcgi_param REQUEST_METHOD $request_method;
|
||||
fastcgi_param CONTENT_TYPE $content_type;
|
||||
fastcgi_param CONTENT_LENGTH $content_length;
|
||||
|
||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_param DOCUMENT_URI $document_uri;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
fastcgi_param SERVER_PROTOCOL $server_protocol;
|
||||
|
||||
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
|
||||
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
|
||||
|
||||
fastcgi_param REMOTE_ADDR $remote_addr;
|
||||
fastcgi_param REMOTE_PORT $remote_port;
|
||||
fastcgi_param SERVER_ADDR $server_addr;
|
||||
fastcgi_param SERVER_PORT $server_port;
|
||||
fastcgi_param SERVER_NAME $server_name;
|
||||
|
||||
fastcgi_param HTTPS $https if_not_empty;
|
||||
|
||||
# PHP only, required if PHP was built with --enable-force-cgi-redirect
|
||||
fastcgi_param REDIRECT_STATUS 200;
|
||||
108
letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf
vendored
Normal file
108
letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# This map is not a full koi8-r <> utf8 map: it does not contain
|
||||
# box-drawing and some other characters. Besides this map contains
|
||||
# several koi8-u and Byelorussian letters which are not in koi8-r.
|
||||
# If you need a full and standard map, use contrib/unicode2nginx/koi-utf
|
||||
# map instead.
|
||||
|
||||
charset_map koi8-r utf-8 {
|
||||
|
||||
80 E282AC; # euro
|
||||
|
||||
95 E280A2; # bullet
|
||||
|
||||
9A C2A0; #
|
||||
|
||||
9E C2B7; # ·
|
||||
|
||||
A3 D191; # small yo
|
||||
A4 D194; # small Ukrainian ye
|
||||
|
||||
A6 D196; # small Ukrainian i
|
||||
A7 D197; # small Ukrainian yi
|
||||
|
||||
AD D291; # small Ukrainian soft g
|
||||
AE D19E; # small Byelorussian short u
|
||||
|
||||
B0 C2B0; # °
|
||||
|
||||
B3 D081; # capital YO
|
||||
B4 D084; # capital Ukrainian YE
|
||||
|
||||
B6 D086; # capital Ukrainian I
|
||||
B7 D087; # capital Ukrainian YI
|
||||
|
||||
B9 E28496; # numero sign
|
||||
|
||||
BD D290; # capital Ukrainian soft G
|
||||
BE D18E; # capital Byelorussian short U
|
||||
|
||||
BF C2A9; # (C)
|
||||
|
||||
C0 D18E; # small yu
|
||||
C1 D0B0; # small a
|
||||
C2 D0B1; # small b
|
||||
C3 D186; # small ts
|
||||
C4 D0B4; # small d
|
||||
C5 D0B5; # small ye
|
||||
C6 D184; # small f
|
||||
C7 D0B3; # small g
|
||||
C8 D185; # small kh
|
||||
C9 D0B8; # small i
|
||||
CA D0B9; # small j
|
||||
CB D0BA; # small k
|
||||
CC D0BB; # small l
|
||||
CD D0BC; # small m
|
||||
CE D0BD; # small n
|
||||
CF D0BE; # small o
|
||||
|
||||
D0 D0BF; # small p
|
||||
D1 D18F; # small ya
|
||||
D2 D180; # small r
|
||||
D3 D181; # small s
|
||||
D4 D182; # small t
|
||||
D5 D183; # small u
|
||||
D6 D0B6; # small zh
|
||||
D7 D0B2; # small v
|
||||
D8 D18C; # small soft sign
|
||||
D9 D18B; # small y
|
||||
DA D0B7; # small z
|
||||
DB D188; # small sh
|
||||
DC D18D; # small e
|
||||
DD D189; # small shch
|
||||
DE D187; # small ch
|
||||
DF D18A; # small hard sign
|
||||
|
||||
E0 D0AE; # capital YU
|
||||
E1 D090; # capital A
|
||||
E2 D091; # capital B
|
||||
E3 D0A6; # capital TS
|
||||
E4 D094; # capital D
|
||||
E5 D095; # capital YE
|
||||
E6 D0A4; # capital F
|
||||
E7 D093; # capital G
|
||||
E8 D0A5; # capital KH
|
||||
E9 D098; # capital I
|
||||
EA D099; # capital J
|
||||
EB D09A; # capital K
|
||||
EC D09B; # capital L
|
||||
ED D09C; # capital M
|
||||
EE D09D; # capital N
|
||||
EF D09E; # capital O
|
||||
|
||||
F0 D09F; # capital P
|
||||
F1 D0AF; # capital YA
|
||||
F2 D0A0; # capital R
|
||||
F3 D0A1; # capital S
|
||||
F4 D0A2; # capital T
|
||||
F5 D0A3; # capital U
|
||||
F6 D096; # capital ZH
|
||||
F7 D092; # capital V
|
||||
F8 D0AC; # capital soft sign
|
||||
F9 D0AB; # capital Y
|
||||
FA D097; # capital Z
|
||||
FB D0A8; # capital SH
|
||||
FC D0AD; # capital E
|
||||
FD D0A9; # capital SHCH
|
||||
FE D0A7; # capital CH
|
||||
FF D0AA; # capital hard sign
|
||||
}
|
||||
102
letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win
vendored
Normal file
102
letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
charset_map koi8-r windows-1251 {
|
||||
|
||||
80 88; # euro
|
||||
|
||||
95 95; # bullet
|
||||
|
||||
9A A0; #
|
||||
|
||||
9E B7; # ·
|
||||
|
||||
A3 B8; # small yo
|
||||
A4 BA; # small Ukrainian ye
|
||||
|
||||
A6 B3; # small Ukrainian i
|
||||
A7 BF; # small Ukrainian yi
|
||||
|
||||
AD B4; # small Ukrainian soft g
|
||||
AE A2; # small Byelorussian short u
|
||||
|
||||
B0 B0; # °
|
||||
|
||||
B3 A8; # capital YO
|
||||
B4 AA; # capital Ukrainian YE
|
||||
|
||||
B6 B2; # capital Ukrainian I
|
||||
B7 AF; # capital Ukrainian YI
|
||||
|
||||
B9 B9; # numero sign
|
||||
|
||||
BD A5; # capital Ukrainian soft G
|
||||
BE A1; # capital Byelorussian short U
|
||||
|
||||
BF A9; # (C)
|
||||
|
||||
C0 FE; # small yu
|
||||
C1 E0; # small a
|
||||
C2 E1; # small b
|
||||
C3 F6; # small ts
|
||||
C4 E4; # small d
|
||||
C5 E5; # small ye
|
||||
C6 F4; # small f
|
||||
C7 E3; # small g
|
||||
C8 F5; # small kh
|
||||
C9 E8; # small i
|
||||
CA E9; # small j
|
||||
CB EA; # small k
|
||||
CC EB; # small l
|
||||
CD EC; # small m
|
||||
CE ED; # small n
|
||||
CF EE; # small o
|
||||
|
||||
D0 EF; # small p
|
||||
D1 FF; # small ya
|
||||
D2 F0; # small r
|
||||
D3 F1; # small s
|
||||
D4 F2; # small t
|
||||
D5 F3; # small u
|
||||
D6 E6; # small zh
|
||||
D7 E2; # small v
|
||||
D8 FC; # small soft sign
|
||||
D9 FB; # small y
|
||||
DA E7; # small z
|
||||
DB F8; # small sh
|
||||
DC FD; # small e
|
||||
DD F9; # small shch
|
||||
DE F7; # small ch
|
||||
DF FA; # small hard sign
|
||||
|
||||
E0 DE; # capital YU
|
||||
E1 C0; # capital A
|
||||
E2 C1; # capital B
|
||||
E3 D6; # capital TS
|
||||
E4 C4; # capital D
|
||||
E5 C5; # capital YE
|
||||
E6 D4; # capital F
|
||||
E7 C3; # capital G
|
||||
E8 D5; # capital KH
|
||||
E9 C8; # capital I
|
||||
EA C9; # capital J
|
||||
EB CA; # capital K
|
||||
EC CB; # capital L
|
||||
ED CC; # capital M
|
||||
EE CD; # capital N
|
||||
EF CE; # capital O
|
||||
|
||||
F0 CF; # capital P
|
||||
F1 DF; # capital YA
|
||||
F2 D0; # capital R
|
||||
F3 D1; # capital S
|
||||
F4 D2; # capital T
|
||||
F5 D3; # capital U
|
||||
F6 C6; # capital ZH
|
||||
F7 C2; # capital V
|
||||
F8 DC; # capital soft sign
|
||||
F9 DB; # capital Y
|
||||
FA C7; # capital Z
|
||||
FB D8; # capital SH
|
||||
FC DD; # capital E
|
||||
FD D9; # capital SHCH
|
||||
FE D7; # capital CH
|
||||
FF DA; # capital hard sign
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml rss;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/x-javascript js;
|
||||
application/atom+xml atom;
|
||||
|
||||
text/mathml mml;
|
||||
text/plain txt;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
text/vnd.wap.wml wml;
|
||||
text/x-component htc;
|
||||
|
||||
image/png png;
|
||||
image/tiff tif tiff;
|
||||
image/vnd.wap.wbmp wbmp;
|
||||
image/x-icon ico;
|
||||
image/x-jng jng;
|
||||
image/x-ms-bmp bmp;
|
||||
image/svg+xml svg svgz;
|
||||
|
||||
application/java-archive jar war ear;
|
||||
application/json json;
|
||||
application/mac-binhex40 hqx;
|
||||
application/msword doc;
|
||||
application/pdf pdf;
|
||||
application/postscript ps eps ai;
|
||||
application/rtf rtf;
|
||||
application/vnd.ms-excel xls;
|
||||
application/vnd.ms-powerpoint ppt;
|
||||
application/vnd.wap.wmlc wmlc;
|
||||
application/vnd.google-earth.kml+xml kml;
|
||||
application/vnd.google-earth.kmz kmz;
|
||||
application/x-7z-compressed 7z;
|
||||
application/x-cocoa cco;
|
||||
application/x-java-archive-diff jardiff;
|
||||
application/x-java-jnlp-file jnlp;
|
||||
application/x-makeself run;
|
||||
application/x-perl pl pm;
|
||||
application/x-pilot prc pdb;
|
||||
application/x-rar-compressed rar;
|
||||
application/x-redhat-package-manager rpm;
|
||||
application/x-sea sea;
|
||||
application/x-shockwave-flash swf;
|
||||
application/x-stuffit sit;
|
||||
application/x-tcl tcl tk;
|
||||
application/x-x509-ca-cert der pem crt;
|
||||
application/x-xpinstall xpi;
|
||||
application/xhtml+xml xhtml;
|
||||
application/zip zip;
|
||||
|
||||
application/octet-stream bin exe dll;
|
||||
application/octet-stream deb;
|
||||
application/octet-stream dmg;
|
||||
application/octet-stream eot;
|
||||
application/octet-stream iso img;
|
||||
application/octet-stream msi msp msm;
|
||||
application/ogg ogx;
|
||||
|
||||
audio/midi mid midi kar;
|
||||
audio/mpeg mpga mpega mp2 mp3 m4a;
|
||||
audio/ogg oga ogg spx;
|
||||
audio/x-realaudio ra;
|
||||
audio/webm weba;
|
||||
|
||||
video/3gpp 3gpp 3gp;
|
||||
video/mp4 mp4;
|
||||
video/mpeg mpeg mpg mpe;
|
||||
video/ogg ogv;
|
||||
video/quicktime mov;
|
||||
video/webm webm;
|
||||
video/x-flv flv;
|
||||
video/x-mng mng;
|
||||
video/x-ms-asf asx asf;
|
||||
video/x-ms-wmv wmv;
|
||||
video/x-msvideo avi;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
[nx_extract]
|
||||
username = naxsi_web
|
||||
password = test
|
||||
port = 8081
|
||||
rules_path = /etc/nginx/naxsi_core.rules
|
||||
|
||||
[nx_intercept]
|
||||
port = 8080
|
||||
|
||||
[sql]
|
||||
dbtype = sqlite
|
||||
username = root
|
||||
password =
|
||||
hostname = 127.0.0.1
|
||||
dbname = naxsi_sig
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Sample rules file for default vhost.
|
||||
|
||||
LearningMode;
|
||||
SecRulesEnabled;
|
||||
#SecRulesDisabled;
|
||||
DeniedUrl "/RequestDenied";
|
||||
|
||||
## check rules
|
||||
CheckRule "$SQL >= 8" BLOCK;
|
||||
CheckRule "$RFI >= 8" BLOCK;
|
||||
CheckRule "$TRAVERSAL >= 4" BLOCK;
|
||||
CheckRule "$EVADE >= 4" BLOCK;
|
||||
CheckRule "$XSS >= 8" BLOCK;
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
##################################
|
||||
## INTERNAL RULES IDS:1-10 ##
|
||||
##################################
|
||||
#weird_request : 1
|
||||
#big_body : 2
|
||||
#no_content_type : 3
|
||||
|
||||
#MainRule "str:yesone" "msg:foobar test pattern" "mz:ARGS" "s:$SQL:42" id:1999;
|
||||
|
||||
##################################
|
||||
## SQL Injections IDs:1000-1099 ##
|
||||
##################################
|
||||
MainRule "rx:select|union|update|delete|insert|table|from|ascii|hex|unhex" "msg:sql keywords" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1000;
|
||||
MainRule "str:\"" "msg:double quote" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1001;
|
||||
MainRule "str:0x" "msg:0x, possible hex encoding" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:2" id:1002;
|
||||
## Hardcore rules
|
||||
MainRule "str:/*" "msg:mysql comment (/*)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1003;
|
||||
MainRule "str:*/" "msg:mysql comment (*/)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1004;
|
||||
MainRule "str:|" "msg:mysql keyword (|)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1005;
|
||||
MainRule "rx:&&" "msg:mysql keyword (&&)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1006;
|
||||
## end of hardcore rules
|
||||
MainRule "str:--" "msg:mysql comment (--)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1007;
|
||||
MainRule "str:;" "msg:; in stuff" "mz:BODY|URL|ARGS" "s:$SQL:4" id:1008;
|
||||
MainRule "str:=" "msg:equal in var, probable sql/xss" "mz:ARGS|BODY" "s:$SQL:2" id:1009;
|
||||
MainRule "str:(" "msg:parenthesis, probable sql/xss" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1010;
|
||||
MainRule "str:)" "msg:parenthesis, probable sql/xss" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1011;
|
||||
MainRule "str:'" "msg:simple quote" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1013;
|
||||
MainRule "str:\"" "msg:double quote" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1014;
|
||||
MainRule "str:," "msg:, in stuff" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1015;
|
||||
MainRule "str:#" "msg:mysql comment (#)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1016;
|
||||
|
||||
###############################
|
||||
## OBVIOUS RFI IDs:1100-1199 ##
|
||||
###############################
|
||||
MainRule "str:http://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1100;
|
||||
MainRule "str:https://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1101;
|
||||
MainRule "str:ftp://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1102;
|
||||
MainRule "str:php://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1103;
|
||||
|
||||
#######################################
|
||||
## Directory traversal IDs:1200-1299 ##
|
||||
#######################################
|
||||
MainRule "str:.." "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1200;
|
||||
MainRule "str:/etc/passwd" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1202;
|
||||
MainRule "str:c:\\" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1203;
|
||||
MainRule "str:cmd.exe" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1204;
|
||||
MainRule "str:\\" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1205;
|
||||
#MainRule "str:/" "msg:slash in args" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:2" id:1206;
|
||||
########################################
|
||||
## Cross Site Scripting IDs:1300-1399 ##
|
||||
########################################
|
||||
MainRule "str:<" "msg:html open tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1302;
|
||||
MainRule "str:>" "msg:html close tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1303;
|
||||
MainRule "str:'" "msg:simple quote" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1306;
|
||||
MainRule "str:\"" "msg:double quote" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1307;
|
||||
MainRule "str:(" "msg:parenthesis" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1308;
|
||||
MainRule "str:)" "msg:parenthesis" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1309;
|
||||
MainRule "str:[" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1310;
|
||||
MainRule "str:]" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1311;
|
||||
MainRule "str:~" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1312;
|
||||
MainRule "str:;" "msg:semi coma" "mz:ARGS|URL|BODY" "s:$XSS:8" id:1313;
|
||||
MainRule "str:`" "msg:grave accent !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1314;
|
||||
MainRule "rx:%[2|3]." "msg:double encoding !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1315;
|
||||
|
||||
####################################
|
||||
## Evading tricks IDs: 1400-1500 ##
|
||||
####################################
|
||||
MainRule "str:&#" "msg: utf7/8 encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1400;
|
||||
MainRule "str:%U" "msg: M$ encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1401;
|
||||
MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither mulipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402;
|
||||
|
||||
#############################
|
||||
## File uploads: 1500-1600 ##
|
||||
#############################
|
||||
MainRule "rx:.ph*|.asp*" "msg:asp/php file upload!" "mz:FILE_EXT" "s:$UPLOAD:8" id:1500;
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
user www-data;
|
||||
worker_processes 4;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
##
|
||||
# Basic Settings
|
||||
##
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
# server_tokens off;
|
||||
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
##
|
||||
# Gzip Settings
|
||||
##
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
# gzip_vary on;
|
||||
# gzip_proxied any;
|
||||
# gzip_comp_level 6;
|
||||
# gzip_buffers 16 8k;
|
||||
# gzip_http_version 1.1;
|
||||
# gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
##
|
||||
# nginx-naxsi config
|
||||
##
|
||||
# Uncomment it if you installed nginx-naxsi
|
||||
##
|
||||
|
||||
#include /etc/nginx/naxsi_core.rules;
|
||||
|
||||
##
|
||||
# nginx-passenger config
|
||||
##
|
||||
# Uncomment it if you installed nginx-passenger
|
||||
##
|
||||
|
||||
#passenger_root /usr;
|
||||
#passenger_ruby /usr/bin/ruby;
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
||||
|
||||
#mail {
|
||||
# # See sample authentication script at:
|
||||
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
|
||||
#
|
||||
# # auth_http localhost/auth.php;
|
||||
# # pop3_capabilities "TOP" "USER";
|
||||
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
|
||||
#
|
||||
# server {
|
||||
# listen localhost:110;
|
||||
# protocol pop3;
|
||||
# proxy on;
|
||||
# }
|
||||
#
|
||||
# server {
|
||||
# listen localhost:143;
|
||||
# protocol imap;
|
||||
# proxy on;
|
||||
# }
|
||||
#}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
scgi_param REQUEST_METHOD $request_method;
|
||||
scgi_param REQUEST_URI $request_uri;
|
||||
scgi_param QUERY_STRING $query_string;
|
||||
scgi_param CONTENT_TYPE $content_type;
|
||||
|
||||
scgi_param DOCUMENT_URI $document_uri;
|
||||
scgi_param DOCUMENT_ROOT $document_root;
|
||||
scgi_param SCGI 1;
|
||||
scgi_param SERVER_PROTOCOL $server_protocol;
|
||||
|
||||
scgi_param REMOTE_ADDR $remote_addr;
|
||||
scgi_param REMOTE_PORT $remote_port;
|
||||
scgi_param SERVER_PORT $server_port;
|
||||
scgi_param SERVER_NAME $server_name;
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# You may add here your
|
||||
# server {
|
||||
# ...
|
||||
# }
|
||||
# statements for each of your virtual hosts to this file
|
||||
|
||||
##
|
||||
# You should look at the following URL's in order to grasp a solid understanding
|
||||
# of Nginx configuration files in order to fully unleash the power of Nginx.
|
||||
# http://wiki.nginx.org/Pitfalls
|
||||
# http://wiki.nginx.org/QuickStart
|
||||
# http://wiki.nginx.org/Configuration
|
||||
#
|
||||
# Generally, you will want to move this file somewhere, and start with a clean
|
||||
# file but keep this around for reference. Or just disable in sites-enabled.
|
||||
#
|
||||
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
|
||||
##
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server ipv6only=on;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
# Make site accessible from http://localhost/
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
# First attempt to serve request as file, then
|
||||
# as directory, then fall back to displaying a 404.
|
||||
try_files $uri $uri/ =404;
|
||||
# Uncomment to enable naxsi on this location
|
||||
# include /etc/nginx/naxsi.rules
|
||||
}
|
||||
|
||||
# Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests
|
||||
#location /RequestDenied {
|
||||
# proxy_pass http://127.0.0.1:8080;
|
||||
#}
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
#error_page 500 502 503 504 /50x.html;
|
||||
#location = /50x.html {
|
||||
# root /usr/share/nginx/html;
|
||||
#}
|
||||
|
||||
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
|
||||
#
|
||||
#location ~ \.php$ {
|
||||
# fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
# # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
|
||||
#
|
||||
# # With php5-cgi alone:
|
||||
# fastcgi_pass 127.0.0.1:9000;
|
||||
# # With php5-fpm:
|
||||
# fastcgi_pass unix:/var/run/php5-fpm.sock;
|
||||
# fastcgi_index index.php;
|
||||
# include fastcgi_params;
|
||||
#}
|
||||
|
||||
# deny access to .htaccess files, if Apache's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
#location ~ /\.ht {
|
||||
# deny all;
|
||||
#}
|
||||
}
|
||||
|
||||
|
||||
# another virtual host using mix of IP-, name-, and port-based configuration
|
||||
#
|
||||
#server {
|
||||
# listen 8000;
|
||||
# listen somename:8080;
|
||||
# server_name somename alias another.alias;
|
||||
# root html;
|
||||
# index index.html index.htm;
|
||||
#
|
||||
# location / {
|
||||
# try_files $uri $uri/ =404;
|
||||
# }
|
||||
#}
|
||||
|
||||
|
||||
# HTTPS server
|
||||
#
|
||||
#server {
|
||||
# listen 443;
|
||||
# server_name localhost;
|
||||
#
|
||||
# root html;
|
||||
# index index.html index.htm;
|
||||
#
|
||||
# ssl on;
|
||||
# ssl_certificate cert.pem;
|
||||
# ssl_certificate_key cert.key;
|
||||
#
|
||||
# ssl_session_timeout 5m;
|
||||
#
|
||||
# ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
|
||||
# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
|
||||
# ssl_prefer_server_ciphers on;
|
||||
#
|
||||
# location / {
|
||||
# try_files $uri $uri/ =404;
|
||||
# }
|
||||
#}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/etc/nginx/sites-available/default
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
uwsgi_param QUERY_STRING $query_string;
|
||||
uwsgi_param REQUEST_METHOD $request_method;
|
||||
uwsgi_param CONTENT_TYPE $content_type;
|
||||
uwsgi_param CONTENT_LENGTH $content_length;
|
||||
|
||||
uwsgi_param REQUEST_URI $request_uri;
|
||||
uwsgi_param PATH_INFO $document_uri;
|
||||
uwsgi_param DOCUMENT_ROOT $document_root;
|
||||
uwsgi_param SERVER_PROTOCOL $server_protocol;
|
||||
uwsgi_param UWSGI_SCHEME $scheme;
|
||||
|
||||
uwsgi_param REMOTE_ADDR $remote_addr;
|
||||
uwsgi_param REMOTE_PORT $remote_port;
|
||||
uwsgi_param SERVER_PORT $server_port;
|
||||
uwsgi_param SERVER_NAME $server_name;
|
||||
125
letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf
vendored
Normal file
125
letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# This map is not a full windows-1251 <> utf8 map: it does not
|
||||
# contain Serbian and Macedonian letters. If you need a full map,
|
||||
# use contrib/unicode2nginx/win-utf map instead.
|
||||
|
||||
charset_map windows-1251 utf-8 {
|
||||
|
||||
82 E2809A; # single low-9 quotation mark
|
||||
|
||||
84 E2809E; # double low-9 quotation mark
|
||||
85 E280A6; # ellipsis
|
||||
86 E280A0; # dagger
|
||||
87 E280A1; # double dagger
|
||||
88 E282AC; # euro
|
||||
89 E280B0; # per mille
|
||||
|
||||
91 E28098; # left single quotation mark
|
||||
92 E28099; # right single quotation mark
|
||||
93 E2809C; # left double quotation mark
|
||||
94 E2809D; # right double quotation mark
|
||||
95 E280A2; # bullet
|
||||
96 E28093; # en dash
|
||||
97 E28094; # em dash
|
||||
|
||||
99 E284A2; # trade mark sign
|
||||
|
||||
A0 C2A0; #
|
||||
A1 D18E; # capital Byelorussian short U
|
||||
A2 D19E; # small Byelorussian short u
|
||||
|
||||
A4 C2A4; # currency sign
|
||||
A5 D290; # capital Ukrainian soft G
|
||||
A6 C2A6; # borken bar
|
||||
A7 C2A7; # section sign
|
||||
A8 D081; # capital YO
|
||||
A9 C2A9; # (C)
|
||||
AA D084; # capital Ukrainian YE
|
||||
AB C2AB; # left-pointing double angle quotation mark
|
||||
AC C2AC; # not sign
|
||||
AD C2AD; # soft hypen
|
||||
AE C2AE; # (R)
|
||||
AF D087; # capital Ukrainian YI
|
||||
|
||||
B0 C2B0; # °
|
||||
B1 C2B1; # plus-minus sign
|
||||
B2 D086; # capital Ukrainian I
|
||||
B3 D196; # small Ukrainian i
|
||||
B4 D291; # small Ukrainian soft g
|
||||
B5 C2B5; # micro sign
|
||||
B6 C2B6; # pilcrow sign
|
||||
B7 C2B7; # ·
|
||||
B8 D191; # small yo
|
||||
B9 E28496; # numero sign
|
||||
BA D194; # small Ukrainian ye
|
||||
BB C2BB; # right-pointing double angle quotation mark
|
||||
|
||||
BF D197; # small Ukrainian yi
|
||||
|
||||
C0 D090; # capital A
|
||||
C1 D091; # capital B
|
||||
C2 D092; # capital V
|
||||
C3 D093; # capital G
|
||||
C4 D094; # capital D
|
||||
C5 D095; # capital YE
|
||||
C6 D096; # capital ZH
|
||||
C7 D097; # capital Z
|
||||
C8 D098; # capital I
|
||||
C9 D099; # capital J
|
||||
CA D09A; # capital K
|
||||
CB D09B; # capital L
|
||||
CC D09C; # capital M
|
||||
CD D09D; # capital N
|
||||
CE D09E; # capital O
|
||||
CF D09F; # capital P
|
||||
|
||||
D0 D0A0; # capital R
|
||||
D1 D0A1; # capital S
|
||||
D2 D0A2; # capital T
|
||||
D3 D0A3; # capital U
|
||||
D4 D0A4; # capital F
|
||||
D5 D0A5; # capital KH
|
||||
D6 D0A6; # capital TS
|
||||
D7 D0A7; # capital CH
|
||||
D8 D0A8; # capital SH
|
||||
D9 D0A9; # capital SHCH
|
||||
DA D0AA; # capital hard sign
|
||||
DB D0AB; # capital Y
|
||||
DC D0AC; # capital soft sign
|
||||
DD D0AD; # capital E
|
||||
DE D0AE; # capital YU
|
||||
DF D0AF; # capital YA
|
||||
|
||||
E0 D0B0; # small a
|
||||
E1 D0B1; # small b
|
||||
E2 D0B2; # small v
|
||||
E3 D0B3; # small g
|
||||
E4 D0B4; # small d
|
||||
E5 D0B5; # small ye
|
||||
E6 D0B6; # small zh
|
||||
E7 D0B7; # small z
|
||||
E8 D0B8; # small i
|
||||
E9 D0B9; # small j
|
||||
EA D0BA; # small k
|
||||
EB D0BB; # small l
|
||||
EC D0BC; # small m
|
||||
ED D0BD; # small n
|
||||
EE D0BE; # small o
|
||||
EF D0BF; # small p
|
||||
|
||||
F0 D180; # small r
|
||||
F1 D181; # small s
|
||||
F2 D182; # small t
|
||||
F3 D183; # small u
|
||||
F4 D184; # small f
|
||||
F5 D185; # small kh
|
||||
F6 D186; # small ts
|
||||
F7 D187; # small ch
|
||||
F8 D188; # small sh
|
||||
F9 D189; # small shch
|
||||
FA D18A; # small hard sign
|
||||
FB D18B; # small y
|
||||
FC D18C; # small soft sign
|
||||
FD D18D; # small e
|
||||
FE D18E; # small yu
|
||||
FF D18F; # small ya
|
||||
}
|
||||
76
letsencrypt/client/plugins/nginx/tests/util.py
Normal file
76
letsencrypt/client/plugins/nginx/tests/util.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""Common utilities for letsencrypt.client.nginx."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt.client.plugins.nginx import constants
|
||||
from letsencrypt.client.plugins.nginx import configurator
|
||||
|
||||
|
||||
class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
||||
|
||||
def setUp(self):
|
||||
super(NginxTest, self).setUp()
|
||||
|
||||
self.temp_dir, self.config_dir, self.work_dir = dir_setup(
|
||||
"testdata")
|
||||
|
||||
self.ssl_options = setup_nginx_ssl_options(self.config_dir)
|
||||
|
||||
self.config_path = os.path.join(
|
||||
self.temp_dir, "testdata")
|
||||
|
||||
self.rsa256_file = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
self.rsa256_pem = pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
|
||||
|
||||
def get_data_filename(filename):
|
||||
"""Gets the filename of a test data file."""
|
||||
return pkg_resources.resource_filename(
|
||||
"letsencrypt.client.plugins.nginx.tests", "testdata/%s" % filename)
|
||||
|
||||
|
||||
def dir_setup(test_dir="debian_nginx/two_vhost_80"):
|
||||
"""Setup the directories necessary for the configurator."""
|
||||
temp_dir = tempfile.mkdtemp("temp")
|
||||
config_dir = tempfile.mkdtemp("config")
|
||||
work_dir = tempfile.mkdtemp("work")
|
||||
|
||||
test_configs = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.plugins.nginx.tests", test_dir)
|
||||
|
||||
shutil.copytree(
|
||||
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
|
||||
|
||||
return temp_dir, config_dir, work_dir
|
||||
|
||||
|
||||
def setup_nginx_ssl_options(config_dir):
|
||||
"""Move the ssl_options into position and return the path."""
|
||||
option_path = os.path.join(config_dir, "options-ssl.conf")
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, option_path)
|
||||
return option_path
|
||||
|
||||
|
||||
def get_nginx_configurator(
|
||||
config_path, config_dir, work_dir, ssl_options, version=(1, 6, 2)):
|
||||
"""Create an Nginx Configurator with the specified options."""
|
||||
|
||||
backups = os.path.join(work_dir, "backups")
|
||||
|
||||
config = configurator.NginxConfigurator(
|
||||
mock.MagicMock(
|
||||
nginx_server_root=config_path, nginx_mod_ssl_conf=ssl_options,
|
||||
le_vhost_ext="-le-ssl.conf", backup_dir=backups,
|
||||
config_dir=config_dir, work_dir=work_dir,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS")),
|
||||
version)
|
||||
config.prepare()
|
||||
return config
|
||||
|
|
@ -8,8 +8,10 @@ from letsencrypt.acme import challenges
|
|||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", os.path.join("testdata", "rsa256_key.pem")))
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests",
|
||||
os.path.join("testdata", "rsa256_key.pem"))))
|
||||
|
||||
# Challenges
|
||||
SIMPLE_HTTPS = challenges.SimpleHTTPS(
|
||||
|
|
@ -27,40 +29,40 @@ POP = challenges.ProofOfPossession(
|
|||
alg="RS256", nonce="xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ",
|
||||
hints=challenges.ProofOfPossession.Hints(
|
||||
jwk=jose.JWKRSA(key=KEY.publickey()),
|
||||
cert_fingerprints=[
|
||||
cert_fingerprints=(
|
||||
"93416768eb85e33adc4277f4c9acd63e7418fcfe",
|
||||
"16d95b7b63f1972b980b14c20291f3c0d1855d95",
|
||||
"48b46570d9fc6358108af43ad1649484def0debf"
|
||||
],
|
||||
certs=[], # TODO
|
||||
subject_key_identifiers=["d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"],
|
||||
serial_numbers=[34234239832, 23993939911, 17],
|
||||
issuers=[
|
||||
),
|
||||
certs=(), # TODO
|
||||
subject_key_identifiers=("d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"),
|
||||
serial_numbers=(34234239832, 23993939911, 17),
|
||||
issuers=(
|
||||
"C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA",
|
||||
"O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure",
|
||||
],
|
||||
authorized_for=["www.example.com", "example.net"],
|
||||
),
|
||||
authorized_for=("www.example.com", "example.net"),
|
||||
)
|
||||
)
|
||||
|
||||
CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP]
|
||||
DV_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.DVChallenge)]
|
||||
CLIENT_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.ClientChallenge)]
|
||||
CONT_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.ContinuityChallenge)]
|
||||
|
||||
|
||||
def gen_combos(challs):
|
||||
"""Generate natural combinations for challs."""
|
||||
dv_chall = []
|
||||
renewal_chall = []
|
||||
cont_chall = []
|
||||
|
||||
for i, chall in enumerate(challs): # pylint: disable=redefined-outer-name
|
||||
if isinstance(chall, challenges.DVChallenge):
|
||||
dv_chall.append(i)
|
||||
else:
|
||||
renewal_chall.append(i)
|
||||
cont_chall.append(i)
|
||||
|
||||
# Gen combos for 1 of each type
|
||||
return [[i, j] for i in xrange(len(dv_chall))
|
||||
for j in xrange(len(renewal_chall))]
|
||||
# Gen combos for 1 of each type, lowest index first (makes testing easier)
|
||||
return tuple((i, j) if i < j else (j, i)
|
||||
for i in dv_chall for j in cont_chall)
|
||||
|
|
|
|||
|
|
@ -30,17 +30,17 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
|
||||
self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator")
|
||||
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
|
||||
|
||||
self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI]
|
||||
self.mock_client_auth.get_chall_pref.return_value = [
|
||||
self.mock_cont_auth.get_chall_pref.return_value = [
|
||||
challenges.RecoveryToken]
|
||||
|
||||
self.mock_client_auth.perform.side_effect = gen_auth_resp
|
||||
self.mock_cont_auth.perform.side_effect = gen_auth_resp
|
||||
self.mock_dv_auth.perform.side_effect = gen_auth_resp
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_client_auth, None)
|
||||
self.mock_dv_auth, self.mock_cont_auth, None)
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
|
|
@ -61,9 +61,9 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual("DVSNI0", self.handler.responses[dom][0])
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
|
||||
def test_name1_rectok1(self):
|
||||
dom = "0"
|
||||
|
|
@ -78,16 +78,16 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
|
||||
# Test if statement for dv_auth perform
|
||||
self.assertEqual(self.mock_client_auth.perform.call_count, 1)
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 1)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 0)
|
||||
|
||||
self.assertEqual("RecoveryToken0", self.handler.responses[dom][0])
|
||||
# Assert 1 domain
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
# Assert 1 auth challenge, 0 dv
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
def test_name5_dvsni5(self):
|
||||
for i in xrange(5):
|
||||
|
|
@ -102,11 +102,11 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
# Each message contains 1 auth, 0 client
|
||||
|
||||
# Test proper call count for methods
|
||||
self.assertEqual(self.mock_client_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
|
||||
|
||||
for i in xrange(5):
|
||||
|
|
@ -114,7 +114,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
self.assertEqual(self.handler.responses[dom][0], "DVSNI%d" % i)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.DVSNI))
|
||||
|
||||
|
|
@ -138,10 +138,10 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(len(self.handler.responses[dom]),
|
||||
len(acme_util.DV_CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
|
||||
# Test if statement for client_auth perform
|
||||
self.assertEqual(self.mock_client_auth.perform.call_count, 0)
|
||||
# Test if statement for cont_auth perform
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
|
||||
|
||||
self.assertEqual(
|
||||
|
|
@ -149,7 +149,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self._get_exp_response(dom, path, acme_util.DV_CHALLENGES))
|
||||
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.SimpleHTTPS))
|
||||
|
||||
|
|
@ -175,16 +175,16 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
len(self.handler.responses[dom]), len(acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, acme_util.CHALLENGES))
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.SimpleHTTPS))
|
||||
self.assertTrue(isinstance(self.handler.client_c[dom][0].achall,
|
||||
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
|
||||
achallenges.RecoveryToken))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
|
|
@ -209,7 +209,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
len(self.handler.responses[str(i)]), len(acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
|
|
@ -217,11 +217,11 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.DVSNI))
|
||||
self.assertTrue(isinstance(self.handler.client_c[dom][0].achall,
|
||||
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
|
||||
achallenges.RecoveryContact))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
|
|
@ -255,7 +255,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
|
|
@ -263,7 +263,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(self.handler.responses[dom], resp)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(
|
||||
len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1)
|
||||
len(self.handler.cont_c[dom]), len(chosen_chall[i]) - 1)
|
||||
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["0"][0].achall, achallenges.DNS))
|
||||
|
|
@ -276,10 +276,10 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["4"][0].achall, achallenges.DNS))
|
||||
|
||||
self.assertTrue(isinstance(self.handler.client_c["2"][0].achall,
|
||||
self.assertTrue(isinstance(self.handler.cont_c["2"][0].achall,
|
||||
achallenges.ProofOfPossession))
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.client_c["4"][0].achall, achallenges.RecoveryToken))
|
||||
self.handler.cont_c["4"][0].achall, achallenges.RecoveryToken))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_perform_exception_cleanup(self, mock_chall_path):
|
||||
|
|
@ -309,11 +309,11 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
|
||||
# Verify cleanup is actually run correctly
|
||||
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 2)
|
||||
self.assertEqual(self.mock_client_auth.cleanup.call_count, 2)
|
||||
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 2)
|
||||
|
||||
|
||||
dv_cleanup_args = self.mock_dv_auth.cleanup.call_args_list
|
||||
client_cleanup_args = self.mock_client_auth.cleanup.call_args_list
|
||||
cont_cleanup_args = self.mock_cont_auth.cleanup.call_args_list
|
||||
|
||||
# Check DV cleanup
|
||||
for i in xrange(2):
|
||||
|
|
@ -325,10 +325,10 @@ class SatisfyChallengesTest(unittest.TestCase):
|
|||
|
||||
# Check Auth cleanup
|
||||
for i in xrange(2):
|
||||
client_chall_list = client_cleanup_args[i][0][0]
|
||||
self.assertEqual(len(client_chall_list), 1)
|
||||
cont_chall_list = cont_cleanup_args[i][0][0]
|
||||
self.assertEqual(len(cont_chall_list), 1)
|
||||
self.assertTrue(
|
||||
isinstance(client_chall_list[0], achallenges.ProofOfPossession))
|
||||
isinstance(cont_chall_list[0], achallenges.ProofOfPossession))
|
||||
|
||||
|
||||
def _get_exp_response(self, domain, path, challs):
|
||||
|
|
@ -346,7 +346,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
|
||||
self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator")
|
||||
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
|
||||
|
||||
self.mock_sat_chall = mock.MagicMock(name="_satisfy_challenges")
|
||||
self.mock_acme_auth = mock.MagicMock(name="acme_authorization")
|
||||
|
|
@ -354,7 +354,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
self.iteration = 0
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_client_auth, None)
|
||||
self.mock_dv_auth, self.mock_cont_auth, None)
|
||||
|
||||
self.handler._satisfy_challenges = self.mock_sat_chall
|
||||
self.handler.acme_authorization = self.mock_acme_auth
|
||||
|
|
@ -388,7 +388,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
# Assignment was > 80 char...
|
||||
dv_c, c_c = self.handler._challenge_factory(dom, [0])
|
||||
|
||||
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
|
||||
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
|
||||
|
||||
def test_progress_failure(self):
|
||||
self.handler.add_chall_msg(
|
||||
|
|
@ -414,7 +414,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
self.handler.msgs[dom].challenges)
|
||||
dv_c, c_c = self.handler._challenge_factory(
|
||||
dom, self.handler.paths[dom])
|
||||
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
|
||||
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
|
||||
|
||||
def test_incremental_progress(self):
|
||||
for dom, challs in [("0", acme_util.CHALLENGES),
|
||||
|
|
@ -444,9 +444,9 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
self.handler.paths["1"] = [2]
|
||||
# This is probably overkill... but set it anyway
|
||||
dv_c, c_c = self.handler._challenge_factory("0", [1, 3])
|
||||
self.handler.dv_c["0"], self.handler.client_c["0"] = dv_c, c_c
|
||||
self.handler.dv_c["0"], self.handler.cont_c["0"] = dv_c, c_c
|
||||
dv_c, c_c = self.handler._challenge_factory("1", [2])
|
||||
self.handler.dv_c["1"], self.handler.client_c["1"] = dv_c, c_c
|
||||
self.handler.dv_c["1"], self.handler.cont_c["1"] = dv_c, c_c
|
||||
|
||||
self.iteration += 1
|
||||
|
||||
|
|
@ -513,6 +513,77 @@ class PathSatisfiedTest(unittest.TestCase):
|
|||
self.assertFalse(self.handler._path_satisfied(dom[i]))
|
||||
|
||||
|
||||
class GenChallengePathTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.auth_handler.gen_challenge_path.
|
||||
|
||||
.. todo:: Add more tests for dumb_path... depending on what we want to do.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
logging.disable(logging.fatal)
|
||||
|
||||
def tearDown(self):
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
@classmethod
|
||||
def _call(cls, challs, preferences, combinations):
|
||||
from letsencrypt.client.auth_handler import gen_challenge_path
|
||||
return gen_challenge_path(challs, preferences, combinations)
|
||||
|
||||
def test_common_case(self):
|
||||
"""Given DVSNI and SimpleHTTPS with appropriate combos."""
|
||||
challs = (acme_util.DVSNI, acme_util.SIMPLE_HTTPS)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0,), (1,))
|
||||
|
||||
# Smart then trivial dumb path test
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0,))
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
# Rearrange order...
|
||||
self.assertEqual(self._call(challs[::-1], prefs, combos), (1,))
|
||||
self.assertTrue(self._call(challs[::-1], prefs, None))
|
||||
|
||||
def test_common_case_with_continuity(self):
|
||||
challs = (acme_util.RECOVERY_TOKEN,
|
||||
acme_util.RECOVERY_CONTACT,
|
||||
acme_util.DVSNI,
|
||||
acme_util.SIMPLE_HTTPS)
|
||||
prefs = [challenges.RecoveryToken, challenges.DVSNI]
|
||||
combos = acme_util.gen_combos(challs)
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0, 2))
|
||||
|
||||
# dumb_path() trivial test
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
|
||||
def test_full_cont_server(self):
|
||||
challs = (acme_util.RECOVERY_TOKEN,
|
||||
acme_util.RECOVERY_CONTACT,
|
||||
acme_util.POP,
|
||||
acme_util.DVSNI,
|
||||
acme_util.SIMPLE_HTTPS,
|
||||
acme_util.DNS)
|
||||
# Typical webserver client that can do everything except DNS
|
||||
# Attempted to make the order realistic
|
||||
prefs = [challenges.RecoveryToken,
|
||||
challenges.ProofOfPossession,
|
||||
challenges.SimpleHTTPS,
|
||||
challenges.DVSNI,
|
||||
challenges.RecoveryContact]
|
||||
combos = acme_util.gen_combos(challs)
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0, 4))
|
||||
|
||||
# Dumb path trivial test
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
|
||||
def test_not_supported(self):
|
||||
challs = (acme_util.POP, acme_util.DVSNI)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0, 1),)
|
||||
|
||||
self.assertRaises(errors.LetsEncryptAuthHandlerError,
|
||||
self._call, challs, prefs, combos)
|
||||
|
||||
|
||||
class MutuallyExclusiveTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.auth_handler.mutually_exclusive."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Test the ClientAuthenticator dispatcher."""
|
||||
"""Test the ContinuityAuthenticator dispatcher."""
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
|
@ -13,9 +13,9 @@ class PerformTest(unittest.TestCase):
|
|||
"""Test client perform function."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.client_authenticator import ClientAuthenticator
|
||||
from letsencrypt.client.continuity_auth import ContinuityAuthenticator
|
||||
|
||||
self.auth = ClientAuthenticator(
|
||||
self.auth = ContinuityAuthenticator(
|
||||
mock.MagicMock(server="demo_server.org"))
|
||||
self.auth.rec_token.perform = mock.MagicMock(
|
||||
name="rec_token_perform", side_effect=gen_client_resp)
|
||||
|
|
@ -38,7 +38,7 @@ class PerformTest(unittest.TestCase):
|
|||
|
||||
def test_unexpected(self):
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptClientAuthError, self.auth.perform, [
|
||||
errors.LetsEncryptContAuthError, self.auth.perform, [
|
||||
achallenges.DVSNI(chall=None, domain="0", key="invalid_key")])
|
||||
|
||||
def test_chall_pref(self):
|
||||
|
|
@ -50,9 +50,9 @@ class CleanupTest(unittest.TestCase):
|
|||
"""Test the Authenticator cleanup function."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.client_authenticator import ClientAuthenticator
|
||||
from letsencrypt.client.continuity_auth import ContinuityAuthenticator
|
||||
|
||||
self.auth = ClientAuthenticator(
|
||||
self.auth = ContinuityAuthenticator(
|
||||
mock.MagicMock(server="demo_server.org"))
|
||||
self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup")
|
||||
self.auth.rec_token.cleanup = self.mock_cleanup
|
||||
|
|
@ -70,7 +70,7 @@ class CleanupTest(unittest.TestCase):
|
|||
token = achallenges.RecoveryToken(chall=None, domain="0")
|
||||
unexpected = achallenges.DVSNI(chall=None, domain="0", key="dummy_key")
|
||||
|
||||
self.assertRaises(errors.LetsEncryptClientAuthError,
|
||||
self.assertRaises(errors.LetsEncryptContAuthError,
|
||||
self.auth.cleanup, [token, unexpected])
|
||||
|
||||
|
||||
458
letsencrypt/client/tests/network2_test.py
Normal file
458
letsencrypt/client/tests/network2_test.py
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
"""Tests for letsencrypt.client.network2."""
|
||||
import datetime
|
||||
import httplib
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import M2Crypto
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from letsencrypt.client import errors
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages2
|
||||
|
||||
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/cert.pem'))))
|
||||
CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/cert-san.pem'))))
|
||||
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/csr.pem'))))
|
||||
KEY = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/rsa512_key.pem')))
|
||||
KEY2 = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/rsa256_key.pem')))
|
||||
|
||||
|
||||
class NetworkTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.network2.Network."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.network2 import Network
|
||||
self.net = Network(
|
||||
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
key=KEY, alg=jose.RS256)
|
||||
self.response = mock.MagicMock(ok=True, status_code=httplib.OK)
|
||||
self.response.headers = {}
|
||||
self.response.links = {}
|
||||
|
||||
self.identifier = messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example.com')
|
||||
|
||||
# Registration
|
||||
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
|
||||
reg = messages2.Registration(
|
||||
contact=self.contact, key=KEY.public(), recovery_token='t')
|
||||
self.regr = messages2.RegistrationResource(
|
||||
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
|
||||
new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
terms_of_service='https://www.letsencrypt-demo.org/tos')
|
||||
|
||||
# Authorization
|
||||
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
|
||||
challb = messages2.ChallengeBody(
|
||||
uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID,
|
||||
chall=challenges.DNS(token='foo'))
|
||||
self.challr = messages2.ChallengeResource(
|
||||
body=challb, authzr_uri=authzr_uri)
|
||||
self.authz = messages2.Authorization(
|
||||
identifier=messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example.com'),
|
||||
challenges=(challb,), combinations=None, key=KEY.public())
|
||||
self.authzr = messages2.AuthorizationResource(
|
||||
body=self.authz, uri=authzr_uri,
|
||||
new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert')
|
||||
|
||||
# Request issuance
|
||||
self.certr = messages2.CertificateResource(
|
||||
body=CERT, authzrs=(self.authzr,),
|
||||
uri='https://www.letsencrypt-demo.org/acme/cert/1',
|
||||
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
|
||||
|
||||
def _mock_post_get(self):
|
||||
# pylint: disable=protected-access
|
||||
self.net._post = mock.MagicMock(return_value=self.response)
|
||||
self.net._get = mock.MagicMock(return_value=self.response)
|
||||
|
||||
def test_wrap_in_jws(self):
|
||||
class MockJSONDeSerializable(jose.JSONDeSerializable):
|
||||
# pylint: disable=missing-docstring
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def to_partial_json(self):
|
||||
return self.value
|
||||
@classmethod
|
||||
def from_json(cls, value):
|
||||
return cls(value)
|
||||
# pylint: disable=protected-access
|
||||
jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo'))
|
||||
self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"')
|
||||
|
||||
def test_check_response_not_ok_jobj_no_error(self):
|
||||
self.response.ok = False
|
||||
self.response.json.return_value = {}
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_not_ok_jobj_error(self):
|
||||
self.response.ok = False
|
||||
self.response.json.return_value = messages2.Error(detail='foo')
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
messages2.Error, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_not_ok_no_jobj(self):
|
||||
self.response.ok = False
|
||||
self.response.json.side_effect = ValueError
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_ok_no_jobj_ct_required(self):
|
||||
self.response.json.side_effect = ValueError
|
||||
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
|
||||
self.response.headers['Content-Type'] = response_ct
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response,
|
||||
content_type=self.net.JSON_CONTENT_TYPE)
|
||||
|
||||
def test_check_response_ok_no_jobj_no_ct(self):
|
||||
self.response.json.side_effect = ValueError
|
||||
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
|
||||
self.response.headers['Content-Type'] = response_ct
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response(self.response)
|
||||
|
||||
def test_check_response_jobj(self):
|
||||
self.response.json.return_value = {}
|
||||
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
|
||||
self.response.headers['Content-Type'] = response_ct
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response(self.response)
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_get_requests_error_passthrough(self, requests_mock):
|
||||
requests_mock.exceptions = requests.exceptions
|
||||
requests_mock.get.side_effect = requests.exceptions.RequestException
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(errors.NetworkError, self.net._get, 'uri')
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_get(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = mock.MagicMock()
|
||||
self.net._get('uri', content_type='ct')
|
||||
self.net._check_response.assert_called_once_with(
|
||||
requests_mock.get('uri'), content_type='ct')
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_post_requests_error_passthrough(self, requests_mock):
|
||||
requests_mock.exceptions = requests.exceptions
|
||||
requests_mock.post.side_effect = requests.exceptions.RequestException
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(errors.NetworkError, self.net._post, 'uri', 'data')
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_post(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = mock.MagicMock()
|
||||
self.net._post('uri', 'data', content_type='ct')
|
||||
self.net._check_response.assert_called_once_with(
|
||||
requests_mock.post('uri', 'data'), content_type='ct')
|
||||
|
||||
def test_register(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.links.update({
|
||||
'next': {'url': self.regr.new_authzr_uri},
|
||||
'terms-of-service': {'url': self.regr.terms_of_service},
|
||||
})
|
||||
|
||||
self._mock_post_get()
|
||||
self.assertEqual(self.regr, self.net.register(self.contact))
|
||||
# TODO: test POST call arguments
|
||||
|
||||
# TODO: split here and separate test
|
||||
reg_wrong_key = self.regr.body.update(key=KEY2.public())
|
||||
self.response.json.return_value = reg_wrong_key.to_json()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.register, self.contact)
|
||||
|
||||
def test_register_missing_next(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.register, self.regr.body)
|
||||
|
||||
def test_update_registration(self):
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self._mock_post_get()
|
||||
self.assertEqual(self.regr, self.net.update_registration(self.regr))
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.response.json.return_value = self.regr.body.update(
|
||||
contact=()).to_json()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.update_registration, self.regr)
|
||||
|
||||
def test_request_challenges(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self.response.headers['Location'] = self.authzr.uri
|
||||
self.response.json.return_value = self.authz.to_json()
|
||||
self.response.links = {
|
||||
'next': {'url': self.authzr.new_cert_uri},
|
||||
}
|
||||
|
||||
self._mock_post_get()
|
||||
self.net.request_challenges(self.identifier, self.regr)
|
||||
# TODO: test POST call arguments
|
||||
|
||||
# TODO: split here and separate test
|
||||
authz_wrong_key = self.authz.update(key=KEY2.public())
|
||||
self.response.json.return_value = authz_wrong_key.to_json()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.request_challenges,
|
||||
self.identifier, self.regr)
|
||||
|
||||
def test_request_challenges_missing_next(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.request_challenges,
|
||||
self.identifier, self.regr)
|
||||
|
||||
def test_request_domain_challenges(self):
|
||||
self.net.request_challenges = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
self.net.request_challenges(self.identifier),
|
||||
self.net.request_domain_challenges('example.com', self.regr))
|
||||
|
||||
def test_answer_challenge(self):
|
||||
self.response.links['up'] = {'url': self.challr.authzr_uri}
|
||||
self.response.json.return_value = self.challr.body.to_json()
|
||||
|
||||
chall_response = challenges.DNSResponse()
|
||||
|
||||
self._mock_post_get()
|
||||
self.net.answer_challenge(self.challr.body, chall_response)
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge,
|
||||
self.challr.body.update(uri='foo'), chall_response)
|
||||
|
||||
def test_answer_challenge_missing_next(self):
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.answer_challenge,
|
||||
self.challr.body, challenges.DNSResponse())
|
||||
|
||||
def test_answer_challenges(self):
|
||||
self.net.answer_challenge = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
[self.net.answer_challenge(
|
||||
self.challr.body, challenges.DNSResponse())],
|
||||
self.net.answer_challenges(
|
||||
[self.challr.body], [challenges.DNSResponse()]))
|
||||
|
||||
def test_retry_after_date(self):
|
||||
self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT'
|
||||
self.assertEqual(
|
||||
datetime.datetime(1999, 12, 31, 23, 59, 59),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
def test_retry_after_invalid(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
self.response.headers['Retry-After'] = 'foooo'
|
||||
self.assertEqual(
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 10),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
def test_retry_after_seconds(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
self.response.headers['Retry-After'] = '50'
|
||||
self.assertEqual(
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 50),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
def test_retry_after_missing(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
self.assertEqual(
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 10),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
def test_poll(self):
|
||||
self.response.json.return_value = self.authzr.body.to_json()
|
||||
self._mock_post_get()
|
||||
self.assertEqual((self.authzr, self.response),
|
||||
self.net.poll(self.authzr))
|
||||
|
||||
def test_request_issuance(self):
|
||||
self.response.content = CERT.as_der()
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self.response.links['up'] = {'url': self.certr.cert_chain_uri}
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr, self.net.request_issuance(CSR, (self.authzr,)))
|
||||
# TODO: check POST args
|
||||
|
||||
def test_request_issuance_missing_up(self):
|
||||
self.response.content = CERT.as_der()
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr.update(cert_chain_uri=None),
|
||||
self.net.request_issuance(CSR, (self.authzr,)))
|
||||
|
||||
def test_request_issuance_missing_location(self):
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.request_issuance,
|
||||
CSR, (self.authzr,))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
@mock.patch('letsencrypt.client.network2.time')
|
||||
def test_poll_and_request_issuance(self, time_mock, dt_mock):
|
||||
# clock.dt | pylint: disable=no-member
|
||||
clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27))
|
||||
|
||||
def sleep(seconds):
|
||||
"""increment clock"""
|
||||
clock.dt += datetime.timedelta(seconds=seconds)
|
||||
time_mock.sleep.side_effect = sleep
|
||||
|
||||
def now():
|
||||
"""return current clock value"""
|
||||
return clock.dt
|
||||
dt_mock.datetime.now.side_effect = now
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
def poll(authzr): # pylint: disable=missing-docstring
|
||||
# record poll start time based on the current clock value
|
||||
authzr.times.append(clock.dt)
|
||||
|
||||
# suppose it takes 2 seconds for server to produce the
|
||||
# result, increment clock
|
||||
clock.dt += datetime.timedelta(seconds=2)
|
||||
|
||||
if not authzr.retries: # no more retries
|
||||
done = mock.MagicMock(uri=authzr.uri, times=authzr.times)
|
||||
done.body.status = messages2.STATUS_VALID
|
||||
return done, []
|
||||
|
||||
# response (2nd result tuple element) is reduced to only
|
||||
# Retry-After header contents represented as integer
|
||||
# seconds; authzr.retries is a list of Retry-After
|
||||
# headers, head(retries) is peeled of as a current
|
||||
# Retry-After header, and tail(retries) is persisted for
|
||||
# later poll() calls
|
||||
return (mock.MagicMock(retries=authzr.retries[1:],
|
||||
uri=authzr.uri + '.', times=authzr.times),
|
||||
authzr.retries[0])
|
||||
self.net.poll = mock.MagicMock(side_effect=poll)
|
||||
|
||||
mintime = 7
|
||||
|
||||
def retry_after(response, default): # pylint: disable=missing-docstring
|
||||
# check that poll_and_request_issuance correctly passes mintime
|
||||
self.assertEqual(default, mintime)
|
||||
return clock.dt + datetime.timedelta(seconds=response)
|
||||
self.net.retry_after = mock.MagicMock(side_effect=retry_after)
|
||||
|
||||
def request_issuance(csr, authzrs): # pylint: disable=missing-docstring
|
||||
return csr, authzrs
|
||||
self.net.request_issuance = mock.MagicMock(side_effect=request_issuance)
|
||||
|
||||
csr = mock.MagicMock()
|
||||
authzrs = (
|
||||
mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)),
|
||||
mock.MagicMock(uri='b', times=[], retries=(5,)),
|
||||
)
|
||||
|
||||
cert, updated_authzrs = self.net.poll_and_request_issuance(
|
||||
csr, authzrs, mintime=mintime)
|
||||
self.assertTrue(cert[0] is csr)
|
||||
self.assertTrue(cert[1] is updated_authzrs)
|
||||
self.assertEqual(updated_authzrs[0].uri, 'a...')
|
||||
self.assertEqual(updated_authzrs[1].uri, 'b.')
|
||||
self.assertEqual(updated_authzrs[0].times, [
|
||||
datetime.datetime(2015, 3, 27),
|
||||
# a is scheduled for 10, but b is polling [9..11), so it
|
||||
# will be picked up as soon as b is finished, without
|
||||
# additional sleeping
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 11),
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 33),
|
||||
datetime.datetime(2015, 3, 27, 0, 1, 5),
|
||||
])
|
||||
self.assertEqual(updated_authzrs[1].times, [
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 2),
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 9),
|
||||
])
|
||||
self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7))
|
||||
|
||||
def test_check_cert(self):
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self.response.content = CERT2.as_der()
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr.update(body=CERT2), self.net.check_cert(self.certr))
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.response.headers['Location'] = 'foo'
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.check_cert, self.certr)
|
||||
|
||||
def test_check_cert_missing_location(self):
|
||||
self.response.content = CERT2.as_der()
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr)
|
||||
|
||||
def test_refresh(self):
|
||||
self.net.check_cert = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
self.net.check_cert(self.certr), self.net.refresh(self.certr))
|
||||
|
||||
def test_fetch_chain(self):
|
||||
# pylint: disable=protected-access
|
||||
self.net._get_cert = mock.MagicMock()
|
||||
self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri),
|
||||
self.net.fetch_chain(self.certr))
|
||||
|
||||
def test_fetch_chain_no_up_link(self):
|
||||
self.assertTrue(self.net.fetch_chain(self.certr.update(
|
||||
cert_chain_uri=None)) is None)
|
||||
|
||||
def test_revoke(self):
|
||||
self._mock_post_get()
|
||||
self.net.revoke(self.certr, when=messages2.Revocation.NOW)
|
||||
# pylint: disable=protected-access
|
||||
self.net._post.assert_called_once_with(self.certr.uri, mock.ANY)
|
||||
|
||||
def test_revoke_bad_status_raises_error(self):
|
||||
self.response.status_code = httplib.METHOD_NOT_ALLOWED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.revoke, self.certr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
11
setup.py
11
setup.py
|
|
@ -11,6 +11,7 @@ from setuptools import setup
|
|||
if os.path.abspath(__file__).split(os.path.sep)[1] == 'vagrant':
|
||||
del os.link
|
||||
|
||||
|
||||
def read_file(filename, encoding='utf8'):
|
||||
"""Read unicode from given file."""
|
||||
with codecs.open(filename, encoding=encoding) as fd:
|
||||
|
|
@ -36,9 +37,13 @@ install_requires = [
|
|||
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
|
||||
'pycrypto',
|
||||
'PyOpenSSL',
|
||||
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
|
||||
'pyrfc3339',
|
||||
'python-augeas',
|
||||
'python2-pythondialog',
|
||||
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
|
||||
'pytz',
|
||||
'requests',
|
||||
'werkzeug',
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
# order of items in install_requires DOES matter and M2Crypto has
|
||||
|
|
@ -100,6 +105,8 @@ setup(
|
|||
'letsencrypt.client.plugins',
|
||||
'letsencrypt.client.plugins.apache',
|
||||
'letsencrypt.client.plugins.apache.tests',
|
||||
'letsencrypt.client.plugins.nginx',
|
||||
'letsencrypt.client.plugins.nginx.tests',
|
||||
'letsencrypt.client.plugins.standalone',
|
||||
'letsencrypt.client.plugins.standalone.tests',
|
||||
'letsencrypt.client.tests',
|
||||
|
|
@ -124,6 +131,8 @@ setup(
|
|||
'letsencrypt.plugins': [
|
||||
'apache = letsencrypt.client.plugins.apache.configurator'
|
||||
':ApacheConfigurator',
|
||||
'nginx = letsencrypt.client.plugins.nginx.configurator'
|
||||
':NginxConfigurator',
|
||||
'standalone = letsencrypt.client.plugins.standalone.authenticator'
|
||||
':StandaloneAuthenticator',
|
||||
],
|
||||
|
|
|
|||
2
tox.ini
2
tox.ini
|
|
@ -19,7 +19,7 @@ setenv =
|
|||
basepython = python2.7
|
||||
commands =
|
||||
pip install -e .[testing]
|
||||
python setup.py nosetests --with-coverage --cover-min-percentage=86
|
||||
python setup.py nosetests --with-coverage --cover-min-percentage=87
|
||||
|
||||
[testenv:lint]
|
||||
# recent versions of pylint do not support Python 2.6 (#97, #187)
|
||||
|
|
|
|||
Loading…
Reference in a new issue