Compare commits

...

148 Commits

Author SHA1 Message Date
Brad Warren
d24b1f7310 document default is ACMEv2 2018-04-03 13:13:49 -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
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
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
183 changed files with 6244 additions and 1739 deletions

3
.gitignore vendored
View File

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

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,12 @@ matrix:
addons:
- python: "2.7"
env: TOXENV=lint
- python: "2.6"
env: TOXENV=py26
sudo: required
services: docker
- 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"

View File

@@ -2,6 +2,173 @@
Certbot adheres to [Semantic Versioning](http://semver.org/).
## 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,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

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

@@ -16,6 +16,7 @@ import re
import requests
import sys
from acme import crypto_util
from acme import errors
from acme import jws
from acme import messages
@@ -39,39 +40,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 +69,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 +82,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 +103,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 +127,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 +150,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 +211,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 +366,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,
@@ -492,26 +551,317 @@ 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,
"""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.
"""
def __init__(self, key, account=None, alg=jose.RS256, verify_ssl=True,
user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT):
# pylint: disable=too-many-arguments
self.key = key
self.account = account
self.alg = alg
self.verify_ssl = verify_ssl
self._nonces = set()
@@ -527,21 +877,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):
@@ -714,8 +1074,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
@@ -18,13 +20,32 @@ from acme import test_util
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 +54,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')
@@ -57,8 +63,7 @@ class ClientTest(unittest.TestCase):
contact=self.contact, key=KEY.public_key())
self.new_reg = messages.NewRegistration(**dict(reg))
self.regr = messages.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
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 +80,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 +299,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 +360,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 +386,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 +645,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 +660,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 +822,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 +1072,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')]

View File

@@ -5,9 +5,10 @@ import logging
import os
import re
import socket
import sys
import OpenSSL
import josepy as jose
from acme import errors
@@ -130,8 +131,7 @@ def probe_sni(name, host, port=443, timeout=300,
context = OpenSSL.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
@@ -186,6 +186,15 @@ def make_csr(private_key_pem, domains, must_staple=False):
return OpenSSL.crypto.dump_certificate_request(
OpenSSL.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.
@@ -271,3 +280,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=OpenSSL.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 OpenSSL.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

@@ -65,6 +65,30 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
# self.assertRaises(errors.Error, self._probe, b'bar')
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):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
@@ -170,9 +194,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 +212,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 +225,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."""

View File

@@ -171,9 +171,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 +272,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:'
@@ -482,3 +504,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

@@ -157,7 +157,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 +165,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 +408,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

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:

View File

@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
@@ -19,19 +19,10 @@ 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',
'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',
@@ -52,16 +43,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

@@ -5,6 +5,7 @@ import logging
import os
import pkg_resources
import re
import six
import socket
import time
@@ -152,6 +153,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.assoc = dict()
# Outstanding challenges
self._chall_out = set()
# List of vhosts configured per wildcard domain on this run.
# used by deploy_cert() and enhance()
self._wildcard_vhosts = dict()
# Maps enhancements to vhosts we've enabled the enhancement for
self._enhanced_vhosts = defaultdict(set)
@@ -262,12 +266,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
@@ -280,9 +299,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)]
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
@@ -302,16 +424,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,
@@ -327,8 +455,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)
@@ -391,7 +519,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:
@@ -1269,7 +1397,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
@@ -1373,8 +1504,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
except KeyError:
raise errors.PluginError(
"Unsupported enhancement: {0}".format(enhancement))
vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)
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
@@ -1989,5 +2123,3 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# to be modified.
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)

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

@@ -11,30 +11,43 @@ logger = logging.getLogger(__name__)
class ApacheHttp01(common.TLSSNI01):
"""Class that performs HTTP-01 challenges within the Apache configurator."""
CONFIG_TEMPLATE22 = """\
CONFIG_TEMPLATE22_PRE = """\
RewriteEngine on
RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [L]
"""
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_TEMPLATE24 = """\
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")
"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")
@@ -79,24 +92,32 @@ class ApacheHttp01(common.TLSSNI01):
chall.domain, filter_defaults=False,
port=str(self.configurator.config.http01_port))
if vh:
self._set_up_include_directive(vh)
self._set_up_include_directives(vh)
else:
for vh in self._relevant_vhosts():
self._set_up_include_directive(vh)
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_template_pre = self.CONFIG_TEMPLATE24_PRE
config_template_post = self.CONFIG_TEMPLATE24_POST
config_text = config_template.format(self.challenge_dir)
config_text_pre = config_template_pre.format(self.challenge_dir)
config_text_post = config_template_post.format(self.challenge_dir)
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)
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)
@@ -137,14 +158,17 @@ class ApacheHttp01(common.TLSSNI01):
return response
def _set_up_include_directive(self, vhost):
"""Includes override configuration to the beginning of VirtualHost.
Note that this include isn't added to Augeas search tree"""
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)
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

@@ -167,6 +167,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

View File

@@ -335,6 +335,33 @@ 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
tried_to_add = []
for a in mock_add.call_args_list:
tried_to_add.append(a[0][1] == "Include" and
a[0][2] == self.config.mod_ssl_conf)
# Include shouldn't be added, as patched find_dir "finds" existing one
self.assertFalse(any(tried_to_add))
def test_deploy_cert(self):
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
@@ -414,13 +441,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")
@@ -474,7 +525,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()
@@ -1306,6 +1361,106 @@ 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.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):
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)
class AugeasVhostsTest(util.ApacheTest):
"""Test vhosts with illegal names dependent on augeas version."""
# pylint: disable=protected-access

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

@@ -158,23 +158,31 @@ class ApacheHttp01Test(util.ApacheTest):
for vhost in vhosts:
if not vhost.ssl:
matches = self.config.parser.find_dir("Include",
self.http.challenge_conf,
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):
with open(self.http.challenge_conf) as f:
conf_contents = f.read()
with open(self.http.challenge_conf_pre) as f:
pre_conf_contents = f.read()
self.assertTrue("RewriteEngine on" in conf_contents)
self.assertTrue("RewriteRule" in conf_contents)
self.assertTrue(self.http.challenge_dir in conf_contents)
with open(self.http.challenge_conf_post) as f:
post_conf_contents = f.read()
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

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

View File

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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-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 +31,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 +40,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.21.0"
LE_AUTO_VERSION="0.22.2"
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)
@@ -246,7 +249,7 @@ DeprecationBootstrap() {
fi
}
MIN_PYTHON_VERSION="2.6"
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
@@ -761,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
@@ -863,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.
@@ -1190,24 +1199,24 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.21.0 \
--hash=sha256:b6fc9cf80e8e2925827c61ca92c32faa935bbadaf14448e2d7f40e1f8f2cccdb \
--hash=sha256:07ca3246d3462fe73418113cc5c1036545f4b2312831024da923054de3a85857
acme==0.21.0 \
--hash=sha256:4ef91a62c30b9d6bd1dd0b5ac3a8c7e70203e08e5269d3d26311dd6648aaacda \
--hash=sha256:d64eae267c0bb21c98fa889b4e0be4c473ca8e80488d3de057e803d6d167544d
certbot-apache==0.21.0 \
--hash=sha256:026c23fec4def727f88acd15f66b5641f7ba1f767f0728fd56798cf3500be0c5 \
--hash=sha256:185dae50c680fa3c09646907a6256c6b4ddf8525723d3b13b9b33d1a3118663b
certbot-nginx==0.21.0 \
--hash=sha256:e5ac3a203871f13e7e72d4922e401364342f2999d130c959f90949305c33d2bc \
--hash=sha256:88be95916935980edc4c6ec3f39031ac47f5b73d6e43dfa3694b927226432642
certbot==0.22.2 \
--hash=sha256:c8c63bdf0fed6258bdbc892454314ec37bcd1c35a7f62524a083d93ccdfc420d \
--hash=sha256:e6e3639293e78397f31f7d99e3c63aff82d91e2b0d50d146ee3c77f830464bef
acme==0.22.2 \
--hash=sha256:59a55244612ee305d2caa6bb4cddd400fb60ec841bf011ed29a2899832a682c2 \
--hash=sha256:0ecd0ea369f53d5bc744d6e72717f9af2e1ceb558d109dbd433148851027adb4
certbot-apache==0.22.2 \
--hash=sha256:b5340d4b9190358fde8eb6a5be0def37e32014b5142ee79ef5d2319ccbbde754 \
--hash=sha256:3cd26912bb5732d917ddf7aad2fe870090d4ece9a408b2c2de8e9723ec99c759
certbot-nginx==0.22.2 \
--hash=sha256:91feef0d879496835d355e82841f92e5ecb5abbf6f23ea0ee5bbb8f5a92b278a \
--hash=sha256:b10bf04c1a20cf878d5e0d1877deb0e0780bc31b0ffda08ce7199bbc39d0753b
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,
@@ -1231,6 +1240,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
@@ -1264,14 +1274,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 [])
@@ -1279,18 +1289,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')
]
@@ -1311,12 +1317,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):
@@ -1326,8 +1333,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):
@@ -1340,6 +1348,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])
@@ -1347,11 +1373,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:
@@ -1422,6 +1450,12 @@ UNLIKELY_EOF
say "Installation succeeded."
fi
if [ "$INSTALL_ONLY" = 1 ]; then
say "Certbot is installed."
exit 0
fi
"$VENV_BIN/letsencrypt" "$@"
else

View File

@@ -10,6 +10,8 @@ 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

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

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'
version = '0.23.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

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +30,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 +39,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

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +30,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 +41,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

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

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +31,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 +40,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

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +30,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 +41,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

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +30,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 +41,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

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

@@ -4,20 +4,19 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +35,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 +44,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]

View File

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +30,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 +41,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-nsone
RUN pip install --no-cache-dir --editable src/certbot-dns-nsone

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-nsone's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_nsone
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_nsone
: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-nsone[docs]

View File

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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 +30,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 +41,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-rfc2136
RUN pip install --no-cache-dir --editable src/certbot-dns-rfc2136

View File

@@ -70,11 +70,11 @@ class Authenticator(dns_common.DNSAuthenticator):
self._validate_algorithm
)
def _perform(self, domain, validation_name, validation):
self._get_rfc2136_client().add_txt_record(domain, validation_name, validation, self.ttl)
def _perform(self, _domain, validation_name, validation):
self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
def _cleanup(self, domain, validation_name, validation):
self._get_rfc2136_client().del_txt_record(domain, validation_name, validation)
def _cleanup(self, _domain, validation_name, validation):
self._get_rfc2136_client().del_txt_record(validation_name, validation)
def _get_rfc2136_client(self):
return _RFC2136Client(self.credentials.conf('server'),
@@ -95,18 +95,17 @@ class _RFC2136Client(object):
})
self.algorithm = key_algorithm
def add_txt_record(self, domain_name, record_name, record_content, record_ttl):
def add_txt_record(self, record_name, record_content, record_ttl):
"""
Add a TXT record using the supplied information.
:param str domain: The domain to use to find the closest SOA.
:param str record_name: The record name (typically beginning with '_acme-challenge.').
:param str record_content: The record content (typically the challenge validation).
:param int record_ttl: The record TTL (number of seconds that the record may be cached).
:raises certbot.errors.PluginError: if an error occurs communicating with the DNS server
"""
domain = self._find_domain(domain_name)
domain = self._find_domain(record_name)
n = dns.name.from_text(record_name)
o = dns.name.from_text(domain)
@@ -131,18 +130,17 @@ class _RFC2136Client(object):
raise errors.PluginError('Received response from server: {0}'
.format(dns.rcode.to_text(rcode)))
def del_txt_record(self, domain_name, record_name, record_content):
def del_txt_record(self, record_name, record_content):
"""
Delete a TXT record using the supplied information.
:param str domain: The domain to use to find the closest SOA.
:param str record_name: The record name (typically beginning with '_acme-challenge.').
:param str record_content: The record content (typically the challenge validation).
:param int record_ttl: The record TTL (number of seconds that the record may be cached).
:raises certbot.errors.PluginError: if an error occurs communicating with the DNS server
"""
domain = self._find_domain(domain_name)
domain = self._find_domain(record_name)
n = dns.name.from_text(record_name)
o = dns.name.from_text(domain)
@@ -167,17 +165,17 @@ class _RFC2136Client(object):
raise errors.PluginError('Received response from server: {0}'
.format(dns.rcode.to_text(rcode)))
def _find_domain(self, domain_name):
def _find_domain(self, record_name):
"""
Find the closest domain with an SOA record for a given domain name.
:param str domain_name: The domain name for which to find the closest SOA record.
:param str record_name: The record name for which to find the closest SOA record.
:returns: The domain, if found.
:rtype: str
:raises certbot.errors.PluginError: if no SOA record can be found.
"""
domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)
domain_name_guesses = dns_common.base_domain_name_guesses(record_name)
# Loop through until we find an authoritative SOA record
for guess in domain_name_guesses:
@@ -185,7 +183,7 @@ class _RFC2136Client(object):
return guess
raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'
.format(domain_name, domain_name_guesses))
.format(record_name, domain_name_guesses))
def _query_soa(self, domain_name):
"""

View File

@@ -41,7 +41,7 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
def test_perform(self):
self.auth.perform([self.achall])
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
expected = [mock.call.add_txt_record('_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
self.assertEqual(expected, self.mock_client.mock_calls)
def test_cleanup(self):
@@ -49,7 +49,7 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
self.auth._attempt_cleanup = True
self.auth.cleanup([self.achall])
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]
expected = [mock.call.del_txt_record('_acme-challenge.'+DOMAIN, mock.ANY)]
self.assertEqual(expected, self.mock_client.mock_calls)
def test_invalid_algorithm_raises(self):
@@ -82,7 +82,7 @@ class RFC2136ClientTest(unittest.TestCase):
# _find_domain | pylint: disable=protected-access
self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com")
self.rfc2136_client.add_txt_record(DOMAIN, "bar", "baz", 42)
self.rfc2136_client.add_txt_record("bar", "baz", 42)
query_mock.assert_called_with(mock.ANY, SERVER)
self.assertTrue("bar. 42 IN TXT \"baz\"" in str(query_mock.call_args[0][0]))
@@ -96,7 +96,7 @@ class RFC2136ClientTest(unittest.TestCase):
self.assertRaises(
errors.PluginError,
self.rfc2136_client.add_txt_record,
DOMAIN, "bar", "baz", 42)
"bar", "baz", 42)
@mock.patch("dns.query.tcp")
def test_add_txt_record_server_error(self, query_mock):
@@ -107,7 +107,7 @@ class RFC2136ClientTest(unittest.TestCase):
self.assertRaises(
errors.PluginError,
self.rfc2136_client.add_txt_record,
DOMAIN, "bar", "baz", 42)
"bar", "baz", 42)
@mock.patch("dns.query.tcp")
def test_del_txt_record(self, query_mock):
@@ -115,7 +115,7 @@ class RFC2136ClientTest(unittest.TestCase):
# _find_domain | pylint: disable=protected-access
self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com")
self.rfc2136_client.del_txt_record(DOMAIN, "bar", "baz")
self.rfc2136_client.del_txt_record("bar", "baz")
query_mock.assert_called_with(mock.ANY, SERVER)
self.assertTrue("bar. 0 NONE TXT \"baz\"" in str(query_mock.call_args[0][0]))
@@ -129,7 +129,7 @@ class RFC2136ClientTest(unittest.TestCase):
self.assertRaises(
errors.PluginError,
self.rfc2136_client.del_txt_record,
DOMAIN, "bar", "baz")
"bar", "baz")
@mock.patch("dns.query.tcp")
def test_del_txt_record_server_error(self, query_mock):
@@ -140,7 +140,7 @@ class RFC2136ClientTest(unittest.TestCase):
self.assertRaises(
errors.PluginError,
self.rfc2136_client.del_txt_record,
DOMAIN, "bar", "baz")
"bar", "baz")
def test_find_domain(self):
# _query_soa | pylint: disable=protected-access

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-rfc2136's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_rfc2136
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_rfc2136
: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-rfc2136[docs]

View File

@@ -4,17 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.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',
'dnspython',
'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 +30,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 +39,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-route53
RUN pip install --no-cache-dir --editable src/certbot-dns-route53

View File

@@ -1,4 +1,5 @@
"""Certbot Route53 authenticator plugin."""
import collections
import logging
import time
@@ -33,6 +34,7 @@ class Authenticator(dns_common.DNSAuthenticator):
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
self.r53 = boto3.client("route53")
self._resource_records = collections.defaultdict(list)
def more_info(self): # pylint: disable=missing-docstring,no-self-use
return "Solve a DNS01 challenge using AWS Route53"
@@ -88,6 +90,20 @@ class Authenticator(dns_common.DNSAuthenticator):
def _change_txt_record(self, action, validation_domain_name, validation):
zone_id = self._find_zone_id_for_domain(validation_domain_name)
rrecords = self._resource_records[validation_domain_name]
challenge = {"Value": '"{0}"'.format(validation)}
if action == "DELETE":
# Remove the record being deleted from the list of tracked records
rrecords.remove(challenge)
if rrecords:
# Need to update instead, as we're not deleting the rrset
action = "UPSERT"
else:
# Create a new list containing the record to use with DELETE
rrecords = [challenge]
else:
rrecords.append(challenge)
response = self.r53.change_resource_record_sets(
HostedZoneId=zone_id,
ChangeBatch={
@@ -99,11 +115,7 @@ class Authenticator(dns_common.DNSAuthenticator):
"Name": validation_domain_name,
"Type": "TXT",
"TTL": self.ttl,
"ResourceRecords": [
# For some reason TXT records need to be
# manually quoted.
{"Value": '"{0}"'.format(validation)}
],
"ResourceRecords": rrecords,
}
}
]

View File

@@ -186,6 +186,48 @@ class ClientTest(unittest.TestCase):
call_count = self.client.r53.change_resource_record_sets.call_count
self.assertEqual(call_count, 1)
def test_change_txt_record_delete(self):
self.client._find_zone_id_for_domain = mock.MagicMock()
self.client.r53.change_resource_record_sets = mock.MagicMock(
return_value={"ChangeInfo": {"Id": 1}})
validation = "some-value"
validation_record = {"Value": '"{0}"'.format(validation)}
self.client._resource_records[DOMAIN] = [validation_record]
self.client._change_txt_record("DELETE", DOMAIN, validation)
call_count = self.client.r53.change_resource_record_sets.call_count
self.assertEqual(call_count, 1)
call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1]
call_args_batch = call_args["ChangeBatch"]["Changes"][0]
self.assertEqual(call_args_batch["Action"], "DELETE")
self.assertEqual(
call_args_batch["ResourceRecordSet"]["ResourceRecords"],
[validation_record])
def test_change_txt_record_multirecord(self):
self.client._find_zone_id_for_domain = mock.MagicMock()
self.client._get_validation_rrset = mock.MagicMock()
self.client._resource_records[DOMAIN] = [
{"Value": "\"pre-existing-value\""},
{"Value": "\"pre-existing-value-two\""},
]
self.client.r53.change_resource_record_sets = mock.MagicMock(
return_value={"ChangeInfo": {"Id": 1}})
self.client._change_txt_record("DELETE", DOMAIN, "pre-existing-value")
call_count = self.client.r53.change_resource_record_sets.call_count
call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1]
call_args_batch = call_args["ChangeBatch"]["Changes"][0]
self.assertEqual(call_args_batch["Action"], "UPSERT")
self.assertEqual(
call_args_batch["ResourceRecordSet"]["ResourceRecords"],
[{"Value": "\"pre-existing-value-two\""}])
self.assertEqual(call_count, 1)
def test_wait_for_change(self):
self.client.r53.get_change = mock.MagicMock(
side_effect=[{"ChangeInfo": {"Status": "PENDING"}},

View File

@@ -10,14 +10,14 @@ Welcome to certbot-dns-route53's documentation!
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_dns_route53
:members:
.. toctree::
:maxdepth: 1
api
.. automodule:: certbot_dns_route53
: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-route53[docs]

View File

@@ -3,16 +3,16 @@ import sys
from distutils.core import setup
from setuptools import find_packages
version = '0.21.0'
version = '0.23.0.dev0'
# 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',
'boto3',
'mock',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.interface',
]
@@ -24,6 +24,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',
@@ -32,10 +33,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

@@ -23,6 +23,7 @@ from certbot import util
from certbot.plugins import common
from certbot_nginx import constants
from certbot_nginx import display_ops
from certbot_nginx import nginxparser
from certbot_nginx import parser
from certbot_nginx import tls_sni_01
@@ -31,16 +32,6 @@ from certbot_nginx import http_01
logger = logging.getLogger(__name__)
REDIRECT_BLOCK = [
['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'],
['\n']
]
REDIRECT_COMMENT_BLOCK = [
['\n ', '#', ' Redirect non-https traffic to https'],
['\n ', '#', ' return 301 https://$host$request_uri;'],
['\n']
]
@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
@zope.interface.provider(interfaces.IPluginFactory)
@@ -70,6 +61,9 @@ class NginxConfigurator(common.Installer):
DEFAULT_LISTEN_PORT = '80'
# SSL directives that Certbot can add when installing a new certificate.
SSL_DIRECTIVES = ['ssl_certificate', 'ssl_certificate_key', 'ssl_dhparam']
@classmethod
def add_parser_arguments(cls, add):
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
@@ -102,6 +96,11 @@ class NginxConfigurator(common.Installer):
# For creating new vhosts if no names match
self.new_vhost = None
# List of vhosts configured per wildcard domain on this run.
# used by deploy_cert() and enhance()
self._wildcard_vhosts = {}
self._wildcard_redirect_vhosts = {}
# Add number of outstanding challenges
self._chall_out = 0
@@ -109,6 +108,7 @@ class NginxConfigurator(common.Installer):
self.parser = None
self.version = version
self._enhance_func = {"redirect": self._enable_redirect,
"ensure-http-header": self._set_http_header,
"staple-ocsp": self._enable_ocsp_stapling}
self.reverter.recovery_routine()
@@ -156,6 +156,7 @@ class NginxConfigurator(common.Installer):
raise errors.PluginError(
'Unable to lock %s', self.conf('server-root'))
# Entry point in main.py for installing cert
def deploy_cert(self, domain, cert_path, key_path,
chain_path=None, fullchain_path=None):
@@ -176,14 +177,24 @@ class NginxConfigurator(common.Installer):
"The nginx plugin currently requires --fullchain-path to "
"install a cert.")
vhost = self.choose_vhost(domain, create_if_no_match=True)
vhosts = self.choose_vhosts(domain, create_if_no_match=True)
for vhost in vhosts:
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
def _deploy_cert(self, vhost, cert_path, key_path, chain_path, fullchain_path):
# pylint: disable=unused-argument
"""
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
"""
cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path],
['\n ', 'ssl_certificate_key', ' ', key_path]]
self.parser.add_server_directives(vhost,
cert_directives, replace=True)
logger.info("Deployed Certificate to VirtualHost %s for %s",
vhost.filep, ", ".join(vhost.names))
self.parser.update_or_add_server_directives(vhost,
cert_directives)
logger.info("Deploying Certificate to VirtualHost %s", vhost.filep)
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
(vhost.filep,
@@ -191,10 +202,61 @@ class NginxConfigurator(common.Installer):
self.save_notes += "\tssl_certificate %s\n" % fullchain_path
self.save_notes += "\tssl_certificate_key %s\n" % key_path
def _choose_vhosts_wildcard(self, domain, prefer_ssl, no_ssl_filter_port=None):
"""Prompts user to choose vhosts to install a wildcard certificate for"""
if prefer_ssl:
vhosts_cache = self._wildcard_vhosts
preference_test = lambda x: x.ssl
else:
vhosts_cache = self._wildcard_redirect_vhosts
preference_test = lambda x: not x.ssl
# Caching!
if domain in vhosts_cache:
# Vhosts for a wildcard domain were already selected
return vhosts_cache[domain]
# Get all vhosts whether or not they are covered by the wildcard domain
vhosts = self.parser.get_vhosts()
# Go through the vhosts, making sure that we cover all the names
# present, but preferring the SSL or non-SSL vhosts
filtered_vhosts = {}
for vhost in vhosts:
# Ensure we're listening non-sslishly on no_ssl_filter_port
if no_ssl_filter_port is not None:
if not self._vhost_listening_on_port_no_ssl(vhost, no_ssl_filter_port):
continue
for name in vhost.names:
if preference_test(vhost):
# Prefer either SSL or non-SSL vhosts
filtered_vhosts[name] = vhost
elif name not in filtered_vhosts:
# 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
return_vhosts = display_ops.select_vhost_multiple(list(dialog_input))
for vhost in return_vhosts:
if domain not in vhosts_cache:
vhosts_cache[domain] = []
vhosts_cache[domain].append(vhost)
return return_vhosts
#######################
# Vhost parsing methods
#######################
def choose_vhost(self, target_name, create_if_no_match=False):
def _choose_vhost_single(self, target_name):
matches = self._get_ranked_matches(target_name)
vhosts = [x for x in [self._select_best_name_match(matches)] if x is not None]
return vhosts
def choose_vhosts(self, target_name, create_if_no_match=False):
"""Chooses a virtual host based on the given domain name.
.. note:: This makes the vhost SSL-enabled if it isn't already. Follows
@@ -212,17 +274,19 @@ class NginxConfigurator(common.Installer):
when there is no match found. If we can't choose a default, raise a
MisconfigurationError.
:returns: ssl vhost associated with name
:rtype: :class:`~certbot_nginx.obj.VirtualHost`
:returns: ssl vhosts associated with name
:rtype: list of :class:`~certbot_nginx.obj.VirtualHost`
"""
vhost = None
matches = self._get_ranked_matches(target_name)
vhost = self._select_best_name_match(matches)
if not vhost:
if util.is_wildcard_domain(target_name):
# Ask user which VHosts to support.
vhosts = self._choose_vhosts_wildcard(target_name, prefer_ssl=True)
else:
vhosts = self._choose_vhost_single(target_name)
if not vhosts:
if create_if_no_match:
vhost = self._vhost_from_duplicated_default(target_name)
# result will not be [None] because it errors on failure
vhosts = [self._vhost_from_duplicated_default(target_name)]
else:
# No matches. Raise a misconfiguration error.
raise errors.MisconfigurationError(
@@ -232,10 +296,11 @@ class NginxConfigurator(common.Installer):
"nginx configuration: "
"https://nginx.org/en/docs/http/server_names.html") % (target_name))
# Note: if we are enhancing with ocsp, vhost should already be ssl.
if not vhost.ssl:
self._make_server_ssl(vhost)
for vhost in vhosts:
if not vhost.ssl:
self._make_server_ssl(vhost)
return vhost
return vhosts
def ipv6_info(self, port):
"""Returns tuple of booleans (ipv6_active, ipv6only_present)
@@ -250,6 +315,9 @@ class NginxConfigurator(common.Installer):
configuration, and existence of ipv6only directive for specified port
:rtype: tuple of type (bool, bool)
"""
# port should be a string, but it's easy to mess up, so let's
# make sure it is one
port = str(port)
vhosts = self.parser.get_vhosts()
ipv6_active = False
ipv6only_present = False
@@ -264,7 +332,8 @@ class NginxConfigurator(common.Installer):
def _vhost_from_duplicated_default(self, domain, port=None):
if self.new_vhost is None:
default_vhost = self._get_default_vhost(port)
self.new_vhost = self.parser.duplicate_vhost(default_vhost, delete_default=True)
self.new_vhost = self.parser.duplicate_vhost(default_vhost,
remove_singleton_listen_params=True)
self.new_vhost.names = set()
self._add_server_name_to_vhost(self.new_vhost, domain)
@@ -276,7 +345,7 @@ class NginxConfigurator(common.Installer):
for name in vhost.names:
name_block[0].append(' ')
name_block[0].append(name)
self.parser.add_server_directives(vhost, name_block, replace=True)
self.parser.update_or_add_server_directives(vhost, name_block)
def _get_default_vhost(self, port):
vhost_list = self.parser.get_vhosts()
@@ -369,7 +438,7 @@ class NginxConfigurator(common.Installer):
return sorted(matches, key=lambda x: x['rank'])
def choose_redirect_vhost(self, target_name, port, create_if_no_match=False):
def choose_redirect_vhosts(self, target_name, port, create_if_no_match=False):
"""Chooses a single virtual host for redirect enhancement.
Chooses the vhost most closely matching target_name that is
@@ -387,15 +456,20 @@ class NginxConfigurator(common.Installer):
when there is no match found. If we can't choose a default, raise a
MisconfigurationError.
:returns: vhost associated with name
:rtype: :class:`~certbot_nginx.obj.VirtualHost`
:returns: vhosts associated with name
:rtype: list of :class:`~certbot_nginx.obj.VirtualHost`
"""
matches = self._get_redirect_ranked_matches(target_name, port)
vhost = self._select_best_name_match(matches)
if not vhost and create_if_no_match:
vhost = self._vhost_from_duplicated_default(target_name, port=port)
return vhost
if util.is_wildcard_domain(target_name):
# Ask user which VHosts to enhance.
vhosts = self._choose_vhosts_wildcard(target_name, prefer_ssl=False,
no_ssl_filter_port=port)
else:
matches = self._get_redirect_ranked_matches(target_name, port)
vhosts = [x for x in [self._select_best_name_match(matches)]if x is not None]
if not vhosts and create_if_no_match:
vhosts = [self._vhost_from_duplicated_default(target_name, port=port)]
return vhosts
def _port_matches(self, test_port, matching_port):
# test_port is a number, matching is a number or "" or None
@@ -405,6 +479,23 @@ class NginxConfigurator(common.Installer):
else:
return test_port == matching_port
def _vhost_listening_on_port_no_ssl(self, vhost, port):
found_matching_port = False
if len(vhost.addrs) == 0:
# if there are no listen directives at all, Nginx defaults to
# listening on port 80.
found_matching_port = (port == self.DEFAULT_LISTEN_PORT)
else:
for addr in vhost.addrs:
if self._port_matches(port, addr.get_port()) and addr.ssl == False:
found_matching_port = True
if found_matching_port:
# make sure we don't have an 'ssl on' directive
return not self.parser.has_ssl_on_directive(vhost)
else:
return False
def _get_redirect_ranked_matches(self, target_name, port):
"""Gets a ranked list of plaintextish port-listening vhosts matching target_name
@@ -421,21 +512,7 @@ class NginxConfigurator(common.Installer):
all_vhosts = self.parser.get_vhosts()
def _vhost_matches(vhost, port):
found_matching_port = False
if len(vhost.addrs) == 0:
# if there are no listen directives at all, Nginx defaults to
# listening on port 80.
found_matching_port = (port == self.DEFAULT_LISTEN_PORT)
else:
for addr in vhost.addrs:
if self._port_matches(port, addr.get_port()) and addr.ssl == False:
found_matching_port = True
if found_matching_port:
# make sure we don't have an 'ssl on' directive
return not self.parser.has_ssl_on_directive(vhost)
else:
return False
return self._vhost_listening_on_port_no_ssl(vhost, port)
matching_vhosts = [vhost for vhost in all_vhosts if _vhost_matches(vhost, port)]
@@ -508,7 +585,7 @@ class NginxConfigurator(common.Installer):
# have it continue to do so.
if len(vhost.addrs) == 0:
listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]]
self.parser.add_server_directives(vhost, listen_block, replace=False)
self.parser.add_server_directives(vhost, listen_block)
if vhost.ipv6_enabled():
ipv6_block = ['\n ',
@@ -542,14 +619,14 @@ class NginxConfigurator(common.Installer):
])
self.parser.add_server_directives(
vhost, ssl_block, replace=False)
vhost, ssl_block)
##################################
# enhancement methods (IInstaller)
##################################
def supported_enhancements(self): # pylint: disable=no-self-use
"""Returns currently supported enhancements."""
return ['redirect', 'staple-ocsp']
return ['redirect', 'ensure-http-header', 'staple-ocsp']
def enhance(self, domain, enhancement, options=None):
"""Enhance configuration.
@@ -571,24 +648,84 @@ class NginxConfigurator(common.Installer):
logger.warning("Failed %s for %s", enhancement, domain)
raise
def _has_certbot_redirect(self, vhost):
test_redirect_block = _test_block_from_block(REDIRECT_BLOCK)
def _has_certbot_redirect(self, vhost, domain):
test_redirect_block = _test_block_from_block(_redirect_block_for_domain(domain))
return vhost.contains_list(test_redirect_block)
def _has_certbot_redirect_comment(self, vhost):
test_redirect_comment_block = _test_block_from_block(REDIRECT_COMMENT_BLOCK)
return vhost.contains_list(test_redirect_comment_block)
def _set_http_header(self, domain, header_substring):
"""Enables header identified by header_substring on domain.
def _add_redirect_block(self, vhost, active=True):
If the vhost is listening plaintextishly, separates out the relevant
directives into a new server block, and only add header directive to
HTTPS block.
:param str domain: the domain to enable header for.
:param str header_substring: String to uniquely identify a header.
e.g. Strict-Transport-Security, Upgrade-Insecure-Requests
:returns: Success
:raises .errors.PluginError: If no viable HTTPS host can be created or
set with header header_substring.
"""
vhosts = self.choose_vhosts(domain)
if not vhosts:
raise errors.PluginError(
"Unable to find corresponding HTTPS host for enhancement.")
for vhost in vhosts:
if vhost.has_header(header_substring):
raise errors.PluginEnhancementAlreadyPresent(
"Existing %s header" % (header_substring))
# if there is no separate SSL block, break the block into two and
# choose the SSL block.
if vhost.ssl and any([not addr.ssl for addr in vhost.addrs]):
_, vhost = self._split_block(vhost)
header_directives = [
['\n ', 'add_header', ' ', header_substring, ' '] +
constants.HEADER_ARGS[header_substring],
['\n']]
self.parser.add_server_directives(vhost, header_directives)
def _add_redirect_block(self, vhost, domain):
"""Add redirect directive to vhost
"""
if active:
redirect_block = REDIRECT_BLOCK
else:
redirect_block = REDIRECT_COMMENT_BLOCK
redirect_block = _redirect_block_for_domain(domain)
self.parser.add_server_directives(
vhost, redirect_block, replace=False)
vhost, redirect_block, insert_at_top=True)
def _split_block(self, vhost, only_directives=None):
"""Splits this "virtual host" (i.e. this nginx server block) into
separate HTTP and HTTPS blocks.
:param vhost: The server block to break up into two.
:param list only_directives: If this exists, only duplicate these directives
when splitting the block.
:type vhost: :class:`~certbot_nginx.obj.VirtualHost`
:returns: tuple (http_vhost, https_vhost)
:rtype: tuple of type :class:`~certbot_nginx.obj.VirtualHost`
"""
http_vhost = self.parser.duplicate_vhost(vhost, only_directives=only_directives)
def _ssl_match_func(directive):
return 'ssl' in directive
def _ssl_config_match_func(directive):
return self.mod_ssl_conf in directive
def _no_ssl_match_func(directive):
return 'ssl' not in directive
# remove all ssl addresses and related directives from the new block
for directive in self.SSL_DIRECTIVES:
self.parser.remove_server_directives(http_vhost, directive)
self.parser.remove_server_directives(http_vhost, 'listen', match_func=_ssl_match_func)
self.parser.remove_server_directives(http_vhost, 'include',
match_func=_ssl_config_match_func)
# remove all non-ssl addresses from the existing block
self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func)
return http_vhost, vhost
def _enable_redirect(self, domain, unused_options):
"""Redirect all equivalent HTTP traffic to ssl_vhost.
@@ -604,47 +741,47 @@ class NginxConfigurator(common.Installer):
"""
port = self.DEFAULT_LISTEN_PORT
vhost = None
# If there are blocks listening plaintextishly on self.DEFAULT_LISTEN_PORT,
# choose the most name-matching one.
vhost = self.choose_redirect_vhost(domain, port)
vhosts = self.choose_redirect_vhosts(domain, port)
if vhost is None:
if not vhosts:
logger.info("No matching insecure server blocks listening on port %s found.",
self.DEFAULT_LISTEN_PORT)
return
for vhost in vhosts:
self._enable_redirect_single(domain, vhost)
def _enable_redirect_single(self, domain, vhost):
"""Redirect all equivalent HTTP traffic to ssl_vhost.
If the vhost is listening plaintextishly, separate out the
relevant directives into a new server block and add a rewrite directive.
.. note:: This function saves the configuration
:param str domain: domain to enable redirect for
:param `~obj.Vhost` vhost: vhost to enable redirect for
"""
http_vhost = None
if vhost.ssl:
new_vhost = self.parser.duplicate_vhost(vhost,
only_directives=['listen', 'server_name'])
http_vhost, _ = self._split_block(vhost, ['listen', 'server_name'])
def _ssl_match_func(directive):
return 'ssl' in directive
# Add this at the bottom to get the right order of directives
return_404_directive = [['\n ', 'return', ' ', '404']]
self.parser.add_server_directives(http_vhost, return_404_directive)
def _no_ssl_match_func(directive):
return 'ssl' not in directive
vhost = http_vhost
# remove all ssl addresses from the new block
self.parser.remove_server_directives(new_vhost, 'listen', match_func=_ssl_match_func)
# remove all non-ssl addresses from the existing block
self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func)
vhost = new_vhost
if self._has_certbot_redirect(vhost):
if self._has_certbot_redirect(vhost, domain):
logger.info("Traffic on port %s already redirecting to ssl in %s",
self.DEFAULT_LISTEN_PORT, vhost.filep)
elif vhost.has_redirect():
if not self._has_certbot_redirect_comment(vhost):
self._add_redirect_block(vhost, active=False)
logger.info("The appropriate server block is already redirecting "
"traffic. To enable redirect anyway, uncomment the "
"redirect lines in %s.", vhost.filep)
else:
# Redirect plaintextish host to https
self._add_redirect_block(vhost, active=True)
self._add_redirect_block(vhost, domain)
logger.info("Redirecting all traffic on port %s to ssl in %s",
self.DEFAULT_LISTEN_PORT, vhost.filep)
@@ -656,7 +793,18 @@ class NginxConfigurator(common.Installer):
:type chain_path: `str` or `None`
"""
vhost = self.choose_vhost(domain)
vhosts = self.choose_vhosts(domain)
for vhost in vhosts:
self._enable_ocsp_stapling_single(vhost, chain_path)
def _enable_ocsp_stapling_single(self, vhost, chain_path):
"""Include OCSP response in TLS handshake
:param str vhost: vhost to enable OCSP response for
:param chain_path: chain file path
:type chain_path: `str` or `None`
"""
if self.version < (1, 3, 7):
raise errors.PluginError("Version 1.3.7 or greater of nginx "
"is needed to enable OCSP stapling")
@@ -674,7 +822,7 @@ class NginxConfigurator(common.Installer):
try:
self.parser.add_server_directives(vhost,
stapling_directives, replace=False)
stapling_directives)
except errors.MisconfigurationError as error:
logger.debug(error)
raise errors.PluginError("An error occurred while enabling OCSP "
@@ -748,7 +896,7 @@ class NginxConfigurator(common.Installer):
raise errors.PluginError(
"Unable to run %s -V" % self.conf('ctl'))
version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE)
version_regex = re.compile(r"nginx version: ([^/]+)/([0-9\.]*)", re.IGNORECASE)
version_matches = version_regex.findall(text)
sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE)
@@ -765,7 +913,12 @@ class NginxConfigurator(common.Installer):
if not sni_matches:
raise errors.PluginError("Nginx build doesn't support SNI")
nginx_version = tuple([int(i) for i in version_matches[0].split(".")])
product_name, product_version = version_matches[0]
if product_name != 'nginx':
logger.warning("NGINX derivative %s is not officially supported by"
" certbot", product_name)
nginx_version = tuple([int(i) for i in product_version.split(".")])
# nginx < 0.8.48 uses machine hostname as default server_name instead of
# the empty string
@@ -907,6 +1060,23 @@ def _test_block_from_block(block):
parser.comment_directive(test_block, 0)
return test_block[:-1]
def _redirect_block_for_domain(domain):
updated_domain = domain
match_symbol = '='
if util.is_wildcard_domain(domain):
match_symbol = '~'
updated_domain = updated_domain.replace('.', r'\.')
updated_domain = updated_domain.replace('*', '[^.]+')
updated_domain = '^' + updated_domain + '$'
redirect_block = [[
['\n ', 'if', ' ', '($host', ' ', match_symbol, ' ', '%s)' % updated_domain, ' '],
[['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'],
'\n ']],
['\n']]
return redirect_block
def nginx_restart(nginx_ctl, nginx_conf):
"""Restarts the Nginx Server.

View File

@@ -44,3 +44,7 @@ def os_constant(key):
:return: value of constant for active os
"""
return CLI_DEFAULTS[key]
HSTS_ARGS = ['\"max-age=31536000\"', ' ', 'always']
HEADER_ARGS = {'Strict-Transport-Security': HSTS_ARGS}

View File

@@ -0,0 +1,44 @@
"""Contains UI methods for Nginx operations."""
import logging
import zope.component
from certbot import interfaces
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 Nginx 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 server blocks would you like to modify?",
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

View File

@@ -159,16 +159,22 @@ class NginxHttp01(common.ChallengePerformer):
document_root = os.path.join(
self.configurator.config.work_dir, "http_01_nonexistent")
block.extend([['server_name', ' ', achall.domain],
['root', ' ', document_root],
self._location_directive_for_achall(achall)
])
# TODO: do we want to return something else if they otherwise access this block?
return [['server'], block]
def _location_directive_for_achall(self, achall):
validation = achall.validation(achall.account_key)
validation_path = self._get_validation_path(achall)
block.extend([['server_name', ' ', achall.domain],
['root', ' ', document_root],
[['location', ' ', '=', ' ', validation_path],
[['default_type', ' ', 'text/plain'],
['return', ' ', '200', ' ', validation]]]])
# TODO: do we want to return something else if they otherwise access this block?
return [['server'], block]
location_directive = [['location', ' ', '=', ' ', validation_path],
[['default_type', ' ', 'text/plain'],
['return', ' ', '200', ' ', validation]]]
return location_directive
def _make_or_mod_server_block(self, achall):
"""Modifies a server block to respond to a challenge.
@@ -179,25 +185,24 @@ class NginxHttp01(common.ChallengePerformer):
"""
try:
vhost = self.configurator.choose_redirect_vhost(achall.domain,
vhosts = self.configurator.choose_redirect_vhosts(achall.domain,
'%i' % self.configurator.config.http01_port, create_if_no_match=True)
except errors.MisconfigurationError:
# Couldn't find either a matching name+port server block
# or a port+default_server block, so create a dummy block
return self._make_server_block(achall)
# Modify existing server block
validation = achall.validation(achall.account_key)
validation_path = self._get_validation_path(achall)
# len is max 1 because Nginx doesn't authenticate wildcards
# if len were or vhosts None, we would have errored
vhost = vhosts[0]
location_directive = [[['location', ' ', '=', ' ', validation_path],
[['default_type', ' ', 'text/plain'],
['return', ' ', '200', ' ', validation]]]]
# Modify existing server block
location_directive = [self._location_directive_for_achall(achall)]
self.configurator.parser.add_server_directives(vhost,
location_directive, replace=False)
location_directive)
rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)',
' ', '$1', ' ', 'break']]
self.configurator.parser.add_server_directives(vhost,
rewrite_directive, replace=False, insert_at_top=True)
rewrite_directive, insert_at_top=True)

View File

@@ -5,7 +5,7 @@ import six
from certbot.plugins import common
REDIRECT_DIRECTIVES = ['return', 'rewrite']
ADD_HEADER_DIRECTIVE = 'add_header'
class Addr(common.Addr):
r"""Represents an Nginx address, i.e. what comes after the 'listen'
@@ -29,6 +29,8 @@ class Addr(common.Addr):
:param str port: port number or "\*" or ""
:param bool ssl: Whether the directive includes 'ssl'
:param bool default: Whether the directive includes 'default_server'
:param bool default: Whether this is an IPv6 address
:param bool ipv6only: Whether the directive includes 'ipv6only=on'
"""
UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0')
@@ -88,6 +90,8 @@ class Addr(common.Addr):
ssl = True
elif nextpart == 'default_server':
default = True
elif nextpart == 'default':
default = True
elif nextpart == "ipv6only=on":
ipv6only = True
@@ -193,14 +197,18 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
return False
def has_redirect(self):
"""Determine if this vhost has a redirecting statement
def __hash__(self):
return hash((self.filep, tuple(self.path),
tuple(self.addrs), tuple(self.names),
self.ssl, self.enabled))
def has_header(self, header_name):
"""Determine if this server block has a particular header set.
:param str header_name: The name of the header to check for, e.g.
'Strict-Transport-Security'
"""
for directive_name in REDIRECT_DIRECTIVES:
found = _find_directive(self.raw, directive_name)
if found is not None:
return True
return False
found = _find_directive(self.raw, ADD_HEADER_DIRECTIVE, header_name)
return found is not None
def contains_list(self, test):
"""Determine if raw server block contains test list at top level
@@ -226,14 +234,30 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
if not a.ipv6:
return True
def _find_directive(directives, directive_name):
"""Find a directive of type directive_name in directives
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.names),
https="Yes" if self.ssl else "No"))
def _find_directive(directives, directive_name, match_content=None):
"""Find a directive of type directive_name in directives. If match_content is given,
Searches for `match_content` in the directive arguments.
"""
if not directives or isinstance(directives, six.string_types) or len(directives) == 0:
return None
if directives[0] == directive_name:
# If match_content is None, just match on directive type. Otherwise, match on
# both directive type -and- the content!
if directives[0] == directive_name and \
(match_content is None or match_content in directives):
return directives
matches = (_find_directive(line, directive_name) for line in directives)
matches = (_find_directive(line, directive_name, match_content) for line in directives)
return next((m for m in matches if m is not None), None)

View File

@@ -276,15 +276,13 @@ class NginxParser(object):
return False
def add_server_directives(self, vhost, directives, replace, insert_at_top=False):
"""Add or replace directives in the server block identified by vhost.
def add_server_directives(self, vhost, directives, insert_at_top=False):
"""Add directives to the server block identified by vhost.
This method modifies vhost to be fully consistent with the new directives.
..note :: If replace is True and the directive already exists, the first
instance will be replaced. Otherwise, the directive is added.
..note :: If replace is False nothing gets added if an identical
block exists already.
..note :: It's an error to try and add a nonrepeatable directive that already
exists in the config block with a conflicting value.
..todo :: Doesn't match server blocks whose server_name directives are
split across multiple conf files.
@@ -292,13 +290,34 @@ class NginxParser(object):
:param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost
whose information we use to match on
:param list directives: The directives to add
:param bool replace: Whether to only replace existing directives
:param bool insert_at_top: True if the directives need to be inserted at the top
of the server block instead of the bottom
"""
self._modify_server_directives(vhost,
functools.partial(_add_directives, directives, replace, insert_at_top))
functools.partial(_add_directives, directives, insert_at_top))
def update_or_add_server_directives(self, vhost, directives, insert_at_top=False):
"""Add or replace directives in the server block identified by vhost.
This method modifies vhost to be fully consistent with the new directives.
..note :: When a directive with the same name already exists in the
config block, the first instance will be replaced. Otherwise, the directive
will be appended/prepended to the config block as in add_server_directives.
..todo :: Doesn't match server blocks whose server_name directives are
split across multiple conf files.
:param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost
whose information we use to match on
:param list directives: The directives to add
:param bool insert_at_top: True if the directives need to be inserted at the top
of the server block instead of the bottom
"""
self._modify_server_directives(vhost,
functools.partial(_update_or_add_directives, directives, insert_at_top))
def remove_server_directives(self, vhost, directive_name, match_func=None):
"""Remove all directives of type directive_name.
@@ -335,13 +354,14 @@ class NginxParser(object):
except errors.MisconfigurationError as err:
raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err)))
def duplicate_vhost(self, vhost_template, delete_default=False, only_directives=None):
def duplicate_vhost(self, vhost_template, remove_singleton_listen_params=False,
only_directives=None):
"""Duplicate the vhost in the configuration files.
:param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost
whose information we copy
:param bool delete_default: If we should remove default_server
from listen directives in the block.
:param bool remove_singleton_listen_params: If we should remove parameters
from listen directives in the block that can only be used once per address
:param list only_directives: If it exists, only duplicate the named directives. Only
looks at first level of depth; does not expand includes.
@@ -368,15 +388,21 @@ class NginxParser(object):
enclosing_block.append(raw_in_parsed)
new_vhost.path[-1] = len(enclosing_block) - 1
if delete_default:
if remove_singleton_listen_params:
for addr in new_vhost.addrs:
addr.default = False
addr.ipv6only = False
for directive in enclosing_block[new_vhost.path[-1]][1]:
if (len(directive) > 0 and directive[0] == 'listen'
and 'default_server' in directive):
del directive[directive.index('default_server')]
if len(directive) > 0 and directive[0] == 'listen':
if 'default_server' in directive:
del directive[directive.index('default_server')]
if 'default' in directive:
del directive[directive.index('default')]
if 'ipv6only=on' in directive:
del directive[directive.index('ipv6only=on')]
return new_vhost
def _parse_ssl_options(ssl_options):
if ssl_options is not None:
try:
@@ -523,32 +549,23 @@ def _is_ssl_on_directive(entry):
len(entry) == 2 and entry[0] == 'ssl' and
entry[1] == 'on')
def _add_directives(directives, replace, insert_at_top, block):
"""Adds or replaces directives in a config block.
When replace=False, it's an error to try and add a nonrepeatable directive that already
exists in the config block with a conflicting value.
When replace=True and a directive with the same name already exists in the
config block, the first instance will be replaced. Otherwise, the directive
will be added to the config block.
..todo :: Find directives that are in included files.
:param list directives: The new directives.
:param bool replace: Described above.
:param bool insert_at_top: Described above.
:param list block: The block to replace in
"""
def _add_directives(directives, insert_at_top, block):
"""Adds directives to a config block."""
for directive in directives:
_add_directive(block, directive, replace, insert_at_top)
_add_directive(block, directive, insert_at_top)
if block and '\n' not in block[-1]: # could be " \n " or ["\n"] !
block.append(nginxparser.UnspacedList('\n'))
def _update_or_add_directives(directives, insert_at_top, block):
"""Adds or replaces directives in a config block."""
for directive in directives:
_update_or_add_directive(block, directive, insert_at_top)
if block and '\n' not in block[-1]: # could be " \n " or ["\n"] !
block.append(nginxparser.UnspacedList('\n'))
INCLUDE = 'include'
REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'location', 'rewrite'])
REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'rewrite'])
COMMENT = ' managed by Certbot'
COMMENT_BLOCK = [' ', '#', COMMENT]
@@ -600,28 +617,20 @@ def _find_location(block, directive_name, match_func=None):
return next((index for index, line in enumerate(block) \
if line and line[0] == directive_name and (match_func is None or match_func(line))), None)
def _add_directive(block, directive, replace, insert_at_top):
"""Adds or replaces a single directive in a config block.
def _is_whitespace_or_comment(directive):
"""Is this directive either a whitespace or comment directive?"""
return len(directive) == 0 or directive[0] == '#'
See _add_directives for more documentation.
"""
directive = nginxparser.UnspacedList(directive)
def is_whitespace_or_comment(directive):
"""Is this directive either a whitespace or comment directive?"""
return len(directive) == 0 or directive[0] == '#'
if is_whitespace_or_comment(directive):
def _add_directive(block, directive, insert_at_top):
if not isinstance(directive, nginxparser.UnspacedList):
directive = nginxparser.UnspacedList(directive)
if _is_whitespace_or_comment(directive):
# whitespace or comment
block.append(directive)
return
location = _find_location(block, directive[0])
if replace:
if location is not None:
block[location] = directive
comment_directive(block, location)
return
# Append or prepend directive. Fail if the name is not a repeatable directive name,
# and there is already a copy of that directive with a different value
# in the config file.
@@ -646,7 +655,7 @@ def _add_directive(block, directive, replace, insert_at_top):
for included_directive in included_directives:
included_dir_loc = _find_location(block, included_directive[0])
included_dir_name = included_directive[0]
if not is_whitespace_or_comment(included_directive) \
if not _is_whitespace_or_comment(included_directive) \
and not can_append(included_dir_loc, included_dir_name):
if block[included_dir_loc] != included_directive:
raise errors.MisconfigurationError(err_fmt.format(included_directive,
@@ -667,6 +676,30 @@ def _add_directive(block, directive, replace, insert_at_top):
elif block[location] != directive:
raise errors.MisconfigurationError(err_fmt.format(directive, block[location]))
def _update_directive(block, directive, location):
block[location] = directive
comment_directive(block, location)
def _update_or_add_directive(block, directive, insert_at_top):
if not isinstance(directive, nginxparser.UnspacedList):
directive = nginxparser.UnspacedList(directive)
if _is_whitespace_or_comment(directive):
# whitespace or comment
block.append(directive)
return
location = _find_location(block, directive[0])
# we can update directive
if location is not None:
_update_directive(block, directive, location)
return
_add_directive(block, directive, insert_at_top)
def _is_certbot_comment(directive):
return '#' in directive and COMMENT in directive
def _remove_directives(directive_name, match_func, block):
"""Removes directives of name directive_name from a config block if match_func matches.
"""
@@ -674,6 +707,9 @@ def _remove_directives(directive_name, match_func, block):
location = _find_location(block, directive_name, match_func=match_func)
if location is None:
return
# if the directive was made by us, remove the comment following
if location + 1 < len(block) and _is_certbot_comment(block[location + 1]):
del block[location + 1]
del block[location]
def _apply_global_addr_ssl(addr_to_ssl, parsed_server):
@@ -707,7 +743,7 @@ def _parse_server_raw(server):
if addr.ssl:
parsed_server['ssl'] = True
elif directive[0] == 'server_name':
parsed_server['names'].update(directive[1:])
parsed_server['names'].update(x.strip('"\'') for x in directive[1:])
elif _is_ssl_on_directive(directive):
parsed_server['ssl'] = True
apply_ssl_to_all_addrs = True

View File

@@ -18,6 +18,8 @@ from certbot.tests import util as certbot_test_util
from certbot_nginx import constants
from certbot_nginx import obj
from certbot_nginx import parser
from certbot_nginx.configurator import _redirect_block_for_domain
from certbot_nginx.nginxparser import UnspacedList
from certbot_nginx.tests import util
@@ -92,7 +94,7 @@ class NginxConfiguratorTest(util.NginxTest):
"globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com"]))
def test_supported_enhancements(self):
self.assertEqual(['redirect', 'staple-ocsp'],
self.assertEqual(['redirect', 'ensure-http-header', 'staple-ocsp'],
self.config.supported_enhancements())
def test_enhance(self):
@@ -111,8 +113,7 @@ class NginxConfiguratorTest(util.NginxTest):
None, [0])
self.config.parser.add_server_directives(
mock_vhost,
[['listen', ' ', '5001', ' ', 'ssl']],
replace=False)
[['listen', ' ', '5001', ' ', 'ssl']])
self.config.save()
# pylint: disable=protected-access
@@ -126,7 +127,7 @@ class NginxConfiguratorTest(util.NginxTest):
['#', parser.COMMENT]]]],
parsed[0])
def test_choose_vhost(self):
def test_choose_vhosts(self):
localhost_conf = set(['localhost', r'~^(www\.)?(example|bar)\.'])
server_conf = set(['somename', 'another.alias', 'alias'])
example_conf = set(['.example.com', 'example.*'])
@@ -157,7 +158,7 @@ class NginxConfiguratorTest(util.NginxTest):
'69.255.225.155']
for name in results:
vhost = self.config.choose_vhost(name)
vhost = self.config.choose_vhosts(name)[0]
path = os.path.relpath(vhost.filep, self.temp_dir)
self.assertEqual(results[name], vhost.names)
@@ -171,7 +172,7 @@ class NginxConfiguratorTest(util.NginxTest):
for name in bad_results:
self.assertRaises(errors.MisconfigurationError,
self.config.choose_vhost, name)
self.config.choose_vhosts, name)
def test_ipv6only(self):
# ipv6_info: (ipv6_active, ipv6only_present)
@@ -179,6 +180,18 @@ class NginxConfiguratorTest(util.NginxTest):
# Port 443 has ipv6only=on because of ipv6ssl.com vhost
self.assertEquals((True, True), self.config.ipv6_info("443"))
def test_ipv6only_detection(self):
self.config.version = (1, 3, 1)
self.config.deploy_cert(
"ipv6.com",
"example/cert.pem",
"example/key.pem",
"example/chain.pem",
"example/fullchain.pem")
for addr in self.config.choose_vhosts("ipv6.com")[0].addrs:
self.assertFalse(addr.ipv6only)
def test_more_info(self):
self.assertTrue('nginx.conf' in self.config.more_info())
@@ -192,9 +205,9 @@ class NginxConfiguratorTest(util.NginxTest):
"example/chain.pem",
None)
@mock.patch('certbot_nginx.parser.NginxParser.add_server_directives')
def test_deploy_cert_raise_on_add_error(self, mock_add_server_directives):
mock_add_server_directives.side_effect = errors.MisconfigurationError()
@mock.patch('certbot_nginx.parser.NginxParser.update_or_add_server_directives')
def test_deploy_cert_raise_on_add_error(self, mock_update_or_add_server_directives):
mock_update_or_add_server_directives.side_effect = errors.MisconfigurationError()
self.assertRaises(
errors.PluginError,
self.config.deploy_cert,
@@ -447,7 +460,7 @@ class NginxConfiguratorTest(util.NginxTest):
def test_redirect_enhance(self):
# Test that we successfully add a redirect when there is
# a listen directive
expected = ['return', '301', 'https://$host$request_uri']
expected = UnspacedList(_redirect_block_for_domain("www.example.com"))[0]
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
self.config.enhance("www.example.com", "redirect")
@@ -460,6 +473,8 @@ class NginxConfiguratorTest(util.NginxTest):
migration_conf = self.config.parser.abs_path('sites-enabled/migration.com')
self.config.enhance("migration.com", "redirect")
expected = UnspacedList(_redirect_block_for_domain("migration.com"))[0]
generated_conf = self.config.parser.parsed[migration_conf]
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
@@ -484,100 +499,74 @@ class NginxConfiguratorTest(util.NginxTest):
['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'],
[], []]],
[['server'], [
[['if', '($host', '=', 'www.example.com)'], [
['return', '301', 'https://$host$request_uri']]],
['#', ' managed by Certbot'], [],
['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
['server_name', '.example.com'],
['server_name', 'example.*'],
['return', '301', 'https://$host$request_uri'], ['#', ' managed by Certbot'],
[], []]]],
['return', '404'], ['#', ' managed by Certbot'], [], [], []]]],
generated_conf)
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
def test_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list):
# Test that we add no redirect statement if there is already a
# redirect in the block that is managed by certbot
# Has a certbot redirect
mock_has_redirect.return_value = True
mock_contains_list.return_value = True
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
self.config.enhance("www.example.com", "redirect")
self.assertEqual(mock_logger.info.call_args[0][0],
"Traffic on port %s already redirecting to ssl in %s")
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
def test_non_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list):
# Test that we add a redirect as a comment if there is already a
# redirect-class statement in the block that isn't managed by certbot
def test_split_for_headers(self):
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
# Has a non-Certbot redirect, and has no existing comment
mock_contains_list.return_value = False
mock_has_redirect.return_value = True
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
self.config.enhance("www.example.com", "redirect")
self.assertEqual(mock_logger.info.call_args[0][0],
"The appropriate server block is already redirecting "
"traffic. To enable redirect anyway, uncomment the "
"redirect lines in %s.")
generated_conf = self.config.parser.parsed[example_conf]
expected = [
['#', ' Redirect non-https traffic to https'],
['#', ' return 301 https://$host$request_uri;'],
]
for line in expected:
self.assertTrue(util.contains_at_depth(generated_conf, line, 2))
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
def test_non_certbot_redirect_exists_has_ssl_copy(self, mock_has_redirect, mock_contains_list):
# Test that we add a redirect as a comment if there is already a
# redirect-class statement in the block that isn't managed by certbot
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
self.config.deploy_cert(
"example.org",
"example/cert.pem",
"example/key.pem",
"example/chain.pem",
"example/fullchain.pem")
self.config.enhance("www.example.com", "ensure-http-header", "Strict-Transport-Security")
generated_conf = self.config.parser.parsed[example_conf]
self.assertEqual(
[[['server'], [
['server_name', '.example.com'],
['server_name', 'example.*'], [],
['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],
['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'],
['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'],
['include', self.config.mod_ssl_conf], ['#', ' managed by Certbot'],
['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'],
[], [],
['add_header', 'Strict-Transport-Security', '"max-age=31536000"', 'always'],
['#', ' managed by Certbot'],
[], []]],
[['server'], [
['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
['server_name', '.example.com'],
['server_name', 'example.*'],
[], [], []]]],
generated_conf)
# Has a non-Certbot redirect, and has no existing comment
mock_contains_list.return_value = False
mock_has_redirect.return_value = True
def test_http_header_hsts(self):
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
self.config.enhance("www.example.com", "ensure-http-header",
"Strict-Transport-Security")
expected = ['add_header', 'Strict-Transport-Security', '"max-age=31536000"', 'always']
generated_conf = self.config.parser.parsed[example_conf]
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
def test_http_header_hsts_twice(self):
self.config.enhance("www.example.com", "ensure-http-header",
"Strict-Transport-Security")
self.assertRaises(
errors.PluginEnhancementAlreadyPresent,
self.config.enhance, "www.example.com",
"ensure-http-header", "Strict-Transport-Security")
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
def test_certbot_redirect_exists(self, mock_contains_list):
# Test that we add no redirect statement if there is already a
# redirect in the block that is managed by certbot
# Has a certbot redirect
mock_contains_list.return_value = True
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
self.config.enhance("www.example.com", "redirect")
self.assertEqual(mock_logger.info.call_args[0][0],
"The appropriate server block is already redirecting "
"traffic. To enable redirect anyway, uncomment the "
"redirect lines in %s.")
generated_conf = self.config.parser.parsed[example_conf]
expected = [
['#', ' Redirect non-https traffic to https'],
['#', ' return 301 https://$host$request_uri;'],
]
for line in expected:
self.assertTrue(util.contains_at_depth(generated_conf, line, 2))
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
@mock.patch('certbot_nginx.configurator.NginxConfigurator._has_certbot_redirect_comment')
@mock.patch('certbot_nginx.configurator.NginxConfigurator._add_redirect_block')
def test_redirect_comment_exists(self, mock_add_redirect_block,
mock_has_cb_redirect_comment, mock_has_redirect, mock_contains_list):
# Test that we add nothing if there is a non-Certbot redirect and a
# preexisting comment
# Has a non-Certbot redirect and a comment
mock_has_redirect.return_value = True
mock_contains_list.return_value = False # self._has_certbot_redirect(vhost):
mock_has_cb_redirect_comment.return_value = True
# assert _add_redirect_block not called
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
self.config.enhance("www.example.com", "redirect")
self.assertFalse(mock_add_redirect_block.called)
self.assertTrue(mock_logger.info.called)
"Traffic on port %s already redirecting to ssl in %s")
def test_redirect_dont_enhance(self):
# Test that we don't accidentally add redirect to ssl-only block
@@ -586,22 +575,18 @@ class NginxConfiguratorTest(util.NginxTest):
self.assertEqual(mock_logger.info.call_args[0][0],
'No matching insecure server blocks listening on port %s found.')
def test_no_double_redirect(self):
# Test that we don't also add the commented redirect if we've just added
# a redirect to that vhost this run
def test_double_redirect(self):
# Test that we add one redirect for each domain
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
self.config.enhance("example.com", "redirect")
self.config.enhance("example.org", "redirect")
unexpected = [
['#', ' Redirect non-https traffic to https'],
['#', ' if ($scheme != "https") {'],
['#', ' return 301 https://$host$request_uri;'],
['#', ' } # managed by Certbot']
]
expected1 = UnspacedList(_redirect_block_for_domain("example.com"))[0]
expected2 = UnspacedList(_redirect_block_for_domain("example.org"))[0]
generated_conf = self.config.parser.parsed[example_conf]
for line in unexpected:
self.assertFalse(util.contains_at_depth(generated_conf, line, 2))
self.assertTrue(util.contains_at_depth(generated_conf, expected1, 2))
self.assertTrue(util.contains_at_depth(generated_conf, expected2, 2))
def test_staple_ocsp_bad_version(self):
self.config.version = (1, 3, 1)
@@ -654,7 +639,7 @@ class NginxConfiguratorTest(util.NginxTest):
self.assertEqual([[['server'],
[['listen', 'myhost', 'default_server'],
['listen', 'otherhost', 'default_server'],
['server_name', 'www.example.org'],
['server_name', '"www.example.org"'],
[['location', '/'],
[['root', 'html'],
['index', 'index.html', 'index.htm']]]]],
@@ -763,7 +748,7 @@ class NginxConfiguratorTest(util.NginxTest):
self.config.parser.load()
expected = ['return', '301', 'https://$host$request_uri']
expected = UnspacedList(_redirect_block_for_domain("www.nomatch.com"))[0]
generated_conf = self.config.parser.parsed[default_conf]
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
@@ -776,6 +761,100 @@ class NginxConfiguratorTest(util.NginxTest):
self.config.rollback_checkpoints()
self.assertTrue(mock_parser_load.call_count == 3)
def test_choose_vhosts_wildcard(self):
# pylint: disable=protected-access
mock_path = "certbot_nginx.display_ops.select_vhost_multiple"
with mock.patch(mock_path) as mock_select_vhs:
vhost = [x for x in self.config.parser.get_vhosts()
if 'summer.com' in x.names][0]
mock_select_vhs.return_value = [vhost]
vhs = self.config._choose_vhosts_wildcard("*.com",
prefer_ssl=True)
# Check that the dialog was called with migration.com
self.assertTrue(vhost in mock_select_vhs.call_args[0][0])
# And the actual returned values
self.assertEquals(len(vhs), 1)
self.assertEqual(vhs[0], vhost)
def test_choose_vhosts_wildcard_redirect(self):
# pylint: disable=protected-access
mock_path = "certbot_nginx.display_ops.select_vhost_multiple"
with mock.patch(mock_path) as mock_select_vhs:
vhost = [x for x in self.config.parser.get_vhosts()
if 'summer.com' in x.names][0]
mock_select_vhs.return_value = [vhost]
vhs = self.config._choose_vhosts_wildcard("*.com",
prefer_ssl=False)
# Check that the dialog was called with migration.com
self.assertTrue(vhost in mock_select_vhs.call_args[0][0])
# And the actual returned values
self.assertEquals(len(vhs), 1)
self.assertEqual(vhs[0], vhost)
def test_deploy_cert_wildcard(self):
# pylint: disable=protected-access
mock_choose_vhosts = mock.MagicMock()
vhost = [x for x in self.config.parser.get_vhosts()
if 'geese.com' in x.names][0]
mock_choose_vhosts.return_value = [vhost]
self.config._choose_vhosts_wildcard = mock_choose_vhosts
mock_d = "certbot_nginx.configurator.NginxConfigurator._deploy_cert"
with mock.patch(mock_d) as mock_dep:
self.config.deploy_cert("*.com", "/tmp/path",
"/tmp/path", "/tmp/path", "/tmp/path")
self.assertTrue(mock_dep.called)
self.assertEquals(len(mock_dep.call_args_list), 1)
self.assertEqual(vhost, mock_dep.call_args_list[0][0][0])
@mock.patch("certbot_nginx.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_nginx.display_ops.select_vhost_multiple")
def test_enhance_wildcard_ocsp_after_install(self, mock_dialog):
# pylint: disable=protected-access
vhost = [x for x in self.config.parser.get_vhosts()
if 'geese.com' in x.names][0]
self.config._wildcard_vhosts["*.com"] = [vhost]
self.config.enhance("*.com", "staple-ocsp", "example/chain.pem")
self.assertFalse(mock_dialog.called)
@mock.patch("certbot_nginx.display_ops.select_vhost_multiple")
def test_enhance_wildcard_redirect_or_ocsp_no_install(self, mock_dialog):
vhost = [x for x in self.config.parser.get_vhosts()
if 'summer.com' in x.names][0]
mock_dialog.return_value = [vhost]
self.config.enhance("*.com", "staple-ocsp", "example/chain.pem")
self.assertTrue(mock_dialog.called)
@mock.patch("certbot_nginx.display_ops.select_vhost_multiple")
def test_enhance_wildcard_double_redirect(self, mock_dialog):
# pylint: disable=protected-access
vhost = [x for x in self.config.parser.get_vhosts()
if 'summer.com' in x.names][0]
self.config._wildcard_redirect_vhosts["*.com"] = [vhost]
self.config.enhance("*.com", "redirect")
self.assertFalse(mock_dialog.called)
def test_choose_vhosts_wildcard_no_ssl_filter_port(self):
# pylint: disable=protected-access
mock_path = "certbot_nginx.display_ops.select_vhost_multiple"
with mock.patch(mock_path) as mock_select_vhs:
mock_select_vhs.return_value = []
self.config._choose_vhosts_wildcard("*.com",
prefer_ssl=False,
no_ssl_filter_port='80')
# Check that the dialog was called with only port 80 vhosts
self.assertEqual(len(mock_select_vhs.call_args[0][0]), 4)
class InstallSslOptionsConfTest(util.NginxTest):
"""Test that the options-ssl-nginx.conf file is installed and updated properly."""

View File

@@ -0,0 +1,45 @@
"""Test certbot_apache.display_ops."""
import unittest
from certbot.display import util as display_util
from certbot.tests import util as certbot_util
from certbot_nginx import parser
from certbot_nginx.display_ops import select_vhost_multiple
from certbot_nginx.tests import util
class SelectVhostMultiTest(util.NginxTest):
"""Tests for certbot_nginx.display_ops.select_vhost_multiple."""
def setUp(self):
super(SelectVhostMultiTest, self).setUp()
nparser = parser.NginxParser(self.config_path)
self.vhosts = nparser.get_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)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -14,6 +14,7 @@ class AddrTest(unittest.TestCase):
self.addr5 = Addr.fromstring("myhost")
self.addr6 = Addr.fromstring("80 default_server spdy")
self.addr7 = Addr.fromstring("unix:/var/run/nginx.sock")
self.addr8 = Addr.fromstring("*:80 default ssl")
def test_fromstring(self):
self.assertEqual(self.addr1.get_addr(), "192.168.1.1")
@@ -46,6 +47,8 @@ class AddrTest(unittest.TestCase):
self.assertFalse(self.addr6.ssl)
self.assertTrue(self.addr6.default)
self.assertTrue(self.addr8.default)
self.assertEqual(None, self.addr7)
def test_str(self):
@@ -55,6 +58,7 @@ class AddrTest(unittest.TestCase):
self.assertEqual(str(self.addr4), "*:80 default_server ssl")
self.assertEqual(str(self.addr5), "myhost")
self.assertEqual(str(self.addr6), "80 default_server")
self.assertEqual(str(self.addr8), "*:80 default_server ssl")
def test_to_string(self):
self.assertEqual(self.addr1.to_string(), "192.168.1.1")
@@ -77,7 +81,8 @@ class AddrTest(unittest.TestCase):
from certbot_nginx.obj import Addr
any_addresses = ("0.0.0.0:80 default_server ssl",
"80 default_server ssl",
"*:80 default_server ssl")
"*:80 default_server ssl",
"80 default ssl")
for first, second in itertools.combinations(any_addresses, 2):
self.assertEqual(Addr.fromstring(first), Addr.fromstring(second))
@@ -143,6 +148,15 @@ class VirtualHostTest(unittest.TestCase):
"filp",
set([Addr.fromstring("localhost")]), False, False,
set(['localhost']), raw4, [])
raw_has_hsts = [
['listen', '69.50.225.155:9000'],
['server_name', 'return.com'],
['add_header', 'always', 'set', 'Strict-Transport-Security', '\"max-age=31536000\"'],
]
self.vhost_has_hsts = VirtualHost(
"filep",
set([Addr.fromstring("localhost")]), False, False,
set(['localhost']), raw_has_hsts, [])
def test_eq(self):
from certbot_nginx.obj import Addr
@@ -162,17 +176,21 @@ class VirtualHostTest(unittest.TestCase):
'enabled: False'])
self.assertEqual(stringified, str(self.vhost1))
def test_has_redirect(self):
self.assertTrue(self.vhost1.has_redirect())
self.assertTrue(self.vhost2.has_redirect())
self.assertTrue(self.vhost3.has_redirect())
self.assertFalse(self.vhost4.has_redirect())
def test_has_header(self):
self.assertTrue(self.vhost_has_hsts.has_header('Strict-Transport-Security'))
self.assertFalse(self.vhost_has_hsts.has_header('Bogus-Header'))
self.assertFalse(self.vhost1.has_header('Strict-Transport-Security'))
self.assertFalse(self.vhost1.has_header('Bogus-Header'))
def test_contains_list(self):
from certbot_nginx.obj import VirtualHost
from certbot_nginx.obj import Addr
from certbot_nginx.configurator import REDIRECT_BLOCK, _test_block_from_block
test_needle = _test_block_from_block(REDIRECT_BLOCK)
from certbot_nginx.configurator import _test_block_from_block
test_block = [
['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'],
['\n']
]
test_needle = _test_block_from_block(test_block)
test_haystack = [['listen', '80'], ['root', '/var/www/html'],
['index', 'index.html index.htm index.nginx-debian.html'],
['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],

View File

@@ -191,6 +191,31 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
['server_name', '*.www.foo.com', '*.www.example.com']]
self.assertTrue(nparser.has_ssl_on_directive(mock_vhost))
def test_remove_server_directives(self):
nparser = parser.NginxParser(self.config_path)
mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'),
None, None, None,
set(['localhost',
r'~^(www\.)?(example|bar)\.']),
None, [10, 1, 9])
example_com = nparser.abs_path('sites-enabled/example.com')
names = set(['.example.com', 'example.*'])
mock_vhost.filep = example_com
mock_vhost.names = names
mock_vhost.path = [0]
nparser.add_server_directives(mock_vhost,
[['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert2.pem']])
nparser.remove_server_directives(mock_vhost, 'foo')
nparser.remove_server_directives(mock_vhost, 'ssl_certificate')
self.assertEqual(nparser.parsed[example_com],
[[['server'], [['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
['server_name', '.example.com'],
['server_name', 'example.*'],
[]]]])
def test_add_server_directives(self):
nparser = parser.NginxParser(self.config_path)
mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'),
@@ -200,8 +225,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
None, [10, 1, 9])
nparser.add_server_directives(mock_vhost,
[['foo', 'bar'], ['\n ', 'ssl_certificate', ' ',
'/etc/ssl/cert.pem']],
replace=False)
'/etc/ssl/cert.pem']])
ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem')
dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')])
self.assertEqual(1, len(re.findall(ssl_re, dump)))
@@ -213,10 +237,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
mock_vhost.path = [0]
nparser.add_server_directives(mock_vhost,
[['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert2.pem']],
replace=False)
nparser.add_server_directives(mock_vhost, [['foo', 'bar']],
replace=False)
'/etc/ssl/cert2.pem']])
nparser.add_server_directives(mock_vhost, [['foo', 'bar']])
from certbot_nginx.parser import COMMENT
self.assertEqual(nparser.parsed[example_com],
[[['server'], [['listen', '69.50.225.155:9000'],
@@ -238,8 +260,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
nparser.add_server_directives,
mock_vhost,
[['foo', 'bar'],
['ssl_certificate', '/etc/ssl/cert2.pem']],
replace=False)
['ssl_certificate', '/etc/ssl/cert2.pem']])
def test_comment_is_repeatable(self):
nparser = parser.NginxParser(self.config_path)
@@ -249,12 +270,10 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
set(['.example.com', 'example.*']),
None, [0])
nparser.add_server_directives(mock_vhost,
[['\n ', '#', ' ', 'what a nice comment']],
replace=False)
[['\n ', '#', ' ', 'what a nice comment']])
nparser.add_server_directives(mock_vhost,
[['\n ', 'include', ' ',
nparser.abs_path('comment_in_file.conf')]],
replace=False)
nparser.abs_path('comment_in_file.conf')]])
from certbot_nginx.parser import COMMENT
self.assertEqual(nparser.parsed[example_com],
[[['server'], [['listen', '69.50.225.155:9000'],
@@ -273,8 +292,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
target = set(['.example.com', 'example.*'])
filep = nparser.abs_path('sites-enabled/example.com')
mock_vhost = obj.VirtualHost(filep, None, None, None, target, None, [0])
nparser.add_server_directives(
mock_vhost, [['server_name', 'foobar.com']], replace=True)
nparser.update_or_add_server_directives(
mock_vhost, [['server_name', 'foobar.com']])
from certbot_nginx.parser import COMMENT
self.assertEqual(
nparser.parsed[filep],
@@ -284,8 +303,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
['server_name', 'example.*'], []
]]])
mock_vhost.names = set(['foobar.com', 'example.*'])
nparser.add_server_directives(
mock_vhost, [['ssl_certificate', 'cert.pem']], replace=True)
nparser.update_or_add_server_directives(
mock_vhost, [['ssl_certificate', 'cert.pem']])
self.assertEqual(
nparser.parsed[filep],
[[['server'], [['listen', '69.50.225.155:9000'],
@@ -411,7 +430,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
vhosts = nparser.get_vhosts()
default = [x for x in vhosts if 'default' in x.filep][0]
new_vhost = nparser.duplicate_vhost(default, delete_default=True)
new_vhost = nparser.duplicate_vhost(default, remove_singleton_listen_params=True)
nparser.filedump(ext='')
# check properties of new vhost
@@ -429,6 +448,28 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
self.assertEqual(len(default.raw), len(new_vhost_parsed.raw))
self.assertTrue(next(iter(default.addrs)).super_eq(next(iter(new_vhost_parsed.addrs))))
def test_duplicate_vhost_remove_ipv6only(self):
nparser = parser.NginxParser(self.config_path)
vhosts = nparser.get_vhosts()
ipv6ssl = [x for x in vhosts if 'ipv6ssl' in x.filep][0]
new_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=True)
nparser.filedump(ext='')
for addr in new_vhost.addrs:
self.assertFalse(addr.ipv6only)
identical_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=False)
nparser.filedump(ext='')
called = False
for addr in identical_vhost.addrs:
if addr.ipv6:
self.assertTrue(addr.ipv6only)
called = True
self.assertTrue(called)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -1,7 +1,7 @@
server {
listen myhost default_server;
listen otherhost default_server;
server_name www.example.org;
server_name "www.example.org";
location / {
root html;

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