Compare commits

...

552 Commits

Author SHA1 Message Date
Brad Warren
dc1364a3bb add py37 tox environment 2018-07-02 15:33:18 -07:00
Brad Warren
6cd17e1283 Reorganize packages in tox to allow for py37 tests
certbot-dns-cloudflare doesn't currently work in Python 3.7 because it transitively depends on pyYAML which doesn't yet support Python 3.7. See https://github.com/yaml/pyyaml/issues/126 for more info.
2018-07-02 15:29:52 -07:00
Brad Warren
552e60a126 Don't use hardcoded port in tests (#6145)
* Don't use port 1234 in standalone tests.

* rename unused variable

* add back failure case

* Add back probe connection error test.

* fix lint

* remove unused import

* fix test file coverage

* prevent future heisenbug
2018-06-29 15:27:58 +03:00
Brad Warren
6e13c2ccc7 Add --disable=locally-enabled to .pylintrc. (#6159) 2018-06-28 15:06:52 -07:00
ohemorange
a816cc8979 Use account reuse symlink logic when loading an account (#6156)
Fixes #6154.

* add symlinking to load flow

* test account reuse on load
2018-06-28 13:34:16 -07:00
Brad Warren
64e06d4201 Use greater than or equal to in requirements. (#6117)
* Use greater than or equal to in requirements.

This changes the existing requirements using strictly greater than to greater
than or equal to so that they're more conventional.

* Use >= for certbot-postfix.

Despite it previously saying 'certbot>0.23.0', certbot-postfix/local-oldest-requirements.txt was pinned to 0.23.0 so let's just use certbot>=0.23.0.
2018-06-28 10:55:21 -07:00
Bahram Aghaei
d00a31622d run Isort on imported packages (#6138) 2018-06-28 10:43:52 -07:00
Brad Warren
742a57722b fix server_root default tests on macOS (#6149) 2018-06-27 17:35:43 -07:00
Brad Warren
ad3c547e1f Update cli-help.txt to use generic values (#6143) 2018-06-27 14:29:21 -07:00
Joona Hoikkala
f169e7374b Interactive certificate selection with install verb (#6097)
If either --cert-name or both --key-path and --cert-path (in which case the user requests installation for a certificate not managed by Certbot) are not provided, prompt the user with managed certificates and let them choose.

Fixes: #5824
2018-06-27 11:35:01 -07:00
Joona Hoikkala
2304f7fcda Remove unnecessary dotfiles (#6151) 2018-06-27 09:32:53 -07:00
Brad Warren
a4760cfe56 Partially revert "Implement TLS-ALPN-01 challenge and standalone TLS-ALPN server (#5894)" (#6144)
This partially reverts commit 15f1405fff.

A basic tls-alpn-01 implementation is left so we can successfully parse the
challenge so it can be used in boulder's tests.
2018-06-26 15:33:41 -07:00
ohemorange
87e1912bf9 Show both possible Nginx default server root values in docs (#6137)
See https://github.com/certbot/website/pull/348#issuecomment-399257703.

```
$ certbot --help all | grep -C 3  nginx-server-root
nginx:
  Nginx Web Server plugin - Alpha

  --nginx-server-root NGINX_SERVER_ROOT
                        Nginx server root directory. (default: /etc/nginx)
  --nginx-ctl NGINX_CTL
                        Path to the 'nginx' binary, used for 'configtest' and
 ```

```
$ CERTBOT_DOCS=1 certbot --help all | grep -C 3  nginx-server-root
nginx:
  Nginx Web Server plugin - Alpha

  --nginx-server-root NGINX_SERVER_ROOT
                        Nginx server root directory. (default: /etc/nginx or
                        /usr/local/etc/nginx)
  --nginx-ctl NGINX_CTL
```

* Show both possible Nginx default server root values in docs

* add test

* check that exactly one server root is in the default

* use default magic
2018-06-25 18:09:30 -07:00
r5d
80cd134847 certbot.cli: Remove debug-challenges option for renew subcommand. (#6141)
Addresses issue #5005.
2018-06-25 18:02:07 -07:00
sydneyli
7890de62ec doc(postfix): install instructions (#6136)
fixes #6131

* doc(postfix): install instructions

* address brad's comments
2018-06-21 16:11:02 -07:00
Brad Warren
1e1e7d8e97 Improve UA default in docs (#6120)
* Use less informative UA values in docs.

* set CERTBOT_DOCS during release
2018-06-21 15:40:42 -07:00
ohemorange
2ac0b55208 Reuse ACMEv1 accounts for ACMEv2 in production (#6134)
* Reuse accounts made with ACMEv1 when using an ACMEv2 Let's Encrypt server. This commit turns the feature on for the production server; the bulk of the work was done in 8e4303a.

* add upgrade test for production server
2018-06-21 13:23:09 -07:00
Harlan Lieberman-Berg
6771b8e05b docs: move warning about distro provided renewal (#6133)
Currently, you must read ten paragraphs about writing renewal hooks
before you find that most distributions will automatically renew certs
for you.  This is burying the lede in a major way; moving it up to the
header seems a better choice.
2018-06-21 12:52:08 -07:00
Joona Hoikkala
3877af6619 Gradually increasing HSTS max-age (#5912)
This PR adds the functionality to enhance Apache configuration to include HTTP Strict Transport Security header with a low initial max-age value.

The max-age value will get increased on every (scheduled) run of certbot renew regardless of the certificate actually getting renewed, if the last increase took place longer than ten hours ago. The increase steps are visible in constants.AUTOHSTS_STEPS.

Upon the first actual renewal after reaching the maximum increase step, the max-age value will be made "permanent" and will get value of one year.

To achieve accurate VirtualHost discovery on subsequent runs, a comment with unique id string will be added to each enhanced VirtualHost.

* AutoHSTS code rebased on master

* Fixes to match the changes in master

* Make linter happy with metaclass registration

* Address small review comments

* Use new enhancement interfaces

* New style enhancement changes

* Do not allow --hsts and --auto-hsts simultaneuously

* MyPy annotation fixes and added test

* Change oldest requrements to point to local certbot core version

* Enable new style enhancements for run and install verbs

* Test refactor

* New test class for main.install tests

* Move a test to a correct test class
2018-06-21 07:27:19 -07:00
Brad Warren
a875246a4b Merge pull request #6121 from certbot/squashed-postfix
Postfix plugin
2018-06-15 17:29:34 -07:00
sydneyli
40c50231ed Merge branch 'master' into squashed-postfix 2018-06-15 16:21:15 -07:00
Sydney Li
4ba153949d Fixing up postfix plugin
- Finishing refactor of postconf/postfix command-line utilities
 - Plugin uses starttls_policy plugin to specify per-domain policies

Cleaning up TLS policy code.

Print warning when setting configuration parameter that is overridden by master.

Update client to use new policy API

Cleanup and test fixes

Documentation fix

smaller fixes

Policy is now an enhancement and reverting works

Added a README, and small documentation fixes throughout

Moving testing infra from starttls repo to certbot-postfix

fixing tests and lint

Changes against new policy API

starttls-everywhere => starttls-policy

testing(postfix): Added more varieties of certificates to test against.

Moar fixes against policy API.

Address comments on README and setup.py

Address small comments on postconf and util

Address comments in installer

Python 3 fixes and Postconf tester extends TempDir test class

Mock out postconf calls from tests and test coverage for master overrides

More various fixes. Everything minus testing done

Remove STARTTLS policy enhancement from this branch.

sphinx quickstart

99% test coverage

some cleanup and testfixing

cleanup leftover files

Remove print statement

testfix for python 3.4

Revert dockerfile change

mypy fix

fix(postfix): brad's comments

test(postfix): coverage to 100

test(postfix): mypy

import mypy types

fix(postfix docs): add .rst files and fix build

fix(postfix): tls_only and server_only params behave nicely together

some cleanup

lint

fix more comments

bump version number
2018-06-15 15:46:55 -07:00
Brad Warren
5025b4ea96 Add certbot-postfix to tools
pep8ify

Delint

cover++

test more_info()

Refactor get_config_var

Don't duplicate changes to Postfix config

document instance variables

Always clear save_notes on save

Test deploy_cert and save and add MockPostfix.

Move mock and call to InstallerTest

Add getters and setters

Use postfix getters and setters

protect get_config_var

bump cover to 100%

bump required coverage to 100

s/config_dir/config_utility

Decrease minimum version to Postfix 2.6.

This is the minimum version that allows us to set ciphers to be used with
opportunistic TLS and is the oldest version packaged in any major distro.

Use tls_security_level instead of use_tls.

smtpd_tls_security_level should be used instead according to Postfix documentation.

Test smtpd_tls_security_level conditional

make dunder method an under method

refactor postconf usage

add check_all_output

test check_all_output

Add and test verify_exe_exists

Add PostfixUtilBase

Add ReadOnlyMainMap

Use _get_output instead of _call

Fix split strip typo
2018-06-15 15:46:48 -07:00
sydneyli
adc07ef933 fix(display): alternate spaces and dashes (#6119)
* fix(display): alternate spaces and dashes

* add comment
2018-06-15 15:03:58 -07:00
Brad Warren
3316eac178 Separate integration coverage (#6113)
* check coverage separately

* Add coverage minimums for integration tests.
2018-06-15 09:55:16 -07:00
Brad Warren
8b16a56de8 remove comment about renewer (#6115) 2018-06-15 11:43:48 +03:00
Brad Warren
453eafb11e Used packaged acme in oldest tests. (#6112) 2018-06-15 11:41:38 +03:00
r5d
c4ae376279 Add autorenew option to renew subcommand (#5911)
* Add autorenew option to `renew` subcommand.

* Change default value for 'autorenew' cli option.

* Update certbot.cli.prepare_and_parse_args (autorenew)

Set `default` for --autorenew and --no-autorenew.

* Update certbot.storage.RenewableCert.should_autorenew.

- Remove `interactive` argument in RenewableCert.should_autorenew.
- Update certbot.renewal.should_renew.

* Move autorenew enable/disable check to certbot.storage.

- Remove autorenew enable/disable check in
  `certbot.renewal.handle_renewal_request`.
- Fix RenewableCert.autorenewal_is_enabled; autorenew is stored in
  'renewalparams'.
- Add autorenew enable/disable check in
  `RenewableCert.should_autorenew`.
- Update tests test_time_interval_judgments,
  test_autorenewal_is_enabled, test_should_autorenew tests in
  storage_test.py

* certbot: Update RenewableCert.should_autorenew

Remove block that sets autorenew option in the renewal configuration
file.

* certbot: Update prepare_and_parse_args.

Remove --autorenew option.

* certbot: Update CLI_DEFAULTS.

Set default of `autorenew` to True.

* Remove unused imports in certbot.storage.
2018-06-13 15:24:51 -07:00
Brad Warren
fccfbd14b1 add 0.25.1 changelog (#6111) 2018-06-13 14:20:43 -07:00
Brad Warren
c9ae365f66 0.25.1 update for master (#6110)
* Release 0.25.1

(cherry picked from commit 21b5e4eadb)

* Bump version to 0.26.0
2018-06-13 14:20:15 -07:00
Brad Warren
9f20fa0ef9 Fixes #6085. (#6091)
The value of norecusedirs is the default in newer versions of pytest which is
listed at
https://docs.pytest.org/en/3.0.0/customize.html#confval-norecursedirs.
2018-06-12 17:31:22 -07:00
Brad Warren
95892cd4ab Require acme>=0.25.0 for nginx (#6099) 2018-06-12 17:24:19 -07:00
Roland Bracewell Shoemaker
da028ca9c2 Wrap TLS-ALPN extension with ASN.1 (#6089)
* Wrap TLS-ALPN extension with ASN.1

* Fix test
2018-06-11 11:59:57 -07:00
Brad Warren
5bf1c51de7 Merge pull request #6075 from certbot/candidate-0.25.0
Update certbot-auto and version numbers
2018-06-11 10:41:57 -07:00
Brad Warren
afa7e3fb82 Unrevert #6000 and silence deprecation warnings (#6082)
* Revert "Revert "switch signature verification to use pure cryptography (#6000)" (#6074)"

This reverts commit 3cffe1449c.

* Fixes #6073.

This silences the deprecation warnings from cryptography. I looked into only
silencing the cryptography warning specifically in the function, however,
CryptographyDeprecationWarning doesn't seem to be publicly documented, so we
probably shouldn't depend on it.
2018-06-08 00:45:23 +03:00
Brad Warren
3a8de6d172 Upgrade pinned twine version. (#6078)
For the past couple of releases, twine has errored while trying to upload
packages and this is fixed by upgrading to a newer version of twine. This
commit updates our pinned version installed when using tools/venv.sh to the
latest available version. pkginfo had to be upgraded as well to support the
latest version of twine.
2018-06-07 07:50:36 -07:00
Brad Warren
780a1b3a26 Don't require festival during signing. (#6079)
Festival isn't available via Homebrew and is only needed to read the hash
aloud, so let's not make it a strict requirement that it's installed. You can
simply read the hash from the terminal instead.
2018-06-07 11:43:45 +03:00
Brad Warren
da6320f4d1 Stop testing against Debian 7. (#6077)
Debian Wheezy is no longer supported (see https://wiki.debian.org/LTS) and
Amazon shut down their Debian 7 mirrors so let's stop trying to use Debian 7
during testing.
2018-06-07 11:11:06 +03:00
Brad Warren
eec37f65a8 Update changelog for 0.25.0 (#6076) 2018-06-06 19:01:55 -07:00
Brad Warren
4b11fe1fda Bump version to 0.26.0 2018-06-06 13:50:46 -07:00
Brad Warren
4ae2390c44 Release 0.25.0 2018-06-06 13:50:30 -07:00
Brad Warren
3cffe1449c Revert "switch signature verification to use pure cryptography (#6000)" (#6074)
This reverts commit 366c50e28e.
2018-06-06 07:58:50 -07:00
Brad Warren
868e5b831b Make python setup.py test use pytest for acme (#6072) 2018-06-05 17:59:11 -07:00
ohemorange
d905886f4c Automatically select among default vhosts if we have a port preference in nginx (#5944)
* automatically select among default vhosts if we have a port preference

* ports should be strings in the nginx plugin

* clarify port vs preferred_port behavior by adding allow_port_mismatch flag

* update all instances of default_vhosts to all_default_vhosts

* require port

* port should never be None in _get_default_vhost
2018-06-05 13:40:48 -07:00
sydneyli
09a28c7a27 Allow multiple add_headers directives (#6068)
* fix(nginx-hsts): allow multiple add_headers

* test(nginx): fix nginx tests
2018-06-04 17:44:51 -07:00
ohemorange
8e4303af9f Reuse ACMEv1 accounts for ACMEv2 (#5902)
* Reuse ACMEv1 accounts for ACMEv2

* Correct behavior

* add unit tests

* add _find_all_inner to comply with interface

* acme-staging-v01 --> acme-staging

* only create symlink to previous account if there is one there

* recurse on server path

* update tests and change internal use of load to use server_path

* fail gracefully on corrupted account file by returning [] when rmdir fails

* only reuse accounts in staging for now
2018-06-04 16:04:47 -07:00
Brad Warren
236f9630e0 Remove unneeded sys import (#5873)
* Remove unneeded sys import.

Once upon a time we needed this in some of these setup.py files because we were
using sys in the file, but we aren't anymore so let's remove the import.

* use setuptools instead of distutils
2018-06-04 15:04:56 -07:00
Maciej Dębski
15f1405fff Implement TLS-ALPN-01 challenge and standalone TLS-ALPN server (#5894)
The new challenge is described in https://github.com/rolandshoemaker/acme-tls-alpn.

* TLS-ALPN tests

* Implement TLS-ALPN challenge

* Skip TLS-ALPN tests on old pyopenssl

* make _selection methods private.
2018-06-04 14:54:17 -07:00
Brad Warren
4151737e17 Read in bytes to fix --reuse-key on Python 3 (#6069)
* Read bytes for now for compatibility

* add clarifying comment
2018-06-04 13:13:23 -07:00
Joona Hoikkala
f19ebab441 Make sure the pluginstorage file gets truncated when writing to it (#6062) 2018-06-04 11:08:40 -07:00
schoen
e2d6faa8a9 Add --reuse-key feature (#5901)
* Initial work on new version of --reuse-key

* Test for reuse_key

* Make lint happier

* Also test a non-dry-run reuse_key renewal

* Test --reuse-key in boulder integration test

* Better reuse-key integration testing

* Log fact that key was reused

* Test that the certificates themselves are different

* Change "oldkeypath" to "old_keypath"

* Simply appearance of new-key generation logic

* Reorganize new-key logic

* Move awk logic into TotalAndDistinctLines function

* After refactor, there's now explicit None rather than missing param

* Indicate for MyPy that key can be None

* Actually import the Optional type

* magic_typing is too magical for pylint

* Remove --no-reuse-key option

* Correct pylint test disable
2018-06-01 15:21:02 -07:00
Josh Soref
fb0d2ec3d6 Include missing space (#6061) 2018-06-01 15:09:02 -07:00
ohemorange
d53ef1f7c2 Add mypy info to Certbot docs (#6033)
* Add mypy info to Certbot docs

* break up lines

* link to mypy docs and links to https

* Expand on import wording

* be consistent about mypy styling
2018-05-31 13:57:23 -07:00
Joona Hoikkala
9f6b147d6f Do not call updaters and deployers when run with --dry-run (#6038)
When Certbot is run with --dry-run, skip running GenericUpdater and RenewDeployer interface methods.

This PR also makes the parameter order of updater.run_generic_updaters and updater.run_renewal_deployer consistent.

Fixes #5927

* Do not call updaters and deployers when run with --dry-run

* Use ConfigTestCase instead of mocking config objects manually
2018-05-26 08:31:23 -07:00
Joona Hoikkala
e48c653245 Change GenericUpdater parameter to lineage (#6030)
In order to give more flexibility for plugins using interfaces.GenericUpdater interface, lineage needs to be passed to the updater method instead of individual domains. All of the (present and potential) installers do not work on per domain basis, while the lineage does contain a list of them for installers which do.

This also means that we don't unnecessarily run the updater method multiple times, potentially invoking expensive tooling up to $max_san_amount times.

* Make GenericUpdater use lineage as parameter and get invoked only once per lineage
2018-05-25 11:00:37 -07:00
Jacob Hoffman-Andrews
a03c68fc83 Clean up boulder-fetch a bit. (#6032)
The value for FAKE_DNS is now always the same because Boulder's
docker-compose hardcodes it, so skip some sed.

Set a time limit on how long we'll wait for boulder to come up.
2018-05-24 10:53:21 -07:00
Jeremy Gillula
b1bcccb04b Changing opt-in ask for emails (#6035) 2018-05-23 20:40:34 -07:00
ohemorange
a1f5dc27f2 Add domain to error message when no matching server block found (#6034) 2018-05-23 14:03:30 -07:00
Brad Warren
0b215366b1 turn off cancel notifications (#5918) 2018-05-23 13:57:22 -07:00
Jacob Hoffman-Andrews
4304ff0d62 Bring up just the boulder container. (#6031)
Boulder recently added a "netaccess" container which may conflict.
2018-05-23 11:33:21 -07:00
Kevin Le
deb5b072d9 Log cases when standalone fails to bind a port. (#5985)
* Log cases when standalone fails to bind a port.

* Fix linter + changed log message

* Changed multiline string format

* Fixed indentation in standalone.py
2018-05-23 19:59:49 +03:00
pdamodaran
8440d0814d fixed dependency-requirements.txt (#6023) 2018-05-22 15:35:12 -07:00
Quang Vu
cfd4b8f363 #4242 Support multi emails register (#5994)
This change will allow registering/updating account with multi emails.
Detail is enclosed in #4242

* support multi emails register

* add more test cases

* update test to unregister before register

* update create path to support multi emaill

* refactor payload updating

* fix typo

* move command line doc to another place

* revert the change for updating account registration info, added unit test

* rearrange text for consistency
2018-05-22 15:32:44 -07:00
Brad Warren
c9a206ca89 Get mypy passing with check_untyped_defs everywhere (#6021)
* unchecked_typed_defs everywhere

* fix mypy for lock_test

* add magic_typing

* fix mypy in letshelp

* fix validator errors in compat test

* fix mypy for test_driver.py

* fix mypy in util.py

* delint
2018-05-21 20:23:21 -07:00
ohemorange
0d3a157525 Merge pull request #6020 from certbot/mabayhan-patch
Set correct Nginx server root on FreeBSD and Darwin
2018-05-21 16:53:11 -07:00
schoen
5d1df1cb4c Merge pull request #6022 from certbot/revert-pycon
Revert "Add link to pycon issues (#5959)"
2018-05-21 15:23:58 -07:00
Brad Warren
dec97fc126 Revert "Add link to pycon issues (#5959)"
This reverts commit 68359086ff.
2018-05-18 17:48:30 -07:00
Paul Kehrer
366c50e28e switch signature verification to use pure cryptography (#6000)
* switch signature verification to use pure cryptography

On systems that prevent write/execute pages this prevents a segfault
that is caused by pyopenssl creating a dynamic callback in the
verification helper.

* switch to using a verifier for older cryptography releases

also add ec support, test vectors, and a test
2018-05-18 09:10:41 -07:00
Dmitry Figol
36dfd06503 Prepare certbot module for mypy check untyped defs (#6005)
* Prepare certbot module for mypy check untyped defs

* Fix #5952

* Bump mypy to version 0.600 and fix associated bugs

* Fix pylint bugs after introducing mypy

* Implement Brad's suggestions

* Reenabling pylint and adding nginx mypy back
2018-05-18 06:28:17 -07:00
Brad Warren
250c0d6691 cd before running tests (#6017)
When importing a module, Python first searches the current directory. See
https://docs.python.org/3/tutorial/modules.html#the-module-search-path. This
means that running something like `import certbot` from the root of the Certbot
repo will use the local Certbot files regardless of the version installed on
the system or virtual environment.

Normally this behavior is fine because the local files are what we want to
test, however, during our "oldest" tests, we test against older versions of our
packages to make sure we're keeping compatibility. To make sure our tests use
the correct versions, this commit has our tests cd to an empty temporary
directory before running tests.

We also had to change the package names given to pytest to be the names used in
Python to import the package rather than the name of the files locally to
accommodate this.
2018-05-18 06:05:26 -07:00
Brad Warren
94bf97b812 Add remaining DNS plugins to mypy.ini and sort (#6018) 2018-05-18 12:26:10 +03:00
Erica Portnoy
1239d7a881 check platform with correct python 2018-05-17 20:02:27 -07:00
TyrannosourceExe
9b2862ebb0 3692 --dry-run expiration emails (#6015)
* If --dry-run is used and there exists no staging account, create account with no email

* added unit testing of dry-run to ensure certbot does not ask the user to create an email, and that certbot creates an account with no email
2018-05-17 16:03:01 -07:00
Brad Warren
1be1bd9211 remove PYTHONPATH (#6016) 2018-05-17 09:23:05 -07:00
pdamodaran
20418cdd68 Fixed #5859 (#6011) 2018-05-17 06:52:11 -07:00
Brad Warren
41e1976c17 Fix noisy tests (#6004)
* Fixes #5570.

The issue is calls to atexit aren't mocked out. During the tests there are many
repeated calls registering functions to be called when the process exits so
when the tests finishes, it prints a ton of output from running those
registered functions. This suppresses that by mocking out atexit.

* Mock at a lower level.

This ensures we don't mess with any other mocks in this test class by mocking
at the lowest level we can. Other tests shouldn't be mocking out specific
internals of functions in other modules, so this should work just fine.
2018-05-16 06:24:14 -07:00
Douglas Anger
722dac86d5 Fix crash when email submission endpoint unavailable (#6002)
* Fix crash when email submission endpoint unavailable

Handle KeyError and ValueError so that if the email submission endpoint
goes down, Certbot can still run.

Add tests to eff_test.py:
 - simulate non-JSON response as described in issue #5858
 - simulate JSON response without 'status' element

Non-JSON response throws an uncaught ValueError when attempting to
decode as JSON. A JSON response missing the 'status' element throws an
uncaught KeyError when checking whether status is True or False.

Teach _check_response to handle ValueError and KeyError and report an
issue to the user.

Rewrite if statement as assertion with try-except block to make error
handling consistent within the function. Update test_not_ok to make
mocked raise_for_status function raise a requests.exceptions.HTTPError.

Resolves #5858

* Update PR with requested changes

- Use `if` instead of `assert` to check `status` element of response JSON
- Handle KeyError and ValueError in the same way
- Import requests at the beginning of eff_test.py
- Clear JSON in test case in a more idiomatic way
2018-05-15 12:50:47 -07:00
GmH
751f9843b4 fixed issue #5974 for certbot-dns-route53 (#5984)
* fixed issue #5974 for certbot-dns-route53

* fixed issue #5967 for certbot-dns-digitalocean

* update to use acme.magic_typing and DefaultDict class

* added no-name-in-module identifier, for issue #5974

* added unused-import identifier to disable option, for issue #5974
2018-05-15 11:22:09 -07:00
dschlessman
9bd5b3dda2 Issue 5951/check untyped defs apache (#5989)
* resolved mypy untyped defs in parser.py

* resolved mypy untyped defs in obj.py

* removed unused imports

* resolved mypy untyped defs in http_01.py

* resolved mypy untyped defs in tls_sni_01.py

* resolved mypy untyped defs in configurator.py

* address mypy too-many-arguments error in override_centos.py

* resolved mypy untyped defs in http_01_test.py

* removed unused 'conf' argument that was causing mypy method assignment error

* address mypy error where same variable reassigned to different type

* address pylint and coverage issues

* one character space change for formatting

* fix required acme version for certbot-apache
2018-05-15 10:40:32 -07:00
James Hiebert
307f45f88f Enable checking of type annotation in Nginx plugin (#5997)
* Adds type checking for certbot-nginx

* First pass at type annotation in certbot-nginx

* Ensure linting is disabled for timing imports

* Makes container types specific per PR comments

* Removes unnecessary lint option
2018-05-15 09:36:47 -07:00
signop
802fcc99ee Add requests-toolbelt hashes to requirements. (#6001)
Fixes certbot/certbot#5993
2018-05-15 08:50:09 -07:00
Douglas Anger
2d68c9b81e Display (None) instead of a bullet for empty lists (#5999)
Include a line break before "(None)" to maintain consistency with output
for lists that are not empty.

Previous result as expected for non-empty lists:

    >>> _format_list('+', ['one', 'two', 'three'])
    '\n+ one\n+ two\n+ three'

Previous unexpected result for empty lists:

    >>> _format_list('+', [])
    '\n+ '

New result as expected (unchanged) for non-empty lists:

    >>> _format_list('+', ['one', 'two', 'three'])
    '\n+ one\n+ two\n+ three'

New behavior more explicit for empty lists:

    >>> _format_list('+', [])
    '\n(None)'

Resolves #5886
2018-05-15 08:46:36 -07:00
James Hiebert
42ef252043 Adds a note about Python3 in the Developer Guide (#5998) 2018-05-15 07:58:33 -07:00
Douglas Anger
99d94cc7e8 Make request logs pretty in Python 3 (#5992)
Decode response data as UTF-8 to eliminate ugly bytes repr in Python 3.

Resolves #5932
2018-05-14 14:45:25 -07:00
signop
02b128a128 Add support for specifying source_address to ClientNetwork. (#5990)
For certbot/certbot#3489
2018-05-14 14:43:43 -07:00
speter
372d4a046c docs/using.rst: fix typo (#5962) 2018-05-14 14:40:06 -07:00
Sarah Braden
a724dc659b correct error message text now prompts user to run $certbot certificates (#5988) 2018-05-14 14:21:09 -07:00
Douglas Anger
907ee79715 Check_untyped_defs in mypy with clean output for certbot-dns-nsone (#5987)
* check_untyped_defs in mypy with clean output for certbot-dns-nsone

Resolves #5972
2018-05-14 13:18:49 -07:00
Kevin Le
c1471fe873 Document IPv6/IPv4 binding on standalone. (#5983) 2018-05-14 13:15:02 -07:00
Brad Warren
94829e35be Merge pull request #5982 from certbot/handle-mypy-ini-conflicts
Check unchecked defs in various DNS plugins
2018-05-14 11:43:33 -07:00
Brad Warren
19c6a5e6ee Merge branch 'issue_5968' into handle-mypy-ini-conflicts 2018-05-14 11:12:31 -07:00
Brad Warren
875c8d4c01 Merge branch 'feature/5971' into handle-mypy-ini-conflicts 2018-05-14 11:11:08 -07:00
Brad Warren
8d994ae30d Merge branch 'issue_5970' into handle-mypy-ini-conflicts 2018-05-14 11:10:30 -07:00
Sarah Braden
be03a976d5 fixed issue #5969 for certbot-dns-dnsmadeeasy (#5976) 2018-05-14 10:54:26 -07:00
Kevin Le
74448e9344 Set pause=False to fix view_config_changes (#5977) 2018-05-14 10:45:12 -07:00
Sarah Braden
33583792fa blank line at eof 2018-05-14 13:43:15 -04:00
Sarah Braden
430f9414a9 fix for issue #5968 for certbot-dns-dnsimple 2018-05-14 13:42:12 -04:00
Douglas Anger
5636b55507 Check_untyped_defs in mypy with clean output for certbot-dns-luadns
* check_untyped_defs in mypy with clean output for certbot-dns-luadns

Resolves #5971
2018-05-14 13:40:27 -04:00
Sarah Braden
4bd9f4dac4 fix for issue #5970 regarding certbot-dns-google 2018-05-14 13:30:38 -04:00
Douglas Anger
2d45b0b07a Check_untyped_defs in mypy with clean output for certbot_dns_rfc2136 (#5975)
* check_untyped_defs in mypy with clean output for certbot_dns_rfc2136

Resolves #5973
2018-05-14 10:26:33 -07:00
Sarah Braden
a0775f42ba fixed issue #5969 for certbot-dns-dnsmadeeasy 2018-05-14 12:34:27 -04:00
Brad Warren
cce23c86c7 partially revert #5953 (#5964) 2018-05-14 08:08:24 -07:00
Brad Warren
68359086ff Add link to pycon issues (#5959)
* add link to pycon issues

* add especially
2018-05-13 08:06:19 -07:00
ohemorange
5940ee92ab add ready status type (#5941) 2018-05-11 14:25:02 -07:00
Brad Warren
9568f9d5b0 Add instructions on how to ask for help (#5957)
* Add instructions on how to ask for help

* s/setup/set up
2018-05-11 10:52:45 -07:00
Brad Warren
2f89a10f50 Small dev docs improvements (#5953)
* Clarify UNIX only

* Have people develop natively.

Some systems like Arch Linux and macOS require --debug in order to install
dependencies.

Our bootstrapping script for macOS works, so let's let people who want to
develop natively.

* briefly mention docker as dev option

* remove bad _common.sh info

* update OS dep section

* Remove sudo from certbot-auto usage

When sudo isn't available, Certbot is able to fall back to su. Removing it from
the instructions here allows the command to work when its run in minimal
systems like Docker where sudo may not be installed.

* copy advice about missing interpreters

* Improve integration tests docs

Explain what a boulder is and tell people they probably should just let the
tests run in Travis.

* Don't tell people to run integration tests.

I don't think any paid Certbot devs run integration tests locally and instead
rely on Travis. Let's not make others do it.

* fix spacing

* you wouldn't download a CA

* address review comments
2018-05-11 07:07:02 -07:00
Brad Warren
8851141dcf Revert "disable apacheconftest (#5937)" (#5954)
This reverts commit 83ea820525.
2018-05-11 06:12:10 -07:00
ohemorange
832941279b Specify that every domain name needs to be under a server_name directive (#5949) 2018-05-10 16:46:57 -07:00
Brad Warren
83ea820525 disable apacheconftest (#5937) 2018-05-09 12:11:36 -07:00
ohemorange
3eaf35f1e2 Check_untyped_defs in mypy with clean output for acme (#5874)
* check_untyped_defs in mypy with clean output for acme

* test entire acme module

* Add typing as a dependency because it's only in the stdlib for 3.5+

* Add str_utils, modified for python2.7 compatibility

* make mypy happy in acme

* typing is needed in prod

* we actually only need typing in acme so far

* add tests and more docs for str_utils

* pragma no cover

* add magic_typing

* s/from typing/from magic_typing/g

* move typing to dev_extras

* correctly set up imports

* remove str_utils

* only type: ignore for OpenSSL.SSL, not crypto

* Since we only run mypy with python3 anyway and we're fine importing it when it's not actually there, there's no actual need for typing to be present as a dependency

* comment magic_typing.py

* disable wildcard-import im magic_typing

* disable pylint errors

* add magic_typing_test

* make magic_typing tests work alongside other tests

* make sure temp_typing is set

* add typing as a dev dependency for python3.4

* run mypy with python3.4 on travis to get a little more testing with different environments

* don't stick typing into sys.modules

* reorder imports
2018-05-03 13:10:33 -07:00
Brad Warren
32e85e9a23 correct metaclass usage everywhere (#5919) 2018-05-03 10:59:25 +03:00
Joona Hoikkala
95c0c4a708 Py2 and Py3 compatibility for metaclass interfaces (#5913) 2018-05-02 15:56:37 -07:00
Brad Warren
03b20d972c Merge pull request #5914 from certbot/candidate-0.24.0
Update autos and version number for 0.24.0 release
2018-05-02 12:19:09 -07:00
Brad Warren
7fa3455dc6 Update changelog for 0.24.0 (#5915) 2018-05-02 12:18:29 -07:00
Joona Hoikkala
552bfa5eb7 New interfaces for installers to run tasks on renew verb (#5879)
* ServerTLSUpdater and InstallerSpecificUpdater implementation

* Fixed tests and added disables for linter :/

* Added error logging for misconfigurationerror from plugin check

* Remove redundant parameter from interfaces

* Renaming the interfaces

* Finalize interface renaming and move tests to own file

* Refactored the runners

* Refactor the cli params

* Fix the interface args

* Fixed documentation

* Documentation and naming fixes

* Remove ServerTLSConfigurationUpdater

* Remove unnecessary linter disable

* Rename run_renewal_updaters to run_generic_updaters

* Do not raise exception, but make log message more informative and visible for the user

* Run renewal deployer before installer restart
2018-05-02 10:52:54 +03:00
Brad Warren
0ec0d79c35 Bump version to 0.25.0 2018-05-01 16:59:48 -07:00
Brad Warren
4b870ef940 Release 0.24.0 2018-05-01 16:59:32 -07:00
Jacob Hoffman-Andrews
bf30226c69 Restore parallel waiting to Route53 plugin (#5712)
* Bring back parallel updates to route53.

* Re-add try

* Fix TTL.

* Remove unnecessary wait.

* Add pylint exceptions.

* Add dummy perform.

* review.feedback

* Fix underscore.

* Fix lint.
2018-04-25 15:09:50 -07:00
Brad Warren
f510f4bddf Update good volunteer task to good first issue. (#5891) 2018-04-24 06:38:15 -07:00
Aleksandr Volochnev
9c15fd354f Updated base image to python:2-alpine3.7 (#5889)
Updated base image from python:2-alpine to python:2-alpine3.7. Python:2-alpine internally utilises alpine version 3.4 which end-of-life date is the first of May 2018.
2018-04-20 15:17:05 -07:00
Brad Warren
726f3ce8b3 Remove EOL'd Ubuntu from targets.yaml (#5887)
See https://wiki.ubuntu.com/Releases.

Ubuntu 15.* repositories have been shut down for months now causing our tests
to always fail on these systems. While the tests on Ubuntu 12.04 still work, it
has been unsupported by Canonical for almost a year and I don't think we should
hamstring ourselves trying to continue to support it ourselves.
2018-04-19 17:57:41 -07:00
Erik Rose
f40e04401f Don't install enum34 when using Python 3.4 or later. Fix #5456. (#5846)
The re stdlib module requires attrs that don't exist in the backported 3.4 version.

Technically, we are changing our install behavior beyond what is necessary. Previously, enum34 was used for 3.4 and 3.5 as well, and it happened not to conflict, but I think it's better to use the latest bug-fixed stdlib versions as long as they meet the needs of `cryptography`, which is what depends on enum34. That way, at least the various stdlib modules are guaranteed not to conflict with each other.
2018-04-19 16:35:21 -07:00
Jeremy Gillula
398bd4a2cd Emphasizing the warnings in the READMEs in /etc/letsencrypt/live/exam… (#5871)
* Emphasizing the warnings in the READMEs in /etc/letsencrypt/live/example.com

* Making the warning more of a statement
2018-04-18 16:46:39 -07:00
Joona Hoikkala
a024aaf59d Enhance verb (#5596)
* Add the cli parameters

* Tests and error messages

* Requested fixes

* Only handle SSL vhosts

* Interactive cert-name selection if not defined on CLI

* Address PR comments

* Address review comments

* Added tests and addressed review comments

* Move cert manager tests to correct file

* Add display ops tests

* Use display util constants instead of hardcoded values in tests
2018-04-18 11:08:30 -07:00
Brad Warren
261d063b10 Revert fix-macos-pytest (#5853)
* Revert "Fix pytest on macOS in Travis (#5360)"

This reverts commit 5388842e5b.

* remove oldest passenv
2018-04-18 10:02:31 -07:00
Brad Warren
a9e01ade4c Revert "use older boulder version (#5852)" (#5855)
This reverts commit 6b29d159a2.
2018-04-17 17:17:15 -07:00
Kiel C
5c7fc07ccf Adjust file paths message from Warning to Info. (#5743) 2018-04-17 13:52:39 -07:00
Brad Warren
6dc8b66760 Add _choose_lineagename and update docs (#5650) 2018-04-17 11:27:45 -07:00
mabayhan
b39507c5af Update constants.py
Fixed comma.
2018-04-17 09:09:27 -07:00
ohemorange
590ec375ec Get mypy running in travis for easier review (#5875) 2018-04-13 16:10:58 -07:00
Axel
523cdc578d Add port option for rfc2136 plugin (#5844) 2018-04-13 19:17:08 +03:00
Brad Warren
e0a5b1229f help clarify version number (#5865)
(Hopefully) helps make it clearer that that 22 and 24 corresponds to Apache 2.2 and 2.4.
2018-04-12 19:02:40 -07:00
ohemorange
6253acf335 Get mypy running with clean output (#5864)
Fixes #5849.

* extract mypy flags into mypy.ini file

* Get mypy running with clean output
2018-04-12 18:53:07 -07:00
Brad Warren
a708504d5b remove test metaclass (#5863) 2018-04-12 18:28:00 -07:00
mabayhan
c443db0618 Update constants.py
On FreeBSD or MacOS, "certbot --nginx" fails. The reason is, at these op. systems, nginx directory is different than linux.
2018-04-12 16:33:10 -07:00
ohemorange
2d31598484 Get mypy tox env running in the current setup (#5861)
* get mypy tox env running in the current setup

* use any python3 with mypy

* pin mypy dependencies
2018-04-12 15:47:39 -07:00
Brad Warren
6b29d159a2 use older boulder version (#5852) 2018-04-11 16:14:55 -07:00
sydneyli
88ceaa38d5 Bump certbot's acme depenency to 0.22.1 (#5826) 2018-04-11 14:36:53 -07:00
Brad Warren
e7db97df87 Update CHANGELOG for 0.23.0 (#5822)
* Update CHANGELOG for 0.23.0

* correct date
2018-04-11 11:16:12 -07:00
Joona Hoikkala
4a8e35289c PluginStorage to store variables between invocations. (#5468)
The base class for Installer plugins `certbot.plugins.common.Installer` now provides functionality of `PluginStorage` to all installer plugins. This allows a plugin to save and retrieve variables in between of invocations.

The on disk storage is basically a JSON file at `config_dir`/`.pluginstorage.json`, usually `/etc/letsencrypt/.pluginstorage.json`. The JSON structure is automatically namespaced using the internal plugin name as a namespace key. Because the actual storage is JSON, the supported data types are: dict, list, tuple, str, unicode, int, long, float, boolean and nonetype.

To add a variable from inside the plugin class:
`self.storage.put("my_variable_name", my_var)`

To fetch a variable from inside the plugin class:
`my_var = self.storage.fetch("my_variable_key")`

The storage state isn't written on disk automatically, but needs to be called:
`self.storage.save()`

* Plugin storage implementation

* Added config_dir to existing test mocks

* PluginStorage test cases

* Saner handling of bad config_dir paths

* Storage moved to Installer and not initialized on plugin __init__

* Finetuning and renaming
2018-04-11 08:54:55 -07:00
Brad Warren
58626c3197 Double max_rounds (#5842) 2018-04-09 16:58:58 -07:00
schoen
56fb667e15 Merge pull request #5754 from edaleeta/updating-developer-guide
Add note to developer guide docs about installing docs extras. (#4946)
2018-04-09 16:36:36 -07:00
Brad Warren
0153c04af3 Merge pull request #5829 from certbot/candidate-0.23.0
Update certbot-auto and versions to reflect 0.23.0 release
2018-04-09 12:45:15 -07:00
Peter Linss
db938dcc0e Update messages.py (#5759)
Add wildcard field to AuthorizationResource
2018-04-05 13:39:35 -07:00
Brad Warren
0e30621355 Bump version to 0.24.0 2018-04-04 15:05:08 -07:00
Brad Warren
16b2539f72 Release 0.23.0 2018-04-04 15:04:43 -07:00
Brad Warren
b6afba0d64 Include testdata (#5827) 2018-04-04 14:33:41 -07:00
Brad Warren
b24d9dddc3 Revert ACMEv2 default (#5819)
* Revert "document default is ACMEv2 (#5818)"

This reverts commit 2c502e6f8b.

* Revert "Update default to ACMEv2 server (#5722)"

This reverts commit 4d706ac77e.
2018-04-03 17:55:12 -07:00
Joona Hoikkala
9996730fb1 If restart fails, try alternative restart command if available (#5500)
* Use alternative restart command if available in distro overrides
2018-04-03 14:05:37 -07:00
Brad Warren
2c502e6f8b document default is ACMEv2 (#5818) 2018-04-03 14:04:51 -07:00
ohemorange
bdaccb645b Support quoted server names in Nginx (#5811)
* Support quoted server names in Nginx

* add unit test to check that we strip quotes

* update configurator test
2018-04-03 12:14:23 -07:00
Joona Hoikkala
f5ad08047b Fix comparison to check values (#5815) 2018-04-03 12:04:57 -07:00
Brad Warren
8fd3f6c64c fixes #5380 (#5812) 2018-04-03 11:44:13 -07:00
Joshua Bowman
4d706ac77e Update default to ACMEv2 server (#5722) 2018-03-30 17:16:48 -07:00
sydneyli
8231b1a19c Pin Lexicon version to 2.2.1 (#5803) 2018-03-29 17:09:21 -07:00
ohemorange
5ff7f2211e Explicitly add six as a dependency in letsencrypt-auto-source dockerfiles (#5808)
* update documentation

* explicitly add six as a dependency in letsencrypt-auto-source dockerfiles

* pin six version
2018-03-29 15:34:38 -07:00
Brad Warren
7630550ac4 Revert "Update oldest tests to test against 0.22.0 versions (#5800)" (#5809)
This reverts commit 336950c0b9.
2018-03-29 14:15:59 -07:00
Brad Warren
336950c0b9 Update oldest tests to test against 0.22.0 versions (#5800) 2018-03-28 08:37:00 -07:00
ohemorange
a779e06d47 Add integration tests for nginx plugin (#5441)
* Add a rewrite directive for the .well-known location so we don't hit existing rewrites

* add comment

* Add (nonexistent) document root so we don't use the default value

* Add integration tests for nginx plugin

* add a sleep 5 to test on travis

* put sleep 5 in the right spot

* test return status of grep respecting -e and note that we're actually not posix compliant

* redelete newline
2018-03-27 17:33:48 -07:00
ohemorange
669312d248 We don't try to add location blocks through a mechanism that checks REPEATABLE_DIRECTIVES, and it wouldn't work as an accurate check even if we did, so just remove it (#5787) 2018-03-27 15:25:34 -07:00
ohemorange
4d082e22e6 Remove ipv6only=on from duplicated vhosts (#5793)
* rename delete_default to remove_singleton_listen_params

* update docstring

* add documentation to obj.py

* add test for remove duplicate ipv6only

* Remove ipv6only=on from duplicated vhosts

* add test to make sure ipv6only=on is not erroneously removed
2018-03-27 15:11:39 -07:00
sydneyli
af2cce4ca8 fix(auth_handler): cleanup is always called (#5779)
* fix(auth_handler): cleanup is always called

* test(auth_handler): tests for various error cases
2018-03-26 17:09:02 -07:00
ohemorange
804fd4b78a factor out location_directive_for_achall (#5794) 2018-03-26 16:28:30 -07:00
Andrew Starr-Bochicchio
8cdb213a61 Google DNS: Mock API discovery to run tests without internet connection. (#5791)
* Google DNS: Mock API discovery to run tests without internet connection.

* Allow test to pass when run from main cerbot package.
2018-03-26 16:12:55 -07:00
ohemorange
e9707ebc26 Allow 'default' along with 'default_server' in Nginx (#5788)
* test default detection

* Allow 'default' along with 'default_server' in Nginx

* Test that default gets written out as default_server in canonical string

* remove superfulous parens
2018-03-26 14:56:31 -07:00
ohemorange
8d0d42a739 Refactor _add_directive into separate functions (#5786)
* Refactor _add_directive to separate functions

* UnspacedList isn't idempotent

* refactor parser in add_server_directives and update_or_add_server_directives

* update parser tests

* remove replace=False and add to update_or_add for replace=True in configurator

* remove replace=False and add to update_or_add for replace=True in http01

* update documentation
2018-03-23 16:30:13 -07:00
Alokin Software Pvt Ltd
693cb1d162 Support Openresty in the NGINX plugin (#5467)
* fixes #4919 openresty_support

* making the regex more general

* reformatting warning to pass lint

* Fix string formatting in logging function

* Fix LE_AUTO_VERSION
2018-03-22 17:50:05 -07:00
Delan Azabani
8e9a4447ff make pip_install.sh compatible with POSIX sh(1) again (#5622) 2018-03-22 12:24:53 -07:00
sydneyli
bca0aa48c2 logging: log timestamps as local timezone instead of UTC (#5607)
* logging: log timestamps as local timezone instead of UTC

* test(logging): expect localtime instead of gmtime

* linter fix in logging
2018-03-21 15:41:33 -07:00
Brad Warren
afb6260c34 update changelog for 0.22.1 and 0.22.2 (#5770) 2018-03-21 11:21:35 -07:00
Brad Warren
3f291e51c6 Update certbot auto to reflect 0.22 point releases (#5768)
* Release 0.22.1

(cherry picked from commit 05c75e34e2)

* Bump version to 0.23.0

(cherry picked from commit 6fd3a57791)

* Release 0.22.2

(cherry picked from commit ea445ed11e)

* Bump version to 0.23.0

(cherry picked from commit cbe87d451c66931a084f4e513d899aae085a37d3)
2018-03-21 11:21:09 -07:00
Sebastiaan Lokhorst
fe8e0c98c5 Update docs for Apache plugin (#5776)
The supported OSs are now listed in another file. The table also contradicted the text below.
2018-03-21 11:18:39 -07:00
Harlan Lieberman-Berg
cbd827382e Documentation on cron renewal (#5460) 2018-03-21 08:17:06 -07:00
Edelita Valdez
f01aa1295f Add quotes to command for docs extras. 2018-03-20 23:40:44 -07:00
noci2012
c0dc31fd88 Allow _acme-challenge as a zone (#5707)
* Allow _acme-challenge as a zone

Like described here:
https://github.com/lukas2511/dehydrated/wiki/example-dns-01-nsupdate-script

Not using this patch may be an issue if the parent zone has been (where a wildcard certificate has been requested.) signed by DNSSEC.

Please consider this also for inclusion before dns-01 will be allowed for wildcards.

* Update dns_rfc2136.py

forgot one domain_name reference

* Update dns_rfc2136.py

moved domain up & added assignment.

* Update dns_rfc2136_test.py

tests adjusted to new calls.

* Update dns_rfc2136_test.py

Forgot on DOMAIN...

* Update dns_rfc2136_test.py

* Update dns_rfc2136.py

pydoc updates.

* Update dns_rfc2136.py
2018-03-20 13:29:24 -07:00
Brad Warren
41ce108881 Fix cleanup_challenges call (#5761)
* fixes cleanup_challenges

* add test to prevent regressions
2018-03-19 16:51:01 -07:00
Gopal Adhikari
41ed6367b4 Fix typo: damain -> domain (#5756)
Fix typo: damain -> domain in certbot/util.py:607
2018-03-19 11:08:45 -07:00
Edelita Valdez
a26a78e84e Add note to developer guide docs about installing docs extras. (#4946) 2018-03-17 19:24:14 -07:00
sydneyli
3077b51500 Merge pull request #5749 from certbot/fix-docker-link
Fix Docker link
2018-03-16 18:15:05 -07:00
Brad Warren
d4834da0f4 fix docker link 2018-03-16 17:48:46 -07:00
Brad Warren
ba6bdb5099 Fix acme.client.Client.__init__ (#5747)
* fixes #5738

* add test to prevent regressions
2018-03-16 17:45:46 -07:00
sydneyli
79d90d6745 feat(nginx plugin): add HSTS enhancement (#5463)
* feat(nginx plugin): add HSTS enhancement

* chore(nginx): factor out block-splitting code from redirect & hsts enhancements!

* chore(nginx): merge fixes

* address comments

* fix linter: remove a space

* fix(config): remove SSL directives in HTTP block after block split, and remove_directive removes 'Managed by certbot' comment

* chore(nginx-hsts): Move added SSL directives to a constant on Configurator class

* fix(nginx-hsts): rebase on wildcard cert changes
2018-03-16 15:27:39 -07:00
ohemorange
5ecb68f2ed Update instances of acme-staging url to acme-staging-v02 (#5734)
* update instances of acme-staging url to acme-staging-v02

* keep example client as v1

* keep deactivate script as v1
2018-03-16 15:24:55 -07:00
Brad Warren
b3e73bd2ab removes blank line from chain.pem (#5730) 2018-03-14 17:38:37 -07:00
Spencer Eick
065e923bc9 Improve "cannot find cert of key directive" error (#5525) (#5679)
- Fix code to log separate error messages when either SSLCertificateFile or SSLCertificateKeyFile -
 directives are not found.
- Update the section in install.rst where the relevant error is referenced.
- Edit a docstring where 'cert' previously referred to certificate.
- Edit test_deploy_cert_invalid_vhost in the test suite to cover changes.

Fixes #5525.
2018-03-14 12:59:13 -07:00
cclauss
e405aaa4c1 Fix print() and xrange() for Python 3 (#5590) 2018-03-14 09:37:29 -07:00
Brad Warren
9ea14d2e2b Add docs about --server (#5713)
* Add docs about --server

* address review comments

* mention server in Docker docs

* correct server URL

* Use prod ACMEv2 example
2018-03-14 08:48:40 -07:00
Brad Warren
1d0e3b1bfa Add documentation about DNS plugins and Docker (#5710)
* make binding port optional

* Add DNS docker docs

* add basic DNS plugin docs

* Add link to DNS plugin docs from Docker docs

* Shrink table size
2018-03-13 07:08:01 -07:00
Brad Warren
d310ad18c7 Put API link at the bottom of DNS plugin docs (#5699)
* Put link to API at the bottom for future docs.

* Put API link at the bottom of existing docs.
2018-03-12 17:10:23 -07:00
Brad Warren
53c6b9a08f Merge pull request #5682 from certbot/candidate-0.22.0
Release 0.22.0
2018-03-12 13:06:30 -07:00
Brad Warren
64d647774e Update the changelog to reflect 0.22.0 (#5691) 2018-03-12 10:57:46 -07:00
Brad Warren
f13fdccf04 document resps param (#5695) 2018-03-12 10:51:45 -07:00
Brad Warren
2e6d65d9ec Add readthedocs requirements files (#5696)
* Add readthedocs requirements files.

* Only install docs extras for plugin.
2018-03-08 17:24:30 -08:00
Brad Warren
cc24b4e40a Fix --allow-subset-of-names (#5690)
* Remove aauthzr instance variable

* If domain begins with fail, fail the challenge.

* test --allow-subset-of-names

* Fix renewal and add extra check

* test after hook checks
2018-03-08 11:12:33 -08:00
Brad Warren
cc18da926e Quiet pylint (#5689) 2018-03-08 11:09:31 -08:00
sydneyli
f4bac423fb fix(acme): client._revoke sends default content_type (#5687) 2018-03-07 15:09:47 -08:00
Brad Warren
7a495f2656 Bump version to 0.23.0 2018-03-07 10:26:08 -08:00
Brad Warren
77fdb4d7d6 Release 0.22.0 2018-03-07 10:25:42 -08:00
Brad Warren
e0ae356aa3 Upgrade pipstrap to 1.5.1 (#5681)
* upgrade pipstrap to 1.5.1

* build leauto
2018-03-07 09:10:47 -08:00
Brad Warren
6357e051f4 Fallback without dns.resourceRecordSets.list permission (#5678)
* Add rrset list fallback

* List dns.resourceRecordSets.list as required

* Handle list failures differently for add and del

* Quote record content

* disable not-callable for iter_entry_points

* List update permission
2018-03-06 15:32:22 -08:00
Brad Warren
d62c56f9c9 Remove the assumption the domain is unique in the manual plugin (#5670)
* use entire achall as key

* Add manual cleanup hook

* use manual cleanup hook
2018-03-06 07:21:01 -08:00
Brad Warren
cee9ac586e Don't report coverage on Apache during integration tests (#5669)
* ignore Apache coverage

* drop min coverage to 67
2018-03-06 07:20:34 -08:00
Brad Warren
a643877f88 Merge pull request #5672 from certbot/route53_acmev2v2
Version 2 of ACMEv2 support for Route53 plugin
2018-03-06 07:19:46 -08:00
Brad Warren
7bc45121a1 Remove the need for route53:ListResourceRecordSets
* add test_change_txt_record_delete
2018-03-05 18:58:32 -08:00
Joona Hoikkala
fe682e779b ACMEv2 support for Route53 plugin 2018-03-05 18:58:27 -08:00
Joona Hoikkala
441625c610 Allow Google DNS plugin to write multiple TXT record values (#5652)
* Allow Google DNS plugin to write multiple TXT record values in same resourcerecord

* Atomic updates

* Split rrsets request
2018-03-05 12:49:02 -08:00
Brad Warren
cc344bfd1e Break lockstep between our packages (#5655)
Fixes #5490.

There's a lot of possibilities discussed in #5490, but I'll try and explain what I actually did here as succinctly as I can. Unfortunately, there's a fair bit to explain. My goal was to break lockstep and give us tests to ensure the minimum specified versions are correct without taking the time now to refactor our whole test setup.

To handle specifying each package's minimum acme/certbot version, I added a requirements file to each package. This won't actually be included in the shipped package (because it's not in the MANIFEST).

After creating these files and modifying tools/pip_install.sh to use them, I created a separate tox env for most packages (I kept the DNS plugins together for convenience). The reason this is necessary is because we currently use a single environment for each plugin, but if we used this approach for these tests we'd hit issues due to different installed plugins requiring different versions of acme/certbot. There's a lot more discussion about this in #5490 if you're interested in this piece. I unfortunately wasted a lot of time trying to remove the boilerplate this approach causes in tox.ini, but to do this I think we need negations described at complex factor conditions which hasn't made it into a tox release yet.

The biggest missing piece here is how to make sure the oldest versions that are currently pinned to master get updated. Currently, they'll stay pinned that way without manual intervention and won't be properly testing the oldest version. I think we should solve this during the larger test/repo refactoring after the release because the tests are using the correct values now and I don't see a simple way around the problem.

Once this lands, I'm planning on updating the test-everything tests to do integration tests with the "oldest" versions here.

* break lockstep between packages

* Use per package requirements files

* add local oldest requirements files

* update tox.ini

* work with dev0 versions

* Install requirements in separate step.

* don't error when we don't have requirements

* install latest packages in editable mode

* Update .travis.yml

* Add reminder comments

* move dev to requirements

* request acme[dev]

* Update pip_install documentation
2018-03-05 09:50:19 -08:00
Brad Warren
e1878593d5 Ensure fullchain_pem in the order is unicode/str (#5654)
* Decode fullchain_pem in ACMEv1

* Convert back to bytes in Certbot

* document bytes are returned
2018-03-05 07:27:44 -08:00
Brad Warren
31805c5a5f Merge pull request #5628 from certbot/dns-docker
Add DNS Dockerfiles
2018-03-02 11:36:16 -08:00
ohemorange
8bc9cd67f0 Fix ipv6only detection (#5648)
* Fix ipv6only detection

* move str() to inside ipv6_info

* add regression test

* Update to choose_vhosts
2018-03-01 15:08:53 -08:00
Brad Warren
d8a54dc444 Remove leading *. from default cert name. (#5639) 2018-03-01 14:55:45 -08:00
Brad Warren
8121acf2c1 Add user friendly wildcard error for ACMEv1 (#5636)
* add WildcardUnsupportedError

* Add friendly unsupported wildcard error msg

* correct documentation

* add version specifier
2018-03-01 14:54:48 -08:00
ohemorange
f0b337532c Nginx plugin wildcard support for ACMEv2 (#5619)
* support wildcards for deploy_cert

* support wildcards for enhance

* redirect enhance and some tests

* update tests

* add display_ops and display_repr

* update display_ops_test and errors found

* say server block

* match redirects properly

* functional code

* start adding tests and lint errors

* add configurator tests

* lint

* change message to be generic to installation and enhancement

* remove _wildcard_domain

* take selecting vhosts out of loop

* remove extra newline

* filter wildcard vhosts by port

* lint

* don't filter by domain

* [^.]+

* lint

* make vhost hashable

* one more tuple
2018-03-01 14:05:49 -08:00
Brad Warren
559220c2ef Add basic ACMEv2 integration tests (#5635)
* Use newer boulder config

* Use ACMEv2 endpoint if requested

* Add v2 integration tests

* Work with unset variables

* Add wildcard issuance test

* quote domains
2018-03-01 10:11:15 -08:00
Brad Warren
38d5144fff Drop min coverage to 63 (#5641) 2018-03-01 08:25:32 -08:00
Brad Warren
78735fa2c3 Suggest DNS authenticator when it's needed (#5638) 2018-02-28 16:08:06 -08:00
Joona Hoikkala
e9bc4a319b Apache plugin wildcard support for ACMEv2 (#5608)
In `deploy_cert()` and `enhance()`, the user will be presented with a dialog to choose from the VirtualHosts that can be covered by the wildcard domain name. The (multiple) selection result will then be handled in a similar way that we previously handled a single VirtualHost that was returned by the `_find_best_vhost()`.

Additionally the selected VirtualHosts are added to a dictionary that maps selections to a wildcard domain to be reused in the later `enhance()` call and not forcing the user to select the same VirtualHosts again.

* Apache plugin wildcard support

* Present dialog only once per domain, added tests

* Raise exception if no VHosts selected for wildcard domain
2018-02-28 11:31:47 -08:00
Brad Warren
a39d2fe55b Fix wildcard issuance (#5620)
* Add is_wildcard_domain to certbot.util.

* Error with --allow-subset-of-names and wildcards.

* Fix issue preventing wildcard cert issuance.

* Kill assumption domain is unique in auth_handler

* fix typo and add test

* update comments
2018-02-27 18:05:33 -08:00
Brad Warren
b18696b6a0 Don't run tests with Python 2.6 (#5627)
* Don't run tests with Python 2.6.

* Revert "Don't run tests with Python 2.6."

This reverts commit 4a9d778cca62ae2bec4cf060726e88f1fd66f374.

* Revert changes to auto_test.py.
2018-02-27 16:47:43 -08:00
Brad Warren
6f86267a26 Fix revocation in ACMEv2 (#5626)
* Allow revoke to pass in a url

* Add revocation support to ACMEv2.

* Provide regr for account based revocation.

* Add revoke wrapper to BackwardsCompat client
2018-02-27 12:42:13 -08:00
Brad Warren
57bdc590df Add DNS Dockerfiles 2018-02-26 16:27:38 -08:00
Brad Warren
43ba9cbf33 Merge pull request #5605 from certbot/rm-eol-2.6
Drop Python 2.6 and 3.3 support
2018-02-26 13:34:50 -08:00
Nick Bebout
f3a0deba84 Remove min version of setuptools (#5617) 2018-02-23 13:26:11 -08:00
Brad Warren
1e46d26ac3 Fix ACMEv2 issues (#5612)
* Add post wrapper to automatically add acme_version

* Add uri to authzr.

* Only add kid when account is set.

* Add content_type when downloading certificate.

* Only save new_authz URL when it exists.

* Handle combinations in ACMEv1 and ACMEv2.

* Add tests for ACMEv2 "combinations".
2018-02-22 16:28:50 -08:00
ohemorange
990b211a76 Remove extra :returns: (#5611) 2018-02-22 12:33:55 -08:00
ohemorange
457269b005 Add finalize_order to shim object, update Certbot to use it (#5601)
* update order object with returned authorizations

* major structure of finalize_order shim refactor

* util methods and imports for finalize_order shim refactor

* update certbot.tests.client_test.py

* extraneous client_test imports

* remove correct import

* update renewal call

* add test for acme.dump_pyopenssl_chain

* Add test for certbot.crypto_util.cert_and_chain_from_fullchain

* add tests for acme.client and change to fetch chain failure to TimeoutError

* s/rytpe/rtype

* remove ClientV1 passthrough

* dump the wrapped cert

* remove dead code

* remove the correct dead code

* support earlier mock
2018-02-22 10:14:29 -08:00
Marcus LaFerrera
c3659c300b Return str rather than bytes (#5585)
* Return str rather than bytes

Project id is returned as bytes, which causes issues when constructing the google cloud API url, converting `b'PROJECT_ID'` to `b%27PROJECT_ID%27` causing the request to fail.

* Ensure we handle both bytes and str types

* project_id should be a str or bytes, not int
2018-02-22 10:09:06 -08:00
Brad Warren
f3b23662f1 Don't error immediately on wildcards. (#5600) 2018-02-21 20:52:04 -08:00
Brad Warren
f1b7017c0c Finish dropping Python 2.6 and 3.3 support
* Undo letsencrypt-auto changes

* Remove ordereddict import

* Add Python 3.4 tests to replace 3.3

* Add python_requires

* update pipstrap
2018-02-21 19:11:01 -08:00
ohemorange
ea3b78e3c9 update order object with returned authorizations (#5598) 2018-02-20 18:53:48 -08:00
ohemorange
02b56bd7f3 Merge pull request #5588 from certbot/request_authorizations
Support new_order-style in Certbot
2018-02-20 17:10:05 -08:00
Erica Portnoy
d13a4ed18d add tests for if partial auth success 2018-02-20 16:50:23 -08:00
Erica Portnoy
df50f2d5fa client test 2018-02-20 16:12:15 -08:00
Erica Portnoy
dea43e90b6 lint 2018-02-20 16:11:36 -08:00
Erica Portnoy
a7eadf8862 add new order test for v1 2018-02-20 16:08:46 -08:00
Erica Portnoy
65d0b9674c Fix client test 2018-02-20 16:01:35 -08:00
Erica Portnoy
26bcaff85c add test for new_order for v2 2018-02-20 15:59:58 -08:00
Erica Portnoy
d5a90c5a6e delete key and csr before trying again 2018-02-20 15:43:27 -08:00
Erica Portnoy
051664a142 lint 2018-02-20 15:39:30 -08:00
Erica Portnoy
7c073dbcaf lint 2018-02-20 15:38:18 -08:00
Erica Portnoy
d29c637bf9 support best_effort 2018-02-20 15:36:35 -08:00
Erica Portnoy
d6af978472 remove if/pass 2018-02-20 14:52:11 -08:00
Erica Portnoy
3dfeb483ee lint 2018-02-20 14:49:23 -08:00
Erica Portnoy
76a0cbf9c2 client tests passing 2018-02-20 14:43:12 -08:00
Erica Portnoy
a0e84e65ce auth_handler tests are happy 2018-02-20 14:29:04 -08:00
Erica Portnoy
11f2f1e576 remove extra spaces 2018-02-20 13:20:41 -08:00
Erica Portnoy
d6b4e2001b put back in best_effort code, with a todo for actually supporting it in ACMEv2 2018-02-20 13:19:04 -08:00
schoen
59a1387764 Merge pull request #5594 from DrMattChristian/master
Fix Certbot Apache plugin on Oracle Linux Server, a clone of CentOS, RHEL
2018-02-20 09:12:57 -08:00
Matt Christian
9c84fe1144 Add override class for ID="ol" AKA Oracle Linux Server, a clone of CentOS/RHEL. 2018-02-18 15:45:22 -06:00
Erica Portnoy
68e24a8ea7 start test updates 2018-02-16 17:59:51 -08:00
Erica Portnoy
20d0b91c71 switch interface to new_order and remove best_effort flag 2018-02-16 17:35:10 -08:00
Erica Portnoy
ea2022588b add docstring 2018-02-16 16:32:49 -08:00
Erica Portnoy
eaf739184c pass pem to auth_handler 2018-02-16 16:29:42 -08:00
Erica Portnoy
73bd801f35 add and use request_authorizations 2018-02-16 16:22:26 -08:00
Hugo
42638afc75 Drop support for EOL Python 2.6 and 3.3
* Drop support for EOL Python 2.6

* Use more helpful assertIn/NotIn instead of assertTrue/False

* Drop support for EOL Python 3.3

* Remove redundant Python 3.3 code

* Restore code for RHEL 6 and virtualenv for Py2.7

* Revert pipstrap.py to upstream

* Merge py26_packages and non_py26_packages into all_packages

* Revert changes to *-auto in root

* Update by calling letsencrypt-auto-source/build.py

* Revert permissions for pipstrap.py
2018-02-16 16:14:01 -08:00
ohemorange
e95e963ad6 Get common name from CSR in new_order in ClientV2 (#5587)
* switch new_order to use crypto_util._pyopenssl_cert_or_req_san

* move certbot.crypto_util._get_names_from_loaded_cert_or_req functionality to acme.crypto_util._pyopenssl_cert_or_req_all_names
2018-02-16 16:05:16 -08:00
Brad Warren
2a142aa932 Make Certbot depend on josepy (#5542) 2018-02-16 14:47:10 -08:00
Brad Warren
adec7a8fed Cleanup dockerfile-dev (#5435)
* cleanup dockerfile-dev

* map port 80

* remove python3-dev package
2018-02-16 09:51:27 -08:00
ohemorange
dba6990f70 Merge pull request #5578 from certbot/v2-orders-v2
Add order support and tests
2018-02-15 19:43:06 -08:00
Brad Warren
70a75ebe9d Add tests and fix minor bugs in Order support
* delint

* refactor client tests

* Add test for new order and fix identifiers parsing.

* Add poll_and_finalize test

* Test and fix poll_authorizations timeout

* Add test_failed_authorizations

* Add test_poll_authorizations_success

* Test and fix finalize_order success

* add test_finalize_order_timeout

* add test_finalize_order_error

* test sleep code
2018-02-15 19:26:01 -08:00
Jacob Hoffman-Andrews
e48898a8c8 ACMEv2: Add Order support
This adds two new classes in messages: Order and OrderResource. It also adds methods to ClientV2 to create orders, and poll orders then request issuance.

The CSR is stored on the OrderResource so it can be carried along and submitted when it's time to finalize the order.
2018-02-15 19:12:15 -08:00
ohemorange
d467a4ae95 Add mechanism to detect acme version (#5554)
Detects acme version by checking for newNonce field in the directory, since it's mandatory. Also updates ClientNetwork.account on register and update_registration.

* add mechanism to detect acme version

* update ClientNetwork.account comment

* switch to MultiVersionClient object in acme

* add shim methods

* add returns

* use backwards-compatible format and implement register

* update to actual representation of tos v2

* add tos fields and pass through to v1 for partial updates

* update tests

* pass more tests

* allow instance variable pass-through and lint

* update certbot and tests to use new_account_and_tos method

* remove --agree-tos test from main_test for now because we moved the callback into acme

* add docstrings

* use hasattr

* all most review comments

* use terms_of_service for both v1 and v2

* add tests for acme/client.py

* tests for acme/messages.py
2018-02-15 19:04:17 -08:00
sydneyli
d5efefd979 Re-land proper webroot directory cleanup (#5577)
* fix(webroot): clean up directories properly

* fix(webroot): undo umask in finally

* Fix for MacOS
2018-02-15 15:55:08 -08:00
cclauss
09b5927e6a from botocore.exceptions import ClientError (#5507)
Fixes undefined name 'botocore' in flake8 testing of https://github.com/certbot/certbot

$ __flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics__
```
./tests/letstest/multitester.py:144:12: F821 undefined name 'botocore'
    except botocore.exceptions.ClientError as e:
           ^
1     F821 undefined name 'botocore'
```
2018-02-15 11:07:35 -08:00
sydneyli
7e6d2f1efe Merge pull request #5259 from certbot/issue_5045
Show expiration dates for cert when skipping its renewal
2018-02-15 09:53:13 -08:00
Sydney Li
608875cd65 Add test for skipped certs 2018-02-14 15:45:08 -08:00
sydneyli
99aec1394d Revert "Proper webroot directory cleanup (#5453)" (#5574)
This reverts commit ad0a99a1f5.
2018-02-14 12:09:17 -08:00
Joona Hoikkala
fbace69b5e Fix install verb (#5536)
* Fix install verb

* Fix error message, tests and remove global pylint change

* Fix boulder integration test keypath

* Also use chain_path from lineage if not defined on CLI
2018-02-14 09:28:36 -08:00
Joona Hoikkala
ac464a58e5 Only add Include for TLS configuration if not already there (#5498)
* Only add Include for TLS configuration if not already there

* Add tests to prevent future regression
2018-02-14 08:16:20 -08:00
sydneyli
9277710f6f Added install-only flag (#5531) 2018-02-13 11:15:08 -08:00
sydneyli
ad0a99a1f5 Proper webroot directory cleanup (#5453)
* fix(webroot): clean up directories properly

* fix(webroot): undo umask in finally
2018-02-13 10:50:04 -08:00
Brad Warren
49edf17cb7 ignore .docker (#5477) 2018-02-13 09:52:04 -08:00
Joona Hoikkala
932ecbb9c2 Fix test inconsistence in Apache plugin configurator_test (#5520) 2018-02-12 16:43:59 -08:00
Eli Young
90664f196f Remove autodocs for long-removed acme.other module (#5529)
This module was removed in 22a9c7e3c2. The
autodocs are therefore unnecessary. Furthermore, they are starting to
cause build failures for Fedora.
2018-02-12 16:43:11 -08:00
Jacob Hoffman-Andrews
789be8f9bc Change "Attempting to parse" warning to info. (#5557)
* Change "Attempting to parse" warning to info.

This message shows up on every renewal run when the config was updated
by a newer version of Certbot than the one being run. For instance, if a
user has the certbot packages installed from PPA (currently 0.18.2), but
runs certbot-auto once to try out the latest version (0.21.1), they will
start getting this message via email every 12 hours.
2018-02-12 14:55:41 -08:00
Peter Eckersley
abc4a27613 [Docs] restore docs for ppl just using Certbot git master (#5420)
- Dev / test cycles are one use case for the "running a local copy of
 the client" instructions, but simply running bleeding edge Certbot is
 another
 - So edit the docs to once again explain how to just run bleeding edge
 Certbot, without (say) always getting staging certs.
2018-02-12 14:07:33 -08:00
Brad Warren
1f45832460 Suggest people try the community forum. (#5561) 2018-02-09 16:41:05 -08:00
schoen
a58c875b2a Merge pull request #5526 from certbot/certificates
Use "certificate" instead of "cert" in docs.
2018-02-09 16:32:47 -08:00
ohemorange
d6b247c002 Set ClientNetwork.account after registering (#5558) 2018-02-09 12:54:15 -08:00
Brad Warren
4f0aeb12fa Add find-duplicative-certs docs (#5547)
* add find-duplicative-certs docs

* address review feedback
2018-02-07 14:14:26 -08:00
Jacob Hoffman-Andrews
530a9590e6 Add sudo to certbot-auto instructions. (#5501) 2018-02-07 14:08:03 -08:00
Brad Warren
0416382633 Update leauto_upgrades with tests from #5402. (#5407) 2018-02-06 17:01:58 -08:00
Jacob Hoffman-Andrews
9baf75d6c8 client.py changes for ACMEv2 (#5287)
* Implement ACMEv2 signing of POST bodies.

* Add account, and make acme_version explicit.

* Remove separate NewAccount.

* Rename to add v2.

* Add terms_of_service_agreed.

* Split out wrap_in_jws_v2 test.

* Re-add too-many-public-methods.

* Split Client into ClientBase / Client / ClientV2

* Use camelCase for newAccount.

* Make acme_version optional parameter on .post().

This allows us to instantiate a ClientNetwork before knowing the version.

* Add kid unconditionally.
2018-02-06 16:45:33 -08:00
sydneyli
e085ff06a1 Update old issue link to point to letsencrypt community forums. (#5538) 2018-02-05 16:27:21 -08:00
Jacob Hoffman-Andrews
72b63ca5ac Use "certificate" instead of "cert" in docs. 2018-02-01 13:14:43 -08:00
Brad Warren
45613fd31c update changelog for 0.21.1 (#5504) 2018-01-26 16:02:19 -08:00
Noah Swartz
b05be7fa65 Add expiration date to skipped message 2018-01-26 14:37:50 -08:00
Brad Warren
43bbaadd11 Update certbot-auto and help (#5487)
* Release 0.21.1

(cherry picked from commit ff60d70e68)

* Bump version to 0.22.0
2018-01-25 15:29:38 -08:00
Noah Swartz
a166396358 Merge pull request #5471 from certbot/issue_5449
add let's encrypt status to footer and fix link
2018-01-25 12:20:25 -08:00
Brad Warren
a2239baa45 fix test_tests.sh (#5478) 2018-01-24 22:38:36 -08:00
Brad Warren
a1aba5842e Fix --no-bootstrap on CentOS/RHEL 6 (#5476)
* fix --no-bootstrap on RHEL6

* Add regression test
2018-01-24 22:23:20 -08:00
ohemorange
8a9f21cdd3 Fix Nginx redirect issue (#5479)
* wrap redirect in if host matches

* return 404 if we've created a new block

* change domain matching to exact match

* insert new redirect directive at the top

* add a redirect block to the top if it doesn't already exist, even if there's an existing redirect

* fix obj tests

* remove active parameter

* update tests

* add back spaces

* move imports

* remove unused code
2018-01-24 22:19:32 -08:00
Jacob Hoffman-Andrews
0a4f926b16 Remove Default Detector log line. (#5372)
This produces a super-long log line that wraps to 30-60 lines, depending on
screen width. Even though it's just at debug level, it clutters up the integration
test output without providing proportional debugging value.

* Remove Default Detector log line.

This produces about 30 lines of log output. Even though it's just at debug
level, it clutters up the integration test output without providing proportional
debugging value.

* Add more useful logs.
2018-01-24 15:01:42 -08:00
Noah Swartz
c0068791ce add let's encrypt status to footer and fix link 2018-01-24 13:56:40 -08:00
Joona Hoikkala
b0aa8b7c0b Work around Basic Authentication for challenge dir in Apache (#5461)
Unfortunately, the way that Apache merges the configuration directives is different for mod_rewrite and <Location> / <Directory> directives.

To work around basic auth in VirtualHosts, the challenge override Include had to be split in two. The first part handles overrides for RewriteRule and the other part will handle overrides for <Directory> and <Location> directives.
2018-01-23 16:46:36 -08:00
Noah Swartz
a67a917eca Merge pull request #5446 from certbot/0.21.0-changelog
Add 0.21.0 changelog
2018-01-18 13:03:47 -08:00
Brad Warren
103039ca40 Add 0.21.0 changelog 2018-01-17 17:46:56 -08:00
Brad Warren
aa01b7d0c0 Merge pull request #5445 from certbot/candidate-0.21.0
Release 0.21.0
2018-01-17 17:43:57 -08:00
Brad Warren
325a97c1ed Bump version to 0.22.0 2018-01-17 15:55:41 -08:00
Brad Warren
bf695d048d Release 0.21.0 2018-01-17 15:55:29 -08:00
Brad Warren
1bb2cfadf7 hardcode vhosts and names for test (#5444) 2018-01-17 15:34:34 -08:00
Brad Warren
f43a95e9c1 Merge pull request #5442 from certbot/apache-http-01
Better Apache HTTP01 Support
2018-01-17 11:18:58 -08:00
Brad Warren
522532dc30 Improve no vhost error message 2018-01-17 11:01:24 -08:00
Joona Hoikkala
6dd724e1f4 Merge branch 'apache-http-01' of github.com:certbot/certbot into apache-http-01 2018-01-17 20:08:22 +02:00
Joona Hoikkala
63136be2e5 Make sure the HTTP tests do not use wrong vhosts for asserts 2018-01-17 20:07:38 +02:00
Brad Warren
bd231a3855 Error without vhosts and fix tests token type 2018-01-17 09:38:10 -08:00
ohemorange
e9b57e1783 Add (nonexistent) document root so we don't use the default value (#5437) 2018-01-17 08:02:10 -08:00
ohemorange
2c379cd363 Add a rewrite directive for the .well-known location so we don't hit existing rewrites (#5436) 2018-01-17 08:01:44 -08:00
Joona Hoikkala
b8f288a372 Add include to every VirtualHost if definite one not found based on name 2018-01-17 14:08:45 +02:00
Brad Warren
f420b19492 Apache HTTP01 Improvements
* Fix docstring quote spacing

* Remove unneeded directives

* Enable mod_rewrite

* Remove ifmod rewrite

* Use stricter rewriterule

* Uncomment tests

* Fix order args

* Remove S which doesn't seem to work across contexts

* Use double backslash to make pylint

* Fix enmod test

* Fix http-01 tests

* Test for rewrite

* check for Include in vhost

* add test_same_vhost

* Don't add includes twice

* Include default vhosts in search

* Respect port in find_best_http_vhost

* Add find_best_http_vhost port test

* Filter by port in http01
2018-01-16 23:17:08 -08:00
Joona Hoikkala
314c5f19e5 Set up vhost discovery and overrides for HTTP-01
* Finalized HTTP vhost discovery and added overrides

* Include overrides to every VirtualHost
2018-01-16 23:08:46 -08:00
ohemorange
7e463bccad Handle more edge cases for HTTP-01 support in Nginx (#5421)
* only when using http01, only match default_server by port

* import errors

* put back in the code that creates a dummy block, but only when we can't find anything else
2018-01-16 14:58:45 -08:00
Brad Warren
368ca0c109 Small cleanup for Apache HTTP-01
* Remove http_doer from self

* Refactor _find_best_vhost
2018-01-15 22:08:37 -08:00
Joona Hoikkala
60dd67a60e Use static directory under workdir for HTTP challenges (#5428)
* Use static directory under workdir for HTTP challenges

* Handle the reverter file registration before opening file handle
2018-01-14 15:22:22 -08:00
ohemorange
2cb9d9e2aa Implement HTTP-01 challenge for Nginx (#5414)
* get http01 challenge working

* support multiple challenge types in configurator.py

* update existing nginx tests

* lint

* refactor NginxHttp01 and NginxTlsSni01 to both now  inherit from NginxChallengePerformer

* remove TODO

* challenges_test tests with both tlssni01 and http01

* Make challenges.py more abstract to make lint happier

* add pylint disables to the tests to make pylint happier about the inheritance and abstraction situation

* no need to cover raise NotImplementedError() lines

* python3 compatibility

* test that http01 perform is called

* only remove ssl from addresses during http01

* Initialize addrs_to_add

* Change Nginx http01 to modify server block so the site doesn't stop serving while getting a cert

* pass existing unit tests

* rename sni --> http01 in unit tests

* lint

* fix configurator test

* select an http block instead of https

* properly test for port number

* use domains that have matching addresses

* remove debugger

* remove access_log and error_log cruft that wasn't being executed

* continue to return None from choose_redirect_vhost when create_if_no_match is False

* add nginx integration test
2018-01-11 17:06:23 -08:00
Brad Warren
66953435c9 Merge remote-tracking branch 'starttls-everywhere/certbotify' into postfix 2017-08-29 10:44:07 -07:00
Brad Warren
dde0bf0821 Remove non-plugin files 2017-08-29 10:42:31 -07:00
Brad Warren
142bc33545 Remove dead code 2017-08-29 10:38:53 -07:00
Brad Warren
a339de80f4 Add save() 2017-08-28 17:17:29 -07:00
Brad Warren
d663f7981a Add _write_config_changes 2017-08-28 15:24:51 -07:00
Brad Warren
72637b2cf6 Add util.check_call 2017-08-28 15:20:22 -07:00
Brad Warren
d0ea5958f9 Protect _set_config_var 2017-08-28 14:54:53 -07:00
Brad Warren
4805fb4b88 Add deploy_cert 2017-08-28 14:52:05 -07:00
Brad Warren
11b820c0e4 add set_config_var 2017-08-28 14:41:07 -07:00
Brad Warren
b21b66c0c0 Test restart command. 2017-08-25 14:29:50 -07:00
Brad Warren
218e15c9d4 Make restart() more robust. 2017-08-25 14:25:22 -07:00
Brad Warren
a6c08a2e25 Update prepare docstring. 2017-08-25 14:03:29 -07:00
Brad Warren
68dc678eed Call config_test in prepare 2017-08-25 14:02:57 -07:00
Brad Warren
5f3be9b1cf test config_test failure 2017-08-25 13:58:45 -07:00
Brad Warren
2ae187b1d6 Update config_test method 2017-08-25 13:55:21 -07:00
Brad Warren
b4b5c44750 Check postfix executable is found. 2017-08-25 13:38:07 -07:00
Brad Warren
a29a99fb6f Add --postfix-ctl flag. 2017-08-25 13:34:03 -07:00
Brad Warren
0f4c5c2305 Use common installer base 2017-08-25 11:41:30 -07:00
Brad Warren
60c6cc5f2a Write enhance() 2017-08-24 15:26:52 -07:00
Brad Warren
00e28592b6 Add supported_enhancements 2017-08-24 15:18:40 -07:00
Brad Warren
b92df1b71c Remove unused find_postfix_cf 2017-08-24 15:15:41 -07:00
Brad Warren
baf0d3343a Remove postfix version notes 2017-08-24 15:13:02 -07:00
Brad Warren
cc3896d5d4 Add test_lock_error 2017-08-24 15:12:11 -07:00
Brad Warren
b94e268f83 Remove unused certs_only_config 2017-08-24 15:05:39 -07:00
Brad Warren
b9177948d3 Remove _write_config 2017-08-24 15:05:24 -07:00
Brad Warren
3b2e9e49be Remove unneeded instance variables 2017-08-24 15:04:36 -07:00
Brad Warren
0efc02d6ee Lock the Postfix config dir 2017-08-24 15:03:11 -07:00
Brad Warren
967a1830e6 Rewrite get_all_names 2017-08-24 14:59:25 -07:00
Brad Warren
90ffe2aac0 Remove legacy get_all_certs_and_keys() method 2017-08-24 09:04:57 -07:00
Brad Warren
b342f40c2b remove old comments 2017-08-23 15:16:36 -07:00
Brad Warren
83e37acc8b group IPlugin methods 2017-08-23 15:16:07 -07:00
Brad Warren
d1f3a2deef move _get_version 2017-08-23 15:13:06 -07:00
Brad Warren
c9813a44d7 protect get_version() 2017-08-10 16:42:48 -07:00
Brad Warren
b98f541b91 clean up prepare() 2017-08-10 16:41:59 -07:00
Brad Warren
50a1f6340f Add _check_version. 2017-08-10 16:38:27 -07:00
Brad Warren
8c4ff5cb63 Use context manager to read conf file. 2017-08-10 16:36:08 -07:00
Brad Warren
290f5b8ce7 add test_set_config_dir 2017-08-10 16:35:21 -07:00
Brad Warren
48c5731a6b Write out temp config instead of mocking. 2017-08-10 16:29:08 -07:00
Brad Warren
749f758adb use a temporary directory 2017-08-10 16:25:30 -07:00
Brad Warren
2e8a8dfed5 add _set_config_dir 2017-08-10 16:17:46 -07:00
Brad Warren
25d1f6ec75 Test all branches of test_get_config_var 2017-08-10 16:11:09 -07:00
Brad Warren
4c4b63437f Test building of get_config_var command 2017-08-10 15:28:17 -07:00
Brad Warren
02c7eca6da Rename test classes and methods. 2017-08-10 15:08:41 -07:00
Brad Warren
b72dfc0c08 Add get_config_var 2017-08-10 15:07:32 -07:00
Brad Warren
7334fc3066 Rename postfix_dir to config_dir 2017-08-10 14:29:56 -07:00
Brad Warren
4e5740615c Use util.check_output in Postfix installer 2017-08-10 14:25:08 -07:00
Brad Warren
5a1d031f07 Rename util to certbot_util 2017-08-10 14:19:44 -07:00
Brad Warren
4715b2b12c Further document check_output 2017-08-10 14:18:27 -07:00
Brad Warren
5beaae3b65 Add check_output function and tests. 2017-08-10 14:17:13 -07:00
Brad Warren
dfd1cceb9b Test prepare() failure due to missing postconf 2017-08-10 12:26:47 -07:00
Brad Warren
192f0f60da test add_parser_arguments 2017-08-10 12:19:59 -07:00
Brad Warren
b395b72d1b Don't hardcode postconf path. 2017-08-10 11:51:01 -07:00
Brad Warren
a2dbf2fe4c Fix spacing 2017-08-10 11:20:21 -07:00
Brad Warren
86fe5ad362 Move calls to postconf to prepare(). 2017-08-10 11:16:46 -07:00
Brad Warren
89ae874f89 (temporarily) remove ca-certificates logic 2017-08-10 11:03:47 -07:00
Brad Warren
2a217189a6 (temporarily) remove policy_file 2017-08-10 10:58:27 -07:00
Brad Warren
481fb8413b Fix Postfix Installer __init__() 2017-08-10 08:50:08 -07:00
Brad Warren
d97a15861b Add --postfix-config-dir argument 2017-08-09 16:06:06 -07:00
Brad Warren
a15fe57225 remove policy config param 2017-08-09 15:57:19 -07:00
Brad Warren
6c5a8423b8 Remove unused logger from tests 2017-08-04 10:28:52 -07:00
Brad Warren
a66500ea38 Remove unused version argument. 2017-08-04 10:25:09 -07:00
Brad Warren
49cdfcec06 add six dependency 2017-08-04 10:20:36 -07:00
Brad Warren
b50a71ff4e Remove fopen argument in favor of mock.
This simplifies the actual production code and is a more standard approach in
Python.
2017-08-04 10:19:46 -07:00
Brad Warren
b37be61807 Import installer module directly in tests. 2017-08-04 10:12:54 -07:00
Brad Warren
4a3fd19c93 Move parse_line to the end of installer.py 2017-08-04 10:00:04 -07:00
Brad Warren
66ba0b5276 Remove invalid permissions exception.
Once things like locks are added, this error shouldn't be possible as it will
have occurred earlier.
2017-08-04 09:57:44 -07:00
Brad Warren
61c2209110 Use Certbot error types in the Postfix Installer 2017-08-04 09:56:39 -07:00
Brad Warren
694746409f s/ExistingConfigError/MisconfigurationError 2017-08-04 09:42:26 -07:00
Brad Warren
1c258c0a2c Add basic docstrings to Installer 2017-08-04 09:31:10 -07:00
Brad Warren
6c4b3c08a7 Clean up installer imports 2017-08-04 09:30:11 -07:00
Brad Warren
c2a8ce59ae Remove code to run the installer as on its own. 2017-08-04 09:28:22 -07:00
Brad Warren
5bf4ad1f52 Rename PostfixConfigGenerator to simply Installer 2017-08-04 09:25:12 -07:00
Brad Warren
ae08dc6bea Fix Postfix installer tests 2017-08-04 09:24:04 -07:00
Brad Warren
f89051cc2a Completely implement the Certbot plugin interfaces 2017-08-04 09:18:51 -07:00
Brad Warren
74b22a596e Ignore egg-info dirs 2017-08-04 09:03:30 -07:00
Brad Warren
e2d95b3719 Create packaging around PostfixConfigGenerator. 2017-08-04 09:02:56 -07:00
Aaron Zauner
dca274085d Merge pull request #38 from ekohl/patch-1
Correct markdown link syntax
2017-05-13 07:59:15 +02:00
Ewoud Kohl van Wijngaarden
619e273ae5 Correct markdown link syntax 2017-05-10 15:44:55 +02:00
Peter Eckersley
baa563f359 Merge pull request #30 from EFForg/azet/readme-fixup
README fixup
2016-05-12 19:12:50 -07:00
Aaron Zauner
64f2ddfa80 Merge pull request #31 from EFForg/dmwilcox/start-le-api
Dmwilcox/start le api
2016-05-03 13:45:42 +07:00
Daniel Wilcox
a5f23b5314 Configure logger to be a touch louder... than silent 2016-04-28 17:10:21 -07:00
Daniel Wilcox
af38c30c9c Fix path to postfix config variable. 2016-04-28 17:02:29 -07:00
Daniel Wilcox
887871833d Fix typo in changing quotes. 2016-04-28 16:44:25 -07:00
Daniel Wilcox
5d07b70269 Change over to using logging module from print statements. 2016-04-28 16:40:06 -07:00
Daniel Wilcox
c43602c908 Add simple config_test implementation. 2016-04-28 16:30:08 -07:00
Daniel Wilcox
4d24eb83a8 Move version fetching into get_version and implement more_info method. 2016-04-28 16:11:37 -07:00
Daniel Wilcox
7edceec8ac Add test case and fix to properly handle configs with no smtpd_tls_* vars. 2016-04-28 15:27:11 -07:00
Daniel Wilcox
c6baa82ee4 Implement basic get_all_certs_keys, tests pass. 2016-04-28 15:14:06 -07:00
Daniel Wilcox
e75bafa439 Add basic test for get_all_certs_keys IInstaller interface method. 2016-04-28 12:27:49 -07:00
Daniel Wilcox
cc83e9ba52 Wrap some lines, new style exceptions, return check for restart. 2016-04-28 12:26:56 -07:00
Peter Eckersley
5d355044c4 Merge pull request #28 from EFForg/azet/disable-v2-v3-fixup
set _all_ client&server options to exclude v2 and v3 #24
2016-04-25 23:18:05 +10:00
Aaron Zauner
1f95ac9640 README fixup pt. 1 2016-04-20 12:43:51 +07:00
Daniel Wilcox
5928fae89e Merge branch 'master' of github.com:dmwilcox/starttls-everywhere into hackathon 2016-03-29 15:04:23 -07:00
Daniel Wilcox
0bf2537a55 Add initial gitignore. 2016-03-29 14:32:53 -07:00
Daniel Wilcox
fd1cef3fa0 Implement get_all_names. 2016-03-29 14:31:27 -07:00
Daniel Wilcox
fee9c86233 Add failing test for get_all_names. 2016-03-29 14:20:33 -07:00
Daniel Wilcox
5cc317408c Move attributes into init and allow for injecting file contents for testing. 2016-03-29 14:19:13 -07:00
Aaron Zauner
2900d5122c set _all_ client&server options to exclude v2 and v3 #24 2016-03-29 19:31:00 +02:00
Peter Eckersley
c7a8d1cb7a Merge pull request #27 from EFForg/azet/disable-v2-v3
disable SSLv2,3 client-side too #24
2016-03-29 10:12:48 -07:00
Aaron Zauner
0ba508ee2d disable SSLv2,3 client-side too #24 2016-03-29 19:02:00 +02:00
Aaron Zauner
1d37e94e17 disable SSLv3 and v3 by default #24 2016-03-29 18:43:08 +02:00
Peter Eckersley
843e156a51 Merge pull request #26 from EFForg/log-summary
Improve log summary code
2016-03-29 09:23:43 -07:00
Peter Eckersley
ce88098ba6 Merge remote-tracking branch 'origin/master' 2016-03-29 09:21:15 -07:00
Aaron Zauner
1cde7f9b54 added doc. on postfix version dependent features 2016-03-29 16:22:19 +02:00
Aaron Zauner
e42a222c5d preliminary Postfix version check 2016-03-29 15:44:51 +02:00
Peter Eckersley
87022782fb Catch stray missing line 2016-03-09 10:59:10 -08:00
Peter Eckersley
a58c652443 ConfigParser is gone! 2016-03-09 10:54:15 -08:00
Peter Eckersley
0ce1684ba6 Changes to MTAConfigGenerator needed to be moved by hand 2016-03-09 10:50:15 -08:00
Peter Eckersley
af1e94be5a Merge branch 'log-summary'
Including adding dmwilcox's mandatory policy argument to jsha's argparse code
2016-03-09 10:05:43 -08:00
Peter Eckersley
e88eac65da [documentation] Add links to LEPC interface sources 2016-03-02 10:47:59 -08:00
Peter Eckersley
874c59012a Update README 2016-03-02 10:47:52 -08:00
Peter Eckersley
b2977ad6a9 Documentation details 2016-03-01 19:06:02 -08:00
Peter Eckersley
a435036a1e Document MVP objectives 2016-03-01 19:03:47 -08:00
Peter Eckersley
3a8e1d7a70 Further status update 2016-03-01 16:40:03 -08:00
Peter Eckersley
1210c04f14 tweak README 2016-03-01 16:38:55 -08:00
Peter Eckersley
6e2b6a0817 Update README 2016-03-01 16:37:56 -08:00
Peter Eckersley
9a122626b9 Merge pull request #23 from EFForg/delimiters
Fix postfix TLS policy map delimeters
2016-03-01 16:23:10 -08:00
Peter Eckersley
d0629e62ea Merge remote-tracking branch 'origin/master' into delimiters 2016-02-24 19:08:37 -08:00
Peter Eckersley
b8ce13f96a Merge pull request #21 from EFForg/master
Restructured project directory, unified scripts, moved to subdirs
2016-02-24 19:06:06 -08:00
Peter Eckersley
6db2858825 Correct policy map delimitation 2016-02-24 18:19:47 -08:00
Aaron Zauner
4d14423a21 re-structured project folder..
* Removed `ConfigParser.py` (ACK by Daniel).
* Removed MTAConfigGenerator-stub and renamed to `PostfixConfigGenerator.py`
* Moved all text/csv processing scripts to `tools/`.
* Moved all configuration files into dedicated `examples/` directory.
* unified all shebangs to `#!/usr/bin/env python` (default system python).
* Moved domain CSV and text-files to `share/`.
2016-02-24 23:35:52 +01:00
Peter Eckersley
959e943de8 Merge remote-tracking branch 'github/master' 2016-02-18 18:58:45 -08:00
Peter Eckersley
9e42f6ed08 Clarify project status 2016-02-18 18:58:34 -08:00
dmwilcox
bd20b50879 Merge pull request #19 from EFForg/hackathon
We now install certs!
2016-02-17 12:34:23 -08:00
Peter Eckersley
28bb0eb6ac Obtain acceptable_mxs the right way 2016-02-17 12:20:26 -08:00
Peter Eckersley
074fef773b Make up a domain 2016-02-17 12:14:24 -08:00
Peter Eckersley
3aeb62cf7e bugfixes, cleanups 2016-02-17 12:13:28 -08:00
Peter Eckersley
8f5b8558d2 Actually deploy a cert?
- Also add missing selves to interface methods
2016-02-17 12:09:25 -08:00
Peter Eckersley
47a5b7e3ba Start implementing cert installation 2016-02-17 11:56:41 -08:00
Peter Eckersley
cdc8b94823 Merge remote-tracking branch 'dmwilcox/master' into hackathon 2016-02-17 11:38:06 -08:00
dmwilcox
fedf970284 Fix bad import to be import *as*... as it should be. 2016-02-17 11:37:28 -08:00
Peter Eckersley
965027ce52 Start metamorphising to use LE's IInstaller interface 2016-02-17 11:36:38 -08:00
dmwilcox
1cde095dc2 Merge pull request #18 from EFForg/hackathon
hackathon work
2016-02-17 11:22:18 -08:00
Peter Eckersley
39a01190d5 Use 4 space indents
For greater compatibility with LE codebases
2016-02-17 11:19:02 -08:00
Peter Eckersley
9539f21390 Merge pull request #16 from dmwilcox/master
New Config container -- drop-in replacement
2016-02-17 10:34:14 -08:00
dmwilcox
9abef4c0bd Log MX records that will not be configured. 2016-02-17 10:20:56 -08:00
dmwilcox
146fce3878 Add comment about magic hat trick with class properties. 2016-02-17 09:51:37 -08:00
dmwilcox
904dc11b03 Add docstrings for Config objects update/merge methods. 2016-02-17 09:45:37 -08:00
dmwilcox
7c6c3efb0f Confirmed Postfix log parsing is working again. 2016-01-21 01:46:30 -08:00
dmwilcox
c87b5d6a78 Hook the MTA config generation into the new config container. 2016-01-21 00:56:29 -08:00
pypoet
9a71b18b85 Fix updates and merges, add testing to make sure they stay fixed. 2015-10-23 18:26:26 -07:00
pypoet
6da5de6b19 Beginnings of generic config composibility in place. 2015-10-16 00:57:42 -04:00
pypoet
147f58bdbc Rounds out missing features and is now on par with ConfigParser.py.
Still missing logging, composibility and a couple of attributes.
2015-10-14 02:49:34 -04:00
pypoet
fe17c873c0 Initial re-vamp of the Config object to centralize validation and
lay the basis for making compositions of configs and overrides.
Lots of TODOs, be warned.
2015-10-13 17:09:03 -04:00
Jacob Hoffman-Andrews
613a8f5e88 Improve cronjob operation to only process diffs. 2014-10-20 16:15:13 -04:00
Jacob Hoffman-Andrews
f04e8259a9 Protocols separated by colons, not commas. 2014-10-20 09:29:22 -04:00
Jacob Hoffman-Andrews
828c00b758 Find deferred lines in log summary.
Also add a cron mode that only emits output if there's something wrong.
2014-10-17 15:28:49 -04:00
Jacob Hoffman-Andrews
1d47acddfd Remove extraneous print of config. 2014-10-17 15:28:44 -04:00
Jacob Hoffman-Andrews
726afb8b95 Install CAfile on config generation. 2014-10-17 15:28:32 -04:00
Jacob Hoffman-Andrews
a1d016d031 Add motivating examples to README 2014-10-13 15:57:59 -04:00
jsha
91f6be6b3b Merge pull request #6 from jsha/move-collection
Move collection
2014-10-08 16:36:19 -04:00
Peter Eckersley
97cce82e5a Merge pull request #7 from jsha/min-tls-version
Treat min-tls-version as a minimum.
2014-10-08 11:32:54 -07:00
Jacob Hoffman-Andrews
622fc72dc1 Treat min-tls-version as a minimum.
Fixes #5.
2014-09-10 17:36:46 -04:00
Jacob Hoffman-Andrews
42c63cb6dd More informative message for RDNS fail. 2014-09-08 16:25:40 -04:00
Jacob Hoffman-Andrews
31e320d0a7 Collect certs in a subdir. 2014-08-13 15:59:50 -04:00
Jacob Hoffman-Andrews
7f9dadd681 Merge branch 'master' of github.com:EFForg/starttls-everywhere 2014-08-13 10:50:18 -04:00
Jacob Hoffman-Andrews
ad40618897 Respond to pde's comments 2014-08-13 10:49:50 -04:00
jsha
e0edc1b7ec Add link to mailing list. 2014-08-12 12:18:32 -04:00
Peter Eckersley
78a55c3823 Question about postfix logparsing output 2014-08-08 13:16:40 -07:00
Peter Eckersley
9cafcf1caf Comment regexp 2014-08-08 13:15:15 -07:00
Peter Eckersley
8c6d28ce95 Comment with some sample postfix log lines
(both those we support already, and those we may want to in the future...)
2014-08-08 13:01:33 -07:00
Peter Eckersley
30ba7e9305 Deduplicate stray comment line 2014-08-08 13:01:04 -07:00
Peter Eckersley
a6700e3172 Merge branch 'misc' of ssh://github.com/EFForg/starttls-everywhere into misc 2014-08-08 11:58:26 -07:00
Peter Eckersley
0bd8134e5f Comments
(and code review in comment form)
2014-08-08 11:57:01 -07:00
Peter Eckersley
ff5810d78f Don't accept files on the command line that don't do anything 2014-08-08 11:36:04 -07:00
Peter Eckersley
21ff3acf93 Set/list comprehensions are a bit more readable than lambdas 2014-08-08 11:26:59 -07:00
Jacob Hoffman-Andrews
cebc6f9a20 ProcessGoogleSTARTTLSDomains -> latest CSV format.
Also output in a more useful format, require >= 99% encrypted output in the CSV,
hande .{...} domains, and manually add gmail.com.
2014-08-08 13:28:01 -04:00
Jacob Hoffman-Andrews
749c4e39e0 Update meta-config with latest domains. 2014-08-07 17:34:40 -04:00
Jacob Hoffman-Andrews
6a1aa8e6b6 Update to latest config format. 2014-08-07 17:34:21 -04:00
Jacob Hoffman-Andrews
bdd4d01dc7 Update and sort golden-domains.txt, commit google-starttls-domains.csv 2014-08-07 16:57:51 -04:00
Jacob Hoffman-Andrews
6a40e1964b Notice if there are no "Trusted" entries. 2014-08-07 15:05:10 -04:00
Jacob Hoffman-Andrews
82ef8b185a Merge branch 'master' of github.com:EFForg/starttls-everywhere into misc
Conflicts:
	MTAConfigGenerator.py
	starttls-everywhere.json
2014-08-06 18:26:31 -04:00
Jacob Hoffman-Andrews
dd4f9d35ae Improve checker and starttls-everywhere.json.
Now we alphabetize keys on output for more useful diffs.
2014-08-06 18:23:13 -04:00
Jacob Hoffman-Andrews
127d49e837 Manually add a couple known-good domains.
These were skipped because in the Google data they are represented as,
e.g. 'gmail.{..}'.
2014-08-06 18:22:32 -04:00
Jacob Hoffman-Andrews
a85fad98c0 Restore default config 2014-08-06 18:22:08 -04:00
Jacob Hoffman-Andrews
1c3c69aaad Allow parameters to MTAConfigGenerator. 2014-08-06 17:08:49 -04:00
Jacob Hoffman-Andrews
3de8c2a651 Do a better job finding address domain from mx doman. 2014-08-06 17:08:30 -04:00
Peter Eckersley
803c39e585 Merge remote-tracking branch 'github/master'
Conflicts:
	ConfigParser.py
2014-06-19 09:11:15 -07:00
Jacob Hoffman-Andrews
51980e212f First pass at logs analysis 2014-06-18 17:50:41 -04:00
Peter Eckersley
3df343495e Various fixups 2014-06-18 10:58:53 -07:00
Peter Eckersley
45aeb5b003 Merge remote-tracking branch 'github/master'
Conflicts:
	ConfigParser.py
	starttls-everywhere.json
2014-06-18 10:56:48 -07:00
Jacob Hoffman-Andrews
67ee3b0488 Config format change - don't use * as it's misleading. 2014-06-18 12:32:17 -04:00
Peter Eckersley
2eba47a716 Verify more of the policy language 2014-06-16 14:23:51 -07:00
Jacob Hoffman-Andrews
51f90ffafb Write policies based on address domain, not stripped mx-domain 2014-06-16 18:26:56 +00:00
Peter Eckersley
8d6b6c358a Merge branch 'master' of ssh://github.com/jsha/starttls-everywhere
Conflicts:
	MTAConfigGenerator.py
2014-06-16 02:47:15 -07:00
Peter Eckersley
9da4f93ae5 Changes for live demo 2014-06-16 02:46:46 -07:00
Jacob Hoffman-Andrews
3cf61a54b7 Add alternatives section 2014-06-13 13:57:19 -04:00
Jacob Hoffman-Andrews
43d457aa77 Typo cleanup in MTAConfigGenerator 2014-06-12 13:18:20 -04:00
Jacob Hoffman-Andrews
499f6c2fad Add comment to ProcessgoogleSTARTTLSDomains.py 2014-06-12 11:53:02 -04:00
Jacob Hoffman-Andrews
9cd71642fb Fix italicization boundaries 2014-06-12 11:50:39 -04:00
Jacob Hoffman-Andrews
7a7329fa19 Merge remote-tracking branch 'pde/master' 2014-06-12 11:49:09 -04:00
Jacob Hoffman-Andrews
9ce047980a Add .gitignore 2014-06-12 11:40:17 -04:00
Peter Eckersley
02abaf57bd Remove stray conf entry from README 2014-06-12 05:24:51 -07:00
Peter Eckersley
3d7b53daf1 Tune verbosity; reload postfix conf if we can 2014-06-12 05:24:05 -07:00
Peter Eckersley
e99abfacfd Include the demo example with the real stuff, temporarily 2014-06-12 05:22:30 -07:00
Peter Eckersley
a2ee328bc0 Paramaterise "/etc/postfix" 2014-06-11 10:32:52 -07:00
Peter Eckersley
34cba3accf Now successfully parsing the larger policy set 2014-06-11 09:51:56 -07:00
Peter Eckersley
3712a45399 Further (and different, and better) standardisation 2014-06-11 09:48:43 -07:00
Peter Eckersley
182e9b29e4 Trying to standardize JSON terms 2014-06-11 09:42:17 -07:00
Peter Eckersley
2540f1f1e8 Writing to the domain-wise policy file actually works now. 2014-06-11 09:31:41 -07:00
Peter Eckersley
eea1b0d8c5 Switch naming conventions so that modules are importable :)
It turns out that python won't import modules with hyphens in their names.
It seems that CamelCase is most consistent with our Class naming.  However
feel free to do something different instead :)
2014-06-11 09:18:56 -07:00
Peter Eckersley
6e1bcfdb2a WIP implementing domain-wise TLS policies 2014-06-11 09:17:50 -07:00
Peter Eckersley
d7e4d93190 Merge branch 'master' of ssh://github.com/jsha/starttls-everywhere 2014-06-11 09:13:01 -07:00
Jacob Hoffman-Andrews
0c4e332811 Set up test CA and valid signed cert by that CA.
Also require valid cert for host 'valid'.
2014-06-11 11:45:28 -04:00
Peter Eckersley
46ce09d36d MTA config wrangling seems to work 2014-06-11 05:01:46 -07:00
Peter Eckersley
269f15f9ee Merge branch 'master' of ssh://github.com/jsha/starttls-everywhere 2014-06-11 00:38:40 -07:00
Jacob Hoffman-Andrews
21e841fd13 Move synced folders into a common one.
Also, create certificates.
2014-06-10 15:08:52 -04:00
Peter Eckersley
a03db04ff4 WIP implementing deletion of existing cf lines 2014-06-10 08:08:40 -07:00
Peter Eckersley
d0bcc13059 Break ground on an postfix config wrangling engine 2014-06-10 08:08:17 -07:00
Jacob Hoffman-Andrews
17425e7337 Merge branch 'master' of github.com:jsha/starttls-everywhere
Conflicts:
	README.md
2014-06-09 14:56:34 -07:00
jsha
7bdb63376c Merge pull request #2 from pde/master
Initial checkin of config parser, some changes to the config format.
2014-06-09 14:53:23 -07:00
Jacob Hoffman-Andrews
bdbc46fc84 Add candidate starttls-everywhere config json 2014-06-09 13:10:43 -07:00
Jacob Hoffman-Andrews
0d43d2988a Update check-starttls.py to generate starttls everywhere config. 2014-06-09 13:08:01 -07:00
Jacob Hoffman-Andrews
79924108c7 Reorder JSON file to emphasize MX policies over address-domain -> MX domain mapping. 2014-06-09 10:12:09 -07:00
Peter Eckersley
c033905b16 Now validating most of the config json 2014-06-08 06:22:32 -07:00
Peter Eckersley
839c523048 Fix typos 2014-06-08 06:22:22 -07:00
Peter Eckersley
7c81f23a07 Merge branch 'master' of ssh://github.com/jsha/starttls-everywhere 2014-06-07 06:50:21 -07:00
Jacob Hoffman-Andrews
e534a43d1a Make sender actually attempt TLS on outbound connections. 2014-06-06 16:07:38 -07:00
Peter Eckersley
fcd1a98201 Break ground on a config parser 2014-06-06 15:54:33 -07:00
Peter Eckersley
dc606eac7d Some tweaks to the config format 2014-06-06 15:54:22 -07:00
Jacob Hoffman-Andrews
372c96d9fd Update Postfix configuration and mail-send-loop 2014-06-06 14:06:08 -07:00
Jacob Hoffman-Andrews
5a9f90dc30 Merge branch 'master' of github.com:jsha/starttls-everywhere 2014-06-06 14:05:03 -07:00
Jacob Hoffman-Andrews
ce0a6a1814 Add example config.json 2014-06-06 14:04:38 -07:00
jsha
fa5acdf674 Simplify SPKI hash usage 2014-06-06 13:44:03 -07:00
jsha
31db3b7034 Merge pull request #1 from pde/master
Add specific postfix configs
2014-06-06 12:59:41 -07:00
Peter Eckersley
30938260d4 Split postfix configs across the VMs, and start making them do things 2014-06-05 19:45:21 -07:00
Peter Eckersley
7bd06a4d35 Work in progress 2014-06-05 17:04:28 -07:00
Peter Eckersley
ace9d2383d Start work on forcing TLS to valid 2014-06-05 16:51:38 -07:00
Jacob Hoffman-Andrews
ed0c024209 Improved provisioning and certificate checking. 2014-06-05 16:01:07 -07:00
Jacob Hoffman-Andrews
714cb17dcb Clarify example usage of pinsets by including a CA 2014-06-05 14:26:45 -07:00
Jacob Hoffman-Andrews
6fb51d5422 Example shouldn't include hashes from Chrome source. 2014-06-05 14:11:08 -07:00
jsha
4b5b9f164f nexthop-domains -> address-domains 2014-06-05 11:51:19 -07:00
jsha
3d9d5607bd Formatting issue #2 in design doc 2014-06-05 11:34:12 -07:00
jsha
aa417eec15 Formatting issue in design doc 2014-06-05 11:10:07 -07:00
jsha
844ec79f01 Add design doc 2014-06-05 11:05:08 -07:00
Jacob Hoffman-Andrews
f0b9ef2716 Add a Vagrantfile and the list of golden domains. 2014-06-05 10:43:01 -07:00
Jacob Hoffman-Andrews
8857302347 check-starttls and process-google-starttls-domains.py 2014-06-04 14:41:18 -07:00
285 changed files with 13058 additions and 2738 deletions

4
.gitignore vendored
View File

@@ -38,3 +38,7 @@ tests/letstest/venv/
# pytest cache
.cache
.mypy_cache/
# docker files
.docker

View File

@@ -41,7 +41,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,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
disable=fixme,locally-disabled,locally-enabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
# abstract-class-not-used cannot be disabled locally (at least in
# pylint 1.4.1), same for abstract-class-little-used

View File

@@ -13,7 +13,11 @@ before_script:
matrix:
include:
- python: "2.7"
env: TOXENV=py27_install BOULDER_INTEGRATION=1
env: TOXENV=py27_install BOULDER_INTEGRATION=v1
sudo: required
services: docker
- python: "2.7"
env: TOXENV=py27_install BOULDER_INTEGRATION=v2
sudo: required
services: docker
- python: "2.7"
@@ -25,16 +29,16 @@ matrix:
addons:
- python: "2.7"
env: TOXENV=lint
- python: "2.6"
env: TOXENV=py26
sudo: required
services: docker
- python: "3.4"
env: TOXENV=mypy
- python: "3.5"
env: TOXENV=mypy
- python: "2.7"
env: TOXENV=py27-oldest
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
sudo: required
services: docker
- python: "3.3"
env: TOXENV=py33
- python: "3.4"
env: TOXENV=py34
sudo: required
services: docker
- python: "3.6"
@@ -103,6 +107,7 @@ notifications:
irc:
channels:
- secure: "SGWZl3ownKx9xKVV2VnGt7DqkTmutJ89oJV9tjKhSs84kLijU6EYdPnllqISpfHMTxXflNZuxtGo0wTDYHXBuZL47w1O32W6nzuXdra5zC+i4sYQwYULUsyfOv9gJX8zWAULiK0Z3r0oho45U+FR5ZN6TPCidi8/eGU+EEPwaAw="
on_cancel: never
on_success: never
on_failure: always
use_notice: true

View File

@@ -2,6 +2,345 @@
Certbot adheres to [Semantic Versioning](http://semver.org/).
## 0.25.1 - 2018-06-13
### Fixed
* TLS-ALPN-01 support has been removed from our acme library. Using our current
dependencies, we are unable to provide a correct implementation of this
challenge so we decided to remove it from the library until we can provide
proper support.
* Issues causing test failures when running the tests in the acme package with
pytest<3.0 has been resolved.
* certbot-nginx now correctly depends on acme>=0.25.0.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with changes other than their version number were:
* acme
* certbot-nginx
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/56?closed=1
## 0.25.0 - 2018-06-06
### Added
* Support for the ready status type was added to acme. Without this change,
Certbot and acme users will begin encountering errors when using Let's
Encrypt's ACMEv2 API starting on June 19th for the staging environment and
July 5th for production. See
https://community.letsencrypt.org/t/acmev2-order-ready-status/62866 for more
information.
* Certbot now accepts the flag --reuse-key which will cause the same key to be
used in the certificate when the lineage is renewed rather than generating a
new key.
* You can now add multiple email addresses to your ACME account with Certbot by
providing a comma separated list of emails to the --email flag.
* Support for Let's Encrypt's upcoming TLS-ALPN-01 challenge was added to acme.
For more information, see
https://community.letsencrypt.org/t/tls-alpn-validation-method/63814/1.
* acme now supports specifying the source address to bind to when sending
outgoing connections. You still cannot specify this address using Certbot.
* If you run Certbot against Let's Encrypt's ACMEv2 staging server but don't
already have an account registered at that server URL, Certbot will
automatically reuse your staging account from Let's Encrypt's ACMEv1 endpoint
if it exists.
* Interfaces were added to Certbot allowing plugins to be called at additional
points. The `GenericUpdater` interface allows plugins to perform actions
every time `certbot renew` is run, regardless of whether any certificates are
due for renewal, and the `RenewDeployer` interface allows plugins to perform
actions when a certificate is renewed. See `certbot.interfaces` for more
information.
### Changed
* When running Certbot with --dry-run and you don't already have a staging
account, the created account does not contain an email address even if one
was provided to avoid expiration emails from Let's Encrypt's staging server.
* certbot-nginx does a better job of automatically detecting the location of
Nginx's configuration files when run on BSD based systems.
* acme now requires and uses pytest when running tests with setuptools with
`python setup.py test`.
* `certbot config_changes` no longer waits for user input before exiting.
### Fixed
* Misleading log output that caused users to think that Certbot's standalone
plugin failed to bind to a port when performing a challenge has been
corrected.
* An issue where certbot-nginx would fail to enable HSTS if the server block
already had an `add_header` directive has been resolved.
* certbot-nginx now does a better job detecting the server block to base the
configuration for TLS-SNI challenges on.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with functional changes were:
* acme
* certbot
* certbot-apache
* certbot-nginx
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/54?closed=1
## 0.24.0 - 2018-05-02
### Added
* certbot now has an enhance subcommand which allows you to configure security
enhancements like HTTP to HTTPS redirects, OCSP stapling, and HSTS without
reinstalling a certificate.
* certbot-dns-rfc2136 now allows the user to specify the port to use to reach
the DNS server in its credentials file.
* acme now parses the wildcard field included in authorizations so it can be
used by users of the library.
### Changed
* certbot-dns-route53 used to wait for each DNS update to propagate before
sending the next one, but now it sends all updates before waiting which
speeds up issuance for multiple domains dramatically.
* Certbot's official Docker images are now based on Alpine Linux 3.7 rather
than 3.4 because 3.4 has reached its end-of-life.
* We've doubled the time Certbot will spend polling authorizations before
timing out.
* The level of the message logged when Certbot is being used with
non-standard paths warning that crontabs for renewal included in Certbot
packages from OS package managers may not work has been reduced. This stops
the message from being written to stderr every time `certbot renew` runs.
### Fixed
* certbot-auto now works with Python 3.6.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with changes other than their version number were:
* acme
* certbot
* certbot-apache
* certbot-dns-digitalocean (only style improvements to tests)
* certbot-dns-rfc2136
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/52?closed=1
## 0.23.0 - 2018-04-04
### Added
* Support for OpenResty was added to the Nginx plugin.
### Changed
* The timestamps in Certbot's logfiles now use the system's local time zone
rather than UTC.
* Certbot's DNS plugins that use Lexicon now rely on Lexicon>=2.2.1 to be able
to create and delete multiple TXT records on a single domain.
* certbot-dns-google's test suite now works without an internet connection.
### Fixed
* Removed a small window that if during which an error occurred, Certbot
wouldn't clean up performed challenges.
* The parameters `default` and `ipv6only` are now removed from `listen`
directives when creating a new server block in the Nginx plugin.
* `server_name` directives enclosed in quotation marks in Nginx are now properly
supported.
* Resolved an issue preventing the Apache plugin from starting Apache when it's
not currently running on RHEL and Gentoo based systems.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with changes other than their version number were:
* certbot
* certbot-apache
* certbot-dns-cloudxns
* certbot-dns-dnsimple
* certbot-dns-dnsmadeeasy
* certbot-dns-google
* certbot-dns-luadns
* certbot-dns-nsone
* certbot-dns-rfc2136
* certbot-nginx
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/50?closed=1
## 0.22.2 - 2018-03-19
### Fixed
* A type error introduced in 0.22.1 that would occur during challenge cleanup
when a Certbot plugin raises an exception while trying to complete the
challenge was fixed.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with changes other than their version number were:
* certbot
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/53?closed=1
## 0.22.1 - 2018-03-19
### Changed
* The ACME server used with Certbot's --dry-run and --staging flags is now
Let's Encrypt's ACMEv2 staging server which allows people to also test ACMEv2
features with these flags.
### Fixed
* The HTTP Content-Type header is now set to the correct value during
certificate revocation with new versions of the ACME protocol.
* When using Certbot with Let's Encrypt's ACMEv2 server, it would add a blank
line to the top of chain.pem and between the certificates in fullchain.pem
for each lineage. These blank lines have been removed.
* Resolved a bug that caused Certbot's --allow-subset-of-names flag not to
work.
* Fixed a regression in acme.client.Client that caused the class to not work
when it was initialized without a ClientNetwork which is done by some of the
other projects using our ACME library.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with changes other than their version number were:
* acme
* certbot
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/51?closed=1
## 0.22.0 - 2018-03-07
### Added
* Support for obtaining wildcard certificates and a newer version of the ACME
protocol such as the one implemented by Let's Encrypt's upcoming ACMEv2
endpoint was added to Certbot and its ACME library. Certbot still works with
older ACME versions and will automatically change the version of the protocol
used based on the version the ACME CA implements.
* The Apache and Nginx plugins are now able to automatically install a wildcard
certificate to multiple virtual hosts that you select from your server
configuration.
* The `certbot install` command now accepts the `--cert-name` flag for
selecting a certificate.
* `acme.client.BackwardsCompatibleClientV2` was added to Certbot's ACME library
which automatically handles most of the differences between new and old ACME
versions. `acme.client.ClientV2` is also available for people who only want
to support one version of the protocol or want to handle the differences
between versions themselves.
* certbot-auto now supports the flag --install-only which has the script
install Certbot and its dependencies and exit without invoking Certbot.
* Support for issuing a single certificate for a wildcard and base domain was
added to our Google Cloud DNS plugin. To do this, we now require your API
credentials have additional permissions, however, your credentials will
already have these permissions unless you defined a custom role with fewer
permissions than the standard DNS administrator role provided by Google.
These permissions are also only needed for the case described above so it
will continue to work for existing users. For more information about the
permissions changes, see the documentation in the plugin.
### Changed
* We have broken lockstep between our ACME library, Certbot, and its plugins.
This means that the different components do not need to be the same version
to work together like they did previously. This makes packaging easier
because not every piece of Certbot needs to be repackaged to ship a change to
a subset of its components.
* Support for Python 2.6 and Python 3.3 has been removed from ACME, Certbot,
Certbot's plugins, and certbot-auto. If you are using certbot-auto on a RHEL
6 based system, it will walk you through the process of installing Certbot
with Python 3 and refuse to upgrade to a newer version of Certbot until you
have done so.
* Certbot's components now work with older versions of setuptools to simplify
packaging for EPEL 7.
### Fixed
* Issues caused by Certbot's Nginx plugin adding multiple ipv6only directives
has been resolved.
* A problem where Certbot's Apache plugin would add redundant include
directives for the TLS configuration managed by Certbot has been fixed.
* Certbot's webroot plugin now properly deletes any directories it creates.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/48?closed=1
## 0.21.1 - 2018-01-25
### Fixed
* When creating an HTTP to HTTPS redirect in Nginx, we now ensure the Host
header of the request is set to an expected value before redirecting users to
the domain found in the header. The previous way Certbot configured Nginx
redirects was a potential security issue which you can read more about at
https://community.letsencrypt.org/t/security-issue-with-redirects-added-by-certbots-nginx-plugin/51493.
* Fixed a problem where Certbot's Apache plugin could fail HTTP-01 challenges
if basic authentication is configured for the domain you request a
certificate for.
* certbot-auto --no-bootstrap now properly tries to use Python 3.4 on RHEL 6
based systems rather than Python 2.6.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/49?closed=1
## 0.21.0 - 2018-01-17
### Added
* Support for the HTTP-01 challenge type was added to our Apache and Nginx
plugins. For those not aware, Let's Encrypt disabled the TLS-SNI-01 challenge
type which was what was previously being used by our Apache and Nginx plugins
last week due to a security issue. For more information about Let's Encrypt's
change, click
[here](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188).
Our Apache and Nginx plugins will automatically switch to use HTTP-01 so no
changes need to be made to your Certbot configuration, however, you should
make sure your server is accessible on port 80 and isn't behind an external
proxy doing things like redirecting all traffic from HTTP to HTTPS. HTTP to
HTTPS redirects inside Apache and Nginx are fine.
* IPv6 support was added to the Nginx plugin.
* Support for automatically creating server blocks based on the default server
block was added to the Nginx plugin.
* The flags --delete-after-revoke and --no-delete-after-revoke were added
allowing users to control whether the revoke subcommand also deletes the
certificates it is revoking.
### Changed
* We deprecated support for Python 2.6 and Python 3.3 in Certbot and its ACME
library. Support for these versions of Python will be removed in the next
major release of Certbot. If you are using certbot-auto on a RHEL 6 based
system, it will guide you through the process of installing Python 3.
* We split our implementation of JOSE (Javascript Object Signing and
Encryption) out of our ACME library and into a separate package named josepy.
This package is available on [PyPI](https://pypi.python.org/pypi/josepy) and
on [GitHub](https://github.com/certbot/josepy).
* We updated the ciphersuites used in Apache to the new [values recommended by
Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29).
The major change here is adding ChaCha20 to the list of supported
ciphersuites.
### Fixed
* An issue with our Apache plugin on Gentoo due to differences in their
apache2ctl command have been resolved.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/47?closed=1
## 0.20.0 - 2017-12-06
### Added

View File

@@ -1,4 +1,4 @@
FROM python:2-alpine
FROM python:2-alpine3.7
ENTRYPOINT [ "certbot" ]
EXPOSE 80 443

View File

@@ -1,70 +1,21 @@
# 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>
FROM ubuntu:xenial
# 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 (certbot-docker wrapper that uses standalone
# authenticator and text mode only?)
VOLUME /etc/letsencrypt /var/lib/letsencrypt
# Note: this only exposes the port to other docker containers.
EXPOSE 80 443
WORKDIR /opt/certbot/src
# 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 Apache/Nginx for plugin development.
COPY letsencrypt-auto-source/letsencrypt-auto /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto
RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
apt-get install python3-dev git -y && \
COPY . .
RUN apt-get update && \
apt-get install apache2 git nginx-light -y && \
letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
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 linter_plugin.py tox.cover.sh tox.ini .pylintrc /opt/certbot/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 certbot /opt/certbot/src/certbot/
COPY acme /opt/certbot/src/acme/
COPY certbot-apache /opt/certbot/src/certbot-apache/
COPY certbot-nginx /opt/certbot/src/certbot-nginx/
COPY letshelp-certbot /opt/certbot/src/letshelp-certbot/
COPY certbot-compatibility-test /opt/certbot/src/certbot-compatibility-test/
COPY tests /opt/certbot/src/tests/
RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \
/opt/certbot/venv/bin/pip install -U pip && \
/opt/certbot/venv/bin/pip install -U setuptools && \
/opt/certbot/venv/bin/pip install \
-e /opt/certbot/src/acme \
-e /opt/certbot/src \
-e /opt/certbot/src/certbot-apache \
-e /opt/certbot/src/certbot-nginx \
-e /opt/certbot/src/letshelp-certbot \
-e /opt/certbot/src/certbot-compatibility-test \
-e /opt/certbot/src[dev,docs]
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/certbot/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.
RUN VENV_NAME="../venv" tools/venv.sh
ENV PATH /opt/certbot/venv/bin:$PATH

View File

@@ -1,3 +1,9 @@
If you're having trouble using Certbot and aren't sure you've found a bug or
request for a new feature, please first try asking for help at
https://community.letsencrypt.org/. There is a much larger community there of
people familiar with the project who will be able to more quickly answer your
questions.
## My operating system is (include version):

View File

@@ -1,5 +1,6 @@
include LICENSE.txt
include README.rst
include pytest.ini
recursive-include docs *
recursive-include examples *
recursive-include acme/testdata *

View File

@@ -10,13 +10,3 @@ supported version: `draft-ietf-acme-01`_.
https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01
"""
import sys
import warnings
for (major, minor) in [(2, 6), (3, 3)]:
if sys.version_info[:2] == (major, minor):
warnings.warn(
"Python {0}.{1} support will be dropped in the next release of "
"acme. Please upgrade your Python version.".format(major, minor),
DeprecationWarning,
) #pragma: no cover

View File

@@ -9,6 +9,7 @@ from cryptography.hazmat.primitives import hashes # type: ignore
import josepy as jose
import OpenSSL
import requests
import six
from acme import errors
from acme import crypto_util
@@ -139,16 +140,16 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
return True
@six.add_metaclass(abc.ABCMeta)
class KeyAuthorizationChallenge(_TokenChallenge):
# pylint: disable=abstract-class-little-used,too-many-ancestors
"""Challenge based on Key Authorization.
:param response_cls: Subclass of `KeyAuthorizationChallengeResponse`
that will be used to generate `response`.
:param str typ: type of the challenge
"""
__metaclass__ = abc.ABCMeta
typ = NotImplemented
response_cls = NotImplemented
thumbprint_hash_function = (
KeyAuthorizationChallengeResponse.thumbprint_hash_function)
@@ -477,7 +478,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse):
try:
cert = self.probe_cert(domain=domain, **kwargs)
except errors.Error as error:
logger.debug(error, exc_info=True)
logger.debug(str(error), exc_info=True)
return False
return self.verify_cert(cert)
@@ -506,6 +507,21 @@ class TLSSNI01(KeyAuthorizationChallenge):
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
@Challenge.register # pylint: disable=too-many-ancestors
class TLSALPN01(KeyAuthorizationChallenge):
"""ACME tls-alpn-01 challenge.
This class simply allows parsing the TLS-ALPN-01 challenge returned from
the CA. Full TLS-ALPN-01 support is not currently provided.
"""
typ = "tls-alpn-01"
def validation(self, account_key, **kwargs):
"""Generate validation for the challenge."""
raise NotImplementedError()
@Challenge.register # pylint: disable=too-many-ancestors
class DNS(_TokenChallenge):
"""ACME "dns" challenge."""

View File

@@ -393,6 +393,38 @@ class TLSSNI01Test(unittest.TestCase):
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
class TLSALPN01Test(unittest.TestCase):
def setUp(self):
from acme.challenges import TLSALPN01
self.msg = TLSALPN01(
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
self.jmsg = {
'type': 'tls-alpn-01',
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import TLSALPN01
self.assertEqual(self.msg, TLSALPN01.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import TLSALPN01
hash(TLSALPN01.from_json(self.jmsg))
def test_from_json_invalid_token_length(self):
from acme.challenges import TLSALPN01
self.jmsg['token'] = jose.encode_b64jose(b'abcd')
self.assertRaises(
jose.DeserializationError, TLSALPN01.from_json, self.jmsg)
def test_validation(self):
self.assertRaises(NotImplementedError, self.msg.validation, KEY)
class DNSTest(unittest.TestCase):
def setUp(self):

View File

@@ -9,16 +9,20 @@ import time
import six
from six.moves import http_client # pylint: disable=import-error
import josepy as jose
import OpenSSL
import re
from requests_toolbelt.adapters.source import SourceAddressAdapter
import requests
from requests.adapters import HTTPAdapter
import sys
from acme import crypto_util
from acme import errors
from acme import jws
from acme import messages
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict, List, Set, Text
logger = logging.getLogger(__name__)
@@ -39,39 +43,24 @@ DEFAULT_NETWORK_TIMEOUT = 45
DER_CONTENT_TYPE = 'application/pkix-cert'
class Client(object): # pylint: disable=too-many-instance-attributes
"""ACME client.
.. todo::
Clean up raised error types hierarchy, document, and handle (wrap)
instances of `.DeserializationError` raised in `from_json()`.
class ClientBase(object): # pylint: disable=too-many-instance-attributes
"""ACME client base object.
:ivar messages.Directory directory:
:ivar key: `.JWK` (private)
:ivar alg: `.JWASignature`
:ivar bool verify_ssl: Verify SSL certificates?
:ivar .ClientNetwork net: Client network. Useful for testing. If not
supplied, it will be initialized using `key`, `alg` and
`verify_ssl`.
:ivar .ClientNetwork net: Client network.
:ivar int acme_version: ACME protocol version. 1 or 2.
"""
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
net=None):
def __init__(self, directory, net, acme_version):
"""Initialize.
:param directory: Directory Resource (`.messages.Directory`) or
URI from which the resource will be downloaded.
:param .messages.Directory directory: Directory Resource
:param .ClientNetwork net: Client network.
:param int acme_version: ACME protocol version. 1 or 2.
"""
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
self.directory = directory
self.net = net
self.acme_version = acme_version
@classmethod
def _regr_from_response(cls, response, uri=None, terms_of_service=None):
@@ -83,28 +72,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
uri=response.headers.get('Location', uri),
terms_of_service=terms_of_service)
def register(self, new_reg=None):
"""Register.
:param .NewRegistration new_reg:
:returns: Registration Resource.
:rtype: `.RegistrationResource`
"""
new_reg = messages.NewRegistration() if new_reg is None else new_reg
assert isinstance(new_reg, messages.NewRegistration)
response = self.net.post(self.directory[new_reg], new_reg)
# TODO: handle errors
assert response.status_code == http_client.CREATED
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
return self._regr_from_response(response)
def _send_recv_regr(self, regr, body):
response = self.net.post(regr.uri, body)
response = self._post(regr.uri, body)
# TODO: Boulder returns httplib.ACCEPTED
#assert response.status_code == httplib.OK
@@ -116,6 +85,13 @@ class Client(object): # pylint: disable=too-many-instance-attributes
response, uri=regr.uri,
terms_of_service=regr.terms_of_service)
def _post(self, *args, **kwargs):
"""Wrapper around self.net.post that adds the acme_version.
"""
kwargs.setdefault('acme_version', self.acme_version)
return self.net.post(*args, **kwargs)
def update_registration(self, regr, update=None):
"""Update registration.
@@ -130,6 +106,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
update = regr.body if update is None else update
body = messages.UpdateRegistration(**dict(update))
updated_regr = self._send_recv_regr(regr, body=body)
self.net.account = updated_regr
return updated_regr
def deactivate_registration(self, regr):
@@ -153,65 +130,14 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"""
return self._send_recv_regr(regr, messages.UpdateRegistration())
def agree_to_tos(self, regr):
"""Agree to the terms-of-service.
Agree to the terms-of-service in a Registration Resource.
:param regr: Registration Resource.
:type regr: `.RegistrationResource`
:returns: Updated Registration Resource.
:rtype: `.RegistrationResource`
"""
return self.update_registration(
regr.update(body=regr.body.update(agreement=regr.terms_of_service)))
def _authzr_from_response(self, response, identifier, uri=None):
def _authzr_from_response(self, response, identifier=None, uri=None):
authzr = messages.AuthorizationResource(
body=messages.Authorization.from_json(response.json()),
uri=response.headers.get('Location', uri))
if authzr.body.identifier != identifier:
if identifier is not None and authzr.body.identifier != identifier:
raise errors.UnexpectedUpdate(authzr)
return authzr
def request_challenges(self, identifier, new_authzr_uri=None):
"""Request challenges.
:param .messages.Identifier identifier: Identifier to be challenged.
:param str new_authzr_uri: Deprecated. Do not use.
:returns: Authorization Resource.
:rtype: `.AuthorizationResource`
"""
if new_authzr_uri is not None:
logger.debug("request_challenges with new_authzr_uri deprecated.")
new_authz = messages.NewAuthorization(identifier=identifier)
response = self.net.post(self.directory.new_authz, new_authz)
# TODO: handle errors
assert response.status_code == http_client.CREATED
return self._authzr_from_response(response, identifier)
def request_domain_challenges(self, domain, new_authzr_uri=None):
"""Request challenges for domain names.
This is simply a convenience function that wraps around
`request_challenges`, but works with domain names instead of
generic identifiers. See ``request_challenges`` for more
documentation.
:param str domain: Domain name to be challenged.
:param str new_authzr_uri: Deprecated. Do not use.
:returns: Authorization Resource.
:rtype: `.AuthorizationResource`
"""
return self.request_challenges(messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri)
def answer_challenge(self, challb, response):
"""Answer challenge.
@@ -227,7 +153,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:raises .UnexpectedUpdate:
"""
response = self.net.post(challb.uri, response)
response = self._post(challb.uri, response)
try:
authzr_uri = response.links['up']['url']
except KeyError:
@@ -288,6 +214,142 @@ class Client(object): # pylint: disable=too-many-instance-attributes
response, authzr.body.identifier, authzr.uri)
return updated_authzr, response
def _revoke(self, cert, rsn, url):
"""Revoke certificate.
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509`
:param int rsn: Reason code for certificate revocation.
:param str url: ACME URL to post to
:raises .ClientError: If revocation is unsuccessful.
"""
response = self._post(url,
messages.Revocation(
certificate=cert,
reason=rsn))
if response.status_code != http_client.OK:
raise errors.ClientError(
'Successful revocation must return HTTP OK status')
class Client(ClientBase):
"""ACME client for a v1 API.
.. todo::
Clean up raised error types hierarchy, document, and handle (wrap)
instances of `.DeserializationError` raised in `from_json()`.
:ivar messages.Directory directory:
:ivar key: `josepy.JWK` (private)
:ivar alg: `josepy.JWASignature`
:ivar bool verify_ssl: Verify SSL certificates?
:ivar .ClientNetwork net: Client network. Useful for testing. If not
supplied, it will be initialized using `key`, `alg` and
`verify_ssl`.
"""
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.
"""
# pylint: disable=too-many-arguments
self.key = key
if net is None:
net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl)
if isinstance(directory, six.string_types):
directory = messages.Directory.from_json(
net.get(directory).json())
super(Client, self).__init__(directory=directory,
net=net, acme_version=1)
def register(self, new_reg=None):
"""Register.
:param .NewRegistration new_reg:
:returns: Registration Resource.
:rtype: `.RegistrationResource`
"""
new_reg = messages.NewRegistration() if new_reg is None else new_reg
response = self._post(self.directory[new_reg], new_reg)
# TODO: handle errors
assert response.status_code == http_client.CREATED
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
return self._regr_from_response(response)
def agree_to_tos(self, regr):
"""Agree to the terms-of-service.
Agree to the terms-of-service in a Registration Resource.
:param regr: Registration Resource.
:type regr: `.RegistrationResource`
:returns: Updated Registration Resource.
:rtype: `.RegistrationResource`
"""
return self.update_registration(
regr.update(body=regr.body.update(agreement=regr.terms_of_service)))
def request_challenges(self, identifier, new_authzr_uri=None):
"""Request challenges.
:param .messages.Identifier identifier: Identifier to be challenged.
:param str new_authzr_uri: Deprecated. Do not use.
:returns: Authorization Resource.
:rtype: `.AuthorizationResource`
:raises errors.WildcardUnsupportedError: if a wildcard is requested
"""
if new_authzr_uri is not None:
logger.debug("request_challenges with new_authzr_uri deprecated.")
if identifier.value.startswith("*"):
raise errors.WildcardUnsupportedError(
"Requesting an authorization for a wildcard name is"
" forbidden by this version of the ACME protocol.")
new_authz = messages.NewAuthorization(identifier=identifier)
response = self._post(self.directory.new_authz, new_authz)
# TODO: handle errors
assert response.status_code == http_client.CREATED
return self._authzr_from_response(response, identifier)
def request_domain_challenges(self, domain, new_authzr_uri=None):
"""Request challenges for domain names.
This is simply a convenience function that wraps around
`request_challenges`, but works with domain names instead of
generic identifiers. See ``request_challenges`` for more
documentation.
:param str domain: Domain name to be challenged.
:param str new_authzr_uri: Deprecated. Do not use.
:returns: Authorization Resource.
:rtype: `.AuthorizationResource`
:raises errors.WildcardUnsupportedError: if a wildcard is requested
"""
return self.request_challenges(messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri)
def request_issuance(self, csr, authzrs):
"""Request issuance.
@@ -307,7 +369,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
req = messages.CertificateRequest(csr=csr)
content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
response = self.net.post(
response = self._post(
self.directory.new_cert,
req,
content_type=content_type,
@@ -356,7 +418,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"""
# pylint: disable=too-many-locals
assert max_attempts > 0
attempts = collections.defaultdict(int)
attempts = collections.defaultdict(int) # type: Dict[messages.AuthorizationResource, int]
exhausted = set()
# priority queue with datetime.datetime (based on Retry-After) as key,
@@ -470,7 +532,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
"""
chain = []
chain = [] # type: List[jose.ComparableX509]
uri = certr.cert_chain_uri
while uri is not None and len(chain) < max_length:
response, cert = self._get_cert(uri)
@@ -492,32 +554,333 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:raises .ClientError: If revocation is unsuccessful.
"""
response = self.net.post(self.directory[messages.Revocation],
messages.Revocation(
certificate=cert,
reason=rsn),
content_type=None)
if response.status_code != http_client.OK:
raise errors.ClientError(
'Successful revocation must return HTTP OK status')
return self._revoke(cert, rsn, self.directory[messages.Revocation])
class ClientV2(ClientBase):
"""ACME client for a v2 API.
:ivar messages.Directory directory:
:ivar .ClientNetwork net: Client network.
"""
def __init__(self, directory, net):
"""Initialize.
:param .messages.Directory directory: Directory Resource
:param .ClientNetwork net: Client network.
"""
super(ClientV2, self).__init__(directory=directory,
net=net, acme_version=2)
def new_account(self, new_account):
"""Register.
:param .NewRegistration new_account:
:returns: Registration Resource.
:rtype: `.RegistrationResource`
"""
response = self._post(self.directory['newAccount'], new_account)
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
regr = self._regr_from_response(response)
self.net.account = regr
return regr
def new_order(self, csr_pem):
"""Request a new Order object from the server.
:param str csr_pem: A CSR in PEM format.
:returns: The newly created order.
:rtype: OrderResource
"""
csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# pylint: disable=protected-access
dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
identifiers = []
for name in dnsNames:
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,
value=name))
order = messages.NewOrder(identifiers=identifiers)
response = self._post(self.directory['newOrder'], order)
body = messages.Order.from_json(response.json())
authorizations = []
for url in body.authorizations:
authorizations.append(self._authzr_from_response(self.net.get(url), uri=url))
return messages.OrderResource(
body=body,
uri=response.headers.get('Location'),
authorizations=authorizations,
csr_pem=csr_pem)
def poll_and_finalize(self, orderr, deadline=None):
"""Poll authorizations and finalize the order.
If no deadline is provided, this method will timeout after 90
seconds.
:param messages.OrderResource orderr: order to finalize
:param datetime.datetime deadline: when to stop polling and timeout
:returns: finalized order
:rtype: messages.OrderResource
"""
if deadline is None:
deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)
orderr = self.poll_authorizations(orderr, deadline)
return self.finalize_order(orderr, deadline)
def poll_authorizations(self, orderr, deadline):
"""Poll Order Resource for status."""
responses = []
for url in orderr.body.authorizations:
while datetime.datetime.now() < deadline:
authzr = self._authzr_from_response(self.net.get(url), uri=url)
if authzr.body.status != messages.STATUS_PENDING:
responses.append(authzr)
break
time.sleep(1)
# If we didn't get a response for every authorization, we fell through
# the bottom of the loop due to hitting the deadline.
if len(responses) < len(orderr.body.authorizations):
raise errors.TimeoutError()
failed = []
for authzr in responses:
if authzr.body.status != messages.STATUS_VALID:
for chall in authzr.body.challenges:
if chall.error != None:
failed.append(authzr)
if len(failed) > 0:
raise errors.ValidationError(failed)
return orderr.update(authorizations=responses)
def finalize_order(self, orderr, deadline):
"""Finalize an order and obtain a certificate.
:param messages.OrderResource orderr: order to finalize
:param datetime.datetime deadline: when to stop polling and timeout
:returns: finalized order
:rtype: messages.OrderResource
"""
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem)
wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr))
self._post(orderr.body.finalize, wrapped_csr)
while datetime.datetime.now() < deadline:
time.sleep(1)
response = self.net.get(orderr.uri)
body = messages.Order.from_json(response.json())
if body.error is not None:
raise errors.IssuanceError(body.error)
if body.certificate is not None:
certificate_response = self.net.get(body.certificate,
content_type=DER_CONTENT_TYPE).text
return orderr.update(body=body, fullchain_pem=certificate_response)
raise errors.TimeoutError()
def revoke(self, cert, rsn):
"""Revoke certificate.
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509`
:param int rsn: Reason code for certificate revocation.
:raises .ClientError: If revocation is unsuccessful.
"""
return self._revoke(cert, rsn, self.directory['revokeCert'])
class BackwardsCompatibleClientV2(object):
"""ACME client wrapper that tends towards V2-style calls, but
supports V1 servers.
.. note:: While this class handles the majority of the differences
between versions of the ACME protocol, if you need to support an
ACME server based on version 3 or older of the IETF ACME draft
that uses combinations in authorizations (or lack thereof) to
signal that the client needs to complete something other than
any single challenge in the authorization to make it valid, the
user of this class needs to understand and handle these
differences themselves. This does not apply to either of Let's
Encrypt's endpoints where successfully completing any challenge
in an authorization will make it valid.
:ivar int acme_version: 1 or 2, corresponding to the Let's Encrypt endpoint
:ivar .ClientBase client: either Client or ClientV2
"""
def __init__(self, net, key, server):
directory = messages.Directory.from_json(net.get(server).json())
self.acme_version = self._acme_version_from_directory(directory)
if self.acme_version == 1:
self.client = Client(directory, key=key, net=net)
else:
self.client = ClientV2(directory, net=net)
def __getattr__(self, name):
if name in vars(self.client):
return getattr(self.client, name)
elif name in dir(ClientBase):
return getattr(self.client, name)
else:
raise AttributeError()
def new_account_and_tos(self, regr, check_tos_cb=None):
"""Combined register and agree_tos for V1, new_account for V2
:param .NewRegistration regr:
:param callable check_tos_cb: callback that raises an error if
the check does not work
"""
def _assess_tos(tos):
if check_tos_cb is not None:
check_tos_cb(tos)
if self.acme_version == 1:
regr = self.client.register(regr)
if regr.terms_of_service is not None:
_assess_tos(regr.terms_of_service)
return self.client.agree_to_tos(regr)
return regr
else:
if "terms_of_service" in self.client.directory.meta:
_assess_tos(self.client.directory.meta.terms_of_service)
regr = regr.update(terms_of_service_agreed=True)
return self.client.new_account(regr)
def new_order(self, csr_pem):
"""Request a new Order object from the server.
If using ACMEv1, returns a dummy OrderResource with only
the authorizations field filled in.
:param str csr_pem: A CSR in PEM format.
:returns: The newly created order.
:rtype: OrderResource
:raises errors.WildcardUnsupportedError: if a wildcard domain is
requested but unsupported by the ACME version
"""
if self.acme_version == 1:
csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# pylint: disable=protected-access
dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
authorizations = []
for domain in dnsNames:
authorizations.append(self.client.request_domain_challenges(domain))
return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem)
else:
return self.client.new_order(csr_pem)
def finalize_order(self, orderr, deadline):
"""Finalize an order and obtain a certificate.
:param messages.OrderResource orderr: order to finalize
:param datetime.datetime deadline: when to stop polling and timeout
:returns: finalized order
:rtype: messages.OrderResource
"""
if self.acme_version == 1:
csr_pem = orderr.csr_pem
certr = self.client.request_issuance(
jose.ComparableX509(
OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)),
orderr.authorizations)
chain = None
while datetime.datetime.now() < deadline:
try:
chain = self.client.fetch_chain(certr)
break
except errors.Error:
time.sleep(1)
if chain is None:
raise errors.TimeoutError(
'Failed to fetch chain. You should not deploy the generated '
'certificate, please rerun the command for a new one.')
cert = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode()
chain = crypto_util.dump_pyopenssl_chain(chain).decode()
return orderr.update(fullchain_pem=(cert + chain))
else:
return self.client.finalize_order(orderr, deadline)
def revoke(self, cert, rsn):
"""Revoke certificate.
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509`
:param int rsn: Reason code for certificate revocation.
:raises .ClientError: If revocation is unsuccessful.
"""
return self.client.revoke(cert, rsn)
def _acme_version_from_directory(self, directory):
if hasattr(directory, 'newNonce'):
return 2
else:
return 1
class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
"""Client network."""
"""Wrapper around requests that signs POSTs for authentication.
Also adds user agent, and handles Content-Type.
"""
JSON_CONTENT_TYPE = 'application/json'
JOSE_CONTENT_TYPE = 'application/jose+json'
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
REPLAY_NONCE_HEADER = 'Replay-Nonce'
def __init__(self, key, alg=jose.RS256, verify_ssl=True,
user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT):
"""Initialize.
:param josepy.JWK key: Account private key
:param messages.RegistrationResource account: Account object. Required if you are
planning to use .post() with acme_version=2 for anything other than
creating a new account; may be set later after registering.
:param josepy.JWASignature alg: Algoritm to use in signing JWS.
:param bool verify_ssl: Whether to verify certificates on SSL connections.
:param str user_agent: String to send as User-Agent header.
:param float timeout: Timeout for requests.
:param source_address: Optional source address to bind to when making requests.
:type source_address: str or tuple(str, int)
"""
def __init__(self, key, account=None, alg=jose.RS256, verify_ssl=True,
user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT,
source_address=None):
# pylint: disable=too-many-arguments
self.key = key
self.account = account
self.alg = alg
self.verify_ssl = verify_ssl
self._nonces = set()
self._nonces = set() # type: Set[Text]
self.user_agent = user_agent
self.session = requests.Session()
self._default_timeout = timeout
adapter = HTTPAdapter()
if source_address is not None:
adapter = SourceAddressAdapter(source_address)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def __del__(self):
# Try to close the session, but don't show exceptions to the
@@ -527,21 +890,31 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
except Exception: # pylint: disable=broad-except
pass
def _wrap_in_jws(self, obj, nonce):
def _wrap_in_jws(self, obj, nonce, url, acme_version):
"""Wrap `JSONDeSerializable` object in JWS.
.. todo:: Implement ``acmePath``.
:param .JSONDeSerializable obj:
:param josepy.JSONDeSerializable obj:
:param str url: The URL to which this object will be POSTed
:param bytes nonce:
:rtype: `.JWS`
:rtype: `josepy.JWS`
"""
jobj = obj.json_dumps(indent=2).encode()
logger.debug('JWS payload:\n%s', jobj)
return jws.JWS.sign(
payload=jobj, key=self.key, alg=self.alg,
nonce=nonce).json_dumps(indent=2)
kwargs = {
"alg": self.alg,
"nonce": nonce
}
if acme_version == 2:
kwargs["url"] = url
# newAccount and revokeCert work without the kid
if self.account is not None:
kwargs["kid"] = self.account["uri"]
kwargs["key"] = self.key
# pylint: disable=star-args
return jws.JWS.sign(jobj, **kwargs).json_dumps(indent=2)
@classmethod
def _check_response(cls, response, content_type=None):
@@ -657,7 +1030,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
if response.headers.get("Content-Type") == DER_CONTENT_TYPE:
debug_content = base64.b64encode(response.content)
else:
debug_content = response.content
debug_content = response.content.decode("utf-8")
logger.debug('Received response:\nHTTP %d\n%s\n\n%s',
response.status_code,
"\n".join(["{0}: {1}".format(k, v)
@@ -714,8 +1087,9 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
else:
raise
def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs):
data = self._wrap_in_jws(obj, self._get_nonce(url))
def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE,
acme_version=1, **kwargs):
data = self._wrap_in_jws(obj, self._get_nonce(url), url, acme_version)
kwargs.setdefault('headers', {'Content-Type': content_type})
response = self._send_request('POST', url, data=data, **kwargs)
self._add_nonce(response)

View File

@@ -1,4 +1,5 @@
"""Tests for acme.client."""
import copy
import datetime
import json
import unittest
@@ -7,6 +8,7 @@ from six.moves import http_client # pylint: disable=import-error
import josepy as jose
import mock
import OpenSSL
import requests
from acme import challenges
@@ -15,16 +17,36 @@ from acme import jws as acme_jws
from acme import messages
from acme import messages_test
from acme import test_util
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
CERT_DER = test_util.load_vector('cert.der')
CERT_SAN_PEM = test_util.load_vector('cert-san.pem')
CSR_SAN_PEM = test_util.load_vector('csr-san.pem')
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
DIRECTORY_V1 = messages.Directory({
messages.NewRegistration:
'https://www.letsencrypt-demo.org/acme/new-reg',
messages.Revocation:
'https://www.letsencrypt-demo.org/acme/revoke-cert',
messages.NewAuthorization:
'https://www.letsencrypt-demo.org/acme/new-authz',
messages.CertificateRequest:
'https://www.letsencrypt-demo.org/acme/new-cert',
})
class ClientTest(unittest.TestCase):
"""Tests for acme.client.Client."""
# pylint: disable=too-many-instance-attributes,too-many-public-methods
DIRECTORY_V2 = messages.Directory({
'newAccount': 'https://www.letsencrypt-demo.org/acme/new-account',
'newNonce': 'https://www.letsencrypt-demo.org/acme/new-nonce',
'newOrder': 'https://www.letsencrypt-demo.org/acme/new-order',
'revokeCert': 'https://www.letsencrypt-demo.org/acme/revoke-cert',
})
class ClientTestBase(unittest.TestCase):
"""Base for tests in acme.client."""
def setUp(self):
self.response = mock.MagicMock(
@@ -33,21 +55,6 @@ 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',
messages.NewAuthorization:
'https://www.letsencrypt-demo.org/acme/new-authz',
messages.CertificateRequest:
'https://www.letsencrypt-demo.org/acme/new-cert',
})
from acme.client import Client
self.client = Client(
directory=self.directory, key=KEY, alg=jose.RS256, net=self.net)
self.identifier = messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value='example.com')
@@ -55,10 +62,10 @@ class ClientTest(unittest.TestCase):
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages.Registration(
contact=self.contact, key=KEY.public_key())
self.new_reg = messages.NewRegistration(**dict(reg))
the_arg = dict(reg) # type: Dict
self.new_reg = messages.NewRegistration(**the_arg) # pylint: disable=star-args
self.regr = messages.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
terms_of_service='https://www.letsencrypt-demo.org/tos')
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1')
# Authorization
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
@@ -75,14 +82,217 @@ class ClientTest(unittest.TestCase):
self.authzr = messages.AuthorizationResource(
body=self.authz, uri=authzr_uri)
# Reason code for revocation
self.rsn = 1
class BackwardsCompatibleClientV2Test(ClientTestBase):
"""Tests for acme.client.BackwardsCompatibleClientV2."""
def setUp(self):
super(BackwardsCompatibleClientV2Test, self).setUp()
# contains a loaded cert
self.certr = messages.CertificateResource(
body=messages_test.CERT)
loaded = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, CERT_SAN_PEM)
wrapped = jose.ComparableX509(loaded)
self.chain = [wrapped, wrapped]
self.cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, messages_test.CERT.wrapped).decode()
single_chain = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, loaded).decode()
self.chain_pem = single_chain + single_chain
self.fullchain_pem = self.cert_pem + self.chain_pem
self.orderr = messages.OrderResource(
csr_pem=CSR_SAN_PEM)
def _init(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import BackwardsCompatibleClientV2
return BackwardsCompatibleClientV2(net=self.net,
key=KEY, server=uri)
def test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import BackwardsCompatibleClientV2
BackwardsCompatibleClientV2(net=self.net,
key=KEY, server=uri)
self.net.get.assert_called_once_with(uri)
def test_init_acme_version(self):
self.response.json.return_value = DIRECTORY_V1.to_json()
client = self._init()
self.assertEqual(client.acme_version, 1)
self.response.json.return_value = DIRECTORY_V2.to_json()
client = self._init()
self.assertEqual(client.acme_version, 2)
def test_forwarding(self):
self.response.json.return_value = DIRECTORY_V1.to_json()
client = self._init()
self.assertEqual(client.directory, client.client.directory)
self.assertEqual(client.key, KEY)
self.assertEqual(client.update_registration, client.client.update_registration)
self.assertRaises(AttributeError, client.__getattr__, 'nonexistent')
self.assertRaises(AttributeError, client.__getattr__, 'new_account_and_tos')
self.assertRaises(AttributeError, client.__getattr__, 'new_account')
def test_new_account_and_tos(self):
# v2 no tos
self.response.json.return_value = DIRECTORY_V2.to_json()
with mock.patch('acme.client.ClientV2') as mock_client:
client = self._init()
client.new_account_and_tos(self.new_reg)
mock_client().new_account.assert_called_with(self.new_reg)
# v2 tos good
with mock.patch('acme.client.ClientV2') as mock_client:
mock_client().directory.meta.__contains__.return_value = True
client = self._init()
client.new_account_and_tos(self.new_reg, lambda x: True)
mock_client().new_account.assert_called_with(
self.new_reg.update(terms_of_service_agreed=True))
# v2 tos bad
with mock.patch('acme.client.ClientV2') as mock_client:
mock_client().directory.meta.__contains__.return_value = True
client = self._init()
def _tos_cb(tos):
raise errors.Error
self.assertRaises(errors.Error, client.new_account_and_tos,
self.new_reg, _tos_cb)
mock_client().new_account.assert_not_called()
# v1 yes tos
self.response.json.return_value = DIRECTORY_V1.to_json()
with mock.patch('acme.client.Client') as mock_client:
regr = mock.MagicMock(terms_of_service="TOS")
mock_client().register.return_value = regr
client = self._init()
client.new_account_and_tos(self.new_reg)
mock_client().register.assert_called_once_with(self.new_reg)
mock_client().agree_to_tos.assert_called_once_with(regr)
# v1 no tos
with mock.patch('acme.client.Client') as mock_client:
regr = mock.MagicMock(terms_of_service=None)
mock_client().register.return_value = regr
client = self._init()
client.new_account_and_tos(self.new_reg)
mock_client().register.assert_called_once_with(self.new_reg)
mock_client().agree_to_tos.assert_not_called()
@mock.patch('OpenSSL.crypto.load_certificate_request')
@mock.patch('acme.crypto_util._pyopenssl_cert_or_req_all_names')
def test_new_order_v1(self, mock__pyopenssl_cert_or_req_all_names,
unused_mock_load_certificate_request):
self.response.json.return_value = DIRECTORY_V1.to_json()
mock__pyopenssl_cert_or_req_all_names.return_value = ['example.com', 'www.example.com']
mock_csr_pem = mock.MagicMock()
with mock.patch('acme.client.Client') as mock_client:
mock_client().request_domain_challenges.return_value = mock.sentinel.auth
client = self._init()
orderr = client.new_order(mock_csr_pem)
self.assertEqual(orderr.authorizations, [mock.sentinel.auth, mock.sentinel.auth])
def test_new_order_v2(self):
self.response.json.return_value = DIRECTORY_V2.to_json()
mock_csr_pem = mock.MagicMock()
with mock.patch('acme.client.ClientV2') as mock_client:
client = self._init()
client.new_order(mock_csr_pem)
mock_client().new_order.assert_called_once_with(mock_csr_pem)
@mock.patch('acme.client.Client')
def test_finalize_order_v1_success(self, mock_client):
self.response.json.return_value = DIRECTORY_V1.to_json()
mock_client().request_issuance.return_value = self.certr
mock_client().fetch_chain.return_value = self.chain
deadline = datetime.datetime(9999, 9, 9)
client = self._init()
result = client.finalize_order(self.orderr, deadline)
self.assertEqual(result.fullchain_pem, self.fullchain_pem)
mock_client().fetch_chain.assert_called_once_with(self.certr)
@mock.patch('acme.client.Client')
def test_finalize_order_v1_fetch_chain_error(self, mock_client):
self.response.json.return_value = DIRECTORY_V1.to_json()
mock_client().request_issuance.return_value = self.certr
mock_client().fetch_chain.return_value = self.chain
mock_client().fetch_chain.side_effect = [errors.Error, self.chain]
deadline = datetime.datetime(9999, 9, 9)
client = self._init()
result = client.finalize_order(self.orderr, deadline)
self.assertEqual(result.fullchain_pem, self.fullchain_pem)
self.assertEqual(mock_client().fetch_chain.call_count, 2)
@mock.patch('acme.client.Client')
def test_finalize_order_v1_timeout(self, mock_client):
self.response.json.return_value = DIRECTORY_V1.to_json()
mock_client().request_issuance.return_value = self.certr
deadline = deadline = datetime.datetime.now() - datetime.timedelta(seconds=60)
client = self._init()
self.assertRaises(errors.TimeoutError, client.finalize_order,
self.orderr, deadline)
def test_finalize_order_v2(self):
self.response.json.return_value = DIRECTORY_V2.to_json()
mock_orderr = mock.MagicMock()
mock_deadline = mock.MagicMock()
with mock.patch('acme.client.ClientV2') as mock_client:
client = self._init()
client.finalize_order(mock_orderr, mock_deadline)
mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline)
def test_revoke(self):
self.response.json.return_value = DIRECTORY_V1.to_json()
with mock.patch('acme.client.Client') as mock_client:
client = self._init()
client.revoke(messages_test.CERT, self.rsn)
mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn)
self.response.json.return_value = DIRECTORY_V2.to_json()
with mock.patch('acme.client.ClientV2') as mock_client:
client = self._init()
client.revoke(messages_test.CERT, self.rsn)
mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn)
class ClientTest(ClientTestBase):
"""Tests for acme.client.Client."""
# pylint: disable=too-many-instance-attributes,too-many-public-methods
def setUp(self):
super(ClientTest, self).setUp()
self.directory = DIRECTORY_V1
# Registration
self.regr = self.regr.update(
terms_of_service='https://www.letsencrypt-demo.org/tos')
# Request issuance
self.certr = messages.CertificateResource(
body=messages_test.CERT, authzrs=(self.authzr,),
uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
# Reason code for revocation
self.rsn = 1
from acme.client import Client
self.client = Client(
directory=self.directory, key=KEY, alg=jose.RS256, net=self.net)
def test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
@@ -91,6 +301,16 @@ class ClientTest(unittest.TestCase):
directory=uri, key=KEY, alg=jose.RS256, net=self.net)
self.net.get.assert_called_once_with(uri)
@mock.patch('acme.client.ClientNetwork')
def test_init_without_net(self, mock_net):
mock_net.return_value = mock.sentinel.net
alg = jose.RS256
from acme.client import Client
self.client = Client(
directory=self.directory, key=KEY, alg=alg)
mock_net.called_once_with(KEY, alg=alg, verify_ssl=True)
self.assertEqual(self.client.net, mock.sentinel.net)
def test_register(self):
# "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member
@@ -142,20 +362,23 @@ class ClientTest(unittest.TestCase):
self.client.request_challenges(self.identifier)
self.net.post.assert_called_once_with(
self.directory.new_authz,
messages.NewAuthorization(identifier=self.identifier))
messages.NewAuthorization(identifier=self.identifier),
acme_version=1)
def test_request_challenges_deprecated_arg(self):
self._prepare_response_for_request_challenges()
self.client.request_challenges(self.identifier, new_authzr_uri="hi")
self.net.post.assert_called_once_with(
self.directory.new_authz,
messages.NewAuthorization(identifier=self.identifier))
messages.NewAuthorization(identifier=self.identifier),
acme_version=1)
def test_request_challenges_custom_uri(self):
self._prepare_response_for_request_challenges()
self.client.request_challenges(self.identifier)
self.net.post.assert_called_once_with(
'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY)
'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY,
acme_version=1)
def test_request_challenges_unexpected_update(self):
self._prepare_response_for_request_challenges()
@@ -165,6 +388,13 @@ class ClientTest(unittest.TestCase):
errors.UnexpectedUpdate, self.client.request_challenges,
self.identifier)
def test_request_challenges_wildcard(self):
wildcard_identifier = messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value='*.example.org')
self.assertRaises(
errors.WildcardUnsupportedError, self.client.request_challenges,
wildcard_identifier)
def test_request_domain_challenges(self):
self.client.request_challenges = mock.MagicMock()
self.assertEqual(
@@ -417,7 +647,7 @@ class ClientTest(unittest.TestCase):
def test_revoke(self):
self.client.revoke(self.certr.body, self.rsn)
self.net.post.assert_called_once_with(
self.directory[messages.Revocation], mock.ANY, content_type=None)
self.directory[messages.Revocation], mock.ANY, acme_version=1)
def test_revocation_payload(self):
obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn)
@@ -432,9 +662,150 @@ class ClientTest(unittest.TestCase):
self.certr,
self.rsn)
class ClientV2Test(ClientTestBase):
"""Tests for acme.client.ClientV2."""
def setUp(self):
super(ClientV2Test, self).setUp()
self.directory = DIRECTORY_V2
from acme.client import ClientV2
self.client = ClientV2(self.directory, self.net)
self.new_reg = self.new_reg.update(terms_of_service_agreed=True)
self.authzr_uri2 = 'https://www.letsencrypt-demo.org/acme/authz/2'
self.authz2 = self.authz.update(identifier=messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value='www.example.com'),
status=messages.STATUS_PENDING)
self.authzr2 = messages.AuthorizationResource(
body=self.authz2, uri=self.authzr_uri2)
self.order = messages.Order(
identifiers=(self.authz.identifier, self.authz2.identifier),
status=messages.STATUS_PENDING,
authorizations=(self.authzr.uri, self.authzr_uri2),
finalize='https://www.letsencrypt-demo.org/acme/acct/1/order/1/finalize')
self.orderr = messages.OrderResource(
body=self.order,
uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',
authorizations=[self.authzr, self.authzr2], csr_pem=CSR_SAN_PEM)
def test_new_account(self):
self.response.status_code = http_client.CREATED
self.response.json.return_value = self.regr.body.to_json()
self.response.headers['Location'] = self.regr.uri
self.assertEqual(self.regr, self.client.new_account(self.new_reg))
def test_new_order(self):
order_response = copy.deepcopy(self.response)
order_response.status_code = http_client.CREATED
order_response.json.return_value = self.order.to_json()
order_response.headers['Location'] = self.orderr.uri
self.net.post.return_value = order_response
authz_response = copy.deepcopy(self.response)
authz_response.json.return_value = self.authz.to_json()
authz_response.headers['Location'] = self.authzr.uri
authz_response2 = self.response
authz_response2.json.return_value = self.authz2.to_json()
authz_response2.headers['Location'] = self.authzr2.uri
self.net.get.side_effect = (authz_response, authz_response2)
self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr)
@mock.patch('acme.client.datetime')
def test_poll_and_finalize(self, mock_datetime):
mock_datetime.datetime.now.return_value = datetime.datetime(2018, 2, 15)
mock_datetime.timedelta = datetime.timedelta
expected_deadline = mock_datetime.datetime.now() + datetime.timedelta(seconds=90)
self.client.poll_authorizations = mock.Mock(return_value=self.orderr)
self.client.finalize_order = mock.Mock(return_value=self.orderr)
self.assertEqual(self.client.poll_and_finalize(self.orderr), self.orderr)
self.client.poll_authorizations.assert_called_once_with(self.orderr, expected_deadline)
self.client.finalize_order.assert_called_once_with(self.orderr, expected_deadline)
@mock.patch('acme.client.datetime')
def test_poll_authorizations_timeout(self, mock_datetime):
now_side_effect = [datetime.datetime(2018, 2, 15),
datetime.datetime(2018, 2, 16),
datetime.datetime(2018, 2, 17)]
mock_datetime.datetime.now.side_effect = now_side_effect
self.response.json.side_effect = [
self.authz.to_json(), self.authz2.to_json(), self.authz2.to_json()]
self.assertRaises(
errors.TimeoutError, self.client.poll_authorizations, self.orderr, now_side_effect[1])
def test_poll_authorizations_failure(self):
deadline = datetime.datetime(9999, 9, 9)
challb = self.challr.body.update(status=messages.STATUS_INVALID,
error=messages.Error.with_code('unauthorized'))
authz = self.authz.update(status=messages.STATUS_INVALID, challenges=(challb,))
self.response.json.return_value = authz.to_json()
self.assertRaises(
errors.ValidationError, self.client.poll_authorizations, self.orderr, deadline)
def test_poll_authorizations_success(self):
deadline = datetime.datetime(9999, 9, 9)
updated_authz2 = self.authz2.update(status=messages.STATUS_VALID)
updated_authzr2 = messages.AuthorizationResource(
body=updated_authz2, uri=self.authzr_uri2)
updated_orderr = self.orderr.update(authorizations=[self.authzr, updated_authzr2])
self.response.json.side_effect = (
self.authz.to_json(), self.authz2.to_json(), updated_authz2.to_json())
self.assertEqual(self.client.poll_authorizations(self.orderr, deadline), updated_orderr)
def test_finalize_order_success(self):
updated_order = self.order.update(
certificate='https://www.letsencrypt-demo.org/acme/cert/')
updated_orderr = self.orderr.update(body=updated_order, fullchain_pem=CERT_SAN_PEM)
self.response.json.return_value = updated_order.to_json()
self.response.text = CERT_SAN_PEM
deadline = datetime.datetime(9999, 9, 9)
self.assertEqual(self.client.finalize_order(self.orderr, deadline), updated_orderr)
def test_finalize_order_error(self):
updated_order = self.order.update(error=messages.Error.with_code('unauthorized'))
self.response.json.return_value = updated_order.to_json()
deadline = datetime.datetime(9999, 9, 9)
self.assertRaises(errors.IssuanceError, self.client.finalize_order, self.orderr, deadline)
def test_finalize_order_timeout(self):
deadline = datetime.datetime.now() - datetime.timedelta(seconds=60)
self.assertRaises(errors.TimeoutError, self.client.finalize_order, self.orderr, deadline)
def test_revoke(self):
self.client.revoke(messages_test.CERT, self.rsn)
self.net.post.assert_called_once_with(
self.directory["revokeCert"], mock.ANY, acme_version=2)
class MockJSONDeSerializable(jose.JSONDeSerializable):
# 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
class ClientNetworkTest(unittest.TestCase):
"""Tests for acme.client.ClientNetwork."""
# pylint: disable=too-many-public-methods
def setUp(self):
self.verify_ssl = mock.MagicMock()
@@ -453,25 +824,27 @@ class ClientNetworkTest(unittest.TestCase):
self.assertTrue(self.net.verify_ssl is self.verify_ssl)
def test_wrap_in_jws(self):
class MockJSONDeSerializable(jose.JSONDeSerializable):
# pylint: disable=missing-docstring
def __init__(self, value):
self.value = value
def to_partial_json(self):
return {'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')
MockJSONDeSerializable('foo'), nonce=b'Tg', url="url",
acme_version=1)
jws = acme_jws.JWS.json_loads(jws_dump)
self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'})
self.assertEqual(jws.signature.combined.nonce, b'Tg')
def test_wrap_in_jws_v2(self):
self.net.account = {'uri': 'acct-uri'}
# pylint: disable=protected-access
jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce=b'Tg', url="url",
acme_version=2)
jws = acme_jws.JWS.json_loads(jws_dump)
self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'})
self.assertEqual(jws.signature.combined.nonce, b'Tg')
self.assertEqual(jws.signature.combined.kid, u'acct-uri')
self.assertEqual(jws.signature.combined.url, u'url')
def test_check_response_not_ok_jobj_no_error(self):
self.response.ok = False
self.response.json.return_value = {}
@@ -701,13 +1074,13 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.assertEqual(self.checked_response, self.net.post(
'uri', self.obj, content_type=self.content_type))
self.net._wrap_in_jws.assert_called_once_with(
self.obj, jose.b64decode(self.all_nonces.pop()))
self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1)
self.available_nonces = []
self.assertRaises(errors.MissingNonce, self.net.post,
'uri', self.obj, content_type=self.content_type)
self.net._wrap_in_jws.assert_called_with(
self.obj, jose.b64decode(self.all_nonces.pop()))
self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1)
def test_post_wrong_initial_nonce(self): # HEAD
self.available_nonces = [b'f', jose.b64encode(b'good')]
@@ -756,6 +1129,31 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.assertRaises(requests.exceptions.RequestException,
self.net.post, 'uri', obj=self.obj)
class ClientNetworkSourceAddressBindingTest(unittest.TestCase):
"""Tests that if ClientNetwork has a source IP set manually, the underlying library has
used the provided source address."""
def setUp(self):
self.source_address = "8.8.8.8"
def test_source_address_set(self):
from acme.client import ClientNetwork
net = ClientNetwork(key=None, alg=None, source_address=self.source_address)
for adapter in net.session.adapters.values():
self.assertTrue(self.source_address in adapter.source_address)
def test_behavior_assumption(self):
"""This is a test that guardrails the HTTPAdapter behavior so that if the default for
a Session() changes, the assumptions here aren't violated silently."""
from acme.client import ClientNetwork
# Source address not specified, so the default adapter type should be bound -- this
# test should fail if the default adapter type is changed by requests
net = ClientNetwork(key=None, alg=None)
session = requests.Session()
for scheme in session.adapters.keys():
client_network_adapter = net.session.adapters.get(scheme)
default_adapter = session.adapters.get(scheme)
self.assertEqual(client_network_adapter.__class__, default_adapter.__class__)
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -5,11 +5,15 @@ import logging
import os
import re
import socket
import sys
import OpenSSL
from OpenSSL import crypto
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
import josepy as jose
from acme import errors
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Callable, Union, Tuple, Optional
# pylint: enable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
@@ -24,7 +28,7 @@ logger = logging.getLogger(__name__)
# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni
# should be changed to use "set_options" to disable SSLv2 and SSLv3,
# in case it's used for things other than probing/serving!
_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD # type: ignore
_DEFAULT_TLSSNI01_SSL_METHOD = SSL.SSLv23_METHOD # type: ignore
class SSLSocket(object): # pylint: disable=too-few-public-methods
@@ -63,9 +67,9 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
logger.debug("Server name (%s) not recognized, dropping SSL",
server_name)
return
new_context = OpenSSL.SSL.Context(self.method)
new_context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
new_context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
new_context = SSL.Context(self.method)
new_context.set_options(SSL.OP_NO_SSLv2)
new_context.set_options(SSL.OP_NO_SSLv3)
new_context.use_privatekey(key)
new_context.use_certificate(cert)
connection.set_context(new_context)
@@ -88,18 +92,18 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
def accept(self): # pylint: disable=missing-docstring
sock, addr = self.sock.accept()
context = OpenSSL.SSL.Context(self.method)
context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
context = SSL.Context(self.method)
context.set_options(SSL.OP_NO_SSLv2)
context.set_options(SSL.OP_NO_SSLv3)
context.set_tlsext_servername_callback(self._pick_certificate_cb)
ssl_sock = self.FakeConnection(OpenSSL.SSL.Connection(context, sock))
ssl_sock = self.FakeConnection(SSL.Connection(context, sock))
ssl_sock.set_accept_state()
logger.debug("Performing handshake with %s", addr)
try:
ssl_sock.do_handshake()
except OpenSSL.SSL.Error as error:
except SSL.Error as error:
# _pick_certificate_cb might have returned without
# creating SSL context (wrong server name)
raise socket.error(error)
@@ -127,31 +131,39 @@ def probe_sni(name, host, port=443, timeout=300,
:rtype: OpenSSL.crypto.X509
"""
context = OpenSSL.SSL.Context(method)
context = SSL.Context(method)
context.set_timeout(timeout)
socket_kwargs = {} if sys.version_info < (2, 7) else {
'source_address': source_address}
socket_kwargs = {'source_address': source_address}
host_protocol_agnostic = None if host == '::' or host == '0' else host
host_protocol_agnostic = host
if host == '::' or host == '0':
# https://github.com/python/typeshed/pull/2136
# while PR is not merged, we need to ignore
host_protocol_agnostic = None
try:
# pylint: disable=star-args
logger.debug("Attempting to connect to %s:%d%s.", host_protocol_agnostic, port,
" from {0}:{1}".format(source_address[0], source_address[1]) if \
socket_kwargs else "")
sock = socket.create_connection((host_protocol_agnostic, port), **socket_kwargs)
logger.debug(
"Attempting to connect to %s:%d%s.", host_protocol_agnostic, port,
" from {0}:{1}".format(
source_address[0],
source_address[1]
) if socket_kwargs else ""
)
socket_tuple = (host_protocol_agnostic, port) # type: Tuple[Optional[str], int]
sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore
except socket.error as error:
raise errors.Error(error)
with contextlib.closing(sock) as client:
client_ssl = OpenSSL.SSL.Connection(context, client)
client_ssl = SSL.Connection(context, client)
client_ssl.set_connect_state()
client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13
try:
client_ssl.do_handshake()
client_ssl.shutdown()
except OpenSSL.SSL.Error as error:
except SSL.Error as error:
raise errors.Error(error)
return client_ssl.get_peer_certificate()
@@ -164,18 +176,18 @@ def make_csr(private_key_pem, domains, must_staple=False):
OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
:returns: buffer PEM-encoded Certificate Signing Request.
"""
private_key = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, private_key_pem)
csr = OpenSSL.crypto.X509Req()
private_key = crypto.load_privatekey(
crypto.FILETYPE_PEM, private_key_pem)
csr = crypto.X509Req()
extensions = [
OpenSSL.crypto.X509Extension(
crypto.X509Extension(
b'subjectAltName',
critical=False,
value=', '.join('DNS:' + d for d in domains).encode('ascii')
),
]
if must_staple:
extensions.append(OpenSSL.crypto.X509Extension(
extensions.append(crypto.X509Extension(
b"1.3.6.1.5.5.7.1.24",
critical=False,
value=b"DER:30:03:02:01:05"))
@@ -183,8 +195,17 @@ def make_csr(private_key_pem, domains, must_staple=False):
csr.set_pubkey(private_key)
csr.set_version(2)
csr.sign(private_key, 'sha256')
return OpenSSL.crypto.dump_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr)
return crypto.dump_certificate_request(
crypto.FILETYPE_PEM, csr)
def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
common_name = loaded_cert_or_req.get_subject().CN
sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
if common_name is None:
return sans
else:
return [common_name] + [d for d in sans if d != common_name]
def _pyopenssl_cert_or_req_san(cert_or_req):
"""Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
@@ -212,11 +233,12 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
parts_separator = ", "
prefix = "DNS" + part_separator
if isinstance(cert_or_req, OpenSSL.crypto.X509):
func = OpenSSL.crypto.dump_certificate
if isinstance(cert_or_req, crypto.X509):
# pylint: disable=line-too-long
func = crypto.dump_certificate # type: Union[Callable[[int, crypto.X509Req], bytes], Callable[[int, crypto.X509], bytes]]
else:
func = OpenSSL.crypto.dump_certificate_request
text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
func = crypto.dump_certificate_request
text = func(crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
# WARNING: this function does not support multiple SANs extensions.
# Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
@@ -243,12 +265,12 @@ def gen_ss_cert(key, domains, not_before=None,
"""
assert domains, "Must provide one or more hostnames for the cert."
cert = OpenSSL.crypto.X509()
cert = crypto.X509()
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
cert.set_version(2)
extensions = [
OpenSSL.crypto.X509Extension(
crypto.X509Extension(
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
]
@@ -257,7 +279,7 @@ def gen_ss_cert(key, domains, not_before=None,
cert.set_issuer(cert.get_subject())
if force_san or len(domains) > 1:
extensions.append(OpenSSL.crypto.X509Extension(
extensions.append(crypto.X509Extension(
b"subjectAltName",
critical=False,
value=b", ".join(b"DNS:" + d.encode() for d in domains)
@@ -271,3 +293,26 @@ def gen_ss_cert(key, domains, not_before=None,
cert.set_pubkey(key)
cert.sign(key, "sha256")
return cert
def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM):
"""Dump certificate chain into a bundle.
:param list chain: List of `OpenSSL.crypto.X509` (or wrapped in
:class:`josepy.util.ComparableX509`).
:returns: certificate chain bundle
:rtype: bytes
"""
# XXX: returns empty string when no chain is available, which
# shuts up RenewableCert, but might not be the best solution...
def _dump_cert(cert):
if isinstance(cert, jose.ComparableX509):
# pylint: disable=protected-access
cert = cert.wrapped
return crypto.dump_certificate(filetype, cert)
# assumes that OpenSSL.crypto.dump_certificate includes ending
# newline character
return b"".join(_dump_cert(cert) for cert in chain)

View File

@@ -13,6 +13,7 @@ import OpenSSL
from acme import errors
from acme import test_util
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
class SSLSocketAndProbeSNITest(unittest.TestCase):
@@ -41,28 +42,62 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
self.server_thread = threading.Thread(
# pylint: disable=no-member
target=self.server.handle_request)
self.server_thread.start()
time.sleep(1) # TODO: avoid race conditions in other way
def tearDown(self):
self.server_thread.join()
if self.server_thread.is_alive():
# The thread may have already terminated.
self.server_thread.join() # pragma: no cover
def _probe(self, name):
from acme.crypto_util import probe_sni
return jose.ComparableX509(probe_sni(
name, host='127.0.0.1', port=self.port))
def _start_server(self):
self.server_thread.start()
time.sleep(1) # TODO: avoid race conditions in other way
def test_probe_ok(self):
self._start_server()
self.assertEqual(self.cert, self._probe(b'foo'))
def test_probe_not_recognized_name(self):
self._start_server()
self.assertRaises(errors.Error, self._probe, b'bar')
# TODO: py33/py34 tox hangs forever on do_handshake 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')
def test_probe_connection_error(self):
# pylint has a hard time with six
self.server.server_close() # pylint: disable=no-member
original_timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(1)
self.assertRaises(errors.Error, self._probe, b'bar')
finally:
socket.setdefaulttimeout(original_timeout)
class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_all_names."""
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
from acme.crypto_util import _pyopenssl_cert_or_req_all_names
return _pyopenssl_cert_or_req_all_names(loader(name))
def _call_cert(self, name):
return self._call(test_util.load_cert, name)
def test_cert_one_san_no_common(self):
self.assertEqual(self._call_cert('cert-nocn.der'),
['no-common-name.badssl.com'])
def test_cert_no_sans_yes_common(self):
self.assertEqual(self._call_cert('cert.pem'), ['example.com'])
def test_cert_two_sans_yes_common(self):
self.assertEqual(self._call_cert('cert-san.pem'),
['example.com', 'www.example.com'])
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
@@ -141,7 +176,7 @@ class RandomSnTest(unittest.TestCase):
def setUp(self):
self.cert_count = 5
self.serial_num = []
self.serial_num = [] # type: List[int]
self.key = OpenSSL.crypto.PKey()
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
@@ -170,9 +205,9 @@ class MakeCSRTest(unittest.TestCase):
self.assertTrue(b'--END CERTIFICATE REQUEST--' in csr_pem)
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# In pyopenssl 0.13 (used with TOXENV=py26-oldest and py27-oldest), csr
# objects don't have a get_extensions() method, so we skip this test if
# the method isn't available.
# In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
# have a get_extensions() method, so we skip this test if the method
# isn't available.
if hasattr(csr, 'get_extensions'):
self.assertEquals(len(csr.get_extensions()), 1)
self.assertEquals(csr.get_extensions()[0].get_data(),
@@ -188,9 +223,9 @@ class MakeCSRTest(unittest.TestCase):
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# In pyopenssl 0.13 (used with TOXENV=py26-oldest and py27-oldest), csr
# objects don't have a get_extensions() method, so we skip this test if
# the method isn't available.
# In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
# have a get_extensions() method, so we skip this test if the method
# isn't available.
if hasattr(csr, 'get_extensions'):
self.assertEquals(len(csr.get_extensions()), 2)
# NOTE: Ideally we would filter by the TLS Feature OID, but
@@ -201,5 +236,33 @@ class MakeCSRTest(unittest.TestCase):
self.assertEqual(len(must_staple_exts), 1,
"Expected exactly one Must Staple extension")
class DumpPyopensslChainTest(unittest.TestCase):
"""Test for dump_pyopenssl_chain."""
@classmethod
def _call(cls, loaded):
# pylint: disable=protected-access
from acme.crypto_util import dump_pyopenssl_chain
return dump_pyopenssl_chain(loaded)
def test_dump_pyopenssl_chain(self):
names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem']
loaded = [test_util.load_cert(name) for name in names]
length = sum(
len(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
for cert in loaded)
self.assertEqual(len(self._call(loaded)), length)
def test_dump_pyopenssl_chain_wrapped(self):
names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem']
loaded = [test_util.load_cert(name) for name in names]
wrap_func = jose.ComparableX509
wrapped = [wrap_func(cert) for cert in loaded]
dump_func = OpenSSL.crypto.dump_certificate
length = sum(len(dump_func(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded)
self.assertEqual(len(self._call(wrapped)), length)
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -83,6 +83,28 @@ class PollError(ClientError):
return '{0}(exhausted={1!r}, updated={2!r})'.format(
self.__class__.__name__, self.exhausted, self.updated)
class ValidationError(Error):
"""Error for authorization failures. Contains a list of authorization
resources, each of which is invalid and should have an error field.
"""
def __init__(self, failed_authzrs):
self.failed_authzrs = failed_authzrs
super(ValidationError, self).__init__()
class TimeoutError(Error):
"""Error for when polling an authorization or an order times out."""
class IssuanceError(Error):
"""Error sent by the server after requesting issuance of a certificate."""
def __init__(self, error):
"""Initialize.
:param messages.Error error: The error provided by the server.
"""
self.error = error
super(IssuanceError, self).__init__()
class ConflictError(ClientError):
"""Error for when the server returns a 409 (Conflict) HTTP status.
@@ -93,3 +115,6 @@ class ConflictError(ClientError):
self.location = location
super(ConflictError, self).__init__()
class WildcardUnsupportedError(Error):
"""Error for when a wildcard is requested but is unsupported by ACME CA."""

16
acme/acme/magic_typing.py Normal file
View File

@@ -0,0 +1,16 @@
"""Shim class to not have to depend on typing module in prod."""
import sys
class TypingClass(object):
"""Ignore import errors by getting anything"""
def __getattr__(self, name):
return None
try:
# mypy doesn't respect modifying sys.modules
from typing import * # pylint: disable=wildcard-import, unused-wildcard-import
# pylint: disable=unused-import
from typing import Collection, IO # type: ignore
# pylint: enable=unused-import
except ImportError:
sys.modules[__name__] = TypingClass()

View File

@@ -0,0 +1,41 @@
"""Tests for acme.magic_typing."""
import sys
import unittest
import mock
class MagicTypingTest(unittest.TestCase):
"""Tests for acme.magic_typing."""
def test_import_success(self):
try:
import typing as temp_typing
except ImportError: # pragma: no cover
temp_typing = None # pragma: no cover
typing_class_mock = mock.MagicMock()
text_mock = mock.MagicMock()
typing_class_mock.Text = text_mock
sys.modules['typing'] = typing_class_mock
if 'acme.magic_typing' in sys.modules:
del sys.modules['acme.magic_typing'] # pragma: no cover
from acme.magic_typing import Text # pylint: disable=no-name-in-module
self.assertEqual(Text, text_mock)
del sys.modules['acme.magic_typing']
sys.modules['typing'] = temp_typing
def test_import_failure(self):
try:
import typing as temp_typing
except ImportError: # pragma: no cover
temp_typing = None # pragma: no cover
sys.modules['typing'] = None
if 'acme.magic_typing' in sys.modules:
del sys.modules['acme.magic_typing'] # pragma: no cover
from acme.magic_typing import Text # pylint: disable=no-name-in-module
self.assertTrue(Text is None)
del sys.modules['acme.magic_typing']
sys.modules['typing'] = temp_typing
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -145,6 +145,7 @@ STATUS_PROCESSING = Status('processing')
STATUS_VALID = Status('valid')
STATUS_INVALID = Status('invalid')
STATUS_REVOKED = Status('revoked')
STATUS_READY = Status('ready')
class IdentifierType(_Constant):
@@ -171,9 +172,30 @@ class Directory(jose.JSONDeSerializable):
class Meta(jose.JSONObjectWithFields):
"""Directory Meta."""
terms_of_service = jose.Field('terms-of-service', omitempty=True)
_terms_of_service = jose.Field('terms-of-service', omitempty=True)
_terms_of_service_v2 = jose.Field('termsOfService', omitempty=True)
website = jose.Field('website', omitempty=True)
caa_identities = jose.Field('caa-identities', omitempty=True)
caa_identities = jose.Field('caaIdentities', omitempty=True)
def __init__(self, **kwargs):
kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items())
# pylint: disable=star-args
super(Directory.Meta, self).__init__(**kwargs)
@property
def terms_of_service(self):
"""URL for the CA TOS"""
return self._terms_of_service or self._terms_of_service_v2
def __iter__(self):
# When iterating over fields, use the external name 'terms_of_service' instead of
# the internal '_terms_of_service'.
for name in super(Directory.Meta, self).__iter__():
yield name[1:] if name == '_terms_of_service' else name
def _internal_name(self, name):
return '_' + name if name == 'terms_of_service' else name
@classmethod
def _canon_key(cls, key):
@@ -251,6 +273,7 @@ class Registration(ResourceBody):
contact = jose.Field('contact', omitempty=True, default=())
agreement = jose.Field('agreement', omitempty=True)
status = jose.Field('status', omitempty=True)
terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True)
phone_prefix = 'tel:'
email_prefix = 'mailto:'
@@ -262,7 +285,7 @@ class Registration(ResourceBody):
if phone is not None:
details.append(cls.phone_prefix + phone)
if email is not None:
details.append(cls.email_prefix + email)
details.extend([cls.email_prefix + mail for mail in email.split(',')])
kwargs['contact'] = tuple(details)
return cls(**kwargs)
@@ -413,6 +436,7 @@ class Authorization(ResourceBody):
# be absent'... then acme-spec gives example with 'expires'
# present... That's confusing!
expires = fields.RFC3339Field('expires', omitempty=True)
wildcard = jose.Field('wildcard', omitempty=True)
@challenges.decoder
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
@@ -482,3 +506,50 @@ class Revocation(jose.JSONObjectWithFields):
certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
reason = jose.Field('reason')
class Order(ResourceBody):
"""Order Resource Body.
:ivar list of .Identifier: List of identifiers for the certificate.
:ivar acme.messages.Status status:
:ivar list of str authorizations: URLs of authorizations.
:ivar str certificate: URL to download certificate as a fullchain PEM.
:ivar str finalize: URL to POST to to request issuance once all
authorizations have "valid" status.
:ivar datetime.datetime expires: When the order expires.
:ivar .Error error: Any error that occurred during finalization, if applicable.
"""
identifiers = jose.Field('identifiers', omitempty=True)
status = jose.Field('status', decoder=Status.from_json,
omitempty=True, default=STATUS_PENDING)
authorizations = jose.Field('authorizations', omitempty=True)
certificate = jose.Field('certificate', omitempty=True)
finalize = jose.Field('finalize', omitempty=True)
expires = fields.RFC3339Field('expires', omitempty=True)
error = jose.Field('error', omitempty=True, decoder=Error.from_json)
@identifiers.decoder
def identifiers(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(Identifier.from_json(identifier) for identifier in value)
class OrderResource(ResourceWithURI):
"""Order Resource.
:ivar acme.messages.Order body:
:ivar str csr_pem: The CSR this Order will be finalized with.
:ivar list of acme.messages.AuthorizationResource authorizations:
Fully-fetched AuthorizationResource objects.
:ivar str fullchain_pem: The fetched contents of the certificate URL
produced once the order was finalized, if it's present.
"""
body = jose.Field('body', decoder=Order.from_json)
csr_pem = jose.Field('csr_pem', omitempty=True)
authorizations = jose.Field('authorizations')
fullchain_pem = jose.Field('fullchain_pem', omitempty=True)
@Directory.register
class NewOrder(Order):
"""New order."""
resource_type = 'new-order'
resource = fields.Resource(resource_type)

View File

@@ -6,6 +6,7 @@ import mock
from acme import challenges
from acme import test_util
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
CERT = test_util.load_comparable_cert('cert.der')
@@ -85,7 +86,7 @@ class ConstantTest(unittest.TestCase):
from acme.messages import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES = {}
POSSIBLE_NAMES = {} # type: Dict
self.MockConstant = MockConstant # pylint: disable=invalid-name
self.const_a = MockConstant('a')
@@ -157,7 +158,7 @@ class DirectoryTest(unittest.TestCase):
'meta': {
'terms-of-service': 'https://example.com/acme/terms',
'website': 'https://www.example.com/',
'caa-identities': ['example.com'],
'caaIdentities': ['example.com'],
},
})
@@ -165,6 +166,13 @@ class DirectoryTest(unittest.TestCase):
from acme.messages import Directory
Directory.from_json({'foo': 'bar'})
def test_iter_meta(self):
result = False
for k in self.dir.meta:
if k == 'terms_of_service':
result = self.dir.meta[k] == 'https://example.com/acme/terms'
self.assertTrue(result)
class RegistrationTest(unittest.TestCase):
"""Tests for acme.messages.Registration."""
@@ -401,5 +409,21 @@ class RevocationTest(unittest.TestCase):
hash(Revocation.from_json(self.rev.to_json()))
class OrderResourceTest(unittest.TestCase):
"""Tests for acme.messages.OrderResource."""
def setUp(self):
from acme.messages import OrderResource
self.regr = OrderResource(
body=mock.sentinel.body, uri=mock.sentinel.uri)
def test_to_partial_json(self):
self.assertEqual(self.regr.to_json(), {
'body': mock.sentinel.body,
'uri': mock.sentinel.uri,
'authorizations': None,
})
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -16,6 +16,7 @@ import OpenSSL
from acme import challenges
from acme import crypto_util
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
@@ -66,8 +67,8 @@ class BaseDualNetworkedServers(object):
def __init__(self, ServerClass, server_address, *remaining_args, **kwargs):
port = server_address[1]
self.threads = []
self.servers = []
self.threads = [] # type: List[threading.Thread]
self.servers = [] # type: List[ACMEServerMixin]
# Must try True first.
# Ubuntu, for example, will fail to bind to IPv4 if we've already bound
@@ -82,9 +83,22 @@ class BaseDualNetworkedServers(object):
new_address = (server_address[0],) + (port,) + server_address[2:]
new_args = (new_address,) + remaining_args
server = ServerClass(*new_args, **kwargs) # pylint: disable=star-args
except socket.error:
logger.debug("Failed to bind to %s:%s using %s", new_address[0],
logger.debug(
"Successfully bound to %s:%s using %s", new_address[0],
new_address[1], "IPv6" if ip_version else "IPv4")
except socket.error:
if self.servers:
# Already bound using IPv6.
logger.debug(
"Certbot wasn't able to bind to %s:%s using %s, this " +
"is often expected due to the dual stack nature of " +
"IPv6 socket implementations.",
new_address[0], new_address[1],
"IPv6" if ip_version else "IPv4")
else:
logger.debug(
"Failed to bind to %s:%s using %s", new_address[0],
new_address[1], "IPv6" if ip_version else "IPv4")
else:
self.servers.append(server)
# If two servers are set up and port 0 was passed in, ensure we always
@@ -189,7 +203,7 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
self.simple_http_resources = kwargs.pop("simple_http_resources", set())
socketserver.BaseRequestHandler.__init__(self, *args, **kwargs)
BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
def log_message(self, format, *args): # pylint: disable=redefined-builtin
"""Log arbitrary message."""
@@ -262,7 +276,7 @@ def simple_tls_sni_01_server(cli_args, forever=True):
certs = {}
_, hosts, _ = next(os.walk('.'))
_, hosts, _ = next(os.walk('.')) # type: ignore # https://github.com/python/mypy/issues/465
for host in hosts:
with open(os.path.join(host, "cert.pem")) as cert_file:
cert_contents = cert_file.read()

View File

@@ -4,10 +4,10 @@ import shutil
import socket
import threading
import tempfile
import time
import unittest
from six.moves import http_client # pylint: disable=import-error
from six.moves import queue # pylint: disable=import-error
from six.moves import socketserver # type: ignore # pylint: disable=import-error
import josepy as jose
@@ -16,8 +16,8 @@ import requests
from acme import challenges
from acme import crypto_util
from acme import errors
from acme import test_util
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
class TLSServerTest(unittest.TestCase):
@@ -72,7 +72,7 @@ class HTTP01ServerTest(unittest.TestCase):
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
self.resources = set()
self.resources = set() # type: Set
from acme.standalone import HTTP01Server
self.server = HTTP01Server(('', 0), resources=self.resources)
@@ -201,7 +201,7 @@ class HTTP01DualNetworkedServersTest(unittest.TestCase):
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
self.resources = set()
self.resources = set() # type: Set
from acme.standalone import HTTP01DualNetworkedServers
self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources)
@@ -260,10 +260,9 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
os.path.join(localhost_dir, 'key.pem'))
from acme.standalone import simple_tls_sni_01_server
self.port = 1234
self.thread = threading.Thread(
target=simple_tls_sni_01_server, kwargs={
'cli_args': ('xxx', '--port', str(self.port)),
'cli_args': ('filename',),
'forever': False,
},
)
@@ -275,25 +274,20 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
self.thread.join()
shutil.rmtree(self.test_cwd)
def test_it(self):
max_attempts = 5
for attempt in range(max_attempts):
try:
cert = crypto_util.probe_sni(
b'localhost', b'0.0.0.0', self.port)
except errors.Error:
self.assertTrue(attempt + 1 < max_attempts, "Timeout!")
time.sleep(1) # wait until thread starts
else:
self.assertEqual(jose.ComparableX509(cert),
test_util.load_comparable_cert(
'rsa2048_cert.pem'))
break
@mock.patch('acme.standalone.logger')
def test_it(self, mock_logger):
# Use a Queue because mock objects aren't thread safe.
q = queue.Queue() # type: queue.Queue[int]
# Add port number to the queue.
mock_logger.info.side_effect = lambda *args: q.put(args[-1])
self.thread.start()
if attempt == 0:
# the first attempt is always meant to fail, so we can test
# the socket failure code-path for probe_sni, as well
self.thread.start()
# After the timeout, an exception is raised if the queue is empty.
port = q.get(timeout=5)
cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', port)
self.assertEqual(jose.ComparableX509(cert),
test_util.load_comparable_cert(
'rsa2048_cert.pem'))
if __name__ == "__main__":

View File

@@ -10,7 +10,7 @@ import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import josepy as jose
import OpenSSL
from OpenSSL import crypto
def vector_path(*names):
@@ -39,8 +39,8 @@ def _guess_loader(filename, loader_pem, loader_der):
def load_cert(*names):
"""Load certificate."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_certificate(loader, load_vector(*names))
def load_comparable_cert(*names):
@@ -51,8 +51,8 @@ def load_comparable_cert(*names):
def load_csr(*names):
"""Load certificate request."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_certificate_request(loader, load_vector(*names))
def load_comparable_csr(*names):
@@ -71,8 +71,8 @@ def load_rsa_private_key(*names):
def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_privatekey(loader, load_vector(*names))
def skip_unless(condition, reason): # pragma: no cover

BIN
acme/acme/testdata/cert-nocn.der vendored Normal file

Binary file not shown.

View File

@@ -1,5 +0,0 @@
Other ACME objects
------------------
.. automodule:: acme.other
:members:

2
acme/pytest.ini Normal file
View File

@@ -0,0 +1,2 @@
[pytest]
norecursedirs = .* build dist CVS _darcs {arch} *.egg

View File

@@ -1,10 +1,9 @@
import sys
from setuptools import setup
from setuptools import find_packages
from setuptools.command.test import test as TestCommand
import sys
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
@@ -19,19 +18,11 @@ install_requires = [
'pyrfc3339',
'pytz',
'requests[security]>=2.4.1', # security extras added in 2.4.1
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'requests-toolbelt>=0.3.0',
'setuptools',
'six>=1.9.0', # needed for python_2_unicode_compatible
]
# env markers cause problems with older pip and setuptools
if sys.version_info < (2, 7):
install_requires.extend([
'argparse',
'ordereddict',
])
dev_extras = [
'pytest',
'pytest-xdist',
@@ -43,6 +34,19 @@ docs_extras = [
'sphinx_rtd_theme',
]
class PyTest(TestCommand):
user_options = []
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ''
def run_tests(self):
import shlex
# import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno)
setup(
name='acme',
@@ -52,16 +56,15 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
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.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
@@ -76,5 +79,7 @@ setup(
'dev': dev_extras,
'docs': docs_extras,
},
tests_require=["pytest"],
test_suite='acme',
cmdclass={"test": PyTest},
)

View File

@@ -1,4 +1,5 @@
""" Utility functions for certbot-apache plugin """
import binascii
import os
from certbot import util
@@ -98,3 +99,8 @@ def parse_define_file(filepath, varname):
var_parts = v[2:].partition("=")
return_vars[var_parts[0]] = var_parts[2]
return return_vars
def unique_id():
""" Returns an unique id to be used as a VirtualHost identifier"""
return binascii.hexlify(os.urandom(16)).decode("utf-8")

View File

@@ -5,6 +5,7 @@ import logging
import os
import pkg_resources
import re
import six
import socket
import time
@@ -12,13 +13,16 @@ import zope.component
import zope.interface
from acme import challenges
from acme.magic_typing import Any, DefaultDict, Dict, List, Set, Union # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
from certbot.plugins import common
from certbot.plugins.util import path_surgery
from certbot.plugins.enhancements import AutoHSTSEnhancement
from certbot_apache import apache_util
from certbot_apache import augeas_configurator
@@ -129,10 +133,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
default=cls.OS_DEFAULTS["challenge_location"],
help="Directory path for challenge configuration.")
add("handle-modules", default=cls.OS_DEFAULTS["handle_mods"],
help="Let installer handle enabling required modules for you." +
help="Let installer handle enabling required modules for you. " +
"(Only Ubuntu/Debian currently)")
add("handle-sites", default=cls.OS_DEFAULTS["handle_sites"],
help="Let installer handle enabling sites for you." +
help="Let installer handle enabling sites for you. " +
"(Only Ubuntu/Debian currently)")
util.add_deprecated_argument(add, argument_name="ctl", nargs=1)
util.add_deprecated_argument(
@@ -149,11 +153,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
super(ApacheConfigurator, self).__init__(*args, **kwargs)
# Add name_server association dict
self.assoc = dict()
self.assoc = dict() # type: Dict[str, obj.VirtualHost]
# Outstanding challenges
self._chall_out = set()
self._chall_out = set() # type: Set[KeyAuthorizationAnnotatedChallenge]
# List of vhosts configured per wildcard domain on this run.
# used by deploy_cert() and enhance()
self._wildcard_vhosts = dict() # type: Dict[str, List[obj.VirtualHost]]
# Maps enhancements to vhosts we've enabled the enhancement for
self._enhanced_vhosts = defaultdict(set)
self._enhanced_vhosts = defaultdict(set) # type: DefaultDict[str, Set[obj.VirtualHost]]
# Temporary state for AutoHSTS enhancement
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
# These will be set in the prepare function
self.parser = None
@@ -164,9 +173,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"ensure-http-header": self._set_http_header,
"staple-ocsp": self._enable_ocsp_stapling}
# This will be set during the perform function
self.http_doer = None
@property
def mod_ssl_conf(self):
"""Full absolute path to SSL configuration file."""
@@ -265,12 +271,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.aug, self.conf("server-root"), self.conf("vhost-root"),
self.version, configurator=self)
def _wildcard_domain(self, domain):
"""
Checks if domain is a wildcard domain
:param str domain: Domain to check
:returns: If the domain is wildcard domain
:rtype: bool
"""
if isinstance(domain, six.text_type):
wildcard_marker = u"*."
else:
wildcard_marker = b"*."
return domain.startswith(wildcard_marker)
def deploy_cert(self, domain, cert_path, key_path,
chain_path=None, fullchain_path=None):
"""Deploys certificate to specified virtual host.
Currently tries to find the last directives to deploy the cert in
the VHost associated with the given domain. If it can't find the
Currently tries to find the last directives to deploy the certificate
in the VHost associated with the given domain. If it can't find the
directives, it searches the "included" confs. The function verifies
that it has located the three directives and finally modifies them
to point to the correct destination. After the certificate is
@@ -283,9 +304,112 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
a lack of directives
"""
# Choose vhost before (possible) enabling of mod_ssl, to keep the
# vhost choice namespace similar with the pre-validation one.
vhost = self.choose_vhost(domain)
vhosts = self.choose_vhosts(domain)
for vhost in vhosts:
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
def choose_vhosts(self, domain, create_if_no_ssl=True):
"""
Finds VirtualHosts that can be used with the provided domain
:param str domain: Domain name to match VirtualHosts to
:param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS
counterpart, should one get created
:returns: List of VirtualHosts or None
:rtype: `list` of :class:`~certbot_apache.obj.VirtualHost`
"""
if self._wildcard_domain(domain):
if domain in self._wildcard_vhosts:
# Vhosts for a wildcard domain were already selected
return self._wildcard_vhosts[domain]
# Ask user which VHosts to support.
# Returned objects are guaranteed to be ssl vhosts
return self._choose_vhosts_wildcard(domain, create_if_no_ssl)
else:
return [self.choose_vhost(domain, create_if_no_ssl)]
def _vhosts_for_wildcard(self, domain):
"""
Get VHost objects for every VirtualHost that the user wants to handle
with the wildcard certificate.
"""
# Collect all vhosts that match the name
matched = set()
for vhost in self.vhosts:
for name in vhost.get_names():
if self._in_wildcard_scope(name, domain):
matched.add(vhost)
return list(matched)
def _in_wildcard_scope(self, name, domain):
"""
Helper method for _vhosts_for_wildcard() that makes sure that the domain
is in the scope of wildcard domain.
eg. in scope: domain = *.wild.card, name = 1.wild.card
not in scope: domain = *.wild.card, name = 1.2.wild.card
"""
if len(name.split(".")) == len(domain.split(".")):
return fnmatch.fnmatch(name, domain)
def _choose_vhosts_wildcard(self, domain, create_ssl=True):
"""Prompts user to choose vhosts to install a wildcard certificate for"""
# Get all vhosts that are covered by the wildcard domain
vhosts = self._vhosts_for_wildcard(domain)
# Go through the vhosts, making sure that we cover all the names
# present, but preferring the SSL vhosts
filtered_vhosts = dict()
for vhost in vhosts:
for name in vhost.get_names():
if vhost.ssl:
# Always prefer SSL vhosts
filtered_vhosts[name] = vhost
elif name not in filtered_vhosts and create_ssl:
# Add if not in list previously
filtered_vhosts[name] = vhost
# Only unique VHost objects
dialog_input = set([vhost for vhost in filtered_vhosts.values()])
# Ask the user which of names to enable, expect list of names back
dialog_output = display_ops.select_vhost_multiple(list(dialog_input))
if not dialog_output:
logger.error(
"No vhost exists with servername or alias for domain %s. "
"No vhost was selected. Please specify ServerName or ServerAlias "
"in the Apache config.",
domain)
raise errors.PluginError("No vhost selected")
# Make sure we create SSL vhosts for the ones that are HTTP only
# if requested.
return_vhosts = list()
for vhost in dialog_output:
if not vhost.ssl:
return_vhosts.append(self.make_vhost_ssl(vhost))
else:
return_vhosts.append(vhost)
self._wildcard_vhosts[domain] = return_vhosts
return return_vhosts
def _deploy_cert(self, vhost, cert_path, key_path, chain_path, fullchain_path):
"""
Helper function for deploy_cert() that handles the actual deployment
this exists because we might want to do multiple deployments per
domain originally passed for deploy_cert(). This is especially true
with wildcard certificates
"""
# This is done first so that ssl module is enabled and cert_path,
# cert_key... can all be parsed appropriately
@@ -305,16 +429,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
path["chain_path"] = self.parser.find_dir(
"SSLCertificateChainFile", None, vhost.path)
if not path["cert_path"] or not path["cert_key"]:
# Throw some can't find all of the directives error"
# Handle errors when certificate/key directives cannot be found
if not path["cert_path"]:
logger.warning(
"Cannot find a cert or key directive in %s. "
"Cannot find an SSLCertificateFile directive in %s. "
"VirtualHost was not modified", vhost.path)
# Presumably break here so that the virtualhost is not modified
raise errors.PluginError(
"Unable to find cert and/or key directives")
"Unable to find an SSLCertificateFile directive")
elif not path["cert_key"]:
logger.warning(
"Cannot find an SSLCertificateKeyFile directive for "
"certificate in %s. VirtualHost was not modified", vhost.path)
raise errors.PluginError(
"Unable to find an SSLCertificateKeyFile directive for "
"certificate")
logger.info("Deploying Certificate for %s to VirtualHost %s", domain, vhost.filep)
logger.info("Deploying Certificate to VirtualHost %s", vhost.filep)
if self.version < (2, 4, 8) or (chain_path and not fullchain_path):
# install SSLCertificateFile, SSLCertificateKeyFile,
@@ -330,8 +460,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"version of Apache")
else:
if not fullchain_path:
raise errors.PluginError("Please provide the --fullchain-path\
option pointing to your full chain file")
raise errors.PluginError("Please provide the --fullchain-path "
"option pointing to your full chain file")
set_cert_path = fullchain_path
self.aug.set(path["cert_path"][-1], fullchain_path)
self.aug.set(path["cert_key"][-1], key_path)
@@ -350,20 +480,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if chain_path is not None:
self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path
def choose_vhost(self, target_name, temp=False):
def choose_vhost(self, target_name, create_if_no_ssl=True):
"""Chooses a virtual host based on the given domain name.
If there is no clear virtual host to be selected, the user is prompted
with all available choices.
The returned vhost is guaranteed to have TLS enabled unless temp is
True. If temp is True, there is no such guarantee and the result is
not cached.
The returned vhost is guaranteed to have TLS enabled unless
create_if_no_ssl is set to False, in which case there is no such guarantee
and the result is not cached.
:param str target_name: domain name
:param bool temp: whether the vhost is only used temporarily
:param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS
counterpart, should one get created
:returns: ssl vhost associated with name
:returns: vhost associated with name
:rtype: :class:`~certbot_apache.obj.VirtualHost`
:raises .errors.PluginError: If no vhost is available or chosen
@@ -376,7 +507,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Try to find a reasonable vhost
vhost = self._find_best_vhost(target_name)
if vhost is not None:
if temp:
if not create_if_no_ssl:
return vhost
if not vhost.ssl:
vhost = self.make_vhost_ssl(vhost)
@@ -385,7 +516,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.assoc[target_name] = vhost
return vhost
return self._choose_vhost_from_list(target_name, temp)
# Negate create_if_no_ssl value to indicate if we want a SSL vhost
# to get created if a non-ssl vhost is selected.
return self._choose_vhost_from_list(target_name, temp=not create_if_no_ssl)
def _choose_vhost_from_list(self, target_name, temp=False):
# Select a vhost from a list
@@ -394,7 +527,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
logger.error(
"No vhost exists with servername or alias of %s. "
"No vhost was selected. Please specify ServerName or ServerAlias "
"in the Apache config, or split vhosts into separate files.",
"in the Apache config.",
target_name)
raise errors.PluginError("No vhost selected")
elif temp:
@@ -439,12 +572,35 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return True
return False
def _find_best_vhost(self, target_name):
def find_best_http_vhost(self, target, filter_defaults, port="80"):
"""Returns non-HTTPS vhost objects found from the Apache config
:param str target: Domain name of the desired VirtualHost
:param bool filter_defaults: whether _default_ vhosts should be
included if it is the best match
:param str port: port number the vhost should be listening on
:returns: VirtualHost object that's the best match for target name
:rtype: `obj.VirtualHost` or None
"""
filtered_vhosts = []
for vhost in self.vhosts:
if any(a.is_wildcard() or a.get_port() == port for a in vhost.addrs) and not vhost.ssl:
filtered_vhosts.append(vhost)
return self._find_best_vhost(target, filtered_vhosts, filter_defaults)
def _find_best_vhost(self, target_name, vhosts=None, filter_defaults=True):
"""Finds the best vhost for a target_name.
This does not upgrade a vhost to HTTPS... it only finds the most
appropriate vhost for the given target_name.
:param str target_name: domain handled by the desired vhost
:param vhosts: vhosts to consider
:type vhosts: `collections.Iterable` of :class:`~certbot_apache.obj.VirtualHost`
:param bool filter_defaults: whether a vhost with a _default_
addr is acceptable
:returns: VHost or None
"""
@@ -456,7 +612,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Points 1 - Address name with no SSL
best_candidate = None
best_points = 0
for vhost in self.vhosts:
if vhosts is None:
vhosts = self.vhosts
for vhost in vhosts:
if vhost.modmacro is True:
continue
names = vhost.get_names()
@@ -480,8 +640,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# No winners here... is there only one reasonable vhost?
if best_candidate is None:
# reasonable == Not all _default_ addrs
vhosts = self._non_default_vhosts()
if filter_defaults:
vhosts = self._non_default_vhosts(vhosts)
# remove mod_macro hosts from reasonable vhosts
reasonable_vhosts = [vh for vh
in vhosts if vh.modmacro is False]
@@ -490,9 +650,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return best_candidate
def _non_default_vhosts(self):
def _non_default_vhosts(self, vhosts):
"""Return all non _default_ only vhosts."""
return [vh for vh in self.vhosts if not all(
return [vh for vh in vhosts if not all(
addr.get_addr() == "_default_" for addr in vh.addrs
)]
@@ -504,7 +664,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:rtype: set
"""
all_names = set()
all_names = set() # type: Set[str]
vhost_macro = []
@@ -645,8 +805,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
# Search base config, and all included paths for VirtualHosts
file_paths = {}
internal_paths = defaultdict(set)
file_paths = {} # type: Dict[str, str]
internal_paths = defaultdict(set) # type: DefaultDict[str, Set[str]]
vhs = []
# Make a list of parser paths because the parser_paths
# dictionary may be modified during the loop.
@@ -1084,7 +1244,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if not self.parser.parsed_in_current(ssl_fp):
self.parser.parse_file(ssl_fp)
except IOError:
logger.fatal("Error writing/reading to file in make_vhost_ssl")
logger.critical("Error writing/reading to file in make_vhost_ssl", exc_info=True)
raise errors.PluginError("Unable to write/read in make_vhost_ssl")
if sift:
@@ -1172,7 +1332,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
try:
span_val = self.aug.span(vhost.path)
except ValueError:
logger.fatal("Error while reading the VirtualHost %s from "
logger.critical("Error while reading the VirtualHost %s from "
"file %s", vhost.name, vhost.filep, exc_info=True)
raise errors.PluginError("Unable to read VirtualHost from file")
span_filep = span_val[0]
@@ -1245,7 +1405,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"insert_cert_file_path")
self.parser.add_dir(vh_path, "SSLCertificateKeyFile",
"insert_key_file_path")
self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf)
# Only include the TLS configuration if not already included
existing_inc = self.parser.find_dir("Include", self.mod_ssl_conf, vh_path)
if not existing_inc:
self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf)
def _add_servername_alias(self, target_name, vhost):
vh_path = vhost.path
@@ -1312,6 +1475,67 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if need_to_save:
self.save()
def find_vhost_by_id(self, id_str):
"""
Searches through VirtualHosts and tries to match the id in a comment
:param str id_str: Id string for matching
:returns: The matched VirtualHost or None
:rtype: :class:`~certbot_apache.obj.VirtualHost` or None
:raises .errors.PluginError: If no VirtualHost is found
"""
for vh in self.vhosts:
if self._find_vhost_id(vh) == id_str:
return vh
msg = "No VirtualHost with ID {} was found.".format(id_str)
logger.warning(msg)
raise errors.PluginError(msg)
def _find_vhost_id(self, vhost):
"""Tries to find the unique ID from the VirtualHost comments. This is
used for keeping track of VirtualHost directive over time.
:param vhost: Virtual host to add the id
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:returns: The unique ID or None
:rtype: str or None
"""
# Strip the {} off from the format string
search_comment = constants.MANAGED_COMMENT_ID.format("")
id_comment = self.parser.find_comments(search_comment, vhost.path)
if id_comment:
# Use the first value, multiple ones shouldn't exist
comment = self.parser.get_arg(id_comment[0])
return comment.split(" ")[-1]
return None
def add_vhost_id(self, vhost):
"""Adds an unique ID to the VirtualHost as a comment for mapping back
to it on later invocations, as the config file order might have changed.
If ID already exists, returns that instead.
:param vhost: Virtual host to add or find the id
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:returns: The unique ID for vhost
:rtype: str or None
"""
vh_id = self._find_vhost_id(vhost)
if vh_id:
return vh_id
id_string = apache_util.unique_id()
comment = constants.MANAGED_COMMENT_ID.format(id_string)
self.parser.add_comment(vhost.path, comment)
return id_string
def _escape(self, fp):
fp = fp.replace(",", "\\,")
fp = fp.replace("[", "\\[")
@@ -1349,12 +1573,100 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
except KeyError:
raise errors.PluginError(
"Unsupported enhancement: {0}".format(enhancement))
matched_vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)
# We should be handling only SSL vhosts for enhancements
vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
if not vhosts:
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
"domain {0} for enabling enhancement \"{1}\". The requested "
"enhancement was not configured.")
msg_enhancement = enhancement
if options:
msg_enhancement += ": " + options
msg = msg_tmpl.format(domain, msg_enhancement)
logger.warning(msg)
raise errors.PluginError(msg)
try:
func(self.choose_vhost(domain), options)
for vhost in vhosts:
func(vhost, options)
except errors.PluginError:
logger.warning("Failed %s for %s", enhancement, domain)
raise
def _autohsts_increase(self, vhost, id_str, nextstep):
"""Increase the AutoHSTS max-age value
:param vhost: Virtual host object to modify
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:param str id_str: The unique ID string of VirtualHost
:param int nextstep: Next AutoHSTS max-age value index
"""
nextstep_value = constants.AUTOHSTS_STEPS[nextstep]
self._autohsts_write(vhost, nextstep_value)
self._autohsts[id_str] = {"laststep": nextstep, "timestamp": time.time()}
def _autohsts_write(self, vhost, nextstep_value):
"""
Write the new HSTS max-age value to the VirtualHost file
"""
hsts_dirpath = None
header_path = self.parser.find_dir("Header", None, vhost.path)
if header_path:
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
for match in header_path:
if re.search(pat, self.aug.get(match).lower()):
hsts_dirpath = match
if not hsts_dirpath:
err_msg = ("Certbot was unable to find the existing HSTS header "
"from the VirtualHost at path {0}.").format(vhost.filep)
raise errors.PluginError(err_msg)
# Prepare the HSTS header value
hsts_maxage = "\"max-age={0}\"".format(nextstep_value)
# Update the header
# Our match statement was for string strict-transport-security, but
# we need to update the value instead. The next index is for the value
hsts_dirpath = hsts_dirpath.replace("arg[3]", "arg[4]")
self.aug.set(hsts_dirpath, hsts_maxage)
note_msg = ("Increasing HSTS max-age value to {0} for VirtualHost "
"in {1}\n".format(nextstep_value, vhost.filep))
logger.debug(note_msg)
self.save_notes += note_msg
self.save(note_msg)
def _autohsts_fetch_state(self):
"""
Populates the AutoHSTS state from the pluginstorage
"""
try:
self._autohsts = self.storage.fetch("autohsts")
except KeyError:
self._autohsts = dict()
def _autohsts_save_state(self):
"""
Saves the state of AutoHSTS object to pluginstorage
"""
self.storage.put("autohsts", self._autohsts)
self.storage.save()
def _autohsts_vhost_in_lineage(self, vhost, lineage):
"""
Searches AutoHSTS managed VirtualHosts that belong to the lineage.
Matches the private key path.
"""
return bool(
self.parser.find_dir("SSLCertificateKeyFile",
lineage.key_path, vhost.path))
def _enable_ocsp_stapling(self, ssl_vhost, unused_options):
"""Enables OCSP Stapling
@@ -1596,7 +1908,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# There can be other RewriteRule directive lines in vhost config.
# rewrite_args_dict keys are directive ids and the corresponding value
# for each is a list of arguments to that directive.
rewrite_args_dict = defaultdict(list)
rewrite_args_dict = defaultdict(list) # type: DefaultDict[str, List[str]]
pat = r'(.*directive\[\d+\]).*'
for match in rewrite_path:
m = re.match(pat, match)
@@ -1690,7 +2002,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if ssl_vhost.aliases:
serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases)
rewrite_rule_args = []
rewrite_rule_args = [] # type: List[str]
if self.get_version() >= (2, 3, 9):
rewrite_rule_args = constants.REWRITE_HTTPS_ARGS_WITH_END
else:
@@ -1842,10 +2154,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:raises .errors.MisconfigurationError: If reload fails
"""
error = ""
try:
util.run_script(self.constant("restart_cmd"))
except errors.SubprocessError as err:
raise errors.MisconfigurationError(str(err))
logger.info("Unable to restart apache using %s",
self.constant("restart_cmd"))
alt_restart = self.constant("restart_cmd_alt")
if alt_restart:
logger.debug("Trying alternative restart command: %s",
alt_restart)
# There is an alternative restart command available
# This usually is "restart" verb while original is "graceful"
try:
util.run_script(self.constant(
"restart_cmd_alt"))
return
except errors.SubprocessError as secerr:
error = str(secerr)
else:
error = str(err)
raise errors.MisconfigurationError(error)
def config_test(self): # pylint: disable=no-self-use
"""Check the configuration of Apache for errors.
@@ -1911,7 +2240,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
self._chall_out.update(achalls)
responses = [None] * len(achalls)
self.http_doer = http_01.ApacheHttp01(self)
http_doer = http_01.ApacheHttp01(self)
sni_doer = tls_sni_01.ApacheTlsSni01(self)
for i, achall in enumerate(achalls):
@@ -1919,11 +2248,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# challenge. This helps to put all of the responses back together
# when they are all complete.
if isinstance(achall.chall, challenges.HTTP01):
self.http_doer.add_chall(achall, i)
http_doer.add_chall(achall, i)
else: # tls-sni-01
sni_doer.add_chall(achall, i)
http_response = self.http_doer.perform()
http_response = http_doer.perform()
sni_response = sni_doer.perform()
if http_response or sni_response:
# Must reload in order to activate the challenges.
@@ -1935,7 +2264,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# of identifying when the new configuration is being used.
time.sleep(3)
self._update_responses(responses, http_response, self.http_doer)
self._update_responses(responses, http_response, http_doer)
self._update_responses(responses, sni_response, sni_doer)
return responses
@@ -1956,7 +2285,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.revert_challenge_config()
self.restart()
self.parser.reset_modules()
self.http_doer.cleanup()
def install_ssl_options_conf(self, options_ssl, options_ssl_digest):
"""Copy Certbot's SSL options file into the system's config dir if required."""
@@ -1967,4 +2295,176 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
def enable_autohsts(self, _unused_lineage, domains):
"""
Enable the AutoHSTS enhancement for defined domains
:param _unused_lineage: Certificate lineage object, unused
:type _unused_lineage: certbot.storage.RenewableCert
:param domains: List of domains in certificate to enhance
:type domains: str
"""
self._autohsts_fetch_state()
_enhanced_vhosts = []
for d in domains:
matched_vhosts = self.choose_vhosts(d, create_if_no_ssl=False)
# We should be handling only SSL vhosts for AutoHSTS
vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
if not vhosts:
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
"domain {0} for enabling AutoHSTS enhancement.")
msg = msg_tmpl.format(d)
logger.warning(msg)
raise errors.PluginError(msg)
for vh in vhosts:
try:
self._enable_autohsts_domain(vh)
_enhanced_vhosts.append(vh)
except errors.PluginEnhancementAlreadyPresent:
if vh in _enhanced_vhosts:
continue
msg = ("VirtualHost for domain {0} in file {1} has a " +
"String-Transport-Security header present, exiting.")
raise errors.PluginEnhancementAlreadyPresent(
msg.format(d, vh.filep))
if _enhanced_vhosts:
note_msg = "Enabling AutoHSTS"
self.save(note_msg)
logger.info(note_msg)
self.restart()
# Save the current state to pluginstorage
self._autohsts_save_state()
def _enable_autohsts_domain(self, ssl_vhost):
"""Do the initial AutoHSTS deployment to a vhost
:param ssl_vhost: The VirtualHost object to deploy the AutoHSTS
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost` or None
:raises errors.PluginEnhancementAlreadyPresent: When already enhanced
"""
# This raises the exception
self._verify_no_matching_http_header(ssl_vhost,
"Strict-Transport-Security")
if "headers_module" not in self.parser.modules:
self.enable_mod("headers")
# Prepare the HSTS header value
hsts_header = constants.HEADER_ARGS["Strict-Transport-Security"][:-1]
initial_maxage = constants.AUTOHSTS_STEPS[0]
hsts_header.append("\"max-age={0}\"".format(initial_maxage))
# Add ID to the VirtualHost for mapping back to it later
uniq_id = self.add_vhost_id(ssl_vhost)
self.save_notes += "Adding unique ID {0} to VirtualHost in {1}\n".format(
uniq_id, ssl_vhost.filep)
# Add the actual HSTS header
self.parser.add_dir(ssl_vhost.path, "Header", hsts_header)
note_msg = ("Adding gradually increasing HSTS header with initial value "
"of {0} to VirtualHost in {1}\n".format(
initial_maxage, ssl_vhost.filep))
self.save_notes += note_msg
# Save the current state to pluginstorage
self._autohsts[uniq_id] = {"laststep": 0, "timestamp": time.time()}
def update_autohsts(self, _unused_domain):
"""
Increase the AutoHSTS values of VirtualHosts that the user has enabled
this enhancement for.
:param _unused_domain: Not currently used
:type _unused_domain: Not Available
"""
self._autohsts_fetch_state()
if not self._autohsts:
# No AutoHSTS enabled for any domain
return
curtime = time.time()
save_and_restart = False
for id_str, config in list(self._autohsts.items()):
if config["timestamp"] + constants.AUTOHSTS_FREQ > curtime:
# Skip if last increase was < AUTOHSTS_FREQ ago
continue
nextstep = config["laststep"] + 1
if nextstep < len(constants.AUTOHSTS_STEPS):
# Have not reached the max value yet
try:
vhost = self.find_vhost_by_id(id_str)
except errors.PluginError:
msg = ("Could not find VirtualHost with ID {0}, disabling "
"AutoHSTS for this VirtualHost").format(id_str)
logger.warning(msg)
# Remove the orphaned AutoHSTS entry from pluginstorage
self._autohsts.pop(id_str)
continue
self._autohsts_increase(vhost, id_str, nextstep)
msg = ("Increasing HSTS max-age value for VirtualHost with id "
"{0}").format(id_str)
self.save_notes += msg
save_and_restart = True
if save_and_restart:
self.save("Increased HSTS max-age values")
self.restart()
self._autohsts_save_state()
def deploy_autohsts(self, lineage):
"""
Checks if autohsts vhost has reached maximum auto-increased value
and changes the HSTS max-age to a high value.
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
"""
self._autohsts_fetch_state()
if not self._autohsts:
# No autohsts enabled for any vhost
return
vhosts = []
affected_ids = []
# Copy, as we are removing from the dict inside the loop
for id_str, config in list(self._autohsts.items()):
if config["laststep"]+1 >= len(constants.AUTOHSTS_STEPS):
# max value reached, try to make permanent
try:
vhost = self.find_vhost_by_id(id_str)
except errors.PluginError:
msg = ("VirtualHost with id {} was not found, unable to "
"make HSTS max-age permanent.").format(id_str)
logger.warning(msg)
self._autohsts.pop(id_str)
continue
if self._autohsts_vhost_in_lineage(vhost, lineage):
vhosts.append(vhost)
affected_ids.append(id_str)
save_and_restart = False
for vhost in vhosts:
self._autohsts_write(vhost, constants.AUTOHSTS_PERMANENT)
msg = ("Strict-Transport-Security max-age value for "
"VirtualHost in {0} was made permanent.").format(vhost.filep)
logger.debug(msg)
self.save_notes += msg+"\n"
save_and_restart = True
if save_and_restart:
self.save("Made HSTS max-age permanent")
self.restart()
for id_str in affected_ids:
self._autohsts.pop(id_str)
# Update AutoHSTS storage (We potentially removed vhosts from managed)
self._autohsts_save_state()
AutoHSTSEnhancement.register(ApacheConfigurator) # pylint: disable=no-member

View File

@@ -48,3 +48,16 @@ UIR_ARGS = ["always", "set", "Content-Security-Policy",
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
"Upgrade-Insecure-Requests": UIR_ARGS}
AUTOHSTS_STEPS = [60, 300, 900, 3600, 21600, 43200, 86400]
"""AutoHSTS increase steps: 1min, 5min, 15min, 1h, 6h, 12h, 24h"""
AUTOHSTS_PERMANENT = 31536000
"""Value for the last max-age of HSTS"""
AUTOHSTS_FREQ = 172800
"""Minimum time since last increase to perform a new one: 48h"""
MANAGED_COMMENT = "DO NOT REMOVE - Managed by Certbot"
MANAGED_COMMENT_ID = MANAGED_COMMENT+", VirtualHost id: {0}"
"""Managed by Certbot comments and the VirtualHost identification template"""

View File

@@ -13,10 +13,44 @@ import certbot.display.util as display_util
logger = logging.getLogger(__name__)
def select_vhost_multiple(vhosts):
"""Select multiple Vhosts to install the certificate for
:param vhosts: Available Apache VirtualHosts
:type vhosts: :class:`list` of type `~obj.Vhost`
:returns: List of VirtualHosts
:rtype: :class:`list`of type `~obj.Vhost`
"""
if not vhosts:
return list()
tags_list = [vhost.display_repr()+"\n" for vhost in vhosts]
# Remove the extra newline from the last entry
if len(tags_list):
tags_list[-1] = tags_list[-1][:-1]
code, names = zope.component.getUtility(interfaces.IDisplay).checklist(
"Which VirtualHosts would you like to install the wildcard certificate for?",
tags=tags_list, force_interactive=True)
if code == display_util.OK:
return_vhosts = _reversemap_vhosts(names, vhosts)
return return_vhosts
return []
def _reversemap_vhosts(names, vhosts):
"""Helper function for select_vhost_multiple for mapping string
representations back to actual vhost objects"""
return_vhosts = list()
for selection in names:
for vhost in vhosts:
if vhost.display_repr().strip() == selection.strip():
return_vhosts.append(vhost)
return return_vhosts
def select_vhost(domain, vhosts):
"""Select an appropriate Apache Vhost.
:param vhosts: Available Apache Virtual Hosts
:param vhosts: Available Apache VirtualHosts
:type vhosts: :class:`list` of type `~obj.Vhost`
:returns: VirtualHost or `None`
@@ -25,13 +59,11 @@ def select_vhost(domain, vhosts):
"""
if not vhosts:
return None
while True:
code, tag = _vhost_menu(domain, vhosts)
if code == display_util.OK:
return vhosts[tag]
else:
return None
code, tag = _vhost_menu(domain, vhosts)
if code == display_util.OK:
return vhosts[tag]
else:
return None
def _vhost_menu(domain, vhosts):
"""Select an appropriate Apache Vhost.

View File

@@ -17,6 +17,7 @@ OVERRIDE_CLASSES = {
"centos": override_centos.CentOSConfigurator,
"centos linux": override_centos.CentOSConfigurator,
"fedora": override_centos.CentOSConfigurator,
"ol": override_centos.CentOSConfigurator,
"red hat enterprise linux server": override_centos.CentOSConfigurator,
"rhel": override_centos.CentOSConfigurator,
"amazon": override_centos.CentOSConfigurator,

View File

@@ -1,41 +1,58 @@
"""A class that performs HTTP-01 challenges for Apache"""
import logging
import os
import shutil
import tempfile
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot.plugins import common
from certbot_apache.obj import VirtualHost # pylint: disable=unused-import
logger = logging.getLogger(__name__)
class ApacheHttp01(common.TLSSNI01):
"""Class that performs HTPP-01 challenges within the Apache configurator."""
"""Class that performs HTTP-01 challenges within the Apache configurator."""
CONFIG_TEMPLATE24 = """\
Alias /.well-known/acme-challenge {0}
CONFIG_TEMPLATE22_PRE = """\
RewriteEngine on
RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [L]
<Directory {0} >
Require all granted
</Directory>
"""
CONFIG_TEMPLATE22_POST = """\
<Directory {0}>
Order Allow,Deny
Allow from all
</Directory>
<Location /.well-known/acme-challenge>
Order Allow,Deny
Allow from all
</Location>
"""
"""
CONFIG_TEMPLATE22 = """\
Alias /.well-known/acme-challenge {0}
<Directory {0} >
Order allow,deny
Allow from all
</Directory>
"""
CONFIG_TEMPLATE24_PRE = """\
RewriteEngine on
RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [END]
"""
CONFIG_TEMPLATE24_POST = """\
<Directory {0}>
Require all granted
</Directory>
<Location /.well-known/acme-challenge>
Require all granted
</Location>
"""
def __init__(self, *args, **kwargs):
super(ApacheHttp01, self).__init__(*args, **kwargs)
self.challenge_conf = os.path.join(
self.challenge_conf_pre = os.path.join(
self.configurator.conf("challenge-location"),
"le_http_01_challenge.conf")
self.challenge_dir = None
"le_http_01_challenge_pre.conf")
self.challenge_conf_post = os.path.join(
self.configurator.conf("challenge-location"),
"le_http_01_challenge_post.conf")
self.challenge_dir = os.path.join(
self.configurator.config.work_dir,
"http_challenges")
self.moded_vhosts = set() # type: Set[VirtualHost]
def perform(self):
"""Perform all HTTP-01 challenges."""
@@ -50,23 +67,18 @@ Alias /.well-known/acme-challenge {0}
self.prepare_http01_modules()
responses = self._set_up_challenges()
self._mod_config()
# Save reversible changes
self.configurator.save("HTTP Challenge", True)
return responses
def cleanup(self):
"""Cleanup the challenge directory."""
if self.challenge_dir:
shutil.rmtree(self.challenge_dir, ignore_errors=True)
self.challenge_dir = None
def prepare_http01_modules(self):
"""Make sure that we have the needed modules available for http01"""
if self.configurator.conf("handle-modules"):
needed_modules = ["alias"]
needed_modules = ["rewrite"]
if self.configurator.version < (2, 4):
needed_modules.append("authz_host")
else:
@@ -76,24 +88,58 @@ Alias /.well-known/acme-challenge {0}
self.configurator.enable_mod(mod, temp=True)
def _mod_config(self):
self.configurator.parser.add_include(
self.configurator.parser.loc["default"], self.challenge_conf)
for chall in self.achalls:
vh = self.configurator.find_best_http_vhost(
chall.domain, filter_defaults=False,
port=str(self.configurator.config.http01_port))
if vh:
self._set_up_include_directives(vh)
else:
for vh in self._relevant_vhosts():
self._set_up_include_directives(vh)
self.configurator.reverter.register_file_creation(
True, self.challenge_conf)
True, self.challenge_conf_pre)
self.configurator.reverter.register_file_creation(
True, self.challenge_conf_post)
if self.configurator.version < (2, 4):
config_template = self.CONFIG_TEMPLATE22
config_template_pre = self.CONFIG_TEMPLATE22_PRE
config_template_post = self.CONFIG_TEMPLATE22_POST
else:
config_template = self.CONFIG_TEMPLATE24
config_text = config_template.format(self.challenge_dir)
config_template_pre = self.CONFIG_TEMPLATE24_PRE
config_template_post = self.CONFIG_TEMPLATE24_POST
logger.debug("writing a config file with text:\n %s", config_text)
with open(self.challenge_conf, "w") as new_conf:
new_conf.write(config_text)
config_text_pre = config_template_pre.format(self.challenge_dir)
config_text_post = config_template_post.format(self.challenge_dir)
logger.debug("writing a pre config file with text:\n %s", config_text_pre)
with open(self.challenge_conf_pre, "w") as new_conf:
new_conf.write(config_text_pre)
logger.debug("writing a post config file with text:\n %s", config_text_post)
with open(self.challenge_conf_post, "w") as new_conf:
new_conf.write(config_text_post)
def _relevant_vhosts(self):
http01_port = str(self.configurator.config.http01_port)
relevant_vhosts = []
for vhost in self.configurator.vhosts:
if any(a.is_wildcard() or a.get_port() == http01_port for a in vhost.addrs):
if not vhost.ssl:
relevant_vhosts.append(vhost)
if not relevant_vhosts:
raise errors.PluginError(
"Unable to find a virtual host listening on port {0} which is"
" currently needed for Certbot to prove to the CA that you"
" control your domain. Please add a virtual host for port"
" {0}.".format(http01_port))
return relevant_vhosts
def _set_up_challenges(self):
self.challenge_dir = tempfile.mkdtemp()
os.chmod(self.challenge_dir, 0o755)
if not os.path.isdir(self.challenge_dir):
os.makedirs(self.challenge_dir)
os.chmod(self.challenge_dir, 0o755)
responses = []
for achall in self.achalls:
@@ -105,8 +151,25 @@ Alias /.well-known/acme-challenge {0}
response, validation = achall.response_and_validation()
name = os.path.join(self.challenge_dir, achall.chall.encode("token"))
self.configurator.reverter.register_file_creation(True, name)
with open(name, 'wb') as f:
f.write(validation.encode())
os.chmod(name, 0o644)
return response
def _set_up_include_directives(self, vhost):
"""Includes override configuration to the beginning and to the end of
VirtualHost. Note that this include isn't added to Augeas search tree"""
if vhost not in self.moded_vhosts:
logger.debug(
"Adding a temporary challenge validation Include for name: %s " +
"in: %s", vhost.name, vhost.filep)
self.configurator.parser.add_dir_beginning(
vhost.path, "Include", self.challenge_conf_pre)
self.configurator.parser.add_dir(
vhost.path, "Include", self.challenge_conf_post)
self.moded_vhosts.add(vhost)

View File

@@ -1,6 +1,7 @@
"""Module contains classes used by the Apache Configurator."""
import re
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot.plugins import common
@@ -140,7 +141,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
def get_names(self):
"""Return a set of all names."""
all_names = set()
all_names = set() # type: Set[str]
all_names.update(self.aliases)
# Strip out any scheme:// and <port> field from servername
if self.name is not None:
@@ -167,6 +168,19 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
active="Yes" if self.enabled else "No",
modmacro="Yes" if self.modmacro else "No"))
def display_repr(self):
"""Return a representation of VHost to be used in dialog"""
return (
"File: {filename}\n"
"Addresses: {addrs}\n"
"Names: {names}\n"
"HTTPS: {https}\n".format(
filename=self.filep,
addrs=", ".join(str(addr) for addr in self.addrs),
names=", ".join(self.get_names()),
https="Yes" if self.ssl else "No"))
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.filep == other.filep and self.path == other.path and
@@ -238,7 +252,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
# already_found acts to keep everything very conservative.
# Don't allow multiple ip:ports in same set.
already_found = set()
already_found = set() # type: Set[str]
for addr in vhost.addrs:
for local_addr in self.addrs:

View File

@@ -21,6 +21,7 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
version_cmd=['apachectl', '-v'],
apache_cmd="apachectl",
restart_cmd=['apachectl', 'graceful'],
restart_cmd_alt=['apachectl', 'restart'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
@@ -46,10 +47,10 @@ class CentOSParser(parser.ApacheParser):
self.sysconfig_filep = "/etc/sysconfig/httpd"
super(CentOSParser, self).__init__(*args, **kwargs)
def update_runtime_variables(self, *args, **kwargs):
def update_runtime_variables(self):
""" Override for update_runtime_variables for custom parsing """
# Opportunistic, works if SELinux not enforced
super(CentOSParser, self).update_runtime_variables(*args, **kwargs)
super(CentOSParser, self).update_runtime_variables()
self.parse_sysconfig_var()
def parse_sysconfig_var(self):

View File

@@ -21,6 +21,7 @@ class GentooConfigurator(configurator.ApacheConfigurator):
version_cmd=['/usr/sbin/apache2', '-v'],
apache_cmd="apache2ctl",
restart_cmd=['apache2ctl', 'graceful'],
restart_cmd_alt=['apache2ctl', 'restart'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod=None,
dismod=None,

View File

@@ -9,12 +9,14 @@ import sys
import six
from acme.magic_typing import Dict, List, Set # pylint: disable=unused-import, no-name-in-module
from certbot import errors
logger = logging.getLogger(__name__)
class ApacheParser(object):
# pylint: disable=too-many-public-methods
"""Class handles the fine details of parsing the Apache Configuration.
.. todo:: Make parsing general... remove sites-available etc...
@@ -38,9 +40,9 @@ class ApacheParser(object):
# issues with aug.load() after adding new files / defines to parse tree
self.configurator = configurator
self.modules = set()
self.parser_paths = {}
self.variables = {}
self.modules = set() # type: Set[str]
self.parser_paths = {} # type: Dict[str, List[str]]
self.variables = {} # type: Dict[str, str]
self.aug = aug
# Find configuration root and make sure augeas can parse it.
@@ -119,7 +121,7 @@ class ApacheParser(object):
the iteration issue. Else... parse and enable mods at same time.
"""
mods = set()
mods = set() # type: Set[str]
matches = self.find_dir("LoadModule")
iterator = iter(matches)
# Make sure prev_size != cur_size for do: while: iteration
@@ -332,6 +334,54 @@ class ApacheParser(object):
else:
self.aug.set(aug_conf_path + "/directive[last()]/arg", args)
def add_dir_beginning(self, aug_conf_path, dirname, args):
"""Adds the directive to the beginning of defined aug_conf_path.
:param str aug_conf_path: Augeas configuration path to add directive
:param str dirname: Directive to add
:param args: Value of the directive. ie. Listen 443, 443 is arg
:type args: list or str
"""
first_dir = aug_conf_path + "/directive[1]"
self.aug.insert(first_dir, "directive", True)
self.aug.set(first_dir, dirname)
if isinstance(args, list):
for i, value in enumerate(args, 1):
self.aug.set(first_dir + "/arg[%d]" % (i), value)
else:
self.aug.set(first_dir + "/arg", args)
def add_comment(self, aug_conf_path, comment):
"""Adds the comment to the augeas path
:param str aug_conf_path: Augeas configuration path to add directive
:param str comment: Comment content
"""
self.aug.set(aug_conf_path + "/#comment[last() + 1]", comment)
def find_comments(self, arg, start=None):
"""Finds a comment with specified content from the provided DOM path
:param str arg: Comment content to search
:param str start: Beginning Augeas path to begin looking
:returns: List of augeas paths containing the comment content
:rtype: list
"""
if not start:
start = get_aug_path(self.root)
comments = self.aug.match("%s//*[label() = '#comment']" % start)
results = []
for comment in comments:
c_content = self.aug.get(comment)
if c_content and arg in c_content:
results.append(comment)
return results
def find_dir(self, directive, arg=None, start=None, exclude=True):
"""Finds directive in the configuration.
@@ -391,7 +441,7 @@ class ApacheParser(object):
else:
arg_suffix = "/*[self::arg=~regexp('%s')]" % case_i(arg)
ordered_matches = []
ordered_matches = [] # type: List[str]
# TODO: Wildcards should be included in alphabetical order
# https://httpd.apache.org/docs/2.4/mod/core.html#include

View File

@@ -0,0 +1,181 @@
# pylint: disable=too-many-public-methods,too-many-lines
"""Test for certbot_apache.configurator AutoHSTS functionality"""
import re
import unittest
import mock
# six is used in mock.patch()
import six # pylint: disable=unused-import
from certbot import errors
from certbot_apache import constants
from certbot_apache.tests import util
class AutoHSTSTest(util.ApacheTest):
"""Tests for AutoHSTS feature"""
# pylint: disable=protected-access
def setUp(self): # pylint: disable=arguments-differ
super(AutoHSTSTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.config.parser.modules.add("headers_module")
self.config.parser.modules.add("mod_headers.c")
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
def get_autohsts_value(self, vh_path):
""" Get value from Strict-Transport-Security header """
header_path = self.config.parser.find_dir("Header", None, vh_path)
if header_path:
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
for head in header_path:
if re.search(pat, self.config.parser.aug.get(head).lower()):
return self.config.parser.aug.get(head.replace("arg[3]",
"arg[4]"))
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
def test_autohsts_enable_headers_mod(self, mock_enable, _restart):
self.config.parser.modules.discard("headers_module")
self.config.parser.modules.discard("mod_header.c")
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
self.assertTrue(mock_enable.called)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
def test_autohsts_deploy_already_exists(self, _restart):
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
self.assertRaises(errors.PluginEnhancementAlreadyPresent,
self.config.enable_autohsts,
mock.MagicMock(), ["ocspvhost.com"])
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
def test_autohsts_increase(self, _mock_restart):
maxage = "\"max-age={0}\""
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
# Verify initial value
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
initial_val)
# Increase
self.config.update_autohsts(mock.MagicMock())
# Verify increased value
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
inc_val)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator._autohsts_increase")
def test_autohsts_increase_noop(self, mock_increase, _restart):
maxage = "\"max-age={0}\""
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
# Verify initial value
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
initial_val)
self.config.update_autohsts(mock.MagicMock())
# Freq not patched, so value shouldn't increase
self.assertFalse(mock_increase.called)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
def test_autohsts_increase_no_header(self, _restart):
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
# Remove the header
dir_locs = self.config.parser.find_dir("Header", None,
self.vh_truth[7].path)
dir_loc = "/".join(dir_locs[0].split("/")[:-1])
self.config.parser.aug.remove(dir_loc)
self.assertRaises(errors.PluginError,
self.config.update_autohsts,
mock.MagicMock())
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
def test_autohsts_increase_and_make_permanent(self, _mock_restart):
maxage = "\"max-age={0}\""
max_val = maxage.format(constants.AUTOHSTS_PERMANENT)
mock_lineage = mock.MagicMock()
mock_lineage.key_path = "/etc/apache2/ssl/key-certbot_15.pem"
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
for i in range(len(constants.AUTOHSTS_STEPS)-1):
# Ensure that value is not made permanent prematurely
self.config.deploy_autohsts(mock_lineage)
self.assertNotEquals(self.get_autohsts_value(self.vh_truth[7].path),
max_val)
self.config.update_autohsts(mock.MagicMock())
# Value should match pre-permanent increment step
cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
cur_val)
# Make permanent
self.config.deploy_autohsts(mock_lineage)
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
max_val)
def test_autohsts_update_noop(self):
with mock.patch("time.time") as mock_time:
# Time mock is used to make sure that the execution does not
# continue when no autohsts entries exist in pluginstorage
self.config.update_autohsts(mock.MagicMock())
self.assertFalse(mock_time.called)
def test_autohsts_make_permanent_noop(self):
self.config.storage.put = mock.MagicMock()
self.config.deploy_autohsts(mock.MagicMock())
# Make sure that the execution does not continue when no entries in store
self.assertFalse(self.config.storage.put.called)
@mock.patch("certbot_apache.display_ops.select_vhost")
def test_autohsts_no_ssl_vhost(self, mock_select):
mock_select.return_value = self.vh_truth[0]
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
self.assertRaises(errors.PluginError,
self.config.enable_autohsts,
mock.MagicMock(), "invalid.example.com")
self.assertTrue(
"Certbot was not able to find SSL" in mock_log.call_args[0][0])
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.add_vhost_id")
def test_autohsts_dont_enhance_twice(self, mock_id, _restart):
mock_id.return_value = "1234567"
self.config.enable_autohsts(mock.MagicMock(),
["ocspvhost.com", "ocspvhost.com"])
self.assertEquals(mock_id.call_count, 1)
def test_autohsts_remove_orphaned(self):
# pylint: disable=protected-access
self.config._autohsts_fetch_state()
self.config._autohsts["orphan_id"] = {"laststep": 0, "timestamp": 0}
self.config._autohsts_save_state()
self.config.update_autohsts(mock.MagicMock())
self.assertFalse("orphan_id" in self.config._autohsts)
# Make sure it's removed from the pluginstorage file as well
self.config._autohsts = None
self.config._autohsts_fetch_state()
self.assertFalse(self.config._autohsts)
def test_autohsts_make_permanent_vhost_not_found(self):
# pylint: disable=protected-access
self.config._autohsts_fetch_state()
self.config._autohsts["orphan_id"] = {"laststep": 999, "timestamp": 0}
self.config._autohsts_save_state()
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
self.config.deploy_autohsts(mock.MagicMock())
self.assertTrue(mock_log.called)
self.assertTrue(
"VirtualHost with id orphan_id was not" in mock_log.call_args[0][0])
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -4,6 +4,8 @@ import unittest
import mock
from certbot import errors
from certbot_apache import obj
from certbot_apache import override_centos
from certbot_apache.tests import util
@@ -121,5 +123,17 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
self.assertTrue("MOCK_NOSEP" in self.config.parser.variables.keys())
self.assertEqual("NOSEP_VAL", self.config.parser.variables["NOSEP_TWO"])
@mock.patch("certbot_apache.configurator.util.run_script")
def test_alt_restart_works(self, mock_run_script):
mock_run_script.side_effect = [None, errors.SubprocessError, None]
self.config.restart()
self.assertEquals(mock_run_script.call_count, 3)
@mock.patch("certbot_apache.configurator.util.run_script")
def test_alt_restart_errors(self, mock_run_script):
mock_run_script.side_effect = [None,
errors.SubprocessError,
errors.SubprocessError]
self.assertRaises(errors.MisconfigurationError, self.config.restart)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -126,7 +126,7 @@ class MultipleVhostsTest(util.ApacheTest):
names = self.config.get_all_names()
self.assertEqual(names, set(
["certbot.demo", "ocspvhost.com", "encryption-example.demo",
"nonsym.link", "vhost.in.rootconf"]
"nonsym.link", "vhost.in.rootconf", "www.certbot.demo"]
))
@certbot_util.patch_get_utility()
@@ -146,7 +146,7 @@ class MultipleVhostsTest(util.ApacheTest):
names = self.config.get_all_names()
# Names get filtered, only 5 are returned
self.assertEqual(len(names), 7)
self.assertEqual(len(names), 8)
self.assertTrue("zombo.com" in names)
self.assertTrue("google.com" in names)
self.assertTrue("certbot.demo" in names)
@@ -246,7 +246,7 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot_apache.display_ops.select_vhost")
def test_choose_vhost_select_vhost_with_temp(self, mock_select):
mock_select.return_value = self.vh_truth[0]
chosen_vhost = self.config.choose_vhost("none.com", temp=True)
chosen_vhost = self.config.choose_vhost("none.com", create_if_no_ssl=False)
self.assertEqual(self.vh_truth[0], chosen_vhost)
@mock.patch("certbot_apache.display_ops.select_vhost")
@@ -260,6 +260,20 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertRaises(
errors.PluginError, self.config.choose_vhost, "none.com")
def test_find_best_http_vhost_default(self):
vh = obj.VirtualHost(
"fp", "ap", set([obj.Addr.fromstring("_default_:80")]), False, True)
self.config.vhosts = [vh]
self.assertEqual(self.config.find_best_http_vhost("foo.bar", False), vh)
def test_find_best_http_vhost_port(self):
port = "8080"
vh = obj.VirtualHost(
"fp", "ap", set([obj.Addr.fromstring("*:" + port)]),
False, True, "encryption-example.demo")
self.config.vhosts.append(vh)
self.assertEqual(self.config.find_best_http_vhost("foo.bar", False, port), vh)
def test_findbest_continues_on_short_domain(self):
# pylint: disable=protected-access
chosen_vhost = self.config._find_best_vhost("purple.com")
@@ -305,7 +319,8 @@ class MultipleVhostsTest(util.ApacheTest):
def test_non_default_vhosts(self):
# pylint: disable=protected-access
self.assertEqual(len(self.config._non_default_vhosts()), 8)
vhosts = self.config._non_default_vhosts(self.config.vhosts)
self.assertEqual(len(vhosts), 8)
def test_deploy_cert_enable_new_vhost(self):
# Create
@@ -320,6 +335,30 @@ class MultipleVhostsTest(util.ApacheTest):
"example/cert_chain.pem", "example/fullchain.pem")
self.assertTrue(ssl_vhost.enabled)
def test_no_duplicate_include(self):
def mock_find_dir(directive, argument, _):
"""Mock method for parser.find_dir"""
if directive == "Include" and argument.endswith("options-ssl-apache.conf"):
return ["/path/to/whatever"]
mock_add = mock.MagicMock()
self.config.parser.add_dir = mock_add
self.config._add_dummy_ssl_directives(self.vh_truth[0]) # pylint: disable=protected-access
tried_to_add = False
for a in mock_add.call_args_list:
if a[0][1] == "Include" and a[0][2] == self.config.mod_ssl_conf:
tried_to_add = True
# Include should be added, find_dir is not patched, and returns falsy
self.assertTrue(tried_to_add)
self.config.parser.find_dir = mock_find_dir
mock_add.reset_mock()
self.config._add_dummy_ssl_directives(self.vh_truth[0]) # pylint: disable=protected-access
for a in mock_add.call_args_list:
if a[0][1] == "Include" and a[0][2] == self.config.mod_ssl_conf:
self.fail("Include shouldn't be added, as patched find_dir 'finds' existing one") \
# pragma: no cover
def test_deploy_cert(self):
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
@@ -399,13 +438,37 @@ class MultipleVhostsTest(util.ApacheTest):
self.vh_truth[1].path))
def test_deploy_cert_invalid_vhost(self):
"""For test cases where the `ApacheConfigurator` class' `_deploy_cert`
method is called with an invalid vhost parameter. Currently this tests
that a PluginError is appropriately raised when important directives
are missing in an SSL module."""
self.config.parser.modules.add("ssl_module")
mock_find = mock.MagicMock()
mock_find.return_value = []
self.config.parser.find_dir = mock_find
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
def side_effect(*args):
"""Mocks case where an SSLCertificateFile directive can be found
but an SSLCertificateKeyFile directive is missing."""
if "SSLCertificateFile" in args:
return ["example/cert.pem"]
else:
return []
mock_find_dir = mock.MagicMock(return_value=[])
mock_find_dir.side_effect = side_effect
self.config.parser.find_dir = mock_find_dir
# 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")
# Remove side_effect to mock case where both SSLCertificateFile
# and SSLCertificateKeyFile directives are missing
self.config.parser.find_dir.side_effect = None
self.assertRaises(
errors.PluginError, self.config.deploy_cert, "random.demo",
"example/cert.pem", "example/key.pem", "example/cert_chain.pem")
@@ -459,7 +522,11 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(mock_add_dir.call_count, 3)
self.assertTrue(mock_add_dir.called)
self.assertEqual(mock_add_dir.call_args[0][1], "Listen")
self.assertEqual(mock_add_dir.call_args[0][2], ['1.2.3.4:8080'])
call_found = False
for mock_call in mock_add_dir.mock_calls:
if mock_call[1][2] == ['1.2.3.4:8080']:
call_found = True
self.assertTrue(call_found)
def test_prepare_server_https(self):
mock_enable = mock.Mock()
@@ -747,7 +814,6 @@ class MultipleVhostsTest(util.ApacheTest):
def test_cleanup(self, mock_cfg, mock_restart):
mock_cfg.return_value = ""
_, achalls = self.get_key_and_achalls()
self.config.http_doer = mock.MagicMock()
for achall in achalls:
self.config._chall_out.add(achall) # pylint: disable=protected-access
@@ -756,10 +822,8 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.cleanup([achall])
if i == len(achalls) - 1:
self.assertTrue(mock_restart.called)
self.assertTrue(self.config.http_doer.cleanup.called)
else:
self.assertFalse(mock_restart.called)
self.assertFalse(self.config.http_doer.cleanup.called)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
@@ -773,11 +837,9 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.cleanup([achalls[-1]])
self.assertFalse(mock_restart.called)
self.assertFalse(self.config.http_doer.cleanup.called)
self.config.cleanup(achalls)
self.assertTrue(mock_restart.called)
self.assertTrue(self.config.http_doer.cleanup.called)
@mock.patch("certbot.util.run_script")
def test_get_version(self, mock_script):
@@ -871,6 +933,22 @@ class MultipleVhostsTest(util.ApacheTest):
errors.PluginError,
self.config.enhance, "certbot.demo", "unknown_enhancement")
def test_enhance_no_ssl_vhost(self):
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
self.assertRaises(errors.PluginError, self.config.enhance,
"certbot.demo", "redirect")
# Check that correct logger.warning was printed
self.assertTrue("not able to find" in mock_log.call_args[0][0])
self.assertTrue("\"redirect\"" in mock_log.call_args[0][0])
mock_log.reset_mock()
self.assertRaises(errors.PluginError, self.config.enhance,
"certbot.demo", "ensure-http-header", "Test")
# Check that correct logger.warning was printed
self.assertTrue("not able to find" in mock_log.call_args[0][0])
self.assertTrue("Test" in mock_log.call_args[0][0])
@mock.patch("certbot.util.exe_exists")
def test_ocsp_stapling(self, mock_exe):
self.config.parser.update_runtime_variables = mock.Mock()
@@ -880,6 +958,7 @@ class MultipleVhostsTest(util.ApacheTest):
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "staple-ocsp")
# Get the ssl vhost for certbot.demo
@@ -906,6 +985,7 @@ class MultipleVhostsTest(util.ApacheTest):
mock_exe.return_value = True
# Checking the case with already enabled ocsp stapling configuration
self.config.choose_vhost("ocspvhost.com")
self.config.enhance("ocspvhost.com", "staple-ocsp")
# Get the ssl vhost for letsencrypt.demo
@@ -930,6 +1010,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module")
self.config.get_version = mock.Mock(return_value=(2, 2, 0))
self.config.choose_vhost("certbot.demo")
self.assertRaises(errors.PluginError,
self.config.enhance, "certbot.demo", "staple-ocsp")
@@ -955,6 +1036,7 @@ class MultipleVhostsTest(util.ApacheTest):
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "ensure-http-header",
"Strict-Transport-Security")
@@ -974,7 +1056,8 @@ class MultipleVhostsTest(util.ApacheTest):
# skip the enable mod
self.config.parser.modules.add("headers_module")
# This will create an ssl vhost for certbot.demo
# This will create an ssl vhost for encryption-example.demo
self.config.choose_vhost("encryption-example.demo")
self.config.enhance("encryption-example.demo", "ensure-http-header",
"Strict-Transport-Security")
@@ -993,6 +1076,7 @@ class MultipleVhostsTest(util.ApacheTest):
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "ensure-http-header",
"Upgrade-Insecure-Requests")
@@ -1014,7 +1098,8 @@ class MultipleVhostsTest(util.ApacheTest):
# skip the enable mod
self.config.parser.modules.add("headers_module")
# This will create an ssl vhost for certbot.demo
# This will create an ssl vhost for encryption-example.demo
self.config.choose_vhost("encryption-example.demo")
self.config.enhance("encryption-example.demo", "ensure-http-header",
"Upgrade-Insecure-Requests")
@@ -1032,6 +1117,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.get_version = mock.Mock(return_value=(2, 2))
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "redirect")
# These are not immediately available in find_dir even with save() and
@@ -1082,6 +1168,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.save()
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "redirect")
# These are not immediately available in find_dir even with save() and
@@ -1148,6 +1235,9 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
# Creates ssl vhost for the domain
self.config.choose_vhost("red.blue.purple.com")
self.config.enhance("red.blue.purple.com", "redirect")
verify_no_redirect = ("certbot_apache.configurator."
"ApacheConfigurator._verify_no_certbot_redirect")
@@ -1159,7 +1249,7 @@ class MultipleVhostsTest(util.ApacheTest):
# Skip the enable mod
self.config.parser.modules.add("rewrite_module")
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
self.config.choose_vhost("red.blue.purple.com")
self.config.enhance("red.blue.purple.com", "redirect")
# Clear state about enabling redirect on this run
# pylint: disable=protected-access
@@ -1296,6 +1386,123 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.enable_mod,
"whatever")
def test_wildcard_domain(self):
# pylint: disable=protected-access
cases = {u"*.example.org": True, b"*.x.example.org": True,
u"a.example.org": False, b"a.x.example.org": False}
for key in cases.keys():
self.assertEqual(self.config._wildcard_domain(key), cases[key])
def test_choose_vhosts_wildcard(self):
# pylint: disable=protected-access
mock_path = "certbot_apache.display_ops.select_vhost_multiple"
with mock.patch(mock_path) as mock_select_vhs:
mock_select_vhs.return_value = [self.vh_truth[3]]
vhs = self.config._choose_vhosts_wildcard("*.certbot.demo",
create_ssl=True)
# Check that the dialog was called with one vh: certbot.demo
self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[3])
self.assertEquals(len(mock_select_vhs.call_args_list), 1)
# And the actual returned values
self.assertEquals(len(vhs), 1)
self.assertTrue(vhs[0].name == "certbot.demo")
self.assertTrue(vhs[0].ssl)
self.assertFalse(vhs[0] == self.vh_truth[3])
@mock.patch("certbot_apache.configurator.ApacheConfigurator.make_vhost_ssl")
def test_choose_vhosts_wildcard_no_ssl(self, mock_makessl):
# pylint: disable=protected-access
mock_path = "certbot_apache.display_ops.select_vhost_multiple"
with mock.patch(mock_path) as mock_select_vhs:
mock_select_vhs.return_value = [self.vh_truth[1]]
vhs = self.config._choose_vhosts_wildcard("*.certbot.demo",
create_ssl=False)
self.assertFalse(mock_makessl.called)
self.assertEquals(vhs[0], self.vh_truth[1])
@mock.patch("certbot_apache.configurator.ApacheConfigurator._vhosts_for_wildcard")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.make_vhost_ssl")
def test_choose_vhosts_wildcard_already_ssl(self, mock_makessl, mock_vh_for_w):
# pylint: disable=protected-access
# Already SSL vhost
mock_vh_for_w.return_value = [self.vh_truth[7]]
mock_path = "certbot_apache.display_ops.select_vhost_multiple"
with mock.patch(mock_path) as mock_select_vhs:
mock_select_vhs.return_value = [self.vh_truth[7]]
vhs = self.config._choose_vhosts_wildcard("whatever",
create_ssl=True)
self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[7])
self.assertEquals(len(mock_select_vhs.call_args_list), 1)
# Ensure that make_vhost_ssl was not called, vhost.ssl == true
self.assertFalse(mock_makessl.called)
# And the actual returned values
self.assertEquals(len(vhs), 1)
self.assertTrue(vhs[0].ssl)
self.assertEquals(vhs[0], self.vh_truth[7])
def test_deploy_cert_wildcard(self):
# pylint: disable=protected-access
mock_choose_vhosts = mock.MagicMock()
mock_choose_vhosts.return_value = [self.vh_truth[7]]
self.config._choose_vhosts_wildcard = mock_choose_vhosts
mock_d = "certbot_apache.configurator.ApacheConfigurator._deploy_cert"
with mock.patch(mock_d) as mock_dep:
self.config.deploy_cert("*.wildcard.example.org", "/tmp/path",
"/tmp/path", "/tmp/path", "/tmp/path")
self.assertTrue(mock_dep.called)
self.assertEquals(len(mock_dep.call_args_list), 1)
self.assertEqual(self.vh_truth[7], mock_dep.call_args_list[0][0][0])
@mock.patch("certbot_apache.display_ops.select_vhost_multiple")
def test_deploy_cert_wildcard_no_vhosts(self, mock_dialog):
# pylint: disable=protected-access
mock_dialog.return_value = []
self.assertRaises(errors.PluginError,
self.config.deploy_cert,
"*.wild.cat", "/tmp/path", "/tmp/path",
"/tmp/path", "/tmp/path")
@mock.patch("certbot_apache.configurator.ApacheConfigurator._choose_vhosts_wildcard")
def test_enhance_wildcard_after_install(self, mock_choose):
# pylint: disable=protected-access
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("headers_module")
self.vh_truth[3].ssl = True
self.config._wildcard_vhosts["*.certbot.demo"] = [self.vh_truth[3]]
self.config.enhance("*.certbot.demo", "ensure-http-header",
"Upgrade-Insecure-Requests")
self.assertFalse(mock_choose.called)
@mock.patch("certbot_apache.configurator.ApacheConfigurator._choose_vhosts_wildcard")
def test_enhance_wildcard_no_install(self, mock_choose):
self.vh_truth[3].ssl = True
mock_choose.return_value = [self.vh_truth[3]]
self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("headers_module")
self.config.enhance("*.certbot.demo", "ensure-http-header",
"Upgrade-Insecure-Requests")
self.assertTrue(mock_choose.called)
def test_add_vhost_id(self):
for vh in [self.vh_truth[0], self.vh_truth[1], self.vh_truth[2]]:
vh_id = self.config.add_vhost_id(vh)
self.assertEqual(vh, self.config.find_vhost_by_id(vh_id))
def test_find_vhost_by_id_404(self):
self.assertRaises(errors.PluginError,
self.config.find_vhost_by_id,
"nonexistent")
def test_add_vhost_id_already_exists(self):
first_id = self.config.add_vhost_id(self.vh_truth[0])
second_id = self.config.add_vhost_id(self.vh_truth[0])
self.assertEqual(first_id, second_id)
class AugeasVhostsTest(util.ApacheTest):
"""Test vhosts with illegal names dependent on augeas version."""
# pylint: disable=protected-access

View File

@@ -161,6 +161,8 @@ class MultipleVhostsTestDebian(util.ApacheTest):
self.config.parser.modules.add("mod_ssl.c")
self.config.get_version = mock.Mock(return_value=(2, 4, 7))
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "staple-ocsp")
self.assertTrue("socache_shmcb_module" in self.config.parser.modules)
@@ -172,6 +174,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
mock_exe.return_value = True
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "ensure-http-header",
"Strict-Transport-Security")
self.assertTrue("headers_module" in self.config.parser.modules)
@@ -183,6 +186,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
mock_exe.return_value = True
self.config.get_version = mock.Mock(return_value=(2, 2))
# This will create an ssl vhost for certbot.demo
self.config.choose_vhost("certbot.demo")
self.config.enhance("certbot.demo", "redirect")
self.assertTrue("rewrite_module" in self.config.parser.modules)

View File

@@ -11,9 +11,39 @@ from certbot.tests import util as certbot_util
from certbot_apache import obj
from certbot_apache.display_ops import select_vhost_multiple
from certbot_apache.tests import util
class SelectVhostMultiTest(unittest.TestCase):
"""Tests for certbot_apache.display_ops.select_vhost_multiple."""
def setUp(self):
self.base_dir = "/example_path"
self.vhosts = util.get_vh_truth(
self.base_dir, "debian_apache_2_4/multiple_vhosts")
def test_select_no_input(self):
self.assertFalse(select_vhost_multiple([]))
@certbot_util.patch_get_utility()
def test_select_correct(self, mock_util):
mock_util().checklist.return_value = (
display_util.OK, [self.vhosts[3].display_repr(),
self.vhosts[2].display_repr()])
vhs = select_vhost_multiple([self.vhosts[3],
self.vhosts[2],
self.vhosts[1]])
self.assertTrue(self.vhosts[2] in vhs)
self.assertTrue(self.vhosts[3] in vhs)
self.assertFalse(self.vhosts[1] in vhs)
@certbot_util.patch_get_utility()
def test_select_cancel(self, mock_util):
mock_util().checklist.return_value = (display_util.CANCEL, "whatever")
vhs = select_vhost_multiple([self.vhosts[2], self.vhosts[3]])
self.assertFalse(vhs)
class SelectVhostTest(unittest.TestCase):
"""Tests for certbot_apache.display_ops.select_vhost."""

View File

@@ -4,6 +4,8 @@ import unittest
import mock
from certbot import errors
from certbot_apache import override_gentoo
from certbot_apache import obj
from certbot_apache.tests import util
@@ -123,5 +125,11 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
self.assertEquals(len(self.config.parser.modules), 4)
self.assertTrue("mod_another.c" in self.config.parser.modules)
@mock.patch("certbot_apache.configurator.util.run_script")
def test_alt_restart_works(self, mock_run_script):
mock_run_script.side_effect = [None, errors.SubprocessError, None]
self.config.restart()
self.assertEquals(mock_run_script.call_count, 3)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -4,55 +4,41 @@ import os
import unittest
from acme import challenges
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from certbot import achallenges
from certbot import errors
from certbot.tests import acme_util
from certbot_apache.tests import util
NUM_ACHALLS = 3
class ApacheHttp01TestMeta(type):
"""Generates parmeterized tests for testing perform."""
def __new__(mcs, name, bases, class_dict):
def _gen_test(num_achalls, minor_version):
def _test(self):
achalls = self.achalls[:num_achalls]
self.config.version = (2, minor_version)
self.common_perform_test(achalls)
return _test
for i in range(1, NUM_ACHALLS + 1):
for j in (2, 4):
test_name = "test_perform_{0}_{1}".format(i, j)
class_dict[test_name] = _gen_test(i, j)
return type.__new__(mcs, name, bases, class_dict)
class ApacheHttp01Test(util.ApacheTest):
"""Test for certbot_apache.http_01.ApacheHttp01."""
__metaclass__ = ApacheHttp01TestMeta
def setUp(self, *args, **kwargs):
super(ApacheHttp01Test, self).setUp(*args, **kwargs)
self.account_key = self.rsa512jwk
self.achalls = []
self.achalls = [] # type: List[achallenges.KeyAuthorizationAnnotatedChallenge]
vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
# Takes the vhosts for encryption-example.demo, certbot.demo, and
# vhost.in.rootconf
self.vhosts = [vh_truth[0], vh_truth[3], vh_truth[10]]
for i in range(NUM_ACHALLS):
self.achalls.append(
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.HTTP01(token=((chr(ord('a') + i) * 16))),
challenges.HTTP01(token=((chr(ord('a') + i).encode() * 16))),
"pending"),
domain="example{0}.com".format(i),
account_key=self.account_key))
domain=self.vhosts[i].name, account_key=self.account_key))
modules = ["alias", "authz_core", "authz_host"]
modules = ["rewrite", "authz_core", "authz_host"]
for mod in modules:
self.config.parser.modules.add("mod_{0}.c".format(mod))
self.config.parser.modules.add(mod + "_module")
@@ -64,7 +50,7 @@ class ApacheHttp01Test(util.ApacheTest):
self.assertFalse(self.http.perform())
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
def test_enable_modules_22(self, mock_enmod):
def test_enable_modules_apache_2_2(self, mock_enmod):
self.config.version = (2, 2)
self.config.parser.modules.remove("authz_host_module")
self.config.parser.modules.remove("mod_authz_host.c")
@@ -73,7 +59,7 @@ class ApacheHttp01Test(util.ApacheTest):
self.assertEqual(enmod_calls[0][0][0], "authz_host")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
def test_enable_modules_24(self, mock_enmod):
def test_enable_modules_apache_2_4(self, mock_enmod):
self.config.parser.modules.remove("authz_core_module")
self.config.parser.modules.remove("mod_authz_core.c")
@@ -81,9 +67,9 @@ class ApacheHttp01Test(util.ApacheTest):
self.assertEqual(enmod_calls[0][0][0], "authz_core")
def common_enable_modules_test(self, mock_enmod):
"""Tests enabling mod_alias and other modules."""
self.config.parser.modules.remove("alias_module")
self.config.parser.modules.remove("mod_alias.c")
"""Tests enabling mod_rewrite and other modules."""
self.config.parser.modules.remove("rewrite_module")
self.config.parser.modules.remove("mod_rewrite.c")
self.http.prepare_http01_modules()
@@ -91,15 +77,74 @@ class ApacheHttp01Test(util.ApacheTest):
calls = mock_enmod.call_args_list
other_calls = []
for call in calls:
if "alias" != call[0][0]:
if "rewrite" != call[0][0]:
other_calls.append(call)
# If these lists are equal, we never enabled mod_alias
# If these lists are equal, we never enabled mod_rewrite
self.assertNotEqual(calls, other_calls)
return other_calls
def common_perform_test(self, achalls):
def test_same_vhost(self):
vhost = next(v for v in self.config.vhosts if v.name == "certbot.demo")
achalls = [
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.HTTP01(token=((b'a' * 16))),
"pending"),
domain=vhost.name, account_key=self.account_key),
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.HTTP01(token=((b'b' * 16))),
"pending"),
domain=next(iter(vhost.aliases)), account_key=self.account_key)
]
self.common_perform_test(achalls, [vhost])
def test_anonymous_vhost(self):
vhosts = [v for v in self.config.vhosts if not v.ssl]
achalls = [
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.HTTP01(token=((b'a' * 16))),
"pending"),
domain="something.nonexistent", account_key=self.account_key)]
self.common_perform_test(achalls, vhosts)
def test_no_vhost(self):
for achall in self.achalls:
self.http.add_chall(achall)
self.config.config.http01_port = 12345
self.assertRaises(errors.PluginError, self.http.perform)
def test_perform_1_achall_apache_2_2(self):
self.combinations_perform_test(num_achalls=1, minor_version=2)
def test_perform_1_achall_apache_2_4(self):
self.combinations_perform_test(num_achalls=1, minor_version=4)
def test_perform_2_achall_apache_2_2(self):
self.combinations_perform_test(num_achalls=2, minor_version=2)
def test_perform_2_achall_apache_2_4(self):
self.combinations_perform_test(num_achalls=2, minor_version=4)
def test_perform_3_achall_apache_2_2(self):
self.combinations_perform_test(num_achalls=3, minor_version=2)
def test_perform_3_achall_apache_2_4(self):
self.combinations_perform_test(num_achalls=3, minor_version=4)
def combinations_perform_test(self, num_achalls, minor_version):
"""Test perform with the given achall count and Apache version."""
achalls = self.achalls[:num_achalls]
vhosts = self.vhosts[:num_achalls]
self.config.version = (2, minor_version)
self.common_perform_test(achalls, vhosts)
def common_perform_test(self, achalls, vhosts):
"""Tests perform with the given achalls."""
challenge_dir = self.http.challenge_dir
self.assertFalse(os.path.exists(challenge_dir))
for achall in achalls:
self.http.add_chall(achall)
@@ -114,25 +159,34 @@ class ApacheHttp01Test(util.ApacheTest):
for achall in achalls:
self._test_challenge_file(achall)
challenge_dir = self.http.challenge_dir
self.http.cleanup()
self.assertFalse(os.path.exists(challenge_dir))
for vhost in vhosts:
if not vhost.ssl:
matches = self.config.parser.find_dir("Include",
self.http.challenge_conf_pre,
vhost.path)
self.assertEqual(len(matches), 1)
matches = self.config.parser.find_dir("Include",
self.http.challenge_conf_post,
vhost.path)
self.assertEqual(len(matches), 1)
self.assertTrue(os.path.exists(challenge_dir))
def _test_challenge_conf(self):
self.assertEqual(
len(self.config.parser.find_dir(
"Include", self.http.challenge_conf)), 1)
with open(self.http.challenge_conf_pre) as f:
pre_conf_contents = f.read()
with open(self.http.challenge_conf) as f:
conf_contents = f.read()
with open(self.http.challenge_conf_post) as f:
post_conf_contents = f.read()
alias_fmt = "Alias /.well-known/acme-challenge {0}"
alias = alias_fmt.format(self.http.challenge_dir)
self.assertTrue(alias in conf_contents)
self.assertTrue("RewriteEngine on" in pre_conf_contents)
self.assertTrue("RewriteRule" in pre_conf_contents)
self.assertTrue(self.http.challenge_dir in post_conf_contents)
if self.config.version < (2, 4):
self.assertTrue("Allow from all" in conf_contents)
self.assertTrue("Allow from all" in post_conf_contents)
else:
self.assertTrue("Require all granted" in conf_contents)
self.assertTrue("Require all granted" in post_conf_contents)
def _test_challenge_file(self, achall):
name = os.path.join(self.http.challenge_dir, achall.chall.encode("token"))

View File

@@ -66,6 +66,23 @@ class BasicParserTest(util.ParserTest):
for i, match in enumerate(matches):
self.assertEqual(self.parser.aug.get(match), str(i + 1))
def test_add_dir_beginning(self):
aug_default = "/files" + self.parser.loc["default"]
self.parser.add_dir_beginning(aug_default,
"AddDirectiveBeginning",
"testBegin")
self.assertTrue(
self.parser.find_dir("AddDirectiveBeginning", "testBegin", aug_default))
self.assertEqual(
self.parser.aug.get(aug_default+"/directive[1]"),
"AddDirectiveBeginning")
self.parser.add_dir_beginning(aug_default, "AddList", ["1", "2", "3", "4"])
matches = self.parser.find_dir("AddList", None, aug_default)
for i, match in enumerate(matches):
self.assertEqual(self.parser.aug.get(match), str(i + 1))
def test_empty_arg(self):
self.assertEquals(None,
self.parser.get_arg("/files/whatever/nonexistent"))
@@ -282,6 +299,13 @@ class BasicParserTest(util.ParserTest):
errors.MisconfigurationError,
self.parser.update_runtime_variables)
def test_add_comment(self):
from certbot_apache.parser import get_aug_path
self.parser.add_comment(get_aug_path(self.parser.loc["name"]), "123456")
comm = self.parser.find_comments("123456")
self.assertEquals(len(comm), 1)
self.assertTrue(self.parser.loc["name"] in comm[0])
class ParserInitTest(util.ApacheTest):
def setUp(self): # pylint: disable=arguments-differ

View File

@@ -1,5 +1,6 @@
<VirtualHost *:80>
ServerName certbot.demo
ServerAlias www.certbot.demo
ServerAdmin webmaster@localhost
DocumentRoot /var/www-certbot-reworld/static/

View File

@@ -87,7 +87,6 @@ class ParserTest(ApacheTest):
def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-locals
config_path, vhost_path,
config_dir, work_dir, version=(2, 4, 7),
conf=None,
os_info="generic",
conf_vhost_path=None):
"""Create an Apache Configurator with the specified options.
@@ -133,10 +132,6 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc
config_class = configurator.ApacheConfigurator
config = config_class(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()
return config
@@ -170,7 +165,7 @@ def get_vh_truth(temp_dir, config_name):
os.path.join(prefix, "certbot.conf"),
os.path.join(aug_pre, "certbot.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
"certbot.demo"),
"certbot.demo", aliases=["www.certbot.demo"]),
obj.VirtualHost(
os.path.join(prefix, "mod_macro-example.conf"),
os.path.join(aug_pre,

View File

@@ -3,6 +3,7 @@
import os
import logging
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot.plugins import common
from certbot.errors import PluginError, MissingCommandlineFlag
@@ -93,7 +94,7 @@ class ApacheTlsSni01(common.TLSSNI01):
:rtype: set
"""
addrs = set()
addrs = set() # type: Set[obj.Addr]
config_text = "<IfModule mod_ssl.c>\n"
for achall in self.achalls:
@@ -123,7 +124,8 @@ class ApacheTlsSni01(common.TLSSNI01):
self.configurator.config.tls_sni_01_port)))
try:
vhost = self.configurator.choose_vhost(achall.domain, temp=True)
vhost = self.configurator.choose_vhost(achall.domain,
create_if_no_ssl=False)
except (PluginError, MissingCommandlineFlag):
# We couldn't find the virtualhost for this domain, possibly
# because it's a new vhost that's not configured yet

View File

@@ -0,0 +1,2 @@
acme[dev]==0.25.0
-e .[dev]

View File

@@ -1,20 +1,17 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'acme>=0.25.0',
'certbot>=0.26.0.dev0',
'mock',
'python-augeas',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.component',
'zope.interface',
]
@@ -32,6 +29,7 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
@@ -40,10 +38,8 @@ setup(
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="0.20.0"
LE_AUTO_VERSION="0.25.1"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -47,6 +47,7 @@ Help for certbot itself cannot be provided until it is installed.
--no-bootstrap do not install OS dependencies
--no-self-upgrade do not download updates
--os-packages-only install OS dependencies and exit
--install-only install certbot, upgrade if needed, and exit
-v, --verbose provide more output
-q, --quiet provide only update/error output;
implies --non-interactive
@@ -60,6 +61,8 @@ for arg in "$@" ; do
DEBUG=1;;
--os-packages-only)
OS_PACKAGES_ONLY=1;;
--install-only)
INSTALL_ONLY=1;;
--no-self-upgrade)
# Do not upgrade this script (also prevents client upgrades, because each
# copy of the script pins a hash of the python client)
@@ -68,10 +71,12 @@ for arg in "$@" ; do
NO_BOOTSTRAP=1;;
--help)
HELP=1;;
--noninteractive|--non-interactive|renew)
ASSUME_YES=1;;
--noninteractive|--non-interactive)
NONINTERACTIVE=1;;
--quiet)
QUIET=1;;
renew)
ASSUME_YES=1;;
--verbose)
VERBOSE=1;;
-[!-]*)
@@ -93,7 +98,7 @@ done
if [ $BASENAME = "letsencrypt-auto" ]; then
# letsencrypt-auto does not respect --help or --yes for backwards compatibility
ASSUME_YES=1
NONINTERACTIVE=1
HELP=0
fi
@@ -244,23 +249,42 @@ DeprecationBootstrap() {
fi
}
MIN_PYTHON_VERSION="2.7"
MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//')
# Sets LE_PYTHON to Python version string and PYVER to the first two
# digits of the python version
DeterminePythonVersion() {
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
$EXISTS "$LE_PYTHON" > /dev/null && break
done
if [ "$?" != "0" ]; then
error "Cannot find any Pythons; please install one!"
exit 1
# Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python
#
# If no Python is found, PYVER is set to 0.
if [ "$USE_PYTHON_3" = 1 ]; then
for LE_PYTHON in "$LE_PYTHON" python3; do
# Break (while keeping the LE_PYTHON value) if found.
$EXISTS "$LE_PYTHON" > /dev/null && break
done
else
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
$EXISTS "$LE_PYTHON" > /dev/null && break
done
fi
if [ "$?" != "0" ]; then
if [ "$1" != "NOCRASH" ]; then
error "Cannot find any Pythons; please install one!"
exit 1
else
PYVER=0
return 0
fi
fi
export LE_PYTHON
PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
if [ "$PYVER" -lt 26 ]; then
error "You have an ancient version of Python entombed in your operating system..."
error "This isn't going to work; you'll need at least version 2.6."
exit 1
if [ "$PYVER" -lt "$MIN_PYVER" ]; then
if [ "$1" != "NOCRASH" ]; then
error "You have an ancient version of Python entombed in your operating system..."
error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION."
exit 1
fi
fi
}
@@ -384,23 +408,19 @@ BootstrapDebCommon() {
fi
}
# If new packages are installed by BootstrapRpmCommon below, this version
# number must be increased.
BOOTSTRAP_RPM_COMMON_VERSION=1
BootstrapRpmCommon() {
# Tested with:
# - Fedora 20, 21, 22, 23 (x64)
# - Centos 7 (x64: on DigitalOcean droplet)
# - CentOS 7 Minimal install in a Hyper-V VM
# - CentOS 6 (EPEL must be installed manually)
# If new packages are installed by BootstrapRpmCommonBase below, version
# numbers in rpm_common.sh and rpm_python3.sh must be increased.
# Sets TOOL to the name of the package manager
# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG.
# Enables EPEL if applicable and possible.
InitializeRPMCommonBase() {
if type dnf 2>/dev/null
then
tool=dnf
TOOL=dnf
elif type yum 2>/dev/null
then
tool=yum
TOOL=yum
else
error "Neither yum nor dnf found. Aborting bootstrap!"
@@ -408,15 +428,15 @@ BootstrapRpmCommon() {
fi
if [ "$ASSUME_YES" = 1 ]; then
yes_flag="-y"
YES_FLAG="-y"
fi
if [ "$QUIET" = 1 ]; then
QUIET_FLAG='--quiet'
fi
if ! $tool list *virtualenv >/dev/null 2>&1; then
if ! $TOOL list *virtualenv >/dev/null 2>&1; then
echo "To use Certbot, packages from the EPEL repository need to be installed."
if ! $tool list epel-release >/dev/null 2>&1; then
if ! $TOOL list epel-release >/dev/null 2>&1; then
error "Enable the EPEL repository and try running Certbot again."
exit 1
fi
@@ -425,14 +445,20 @@ BootstrapRpmCommon() {
sleep 1s
/bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..."
sleep 1s
/bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..."
/bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 second..."
sleep 1s
fi
if ! $tool install $yes_flag $QUIET_FLAG epel-release; then
if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then
error "Could not enable EPEL. Aborting bootstrap!"
exit 1
fi
fi
}
BootstrapRpmCommonBase() {
# Arguments: whitespace-delimited python packages to install
InitializeRPMCommonBase # This call is superfluous in practice
pkgs="
gcc
@@ -444,10 +470,39 @@ BootstrapRpmCommon() {
ca-certificates
"
# Most RPM distros use the "python" or "python-" naming convention. Let's try that first.
if $tool list python >/dev/null 2>&1; then
# Add the python packages
pkgs="$pkgs
$1
"
if $TOOL list installed "httpd" >/dev/null 2>&1; then
pkgs="$pkgs
python
mod_ssl
"
fi
if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then
error "Could not install OS dependencies. Aborting bootstrap!"
exit 1
fi
}
# If new packages are installed by BootstrapRpmCommon below, this version
# number must be increased.
BOOTSTRAP_RPM_COMMON_VERSION=1
BootstrapRpmCommon() {
# Tested with:
# - Fedora 20, 21, 22, 23 (x64)
# - Centos 7 (x64: on DigitalOcean droplet)
# - CentOS 7 Minimal install in a Hyper-V VM
# - CentOS 6
InitializeRPMCommonBase
# Most RPM distros use the "python" or "python-" naming convention. Let's try that first.
if $TOOL list python >/dev/null 2>&1; then
python_pkgs="$python
python-devel
python-virtualenv
python-tools
@@ -455,9 +510,8 @@ BootstrapRpmCommon() {
"
# Fedora 26 starts to use the prefix python2 for python2 based packages.
# this elseif is theoretically for any Fedora over version 26:
elif $tool list python2 >/dev/null 2>&1; then
pkgs="$pkgs
python2
elif $TOOL list python2 >/dev/null 2>&1; then
python_pkgs="$python2
python2-libs
python2-setuptools
python2-devel
@@ -468,8 +522,7 @@ BootstrapRpmCommon() {
# Some distros and older versions of current distros use a "python27"
# instead of the "python" or "python-" naming convention.
else
pkgs="$pkgs
python27
python_pkgs="$python27
python27-devel
python27-virtualenv
python27-tools
@@ -477,16 +530,31 @@ BootstrapRpmCommon() {
"
fi
if $tool list installed "httpd" >/dev/null 2>&1; then
pkgs="$pkgs
mod_ssl
"
fi
BootstrapRpmCommonBase "$python_pkgs"
}
if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then
error "Could not install OS dependencies. Aborting bootstrap!"
# If new packages are installed by BootstrapRpmPython3 below, this version
# number must be increased.
BOOTSTRAP_RPM_PYTHON3_VERSION=1
BootstrapRpmPython3() {
# Tested with:
# - CentOS 6
InitializeRPMCommonBase
# EPEL uses python34
if $TOOL list python34 >/dev/null 2>&1; then
python_pkgs="python34
python34-devel
python34-tools
"
else
error "No supported Python package available to install. Aborting bootstrap!"
exit 1
fi
BootstrapRpmCommonBase "$python_pkgs"
}
# If new packages are installed by BootstrapSuseCommon below, this version
@@ -696,13 +764,8 @@ BootstrapMageiaCommon() {
# Set Bootstrap to the function that installs OS dependencies on this system
# and BOOTSTRAP_VERSION to the unique identifier for the current version of
# that function. If Bootstrap is set to a function that doesn't install any
# packages (either because --no-bootstrap was included on the command line or
# we don't know how to bootstrap on this system), BOOTSTRAP_VERSION is not set.
if [ "$NO_BOOTSTRAP" = 1 ]; then
Bootstrap() {
:
}
elif [ -f /etc/debian_version ]; then
# packages BOOTSTRAP_VERSION is not set.
if [ -f /etc/debian_version ]; then
Bootstrap() {
BootstrapMessage "Debian-based OSes"
BootstrapDebCommon
@@ -715,11 +778,27 @@ elif [ -f /etc/mageia-release ]; then
}
BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION"
elif [ -f /etc/redhat-release ]; then
Bootstrap() {
BootstrapMessage "RedHat-based OSes"
BootstrapRpmCommon
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
# Run DeterminePythonVersion to decide on the basis of available Python versions
# whether to use 2.x or 3.x on RedHat-like systems.
# Then, revert LE_PYTHON to its previous state.
prev_le_python="$LE_PYTHON"
unset LE_PYTHON
DeterminePythonVersion "NOCRASH"
if [ "$PYVER" -eq 26 ]; then
Bootstrap() {
BootstrapMessage "RedHat-based OSes that will use Python3"
BootstrapRpmPython3
}
USE_PYTHON_3=1
BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION"
else
Bootstrap() {
BootstrapMessage "RedHat-based OSes"
BootstrapRpmCommon
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
fi
LE_PYTHON="$prev_le_python"
elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
Bootstrap() {
BootstrapMessage "openSUSE-based OSes"
@@ -782,6 +861,17 @@ else
}
fi
# We handle this case after determining the normal bootstrap version to allow
# variables like USE_PYTHON_3 to be properly set. As described above, if the
# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not
# be set so we unset it here.
if [ "$NO_BOOTSTRAP" = 1 ]; then
Bootstrap() {
:
}
unset BOOTSTRAP_VERSION
fi
# Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used
# to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set
# if it is unknown how OS dependencies were installed on this system.
@@ -816,7 +906,11 @@ TempDir() {
mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
}
# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise,
# returns a non-zero number.
OldVenvExists() {
[ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ]
}
if [ "$1" = "--le-auto-phase2" ]; then
# Phase 2: Create venv, install LE, and run.
@@ -824,14 +918,26 @@ if [ "$1" = "--le-auto-phase2" ]; then
shift 1 # the --le-auto-phase2 arg
SetPrevBootstrapVersion
if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then
unset LE_PYTHON
fi
INSTALLED_VERSION="none"
if [ -d "$VENV_PATH" ]; then
if [ -d "$VENV_PATH" ] || OldVenvExists; then
# If the selected Bootstrap function isn't a noop and it differs from the
# previously used version
if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then
# if non-interactive mode or stdin and stdout are connected to a terminal
if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then
rm -rf "$VENV_PATH"
if [ -d "$VENV_PATH" ]; then
rm -rf "$VENV_PATH"
fi
# In the case the old venv was just a symlink to the new one,
# OldVenvExists is now false because we deleted the venv at VENV_PATH.
if OldVenvExists; then
rm -rf "$OLD_VENV_PATH"
ln -s "$VENV_PATH" "$OLD_VENV_PATH"
fi
RerunWithArgs "$@"
else
error "Skipping upgrade because new OS dependencies may need to be installed."
@@ -841,6 +947,10 @@ if [ "$1" = "--le-auto-phase2" ]; then
error "install any required packages."
# Set INSTALLED_VERSION to be the same so we don't update the venv
INSTALLED_VERSION="$LE_AUTO_VERSION"
# Continue to use OLD_VENV_PATH if the new venv doesn't exist
if [ ! -d "$VENV_PATH" ]; then
VENV_BIN="$OLD_VENV_PATH/bin"
fi
fi
elif [ -f "$VENV_BIN/letsencrypt" ]; then
# --version output ran through grep due to python-cryptography DeprecationWarnings
@@ -858,10 +968,18 @@ if [ "$1" = "--le-auto-phase2" ]; then
say "Creating virtual environment..."
DeterminePythonVersion
rm -rf "$VENV_PATH"
if [ "$VERBOSE" = 1 ]; then
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH"
if [ "$PYVER" -le 27 ]; then
if [ "$VERBOSE" = 1 ]; then
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH"
else
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null
fi
else
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null
if [ "$VERBOSE" = 1 ]; then
"$LE_PYTHON" -m venv "$VENV_PATH"
else
"$LE_PYTHON" -m venv "$VENV_PATH" > /dev/null
fi
fi
if [ -n "$BOOTSTRAP_VERSION" ]; then
@@ -937,9 +1055,11 @@ cffi==1.10.0 \
--hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \
--hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5
ConfigArgParse==0.12.0 \
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \
--no-binary ConfigArgParse
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
--no-binary configobj
cryptography==2.0.2 \
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
@@ -971,7 +1091,7 @@ cryptography==2.0.2 \
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
enum34==1.1.2 \
enum34==1.1.2 ; python_version < '3.4' \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
funcsigs==1.0.2 \
@@ -983,11 +1103,19 @@ idna==2.5 \
ipaddress==1.0.16 \
--hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \
--hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0
josepy==1.0.1 \
--hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \
--hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
# Using an older version of mock here prevents regressions of #5276.
mock==1.3.0 \
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f \
--no-binary ordereddict
packaging==16.8 \
--hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \
--hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e
@@ -1013,7 +1141,8 @@ pyRFC3339==1.0 \
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
python-augeas==0.5.0 \
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 \
--no-binary python-augeas
pytz==2015.7 \
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
@@ -1041,9 +1170,11 @@ unittest2==1.1.0 \
--hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \
--hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579
zope.component==4.2.2 \
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a \
--no-binary zope.component
zope.event==4.1.0 \
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 \
--no-binary zope.event
zope.interface==4.1.3 \
--hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \
--hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \
@@ -1062,10 +1193,9 @@ zope.interface==4.1.3 \
--hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \
--hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \
--hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392
# Using an older version of mock here prevents regressions of #5276.
mock==1.3.0 \
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
requests-toolbelt==0.8.0 \
--hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \
--hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5
# Contains the requirements for the letsencrypt package.
#
@@ -1078,24 +1208,24 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.20.0 \
--hash=sha256:c6b6bd288700898d1eb31a65b605e3a5fc10f1e3213ce468207d76a2decb9d35 \
--hash=sha256:cabf505b64fb400c4239dcdbaeb882079477eb6a8442268596a8791b9e34de88
acme==0.20.0 \
--hash=sha256:8b0cee192c0d76d6f4045bdb14b3cfd29d9720e0dad2046794a2a555f1eaccb7 \
--hash=sha256:45121aed6c8cc2f31896ac1083068dfdeb613f3edeff9576dc0d10632ea5a3d5
certbot-apache==0.20.0 \
--hash=sha256:f7e4dbc154d2e9d1461118b6dd3dbd16f6892da468f060eeaa162aff673347e2 \
--hash=sha256:0ba499706451ffbccb172bcf93d6ef4c6cc8599157077a4fa6dfbe5a83c7921f
certbot-nginx==0.20.0 \
--hash=sha256:b6e372e8740b20dd9bd63837646157ac97b3c9a65affd3954571b8e872ae9ecf \
--hash=sha256:6379fdf20d9a7651fe30bb8d4b828cbea178cc263d7af5a380fc4508d793b9ae
certbot==0.25.1 \
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
acme==0.25.1 \
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
certbot-apache==0.25.1 \
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
certbot-nginx==0.25.1 \
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
UNLIKELY_EOF
# -------------------------------------------------------------------------
cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py"
#!/usr/bin/env python
"""A small script that can act as a trust root for installing pip 8
"""A small script that can act as a trust root for installing pip >=8
Embed this in your project, and your VCS checkout is all you have to trust. In
a post-peep era, this lets you claw your way to a hash-checking version of pip,
@@ -1119,6 +1249,7 @@ anything goes wrong, it will exit with a non-zero status code.
from __future__ import print_function
from distutils.version import StrictVersion
from hashlib import sha256
from os import environ
from os.path import join
from pipes import quote
from shutil import rmtree
@@ -1152,14 +1283,14 @@ except ImportError:
from urllib.parse import urlparse # 3.4
__version__ = 1, 3, 0
__version__ = 1, 5, 1
PIP_VERSION = '9.0.1'
DEFAULT_INDEX_BASE = 'https://pypi.python.org'
# wheel has a conditional dependency on argparse:
maybe_argparse = (
[('https://pypi.python.org/packages/18/dd/'
'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/'
[('18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/'
'argparse-1.4.0.tar.gz',
'62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')]
if version_info < (2, 7, 0) else [])
@@ -1167,18 +1298,14 @@ maybe_argparse = (
PACKAGES = maybe_argparse + [
# Pip has no dependencies, as it vendors everything:
('https://pypi.python.org/packages/11/b6/'
'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/'
'pip-{0}.tar.gz'
.format(PIP_VERSION),
('11/b6/abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/'
'pip-{0}.tar.gz'.format(PIP_VERSION),
'09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'),
# This version of setuptools has only optional dependencies:
('https://pypi.python.org/packages/69/65/'
'4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/'
'setuptools-20.2.2.tar.gz',
'24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'),
('https://pypi.python.org/packages/c9/1d/'
'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/'
('59/88/2f3990916931a5de6fa9706d6d75eb32ee8b78627bb2abaab7ed9e6d0622/'
'setuptools-29.0.1.tar.gz',
'b539118819a4857378398891fa5366e090690e46b3e41421a1e07d6e9fd8feb0'),
('c9/1d/bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/'
'wheel-0.29.0.tar.gz',
'1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648')
]
@@ -1199,12 +1326,13 @@ def hashed_download(url, temp, digest):
# >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert
# authenticity has only privacy (not arbitrary code execution)
# implications, since we're checking hashes.
def opener():
def opener(using_https=True):
opener = build_opener(HTTPSHandler())
# Strip out HTTPHandler to prevent MITM spoof:
for handler in opener.handlers:
if isinstance(handler, HTTPHandler):
opener.handlers.remove(handler)
if using_https:
# Strip out HTTPHandler to prevent MITM spoof:
for handler in opener.handlers:
if isinstance(handler, HTTPHandler):
opener.handlers.remove(handler)
return opener
def read_chunks(response, chunk_size):
@@ -1214,8 +1342,9 @@ def hashed_download(url, temp, digest):
break
yield chunk
response = opener().open(url)
path = join(temp, urlparse(url).path.split('/')[-1])
parsed_url = urlparse(url)
response = opener(using_https=parsed_url.scheme == 'https').open(url)
path = join(temp, parsed_url.path.split('/')[-1])
actual_hash = sha256()
with open(path, 'wb') as file:
for chunk in read_chunks(response, 4096):
@@ -1228,6 +1357,24 @@ def hashed_download(url, temp, digest):
return path
def get_index_base():
"""Return the URL to the dir containing the "packages" folder.
Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the
end if it's there; that is likely to give us the right dir.
"""
env_var = environ.get('PIP_INDEX_URL', '').rstrip('/')
if env_var:
SIMPLE = '/simple'
if env_var.endswith(SIMPLE):
return env_var[:-len(SIMPLE)]
else:
return env_var
else:
return DEFAULT_INDEX_BASE
def main():
pip_version = StrictVersion(check_output(['pip', '--version'])
.decode('utf-8').split()[1])
@@ -1235,11 +1382,13 @@ def main():
if pip_version >= min_pip_version:
return 0
has_pip_cache = pip_version >= StrictVersion('6.0')
index_base = get_index_base()
temp = mkdtemp(prefix='pipstrap-')
try:
downloads = [hashed_download(url, temp, digest)
for url, digest in PACKAGES]
downloads = [hashed_download(index_base + '/packages/' + path,
temp,
digest)
for path, digest in PACKAGES]
check_output('pip install --no-index --no-deps -U ' +
# Disable cache since we're not using it and it otherwise
# sometimes throws permission warnings:
@@ -1310,6 +1459,12 @@ UNLIKELY_EOF
say "Installation succeeded."
fi
if [ "$INSTALL_ONLY" = 1 ]; then
say "Certbot is installed."
exit 0
fi
"$VENV_BIN/letsencrypt" "$@"
else
@@ -1319,9 +1474,10 @@ else
# upgrading. Phase 1 checks the version of the latest release of
# certbot-auto (which is always the same as that of the certbot
# package). Phase 2 checks the version of the locally installed certbot.
export PHASE_1_VERSION="$LE_AUTO_VERSION"
if [ ! -f "$VENV_BIN/letsencrypt" ]; then
if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then
if ! OldVenvExists; then
if [ "$HELP" = 1 ]; then
echo "$USAGE"
exit 0
@@ -1353,17 +1509,22 @@ On failure, return non-zero.
"""
from __future__ import print_function
from __future__ import print_function, unicode_literals
from distutils.version import LooseVersion
from json import loads
from os import devnull, environ
from os.path import dirname, join
import re
import ssl
from subprocess import check_call, CalledProcessError
from sys import argv, exit
from urllib2 import build_opener, HTTPHandler, HTTPSHandler
from urllib2 import HTTPError, URLError
try:
from urllib2 import build_opener, HTTPHandler, HTTPSHandler
from urllib2 import HTTPError, URLError
except ImportError:
from urllib.request import build_opener, HTTPHandler, HTTPSHandler
from urllib.error import HTTPError, URLError
PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq
@@ -1385,8 +1546,11 @@ class HttpsGetter(object):
def __init__(self):
"""Build an HTTPS opener."""
# Based on pip 1.4.1's URLOpener
# This verifies certs on only Python >=2.7.9.
self._opener = build_opener(HTTPSHandler())
# This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set.
if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'):
self._opener = build_opener(HTTPSHandler(context=cert_none_context()))
else:
self._opener = build_opener(HTTPSHandler())
# Strip out HTTPHandler to prevent MITM spoof:
for handler in self._opener.handlers:
if isinstance(handler, HTTPHandler):
@@ -1408,7 +1572,7 @@ class HttpsGetter(object):
def write(contents, dir, filename):
"""Write something to a file in a certain directory."""
with open(join(dir, filename), 'w') as file:
with open(join(dir, filename), 'wb') as file:
file.write(contents)
@@ -1416,13 +1580,13 @@ def latest_stable_version(get):
"""Return the latest stable release of letsencrypt."""
metadata = loads(get(
environ.get('LE_AUTO_JSON_URL',
'https://pypi.python.org/pypi/certbot/json')))
'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8'))
# metadata['info']['version'] actually returns the latest of any kind of
# release release, contrary to https://wiki.python.org/moin/PyPIJSON.
# The regex is a sufficient regex for picking out prereleases for most
# packages, LE included.
return str(max(LooseVersion(r) for r
in metadata['releases'].iterkeys()
in metadata['releases'].keys()
if re.match('^[0-9.]+$', r)))
@@ -1439,7 +1603,7 @@ def verified_new_le_auto(get, tag, temp_dir):
'letsencrypt-auto-source/') % tag
write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto')
write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig')
write(PUBLIC_KEY, temp_dir, 'public_key.pem')
write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem')
try:
with open(devnull, 'w') as dev_null:
check_call(['openssl', 'dgst', '-sha256', '-verify',
@@ -1454,6 +1618,14 @@ def verified_new_le_auto(get, tag, temp_dir):
"certbot-auto.", exc)
def cert_none_context():
"""Create a SSLContext object to not check hostname."""
# PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this.
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.verify_mode = ssl.CERT_NONE
return context
def main():
get = HttpsGetter().get
flag = argv[1]
@@ -1475,8 +1647,10 @@ if __name__ == '__main__':
UNLIKELY_EOF
# ---------------------------------------------------------------------------
DeterminePythonVersion
if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
DeterminePythonVersion "NOCRASH"
if [ "$PYVER" -lt "$MIN_PYVER" ]; then
error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates."
elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
error "WARNING: unable to check for updates."
elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then
say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."

View File

@@ -10,9 +10,12 @@ import sys
import OpenSSL
from six.moves import xrange # pylint: disable=import-error,redefined-builtin
from acme import challenges
from acme import crypto_util
from acme import messages
from acme.magic_typing import List, Tuple # pylint: disable=unused-import, no-name-in-module
from certbot import achallenges
from certbot import errors as le_errors
from certbot.tests import acme_util
@@ -50,9 +53,8 @@ def test_authenticator(plugin, config, temp_dir):
try:
responses = plugin.perform(achalls)
except le_errors.Error as error:
logger.error("Performing challenges on %s caused an error:", config)
logger.exception(error)
except le_errors.Error:
logger.error("Performing challenges on %s caused an error:", config, exc_info=True)
return False
success = True
@@ -80,9 +82,8 @@ def test_authenticator(plugin, config, temp_dir):
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)
except le_errors.Error:
logger.error("Challenge cleanup for %s caused an error:", config, exc_info=True)
success = False
if _dirs_are_unequal(config, backup):
@@ -145,9 +146,8 @@ def test_deploy_cert(plugin, temp_dir, domains):
try:
plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path)
plugin.save() # Needed by the Apache plugin
except le_errors.Error as error:
logger.error("**** Plugin failed to deploy certificate for %s:", domain)
logger.exception(error)
except le_errors.Error:
logger.error("**** Plugin failed to deploy certificate for %s:", domain, exc_info=True)
return False
if not _save_and_restart(plugin, "deployed"):
@@ -177,7 +177,7 @@ def test_enhancements(plugin, domains):
"enhancements")
return False
domains_and_info = [(domain, []) for domain in domains]
domains_and_info = [(domain, []) for domain in domains] # type: List[Tuple[str, List[bool]]]
for domain, info in domains_and_info:
try:
@@ -190,10 +190,9 @@ def test_enhancements(plugin, domains):
# 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:
except le_errors.Error:
logger.error("*** An error occurred while enabling redirect for %s:",
domain)
logger.exception(error)
domain, exc_info=True)
if not _save_and_restart(plugin, "enhanced"):
return False
@@ -220,9 +219,8 @@ def _save_and_restart(plugin, title=None):
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)
except le_errors.Error:
logger.error("*** Plugin failed to save and restart server:", exc_info=True)
return False
@@ -230,9 +228,8 @@ 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)
except le_errors.Error:
logger.error("*** Plugin raised an exception during rollback:", exc_info=True)
return False
if _dirs_are_unequal(config, backup):
@@ -261,21 +258,21 @@ def _dirs_are_unequal(dir1, dir2):
logger.error("The following files and directories are only "
"present in one directory")
if dircmp.left_only:
logger.error(dircmp.left_only)
logger.error(str(dircmp.left_only))
else:
logger.error(dircmp.right_only)
logger.error(str(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)
logger.error(str(dircmp.common_funny))
else:
logger.error(dircmp.funny_files)
logger.error(str(dircmp.funny_files))
return True
elif dircmp.diff_files:
logger.error("The following files differ:")
logger.error(dircmp.diff_files)
logger.error(str(dircmp.diff_files))
return True
for subdir in dircmp.subdirs.itervalues():
@@ -352,9 +349,8 @@ def main():
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)
except errors.Error:
logger.error("Tests on %s raised:", config, exc_info=True)
success = False
if success:

View File

@@ -26,12 +26,12 @@ def create_le_config(parent_dir):
config = copy.deepcopy(constants.CLI_DEFAULTS)
le_dir = os.path.join(parent_dir, "certbot")
config["config_dir"] = os.path.join(le_dir, "config")
config["work_dir"] = os.path.join(le_dir, "work")
config["logs_dir"] = os.path.join(le_dir, "logs_dir")
os.makedirs(config["config_dir"])
os.mkdir(config["work_dir"])
os.mkdir(config["logs_dir"])
os.mkdir(le_dir)
for dir_name in ("config", "logs", "work"):
full_path = os.path.join(le_dir, dir_name)
os.mkdir(full_path)
full_name = dir_name + "_dir"
config[full_name] = full_path
config["domains"] = None

View File

@@ -5,6 +5,7 @@ import requests
import zope.interface
import six
from six.moves import xrange # pylint: disable=import-error,redefined-builtin
from acme import crypto_util
from acme import errors as acme_errors
@@ -32,7 +33,7 @@ class Validator(object):
try:
presented_cert = crypto_util.probe_sni(name, host, port)
except acme_errors.Error as error:
logger.exception(error)
logger.exception(str(error))
return False
return presented_cert.digest("sha256") == cert.digest("sha256")
@@ -85,8 +86,7 @@ class Validator(object):
return False
try:
_, max_age_value = max_age[0]
max_age_value = int(max_age_value)
max_age_value = int(max_age[0][1])
except ValueError:
logger.error("Server responded with invalid HSTS header field")
return False

View File

@@ -8,7 +8,7 @@ from certbot_nginx import nginxparser
def roundtrip(stuff):
success = True
for t in stuff:
print t
print(t)
if not os.path.isfile(t):
continue
with open(t, "r") as f:

View File

@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
install_requires = [
'certbot',
@@ -34,16 +34,15 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
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.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -0,0 +1,5 @@
FROM certbot/certbot
COPY . src/certbot-dns-cloudflare
RUN pip install --no-cache-dir --editable src/certbot-dns-cloudflare

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-cloudflare's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_cloudflare
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_cloudflare
:members:
Indices and tables
==================

View File

@@ -0,0 +1,2 @@
acme[dev]==0.21.1
certbot[dev]==0.21.1

View File

@@ -0,0 +1,12 @@
# readthedocs.org gives no way to change the install command to "pip
# install -e .[docs]" (that would in turn install documentation
# dependencies), but it allows to specify a requirements.txt file at
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
# Although ReadTheDocs certainly doesn't need to install the project
# in --editable mode (-e), just "pip install .[docs]" does not work as
# expected and "pip install -e .[docs]" must be used instead
-e acme
-e .
-e certbot-dns-cloudflare[docs]

View File

@@ -1,20 +1,17 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'acme>=0.21.1',
'certbot>=0.21.1',
'cloudflare>=1.5.1',
'mock',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.interface',
]
@@ -31,6 +28,7 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
@@ -39,10 +37,8 @@ setup(
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -0,0 +1,5 @@
FROM certbot/certbot
COPY . src/certbot-dns-cloudxns
RUN pip install --no-cache-dir --editable src/certbot-dns-cloudxns

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-cloudxns's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_cloudxns
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_cloudxns
:members:
Indices and tables

View File

@@ -0,0 +1,2 @@
acme[dev]==0.21.1
certbot[dev]==0.21.1

View File

@@ -0,0 +1,12 @@
# readthedocs.org gives no way to change the install command to "pip
# install -e .[docs]" (that would in turn install documentation
# dependencies), but it allows to specify a requirements.txt file at
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
# Although ReadTheDocs certainly doesn't need to install the project
# in --editable mode (-e), just "pip install .[docs]" does not work as
# expected and "pip install -e .[docs]" must be used instead
-e acme
-e .
-e certbot-dns-cloudxns[docs]

View File

@@ -1,20 +1,17 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'dns-lexicon',
'acme>=0.21.1',
'certbot>=0.21.1',
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
'mock',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.interface',
]
@@ -31,6 +28,7 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
@@ -41,7 +39,6 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -0,0 +1,5 @@
FROM certbot/certbot
COPY . src/certbot-dns-digitalocean
RUN pip install --no-cache-dir --editable src/certbot-dns-digitalocean

View File

@@ -50,7 +50,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
class DigitalOceanClientTest(unittest.TestCase):
id = 1
id_num = 1
record_prefix = "_acme-challenge"
record_name = record_prefix + "." + DOMAIN
record_content = "bar"
@@ -70,7 +71,7 @@ class DigitalOceanClientTest(unittest.TestCase):
domain_mock = mock.MagicMock()
domain_mock.name = DOMAIN
domain_mock.create_new_domain_record.return_value = {'domain_record': {'id': self.id}}
domain_mock.create_new_domain_record.return_value = {'domain_record': {'id': self.id_num}}
self.manager.get_all_domains.return_value = [wrong_domain_mock, domain_mock]

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-digitalocean's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_digitalocean
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_digitalocean
:members:
Indices and tables

View File

@@ -0,0 +1,2 @@
acme[dev]==0.21.1
certbot[dev]==0.21.1

View File

@@ -0,0 +1,12 @@
# readthedocs.org gives no way to change the install command to "pip
# install -e .[docs]" (that would in turn install documentation
# dependencies), but it allows to specify a requirements.txt file at
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
# Although ReadTheDocs certainly doesn't need to install the project
# in --editable mode (-e), just "pip install .[docs]" does not work as
# expected and "pip install -e .[docs]" must be used instead
-e acme
-e .
-e certbot-dns-digitalocean[docs]

View File

@@ -1,20 +1,17 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'acme>=0.21.1',
'certbot>=0.21.1',
'mock',
'python-digitalocean>=1.11',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'six',
'zope.interface',
]
@@ -32,6 +29,7 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
@@ -40,10 +38,8 @@ setup(
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -0,0 +1,5 @@
FROM certbot/certbot
COPY . src/certbot-dns-dnsimple
RUN pip install --no-cache-dir --editable src/certbot-dns-dnsimple

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-dnsimple's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_dnsimple
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_dnsimple
:members:
Indices and tables

View File

@@ -0,0 +1,2 @@
acme[dev]==0.21.1
certbot[dev]==0.21.1

View File

@@ -0,0 +1,12 @@
# readthedocs.org gives no way to change the install command to "pip
# install -e .[docs]" (that would in turn install documentation
# dependencies), but it allows to specify a requirements.txt file at
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
# Although ReadTheDocs certainly doesn't need to install the project
# in --editable mode (-e), just "pip install .[docs]" does not work as
# expected and "pip install -e .[docs]" must be used instead
-e acme
-e .
-e certbot-dns-dnsimple[docs]

View File

@@ -1,20 +1,17 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'dns-lexicon',
'acme>=0.21.1',
'certbot>=0.21.1',
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
'mock',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.interface',
]
@@ -31,6 +28,7 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
@@ -41,7 +39,6 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -0,0 +1,5 @@
FROM certbot/certbot
COPY . src/certbot-dns-dnsmadeeasy
RUN pip install --no-cache-dir --editable src/certbot-dns-dnsmadeeasy

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-dnsmadeeasy's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_dnsmadeeasy
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_dnsmadeeasy
:members:
Indices and tables

View File

@@ -0,0 +1,2 @@
acme[dev]==0.21.1
certbot[dev]==0.21.1

View File

@@ -0,0 +1,12 @@
# readthedocs.org gives no way to change the install command to "pip
# install -e .[docs]" (that would in turn install documentation
# dependencies), but it allows to specify a requirements.txt file at
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
# Although ReadTheDocs certainly doesn't need to install the project
# in --editable mode (-e), just "pip install .[docs]" does not work as
# expected and "pip install -e .[docs]" must be used instead
-e acme
-e .
-e certbot-dns-dnsmadeeasy[docs]

View File

@@ -1,20 +1,17 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'dns-lexicon',
'acme>=0.21.1',
'certbot>=0.21.1',
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
'mock',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.interface',
]
@@ -31,6 +28,7 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
@@ -41,7 +39,6 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -0,0 +1,5 @@
FROM certbot/certbot
COPY . src/certbot-dns-google
RUN pip install --no-cache-dir --editable src/certbot-dns-google

View File

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

View File

@@ -29,6 +29,8 @@ for an account with the following permissions:
* ``dns.managedZones.list``
* ``dns.resourceRecordSets.create``
* ``dns.resourceRecordSets.delete``
* ``dns.resourceRecordSets.list``
* ``dns.resourceRecordSets.update``
Google provides instructions for `creating a service account <https://developers
.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount>`_ and

View File

@@ -81,7 +81,7 @@ class _GoogleClient(object):
Encapsulates all communication with the Google Cloud DNS API.
"""
def __init__(self, account_json=None):
def __init__(self, account_json=None, dns_api=None):
scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']
if account_json is not None:
@@ -92,7 +92,12 @@ class _GoogleClient(object):
credentials = None
self.project_id = self.get_project_id()
self.dns = discovery.build('dns', 'v1', credentials=credentials, cache_discovery=False)
if not dns_api:
self.dns = discovery.build('dns', 'v1',
credentials=credentials,
cache_discovery=False)
else:
self.dns = dns_api
def add_txt_record(self, domain, record_name, record_content, record_ttl):
"""
@@ -107,6 +112,17 @@ class _GoogleClient(object):
zone_id = self._find_managed_zone_id(domain)
record_contents = self.get_existing_txt_rrset(zone_id, record_name)
if record_contents is None:
record_contents = []
add_records = record_contents[:]
if "\""+record_content+"\"" in record_contents:
# The process was interrupted previously and validation token exists
return
add_records.append(record_content)
data = {
"kind": "dns#change",
"additions": [
@@ -114,12 +130,24 @@ class _GoogleClient(object):
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": record_name + ".",
"rrdatas": [record_content, ],
"rrdatas": add_records,
"ttl": record_ttl,
},
],
}
if record_contents:
# We need to remove old records in the same request
data["deletions"] = [
{
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": record_name + ".",
"rrdatas": record_contents,
"ttl": record_ttl,
},
]
changes = self.dns.changes() # changes | pylint: disable=no-member
try:
@@ -154,6 +182,10 @@ class _GoogleClient(object):
logger.warn('Error finding zone. Skipping cleanup.')
return
record_contents = self.get_existing_txt_rrset(zone_id, record_name)
if record_contents is None:
record_contents = ["\"" + record_content + "\""]
data = {
"kind": "dns#change",
"deletions": [
@@ -161,12 +193,26 @@ class _GoogleClient(object):
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": record_name + ".",
"rrdatas": [record_content, ],
"rrdatas": record_contents,
"ttl": record_ttl,
},
],
}
# Remove the record being deleted from the list
readd_contents = [r for r in record_contents if r != "\"" + record_content + "\""]
if readd_contents:
# We need to remove old records in the same request
data["additions"] = [
{
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": record_name + ".",
"rrdatas": readd_contents,
"ttl": record_ttl,
},
]
changes = self.dns.changes() # changes | pylint: disable=no-member
try:
@@ -175,6 +221,37 @@ class _GoogleClient(object):
except googleapiclient_errors.Error as e:
logger.warn('Encountered error deleting TXT record: %s', e)
def get_existing_txt_rrset(self, zone_id, record_name):
"""
Get existing TXT records from the RRset for the record name.
If an error occurs while requesting the record set, it is suppressed
and None is returned.
:param str zone_id: The ID of the managed zone.
:param str record_name: The record name (typically beginning with '_acme-challenge.').
:returns: List of TXT record values or None
:rtype: `list` of `string` or `None`
"""
rrs_request = self.dns.resourceRecordSets() # pylint: disable=no-member
request = rrs_request.list(managedZone=zone_id, project=self.project_id)
# Add dot as the API returns absolute domains
record_name += "."
try:
response = request.execute()
except googleapiclient_errors.Error:
logger.info("Unable to list existing records. If you're "
"requesting a wildcard certificate, this might not work.")
logger.debug("Error was:", exc_info=True)
else:
if response:
for rr in response["rrsets"]:
if rr["name"] == record_name and rr["type"] == "TXT":
return rr["rrdatas"]
return None
def _find_managed_zone_id(self, domain):
"""
Find the managed zone for a given domain.
@@ -224,4 +301,7 @@ class _GoogleClient(object):
if r.status != 200:
raise ValueError("Invalid status code: {0}".format(r))
return content
if isinstance(content, bytes):
return content.decode()
else:
return content

View File

@@ -4,7 +4,9 @@ import os
import unittest
import mock
from googleapiclient import discovery
from googleapiclient.errors import Error
from googleapiclient.http import HttpMock
from httplib2 import ServerNotFoundError
from certbot import errors
@@ -68,16 +70,27 @@ class GoogleClientTest(unittest.TestCase):
def _setUp_client_with_mock(self, zone_request_side_effect):
from certbot_dns_google.dns_google import _GoogleClient
client = _GoogleClient(ACCOUNT_JSON_PATH)
pwd = os.path.dirname(__file__)
rel_path = 'testdata/discovery.json'
discovery_file = os.path.join(pwd, rel_path)
http_mock = HttpMock(discovery_file, {'status': '200'})
dns_api = discovery.build('dns', 'v1', http=http_mock)
client = _GoogleClient(ACCOUNT_JSON_PATH, dns_api)
# Setup
mock_mz = mock.MagicMock()
mock_mz.list.return_value.execute.side_effect = zone_request_side_effect
mock_rrs = mock.MagicMock()
rrsets = {"rrsets": [{"name": "_acme-challenge.example.org.", "type": "TXT",
"rrdatas": ["\"example-txt-contents\""]}]}
mock_rrs.list.return_value.execute.return_value = rrsets
mock_changes = mock.MagicMock()
client.dns.managedZones = mock.MagicMock(return_value=mock_mz)
client.dns.changes = mock.MagicMock(return_value=mock_changes)
client.dns.resourceRecordSets = mock.MagicMock(return_value=mock_rrs)
return client, mock_changes
@@ -137,6 +150,30 @@ class GoogleClientTest(unittest.TestCase):
managedZone=self.zone,
project=PROJECT_ID)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_add_txt_record_delete_old(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
mock_get_rrs = "certbot_dns_google.dns_google._GoogleClient.get_existing_txt_rrset"
with mock.patch(mock_get_rrs) as mock_rrs:
mock_rrs.return_value = ["sample-txt-contents"]
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
self.assertTrue(changes.create.called)
self.assertTrue("sample-txt-contents" in
changes.create.call_args_list[0][1]["body"]["deletions"][0]["rrdatas"])
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_add_txt_record_noop(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
client.add_txt_record(DOMAIN, "_acme-challenge.example.org",
"example-txt-contents", self.record_ttl)
self.assertFalse(changes.create.called)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
@@ -172,7 +209,12 @@ class GoogleClientTest(unittest.TestCase):
def test_del_txt_record(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
mock_get_rrs = "certbot_dns_google.dns_google._GoogleClient.get_existing_txt_rrset"
with mock.patch(mock_get_rrs) as mock_rrs:
mock_rrs.return_value = ["\"sample-txt-contents\"",
"\"example-txt-contents\""]
client.del_txt_record(DOMAIN, "_acme-challenge.example.org",
"example-txt-contents", self.record_ttl)
expected_body = {
"kind": "dns#change",
@@ -180,8 +222,17 @@ class GoogleClientTest(unittest.TestCase):
{
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": self.record_name + ".",
"rrdatas": [self.record_content, ],
"name": "_acme-challenge.example.org.",
"rrdatas": ["\"sample-txt-contents\"", "\"example-txt-contents\""],
"ttl": self.record_ttl,
},
],
"additions": [
{
"kind": "dns#resourceRecordSet",
"type": "TXT",
"name": "_acme-challenge.example.org.",
"rrdatas": ["\"sample-txt-contents\"", ],
"ttl": self.record_ttl,
},
],
@@ -217,15 +268,44 @@ class GoogleClientTest(unittest.TestCase):
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_get_existing(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
# Record name mocked in setUp
found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
self.assertEquals(found, ["\"example-txt-contents\""])
not_found = client.get_existing_txt_rrset(self.zone, "nonexistent.tld")
self.assertEquals(not_found, None)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_get_existing_fallback(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(
[{'managedZones': [{'id': self.zone}]}])
# pylint: disable=no-member
mock_execute = client.dns.resourceRecordSets.return_value.list.return_value.execute
mock_execute.side_effect = API_ERROR
rrset = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
self.assertFalse(rrset)
def test_get_project_id(self):
from certbot_dns_google.dns_google import _GoogleClient
response = DummyResponse()
response.status = 200
with mock.patch('httplib2.Http.request', return_value=(response, 1234)):
with mock.patch('httplib2.Http.request', return_value=(response, 'test-test-1')):
project_id = _GoogleClient.get_project_id()
self.assertEqual(project_id, 1234)
self.assertEqual(project_id, 'test-test-1')
with mock.patch('httplib2.Http.request', return_value=(response, b'test-test-1')):
project_id = _GoogleClient.get_project_id()
self.assertEqual(project_id, 'test-test-1')
failed_response = DummyResponse()
failed_response.status = 404

File diff suppressed because it is too large Load Diff

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-google's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_google
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_google
:members:
Indices and tables

View File

@@ -0,0 +1,2 @@
acme[dev]==0.21.1
certbot[dev]==0.21.1

View File

@@ -0,0 +1,12 @@
# readthedocs.org gives no way to change the install command to "pip
# install -e .[docs]" (that would in turn install documentation
# dependencies), but it allows to specify a requirements.txt file at
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
# Although ReadTheDocs certainly doesn't need to install the project
# in --editable mode (-e), just "pip install .[docs]" does not work as
# expected and "pip install -e .[docs]" must be used instead
-e acme
-e .
-e certbot-dns-google[docs]

View File

@@ -1,23 +1,20 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.21.0.dev0'
version = '0.26.0.dev0'
# Please update tox.ini when modifying dependency version requirements
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'acme>=0.21.1',
'certbot>=0.21.1',
# 1.5 is the first version that supports oauth2client>=2.0
'google-api-python-client>=1.5',
'mock',
# for oauth2client.service_account.ServiceAccountCredentials
'oauth2client>=2.0',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.interface',
# already a dependency of google-api-python-client, but added for consistency
'httplib2'
@@ -36,6 +33,7 @@ setup(
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
@@ -44,10 +42,8 @@ setup(
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',

View File

@@ -0,0 +1,5 @@
FROM certbot/certbot
COPY . src/certbot-dns-luadns
RUN pip install --no-cache-dir --editable src/certbot-dns-luadns

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-luadns's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_luadns
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_luadns
:members:
Indices and tables

View File

@@ -0,0 +1,2 @@
acme[dev]==0.21.1
certbot[dev]==0.21.1

View File

@@ -0,0 +1,12 @@
# readthedocs.org gives no way to change the install command to "pip
# install -e .[docs]" (that would in turn install documentation
# dependencies), but it allows to specify a requirements.txt file at
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
# Although ReadTheDocs certainly doesn't need to install the project
# in --editable mode (-e), just "pip install .[docs]" does not work as
# expected and "pip install -e .[docs]" must be used instead
-e acme
-e .
-e certbot-dns-luadns[docs]

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