Compare commits

...

648 commits

Author SHA1 Message Date
Jacob Hoffman-Andrews
216e589d46 Replace lambda with comprehension. 2015-10-11 12:30:02 -07:00
Jacob Hoffman-Andrews
f16489f762 Go back to hasattr and add a test. 2015-10-11 12:19:39 -07:00
Jacob Hoffman-Andrews
06c85d6b5a Fix line-wrapped function indents. 2015-10-11 11:30:11 -07:00
Jacob Hoffman-Andrews
f0cfd69cdc Respond to review feedback. 2015-10-11 11:28:39 -07:00
Jacob Hoffman-Andrews
cd52fc02b9 Add a sleep to let Nginx finish reloading. 2015-10-11 11:20:21 -07:00
Jacob Hoffman-Andrews
dd8c6d6548 Nginx improvements
Add a server_names_hash_bucket_size directive during challenges to fix an nginx
crash on restart (Fixes #922).

Use fullchain instead of chain (Fixes #610).

Implement OCSP stapling (Fixes #937, Fixes #931).

Hide Boulder output in integration tests to make them more readable.
2015-10-11 10:20:08 -07:00
Peter Eckersley
53d532cfe3 Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt 2015-10-08 11:32:19 -07:00
bmw
52a45d158a Merge pull request #915 from kuba/jwk_thumbprints
Add naive JWK Thumbprint implementation
2015-10-07 17:37:51 -07:00
bmw
a6b9ec1563 Merge pull request #920 from kuba/bugs/884
Add docs to tarballs (fixes #884).
2015-10-07 15:50:58 -07:00
bmw
5a388bbf5c Merge pull request #913 from kuba/bugs/908
Don't save KGS in dist dir in dev release script (fixes #908).
2015-10-07 15:49:25 -07:00
Jakub Warmuz
0034a8fae4
Add docs to tarballs (fixes #884). 2015-10-07 22:40:02 +00:00
bmw
412e6acef4 Merge pull request #916 from kuba/rm-simplehttp-tls
Remove SimpleHTTP TLS from core
2015-10-07 10:13:44 -07:00
bmw
a2c41ca7f5 Merge pull request #911 from kuba/cover
Bump core coverage to 98%
2015-10-07 09:26:48 -07:00
Jakub Warmuz
e4e94b20d4
Remove --no-simple-http-tls from integration tests 2015-10-07 06:23:40 +00:00
Jakub Warmuz
73ee63779c
Remove --no-simple-http-tls 2015-10-07 06:23:28 +00:00
Jakub Warmuz
0d89fa6d88
Remove SimpleHTTP TLS from Manual Plugin. 2015-10-07 06:21:49 +00:00
Jakub Warmuz
c6ebfae15e
Unify quotes 2015-10-06 21:11:52 +00:00
Jakub Warmuz
3dac62f20e
json_dumps_pretty: prettier separators. 2015-10-06 21:11:52 +00:00
Jakub Warmuz
7e1b7ff7ae
Add naive JWK Thumbprint implementation 2015-10-06 21:05:43 +00:00
Jakub Warmuz
ae66253ddf
Don't save KGS in dist dir in dev release script (fixes #908). 2015-10-06 19:07:20 +00:00
Jakub Warmuz
f7241af5ce
Bump core coverage to 98% 2015-10-06 18:58:06 +00:00
bmw
63dc3cbb2a Merge pull request #910 from letsencrypt/dev_stuff
Specify python2 and clarify coverage requirements
2015-10-06 11:22:21 -07:00
bmw
f81174d43d Merge pull request #722 from kuba/mock-2.6
Revert "Revert PR #708."
2015-10-06 11:05:32 -07:00
Brad Warren
b275df13d3 python2 and clarified coverage 2015-10-06 10:49:33 -07:00
bmw
410f9bf383 Merge pull request #847 from kuba/bootstrap
Developer virtualenv bootstrap scripts.
2015-10-05 15:18:30 -07:00
Jakub Warmuz
26e03dbba2
docs: remove venv3. 2015-10-05 22:12:21 +00:00
bmw
4012e67c96 Merge pull request #904 from kuba/bugs/903
Fix #903: docs version parsing
2015-10-05 15:03:29 -07:00
bmw
ef66b5c9ff Merge pull request #890 from kuba/compat-test-manifest
Update MANIFEST.in for compatibility-test
2015-10-05 14:56:30 -07:00
bmw
0aaf26bdda Merge pull request #878 from letsencrypt/automation_is_enabled
Ensures renewal settings are reported correctly
2015-10-05 14:54:02 -07:00
Jakub Warmuz
4cd5a8e42c
Archlinux bootstrap: python-virtualenv 2015-10-05 21:40:55 +00:00
Jakub Warmuz
eec5542cb3
lint archlinux bootstrap script 2015-10-05 21:39:34 +00:00
Jakub Warmuz
18f1bb49f0
Merge remote-tracking branch 'github/letsencrypt/master' into bootstrap 2015-10-05 21:38:06 +00:00
bmw
e985c54b9b Merge pull request #898 from ArchimedesPi/pep8
Fix some pep8 warnings
2015-10-05 13:53:02 -07:00
Jakub Warmuz
0d880e334d
separate pip install -U setuptools pip
https://travis-ci.org/jsha/boulder/jobs/83762761#L557

Success: virtualenv --no-site-packages -p python2 ./venv
./venv/bin/pip install -U setuptools pip
Downloading/unpacking distribute from https://pypi.python.org/packages/source/d/distribute/distribute-0.7.3.zip#md5=c6c59594a7b180af57af8a0cc0cf5b4a
  Downloading distribute-0.7.3.zip (145Kb): 145Kb downloaded
  Running setup.py egg_info for package distribute

Downloading/unpacking pip from https://pypi.python.org/packages/source/p/pip/pip-7.1.2.tar.gz#md5=3823d2343d9f3aaab21cf9c917710196
  Downloading pip-7.1.2.tar.gz (1.0Mb): 1.0Mb downloaded
  Running setup.py egg_info for package pip

    warning: no previously-included files found matching '.coveragerc'
    warning: no previously-included files found matching '.mailmap'
    warning: no previously-included files found matching '.travis.yml'
    warning: no previously-included files found matching 'pip/_vendor/Makefile'
    warning: no previously-included files found matching 'tox.ini'
    warning: no previously-included files found matching 'dev-requirements.txt'
    no previously-included directories found matching '.travis'
    no previously-included directories found matching 'docs/_build'
    no previously-included directories found matching 'contrib'
    no previously-included directories found matching 'tasks'
    no previously-included directories found matching 'tests'
Downloading/unpacking setuptools>=0.7 (from distribute)
  Downloading setuptools-18.3.2.tar.gz (626Kb): 626Kb downloaded
  Running setup.py egg_info for package setuptools

Installing collected packages: distribute, pip, setuptools
  Found existing installation: distribute 0.6.24
    Uninstalling distribute:
      Successfully uninstalled distribute
  Running setup.py install for distribute

  Found existing installation: pip 1.1
    Uninstalling pip:
      Successfully uninstalled pip
  Running setup.py install for pip
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
    ImportError: No module named setuptools
    Complete output from command /home/travis/letsencrypt/venv/bin/python2 -c "import setuptools;__file__='/home/travis/letsencrypt/venv/build/pip/setup.py';exec(compile(open(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --single-version-externally-managed --record /tmp/pip-4lZMdG-record/install-record.txt --install-headers /home/travis/letsencrypt/venv/include/site/python2.7:
    Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: No module named setuptools
----------------------------------------
  Rolling back uninstall of pip
Exception:
Traceback (most recent call last):
  File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/basecommand.py", line 104, in main
    status = self.run(options, args)
  File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/commands/install.py", line 250, in run
    requirement_set.install(install_options, global_options)
  File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 1137, in install
    requirement.rollback_uninstall()
  File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 491, in rollback_uninstall
    self.uninstalled.rollback()
  File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 1450, in rollback
    pth.rollback()
AttributeError: 'str' object has no attribute 'rollback'
Storing complete log in /home/travis/.pip/pip.log
[!] FAILURE: ./venv/bin/pip install -U setuptools pip
./venv/bin/pip install -r requirements.txt -e acme -e . -e letsencrypt-apache -e letsencrypt-nginx
Traceback (most recent call last):
  File "./venv/bin/pip", line 5, in <module>
    from pkg_resources import load_entry_point
ImportError: No module named pkg_resources
2015-10-05 20:06:55 +00:00
bmw
505ebfaa45 Merge pull request #891 from ArchimedesPi/remove_crypto_util_cruft
crypto_util: Remove cruft
2015-10-05 13:00:52 -07:00
Jakub Warmuz
1e18351041
Fix #903: docs version parsing 2015-10-05 19:44:35 +00:00
Jakub Warmuz
c3e28fa909
Merge remote-tracking branch 'github/letsencrypt/master' into mock-2.6 2015-10-05 19:31:05 +00:00
Jakub Warmuz
ee9385c64b
Merge remote-tracking branch 'github/letsencrypt/master' into compat-test-manifest 2015-10-05 19:29:09 +00:00
Peter Eckersley
88def4854b Merge pull request #883 from BKreisel/bootstrap-mac
OS X Integration Tests Environment Setup
2015-10-05 12:19:21 -07:00
Brad Warren
7a49e2bcb3 Merge branch 'master' into automation_is_enabled 2015-10-05 12:09:40 -07:00
Brad Warren
a86ea53a79 Added unit tests 2015-10-05 12:09:35 -07:00
Peter Eckersley
0ecfec56e2 Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt 2015-10-05 12:05:27 -07:00
Peter Eckersley
22d756992b Merge pull request #894 from ArchimedesPi/improve-test-coverage-crypto-util
crypto_util: Refactor, improve test coverage [WIP]
2015-10-05 12:05:14 -07:00
Peter Eckersley
321ae99035 Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt 2015-10-05 11:53:57 -07:00
Peter Eckersley
0f85a74dcd Merge pull request #839 from kuba/release
Prepare for dev releases
2015-10-05 11:53:40 -07:00
Peter Eckersley
289e8f441a Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt 2015-10-05 11:53:04 -07:00
Peter Eckersley
98da3b3ba1 Merge pull request #899 from MartijnBraam/feature/archlinux-bootstrap
Added bootstrap script for archlinux
2015-10-05 11:51:15 -07:00
Peter Eckersley
8825b1f232 Merge pull request #889 from kuba/docs
Add missing API docs
2015-10-05 11:49:42 -07:00
Martijn Braam
9a59a41b04 Added bootstrap script for archlinux 2015-10-05 15:08:43 +02:00
Liam Marshall
d5ebc38b33 Fix pep8 warnings (down to only one now!) 2015-10-04 20:39:19 -05:00
Liam Marshall
917a6d6373 Make lint happy, remove unused variables from crypto_util_test 2015-10-04 15:56:40 -05:00
Liam Marshall
9c8f09ec43 Test that loading an invalid cert throws an error 2015-10-04 15:49:56 -05:00
Liam Marshall
b3bd71b424 Use previously-returned file_type in assertion 2015-10-04 15:43:51 -05:00
Liam Marshall
7b50f5d9bf Make pep8 happy 2015-10-04 15:42:05 -05:00
Liam Marshall
aa15fae11d crypto_util: merge _pyopenssl_load into pyopenssl_load_certificate 2015-10-04 15:39:29 -05:00
Liam Marshall
7b2d40ce55 crypto_util: test pyopenssl_load_certificate(...) 2015-10-04 15:34:57 -05:00
Liam Marshall
032f3e8f64 crypto_util: test _pyopenssl_load 2015-10-04 15:00:57 -05:00
Liam Marshall
884d8e9905 crypto_util: Remove unused import 2015-10-04 11:29:21 -05:00
Liam Marshall
6994dad59b crypto_util: Remove asn1_generalizedtime_to_dt(...) 2015-10-04 11:17:07 -05:00
Liam Marshall
32da607ae5 crypto_util: Remove asn1_generalizedtime_to_dt(...)
Not used by any other code AFAIK (ack'd entire codebase).
2015-10-04 10:39:38 -05:00
Jakub Warmuz
7644613171
Update Copyright notice in subpackages LICENSE.
This corresponds to changes in #871.
2015-10-04 10:10:41 +00:00
Jakub Warmuz
4ef7a6e63f
Merge remote-tracking branch 'github/letsencrypt/master' into release 2015-10-04 10:08:38 +00:00
Jakub Warmuz
5d4e1b68cd
autospec=False for socket.socket (quick-fixes: #779).
Also, https://github.com/testing-cabal/mock/issues/323
2015-10-04 08:57:53 +00:00
Jakub Warmuz
d20088a435
docs: pip install -U setuptools pip 2015-10-04 08:55:13 +00:00
Jakub Warmuz
3f08932479
Merge remote-tracking branch 'github/letsencrypt/master' into mock-2.6 2015-10-04 08:41:01 +00:00
Jakub Warmuz
4925e0b811
Update MANIFEST.in for compatibility-test 2015-10-04 07:47:14 +00:00
Jakub Warmuz
08afe48019
Add API docs for letsencrypt_compatibility_test 2015-10-04 06:37:35 +00:00
Jakub Warmuz
b89bd4b5de
Add API docs for letshelp_letsencrypt 2015-10-04 06:25:02 +00:00
Brandon Kreisel
8409c9c658 Meddle with more documentation and learn how to bash flag 2015-10-03 11:27:39 -04:00
Peter Eckersley
5b26ac14d2 Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt 2015-10-03 01:33:00 -07:00
Brandon Kreisel
0868a5962f Add documentation for OS X bootstrap script 2015-10-02 22:37:22 -04:00
Brandon Kreisel
895faa7dc9 Add OS X bootstrap for integration enviornment
Installs requirements and sets up environment to run boulder and
integration tests
2015-10-02 22:36:56 -04:00
Peter Eckersley
74b2e3bc51 Merge pull request #868 from letsencrypt/sysexit_error
ErrorHandler should ignore sys.exit
2015-10-02 00:59:24 -07:00
Brad Warren
8e45ecd975 Merge branch 'master' into sysexit_error 2015-10-01 15:40:08 -07:00
Brad Warren
d7a16ecfcb Added tests and documentation 2015-10-01 15:39:55 -07:00
bmw
ec09bd4be5 Merge pull request #874 from letsencrypt/nginx_readme
Updated README to reflect state of Nginx plugin
2015-10-01 15:28:15 -07:00
bmw
09d6e7e13c Merge pull request #876 from letsencrypt/fix_indentation
Fixed indentation in storage.py
2015-10-01 15:28:05 -07:00
James Kasten
1808b6fb94 Merge pull request #870 from letsencrypt/apache_no_install
Report Apache correctly when uninstalled
2015-10-01 16:41:03 -04:00
Brad Warren
59348ad30c Made methods private and updated tests 2015-10-01 13:33:22 -07:00
Peter Eckersley
ecd0087df5 Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt 2015-10-01 13:08:42 -07:00
Peter Eckersley
7b63f99749 Merge pull request #871 from letsencrypt/license
Make sure the LICENSE file is accurate for first pre-relase
2015-10-01 13:04:59 -07:00
Brad Warren
c976c0abdf Removed duplicated code 2015-10-01 13:03:23 -07:00
Brad Warren
6bde83c983 Fixed indentation in storage.py 2015-10-01 11:53:11 -07:00
Brad Warren
268368b3e9 Updated README to reflect state of Nginx plugin 2015-10-01 10:12:38 -07:00
Peter Eckersley
8041b35f99 Make sure the LICENSE file is accurate for first pre-relase
- In general copyrights remain with their respective authors or authors'
   organizations, but with license granted by clause 5 of the Apache License.
 - Presently the plurality of the copyright in the client is held by EFF as a result of
   work-for-hire by jdkasten, bmw, schoen, pde, rolandshoemaker and jsha;
   or by Jakub Warmuz or his employer, Google.
2015-09-30 19:23:17 -07:00
Peter Eckersley
7a7971ff8d Make sure the LICENSE file is accurate for first pre-relase
- In general copyrights remain with their respective authors or authors'
   organizations, but with license granted by clause 5 of the Apache License.
 - Presently the plurality of the copyright in the client is held by EFF as a result of
   work-for-hire by jdkasten, jdkasten, bmw, schoen, pde, rolandshoemaker and jsha;
   or by Jakub Warmuz or his employer, Google.
2015-09-30 19:06:16 -07:00
Brad Warren
9cf2ea8a57 Report Apache correctly when uninstalled 2015-09-30 17:16:27 -07:00
Peter Eckersley
393f4b4997 Merge pull request #863 from letsencrypt/subargs
Improve CLI flag and help processing
2015-09-30 17:05:06 -07:00
bmw
078b8c01f6 Merge pull request #869 from letsencrypt/broken-nginx
Mark Nginx as non-working.
2015-09-30 17:00:49 -07:00
Peter Eckersley
43cb36807a Also test top level help 2015-09-30 17:00:09 -07:00
Peter Eckersley
11ca1108c2 Test cases for command line help 2015-09-30 16:53:08 -07:00
Jacob Hoffman-Andrews
95c4b55da0 Mark Nginx as non-working. 2015-09-30 16:49:03 -07:00
Peter Eckersley
2406fc0486 Go back to VERBS as a list
The dictionary was destroying the ordering, which was important.
2015-09-30 16:07:16 -07:00
Peter Eckersley
5ca1a27200 Keep the acme/ subtree compatible with strict pylinting 2015-09-30 15:31:32 -07:00
Peter Eckersley
d85f42d71f Plugins don't need to be in HELP_TOPICS
They're already added as topics automatically, though they do need to be in
the hand-written top level help.
2015-09-30 15:29:29 -07:00
Brad Warren
bb167743f3 Don't call_registered() on SystemExit 2015-09-30 13:00:10 -07:00
Peter Eckersley
2d578468bd Use a verb -> function table instead of eval()
- plugins_cmd() not plugins() broke the more minimalist eval() approach
 - more wrangling was required to mock out calls via the VERBS table
2015-09-30 12:32:44 -07:00
Peter Eckersley
2a3a111d62 Disable pylint invalid-name
It's clearly making our code harder to read and write
2015-09-30 12:10:16 -07:00
Peter Eckersley
1e3c92c714 Cleanup the verb -> subparser mapping 2015-09-30 11:49:46 -07:00
Peter Eckersley
627fca37b4 We didn't actually need to define --help as a verb 2015-09-29 18:55:59 -07:00
Peter Eckersley
18dacc528d Preserve all argparse parameters
Try to restore all variants that applied to the different subcomannds
2015-09-29 18:55:59 -07:00
Peter Eckersley
6b6bc03882 --cert-path was required for install and revoke
Oops
2015-09-29 17:14:40 -07:00
Peter Eckersley
2297349b95 lintian 2015-09-29 16:56:36 -07:00
Peter Eckersley
4437ec5586 Merge remote-tracking branch 'origin/master' into subargs 2015-09-29 14:49:32 -07:00
Peter Eckersley
05d439a339 Update cli tests
We don't expect to error out if called with no args
2015-09-29 14:48:40 -07:00
Peter Eckersley
a0af023b14 --key-path is mandatory for install, optional for revoke 2015-09-29 14:48:26 -07:00
Peter Eckersley
2e0fd36c28 Improve flag and help processing
* letsencrypt --help $SUBCOMMAND now works.
   Fixes #787 #819
 * subcommand arguments are now actually argument groups, so that all flags
   can be placed before or after subcommand verbs as the user wishes
   Fixes: #820

A limitation:

 * args like --cert-path were previously present for multiple verbs
   (auth/install/revoke) with separate docs; they are now in the "paths"
   topic.  That's fine, though it would be good to *also* list them when the
   user types letsencrypt --help install.
2015-09-29 21:01:02 +00:00
bmw
ce520ca532 Merge pull request #859 from letsencrypt/report_success
Report cert issuance to the user
2015-09-29 13:02:00 -07:00
Brad Warren
312057b1b8 changes += kuba_feedback 2015-09-29 12:54:52 -07:00
bmw
23e766a4d3 Merge pull request #861 from letsencrypt/nginx_alpha
Mark Nginx as Alpha
2015-09-29 11:12:19 -07:00
Brad Warren
dcd274ed93 Marked Nginx as Alpha 2015-09-29 11:06:02 -07:00
bmw
74d96f9d92 Merge pull request #860 from kuba/bugs/855
UnrecognizedChallenge (fixes #855).
2015-09-29 08:52:15 -07:00
Jakub Warmuz
0ffef20a20
UnrecognizedChallenge: fix tests and lint. 2015-09-29 07:02:33 +00:00
Jakub Warmuz
ad1fce03f7
UnrecognizedChallenge (fixes #855).
Overrides quick fix from #856.
2015-09-29 06:47:15 +00:00
Brad Warren
dc0b26c278 Raised cover percentage 2015-09-28 18:47:15 -07:00
Brad Warren
ed7977fb03 Added cli tests 2015-09-28 18:45:12 -07:00
bmw
43e55ae924 Merge pull request #856 from letsencrypt/ignore-unknown-challenges
Ignore unknown challenge types
2015-09-28 16:16:11 -07:00
James Kasten
5238f53092 DVChallenge -> Challenge 2015-09-28 16:03:03 -07:00
James Kasten
67ec4d09ee Put in dummy challenge 2015-09-28 15:53:42 -07:00
Brad Warren
243c9e9021 Made cover and lint happy 2015-09-28 15:52:09 -07:00
Brad Warren
4da0e17255 Added message and changed reporter interface 2015-09-28 15:45:31 -07:00
James Kasten
fa992faf52 Fix pylint and add test 2015-09-28 15:24:51 -07:00
Brad Warren
3279aefefb Made PEP8 happy 2015-09-28 15:15:44 -07:00
bmw
e2325b9b52 Merge pull request #840 from BKreisel/master
Add Mac compatibility to boulder-start
2015-09-28 15:07:30 -07:00
Brandon Kreisel
6c90997aa8 Merge remote-tracking branch 'upstream/master' 2015-09-28 17:49:26 -04:00
Brandon Kreisel
b6bbc9e0a2 Add inline Mac comment 2015-09-28 17:39:01 -04:00
Richard Barnes
ab98d5c39f Ignore unknown challenge types 2015-09-28 17:14:33 -04:00
bmw
a70134a5c8 Merge pull request #852 from Jadaw1n/patch-1
Dockerfile: option --text doesn't exist
2015-09-28 12:42:03 -07:00
bmw
917f240423 Merge pull request #854 from letsencrypt/remove_sigpwr
Remove `signal.SIGPWR` from error_handler.py
2015-09-28 12:39:19 -07:00
bmw
3c62e5b936 Merge pull request #853 from kuba/bugs/789
Hide null installer (fixes #789).
2015-09-28 12:39:07 -07:00
Brad Warren
c1012f5f00 Removed SIGPWR entirely 2015-09-28 12:25:37 -07:00
Jakub Warmuz
315b357781
Hide null installer (fixes #789). 2015-09-28 19:08:57 +00:00
Jacob Hoffman-Andrews
9264d54605 Merge pull request #849 from letsencrypt/simple-verify-output
Emit error when simple_verify fails.
2015-09-28 12:04:06 -07:00
Jacob Hoffman-Andrews
27268afdcc Remove extra newline. 2015-09-28 11:58:12 -07:00
Jadaw1n
913a0a9e98 Dockerfile: option --text doesn't exist 2015-09-28 17:34:43 +02:00
Jacob Hoffman-Andrews
a7375eb549 Emit error when simple_verify fails.
When running the manual authenticator, if simple_verify fails, there is no
output to indicate what went wrong, just "Incomplete authorizations."
2015-09-27 23:43:26 -07:00
bmw
928fa7bae0 Merge pull request #846 from davidxia/patch1
Fix CLI --help for OS X
2015-09-27 20:34:55 -07:00
David Xia
96a737bbba Fix CLI --help for OS X
OS X's signal module doesn't have SIGPWR. Don't try to use it.

Fixes #841
2015-09-27 22:11:33 -04:00
bmw
550d73e444 Merge pull request #848 from kuba/docs
Fix various doc generation issues
2015-09-27 14:39:38 -07:00
Jakub Warmuz
5d8e9a3d68
Fix various doc generation issues 2015-09-27 21:07:40 +00:00
Jakub Warmuz
85e5165b5d
nit: missing EOF newline 2015-09-27 20:59:07 +00:00
Jakub Warmuz
6649af9479
Developer virtualenv bootstrap scripts. 2015-09-27 20:57:31 +00:00
Brandon Kreisel
756cfb75ef Merge remote-tracking branch 'upstream/master' 2015-09-27 14:58:34 -04:00
Brandon Kreisel
cbfdae88fc Add Mac compatibility to boulder-start
The version of sort that ships with OS X does not support the -V
version flag. Emulate that functionality with some sed-fu
2015-09-27 14:44:00 -04:00
Jakub Warmuz
3c08b512c3
Simple dev release script 2015-09-27 16:12:50 +00:00
Jakub Warmuz
1d5e1ee37e
Fix letshelp_letsencrypt MANIFEST 2015-09-27 11:10:39 +00:00
Jakub Warmuz
7d3a49b9e7
Add twine and wheel to [dev] 2015-09-27 11:10:39 +00:00
Jakub Warmuz
d621df3320
Make zipsafe 2015-09-27 11:10:34 +00:00
Jakub Warmuz
45a0cd2799
Fix include_package_data. 2015-09-27 08:11:40 +00:00
Peter Eckersley
ddc04c755b work in progress 2015-09-27 07:56:38 +00:00
Peter Eckersley
a29afcc3bd Merge remote-tracking branch 'origin/master' into subargs 2015-09-27 07:54:30 +00:00
Jakub Warmuz
3d638caeb7
Unify quotes in setup.py scripts 2015-09-27 06:32:27 +00:00
Jakub Warmuz
9883f8965d
Add dummy README.rst files 2015-09-27 06:23:13 +00:00
Jakub Warmuz
033ed589cc
Development Status :: 3 - Alpha 2015-09-27 06:23:13 +00:00
Jakub Warmuz
b6819ad05b
Add general classifiers to all setup.py scripts. 2015-09-27 06:23:13 +00:00
Jakub Warmuz
b5036e36ad
Unify setup.py: description/url/author/author_email. 2015-09-27 06:23:12 +00:00
Jakub Warmuz
20131de9fb
Add licences to all subpackages. 2015-09-27 06:23:12 +00:00
Jakub Warmuz
746016be6c
Sync version strings to 0.1.0.dev0, pin same repo deps. 2015-09-27 06:23:12 +00:00
Peter Eckersley
f3c2a096b5 Move the verb/subcommand to the end of the argparse line 2015-09-27 02:48:44 +00:00
Peter Eckersley
001d37f965 "-h" is also a ver. 2015-09-27 02:41:55 +00:00
James Kasten
523dba0b99 Merge pull request #777 from letsencrypt/fix_771
Make 'auth' and 'run' use the same code (fixes #771)
2015-09-26 22:12:46 -04:00
Peter Eckersley
e93593528d Merge branch 'master' of https://github.com/letsencrypt/letsencrypt 2015-09-27 02:09:48 +00:00
bmw
b008e69367 Merge pull request #835 from letsencrypt/fixes
Fix various sources of CLI breakage
2015-09-26 18:45:34 -07:00
Peter Eckersley
e7cbdc4f9a Revert reversion
Revert "Revert "ManualAuthenticator -> Authenticator""

(commit required a pip reinstall but was not inherently broken)

This reverts commit 6f1b1570b1.
2015-09-26 18:20:13 -07:00
Peter Eckersley
405bc99235 --help is effectively a verb for CLI purposes 2015-09-26 18:19:56 -07:00
Peter Eckersley
31fef196c0 --help is effectively a verb for CLI purposes... 2015-09-27 01:15:35 +00:00
Peter Eckersley
63e1c652e1 Undo damage from PEP8ification 2015-09-26 18:05:34 -07:00
Peter Eckersley
6f1b1570b1 Revert "ManualAuthenticator -> Authenticator"
This reverts commit 81f0a973a3.

This was breaking the client.  Not sure if/how it passed any tests?
2015-09-26 17:48:45 -07:00
James Kasten
8dc345a3a0 address naming conventions 2015-09-26 16:04:44 -07:00
James Kasten
655c3c2a0e Address comments 2015-09-26 15:44:57 -07:00
bmw
a21bb67ede Merge pull request #832 from kuba/setuptools
Explicit dependency on setuptools (pkg_resources).
2015-09-26 12:53:35 -07:00
bmw
5e32489644 Merge pull request #834 from kuba/agree-tos
agree-tos in dev-cli.ini
2015-09-26 12:44:17 -07:00
bmw
8e7e30a360 Merge pull request #833 from kuba/manual
ManualAuthenticator -> Authenticator
2015-09-26 12:42:10 -07:00
bmw
780f6bbbe7 Merge pull request #816 from letsencrypt/red_errors
Red errors
2015-09-26 12:38:50 -07:00
Brad Warren
d2a64166c6 Merge branch 'master' into red_errors 2015-09-26 12:18:41 -07:00
Brad Warren
2015811a6c Incorporated Kuba's feedback 2015-09-26 12:18:32 -07:00
Jakub Warmuz
5128a0345f
agree-tos in dev-cli.ini 2015-09-26 11:07:54 +00:00
Jakub Warmuz
d337865f48
add missing pkg_resources comment 2015-09-26 10:54:24 +00:00
Jakub Warmuz
08c0c4aeba
Explicit dependency on setuptools (pkg_resources). 2015-09-26 10:52:28 +00:00
Jakub Warmuz
81f0a973a3
ManualAuthenticator -> Authenticator 2015-09-26 09:07:08 +00:00
James Kasten
c74bc409d8 Merge pull request #824 from letsencrypt/remove_revoker
Remove revoker
2015-09-26 04:35:54 -04:00
James Kasten
98d49ae8bf Remove excessive error handling 2015-09-26 01:34:09 -07:00
James Kasten
e4771cf500 Merge pull request #814 from letsencrypt/crash_recovery
Crash recovery
2015-09-26 04:25:58 -04:00
James Kasten
84267c8f3a Merge branch 'master' into remove_revoker
Conflicts:
	letsencrypt/configuration.py
	letsencrypt/interfaces.py
	letsencrypt/tests/configuration_test.py
2015-09-26 01:19:28 -07:00
Jacob Hoffman-Andrews
47aa846bb4 Merge pull request #823 from letsencrypt/fix_817
rename certs directory to csr directory
2015-09-26 00:16:10 -07:00
James Kasten
514fc49e69 lower coverage due to removing revoker :( 2015-09-25 22:57:39 -07:00
James Kasten
f02653801d Remove revocation from client 2015-09-25 22:54:15 -07:00
James Kasten
c1a959de45 Remove Revocation display 2015-09-25 22:44:33 -07:00
James Kasten
022c5c3c24 Remove revoker and associated code 2015-09-25 22:35:43 -07:00
James Kasten
b72f451a1b rename certs directory to csr directory 2015-09-25 22:26:32 -07:00
James Kasten
8bc260dd64 Fix crypto_util tests 2015-09-25 21:45:56 -07:00
James Kasten
395da0d7d5 Merge branch 'master' into fix_771
Conflicts:
	letsencrypt/cli.py
2015-09-25 20:11:15 -07:00
James Kasten
add23360a5 Take away confirmation screen for testing 2015-09-25 20:04:34 -07:00
James Kasten
cfe103b4ed unify quotes 2015-09-25 20:01:12 -07:00
Brad Warren
817aadae6a Fixed indentation 2015-09-25 13:27:19 -07:00
Brad Warren
fe810020c4 Made error logging entries red in the terminal 2015-09-25 13:26:45 -07:00
Brad Warren
fd0c51e48a Incorporated Kuba's feedback and better defined corner cases 2015-09-24 16:23:40 -07:00
Brad Warren
31e9519ef5 Updated null installer interface 2015-09-23 15:11:10 -07:00
Brad Warren
2b9f72fc29 Finished basic crash recovery 2015-09-23 15:02:20 -07:00
James Kasten
5cc9061413 Merge pull request #780 from letsencrypt/integration
Make boulder-start.sh more robust & helpful
2015-09-23 14:18:06 -04:00
Brad Warren
aa216a96d4 Finished error_handler 2015-09-22 18:24:22 -07:00
bmw
2e130664f5 Merge pull request #805 from letsencrypt/fix_799
fix #799
2015-09-22 15:57:29 -07:00
bmw
5ee88f0902 Merge pull request #806 from letsencrypt/apache_include_quotes
Apache include quotes
2015-09-22 14:43:37 -07:00
Brad Warren
66f4d0961d Merge branch 'master' into crash_recovery 2015-09-22 14:22:23 -07:00
James Kasten
8780798f5e Merge pull request #800 from letsencrypt/manual_output
Remove Hardcoded Signature
2015-09-22 16:16:31 -04:00
Brad Warren
3a7fdbfc2f Merge branch 'master' into crash_recovery 2015-09-22 12:52:00 -07:00
James Kasten
d4d71a73a5 Remove trailing whitespace 2015-09-22 09:16:49 -07:00
James Kasten
e922a82277 letsencrypt-apache/ 2015-09-22 09:06:53 -07:00
James Kasten
202b21f260 Remove extra # 2015-09-22 08:58:02 -07:00
James Kasten
19d65c3e2f Add variable quote parsing 2015-09-22 08:56:26 -07:00
James Kasten
6e4faac9c0 Allow single/double quotes around Include dirs 2015-09-22 08:15:11 -07:00
James Kasten
f16e651794 Merge pull request #802 from diracdeltas/fix/osx-docker
Update Dockerfile-dev and instructions.
2015-09-21 21:41:34 -04:00
James Kasten
cf08fe799a fix #799 2015-09-21 16:57:28 -07:00
Peter Eckersley
92bd74a77b Merge pull request #797 from BKreisel/master
Mac OSX bootstrap: Add dialog dependency and homebrew installation
2015-09-21 16:56:16 -07:00
James Kasten
f1179ba323 Merge pull request #801 from letsencrypt/moar_cover
Increased letsencrypt and letsencrypt-nginx cover minimums
2015-09-21 19:33:15 -04:00
James Kasten
e144a88a27 Merge pull request #796 from kevinlondon/mktemp-removal
Replace mktemp with mkstemp
2015-09-21 18:57:20 -04:00
yan
5b080b6056 Update Dockerfile-dev and instructions. 2015-09-21 15:33:40 -07:00
Peter Eckersley
39c006f61e Merge pull request #786 from letsencrypt/errors
Show the final error message
2015-09-21 15:17:50 -07:00
Brad Warren
f482b5bd53 Removed unnecessary .challb 2015-09-21 15:08:11 -07:00
Brad Warren
65dd4c668c Increased letsencrypt and letsencrypt-nginx cover minimums 2015-09-21 14:55:12 -07:00
Brad Warren
8009993b52 Removed hardcoded signature 2015-09-21 14:50:00 -07:00
kevinlondon
d4fa0363e3 Removed the temp file again, not closing a closed file. 2015-09-21 09:25:27 -07:00
kevinlondon
ff9f12aea6 Use the file handle provided by mkstemp for opening the file. 2015-09-21 08:05:55 -07:00
Brandon Kreisel
fd53b09ab5 Remove homebrew existing message 2015-09-21 06:58:35 -04:00
Brandon Kreisel
f42699a786 Merge branch 'master' of https://github.com/BKreisel/letsencrypt 2015-09-20 19:10:44 -04:00
Brandon Kreisel
7f42beda95 Add homebrew and add dialog dependency per #413 2015-09-20 19:09:36 -04:00
Brandon Kreisel
4ffb74d7df Add dialog dependency and homebrew installation 2015-09-20 19:07:55 -04:00
kevinlondon
31af7b3a02 Replace mktemp with mkstemp 2015-09-20 14:53:45 -07:00
James Kasten
22260a82af Merge pull request #772 from letsencrypt/renewal_config_dir
Change the renewal configuration directory
2015-09-18 01:26:37 -04:00
Peter Eckersley
6c4d9e9324 Satisfy the lintmonster 2015-09-17 13:00:03 -07:00
Peter Eckersley
7c67df1076 traceback actually provides that \n 2015-09-17 12:48:07 -07:00
Peter Eckersley
0e3eae153e Hide tracebacks, but not the ultimate error itself 2015-09-17 12:29:42 -07:00
James Kasten
5e8f311190 Merge pull request #782 from letsencrypt/fix_781
fix #781
2015-09-17 14:17:17 -04:00
James Kasten
b5c8da2188 remove space 2015-09-17 02:20:15 -07:00
James Kasten
f2cee505f5 fix 781 2015-09-17 02:14:53 -07:00
Peter Eckersley
18adec0bf2 Fix paths in test cases 2015-09-16 19:43:57 -07:00
Peter Eckersley
b6461a1af5 Merge pull request #776 from kuba/726-cleanup
Address #726 review comments
2015-09-16 19:20:32 -07:00
Peter Eckersley
1296e55385 Merge pull request #778 from letsencrypt/permissions
Strict permission checking only upon request
2015-09-16 19:11:17 -07:00
Peter Eckersley
740f516561 Make boulder-start.sh more robust & helpful 2015-09-16 19:09:04 -07:00
James Kasten
edbd0a77b2 Rework config utility 2015-09-16 18:52:11 -07:00
James Kasten
67acebff34 pep8 and google style guide 2015-09-16 18:43:32 -07:00
Peter Eckersley
43a73f9a09 neaten
neaten
2015-09-16 17:26:27 -07:00
Peter Eckersley
110f080de0 The renewer also needs a config singleton 2015-09-16 17:15:10 -07:00
Peter Eckersley
630c715350 lintmonster 2015-09-16 17:03:09 -07:00
Peter Eckersley
d89b695be6 client and nginx configs are not the same thing... 2015-09-16 16:58:51 -07:00
Peter Eckersley
f450a290c3 Ensure test cases have a config singleton 2015-09-16 16:49:39 -07:00
Peter Eckersley
0325c6cde6 Make config singleton acquisition more robust
Fixing failures in testing environments
2015-09-16 15:56:06 -07:00
James Kasten
e8611d299a Cleanup formatting issues 2015-09-16 13:23:46 -07:00
Peter Eckersley
e570dac3c6 fix type error 2015-09-16 13:21:21 -07:00
Peter Eckersley
9315161ef2 Better documentation 2015-09-16 13:20:31 -07:00
Peter Eckersley
352d8cc49f Merge remote-tracking branch 'origin/master' into permissions 2015-09-16 13:19:33 -07:00
Peter Eckersley
1a2c983a9c Strict permission checking only upon request
Use --strict-permissions if you're running as a privileged user on a system
  where non-privileged users might have write permissions to parts of the lets
  encrypt config or logging heirarchy.  That should not normally be the case.

  Working toward a fix for #552
2015-09-16 13:13:24 -07:00
James Kasten
f582a85314 mock out make_or_verify 2015-09-16 13:03:42 -07:00
James Kasten
8b9a66d7dd Make sure configs directory exists 2015-09-16 12:33:56 -07:00
James Kasten
a0d67aeed7 correct success message for 'run' 2015-09-16 01:25:08 -07:00
Jakub Warmuz
03e2f043df
Address #726 review comments 2015-09-16 06:49:04 +00:00
James Kasten
23edd48d5a minor fixes 2015-09-15 23:34:00 -07:00
James Kasten
2bc87893c4 Merge branch 'master' into fix_771 2015-09-15 22:49:16 -07:00
James Kasten
c025c17b5d auth use renewal 2015-09-15 22:48:36 -07:00
Peter Eckersley
cb1ff5108e Merge remote-tracking branch 'origin/master' into renewal_config_dir 2015-09-15 19:05:59 -07:00
Peter Eckersley
d55c3e64c0 Merge pull request #773 from letsencrypt/rm-py26
Don't run tox for temporarily-disabled python versions
2015-09-15 19:05:04 -07:00
Peter Eckersley
2945e0657d Don't run tox for temporarily-disabled python versions 2015-09-15 19:01:55 -07:00
Peter Eckersley
1fff04ea9e Change the renewal configuration directory
Fixes #732
2015-09-15 18:51:24 -07:00
Peter Eckersley
5709eacec4 Merge remote-tracking branch 'origin/master' into kuba_chain 2015-09-15 18:33:16 -07:00
James Kasten
d9cf160aca Merge pull request #758 from letsencrypt/treat_duplicate_as_renewal
Treat duplicative certificate requests as renewals by default
2015-09-15 21:30:22 -04:00
Peter Eckersley
67bf3d41cb Merge remote-tracking branch 'origin/master' into kuba_chain 2015-09-15 18:26:39 -07:00
Peter Eckersley
3e59ed6939 Fix new call to save_successor 2015-09-15 18:25:49 -07:00
Peter Eckersley
e224a2ea83 Merge pull request #630 from letsencrypt/rm-py26
Remove Python 2.6 support.
2015-09-15 18:20:43 -07:00
James Kasten
d367694dc3 pep8 fixes 2015-09-15 18:09:00 -07:00
Peter Eckersley
c7b4bebd23 Merge remote-tracking branch 'origin/master' into rm-py26 2015-09-15 18:08:14 -07:00
Peter Eckersley
2611a7bce8 Merge pull request #726 from letsencrypt/doc
Document more dependencies for integration testing
2015-09-15 18:04:56 -07:00
James Kasten
1d10bef7d3 Merge branch 'treat_duplicate_as_renewal' of github.com:letsencrypt/letsencrypt into treat_duplicate_as_renewal 2015-09-15 17:58:45 -07:00
James Kasten
0b8009529b Basic removal of duplicate code through using a base class 2015-09-15 17:58:31 -07:00
Peter Eckersley
bde5a1fe17 Merge branch 'treat_duplicate_as_renewal' into kuba_chain 2015-09-15 17:51:58 -07:00
Peter Eckersley
83c77544a3 Merge pull request #733 from letsencrypt/cli
Make "run" the implicit default
2015-09-15 17:45:06 -07:00
Peter Eckersley
d910fe8655 Merge remote-tracking branch 'origin/master' into cli2 2015-09-15 17:35:36 -07:00
Seth Schoen
f160a51aa7 Don't crash if an existing lineage is slightly corrupt 2015-09-15 17:27:27 -07:00
Peter Eckersley
11c6ef32a8 Remove stray debugging printf 2015-09-15 17:11:20 -07:00
Seth Schoen
c170a7a16b Merge branch 'master' of ssh://github.com/letsencrypt/lets-encrypt-preview into treat_duplicate_as_renewal 2015-09-15 12:57:51 -07:00
bmw
a150828a79 Merge pull request #753 from kuba/dns-v4
acme: v4 DNS challenge
2015-09-14 16:21:38 -04:00
Seth Schoen
a6426867d2 Merge branch 'master' into treat_duplicate_as_renewal 2015-09-14 10:32:03 -07:00
Seth Schoen
d66e65af11 Remove short -D for --duplicate 2015-09-14 10:31:43 -07:00
James Kasten
3c79845b16 Merge pull request #769 from rutsky/patch-2
fix path to script with nginx prerequisites
2015-09-13 17:15:35 -04:00
James Kasten
5d7fcaa50c Merge pull request #768 from rutsky/patch-1
fix typo: "Python'd" -> "Python's"
2015-09-13 17:15:12 -04:00
Vladimir Rutsky
d3cb4746e9 fix path to script with nginx prerequisites
The path is copied from `.. include` directive below.
2015-09-13 09:53:53 +03:00
Vladimir Rutsky
06d87cb56c fix typo: "Python'd" -> "Python's" 2015-09-13 09:47:56 +03:00
Seth Schoen
a38bb41856 PR cleanup 2015-09-11 13:49:26 -07:00
Seth Schoen
791825151a Merge branch 'master' of ssh://github.com/letsencrypt/lets-encrypt-preview into treat_duplicate_as_renewal 2015-09-11 00:42:35 -07:00
James Kasten
b866c2a816 Merge pull request #752 from kuba/lint
pep8 love
2015-09-11 03:37:28 -04:00
Jakub Warmuz
33c2aed021
Merge remote-tracking branch 'github/letsencrypt/master' into lint 2015-09-11 07:15:10 +00:00
James Kasten
00a5214a29 Merge pull request #766 from letsencrypt/fix_765
abspath storage.version (#765)
2015-09-11 03:13:35 -04:00
Jakub Warmuz
0ebef62846
Travis: no fail on pep8 2015-09-11 07:12:49 +00:00
Jakub Warmuz
809f4966d6
Require pep8 in [testing] 2015-09-11 07:04:13 +00:00
James Kasten
1a5f0c434e remove source of abspath problem... not side-effect 2015-09-11 00:02:09 -07:00
James Kasten
63b0d62f7b fix #765 2015-09-10 23:46:02 -07:00
bmw
e3b48051ea Merge pull request #763 from letsencrypt/module_deps_apache
Module deps apache
2015-09-10 23:11:56 -04:00
Brad Warren
1bb62eed4d Started crash recovery mechanism 2015-09-10 22:35:44 -04:00
James Kasten
5bcd1b6680 Merge pull request #760 from kuba/revocation
Revocation: simple implementation with integration tests
2015-09-10 22:00:20 -04:00
James Kasten
4fb27e0350 fix documentation link 2015-09-10 18:48:44 -07:00
James Kasten
c540b9a25a Merge pull request #759 from kuba/acme-directory
ACME Directory Resource
2015-09-10 21:26:27 -04:00
James Kasten
d2595b4f40 Merge pull request #713 from kuba/bugs/679
SimpleHTTP manual plugin: v04 provisioned resource contents (fixes #679)
2015-09-10 20:33:40 -04:00
James Kasten
7a66bfef28 method to func, thanks pylint 2015-09-10 16:49:50 -07:00
James Kasten
9c47b1061c Search for correct module names in dependency list 2015-09-10 16:34:20 -07:00
James Kasten
b2ef041785 Don't log notes if save is temporary 2015-09-10 15:57:27 -07:00
James Kasten
8b093032ae Change debug/info output 2015-09-10 15:39:13 -07:00
James Kasten
c03f497727 Add dependencies for known used modules 2015-09-10 15:28:01 -07:00
Seth Schoen
f09016321b Fix logic error if requesting nonduplicative cert 2015-09-10 15:12:36 -07:00
Seth Schoen
7b5b182f77 Test for _find_duplicative_certs 2015-09-10 14:59:29 -07:00
Seth Schoen
ff85bd30b7 Disable duplicate-code pylint check 2015-09-10 14:59:10 -07:00
Jakub Warmuz
491b7a7cde
Fix multi-cert chains in renewer 2015-09-10 21:48:34 +00:00
Seth Schoen
7c2a87a51d Remove explicit .namespace for easier testing 2015-09-10 14:45:30 -07:00
Jakub Warmuz
62a9556bd2
Add unittest for save_certificate 2015-09-10 21:20:48 +00:00
Jakub Warmuz
b3ade6abe4
Revert "Revocation: expect application/json (boulder#771)."
This reverts commit 39aff967a5.
2015-09-10 20:43:20 +00:00
Jakub Warmuz
0275271ecd
Multi cert chains (fixes #633). 2015-09-10 20:28:22 +00:00
Jakub Warmuz
39aff967a5
Revocation: expect application/json (boulder#771). 2015-09-10 20:17:13 +00:00
Jakub Warmuz
cc607480ae
acme: fetch_chain for multiple up links 2015-09-10 20:12:32 +00:00
Seth Schoen
9524d98411 Merge branch 'master' of ssh://github.com/letsencrypt/lets-encrypt-preview into treat_duplicate_as_renewal 2015-09-09 15:10:55 -07:00
James Kasten
eb2173ca12 Merge pull request #761 from letsencrypt/travis_check_for_756
Travis check for 756
2015-09-09 18:06:34 -04:00
James Kasten
05d565c304 Merge branch 'kuba-bugs/755' 2015-09-09 14:51:34 -07:00
James Kasten
b7738d98d1 Merge branch 'bugs/755' of git://github.com/kuba/letsencrypt into kuba-bugs/755 2015-09-09 14:51:13 -07:00
Jakub Warmuz
67d6b89382
Fix paths in integration testing 2015-09-09 20:54:11 +00:00
Jakub Warmuz
94fa851b01
Merge branch 'acme-directory' into revocation 2015-09-09 20:45:57 +00:00
Jakub Warmuz
ed051b7c28
Boulder Monolithic does not exist 2015-09-09 20:40:04 +00:00
Jakub Warmuz
c93564b99e
Travis: update --server to point at directory 2015-09-09 20:22:38 +00:00
Jakub Warmuz
817ab468d1
py3 compat: str(exc) instead of exc.message 2015-09-09 20:21:33 +00:00
Jakub Warmuz
302e3ceb7d
Revocation: integration testable 2015-09-09 20:04:28 +00:00
Jakub Warmuz
bf754b6302
Add ACME Directory Resource 2015-09-09 17:22:20 +00:00
Seth Schoen
32f0f9d225 Merge branch 'master' of ssh://github.com/letsencrypt/lets-encrypt-preview into treat_duplicate_as_renewal 2015-09-09 00:12:18 -07:00
Seth Schoen
244dc30306 Fewer locals (lint would still complain) 2015-09-09 00:11:44 -07:00
James Kasten
44f7703f00 Merge pull request #754 from kuba/cover
Easier coverage testing for subpackages.
2015-09-09 00:02:31 -04:00
Seth Schoen
2006c3a067 Merge branch 'master' of ssh://github.com/letsencrypt/lets-encrypt-preview into treat_duplicate_as_renewal 2015-09-08 21:02:06 -07:00
Seth Schoen
3cc15c6193 Cleanup 2015-09-08 21:01:53 -07:00
James Kasten
d7b1af2a31 Merge pull request #750 from hlieberman/fix-spelling
Fix minor spelling errors in the code.
2015-09-09 00:00:03 -04:00
James Kasten
0a4c91e018 Merge pull request #747 from kuba/rpm-dnf
rpm bootstrap: yum and dnf support
2015-09-08 23:59:07 -04:00
James Kasten
01b1172df2 Merge pull request #751 from hlieberman/urllib3-injection
Make urllib3 injection more version specific.
2015-09-08 23:55:42 -04:00
James Kasten
b0085cd47b Merge pull request #704 from kuba/py3
Bring back Python 3 support for acme
2015-09-08 23:54:23 -04:00
James Kasten
83309170c7 Merge pull request #743 from kuba/setup.py
setup.py: update/fix deps.
2015-09-08 22:02:35 -04:00
James Kasten
2a41c6b27c Merge pull request #748 from kuba/freebsd
Add basic setup for FreeBSD
2015-09-08 16:17:01 -04:00
James Kasten
10de13e261 Merge pull request #746 from kuba/containers
Travis containers (fixes #617)
2015-09-08 16:00:59 -04:00
Seth Schoen
df42cca26e More useful explanation of --duplicate 2015-09-08 08:58:43 -07:00
Seth Schoen
b375b9c074 Fix indentation 2015-09-08 08:48:46 -07:00
Seth Schoen
7dda21817a Add --duplicate command-line option 2015-09-08 01:50:29 -07:00
Seth Schoen
7aa9fe845a Basic fix for #411 2015-09-08 01:33:03 -07:00
Jakub Warmuz
892b918dad
fix "centos.sh -> freebsd.sh" typo 2015-09-07 05:32:51 +00:00
James Kasten
aed29760a8 Merge pull request #714 from kuba/go-1.5
Travis: remove unused "go: 1.5" stmt
2015-09-06 14:16:43 -04:00
James Kasten
5780ecf704 Merge pull request #744 from sebix/doc-sudo
Minor: use sudo for auth command in using.rst
2015-09-06 14:15:37 -04:00
Harlan Lieberman-Berg
10460eb285 Add no cover pragma, URL for documentation. 2015-09-06 13:58:50 -04:00
Jakub Warmuz
1c27c7ed54
lint: fix no-self-use 2015-09-06 13:00:53 +00:00
Jakub Warmuz
41c08416cd
Cast port to int 2015-09-06 12:54:13 +00:00
Jakub Warmuz
d6e95b4617
Manual plugin test mode busy wait (fixes #755). 2015-09-06 12:47:30 +00:00
Jakub Warmuz
71e665d4cd
Easier coverage testing for subpackages.
You can now call "./tox.cover.sh acme", "./tox.cover acme letsencrypt"
etc. to scope down coverage testing to particular
subpackages. "./tox.cover.sh" checks coverage for all packages.
2015-09-06 12:12:02 +00:00
Jakub Warmuz
dbf5d086bd
v4 DNS challenge 2015-09-06 11:47:56 +00:00
Jakub Warmuz
6fab6c80b6
nit: fix missing EOF newline 2015-09-06 09:27:39 +00:00
Jakub Warmuz
fe3e8d7302
Travis: add pep8 checks 2015-09-06 09:22:34 +00:00
Jakub Warmuz
79a70cfd61
pep8 letshelp-letsencrypt 2015-09-06 09:21:57 +00:00
Jakub Warmuz
413bd6f425
pep8 letsencrypt-compatibility-test 2015-09-06 09:21:28 +00:00
Jakub Warmuz
d8c55f3da3
pep8 letsencrypt-nginx 2015-09-06 09:21:03 +00:00
Jakub Warmuz
95c8edc66c
pep8 letsencrypt-apache 2015-09-06 09:20:41 +00:00
Jakub Warmuz
83185e5553
pep8 letsencrypt 2015-09-06 09:20:11 +00:00
Jakub Warmuz
89c99a1f34
pep8 acme 2015-09-06 09:19:26 +00:00
Jakub Warmuz
138f1d1b28
lint: space check for dict-separator 2015-09-06 08:30:49 +00:00
Harlan Lieberman-Berg
503afebd54 Make urllib3 injection more version specific. 2015-09-05 22:47:25 -04:00
Harlan Lieberman-Berg
dc4cc23377 Fix minor spelling errors in the code. 2015-09-05 22:35:34 -04:00
Jakub Warmuz
86bfe61ea3
Travis: add rsyslog 2015-09-05 21:50:14 +00:00
Jakub Warmuz
75304ab6d1
Add basic setup for FreeBSD 2015-09-05 19:03:01 +00:00
Jakub Warmuz
0978441392
fix indent 2015-09-05 18:28:27 +00:00
Jakub Warmuz
eace5d1161
shell: add missing "fi" 2015-09-05 18:04:57 +00:00
Jakub Warmuz
84d9c773a2
#673 review comments 2015-09-05 17:38:11 +00:00
Jakub Warmuz
f5c9f92c42
Travis: addons.(apt.)packages 2015-09-05 17:23:38 +00:00
Jakub Warmuz
1c04abfe94
Travis: no sudo, install nginx and openssl. 2015-09-05 17:20:26 +00:00
Jakub Warmuz
77137f7716
Travis containers (fixes #617) 2015-09-05 17:17:25 +00:00
Sebastian Wagner
07bd9e689b
docs/using use sudo for auth command
Signed-off-by: Sebastian Wagner <sebix@sebix.at>
2015-09-02 22:11:13 +02:00
Jakub Warmuz
8163e055a1
Disable test_probe_connection_error (problems with Python 3). 2015-09-02 18:50:59 +00:00
Jakub Warmuz
c6e4c7dea1
setup.py: update/fix deps. 2015-09-01 19:57:41 +00:00
Peter Eckersley
f09c8dd740 Satisfy the pylint demon 2015-08-28 18:02:54 -07:00
Peter Eckersley
44d7ac2dce Plumbing for tweaked args
- to make "run" the default, we need to add it to the args
 - which means we need to pass that back up to the actual argparse call

(This is ugly... probably HelpfulArgParser needs to actuall inherit from
argparse...)
2015-08-28 18:02:49 -07:00
Peter Eckersley
7325a3f28d Make the "run" command the default 2015-08-28 18:02:44 -07:00
Peter Eckersley
fd3170e2e8 Disable another silly pylint warning 2015-08-28 18:02:38 -07:00
Peter Eckersley
6604a0ffaa Prevent pylint from complaining about some silly things 2015-08-28 18:02:32 -07:00
schoen
c3941b1a8d Merge pull request #723 from rugk/patch-1
Readme: Added community link
2015-08-28 11:58:06 -07:00
Peter Eckersley
ea9e4d5cd7 Document more dependencies for integration testing 2015-08-28 09:57:30 -07:00
rugk
b0c78ab483 Readme: Added community link
https://community.letsencrypt.org/t/le-github-repo-link-to-forums/535
2015-08-28 16:36:09 +02:00
Jakub Warmuz
a74eff5fbd
Revert "Revert PR #708."
This reverts commit 70e311b43f.
2015-08-28 06:40:19 +00:00
bmw
05d5d4ad96 Merge pull request #721 from letsencrypt/revert-708
Revert PR #708.
2015-08-27 16:56:58 -04:00
Jacob Hoffman-Andrews
70e311b43f Revert PR #708.
https://github.com/letsencrypt/letsencrypt/pull/708 broke the Boulder CI build
because Travis runs Ubuntu 12.04, which has an older setuptools. See
https://github.com/letsencrypt/boulder/issues/681.
2015-08-27 13:38:32 -07:00
James Kasten
89c94ccfbb Merge pull request #720 from letsencrypt/boulder-start
Installs goose for boulder's create_db.sh
2015-08-27 14:44:21 -04:00
James Kasten
091af07c1c Merge pull request #706 from kuba/uri-shift
Shift URIs around (acme-spec#134, fixes #576).
2015-08-27 13:53:50 -04:00
bmw
ed509d15b2 Merge pull request #708 from kuba/mock-2.6
mock<1.1.0 only for py2.6.
2015-08-27 12:22:16 -04:00
Brad Warren
3fed72f9ce Installs goose for boulder's create_db.sh 2015-08-27 11:22:38 -04:00
bmw
c45901e686 Merge pull request #710 from kuba/boulder-start-fixes
boulder-start.sh fixes
2015-08-27 11:15:32 -04:00
bmw
ff24b3614a Merge pull request #697 from sapics/fix-link
Update README.rst
2015-08-27 10:09:33 -04:00
Jakub Warmuz
fc4990cf16
Fix typo 2015-08-26 21:05:08 +00:00
James Kasten
d8e2ee0758 Merge pull request #705 from kuba/manpages
Add basic manpages for all binaries (fixes #700).
2015-08-26 13:37:00 -04:00
Jakub Warmuz
ad2b589d19
Travis: remove unused "go: 1.5" stmt 2015-08-25 18:43:27 +00:00
Jakub Warmuz
3b73b04bfe
SimpleHTTP manual plugin: v04 provisioned resource contents (fixes #679). 2015-08-25 17:57:21 +00:00
Jakub Warmuz
9a618ccda4
Merge branch 'master' into boulder-start-fixes 2015-08-25 06:37:09 +00:00
Jakub Warmuz
217b40379f
Merge branch 'master' into mock-2.6 2015-08-25 06:36:56 +00:00
Jakub Warmuz
5154cc92d6
Merge branch 'master' into uri-shift 2015-08-25 06:36:42 +00:00
Jakub Warmuz
001c84a0a0
Merge branch 'master' into manpages 2015-08-25 06:36:15 +00:00
Jakub Warmuz
7f6d4c5f84
Merge branch 'master' into py3 2015-08-25 06:35:35 +00:00
James Kasten
48467a1b38 Merge pull request #707 from kuba/log-http-req-params
Log HTTP request params (including contents).
2015-08-24 17:10:00 -04:00
James Kasten
8a6dfb1516 Merge pull request #703 from kuba/docs
Fix various Sphinx warnings, errors
2015-08-24 17:08:53 -04:00
James Kasten
ed3abc79c9 Merge pull request #702 from solidgoldbomb/fix-comment-text
examples: fix typos in generate-csr.sh script comments
2015-08-24 16:45:35 -04:00
James Kasten
a6bc53bfa9 Merge pull request #709 from kuba/go-1.5
Use Go 1.5.
2015-08-24 16:43:09 -04:00
Jakub Warmuz
504ade8af5
Travis: gimme 1.5 only for integration tests 2015-08-24 20:29:25 +00:00
Jakub Warmuz
165082b37b
Travis: use gimme for Go 1.5 2015-08-24 20:26:04 +00:00
Jakub Warmuz
529df611b2
Travis: different syntax for Go 1.5 version 2015-08-24 20:09:19 +00:00
Jakub Warmuz
b2b042837f
Use Go 1.5.
The following PRs broke our unittests:

- https://github.com/letsencrypt/boulder/pull/642
- https://github.com/letsencrypt/boulder/pull/635
2015-08-23 21:10:56 +00:00
Stacey Sheldon
7073f947bd examples: fix typos in generate-csr.sh script comments 2015-08-23 10:24:51 -04:00
James Kasten
04c2672de9 Merge pull request #699 from hlieberman/feature-fix-misspelling
Fix minor misspelling error.
2015-08-22 14:56:11 -04:00
Jakub Warmuz
089528ed2a
Fix ambiguous cross-references in docs 2015-08-22 14:37:32 +00:00
Jakub Warmuz
e9c79edb19
Fix various Sphinx build problems 2015-08-22 14:30:59 +00:00
Jakub Warmuz
4b128c69a2
Fix docs include (SEVERE) 2015-08-22 14:30:30 +00:00
Jakub Warmuz
2e90ecf1b0
Add basic manpages for all binaries (fixes #700). 2015-08-22 14:12:16 +00:00
Jakub Warmuz
60aa1b2ecb
Log HTTP request params (including contents). 2015-08-22 12:36:52 +00:00
Jakub Warmuz
79853fa098
Shift URIs around (acme-spec#134, fixes #576). 2015-08-22 12:20:38 +00:00
Jakub Warmuz
a7df468347
mock<1.1.0 only for py2.6. 2015-08-22 11:01:28 +00:00
Jakub Warmuz
667ee132f4
boulder-start.sh: revert -e (fail faast) 2015-08-22 09:22:35 +00:00
Jakub Warmuz
cdb9c8b964
boulder-start.sh: go get with ... 2015-08-22 09:22:35 +00:00
Harlan Lieberman-Berg
bc530e457e Fix minor misspelling error. 2015-08-22 02:51:24 +02:00
Jakub Warmuz
4d30ec07fb
Update test name to match acme v04 semantics. 2015-08-19 20:38:03 +00:00
Jakub Warmuz
504b290726
Fix py3 compat in acme. 2015-08-19 20:35:30 +00:00
Jakub Warmuz
0ec447f418
Revert "Remove Python 3 Travis checks"
This reverts commit 05ee92f8cd.
2015-08-19 20:26:35 +00:00
Jakub Warmuz
4d9db06083
Revert "Removed py3+ tests in tox"
This reverts commit 2c720b05ae.
2015-08-19 20:24:44 +00:00
sapics
8e4d1b1473 Update README.rst 2015-08-18 09:53:57 +09:00
James Kasten
26c1f003d0 Merge pull request #696 from kuba/bugs/689
Normalize plugins.common.dest_namespace (fixes #689).
2015-08-17 16:41:35 -04:00
James Kasten
0a9437713a Merge pull request #693 from letsencrypt/fix_cleanup_enable_mod
Fix enable_mod for "run" command
2015-08-17 16:41:06 -04:00
James Kasten
acc0f960e6 Merge pull request #692 from letsencrypt/update-contr-docs
update docs to include letshelp-letsencrypt
2015-08-17 16:32:46 -04:00
James Kasten
6aabb3839f address comments 2015-08-17 13:27:15 -07:00
James Kasten
8bb02b80c1 fix typo 2015-08-17 13:12:15 -07:00
James Kasten
ea7a427cca revert to simple enable_mod, disable_mod, enable_mod 2015-08-17 13:11:48 -07:00
Jakub Warmuz
b6eafe63b5
Normalize plugins.common.dest_namespace (fixes #689). 2015-08-17 19:50:36 +00:00
James Kasten
d8663fbbc0 Fix #690 2015-08-17 11:30:39 -07:00
James Kasten
5ba6446458 Add additional test for prepare_server_https 2015-08-17 11:07:54 -07:00
James Kasten
4a5f6a5248 Merge branch 'update-contr-docs' into fix_cleanup_enable_mod 2015-08-17 10:53:24 -07:00
James Kasten
c1f9d66a09 update docs to include letshelp-letsencrypt 2015-08-17 10:51:44 -07:00
James Kasten
dde4951be5 fix 691 2015-08-17 10:40:42 -07:00
James Kasten
62ce3e2fc2 Merge pull request #686 from letsencrypt/which_python
Dynamically choose python binary
2015-08-16 17:01:51 -04:00
Brad Warren
a78a1e70b0 Y u fail? 2015-08-14 17:31:31 -07:00
Brad Warren
de1e84ed93 Dynamically choose python binary 2015-08-14 17:04:42 -07:00
bmw
dd4b8e87cb Merge pull request #685 from MalteJ/patch-1
Typo in CLI help
2015-08-14 09:53:14 -07:00
bmw
c42ca92fd9 Merge pull request #682 from letsencrypt/fix_chall_prep_https_apache
Fix enable_mod register_undo_command in challenges
2015-08-14 09:33:30 -07:00
Malte Janduda
8c2afd4e7b Typo in CLI help 2015-08-14 13:45:57 +02:00
Patrick Heppler
aa0407b39f Update _rpm_common.sh 2015-08-14 12:20:03 +02:00
James Kasten
9abb56cb6e Merge pull request #683 from letsencrypt/letshelp-apache
Letshelp apache
2015-08-14 00:34:32 -04:00
James Kasten
c7aebfa26d Merge branch 'letshelp-apache' of git://github.com/bmw/lets-encrypt-preview into bmw-letshelp-apache
Conflicts:
	tox.ini
2015-08-13 21:24:26 -07:00
James Kasten
e30193b623 Fix enable_mod register_undo_command in challenges 2015-08-13 21:14:46 -07:00
James Kasten
a314b40101 Merge pull request #628 from bmw/plugin_tests
Plugin tests
2015-08-13 23:53:33 -04:00
Brad Warren
7cb30ca838 Merge remote-tracking branch 'upstream/master' into letshelp-apache 2015-08-13 19:04:01 -07:00
Brad Warren
8f1162ba7e Removed stray debugging statement 2015-08-13 18:44:28 -07:00
Brad Warren
0aaf9f2be7 Updated tests to update challenge changes 2015-08-13 18:41:49 -07:00
James Kasten
5898bd41e2 Merge pull request #647 from diracdeltas/feature/docker-dev
Add development Dockerfile and instructions
2015-08-13 17:04:29 -04:00
schoen
0db19f0cda Merge pull request #678 from letsencrypt/urandom
Fixes tests
2015-08-13 13:57:25 -07:00
Brad Warren
a97c4caa56 Make travis happy 2015-08-13 13:44:51 -07:00
Brad Warren
bda953a2cb Removed conditional around create_db 2015-08-13 12:31:13 -07:00
Brad Warren
f39e6c672d Create databases in travis 2015-08-13 12:24:34 -07:00
Brad Warren
2b33dd42e8 Specify mariadb version before mysql service? 2015-08-13 12:00:26 -07:00
Brad Warren
baadccb746 Added mysql to travis 2015-08-13 11:15:43 -07:00
Brad Warren
db99970b5a Allowed for arbitrary length strings from urandom 2015-08-12 20:22:03 -07:00
James Kasten
471d168665 Merge pull request #672 from frennkie/master
Minor enhancements (fix typo in docs, remove unneeded code for better cov)
2015-08-12 13:32:42 -04:00
James Kasten
4cc5174524 Merge pull request #674 from letsencrypt/account_key
Changes account in achalls to account_key
2015-08-12 13:21:47 -04:00
James Kasten
970f20bacf Merge pull request #676 from vcavallo/patch-1
Fix typo in command code block in contributing.rst
2015-08-12 12:17:15 -04:00
Vinney
637bc840ac Fix typo in command code block in contributing.rst 2015-08-12 12:12:00 -04:00
schoen
e268557258 Merge pull request #675 from letsencrypt/remove_py3_tests
Removed py3+ tests in tox
2015-08-11 13:47:51 -07:00
Brad Warren
2c720b05ae Removed py3+ tests in tox 2015-08-11 13:37:49 -07:00
Brad Warren
1d7f252b35 Changes account in achalls to account_key 2015-08-11 13:22:03 -07:00
Patrick Heppler
93f43db654 Update _rpm_common.sh
Added switch to use either yum or dnf (fedora 22)
2015-08-10 13:53:29 +02:00
Robert Habermann
6a1cf1b754 fix typo in tox.cover.sh script name 2015-08-09 11:11:23 +02:00
Robert Habermann
95cc031339 remove unneccessary check of len on match_parts
While getting the best match the function _exact_match is called with
the candidate server names as name. Then name.split('.') is called which
can never have a length of 0.
2015-08-09 11:02:21 +02:00
Jacob Hoffman-Andrews
74b677248d Merge pull request #653 from letsencrypt/update-challenges
Update challenges to match latest spec
2015-08-07 15:37:17 -07:00
Jacob Hoffman-Andrews
83dacedd5b Merge pull request #654 from letsencrypt/rm-py3
Remove Python 3 Travis checks
2015-08-06 12:35:38 -07:00
Seth Schoen
05ee92f8cd Remove Python 3 Travis checks 2015-08-06 12:07:00 -07:00
Jacob Hoffman-Andrews
35f81aeb6e Restore py26 toxenv for manual tests. 2015-08-06 10:31:57 -07:00
Jacob Hoffman-Andrews
d235ebf381 Remove tests for Python 3+ also. 2015-08-06 10:30:43 -07:00
Jacob Hoffman-Andrews
e019f36f15 Merge branch 'master' of github.com:letsencrypt/letsencrypt into rm-py26 2015-08-06 10:30:19 -07:00
Jacob Hoffman-Andrews
8a01e094c5 Increase server_names_hash_bucket_size in nginx test config. 2015-08-05 19:26:58 -07:00
Brad Warren
283ccbfc45 Removed -e 2015-08-05 17:28:08 -07:00
Brad Warren
ffd0d6d148 Removed go get 2015-08-05 17:13:10 -07:00
Brad Warren
62ea10cd6c Fixed travis issues? 2015-08-05 16:38:09 -07:00
Brad Warren
8263baecba Merge remote-tracking branch 'upstream/master' into update-challenges 2015-08-05 15:42:08 -07:00
Brad Warren
14c150ae17 Fixed unit tests and lint 2015-08-05 15:39:31 -07:00
James Kasten
1e3c7aa746 Merge pull request #651 from letsencrypt/update_readme
Update README
2015-08-05 12:04:34 -07:00
James Kasten
4cc4d7467a update headers 2015-08-05 10:52:31 -07:00
James Kasten
9a334f3632 add space 2015-08-05 10:48:43 -07:00
James Kasten
ce14e0bea6 Move disclaimer up 2015-08-05 10:47:19 -07:00
James Kasten
a7b52899a6 Clarify Installation, Clarify status of Let's Encrypt 2015-08-05 10:43:48 -07:00
James Kasten
c17837d70c Merge branch 'patch-2' of git://github.com/coolaj86/lets-encrypt-preview into coolaj86-patch-2 2015-08-05 10:35:35 -07:00
Brad Warren
cfabfa1a67 Merge remote-tracking branch 'kuba/update-challenges' into update-challenges 2015-08-04 16:57:53 -07:00
James Kasten
264a7274ad Merge pull request #650 from letsencrypt/renewer_errors
Renewer errors
2015-08-04 15:50:09 -07:00
James Kasten
3088a18f05 finish renewer error handling 2015-08-04 14:54:19 -07:00
James Kasten
3490935733 Generic ValueError -> CertStorageError 2015-08-04 14:45:03 -07:00
Brad Warren
499f802d7c Fixed merge conflict 2015-08-04 12:50:43 -07:00
Brad Warren
2605d5297c Added check for pkcs format 2015-08-04 12:24:50 -07:00
Brad Warren
fb79245773 Added a2dismod support 2015-08-04 12:18:10 -07:00
Brad Warren
0252272430 Fixed directory compare 2015-08-03 19:23:01 -07:00
Brad Warren
5ae2bd06cf Merged in master 2015-08-03 18:32:32 -07:00
yan
b38500fad8 Add development Dockerfile and instructions 2015-08-03 16:26:22 -07:00
Brad Warren
1d7a70f356 No supported enhancements counts as an error 2015-08-03 11:56:17 -07:00
Brad Warren
428b89e0cf Started incorporating James' feedback 2015-08-03 11:38:22 -07:00
James Kasten
9139531af0 Merge pull request #631 from letsencrypt/apache_modules
Apache Overhaul
2015-07-31 16:02:03 -07:00
Jakub Warmuz
f96f059288
Remove nonce_domain remaints from core. 2015-07-31 22:54:25 +00:00
James Kasten
0c3d8a51eb Merge branch 'master' into apache_modules 2015-07-31 15:49:13 -07:00
Jakub Warmuz
eacf658003
py3 compat 2015-07-31 22:45:48 +00:00
James Kasten
1587653366 Final cleanup 2015-07-31 15:38:43 -07:00
Jakub Warmuz
68d34391dd
Fix test_good_token 2015-07-31 22:15:56 +00:00
Jakub Warmuz
bac5a564db
Passing core/Boulder@370d296 integration testing. 2015-07-31 21:31:58 +00:00
Jakub Warmuz
57110f4c18
acme: simplehttp v04 2015-07-31 21:30:08 +00:00
Jakub Warmuz
ceed8a71c1
DeserializationError: more meaningful message 2015-07-31 21:19:07 +00:00
James Kasten
8ba87b8c18 Merge pull request #606 from quinox/issue_518
Consume longest possible match
2015-07-31 13:53:23 -07:00
James Kasten
2953d3b2ee Associate vhost with name after selection 2015-07-31 13:23:31 -07:00
James Kasten
65f4332798 Update IPlugin docs, make augeas conform 2015-07-31 12:49:39 -07:00
James Kasten
33111e1ce6 Added additional errors to IPlugin.prepare interface 2015-07-31 10:44:21 -07:00
James Kasten
06a014de09 Update documentation, reduce spurious logging 2015-07-31 10:29:04 -07:00
James Kasten
776e347382 Merge branch 'master' into apache_modules 2015-07-31 10:14:43 -07:00
James Kasten
df6707209f Merge pull request #642 from letsencrypt/dol-patch-1
Dol patch 1
2015-07-31 10:06:27 -07:00
James Kasten
92dbc4f4eb Finish merge of #17 2015-07-31 09:55:02 -07:00
James Kasten
4aaa9a3240 Merge branch 'patch-1' of git://github.com/dol/lets-encrypt-preview into dol-patch-1
Conflicts:
	letsencrypt/client/CONFIG.py
2015-07-31 09:53:50 -07:00
James Kasten
f71119681c Address most of first round of comments 2015-07-30 23:14:58 -07:00
Brad Warren
5f40718fb6 Merge remote-tracking branch 'upstream/master' into letshelp-apache 2015-07-30 07:25:51 -07:00
Brad Warren
46d8b163be Added include_package_data=True 2015-07-30 07:25:41 -07:00
James Kasten
b37fc95386 py26 compat 2015-07-30 01:49:04 -07:00
James Kasten
47be104e2b Update pick_plugin tests based on misconfigured single plugin 2015-07-30 01:37:05 -07:00
James Kasten
67be424779 Merge branch 'master' into apache_modules 2015-07-30 01:26:08 -07:00
James Kasten
3bcf29be51 Change default rollback to 1 2015-07-30 01:22:59 -07:00
James Kasten
e64e3ceab0 Proper misconfiguration message 2015-07-30 01:19:02 -07:00
James Kasten
1570873312 Fix single candidate plugin misconfiguration bug 2015-07-30 00:29:23 -07:00
James Kasten
7390a39a4d edit spacing 2015-07-29 23:49:02 -07:00
James Kasten
ae4e1d5058 Use a2enmod and update reverter 2015-07-29 23:40:07 -07:00
James Kasten
c0f8c90f93 Merge pull request #638 from kuba/boulder-start-no-arg
"boulder-start.sh amqp" is the default as of #637
2015-07-29 22:15:23 -07:00
Jakub Warmuz
84fe80a347
"boulder-start.py amqp" is the default as of #637 2015-07-30 04:13:14 +00:00
James Kasten
c29c7a0b64 Merge pull request #637 from letsencrypt/integration-path
Fix path for integration test.
2015-07-29 17:05:38 -07:00
Jacob Hoffman-Andrews
06e21d3578 Fix path for integration test. 2015-07-29 15:53:09 -07:00
Jeff Hodges
1bbc3d38f8 Merge pull request #636 from letsencrypt/mktemp
make mktemp in integration tests work on OS X
2015-07-29 15:27:17 -07:00
Jeff Hodges
584f19fef5 add comment for mktemp for @Kuba 2015-07-29 15:08:22 -07:00
Jeff Hodges
6a90737bbb make mktemp in integration tests work on OS X 2015-07-29 14:54:35 -07:00
James Kasten
b7f01d435f Merge pull request #635 from kuba/staging-server
Update default server URI to staging.
2015-07-29 14:33:48 -07:00
Jakub Warmuz
fe237df2cc
Update default server URI to staging.
http://letsencrypt.status.io/pages/maintenance/55957a99e800baa4470002da/55b90ed12c1b61d83b0001eb
2015-07-29 21:15:45 +00:00
Jakub Warmuz
ca5823ffd8
acme: progress with v03 Simple HTTP challenge. 2015-07-29 20:58:54 +00:00
Brad Warren
b88092daab Merge remote-tracking branch 'upstream/master' into letshelp-apache 2015-07-29 13:47:06 -07:00
James Kasten
91227956cf Merge pull request #634 from kuba/include-package-data
include_package_data in apache and nginx plugins
2015-07-29 13:33:27 -07:00
Jakub Warmuz
d86ade674d
include_package_data in apache and nginx plugins
ref https://github.com/letsencrypt/letsencrypt/issues/625
2015-07-29 19:57:12 +00:00
Brad Warren
6d6c6f284b Incorporated Kuba's feedback 2015-07-29 11:47:56 -07:00
Brad Warren
6bbc412748 Incorporated Schoen's feedback and really fixed py26 support?... 2015-07-29 09:40:49 -07:00
Brad Warren
50ce91b769 Wrapped tarfile for py26 2015-07-28 17:23:03 -07:00
Brad Warren
88e472eb08 Fixed py26 support, cleanup, and added filename check 2015-07-28 17:12:01 -07:00
Brad Warren
6d9c62d586 Finished testing and cleanup 2015-07-28 13:39:24 -07:00
Brad Warren
12b9970787 Finished script besides commit and started unit tests 2015-07-27 19:26:28 -07:00
Jakub Warmuz
a55991055e
Human meaningful exception message for decoding fields with minimum length. 2015-07-25 18:17:53 +00:00
Jakub Warmuz
de3b48640b
Doc fixes. 2015-07-25 15:48:14 +00:00
Jakub Warmuz
c1cd2e173d
Merge remote-tracking branch 'github/letsencrypt/master' into update-challenges 2015-07-25 12:11:27 +00:00
Jakub Warmuz
e0651ad050
Remove Recovery Token. 2015-07-25 12:00:04 +00:00
Brad Warren
0a60122cf6 Started submission script 2015-07-24 20:27:16 -07:00
James Kasten
ccf678f146 rmdirs -> shutil.rmtree 2015-07-24 19:23:12 -07:00
James Kasten
ba4a62f185 Add 'empty mods-enabled dir 2015-07-24 19:18:07 -07:00
James Kasten
87bf4969c0 bump coverage 2015-07-24 19:01:35 -07:00
James Kasten
98b7627e18 Merge branch 'master' into apache_modules 2015-07-24 17:07:16 -07:00
James Kasten
6716d9f0b4 Remove a2enmod option 2015-07-24 17:06:27 -07:00
James Kasten
ab42f7fdfe 100% unittests for apache plugin 2015-07-24 17:05:25 -07:00
James Kasten
1ff899ae33 pylint cleanup 2015-07-24 15:47:38 -07:00
Brad Warren
993ad48705 Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-24 15:03:28 -07:00
Brad Warren
c257f5274d More minor fixes 2015-07-24 15:03:18 -07:00
James Kasten
9454995565 remove verify_setup call 2015-07-24 03:29:16 -07:00
James Kasten
647caba164 100% configurator coverage 2015-07-24 03:22:35 -07:00
James Kasten
da1d6acd59 Merge pull request #622 from kuba/manual-integration
Manual plugin integration testing
2015-07-24 00:03:26 -07:00
Jakub Warmuz
ab097d128b
Fix logging -> logger typo. 2015-07-24 06:41:56 +00:00
Brad Warren
fcb4c7ce89 Merge branch 'kuba-compat-test' into plugin_tests 2015-07-23 22:17:11 -07:00
Brad Warren
e46e9cae65 Pulled Kuba's PR 2015-07-23 22:17:02 -07:00
Brad Warren
f0b2c2592d Speed up exit 2015-07-23 17:28:07 -07:00
Brad Warren
c2aec601e1 Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-23 17:09:27 -07:00
Brad Warren
83ad476a6d Improved logging and error handling 2015-07-23 17:09:09 -07:00
James Kasten
aecb7b71d7 100% Augeas Configr unittests 2015-07-23 14:51:07 -07:00
James Kasten
7282d8bc6d Merge pull request #629 from ryh/patch-1
remove sudo before brew (OS X)
2015-07-23 13:54:51 -07:00
Jacob Hoffman-Andrews
31d37a3953 Remove Python 2.6 support.
Fixes https://github.com/letsencrypt/letsencrypt/issues/515
2015-07-23 10:20:47 -07:00
Jakub Warmuz
34aa3b90f6
Post-rename subpkg updates.
Fix compat testing imports, console script name, subpkg project name,
MANIFEST.
2015-07-23 14:47:20 +00:00
Jakub Warmuz
645378d5df
Move compat testing files to subpkg directory. 2015-07-23 14:38:51 +00:00
Jakub Warmuz
9587a4f84d
Remove tests/__init__.py. 2015-07-23 14:38:51 +00:00
James Kasten
c927149185 Unittests and revisions 2015-07-23 01:34:51 -07:00
Bigfish
8e0b271ccd remove sudo before brew (OS X)
brew will refuse sudo  ref: https://github.com/Homebrew/homebrew/issues/9953
2015-07-23 15:47:11 +08:00
Brad Warren
d2e2baa927 Cleanup 2015-07-22 19:51:05 -07:00
Brad Warren
1a1ce7edcf Raised logging level for Docker pull message 2015-07-22 19:02:53 -07:00
Brad Warren
71d12f005d Removed ValidationError 2015-07-22 18:44:43 -07:00
Brad Warren
9ccd46c268 Cleaned up interfaces 2015-07-22 18:31:26 -07:00
Brad Warren
c927f0c89a Finished basic Apache test framework 2015-07-22 18:25:09 -07:00
Brad Warren
b00a3cf963 Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-22 13:47:25 -07:00
Brad Warren
303f7ffe32 Added validator code 2015-07-22 13:47:09 -07:00
James Kasten
8b96264576 improved redirect, testcases 2015-07-22 02:05:01 -07:00
Brad Warren
780d5fcbb9 Finished authenticator tests 2015-07-21 18:14:57 -07:00
James Kasten
d25c7c36e7 Convert to servername/serveralias 2015-07-21 17:16:46 -07:00
Brad Warren
d1653399f5 Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-20 09:42:15 -07:00
James Kasten
6a8590b4cc Improved Apache and unittests 2015-07-19 19:49:44 -07:00
James Kasten
3206880673 add complex parsing tests 2015-07-19 16:48:27 -07:00
Jakub Warmuz
2028c3a454
Remove obsolete comment. 2015-07-19 12:59:10 +00:00
Jakub Warmuz
82147f1f5e
Travis: add le.wtf to /etc/hosts. 2015-07-19 11:22:57 +00:00
Jakub Warmuz
9e2682a025
100% coverage for manual test mode and related code. 2015-07-19 11:04:29 +00:00
Jakub Warmuz
09e658c486
Merge remote-tracking branch 'github/letsencrypt/master' into manual-integration 2015-07-19 10:00:10 +00:00
James Kasten
20e81949c4 Merge branch 'master' into apache_modules 2015-07-19 02:22:41 -07:00
James Kasten
78fd81aed7 Cleanup Apache 2015-07-19 02:22:10 -07:00
Jakub Warmuz
3825633f46
Merge remote-tracking branch 'github/letsencrypt/master' into manual-integration
Conflicts:
	letsencrypt/auth_handler.py
2015-07-19 07:49:32 +00:00
Jakub Warmuz
00298173a2
Merge branch 'dvsni-verify' into dvsni-v03
Conflicts:
	acme/acme/challenges.py
	acme/acme/challenges_test.py
	letsencrypt/achallenges.py
	letsencrypt/crypto_util.py
	letsencrypt/plugins/common.py
	letsencrypt/tests/achallenges_test.py
2015-07-18 17:58:22 +00:00
Brad Warren
124b993429 Cleanup 2015-07-17 17:33:32 -07:00
Brad Warren
94afa42e07 Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-17 16:21:48 -07:00
Brad Warren
f6936d8412 Finished basic Apache wrapper 2015-07-17 16:21:17 -07:00
James Kasten
de4540a1c7 some cleanup 2015-07-17 14:09:46 -07:00
Jakub Warmuz
735bd924bf
Update letsencrypt to DVSNI v03 (fixes #597). 2015-07-17 17:44:35 +00:00
Brad Warren
e8387b10c4 Finished basic Apache 2.4 Proxy 2015-07-16 16:57:47 -07:00
Brad Warren
e715a29362 Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-15 23:02:27 -07:00
Brad Warren
4b098cdce2 Finished basic Apache2.4 proxy 2015-07-15 23:00:18 -07:00
James Kasten
679dda10af Fix parsing tests 2015-07-15 15:25:34 -07:00
James Kasten
0d69e5cff4 Support dvsni_port, reorganize dvsni 2015-07-15 14:34:24 -07:00
Brad Warren
6c6ef2bb40 Started implementation of Apache base 2015-07-14 18:04:43 -07:00
James Kasten
1b13458463 Redesign choose_vhost and prepare_https, Cleanup make_vhost_ssl 2015-07-14 14:03:43 -07:00
James Kasten
ec065e8b58 Parse variables and fnmatch 2015-07-13 23:18:33 -07:00
James Kasten
89d810c06a Fix enable_mod 2015-07-13 14:16:51 -07:00
Ceesjan Luiten
1597546516 Consume longest possible match 2015-07-13 21:50:15 +02:00
Brad Warren
44f69c525d Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-13 12:01:14 -07:00
Brad Warren
22fb1b18c7 Setup directory structure 2015-07-13 11:56:38 -07:00
James Kasten
b04c393a72 Merge branch 'master' into apache_modules 2015-07-13 11:43:35 -07:00
Jakub Warmuz
1bb6763595
acme: Update DVSNI to v03. 2015-07-12 17:23:37 +00:00
James Kasten
a3e257f6fc Merge branch 'master' into apache_modules 2015-07-10 22:29:56 -07:00
James Kasten
1b308cd530 Merge branch 'master' into apache_modules 2015-07-10 15:18:35 -07:00
Brad Warren
f827263592 Merge remote-tracking branch 'upstream/master' into plugin_tests 2015-07-10 11:42:34 -07:00
Brad Warren
7081c440e7 Started building test suite 2015-07-10 11:42:10 -07:00
James Kasten
50f1db4b11 Move dvsni config file to server-root 2015-07-09 18:55:08 -07:00
James Kasten
53e01c19af Use config_test raise appropriate errors 2015-07-09 17:51:08 -07:00
James Kasten
a402382a49 Remove enable mod_ssl/configuration changes on prepare() 2015-07-09 17:37:32 -07:00
James Kasten
ac32e54798 In order directive search 2015-07-09 16:15:45 -07:00
James Kasten
9d17ac7347 Use binary for all Define parameters 2015-07-08 17:17:54 -07:00
James Kasten
8a5bb57a0c Merge branch 'master' into apache_modules
Conflicts:
	letsencrypt/cli.py
2015-07-08 12:16:23 -07:00
James Kasten
9b263f9859 outline init path 2015-07-07 13:18:22 -07:00
Jakub Warmuz
74ce332b5a
Manual SimpleHTTP integration tests. 2015-07-03 09:49:14 +00:00
James Kasten
5c24a4f499 Iterate on loaded modules 2015-07-02 15:18:39 -07:00
James Kasten
24675bef5b Initial exclude directives based on modules/parameters 2015-07-01 18:07:53 -07:00
James Kasten
0f7804cd94 Initial modules and runtime parameter parsing 2015-07-01 16:50:48 -07:00
AJ ONeal
694aa3705b Make it SUPER clear where to find install instructions 2015-06-23 16:15:37 -06:00
James Kasten
d7291d6c8e Merge branch 'martijnbastiaan-validator' into validator 2015-01-29 03:51:04 -08:00
James Kasten
435b7a126a Merge branch 'validator' of git://github.com/martijnbastiaan/lets-encrypt-preview into martijnbastiaan-validator
Conflicts:
	README.md
	letsencrypt/client/interfaces.py
2015-01-29 03:48:07 -08:00
Martijn Bastiaan
9cfa5f27b0 Removed **locals() construction 2015-01-22 12:42:07 +01:00
Martijn Bastiaan
69e0f27a77 Added HSTS age check; included tests 2015-01-22 12:32:37 +01:00
Martijn Bastiaan
d0125f05f3 Added openssl and responses as dependencies, added tests for Validator.{https,redirect} 2015-01-18 17:45:42 +01:00
Martijn Bastiaan
93be5486a8 Extended IValidator, renamed arguments 2015-01-18 16:51:55 +01:00
Martijn Bastiaan
cdf59a8b7f Fixed pylint warnings, added test skeleton, implemented regular expression for SPDY test 2015-01-18 16:50:59 +01:00
Martijn Bastiaan
9e123d9639 Added spdy to Validator 2015-01-13 16:03:27 +01:00
Martijn Bastiaan
b4ef173245 Moved ValidationException, changed https() signature in interface, restructured imports to comply with project guidelines 2015-01-13 14:41:17 +01:00
Martijn Bastiaan
ba1ad6036c Cleaned Validator, implemented ocsp checking 2015-01-13 00:39:56 +01:00
Martijn Bastiaan
29a72cbd72 Added zope inheritance to interface 2015-01-13 00:39:17 +01:00
Martijn Bastiaan
afc40443f6 Forgot negation in if-statement checking for https 2015-01-12 21:18:14 +01:00
Martijn Bastiaan
c506480b64 Added basic validator 2015-01-12 19:03:03 +01:00
Dominic
df850ee980 Reduce the matching of REWRITE_HTTPS_ARGS and add query string
If the backreference of the match is not used, it's enough to match '^' instead of '^.*$'. It's slightly faster.

^ -> Match, if it starts
^.*$` -> Match, if everything matches

In addition it might be useful to append the query string with the flag: QSA
2014-11-19 09:33:31 +01:00
202 changed files with 9565 additions and 3723 deletions

6
.gitignore vendored
View file

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

4
.pep8 Normal file
View file

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

View file

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

View file

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

View file

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

69
Dockerfile-dev Normal file
View file

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

View file

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

View file

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

View file

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

190
acme/LICENSE.txt Normal file
View file

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

View file

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

1
acme/README.rst Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

7
acme/acme/util.py Normal file
View file

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

16
acme/acme/util_test.py Normal file
View file

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

View file

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

View file

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

15
bootstrap/archlinux.sh Executable file
View file

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

1
bootstrap/dev/README Normal file
View file

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

25
bootstrap/dev/_venv_common.sh Executable file
View file

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

13
bootstrap/dev/venv.sh Executable file
View file

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

8
bootstrap/dev/venv3.sh Executable file
View file

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

8
bootstrap/freebsd.sh Executable file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

1
docs/man/jws.rst Normal file
View file

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

View file

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

1
docs/man/letsencrypt.rst Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,55 @@
# Global configuration
PidFile ${APACHE_PID_FILE}
#
# Timeout: The number of seconds before receives and sends time out.
#
Timeout 300
#
# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
#
KeepAlive On
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
# Include module configuration:
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Include generic snippets of statements
IncludeOptional conf-enabled/
# Include the virtual host configurations:
IncludeOptional sites-enabled/*.conf
Define COMPLEX
Define tls_port 1234
Define tls_port_str "1234"
Define fnmatch_filename test_fnmatch.conf
Include test_variables.conf
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View file

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

View file

@ -0,0 +1 @@
FNMATCH_DIRECTIVE Success

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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