Compare commits

...

101 Commits

Author SHA1 Message Date
Adrien Ferrand
9e0f58271e Disable notifications 2019-08-29 14:13:16 +00:00
Adrien Ferrand
b1f3c6f859 Set trusty distribution 2019-08-29 15:04:52 +02:00
Adrien Ferrand
d0c6be244f Simplify Travis config. Fix docker-dev 2019-08-29 15:02:10 +02:00
Adrien Ferrand
3c572b84ab Avoid installing APT packages on OSX ... 2019-08-29 01:05:15 +02:00
Adrien Ferrand
d4f47e920b Fix cleanup 2019-08-28 21:51:22 +02:00
Adrien Ferrand
e1bae626f1 Fixing 2019-08-28 21:28:29 +02:00
Adrien Ferrand
df40057bcc Reconfigure apt install, totally remove sudo that is not used by travis anymore 2019-08-28 19:54:51 +02:00
Adrien Ferrand
95f9ed247a Update packages 2019-08-28 17:47:07 +02:00
Adrien Ferrand
3a411c092f Real path 2019-08-28 17:46:04 +02:00
Adrien Ferrand
3163801123 Merge branch 'master' into apache-integration-tests
# Conflicts:
#	certbot-ci/certbot_integration_tests/utils/certbot_call.py
2019-08-28 17:43:10 +02:00
Adrien Ferrand
a68de55696 Update coverage 2019-08-28 17:27:13 +02:00
Adrien Ferrand
4bc473a2db Finish isolation and toxenv 2019-08-28 17:16:58 +02:00
Adrien Ferrand
23ec9afdb4 Working implementation 2019-08-28 16:05:34 +02:00
Adrien Ferrand
f401750b43 Copy symlinks 2019-08-28 14:13:12 +02:00
Adrien Ferrand
3ef88a68ed Fix various paths 2019-08-28 13:03:54 +02:00
Adrien Ferrand
e6b2689dcb Fix escape characters 2019-08-28 12:56:06 +02:00
Adrien Ferrand
26aae00c36 Setup apache2.conf 2019-08-28 12:53:10 +02:00
Adrien Ferrand
0eeae8196d Continue logic 2019-08-28 12:46:18 +02:00
Adrien Ferrand
11d9107c95 Add logic 2019-08-28 12:23:24 +02:00
Adrien Ferrand
3becf7be16 Create base config 2019-08-28 12:13:25 +02:00
ohemorange
0fe28a6459 Replace platform.linux_distribution with distro.linux_distribution (#7337)
Smallest possible fix for #7106 

* Replace platform.linux_dependencies with distro.linux_dependencies

* run build.py

* Add minimum version of 1.0.1

* Pin back requests package

* Update changelog
2019-08-27 18:31:35 -07:00
Adrien Ferrand
aaeb4582e2 Fix PYTHONPATH in integration tests (#7357)
This PR supersedes #7353.

It fixes the execution of nginx oldest tests when these tests are executed on top of the modifications made in #7337. This execution failure revealed the fact that in some cases, the wrong version of certbot logic was used during integration tests (namely the logic lying in the codebase of the branch built, instead of the logic from the version of certbot declared by certbot-nginx for instance).

I let you appreciate my inline comment for the explanation and the workaround.

Thanks a lot to @bmw who found this python/pytest madness.

You can see the oldest tests succeeding with the logic of #7337 + this PR here: https://travis-ci.com/certbot/certbot/builds/124816254

* Remove certbot root from PYTHONPATH during integration tests

* Add a biiiiig comment.
2019-08-27 16:25:31 -07:00
schoen
fdb0a14812 Merge pull request #7336 from certbot/update-debian-instructions
Update Debian instructions in docs
2019-08-23 13:39:55 -07:00
Adrien Ferrand
0324d1740e Ensure relpath is executed on paths in the same drive (#7335)
On Windows you can have several drives (`C:`, `D:`, ...), that is the roughly (really roughly) equivalent of mount points, since each drive is usually associated to a specific physical partition.

So you can have paths like `C:\one\path`, `D:\another\path`.

In parallel, `os.path.relpath(path, start='.')` calculates the relative path between the given `path` and a `start` path (current directory if not provided). In recent versions of Python, `os.path.relpath` will fail if `path` and `start` are not on the same drive, because a relative path between two paths like `C:\one\path`, `D:\another\path` is not possible.

In saw unit tests failing because of this in two locations. This occurs when the certbot codebase that is tested is on a given drive (like `D:`) while the default temporary directory used by `tempfile` is on another drive (most of the time located in `C:` drive).

This PR fixes that.
2019-08-23 12:53:30 -07:00
Brad Warren
ce325db4e4 address review comments 2019-08-23 12:43:05 -07:00
Brad Warren
74e6736c79 use latest RHEL 7 AMI (#7349) 2019-08-22 09:28:57 -07:00
ohemorange
2ed7608ed3 Merge pull request #7347 from certbot/candidate-0.37.2
Release 0.37.2
2019-08-21 16:21:23 -07:00
ohemorange
eb02acfc4b Merge branch 'master' into candidate-0.37.2 2019-08-21 16:03:51 -07:00
Erica Portnoy
4f19d516d6 Bump version to 0.38.0 2019-08-21 15:23:15 -07:00
Erica Portnoy
3dd918b024 Add contents to CHANGELOG.md for next version 2019-08-21 15:23:15 -07:00
Erica Portnoy
8320018978 Release 0.37.2 2019-08-21 15:23:14 -07:00
Erica Portnoy
c17f2ff6b0 Update changelog for 0.37.2 release 2019-08-21 14:48:40 -07:00
Brad Warren
46a2ef8ba1 Stop turning session tickets off in Nginx (#7344) (#7345)
Related to #7322.

* Stop turning session tickets off in Nginx

* update changelog

(cherry picked from commit 17c1d016c1)
2019-08-21 14:44:09 -07:00
ohemorange
17c1d016c1 Stop turning session tickets off in Nginx (#7344)
Related to #7322.

* Stop turning session tickets off in Nginx

* update changelog
2019-08-21 14:29:10 -07:00
Brad Warren
70ed791709 Update Debian instructions in docs. 2019-08-16 11:42:34 -07:00
Adrien Ferrand
d39f63feca Use travis_retry for farm tests (#7327)
* Use travis_retry in travis builds to retry the farm tests

* travis_retry is a bash function, so it can be called only from current bash

* Update .travis.yml

* Update .travis.yml
2019-08-16 14:55:45 +02:00
Adrien Ferrand
6882f006ac [Windows] Fix closing files descriptors during unit tests (#7326)
* Fix file descriptor cleanup during tests on Windows

* Fix lint

* Remove useless tearDown

* Clean pylint
2019-08-16 11:08:42 +02:00
Adrien Ferrand
9a047a6996 Clean travis config (#7328)
This PR removes some useless capabilities in .travis.yml that are associated to the jobs. This concerns mainly sudo and docker.
2019-08-15 16:41:51 -07:00
Matthias Bilger
a8bd839223 Added DNS plugin for ISPConfig to list (#7332) 2019-08-15 14:43:14 -07:00
tyborr
a1aef4c15c Fix Certbot's Apache plugin doesn't work on Scientific Linux (#7294)
This PR adds OVERRIDE_CLASS in certbot-apache/entrypoint.py for Scientific Linux. Fixes #7248.

* add OVERRIDE_CLASS for Scientific Linux os name

* add entry for Scientific Linux using "scientific" as key

* Update changelog
2019-08-12 12:59:29 -07:00
ohemorange
cb7598b007 Merge pull request #7320 from certbot/merge-cand-0.37.1
Merge cand 0.37.1
2019-08-08 18:01:27 -07:00
Brad Warren
55cf49cebe Merge pull request #7318 from certbot/candidate-0.37.1
Candidate 0.37.1
2019-08-08 17:56:57 -07:00
ohemorange
933f60a3c1 Merge branch 'master' into candidate-0.37.1 2019-08-08 17:48:22 -07:00
Erica Portnoy
44eb048098 Bump version to 0.38.0 2019-08-08 17:01:39 -07:00
Erica Portnoy
794ce57356 Add contents to CHANGELOG.md for next version 2019-08-08 17:01:38 -07:00
Erica Portnoy
48d9715bd5 Release 0.37.1 2019-08-08 17:01:32 -07:00
Erica Portnoy
c5e1be4fd7 Update changelog for 0.37.1 release 2019-08-08 16:39:43 -07:00
Brad Warren
e21401004b Revert disabling TLS session tickets in Apache (#7315) (#7316)
See https://community.letsencrypt.org/t/ssl-error-after-cert-renew/99430.

The first commit of this PR is a simple, clean revert of #7191. Subsequent commits add back pieces of that PR we want to keep.

I also reverted #7299 which landed in a separate PR, but needs to be reverted to keep including the TLS config files in the certbot-apache package when it is built.

I tested this on Ubuntu 18.04 by installing a cert to Apache using Certbot master and then running certbot renew with this branch. I watched the Apache plugin update the configuration file to remove SSLSessionTickets off.

* Revert "Disable TLS session tickets for Apache 2.4.11+ (#7191)"

This reverts commit 9174c631d9.

* Keep hashes with TLS session tickets disabled.

* dont delete changelog entries

* add changelog entry

* Revert "Clean the useless entries in MANIFEST.in (#7299)"

This reverts commit f4d17d9a6b.

(cherry picked from commit 120137eb8d)
2019-08-08 16:36:45 -07:00
Brad Warren
120137eb8d Revert disabling TLS session tickets in Apache (#7315)
See https://community.letsencrypt.org/t/ssl-error-after-cert-renew/99430.

The first commit of this PR is a simple, clean revert of #7191. Subsequent commits add back pieces of that PR we want to keep.

I also reverted #7299 which landed in a separate PR, but needs to be reverted to keep including the TLS config files in the certbot-apache package when it is built.

I tested this on Ubuntu 18.04 by installing a cert to Apache using Certbot master and then running certbot renew with this branch. I watched the Apache plugin update the configuration file to remove SSLSessionTickets off.

* Revert "Disable TLS session tickets for Apache 2.4.11+ (#7191)"

This reverts commit 9174c631d9.

* Keep hashes with TLS session tickets disabled.

* dont delete changelog entries

* add changelog entry

* Revert "Clean the useless entries in MANIFEST.in (#7299)"

This reverts commit f4d17d9a6b.
2019-08-08 16:23:37 -07:00
Matt Nordhoff
2911eda3bd Update link to the Server forum category (#7309)
Let's Encrypt closed it in favor of the Help category.

https://community.letsencrypt.org/t/closing-the-server-category/93016
2019-08-08 11:44:21 -07:00
ohemorange
f1ea37dd71 Merge pull request #7311 from certbot/candidate-0.37.0
Update from 0.37.0 release
2019-08-07 17:44:16 -07:00
Brad Warren
3d3cbc0d16 Don't run tox -e cover. (#7312) 2019-08-08 00:07:37 +02:00
Brad Warren
d978440cb5 Bump version to 0.38.0 2019-08-07 10:35:13 -07:00
Brad Warren
0c04ce3c32 Add contents to CHANGELOG.md for next version 2019-08-07 10:35:13 -07:00
Brad Warren
987ce2c6b2 Release 0.37.0 2019-08-07 10:35:11 -07:00
Brad Warren
dded9290b7 Update changelog for 0.37.0 release 2019-08-07 10:26:34 -07:00
Brad Warren
745ef6e869 Merge pull request #7302 from certbot/rhel8_fix-with-tests
This PR builds off of #7240 to fix #7241.

The code in certbot-auto is unchanged which I +1. Someone else should give it a 2nd review.

For the code in the tests, you can see all tests passing (including test_tests.sh) at  https://travis-ci.com/certbot/certbot/builds/122198270.

I created #7301 to track removing the temporary code in test_leauto_upgrades.sh as suggested at #7282 (comment).

One noteworthy thing here is I did not add the RHEL 8 AMI to the Apache tests due to #7273. This problem is not related to support in certbot-auto though, is an edge case, and I do not personally believe it should block this PR.
2019-08-06 17:02:57 -07:00
Brad Warren
e2844bd0ad Add RHEL8 to test farm targets
* Add RHEL 8 to targets

* Use latest certbot-auto to bootstrap.

* Workaround leauto failures.
2019-08-06 16:39:35 -07:00
Adrien Ferrand
b67fda8832 Fix integration tests on Windows (#7271)
* Fix account_tests

* Fix hook executable test

* Remove the temporary decorator @broken_on_windows

* Fix util_test

* No broken unit test on Windows anymore

* More elegant mock

* Fix context manager

* Fix lint

* Fix mypy

* Adapt coverage

* Corrections

* Fix lint

* Adapt coverage

* Update certbot/tests/compat/filesystem_test.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update util_test.py

* Fix pylint

* Forbid os.access

* Update os_test.py

* Update os.py

* Fix lint

* Update filesystem.py

* Update filesystem.py

* Update filesystem.py

* Update os.py

* Start fixing tests

* Platform independent hooks

* Fix probe fd close

* Add broken_on_windows for integration tests

* Fix a lot of tests

* Use a python hook script, to prepare cross-platform

* New approach to be compliant with Linux and Windows on hook scripts

* New tests fixed

* Test for permissions on Windows

* Permissions comparison for Windows

* No broken tests in certbot core anymore

* Change mode

* Specific config for appveyor

* Use forked pebble for now

* Various fixes

* Assert file permissions for world on private keys

* Clean code

* Fix several things

* Add integration target

* Optimize integration env

* Re-enable all AppVeyor envs

* Use again official pebble

* Update pebble_artifacts.py

* Set PYTEST_ADDOPTS silently

* Update appveyor.yml

* Pin pywin32 for tests, give a minimal requirement for certbot.

* Remove injection of nginx in PATH

* Clean debug code

* Various cleanup, ensure to remove workspace after tests

* Update tox target

* Improve assertions. Control the keyword echoed in hooks

* Fix for virtualenv on Python 3.7.4 for Windows

* Update certbot-ci/certbot_integration_tests/certbot_tests/assertions.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Add conditionally pywin in certbot-ci like in certbot
2019-08-07 00:02:16 +02:00
Michael Watters
d6e6d64848 Update certbot-auto script to work with RHEL 8
/usr/bin/python no longer exists in RHEL 8.  This patch updates
the certbot-auto script to use python3 on nodes running RHEL 8.

Also fixed a bug in the RPM_DIST_VERSION logic which would cause
letsencrypt-auto to fail on servers running CentOS/RHEL 6.
2019-08-06 09:16:14 -04:00
Adrien Ferrand
f4d17d9a6b Clean the useless entries in MANIFEST.in (#7299)
Since #7191, TLS configuration files for Apache have been moved to a dedicated folder tls_configs. Then the entries in MANIFEST.in removed by this PR do not correspond to an existing path, and so are not useful anymore.
2019-08-05 15:57:20 -07:00
Adrien Ferrand
8bcb04af4a Move Nginx TLS configuration files into a specific folder (#7300)
Following discussions in #7298.

This PR moves the three Nginx TLS configuration files into a specific folder, tls_configs, update the MANIFEST to include this folder and its content into the certbot-nginx package, and update tests accordingly.

* Move tls configuration files in a specific folder

* Move new file
2019-08-05 15:45:08 -07:00
ohemorange
14e10f40e5 Follow Mozilla recs for Nginx ssl_protocols, ssl_ciphers, and ssl_prefer_server_ciphers (#7274)
* Follow Mozilla recs for Nginx ssl_protocols, ssl_ciphers, and ssl_prefer_server_ciphers

* Add tests and fix if statement

* Update CHANGELOG.md

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Test that the hashes of all of the current configuration files are in ALL_SSL_OPTIONS_HASHES

* Remove conditioning on OpenSSL version, since Nginx behaves cleanly if its linked OpenSSL doesn't support TLS1.3
2019-08-02 12:25:40 -07:00
Adrien Ferrand
1c7105a940 Create a mock OCSP server for Pebble integration tests (#7281)
* Implement a logic, miss the private key of pebble

* Complete process

* Fix nginx cert path

* Check conditionnally docker

* Update gitignore, fix apacheconftest

* Full object

* Carriage return

* Work in progress

* Move to official v2.1.0 of pebble

* Fix name

* Update acme_server.py

* Link things together with new version of pebble

* Plug the logic to tests

* Update config

* Reinitiate config

* Add OCSP config to pebble

* Working.

* Simplify logic

* Clean code

* Use forked pebble for now

# Conflicts:
#	certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py

* Move full logic of mock at the acme server config

* Continue work

* Finish fixing the date parsing

* Update module name

* Use again official pebble

* Activate mock OCSP server

* Clean code

* Update pebble_artifacts.py

* Remove OCSP stale test

* Add executable permissions

* Clean code

* Update setup.py

* Simplify code

* On-demand import of pebble_ocsp_server

* Revert "Remove OCSP stale test"

This reverts commit 2e4c985b427120cc15526bbcfd15806d02a6f3fc.

# Conflicts:
#	certbot-ci/certbot_integration_tests/utils/misc.py

* Fix for virtualenv on Python 3.7.4 for Windows

* Update acme_server.py
2019-08-02 11:46:12 -07:00
Adrien Ferrand
36b4c312c6 Upgrade virtualenv in dev/tests environments (#7287)
AppVeyor recently upgrade the Python 3.7.x installed in their VM to 3.7.4. However, virtualenv 16.6.1 is broken on that specific version of Python for Windows.

This PR upgrade virtualenv installed for a dev/test environment from 16.6.1 to 16.6.2 in order to fix this issue, and repair the CI jobs execute by AppVeyor on PRs.
2019-08-02 09:47:36 -07:00
Adrien Ferrand
56f609d4f5 Fix unit tests on Windows (#7270)
Fixes #6850

This PR makes the last corrections needed to run all unit tests on Windows:

add a function to check if a hook is executable in a cross-platform compatible way
handle correctly the PATH surgery for Windows during hook execution
handle correctly an account compatibility over both ACMEv1 and ACMEv2
remove (finally!) the @broken_on_windows decorator.

* Fix account_tests

* Fix hook executable test

* Remove the temporary decorator @broken_on_windows

* Fix util_test

* No broken unit test on Windows anymore

* More elegant mock

* Fix context manager

* Adapt coverage

* Corrections

* Adapt coverage

* Forbid os.access
2019-08-01 10:39:46 -07:00
Mikel Kew
2d3f3a042a Update dns-cloudflare docs regarding API Tokens (#7285)
A quick update to the docs to explicitly mention that the Cloudflare Global API Key must me used instead of an API Token.
2019-07-31 10:31:05 +02:00
Brad Warren
bfd4955bad Bump timeout waiting for ACME server to 4 minutes. (#7284)
* Bump timeout to 4 minutes.

* address review comments
2019-07-30 21:28:18 +02:00
Adrien Ferrand
9174c631d9 Disable TLS session tickets for Apache 2.4.11+ (#7191)
* Implement the logic

* Update tests

* Fix lint and changelog

* Update configurator.py

* Move the TLS configs in a dedicated folder. Fix the formalism of their naming and location.

* Improve existing test to check all TLS config have their hash registered in Certbot

* Corrections after review

* Improve a test

* Remove commented useless lines in TLS configs

* Add a nice warning. Because I am nice.

* Fix lint

* Add a test
2019-07-29 22:54:51 +03:00
Adrien Ferrand
81e0b92b43 Refer to ubuntu in install.rst (#6986)
Fixes #5758
2019-07-29 10:27:09 -07:00
Brad Warren
d3da19919f Remove duplicate, failing oldest tests. (#7272)
Nightly tests failed last night at https://travis-ci.com/certbot/certbot/builds/120816454.

The cause was the oldest the version of Ubuntu used in the tests suddenly changed from Trusty to Xenial. You can see Xenial being used in the failing test at  https://travis-ci.com/certbot/certbot/jobs/219873088#L9 and Trusty being used at the last passing test at https://travis-ci.com/certbot/certbot/jobs/218936290#L9. The change in the default doesn't seem to be documented (yet) at https://docs.travis-ci.com/user/reference/overview/.

I started to pin Trusty in these tests, however, I noticed that we are running these same unit tests at e6bf3fe7f8/.travis.yml (L58). These other tests are still succeeding because it appears that including `sudo: required` causes Travis to still default to Trusty.

Deleting these duplicated tests fixes our Travis failures and speeds things up ever so slightly.

* Remove duplicate, failing oldest tests.

* pin trusty
2019-07-26 13:37:16 -07:00
Adrien Ferrand
e6bf3fe7f8 [Windows] Security model for files permissions - STEP 3f (#7233)
* Correct file permissions on TempHandler

* Forbid os.chown and os.geteuid, as theses functions can be harmful to the security model on Windows.

* Implement copy_ownership

* Apply copy_ownership

* Correct webroot tests (and activate another broken test !)

* Correct lint and mypy

* Ensure to apply mode in makedirs

* Apply strict permissions on directories created with tempfile.mkdtemp(), like on Unix.

* Ensure streamHandler has 0600 on Windows

* Reactivate a test on windows

* Pin oldest requirements to current internal libraries (acme and certbot)

* Add dynamically pywin32 in dependencies: always except for certbot-oldest to avoid to break the relevant tests.

* Administrative privileges are always required.

* Correct security implementation (not the logic yet)

* First correction. Allow to manipulate finely file permissions during their generation

* Align to master + fix lint + resolve correctly symbolic links

* Add a test for windows about default paths

* Strenghthen the detection of Linux/Windows to check the standard files layout.

* Fix lint and mypy

* Reflect non usage of cache discovery from dns google plugin to its tests, solving Windows tests on the way

* Apply suggestions from code review

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Add more details in a comment

* Retrigger build.

* Add documentation.

* Fix a test

* Correct RW clear down

* Update util.py

* Remove unused code

* Fix code style

* Adapt certbot coverage threshold on Linux due to Windows specific LOC addition.

* Various optimizations around file owner and file mode

* Fix last error

* Fix copy_ownership_and_apply_mode

* Fix lint

* Correct mypy

* Extract out first part from windows-file-permissions

* Ignore new_compat in coverage for now

* Create test package for compat

* Add unit tests for security module.

* Add pywin32

* Adapt linux coverages to the windows-specific LOCs added

* Clean imports

* Correct import

* Trigger CI

* Reactivate a test

* Create the certbot.compat package. Move logic in certbot.compat.misc

* Clean comment

* Add doc

* Fix lint

* Correct mypy

* Add executable permissions

* Add the delegate certbot.compat.os module, add check coding style to enforce usage of certbot.compat.os instead of standard os

* Load certbot.compat.os instead of os

* Move existing compat test

* Update local oldest requirements

* Import sys

* Fix some mocks

* Update account_test.py

* Update os.py

* Update os.py

* Update local oldest requirements

* Implement the new linter_plugin

* Fix remaining linting errors

* Fix local oldest for nginx

* Remove custom check in favor of pylint plugin

* Remove check coding style

* Update linter_plugin.py

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Add several comments

* Update the setup.py

* Add documentation

* Update acme dependencies

* Update certbot/compat/os.py

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update certbot/compat/os.py

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update certbot/compat/os.py

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update docs/contributing.rst

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update linter_plugin.py

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update linter_plugin.py

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update docs/contributing.rst

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update docs/contributing.rst

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Corrections

* Handle os.path. Simplify checker.

* Add a comment to a reference implementation

* Update changelog

* Fix module registering

* Update docs/contributing.rst

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update docs/contributing.rst

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update docs/contributing.rst

Co-Authored-By: adferrand <adferrand@users.noreply.github.com>

* Update config and changelog

* Correction

* Correct os

* Fix merge

* Disable pylint checks

* Normalize imports

* Simplify security

* Corrections

* Reorganize module

* Clean code

* Clean code

* Remove coverage

* No cover

* Implement security.chmod

* Disable a test for now

* Disable hard error for now

* Add a first test. Remove unused import

* Recalibrate coverage

* Modifications for misc

* Correct function call

* Add some types

* Remove newline

* Use os_rename

* Implement security.open

* Revert to windows-files-permissions approach

* Fix lint

* Implement security.mkdir and security.makedirs

* Fix lint

* Clean lint

* Clean lint

* Revert "Clean lint"

This reverts commit 83bf81960ac6bf3f76c286ca065a5ac850c6870b.

* Correct mock

* Conditionally add pywin32 on setuptools versions that support environment markers.

* Fix separator

* Fix separator

* Rename security into filesystem

* Change module security to filesystem

* Move rename into filesystem

* Rename security into filesystem

* Rename security into filesystem

* Rerun CI

* Fix import

* Fix pylint

* Implement copy_ownership_and_apply_mode

* Fix pylint

* Update certbot/compat/os.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Remove default values

* Rewrite a comment.

* Relaunch CI

* Pass as keyword arguments

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Make the private key permissions transfer platform specific

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Rename variable

* Fix comment0

* Add unit test for copy_ownership_and_apply_mode

* Adapt coverage

* Implement new methods.

* Remove the old method

* Reimplement make_or_verify_dir

* Finish migration

* Start to fix tests

* Fix ownership when creating a file with filesystem.open

* Fix security on TempHandler

* Fix validation path permissions

* Fix owner on mkdir

* Use a proper workdir for crypto tests

* Fix pylint

* Adapt coverage

* Update storage_test.py

* Update util_test.py

* Clean code

* Update certbot/compat/filesystem.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Add comment

* Update certbot/compat/filesystem.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Check permissions

* Change test mode

* Add unit test for filesystem.check_* functions

* Update filesystem_test.py

* Better logic for TempHandler

* Adapt coverage
2019-07-26 00:25:36 +02:00
alexzorin
40da709792 docs: s/certbot_tests/certbot_test/ (#7267) 2019-07-25 10:23:28 +02:00
Brad Warren
bf9c681c4f fix backwards logic (#7265) 2019-07-25 10:20:52 +02:00
alexzorin
391f301dd8 acme: Implement authz deactivation (#7254)
Resolves #4945. First PR in order to address #5116.

* acme: Implement authz deactivation

Resolves #4945

* update AUTHORS and CHANGELOG

* typos in mypy annotations

* formatting: missing newline

* improve test_deactivate_authorization

* improve deactivate_authorization

* test: s/STATUS_INVALID/STATUS_DEACTIVATED/

* simplify dict to keyword argument

* acme: add UpdateAuthorization

* acme: use UpdateAuthorization in deactivate_authz

and add mypy annotation

This allows deactivate_authorization to succeed for both ACME v1
and v2 servers.
2019-07-24 18:04:59 -07:00
Brad Warren
06a0dae67f Fix test_symlink_resolution on macOS. (#7263)
This fixes the test failures which can be seen at
https://travis-ci.com/certbot/certbot/builds/120123338.

The problem here is the path returned by tempfile.mkdtemp() contains a symlink.
For instance, one run of the function produced
'/var/folders/3b/zg8fdh5j71x92yyzc1tyllfw0000gp/T/tmp3k9ytfj1' which is a
symlink to
'/private/var/folders/3b/zg8fdh5j71x92yyzc1tyllfw0000gp/T/tmp3k9ytfj1'.

Removing this symlink before testing filesystem.realpath solves the problem.

You can see the macOS tests passing with this change at https://travis-ci.com/certbot/certbot/builds/120250667.
2019-07-23 11:01:29 -07:00
Adrien Ferrand
a35470292e Remove Dockerfiles (#7257) 2019-07-22 13:43:58 +03:00
Brad Warren
47f64c7280 Remove list of packaging efforts. (#7258)
I think this list maybe had value when distros were first starting to package Certbot, but now I don't think it does. What function does this list serve? The instruction generator at https://certbot.eff.org/instructions does a much better job telling users how to use these packages. On the packaging side, I think anyone capable of packaging Certbot at the various distros would be able to search their repositories to see if a Certbot package is available.

Since this list is hard to maintain as links semi-regularly break and keeping it up to date with all distros and all Certbot components is a fair bit of work, let's just remove it.

This PR was motivated by the Travis failures at https://travis-ci.com/certbot/website/builds/119588518 due to GNU Guix changing the layout of their site.
2019-07-19 10:44:17 -07:00
Brad Warren
f7c736da6f Update pexpect to fix Python 3.7 dev venvs. (#7259) 2019-07-18 15:44:01 -07:00
Adrien Ferrand
71ff47daad Implement a consistent realpath function in certbot.compat.filesystem (#7242)
Fixes #7115 

This PR creates a `realpath` method in `filesystem`, whose goal is to replace any call to `os.path.realpath` in Certbot. The reason is that `os.path.realpath` is broken on some versions of Python for Windows. See https://bugs.python.org/issue9949. The function created here works consistently across Linux and Windows.

As for the other forbidden functions in `os` module, our `certbot.compat.os` will raise an exception if its `path.realpath` function is invoked, and using the `os` module from Python is forbidden from the pylint check implemented in our CI.

Every call to `os.path.realpath` is corrected in `certbot` and `certbot-apache` modules.

* Forbid os.path.realpath

* Finish implementation

* Use filesystem.realpath

* Control symlink loops also for Linux

* Add a test for forbidden method

* Import a new object from os.path module

* Use same approach of wrapping than certbot.compat.os

* Correct errors

* Fix dependencies

* Make path module internal
2019-07-18 14:31:39 -07:00
J0WI
41a17f913e Use Buster as base image (#7251) 2019-07-17 13:05:02 -07:00
Po-Chuan Hsieh
750d6a9686 Unify license filename (LICENSE.txt) (#7239)
* Unify license filename (LICENSE.txt)
2019-07-12 22:53:43 +03:00
Adrien Ferrand
c4684f187a Add a test for the default directories on Windows (#7238)
There is a unit test to check that the default directories for Certbot are not diverging, in certbot.tests.cli_test:FlagDefaultTests:test_linux_directories.

But this test is not done on Windows.

This PR fixes that.
2019-07-11 17:49:52 -07:00
Lucid One
82ad736120 Fixes #7220 to allow config to be loaded from <(envsubst < template) (#7221)
* Fixes #7220 to allow config to be loaded from <(envsubst < template)
2019-07-11 14:40:24 -07:00
Brad Warren
ca893bd836 Merge pull request #7236 from certbot/candidate-0.36.0
Release 0.36.0
2019-07-11 14:00:49 -07:00
Erica Portnoy
d1934e36fe Bump version to 0.37.0 2019-07-11 12:31:53 -07:00
Erica Portnoy
15b1d8e5a7 Add contents to CHANGELOG.md for next version 2019-07-11 12:31:53 -07:00
Erica Portnoy
cbd0a37c7a Release 0.36.0 2019-07-11 12:31:51 -07:00
Erica Portnoy
13c44a0595 Update changelog for 0.36.0 release 2019-07-11 12:12:24 -07:00
Brad Warren
89f52ca9f9 Add mypy to contributing checklist. (#7224) 2019-07-10 18:14:12 -07:00
Brad Warren
d0a9695b09 Make PR template a checklist and suggest mypy. (#7223) 2019-07-10 18:14:01 -07:00
Brad Warren
add24d4861 Run tests on apache-parser-v2 (#7231)
We're planning on using the branch apache-parser-v2 allowing us to incrementally work on the new Apache parser and feel comfortable landing temporary test code that we don't really want in master.

The apache-parser-v2 branch is created and locked down, but neither Travis or AppVeyor are configured to run tests on it. See #7230. This PR fixes that problem.

This could probably just land in the apache-parser-v2 branch, but why unnecessarily deviate the branch from master? It doesn't hurt anything there. Once it lands, I'll get this added to the apache-parser-v2 branch too.

* Run tests on apache-parser-v2.

* add comment

* Don't run full test suite on apache-parser-v2.
2019-07-10 16:30:06 -07:00
Adrien Ferrand
74292a10f5 [Windows] Security model for files permissions - STEP 3e (#7182)
This PR implements the filesystem.copy_ownership_and_apply_mode method from #6497.

This method is used in two places in Certbot, replacing os.chown, to copy the owner and group owner from a file to another one, and apply to the latter the given POSIX mode.

* Implement copy_ownership_and_apply_mode

* Update certbot/compat/os.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Remove default values

* Rewrite a comment.

* Relaunch CI

* Pass as keyword arguments

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Make the private key permissions transfer platform specific

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Rename variable

* Fix comment0

* Add unit test for copy_ownership_and_apply_mode

* Adapt coverage

* Execute unconditionally chmod with copy_ownership_and_apply_mode. Improve doc.
2019-07-10 16:26:30 -07:00
Brad Warren
74bf9ef46a Remove test symlink. (#7232) 2019-07-10 23:48:34 +02:00
Adrien Ferrand
2ac99fefe0 [Windows|Linux] Launch integration tests on Pebble without Docker (#7157)
This PR is a part of the actions necessary to make Certbot-CI work on Windows, in order to execute the integration tests on this platform.

Following #7156, this PR changes how the integration tests are setup against Pebble to not need Docker anymore.

As a reminder, one can check #7156 and letsencrypt/pebble#240 to see the rationale about why using Docker is a problem to run the integration tests on Windows.

Basically, this PR executes directly Pebble using its executable, since it is build using Go, and Go produces self-contained executable that can run without any installation on Linux and on Windows. During the integration tests setup, Certbot-CI will get the Pebble (and Challtestsrv) executables for the defined target version on the GitHub releases. The binaries are persisted on the filesystem, so it is not needed to download them again on the second integration tests execution. Nonetheless, we are talking about 20MB of executables.

Since the setup needs to hold a state, I also took this occasion to refactor the acme_server, in order to use on object oriented approach and improve the readability/maintainability.

Once this PR and #7156 are merged, Docker will not be needed anymore for the main integration tests usecase, that is to use Pebble.

* Complete process

* Fix nginx cert path

* Check conditionnally docker

* Update gitignore, fix apacheconftest

* Full object

* Carriage return

* Move to official v2.1.0 of pebble

* Fix name

* Update acme_server.py

* Relaunch CI

* Update certbot-ci/certbot_integration_tests/utils/acme_server.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update certbot-ci/certbot_integration_tests/utils/acme_server.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>

* Update docstring

* Update documentation

* Configure a stdout to ACMEServer

* Map all process through defined stdout

* Remove unused variable

* Handle using signals

* Use failsafe entering context

* Remove failsafe rmtree, that is not needed anymore
2019-07-10 14:29:57 -07:00
Brad Warren
43f58ca803 Document pytest packaging problems. (#7226)
This is probably unlikely to come up again, but this documents that people should run our tests using setuptools rather than calling something like pytest directly. See https://opensource.eff.org/eff-open-source/pl/wdrky4uyzjguppgch3r7t7qjmc for more info.
2019-07-09 15:07:33 -07:00
Brad Warren
17f2cabbbf Replace broken link with archive link. (#7222) 2019-07-08 10:27:25 -07:00
Adrien Ferrand
7d61e9ea56 [Windows] Security model for files permissions - STEP 3d (#6968)
* Implement security.mkdir and security.makedirs

* Fix lint

* Correct mock

* Rename security into filesystem

* Update apache and nginx plugins requirements

* Update certbot/plugins/webroot.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Reenable pylint here

* Move code

* Reimplement mkdir

* Control errors on eexist, remove superfluous chmod for makedirs

* Add proper skip for windows only tests

* Fix lint

* Fix mypy

* Clean code

* Adapt coverage threshold on Linux with addition of LOC specific to Windows

* Add forbiden functions to tests

* Update certbot/compat/os.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Simplify code

* Sync _get_current_user with part3c

* Use the simpliest implementation

* Remove exist_ok, simplify code.

* Simplify inline comment

* Update filesystem_test.py

* Update certbot/compat/os.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/plugins/webroot.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/plugins/webroot.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Add a test to check we set back os.mkdir correctly after filesystem.makedirs is called.

* Fix lint, adapt coverage
2019-07-03 16:20:43 -07:00
Brad Warren
20b595bc9e Simplify and deprecate viewing config changes (#7198)
* Remove apache and nginx from config_changes help

* Deprecate certbot_config changes.

* Document config_changes deprecation.

* Remove view_config_changes as IInstaller method.

* Remove view_config_changes from plugins.

* Add view_config_changes warnings.

* simplify test_config_changes_deprecation
2019-07-02 17:20:12 -07:00
Adrien Ferrand
88876b9901 [Windows] Security model for files permissions - STEP 3c (#6967)
* Implement security.open

* Clean lint

* Rename security into filesystem

* Update certbot/compat/filesystem.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/util.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/lock.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/compat/os.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/lock.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/compat/os.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Simplify and make more clear comment on os.open.

* Secure implementation preventing race conditions

* Revert "Secure implementation preventing race conditions"

This reverts commit dbb85492195122020ca0b4a685ddb4836fdc6d12.

* Simplify the logic on Windows.

* Implement os.open to prevent race conditions

* Add unit tests

* Handle os.O_CREAT and os.O_EXCL directly from the Windows APIs

* Improve comments

* Use CREATE_ALWAYS

* Adapt coverage threshold to new Windows specific LOCs.

* Update certbot/compat/os.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/compat/os.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/compat/os.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/compat/filesystem.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Add some comments

* Fix pylint

* Improve docstring

* Added test cases

* Improve docstring

* Update certbot/lock.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Update certbot/lock.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>

* Fix lint

* Adapt coverage

* Adapt coverage
2019-07-02 16:21:24 -07:00
Brad Warren
448d159223 Install Python3 only dev tools with tools/venv3.py (#7215)
These packages can be useful and I found that they aren't being installed in our Python 3 development environment. Let's fix that.
2019-07-02 13:45:57 -07:00
165 changed files with 2688 additions and 1450 deletions

View File

@@ -6,13 +6,13 @@ coverage:
flags: linux flags: linux
# Fixed target instead of auto set by #7173, can # Fixed target instead of auto set by #7173, can
# be removed when flags in Codecov are added back. # be removed when flags in Codecov are added back.
target: 98.0 target: 97.5
threshold: 0.1 threshold: 0.1
base: auto base: auto
windows: windows:
flags: windows flags: windows
# Fixed target instead of auto set by #7173, can # Fixed target instead of auto set by #7173, can
# be removed when flags in Codecov are added back. # be removed when flags in Codecov are added back.
target: 96.9 target: 97.6
threshold: 0.1 threshold: 0.1
base: auto base: auto

2
.gitignore vendored
View File

@@ -47,3 +47,5 @@ tests/letstest/venv/
# certbot tests # certbot tests
.certbot_test_workspace .certbot_test_workspace
**/assets/pebble*
**/assets/challtestsrv*

View File

@@ -5,9 +5,21 @@ cache:
- $HOME/.cache/pip - $HOME/.cache/pip
before_script: before_script:
# Install required apt packages
- |
if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then
./certbot-auto --non-interactive --os-packages-only
sudo -E apt-get -yq --no-install-suggests --no-install-recommends install nginx-light
sudo -E /etc/init.d/nginx stop
sudo -E apt-get -yq --no-install-suggests --no-install-recommends install apache2
sudo -E /etc/init.d/apache2 stop
sudo -E chmod 777 -R /var/lib/apache2/module
fi
- 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ulimit -n 1024 ; fi' - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ulimit -n 1024 ; fi'
# On Travis, the fastest parallelization for integration tests has proved to be 4. # On Travis, the fastest parallelization for integration tests has proved to be 4.
- 'if [[ "$TOXENV" == *"integration"* ]]; then export PYTEST_ADDOPTS="--numprocesses 4"; fi' - 'if [[ "$TOXENV" == *"integration"* ]]; then export PYTEST_ADDOPTS="--numprocesses 4"; fi'
# Use Travis retry feature for farm tests since they are flaky
- 'if [[ "$TOXENV" == "travis-test-farm"* ]]; then export TRAVIS_RETRY=travis_retry; fi'
- export TOX_TESTENV_PASSENV=TRAVIS - export TOX_TESTENV_PASSENV=TRAVIS
# Only build pushes to the master branch, PRs, and branches beginning with # Only build pushes to the master branch, PRs, and branches beginning with
@@ -16,6 +28,9 @@ before_script:
# is a cap of on the number of simultaneous runs. # is a cap of on the number of simultaneous runs.
branches: branches:
only: only:
# apache-parser-v2 is a temporary branch for doing work related to
# rewriting the parser in the Apache plugin.
- apache-parser-v2
- master - master
- /^\d+\.\d+\.x$/ - /^\d+\.\d+\.x$/
- /^test-.*$/ - /^test-.*$/
@@ -24,17 +39,16 @@ branches:
not-on-master: &not-on-master not-on-master: &not-on-master
if: NOT (type = push AND branch = master) if: NOT (type = push AND branch = master)
# Jobs for the extended test suite are executed for cron jobs and pushes on non-master branches. # Jobs for the extended test suite are executed for cron jobs and pushes to
# non-development branches. See the explanation for apache-parser-v2 above.
extended-test-suite: &extended-test-suite extended-test-suite: &extended-test-suite
if: type = cron OR (type = push AND branch != master) if: type = cron OR (type = push AND branch NOT IN (apache-parser-v2, master))
matrix: matrix:
include: include:
# Main test suite # Main test suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=pebble TOXENV=integration env: ACME_SERVER=pebble TOXENV=integration
sudo: required
services: docker
<<: *not-on-master <<: *not-on-master
# This job is always executed, including on master # This job is always executed, including on master
@@ -51,46 +65,31 @@ matrix:
env: TOXENV=mypy env: TOXENV=mypy
<<: *not-on-master <<: *not-on-master
- python: "2.7" - python: "2.7"
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest' env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
sudo: required
services: docker
<<: *not-on-master <<: *not-on-master
- python: "3.4" - python: "3.4"
env: TOXENV=py34 env: TOXENV=py34
sudo: required
services: docker
<<: *not-on-master <<: *not-on-master
- python: "3.7" - python: "3.7"
dist: xenial
env: TOXENV=py37 env: TOXENV=py37
sudo: required
services: docker
<<: *not-on-master <<: *not-on-master
- sudo: required - env: TOXENV=apache_compat
env: TOXENV=apache_compat
services: docker
before_install:
addons:
<<: *not-on-master <<: *not-on-master
- sudo: required - env: TOXENV=le_auto_xenial
env: TOXENV=le_auto_xenial
services: docker
<<: *not-on-master <<: *not-on-master
- python: "2.7" - python: "2.7"
env: TOXENV=apacheconftest-with-pebble env: TOXENV=apacheconftest-with-pebble
sudo: required
services: docker
<<: *not-on-master <<: *not-on-master
- python: "2.7" - python: "2.7"
env: TOXENV=nginxroundtrip env: TOXENV=nginxroundtrip
<<: *not-on-master <<: *not-on-master
# Extended test suite on cron jobs and pushes to tested branches other than master # Extended test suite on cron jobs and pushes to tested branches other than master
- sudo: required - env: TOXENV=nginx_compat
env: TOXENV=nginx_compat
services: docker
before_install:
addons:
<<: *extended-test-suite <<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: env:
@@ -115,44 +114,29 @@ matrix:
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw=" - secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.7" - python: "3.7"
dist: xenial
env: TOXENV=py37 CERTBOT_NO_PIN=1 env: TOXENV=py37 CERTBOT_NO_PIN=1
<<: *extended-test-suite <<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: TOXENV=py27-certbot-oldest
<<: *extended-test-suite
- python: "2.7"
env: TOXENV=py27-nginx-oldest
<<: *extended-test-suite <<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration-certbot-oldest env: ACME_SERVER=boulder-v1 TOXENV=integration-certbot-oldest
sudo: required dist: trusty # See py27-{acme,apache,certbot,dns,nginx}-oldest tests
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration-certbot-oldest env: ACME_SERVER=boulder-v2 TOXENV=integration-certbot-oldest
sudo: required dist: trusty # See py27-{acme,apache,certbot,dns,nginx}-oldest tests
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration-nginx-oldest env: ACME_SERVER=boulder-v1 TOXENV=integration-nginx-oldest
sudo: required dist: trusty # See py27-{acme,apache,certbot,dns,nginx}-oldest tests
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration-nginx-oldest env: ACME_SERVER=boulder-v2 TOXENV=integration-nginx-oldest
sudo: required dist: trusty # See py27-{acme,apache,certbot,dns,nginx}-oldest tests
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.4" - python: "3.4"
env: TOXENV=py34 env: TOXENV=py34
@@ -164,66 +148,37 @@ matrix:
env: TOXENV=py36 env: TOXENV=py36
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.7" - python: "3.7"
dist: xenial
env: TOXENV=py37 env: TOXENV=py37
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.4" - python: "3.4"
env: ACME_SERVER=boulder-v1 TOXENV=integration env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.4" - python: "3.4"
env: ACME_SERVER=boulder-v2 TOXENV=integration env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.5" - python: "3.5"
env: ACME_SERVER=boulder-v1 TOXENV=integration env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.5" - python: "3.5"
env: ACME_SERVER=boulder-v2 TOXENV=integration env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.6" - python: "3.6"
env: ACME_SERVER=boulder-v1 TOXENV=integration env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.6" - python: "3.6"
env: ACME_SERVER=boulder-v2 TOXENV=integration env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.7" - python: "3.7"
dist: xenial
env: ACME_SERVER=boulder-v1 TOXENV=integration env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.7" - python: "3.7"
dist: xenial
env: ACME_SERVER=boulder-v2 TOXENV=integration env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- sudo: required - env: TOXENV=le_auto_jessie
env: TOXENV=le_auto_jessie
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- sudo: required - env: TOXENV=le_auto_centos6
env: TOXENV=le_auto_centos6
services: docker
<<: *extended-test-suite <<: *extended-test-suite
- sudo: required - env: TOXENV=docker_dev
env: TOXENV=docker_dev
services: docker
addons:
apt:
packages: # don't install nginx and apache
- libaugeas0
<<: *extended-test-suite <<: *extended-test-suite
- language: generic - language: generic
env: TOXENV=py27 env: TOXENV=py27
@@ -250,41 +205,29 @@ matrix:
- python3 - python3
<<: *extended-test-suite <<: *extended-test-suite
# container-based infrastructure
sudo: false
addons:
apt:
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
- python-dev
- gcc
- libaugeas0
- libssl-dev
- libffi-dev
- ca-certificates
# For certbot-nginx integration testing
- nginx-light
- openssl
# tools/pip_install.py is used to pin packages to a known working version # tools/pip_install.py is used to pin packages to a known working version
# except in tests where the environment variable CERTBOT_NO_PIN is set. # except in tests where the environment variable CERTBOT_NO_PIN is set.
# virtualenv is listed here explicitly to make sure it is upgraded when # virtualenv is listed here explicitly to make sure it is upgraded when
# CERTBOT_NO_PIN is set to work around failures we've seen when using an older # CERTBOT_NO_PIN is set to work around failures we've seen when using an older
# version of virtualenv. # version of virtualenv.
install: "tools/pip_install.py -U codecov tox virtualenv" install: 'tools/pip_install.py -U codecov tox virtualenv'
script: tox # Most of the time TRAVIS_RETRY is an empty string, and has no effect on the
# script command. It is set only to `travis_retry` during farm tests, in
# order to trigger the Travis retry feature, and compensate the inherent
# flakiness of these specific tests.
script: '$TRAVIS_RETRY tox'
after_success: '[ "$TOXENV" == "py27-cover" ] && codecov -F linux' after_success: '[ "$TOXENV" == "py27-cover" ] && codecov -F linux'
notifications: #notifications:
email: false # email: false
irc: # irc:
channels: # channels:
# This is set to a secure variable to prevent forks from sending # # This is set to a secure variable to prevent forks from sending
# notifications. This value was created by installing # # notifications. This value was created by installing
# https://github.com/travis-ci/travis.rb and running # # https://github.com/travis-ci/travis.rb and running
# `travis encrypt "chat.freenode.net#certbot-devel"`. # # `travis encrypt "chat.freenode.net#certbot-devel"`.
- secure: "EWW66E2+KVPZyIPR8ViENZwfcup4Gx3/dlimmAZE0WuLwxDCshBBOd3O8Rf6pBokEoZlXM5eDT6XdyJj8n0DLslgjO62pExdunXpbcMwdY7l1ELxX2/UbnDTE6UnPYa09qVBHNG7156Z6yE0x2lH4M9Ykvp0G0cubjPQHylAwo0=" # - secure: "EWW66E2+KVPZyIPR8ViENZwfcup4Gx3/dlimmAZE0WuLwxDCshBBOd3O8Rf6pBokEoZlXM5eDT6XdyJj8n0DLslgjO62pExdunXpbcMwdY7l1ELxX2/UbnDTE6UnPYa09qVBHNG7156Z6yE0x2lH4M9Ykvp0G0cubjPQHylAwo0="
on_cancel: never # on_cancel: never
on_success: never # on_success: never
on_failure: always # on_failure: always

View File

@@ -15,6 +15,7 @@ Authors
* [Alex Gaynor](https://github.com/alex) * [Alex Gaynor](https://github.com/alex)
* [Alex Halderman](https://github.com/jhalderm) * [Alex Halderman](https://github.com/jhalderm)
* [Alex Jordan](https://github.com/strugee) * [Alex Jordan](https://github.com/strugee)
* [Alex Zorin](https://github.com/alexzorin)
* [Amjad Mashaal](https://github.com/TheNavigat) * [Amjad Mashaal](https://github.com/TheNavigat)
* [Andrew Murray](https://github.com/radarhere) * [Andrew Murray](https://github.com/radarhere)
* [Anselm Levskaya](https://github.com/levskaya) * [Anselm Levskaya](https://github.com/levskaya)
@@ -161,6 +162,7 @@ Authors
* [Michael Schumacher](https://github.com/schumaml) * [Michael Schumacher](https://github.com/schumaml)
* [Michael Strache](https://github.com/Jarodiv) * [Michael Strache](https://github.com/Jarodiv)
* [Michael Sverdlin](https://github.com/sveder) * [Michael Sverdlin](https://github.com/sveder)
* [Michael Watters](https://github.com/blackknight36)
* [Michal Moravec](https://github.com/https://github.com/Majkl578) * [Michal Moravec](https://github.com/https://github.com/Majkl578)
* [Michal Papis](https://github.com/mpapis) * [Michal Papis](https://github.com/mpapis)
* [Minn Soe](https://github.com/MinnSoe) * [Minn Soe](https://github.com/MinnSoe)

View File

@@ -2,7 +2,61 @@
Certbot adheres to [Semantic Versioning](https://semver.org/). Certbot adheres to [Semantic Versioning](https://semver.org/).
## 0.36.0 - master ## 0.38.0 - master
### Added
*
### Changed
* If Certbot fails to rollback your server configuration, the error message
links to the Let's Encrypt forum. Change the link to the Help category now
that the Server category has been closed.
* Replace platform.linux_distribution with distro.linux_distribution as a step
towards Python 3.8 support in Certbot.
### Fixed
* Fixed OS detection in the Apache plugin on Scientific Linux.
More details about these changes can be found on our GitHub repo.
## 0.37.2 - 2019-08-21
* Stop disabling TLS session tickets in Nginx as it caused TLS failures on
some systems.
More details about these changes can be found on our GitHub repo.
## 0.37.1 - 2019-08-08
### Fixed
* Stop disabling TLS session tickets in Apache as it caused TLS failures on
some systems.
More details about these changes can be found on our GitHub repo.
## 0.37.0 - 2019-08-07
### Added
* Turn off session tickets for apache plugin by default
* acme: Authz deactivation added to `acme` module.
### Changed
* Follow updated Mozilla recommendations for Nginx ssl_protocols, ssl_ciphers,
and ssl_prefer_server_ciphers
### Fixed
* Fix certbot-auto failures on RHEL 8.
More details about these changes can be found on our GitHub repo.
## 0.36.0 - 2019-07-11
### Added ### Added
@@ -15,7 +69,12 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
* Update the 'manage your account' help to be more generic. * Update the 'manage your account' help to be more generic.
* The error message when Certbot's Apache plugin is unable to modify your * The error message when Certbot's Apache plugin is unable to modify your
Apache configuration has been improved. Apache configuration has been improved.
* Certbot's config_changes subcommand has been deprecated and will be
removed in a future release.
* `certbot config_changes` no longer accepts a --num parameter. * `certbot config_changes` no longer accepts a --num parameter.
* The functions `certbot.plugins.common.Installer.view_config_changes` and
`certbot.reverter.Reverter.view_config_changes` have been deprecated and will
be removed in a future release.
### Fixed ### Fixed

View File

@@ -1,35 +0,0 @@
FROM python:2-alpine3.9
ENTRYPOINT [ "certbot" ]
EXPOSE 80 443
VOLUME /etc/letsencrypt /var/lib/letsencrypt
WORKDIR /opt/certbot
COPY CHANGELOG.md README.rst setup.py src/
# Generate constraints file to pin dependency versions
COPY letsencrypt-auto-source/pieces/dependency-requirements.txt .
COPY tools /opt/certbot/tools
RUN sh -c 'cat dependency-requirements.txt | /opt/certbot/tools/strip_hashes.py > unhashed_requirements.txt'
RUN sh -c 'cat tools/dev_constraints.txt unhashed_requirements.txt | /opt/certbot/tools/merge_requirements.py > docker_constraints.txt'
COPY acme src/acme
COPY certbot src/certbot
RUN apk add --no-cache --virtual .certbot-deps \
libffi \
libssl1.1 \
openssl \
ca-certificates \
binutils
RUN apk add --no-cache --virtual .build-deps \
gcc \
linux-headers \
openssl-dev \
musl-dev \
libffi-dev \
&& pip install -r /opt/certbot/dependency-requirements.txt \
&& pip install --no-cache-dir --no-deps \
--editable /opt/certbot/src/acme \
--editable /opt/certbot/src \
&& apk del .build-deps

View File

@@ -1,5 +1,5 @@
# This Dockerfile builds an image for development. # This Dockerfile builds an image for development.
FROM ubuntu:xenial FROM debian:buster
# Note: this only exposes the port to other docker containers. # Note: this only exposes the port to other docker containers.
EXPOSE 80 443 EXPOSE 80 443

View File

@@ -123,6 +123,21 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
""" """
return self.update_registration(regr, update={'status': 'deactivated'}) return self.update_registration(regr, update={'status': 'deactivated'})
def deactivate_authorization(self, authzr):
# type: (messages.AuthorizationResource) -> messages.AuthorizationResource
"""Deactivate authorization.
:param messages.AuthorizationResource authzr: The Authorization resource
to be deactivated.
:returns: The Authorization resource that was deactivated.
:rtype: `.AuthorizationResource`
"""
body = messages.UpdateAuthorization(status='deactivated')
response = self._post(authzr.uri, body)
return self._authzr_from_response(response)
def _authzr_from_response(self, response, identifier=None, uri=None): def _authzr_from_response(self, response, identifier=None, uri=None):
authzr = messages.AuthorizationResource( authzr = messages.AuthorizationResource(
body=messages.Authorization.from_json(response.json()), body=messages.Authorization.from_json(response.json()),

View File

@@ -637,6 +637,14 @@ class ClientTest(ClientTestBase):
errors.PollError, self.client.poll_and_request_issuance, errors.PollError, self.client.poll_and_request_issuance,
csr, authzrs, mintime=mintime, max_attempts=2) csr, authzrs, mintime=mintime, max_attempts=2)
def test_deactivate_authorization(self):
authzb = self.authzr.body.update(status=messages.STATUS_DEACTIVATED)
self.response.json.return_value = authzb.to_json()
authzr = self.client.deactivate_authorization(self.authzr)
self.assertEqual(authzb, authzr.body)
self.assertEqual(self.client.net.post.call_count, 1)
self.assertTrue(self.authzr.uri in self.net.post.call_args_list[0][0])
def test_check_cert(self): def test_check_cert(self):
self.response.headers['Location'] = self.certr.uri self.response.headers['Location'] = self.certr.uri
self.response.content = CERT_DER self.response.content = CERT_DER

View File

@@ -168,6 +168,7 @@ STATUS_VALID = Status('valid')
STATUS_INVALID = Status('invalid') STATUS_INVALID = Status('invalid')
STATUS_REVOKED = Status('revoked') STATUS_REVOKED = Status('revoked')
STATUS_READY = Status('ready') STATUS_READY = Status('ready')
STATUS_DEACTIVATED = Status('deactivated')
class IdentifierType(_Constant): class IdentifierType(_Constant):
@@ -471,7 +472,7 @@ class Authorization(ResourceBody):
:ivar datetime.datetime expires: :ivar datetime.datetime expires:
""" """
identifier = jose.Field('identifier', decoder=Identifier.from_json) identifier = jose.Field('identifier', decoder=Identifier.from_json, omitempty=True)
challenges = jose.Field('challenges', omitempty=True) challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True) combinations = jose.Field('combinations', omitempty=True)
@@ -501,6 +502,12 @@ class NewAuthorization(Authorization):
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class UpdateAuthorization(Authorization):
"""Update authorization."""
resource_type = 'authz'
resource = fields.Resource(resource_type)
class AuthorizationResource(ResourceWithURI): class AuthorizationResource(ResourceWithURI):
"""Authorization Resource. """Authorization Resource.

View File

@@ -3,7 +3,7 @@ from setuptools import find_packages
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
import sys import sys
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Please update tox.ini when modifying dependency version requirements # Please update tox.ini when modifying dependency version requirements
install_requires = [ install_requires = [

View File

@@ -4,9 +4,13 @@ environment:
matrix: matrix:
- TOXENV: py35 - TOXENV: py35
- TOXENV: py37-cover - TOXENV: py37-cover
- TOXENV: integration-certbot
branches: branches:
only: only:
# apache-parser-v2 is a temporary branch for doing work related to
# rewriting the parser in the Apache plugin.
- apache-parser-v2
- master - master
- /^\d+\.\d+\.x$/ # Version branches like X.X.X - /^\d+\.\d+\.x$/ # Version branches like X.X.X
- /^test-.*$/ - /^test-.*$/
@@ -21,14 +25,16 @@ init:
install: install:
# Use Python 3.7 by default # Use Python 3.7 by default
- "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" - SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%
# Using 4 processes is proven to be the most efficient integration tests config for AppVeyor
- IF %TOXENV%==integration-certbot SET PYTEST_ADDOPTS=--numprocesses=4
# Check env # Check env
- "python --version" - python --version
# Upgrade pip to avoid warnings # Upgrade pip to avoid warnings
- "python -m pip install --upgrade pip" - python -m pip install --upgrade pip
# Ready to install tox and coverage # Ready to install tox and coverage
# tools/pip_install.py is used to pin packages to a known working version. # tools/pip_install.py is used to pin packages to a known working version.
- "python tools\\pip_install.py tox codecov" - python tools\\pip_install.py tox codecov
build: off build: off

View File

@@ -23,6 +23,7 @@ from certbot import interfaces
from certbot import util from certbot import util
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
from certbot.compat import filesystem
from certbot.compat import os from certbot.compat import os
from certbot.plugins import common from certbot.plugins import common
from certbot.plugins.util import path_surgery from certbot.plugins.util import path_surgery
@@ -895,7 +896,7 @@ class ApacheConfigurator(common.Installer):
if not new_vhost: if not new_vhost:
continue continue
internal_path = apache_util.get_internal_aug_path(new_vhost.path) internal_path = apache_util.get_internal_aug_path(new_vhost.path)
realpath = os.path.realpath(new_vhost.filep) realpath = filesystem.realpath(new_vhost.filep)
if realpath not in file_paths: if realpath not in file_paths:
file_paths[realpath] = new_vhost.filep file_paths[realpath] = new_vhost.filep
internal_paths[realpath].add(internal_path) internal_paths[realpath].add(internal_path)
@@ -1221,11 +1222,11 @@ class ApacheConfigurator(common.Installer):
""" """
if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")): if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
fp = os.path.join(os.path.realpath(self.option("vhost_root")), fp = os.path.join(filesystem.realpath(self.option("vhost_root")),
os.path.basename(non_ssl_vh_fp)) os.path.basename(non_ssl_vh_fp))
else: else:
# Use non-ssl filepath # Use non-ssl filepath
fp = os.path.realpath(non_ssl_vh_fp) fp = filesystem.realpath(non_ssl_vh_fp)
if fp.endswith(".conf"): if fp.endswith(".conf"):
return fp[:-(len(".conf"))] + self.option("le_vhost_ext") return fp[:-(len(".conf"))] + self.option("le_vhost_ext")

View File

@@ -9,6 +9,7 @@ MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-apache-conf-digest.txt" UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-apache-conf-digest.txt"
"""Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`.""" """Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`."""
# NEVER REMOVE A SINGLE HASH FROM THIS LIST UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!
ALL_SSL_OPTIONS_HASHES = [ ALL_SSL_OPTIONS_HASHES = [
'2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a', '2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a',
'4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a', '4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a',
@@ -18,6 +19,10 @@ ALL_SSL_OPTIONS_HASHES = [
'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b', 'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b',
'80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791', '80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791',
'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082', 'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082',
'717b0a89f5e4c39b09a42813ac6e747cfbdeb93439499e73f4f70a1fe1473f20',
'0fcdc81280cd179a07ec4d29d3595068b9326b455c488de4b09f585d5dafc137',
'86cc09ad5415cd6d5f09a947fe2501a9344328b1e8a8b458107ea903e80baa6c',
'06675349e457eae856120cdebb564efe546f0b87399f2264baeb41e442c724c7',
] ]
"""SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC""" """SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC"""

View File

@@ -31,6 +31,8 @@ OVERRIDE_CLASSES = {
"gentoo base system": override_gentoo.GentooConfigurator, "gentoo base system": override_gentoo.GentooConfigurator,
"opensuse": override_suse.OpenSUSEConfigurator, "opensuse": override_suse.OpenSUSEConfigurator,
"suse": override_suse.OpenSUSEConfigurator, "suse": override_suse.OpenSUSEConfigurator,
"scientific": override_centos.CentOSConfigurator,
"scientific linux": override_centos.CentOSConfigurator,
} }

View File

@@ -169,8 +169,7 @@ class ApacheHttp01(common.TLSSNI01):
def _set_up_challenges(self): def _set_up_challenges(self):
if not os.path.isdir(self.challenge_dir): if not os.path.isdir(self.challenge_dir):
os.makedirs(self.challenge_dir) filesystem.makedirs(self.challenge_dir, 0o755)
filesystem.chmod(self.challenge_dir, 0o755)
responses = [] responses = []
for achall in self.achalls: for achall in self.achalls:

View File

@@ -7,6 +7,7 @@ import zope.interface
from certbot import errors from certbot import errors
from certbot import interfaces from certbot import interfaces
from certbot import util from certbot import util
from certbot.compat import filesystem
from certbot.compat import os from certbot.compat import os
from certbot_apache import apache_util from certbot_apache import apache_util
@@ -65,7 +66,7 @@ class DebianConfigurator(configurator.ApacheConfigurator):
try: try:
os.symlink(vhost.filep, enabled_path) os.symlink(vhost.filep, enabled_path)
except OSError as err: except OSError as err:
if os.path.islink(enabled_path) and os.path.realpath( if os.path.islink(enabled_path) and filesystem.realpath(
enabled_path) == vhost.filep: enabled_path) == vhost.filep:
# Already in shape # Already in shape
vhost.enabled = True vhost.enabled = True

View File

@@ -15,7 +15,7 @@ SCRIPT_DIRNAME = os.path.dirname(__file__)
def main(args=None): def main(args=None):
if not args: if not args:
args = sys.argv[1:] args = sys.argv[1:]
with acme_server.setup_acme_server('pebble', [], False) as acme_xdist: with acme_server.ACMEServer('pebble', [], False) as acme_xdist:
environ = os.environ.copy() environ = os.environ.copy()
environ['SERVER'] = acme_xdist['directory_url'] environ['SERVER'] = acme_xdist['directory_url']
command = [os.path.join(SCRIPT_DIRNAME, 'apache-conf-test')] command = [os.path.join(SCRIPT_DIRNAME, 'apache-conf-test')]

View File

@@ -4,6 +4,7 @@ import unittest
import mock import mock
from certbot import errors from certbot import errors
from certbot.compat import filesystem
from certbot.compat import os from certbot.compat import os
from certbot_apache import obj from certbot_apache import obj
@@ -160,7 +161,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
"""Make sure we read the sysconfig OPTIONS variable correctly""" """Make sure we read the sysconfig OPTIONS variable correctly"""
# Return nothing for the process calls # Return nothing for the process calls
mock_cfg.return_value = "" mock_cfg.return_value = ""
self.config.parser.sysconfig_filep = os.path.realpath( self.config.parser.sysconfig_filep = filesystem.realpath(
os.path.join(self.config.parser.root, "../sysconfig/httpd")) os.path.join(self.config.parser.root, "../sysconfig/httpd"))
self.config.parser.variables = {} self.config.parser.variables = {}

View File

@@ -74,14 +74,6 @@ class ConfiguratorReverterTest(util.ApacheTest):
side_effect=errors.ReverterError) side_effect=errors.ReverterError)
self.assertRaises(errors.PluginError, self.config.rollback_checkpoints) self.assertRaises(errors.PluginError, self.config.rollback_checkpoints)
def test_view_config_changes(self):
self.config.view_config_changes()
def test_view_config_changes_error(self):
self.config.reverter.view_config_changes = mock.Mock(
side_effect=errors.ReverterError)
self.assertRaises(errors.PluginError, self.config.view_config_changes)
def test_recovery_routine_reload(self): def test_recovery_routine_reload(self):
mock_load = mock.Mock() mock_load = mock.Mock()
self.config.parser.aug.load = mock_load self.config.parser.aug.load = mock_load

View File

@@ -675,8 +675,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_make_vhost_ssl_nonexistent_vhost_path(self): def test_make_vhost_ssl_nonexistent_vhost_path(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1]) ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
self.assertEqual(os.path.dirname(ssl_vhost.filep), self.assertEqual(os.path.dirname(ssl_vhost.filep),
os.path.dirname(os.path.realpath( os.path.dirname(filesystem.realpath(self.vh_truth[1].filep)))
self.vh_truth[1].filep)))
def test_make_vhost_ssl(self): def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
@@ -1336,7 +1335,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.parser.modules.add("ssl_module") self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c") self.config.parser.modules.add("mod_ssl.c")
self.config.parser.modules.add("socache_shmcb_module") self.config.parser.modules.add("socache_shmcb_module")
tmp_path = os.path.realpath(tempfile.mkdtemp("vhostroot")) tmp_path = filesystem.realpath(tempfile.mkdtemp("vhostroot"))
filesystem.chmod(tmp_path, 0o755) filesystem.chmod(tmp_path, 0o755)
mock_p = "certbot_apache.configurator.ApacheConfigurator._get_ssl_vhost_path" mock_p = "certbot_apache.configurator.ApacheConfigurator._get_ssl_vhost_path"
mock_a = "certbot_apache.parser.ApacheParser.add_include" mock_a = "certbot_apache.parser.ApacheParser.add_include"

View File

@@ -79,9 +79,9 @@ class MultipleVhostsTestDebian(util.ApacheTest):
def test_enable_site_failure(self): def test_enable_site_failure(self):
self.config.parser.root = "/tmp/nonexistent" self.config.parser.root = "/tmp/nonexistent"
with mock.patch("os.path.isdir") as mock_dir: with mock.patch("certbot.compat.os.path.isdir") as mock_dir:
mock_dir.return_value = True mock_dir.return_value = True
with mock.patch("os.path.islink") as mock_link: with mock.patch("certbot.compat.os.path.islink") as mock_link:
mock_link.return_value = False mock_link.return_value = False
self.assertRaises( self.assertRaises(
errors.NotSupportedError, errors.NotSupportedError,

View File

@@ -4,6 +4,7 @@ import unittest
import mock import mock
from certbot import errors from certbot import errors
from certbot.compat import filesystem
from certbot.compat import os from certbot.compat import os
from certbot_apache import obj from certbot_apache import obj
@@ -160,7 +161,7 @@ class MultipleVhostsTestFedora(util.ApacheTest):
"""Make sure we read the sysconfig OPTIONS variable correctly""" """Make sure we read the sysconfig OPTIONS variable correctly"""
# Return nothing for the process calls # Return nothing for the process calls
mock_cfg.return_value = "" mock_cfg.return_value = ""
self.config.parser.sysconfig_filep = os.path.realpath( self.config.parser.sysconfig_filep = filesystem.realpath(
os.path.join(self.config.parser.root, "../sysconfig/httpd")) os.path.join(self.config.parser.root, "../sysconfig/httpd"))
self.config.parser.variables = {} self.config.parser.variables = {}

View File

@@ -4,6 +4,7 @@ import unittest
import mock import mock
from certbot import errors from certbot import errors
from certbot.compat import filesystem
from certbot.compat import os from certbot.compat import os
from certbot_apache import obj from certbot_apache import obj
@@ -81,7 +82,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
"""Make sure we read the Gentoo APACHE2_OPTS variable correctly""" """Make sure we read the Gentoo APACHE2_OPTS variable correctly"""
defines = ['DEFAULT_VHOST', 'INFO', defines = ['DEFAULT_VHOST', 'INFO',
'SSL', 'SSL_DEFAULT_VHOST', 'LANGUAGE'] 'SSL', 'SSL_DEFAULT_VHOST', 'LANGUAGE']
self.config.parser.apacheconfig_filep = os.path.realpath( self.config.parser.apacheconfig_filep = filesystem.realpath(
os.path.join(self.config.parser.root, "../conf.d/apache2")) os.path.join(self.config.parser.root, "../conf.d/apache2"))
self.config.parser.variables = {} self.config.parser.variables = {}
with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"): with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"):

View File

@@ -1,3 +1,3 @@
# Remember to update setup.py to match the package versions below. # Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0 acme[dev]==0.29.0
-e .[dev] certbot[dev]==0.37.0

View File

@@ -4,13 +4,13 @@ from setuptools.command.test import test as TestCommand
import sys import sys
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.
install_requires = [ install_requires = [
'acme>=0.29.0', 'acme>=0.29.0',
'certbot>=0.36.0.dev0', 'certbot>=0.37.0',
'mock', 'mock',
'python-augeas', 'python-augeas',
'setuptools', 'setuptools',

View File

@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi fi
VENV_BIN="$VENV_PATH/bin" VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="0.35.1" LE_AUTO_VERSION="0.37.2"
BASENAME=$(basename $0) BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS] USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -755,13 +755,31 @@ elif [ -f /etc/redhat-release ]; then
prev_le_python="$LE_PYTHON" prev_le_python="$LE_PYTHON"
unset LE_PYTHON unset LE_PYTHON
DeterminePythonVersion "NOCRASH" DeterminePythonVersion "NOCRASH"
# Starting to Fedora 29, python2 is on a deprecation path. Let's move to python3 then.
RPM_DIST_NAME=`(. /etc/os-release 2> /dev/null && echo $ID) || echo "unknown"` RPM_DIST_NAME=`(. /etc/os-release 2> /dev/null && echo $ID) || echo "unknown"`
RPM_DIST_VERSION=0
if [ "$RPM_DIST_NAME" = "fedora" ]; then # Set RPM_DIST_VERSION to VERSION_ID from /etc/os-release after splitting on
RPM_DIST_VERSION=`(. /etc/os-release 2> /dev/null && echo $VERSION_ID) || echo "0"` # '.' characters (e.g. "8.0" becomes "8"). If the command exits with an
# error, RPM_DIST_VERSION is set to "unknown".
RPM_DIST_VERSION=$( (. /etc/os-release 2> /dev/null && echo "$VERSION_ID") | cut -d '.' -f1 || echo "unknown")
# If RPM_DIST_VERSION is an empty string or it contains any nonnumeric
# characters, the value is unexpected so we set RPM_DIST_VERSION to 0.
if [ -z "$RPM_DIST_VERSION" ] || [ -n "$(echo "$RPM_DIST_VERSION" | tr -d '[0-9]')" ]; then
RPM_DIST_VERSION=0
fi fi
# Starting to Fedora 29, python2 is on a deprecation path. Let's move to python3 then.
# RHEL 8 also uses python3 by default.
if [ "$RPM_DIST_NAME" = "fedora" -a "$RPM_DIST_VERSION" -ge 29 -o "$PYVER" -eq 26 ]; then if [ "$RPM_DIST_NAME" = "fedora" -a "$RPM_DIST_VERSION" -ge 29 -o "$PYVER" -eq 26 ]; then
RPM_USE_PYTHON_3=1
elif [ "$RPM_DIST_NAME" = "rhel" -a "$RPM_DIST_VERSION" -ge 8 ]; then
RPM_USE_PYTHON_3=1
else
RPM_USE_PYTHON_3=0
fi
if [ "$RPM_USE_PYTHON_3" = 1 ]; then
Bootstrap() { Bootstrap() {
BootstrapMessage "RedHat-based OSes that will use Python3" BootstrapMessage "RedHat-based OSes that will use Python3"
BootstrapRpmPython3 BootstrapRpmPython3
@@ -775,6 +793,7 @@ elif [ -f /etc/redhat-release ]; then
} }
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
fi fi
LE_PYTHON="$prev_le_python" LE_PYTHON="$prev_le_python"
elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
Bootstrap() { Bootstrap() {
@@ -1314,18 +1333,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.35.1 \ certbot==0.37.2 \
--hash=sha256:24821e10b05084a45c5bf29da704115f2637af613866589737cff502294dad2a \ --hash=sha256:8f6f0097fb2aac64f13e5d6974781ac85a051d84a6cb3f4d79c6b75c5ea451b8 \
--hash=sha256:d7e8ecc14e06ed1dc691c6069bc9ce42dce04e8db1684ddfab446fbd71290860 --hash=sha256:e454368aa8d62559c673091b511319c130c8e0ea1c4dfa314ed7bdc91dd96ef5
acme==0.35.1 \ acme==0.37.2 \
--hash=sha256:3ec62f638f2b3684bcb3d8476345c7ae37c8f3b28f2999622ff836aec6e73d64 \ --hash=sha256:5666ba927a9e7bf3f9ed5a268bd5acf627b5838fb409e8401f05d2aaaee188ba \
--hash=sha256:a988b8b418cc74075e68b4acf3ff64c026bf52c377b0d01223233660a755c423 --hash=sha256:88798fae3bc692397db79c66930bd02fcaba8a6b1fba9a62f111dda42cc47f5c
certbot-apache==0.35.1 \ certbot-apache==0.37.2 \
--hash=sha256:ee4fe10cbd18e0aa7fe36d43ad7792187f41a7298f383610b87049c3a6493bbb \ --hash=sha256:e3ae7057f727506ab3796095ed66ca083f4e295d06f209ab96d2a3f37dea51b9 \
--hash=sha256:69962eafe0ec9be8eb2845e3622da6f37ecaeee7e517ea172d71d7b31f01be71 --hash=sha256:4cb44d1a7c56176a84446a11412c561479ed0fed19848632e61f104dbf6a3031
certbot-nginx==0.35.1 \ certbot-nginx==0.37.2 \
--hash=sha256:22150f13b3c0bd1c3c58b11a64886dad9695796aac42f5809da7ec66de187760 \ --hash=sha256:a92dffdf3daca97db5d7ae2287e505110c3fa01c035b9356abb2ef9fa32e8695 \
--hash=sha256:85e9a48b4b549f6989304f66cb2fad822c3f8717d361bde0d6a43aabb792d461 --hash=sha256:404f7b5b7611f0dce8773739170f306e94a59b69528cb74337e7f354936ac061
UNLIKELY_EOF UNLIKELY_EOF
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------

View File

@@ -0,0 +1,171 @@
import shutil
import subprocess
import os
import pkg_resources
import getpass
def construct_apache_config_dir(apache_root, http_port, https_port, key_path=None,
cert_path=None, wtf_prefix='le'):
config_path = os.path.join(apache_root, 'config')
shutil.copytree('/etc/apache2', config_path, symlinks=True)
webroot_path = os.path.join(apache_root, 'www')
os.mkdir(webroot_path)
with open(os.path.join(webroot_path, 'index.html'), 'w') as file_h:
file_h.write('Hello World!')
main_config_path = os.path.join(config_path, 'apache2.conf')
with open(main_config_path, 'w') as file_h:
file_h.write('''\
ServerRoot "{config}"
DefaultRuntimeDir ${{APACHE_RUN_DIR}}
PidFile ${{APACHE_PID_FILE}}
Timeout 300
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
User ${{APACHE_RUN_USER}}
Group ${{APACHE_RUN_GROUP}}
HostnameLookups Off
ErrorLog ${{APACHE_LOG_DIR}}/error.log
LogLevel warn
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
Include ports.conf
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>
<Directory {webroot}/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
AccessFileName .htaccess
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
LogFormat "%v:%p %h %l %u %t \\"%r\\" %>s %O \\"%{{Referer}}i\\" \\"%{{User-Agent}}i\\"" vhost_combined
LogFormat "%h %l %u %t \\"%r\\" %>s %O \\"%{{Referer}}i\\" \\"%{{User-Agent}}i\\"" combined
LogFormat "%h %l %u %t \\"%r\\" %>s %O" common
LogFormat "%{{Referer}}i -> %U" referer
LogFormat "%{{User-agent}}i" agent
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.conf
'''.format(config=config_path, webroot=webroot_path))
with open(os.path.join(config_path, 'ports.conf'), 'w') as file_h:
file_h.write('''\
Listen {http}
<IfModule ssl_module>
Listen {https}
</IfModule>
<IfModule mod_gnutls.c>
Listen {https}
</IfModule>
'''.format(http=http_port, https=https_port))
new_environ = os.environ.copy()
new_environ['APACHE_CONFDIR'] = config_path
run_path = os.path.join(apache_root, 'run')
lock_path = os.path.join(apache_root, 'lock')
logs_path = os.path.join(apache_root, 'logs')
os.mkdir(run_path)
os.mkdir(lock_path)
os.mkdir(logs_path)
user = getpass.getuser()
user = user if user != 'root' else 'www-data'
group = user
pid_file = os.path.join(run_path, 'apache.pid')
with open(os.path.join(config_path, 'envvars'), 'w') as file_h:
file_h.write('''\
unset HOME
export APACHE_RUN_USER={user}
export APACHE_RUN_GROUP={group}
export APACHE_PID_FILE={pid_file}
export APACHE_RUN_DIR={run_path}
export APACHE_LOCK_DIR={lock_path}
export APACHE_LOG_DIR={logs_path}
export LANG=C
'''.format(user=user, group=group, pid_file=pid_file,
run_path=run_path, lock_path=lock_path, logs_path=logs_path))
new_environ['APACHE_RUN_USER'] = user
new_environ['APACHE_RUN_GROUP'] = group
new_environ['APACHE_PID_FILE'] = pid_file
new_environ['APACHE_RUN_DIR'] = run_path
new_environ['APACHE_LOCK_DIR'] = lock_path
new_environ['APACHE_LOG_DIR'] = logs_path
le_host = 'apache.{0}.wtf'.format(wtf_prefix)
with open(os.path.join(config_path, 'sites-available', '000-default.conf'), 'w') as file_h:
file_h.write('''\
<VirtualHost *:{http}>
ServerAdmin webmaster@localhost
ServerName {le_host}
DocumentRoot {webroot}
ErrorLog ${{APACHE_LOG_DIR}}/error.log
CustomLog ${{APACHE_LOG_DIR}}/access.log combined
</VirtualHost>
'''.format(http=http_port, le_host=le_host, webroot=webroot_path))
key_path = key_path if key_path \
else pkg_resources.resource_filename('certbot_integration_tests', 'assets/key.pem')
cert_path = cert_path if cert_path \
else pkg_resources.resource_filename('certbot_integration_tests', 'assets/cert.pem')
with open(os.path.join(config_path, 'sites-available', 'default-ssl.conf'), 'w') as file_h:
file_h.write('''\
<IfModule mod_ssl.c>
<VirtualHost _default_:{https}>
ServerAdmin webmaster@localhost
ServerName {le_host}
DocumentRoot {webroot}
ErrorLog ${{APACHE_LOG_DIR}}/error.log
CustomLog ${{APACHE_LOG_DIR}}/access.log combined
SSLEngine on
SSLCertificateFile {cert_path}
SSLCertificateKeyFile {key_path}
<FilesMatch "\.(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
<Directory /usr/lib/cgi-bin>
SSLOptions +StdEnvVars
</Directory>
</VirtualHost>
</IfModule>
'''.format(https=https_port, le_host=le_host, webroot=webroot_path,
cert_path=cert_path, key_path=key_path))
return new_environ, config_path, pid_file
def test():
env = construct_apache_config_dir('/tmp/test1', 5001, 5002)
subprocess.call(['apache2ctl', '-DFOREGROUND'], env=env)

View File

@@ -0,0 +1,51 @@
import errno
import os
import signal
import subprocess
from certbot_integration_tests.certbot_tests import context as certbot_context
from certbot_integration_tests.apache_tests import apache_config
from certbot_integration_tests.utils import certbot_call
class IntegrationTestsContext(certbot_context.IntegrationTestsContext):
def __init__(self, request):
super(IntegrationTestsContext, self).__init__(request)
subprocess.check_output(['chmod', '+x', self.workspace])
self.apache_root = os.path.join(self.workspace, 'apache')
os.mkdir(self.apache_root)
self.env, self.config_dir, self.pid_file = apache_config.construct_apache_config_dir(
self.apache_root, self.http_01_port, self.tls_alpn_01_port,
wtf_prefix=self.worker_id)
def cleanup(self):
self._stop_apache()
super(IntegrationTestsContext, self).cleanup()
def certbot_test_apache(self, args):
command = ['--authenticator', 'apache', '--installer', 'apache',
'--apache-server-root', self.config_dir,
'--apache-challenge-location', self.apache_root]
command.extend(args)
return certbot_call.certbot_test(
command, self.directory_url, self.http_01_port, self.tls_alpn_01_port,
self.config_dir, self.workspace, env=self.env, force_renew=True)
def _stop_apache(self):
try:
with open(self.pid_file) as file_h:
pid = int(file_h.read().strip())
except BaseException:
pid = None
if pid:
try:
os.kill(pid, signal.SIGTERM)
except OSError as err:
# Ignore "No such process" error, Apache may already be stopped.
if err.errno != errno.ESRCH:
raise

View File

@@ -0,0 +1,18 @@
import pytest
from certbot_integration_tests.apache_tests import context as apache_context
@pytest.fixture()
def context(request):
# Fixture request is a built-in pytest fixture describing current test request.
integration_test_context = apache_context.IntegrationTestsContext(request)
try:
yield integration_test_context
finally:
integration_test_context.cleanup()
def test_it(context):
command = ['-d', 'apache.{0}.wtf'.format(context.worker_id)]
context.certbot_test_apache(command)

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python
import sys
import os
hook_script_type = os.path.basename(os.path.dirname(sys.argv[1]))
if hook_script_type == 'deploy' and ('RENEWED_DOMAINS' not in os.environ or 'RENEWED_LINEAGE' not in os.environ):
sys.stderr.write('Environment variables not properly set!\n')
sys.exit(1)
with open(sys.argv[2], 'a') as file_h:
file_h.write(hook_script_type + '\n')

View File

@@ -1,6 +1,17 @@
"""This module contains advanced assertions for the certbot integration tests.""" """This module contains advanced assertions for the certbot integration tests."""
import os import os
import grp try:
import grp
POSIX_MODE = True
except ImportError:
import win32api
import win32security
import ntsecuritycon
POSIX_MODE = False
EVERYBODY_SID = 'S-1-1-0'
SYSTEM_SID = 'S-1-5-18'
ADMINS_SID = 'S-1-5-32-544'
def assert_hook_execution(probe_path, probe_content): def assert_hook_execution(probe_path, probe_content):
@@ -10,9 +21,10 @@ def assert_hook_execution(probe_path, probe_content):
:param probe_content: content expected when the hook is executed :param probe_content: content expected when the hook is executed
""" """
with open(probe_path, 'r') as file: with open(probe_path, 'r') as file:
lines = file.readlines() data = file.read()
assert '{0}{1}'.format(probe_content, os.linesep) in lines lines = [line.strip() for line in data.splitlines()]
assert probe_content in lines
def assert_saved_renew_hook(config_dir, lineage): def assert_saved_renew_hook(config_dir, lineage):
@@ -38,16 +50,51 @@ def assert_cert_count_for_lineage(config_dir, lineage, count):
assert len(certs) == count assert len(certs) == count
def assert_equals_permissions(file1, file2, mask): def assert_equals_group_permissions(file1, file2):
""" """
Assert that permissions on two files are identical in respect to a given umask. Assert that two files have the same permissions for group owner.
:param file1: first file path to compare :param file1: first file path to compare
:param file2: second file path to compare :param file2: second file path to compare
:param mask: 3-octal representation of a POSIX umask under which the two files mode
should match (eg. 0o074 will test RWX on group and R on world)
""" """
mode_file1 = os.stat(file1).st_mode & mask # On Windows there is no group, so this assertion does nothing on this platform
mode_file2 = os.stat(file2).st_mode & mask if POSIX_MODE:
mode_file1 = os.stat(file1).st_mode & 0o070
mode_file2 = os.stat(file2).st_mode & 0o070
assert mode_file1 == mode_file2
def assert_equals_world_read_permissions(file1, file2):
"""
Assert that two files have the same read permissions for everyone.
:param file1: first file path to compare
:param file2: second file path to compare
"""
if POSIX_MODE:
mode_file1 = os.stat(file1).st_mode & 0o004
mode_file2 = os.stat(file2).st_mode & 0o004
else:
everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)
security1 = win32security.GetFileSecurity(file1, win32security.DACL_SECURITY_INFORMATION)
dacl1 = security1.GetSecurityDescriptorDacl()
mode_file1 = dacl1.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': everybody,
})
mode_file1 = mode_file1 & ntsecuritycon.FILE_GENERIC_READ
security2 = win32security.GetFileSecurity(file2, win32security.DACL_SECURITY_INFORMATION)
dacl2 = security2.GetSecurityDescriptorDacl()
mode_file2 = dacl2.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': everybody,
})
mode_file2 = mode_file2 & ntsecuritycon.FILE_GENERIC_READ
assert mode_file1 == mode_file2 assert mode_file1 == mode_file2
@@ -57,20 +104,57 @@ def assert_equals_group_owner(file1, file2):
Assert that two files have the same group owner. Assert that two files have the same group owner.
:param file1: first file path to compare :param file1: first file path to compare
:param file2: second file path to compare :param file2: second file path to compare
:return:
""" """
group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0] # On Windows there is no group, so this assertion does nothing on this platform
group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0] if POSIX_MODE:
group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0]
group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0]
assert group_owner_file1 == group_owner_file2 assert group_owner_file1 == group_owner_file2
def assert_world_permissions(file, mode): def assert_world_no_permissions(file):
""" """
Assert that a file has the expected world permission. Assert that the given file is not world-readable.
:param file: file path to check :param file: path of the file to check
:param mode: world permissions mode expected
""" """
mode_file_all = os.stat(file).st_mode & 0o007 if POSIX_MODE:
mode_file_all = os.stat(file).st_mode & 0o007
assert mode_file_all == 0
else:
security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
mode = dacl.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),
})
assert mode_file_all == mode assert not mode
def assert_world_read_permissions(file):
"""
Assert that the given file is world-readable, but not world-writable or world-executable.
:param file: path of the file to check
"""
if POSIX_MODE:
mode_file_all = os.stat(file).st_mode & 0o007
assert mode_file_all == 4
else:
security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
mode = dacl.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),
})
assert not mode & ntsecuritycon.FILE_GENERIC_WRITE
assert not mode & ntsecuritycon.FILE_GENERIC_EXECUTE
assert mode & ntsecuritycon.FILE_GENERIC_READ == ntsecuritycon.FILE_GENERIC_READ
def _get_current_user():
account_name = win32api.GetUserNameEx(win32api.NameSamCompatible)
return win32security.LookupAccountName(None, account_name)[0]

View File

@@ -1,10 +1,11 @@
"""Module to handle the context of integration tests.""" """Module to handle the context of integration tests."""
import logging
import os import os
import shutil import shutil
import sys import sys
import tempfile import tempfile
from certbot_integration_tests.utils import misc, certbot_call from certbot_integration_tests.utils import certbot_call
class IntegrationTestsContext(object): class IntegrationTestsContext(object):
@@ -19,7 +20,7 @@ class IntegrationTestsContext(object):
self.worker_id = 'primary' self.worker_id = 'primary'
acme_xdist = request.config.acme_xdist acme_xdist = request.config.acme_xdist
self.acme_server =acme_xdist['acme_server'] self.acme_server = acme_xdist['acme_server']
self.directory_url = acme_xdist['directory_url'] self.directory_url = acme_xdist['directory_url']
self.tls_alpn_01_port = acme_xdist['https_port'][self.worker_id] self.tls_alpn_01_port = acme_xdist['https_port'][self.worker_id]
self.http_01_port = acme_xdist['http_port'][self.worker_id] self.http_01_port = acme_xdist['http_port'][self.worker_id]
@@ -30,7 +31,10 @@ class IntegrationTestsContext(object):
self.workspace = tempfile.mkdtemp() self.workspace = tempfile.mkdtemp()
self.config_dir = os.path.join(self.workspace, 'conf') self.config_dir = os.path.join(self.workspace, 'conf')
self.hook_probe = tempfile.mkstemp(dir=self.workspace)[1]
probe = tempfile.mkstemp(dir=self.workspace)
os.close(probe[0])
self.hook_probe = probe[1]
self.manual_dns_auth_hook = ( self.manual_dns_auth_hook = (
'{0} -c "import os; import requests; import json; ' '{0} -c "import os; import requests; import json; '

View File

@@ -11,8 +11,11 @@ from os.path import join, exists
import pytest import pytest
from certbot_integration_tests.certbot_tests import context as certbot_context from certbot_integration_tests.certbot_tests import context as certbot_context
from certbot_integration_tests.certbot_tests.assertions import ( from certbot_integration_tests.certbot_tests.assertions import (
assert_hook_execution, assert_saved_renew_hook, assert_cert_count_for_lineage, assert_hook_execution, assert_saved_renew_hook,
assert_world_permissions, assert_equals_group_owner, assert_equals_permissions, assert_cert_count_for_lineage,
assert_world_no_permissions, assert_world_read_permissions,
assert_equals_group_owner, assert_equals_group_permissions, assert_equals_world_read_permissions,
EVERYBODY_SID
) )
from certbot_integration_tests.utils import misc from certbot_integration_tests.utils import misc
@@ -84,9 +87,9 @@ def test_http_01(context):
context.certbot([ context.certbot([
'--domains', certname, '--preferred-challenges', 'http-01', 'run', '--domains', certname, '--preferred-challenges', 'http-01', 'run',
'--cert-name', certname, '--cert-name', certname,
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('wtf_post', context.hook_probe),
'--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe) '--deploy-hook', misc.echo('deploy', context.hook_probe),
]) ])
assert_hook_execution(context.hook_probe, 'deploy') assert_hook_execution(context.hook_probe, 'deploy')
@@ -104,9 +107,9 @@ def test_manual_http_auth(context):
'--cert-name', certname, '--cert-name', certname,
'--manual-auth-hook', scripts[0], '--manual-auth-hook', scripts[0],
'--manual-cleanup-hook', scripts[1], '--manual-cleanup-hook', scripts[1],
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('wtf_post', context.hook_probe),
'--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe) '--renew-hook', misc.echo('renew', context.hook_probe),
]) ])
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@@ -122,9 +125,9 @@ def test_manual_dns_auth(context):
'run', '--cert-name', certname, 'run', '--cert-name', certname,
'--manual-auth-hook', context.manual_dns_auth_hook, '--manual-auth-hook', context.manual_dns_auth_hook,
'--manual-cleanup-hook', context.manual_dns_cleanup_hook, '--manual-cleanup-hook', context.manual_dns_cleanup_hook,
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('wtf_post', context.hook_probe),
'--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe) '--renew-hook', misc.echo('renew', context.hook_probe),
]) ])
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@@ -173,21 +176,19 @@ def test_renew_files_permissions(context):
certname = context.get_domain('renew') certname = context.get_domain('renew')
context.certbot(['-d', certname]) context.certbot(['-d', certname])
privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')
privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
assert_world_permissions( assert_world_no_permissions(privkey1)
join(context.config_dir, 'archive', certname, 'privkey1.pem'), 0)
context.certbot(['renew']) context.certbot(['renew'])
assert_cert_count_for_lineage(context.config_dir, certname, 2) assert_cert_count_for_lineage(context.config_dir, certname, 2)
assert_world_permissions( assert_world_no_permissions(privkey2)
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0) assert_equals_group_owner(privkey1, privkey2)
assert_equals_group_owner( assert_equals_world_read_permissions(privkey1, privkey2)
join(context.config_dir, 'archive', certname, 'privkey1.pem'), assert_equals_group_permissions(privkey1, privkey2)
join(context.config_dir, 'archive', certname, 'privkey2.pem'))
assert_equals_permissions(
join(context.config_dir, 'archive', certname, 'privkey1.pem'),
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0o074)
def test_renew_with_hook_scripts(context): def test_renew_with_hook_scripts(context):
@@ -211,15 +212,35 @@ def test_renew_files_propagate_permissions(context):
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
os.chmod(join(context.config_dir, 'archive', certname, 'privkey1.pem'), 0o444) privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')
privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
if os.name != 'nt':
os.chmod(privkey1, 0o444)
else:
import win32security
import ntsecuritycon
# Get the current DACL of the private key
security = win32security.GetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
# Create a read permission for Everybody group
everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)
dacl.AddAccessAllowedAce(win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody)
# Apply the updated DACL to the private key
security.SetSecurityDescriptorDacl(1, dacl, 0)
win32security.SetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION, security)
context.certbot(['renew']) context.certbot(['renew'])
assert_cert_count_for_lineage(context.config_dir, certname, 2) assert_cert_count_for_lineage(context.config_dir, certname, 2)
assert_world_permissions( if os.name != 'nt':
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 4) # On Linux, read world permissions + all group permissions will be copied from the previous private key
assert_equals_permissions( assert_world_read_permissions(privkey2)
join(context.config_dir, 'archive', certname, 'privkey1.pem'), assert_equals_world_read_permissions(privkey1, privkey2)
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0o074) assert_equals_group_permissions(privkey1, privkey2)
else:
# On Windows, world will never have any permissions, and group permission is irrelevant for this platform
assert_world_no_permissions(privkey2)
def test_graceful_renew_it_is_not_time(context): def test_graceful_renew_it_is_not_time(context):
@@ -229,7 +250,7 @@ def test_graceful_renew_it_is_not_time(context):
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
context.certbot(['renew', '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)], context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],
force_renew=False) force_renew=False)
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
@@ -250,7 +271,7 @@ def test_graceful_renew_it_is_time(context):
with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'w') as file: with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'w') as file:
file.writelines(lines) file.writelines(lines)
context.certbot(['renew', '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)], context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],
force_renew=False) force_renew=False)
assert_cert_count_for_lineage(context.config_dir, certname, 2) assert_cert_count_for_lineage(context.config_dir, certname, 2)
@@ -317,9 +338,9 @@ def test_renew_hook_override(context):
context.certbot([ context.certbot([
'certonly', '-d', certname, 'certonly', '-d', certname,
'--preferred-challenges', 'http-01', '--preferred-challenges', 'http-01',
'--pre-hook', 'echo pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('pre', context.hook_probe),
'--post-hook', 'echo post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('post', context.hook_probe),
'--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe) '--deploy-hook', misc.echo('deploy', context.hook_probe),
]) ])
assert_hook_execution(context.hook_probe, 'pre') assert_hook_execution(context.hook_probe, 'pre')
@@ -330,14 +351,14 @@ def test_renew_hook_override(context):
open(context.hook_probe, 'w').close() open(context.hook_probe, 'w').close()
context.certbot([ context.certbot([
'renew', '--cert-name', certname, 'renew', '--cert-name', certname,
'--pre-hook', 'echo pre-override >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('pre_override', context.hook_probe),
'--post-hook', 'echo post-override >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('post_override', context.hook_probe),
'--deploy-hook', 'echo deploy-override >> "{0}"'.format(context.hook_probe) '--deploy-hook', misc.echo('deploy_override', context.hook_probe),
]) ])
assert_hook_execution(context.hook_probe, 'pre-override') assert_hook_execution(context.hook_probe, 'pre_override')
assert_hook_execution(context.hook_probe, 'post-override') assert_hook_execution(context.hook_probe, 'post_override')
assert_hook_execution(context.hook_probe, 'deploy-override') assert_hook_execution(context.hook_probe, 'deploy_override')
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'pre') assert_hook_execution(context.hook_probe, 'pre')
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@@ -349,11 +370,11 @@ def test_renew_hook_override(context):
open(context.hook_probe, 'w').close() open(context.hook_probe, 'w').close()
context.certbot(['renew', '--cert-name', certname]) context.certbot(['renew', '--cert-name', certname])
assert_hook_execution(context.hook_probe, 'pre-override') assert_hook_execution(context.hook_probe, 'pre_override')
assert_hook_execution(context.hook_probe, 'post-override') assert_hook_execution(context.hook_probe, 'post_override')
assert_hook_execution(context.hook_probe, 'deploy-override') assert_hook_execution(context.hook_probe, 'deploy_override')
def test_invalid_domain_with_dns_challenge(context): def test_invalid_domain_with_dns_challenge(context):
"""Test certificate issuance failure with DNS-01 challenge.""" """Test certificate issuance failure with DNS-01 challenge."""
# Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'. # Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'.
@@ -512,7 +533,7 @@ def test_revoke_multiple_lineages(context):
data = file.read() data = file.read()
data = re.sub('archive_dir = .*\n', data = re.sub('archive_dir = .*\n',
'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1)), 'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1).replace('\\', '\\\\')),
data) data)
with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file: with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file:
@@ -555,11 +576,9 @@ def test_ocsp_status_stale(context):
def test_ocsp_status_live(context): def test_ocsp_status_live(context):
"""Test retrieval of OCSP statuses for live config""" """Test retrieval of OCSP statuses for live config"""
if context.acme_server == 'pebble': cert = context.get_domain('ocsp-check')
pytest.skip('Pebble does not support OCSP status requests.')
# OSCP 1: Check live certificate OCSP status (VALID) # OSCP 1: Check live certificate OCSP status (VALID)
cert = context.get_domain('ocsp-check')
context.certbot(['--domains', cert]) context.certbot(['--domains', cert])
output = context.certbot(['certificates']) output = context.certbot(['certificates'])

View File

@@ -68,17 +68,18 @@ def _setup_primary_node(config):
:param config: Configuration of the pytest primary node :param config: Configuration of the pytest primary node
""" """
# Check for runtime compatibility: some tools are required to be available in PATH # Check for runtime compatibility: some tools are required to be available in PATH
try: if 'boulder' in config.option.acme_server:
subprocess.check_output(['docker', '-v'], stderr=subprocess.STDOUT) try:
except (subprocess.CalledProcessError, OSError): subprocess.check_output(['docker', '-v'], stderr=subprocess.STDOUT)
raise ValueError('Error: docker is required in PATH to launch the integration tests, ' except (subprocess.CalledProcessError, OSError):
'but is not installed or not available for current user.') raise ValueError('Error: docker is required in PATH to launch the integration tests on'
'boulder, but is not installed or not available for current user.')
try: try:
subprocess.check_output(['docker-compose', '-v'], stderr=subprocess.STDOUT) subprocess.check_output(['docker-compose', '-v'], stderr=subprocess.STDOUT)
except (subprocess.CalledProcessError, OSError): except (subprocess.CalledProcessError, OSError):
raise ValueError('Error: docker-compose is required in PATH to launch the integration tests, ' raise ValueError('Error: docker-compose is required in PATH to launch the integration tests, '
'but is not installed or not available for current user.') 'but is not installed or not available for current user.')
# Parameter numprocesses is added to option by pytest-xdist # Parameter numprocesses is added to option by pytest-xdist
workers = ['primary'] if not config.option.numprocesses\ workers = ['primary'] if not config.option.numprocesses\
@@ -86,7 +87,7 @@ def _setup_primary_node(config):
# By calling setup_acme_server we ensure that all necessary acme server instances will be # By calling setup_acme_server we ensure that all necessary acme server instances will be
# fully started. This runtime is reflected by the acme_xdist returned. # fully started. This runtime is reflected by the acme_xdist returned.
acme_server = acme_lib.setup_acme_server(config.option.acme_server, workers) acme_server = acme_lib.ACMEServer(config.option.acme_server, workers)
config.add_cleanup(acme_server.stop) config.add_cleanup(acme_server.stop)
print('ACME xdist config:\n{0}'.format(acme_server.acme_xdist)) print('ACME xdist config:\n{0}'.format(acme_server.acme_xdist))
acme_server.start() acme_server.start()

View File

@@ -21,9 +21,9 @@ def construct_nginx_config(nginx_root, nginx_webroot, http_port, https_port, oth
:rtype: str :rtype: str
""" """
key_path = key_path if key_path \ key_path = key_path if key_path \
else pkg_resources.resource_filename('certbot_integration_tests', 'assets/nginx_key.pem') else pkg_resources.resource_filename('certbot_integration_tests', 'assets/key.pem')
cert_path = cert_path if cert_path \ cert_path = cert_path if cert_path \
else pkg_resources.resource_filename('certbot_integration_tests', 'assets/nginx_cert.pem') else pkg_resources.resource_filename('certbot_integration_tests', 'assets/cert.pem')
return '''\ return '''\
# This error log will be written regardless of server scope error_log # This error log will be written regardless of server scope error_log
# definitions, so we have to set this here in the main scope. # definitions, so we have to set this here in the main scope.

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Module to setup an ACME CA server environment able to run multiple tests in parallel""" """Module to setup an ACME CA server environment able to run multiple tests in parallel"""
from __future__ import print_function from __future__ import print_function
import errno
import json import json
import tempfile import tempfile
import time import time
@@ -11,184 +12,185 @@ import sys
from os.path import join from os.path import join
import requests import requests
import yaml
from certbot_integration_tests.utils import misc, proxy from certbot_integration_tests.utils import misc, proxy, pebble_artifacts
from certbot_integration_tests.utils.constants import * from certbot_integration_tests.utils.constants import *
class ACMEServer(object): class ACMEServer(object):
""" """
Handler exposing methods to start and stop the ACME server, and get its configuration ACMEServer configures and handles the lifecycle of an ACME CA server and an HTTP reverse proxy
(eg. challenges ports). ACMEServer is also a context manager, and so can be used to instance, to allow parallel execution of integration tests against the unique http-01 port
ensure ACME server is started/stopped upon context enter/exit. expected by the ACME CA server.
Typically all pytest integration tests will be executed in this context.
ACMEServer gives access the acme_xdist parameter, listing the ports and directory url to use
for each pytest node. It exposes also start and stop methods in order to start the stack, and
stop it with proper resources cleanup.
ACMEServer is also a context manager, and so can be used to ensure ACME server is started/stopped
upon context enter/exit.
""" """
def __init__(self, acme_xdist, start, server_cleanup): def __init__(self, acme_server, nodes, http_proxy=True, stdout=False):
self._proxy_process = None """
self._server_cleanup = server_cleanup Create an ACMEServer instance.
self.acme_xdist = acme_xdist :param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble)
self.start = start :param list nodes: list of node names that will be setup by pytest xdist
:param bool http_proxy: if False do not start the HTTP proxy
:param bool stdout: if True stream subprocesses stdout to standard stdout
"""
self._construct_acme_xdist(acme_server, nodes)
self._acme_type = 'pebble' if acme_server == 'pebble' else 'boulder'
self._proxy = http_proxy
self._workspace = tempfile.mkdtemp()
self._processes = []
self._stdout = sys.stdout if stdout else open(os.devnull, 'w')
def start(self):
"""Start the test stack"""
try:
if self._proxy:
self._prepare_http_proxy()
if self._acme_type == 'pebble':
self._prepare_pebble_server()
if self._acme_type == 'boulder':
self._prepare_boulder_server()
except BaseException as e:
self.stop()
raise e
def stop(self): def stop(self):
if self._proxy_process: """Stop the test stack, and clean its resources"""
self._proxy_process.terminate() print('=> Tear down the test infrastructure...')
self._proxy_process.wait() try:
self._server_cleanup() for process in self._processes:
try:
process.terminate()
except OSError as e:
# Process may be not started yet, so no PID and terminate fails.
# Then the process never started, and the situation is acceptable.
if e.errno != errno.ESRCH:
raise
for process in self._processes:
process.wait()
if os.path.exists(os.path.join(self._workspace, 'boulder')):
# Boulder docker generates build artifacts owned by root with 0o744 permissions.
# If we started the acme server from a normal user that has access to the Docker
# daemon, this user will not be able to delete these artifacts from the host.
# We need to do it through a docker.
process = self._launch_process(['docker', 'run', '--rm', '-v',
'{0}:/workspace'.format(self._workspace),
'alpine', 'rm', '-rf', '/workspace/boulder'])
process.wait()
finally:
shutil.rmtree(self._workspace)
if self._stdout != sys.stdout:
self._stdout.close()
print('=> Test infrastructure stopped and cleaned up.')
def __enter__(self): def __enter__(self):
self._proxy_process = self.start() self.start()
return self.acme_xdist return self.acme_xdist
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
self.stop() self.stop()
def _construct_acme_xdist(self, acme_server, nodes):
"""Generate and return the acme_xdist dict"""
acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT}
def setup_acme_server(acme_server, nodes, proxy=True): # Directory and ACME port are set implicitly in the docker-compose.yml files of Boulder/Pebble.
""" if acme_server == 'pebble':
This method will setup an ACME CA server and an HTTP reverse proxy instance, to allow parallel acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL
execution of integration tests against the unique http-01 port expected by the ACME CA server. else: # boulder
Typically all pytest integration tests will be executed in this context. acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL \
An ACMEServer instance will be returned, giving access to the ports and directory url to use if acme_server == 'boulder-v2' else BOULDER_V1_DIRECTORY_URL
for each pytest node, and its start and stop methods are appropriately configured to
respectively start the server, and stop it with proper resources cleanup.
:param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble)
:param str[] nodes: list of node names that will be setup by pytest xdist
:param bool proxy: set to False to not start the HTTP proxy
:return: a properly configured ACMEServer instance
:rtype: ACMEServer
"""
acme_type = 'pebble' if acme_server == 'pebble' else 'boulder'
acme_xdist = _construct_acme_xdist(acme_server, nodes)
workspace, server_cleanup = _construct_workspace(acme_type)
def start(): acme_xdist['http_port'] = {node: port for (node, port)
proxy_process = _prepare_http_proxy(acme_xdist) if proxy else None in zip(nodes, range(5200, 5200 + len(nodes)))}
_prepare_acme_server(workspace, acme_type, acme_xdist) acme_xdist['https_port'] = {node: port for (node, port)
in zip(nodes, range(5100, 5100 + len(nodes)))}
acme_xdist['other_port'] = {node: port for (node, port)
in zip(nodes, range(5300, 5300 + len(nodes)))}
return proxy_process self.acme_xdist = acme_xdist
return ACMEServer(acme_xdist, start, server_cleanup) def _prepare_pebble_server(self):
"""Configure and launch the Pebble server"""
print('=> Starting pebble instance deployment...')
pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts.fetch(self._workspace)
# Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid
# nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment.
environ = os.environ.copy()
environ['PEBBLE_VA_NOSLEEP'] = '1'
environ['PEBBLE_WFE_NONCEREJECT'] = '0'
def _construct_acme_xdist(acme_server, nodes): self._launch_process(
"""Generate and return the acme_xdist dict""" [pebble_path, '-config', pebble_config_path, '-dnsserver', '127.0.0.1:8053'],
acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT} env=environ)
# Directory and ACME port are set implicitly in the docker-compose.yml files of Boulder/Pebble. self._launch_process(
if acme_server == 'pebble': [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT), '-defaultIPv6', '""',
acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL '-defaultIPv4', '127.0.0.1', '-http01', '""', '-tlsalpn01', '""', '-https01', '""'])
else: # boulder
acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL \
if acme_server == 'boulder-v2' else BOULDER_V1_DIRECTORY_URL
acme_xdist['http_port'] = {node: port for (node, port) # pebble_ocsp_server is imported here and not at the top of module in order to avoid a useless
in zip(nodes, range(5200, 5200 + len(nodes)))} # ImportError, in the case where cryptography dependency is too old to support ocsp, but
acme_xdist['https_port'] = {node: port for (node, port) # Boulder is used instead of Pebble, so pebble_ocsp_server is not used. This is the typical
in zip(nodes, range(5100, 5100 + len(nodes)))} # situation of integration-certbot-oldest tox testenv.
acme_xdist['other_port'] = {node: port for (node, port) from certbot_integration_tests.utils import pebble_ocsp_server
in zip(nodes, range(5300, 5300 + len(nodes)))} self._launch_process([sys.executable, pebble_ocsp_server.__file__])
return acme_xdist
def _construct_workspace(acme_type):
"""Create a temporary workspace for integration tests stack"""
workspace = tempfile.mkdtemp()
def cleanup():
"""Cleanup function to call that will teardown relevant dockers and their configuration."""
print('=> Tear down the {0} instance...'.format(acme_type))
instance_path = join(workspace, acme_type)
try:
if os.path.isfile(join(instance_path, 'docker-compose.yml')):
_launch_command(['docker-compose', 'down'], cwd=instance_path)
except subprocess.CalledProcessError:
pass
print('=> Finished tear down of {0} instance.'.format(acme_type))
if acme_type == 'boulder' and os.path.exists(os.path.join(workspace, 'boulder')):
# Boulder docker generates build artifacts owned by root user with 0o744 permissions.
# If we started the acme server from a normal user that has access to the Docker
# daemon, this user will not be able to delete these artifacts from the host.
# We need to do it through a docker.
_launch_command(['docker', 'run', '--rm', '-v', '{0}:/workspace'.format(workspace),
'alpine', 'rm', '-rf', '/workspace/boulder'])
shutil.rmtree(workspace)
return workspace, cleanup
def _prepare_acme_server(workspace, acme_type, acme_xdist):
"""Configure and launch the ACME server, Boulder or Pebble"""
print('=> Starting {0} instance deployment...'.format(acme_type))
instance_path = join(workspace, acme_type)
try:
# Load Boulder/Pebble from git, that includes a docker-compose.yml ready for production.
_launch_command(['git', 'clone', 'https://github.com/letsencrypt/{0}'.format(acme_type),
'--single-branch', '--depth=1', instance_path])
if acme_type == 'boulder':
# Allow Boulder to ignore usual limit rate policies, useful for tests.
os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'),
join(instance_path, 'test/rate-limit-policies.yml'))
if acme_type == 'pebble':
with open(os.path.join(instance_path, 'docker-compose.yml'), 'r') as file_handler:
config = yaml.load(file_handler.read())
# Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid
# nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment.
config['services']['pebble'].setdefault('environment', [])\
.extend(['PEBBLE_VA_NOSLEEP=1', 'PEBBLE_WFE_NONCEREJECT=0'])
# Also disable strict mode for now, since Pebble v2.1.0 added specs in
# strict mode for which Certbot is not compliant for now.
# See https://github.com/certbot/certbot/pull/7175
# TODO: Add back -strict mode once Certbot is compliant with Pebble v2.1.0+
config['services']['pebble']['command'] = config['services']['pebble']['command']\
.replace('-strict', '')
with open(os.path.join(instance_path, 'docker-compose.yml'), 'w') as file_handler:
file_handler.write(yaml.dump(config))
# Launch the ACME CA server.
_launch_command(['docker-compose', 'up', '--force-recreate', '-d'], cwd=instance_path)
# Wait for the ACME CA server to be up. # Wait for the ACME CA server to be up.
print('=> Waiting for {0} instance to respond...'.format(acme_type)) print('=> Waiting for pebble instance to respond...')
misc.check_until_timeout(acme_xdist['directory_url']) misc.check_until_timeout(self.acme_xdist['directory_url'])
print('=> Finished pebble instance deployment.')
def _prepare_boulder_server(self):
"""Configure and launch the Boulder server"""
print('=> Starting boulder instance deployment...')
instance_path = join(self._workspace, 'boulder')
# Load Boulder from git, that includes a docker-compose.yml ready for production.
process = self._launch_process(['git', 'clone', 'https://github.com/letsencrypt/boulder',
'--single-branch', '--depth=1', instance_path])
process.wait()
# Allow Boulder to ignore usual limit rate policies, useful for tests.
os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'),
join(instance_path, 'test/rate-limit-policies.yml'))
# Launch the Boulder server
self._launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path)
# Wait for the ACME CA server to be up.
print('=> Waiting for boulder instance to respond...')
misc.check_until_timeout(self.acme_xdist['directory_url'], attempts=240)
# Configure challtestsrv to answer any A record request with ip of the docker host. # Configure challtestsrv to answer any A record request with ip of the docker host.
acme_subnet = '10.77.77' if acme_type == 'boulder' else '10.30.50' response = requests.post('http://localhost:{0}/set-default-ipv4'.format(CHALLTESTSRV_PORT),
response = requests.post('http://localhost:{0}/set-default-ipv4' json={'ip': '10.77.77.1'})
.format(acme_xdist['challtestsrv_port']),
json={'ip': '{0}.1'.format(acme_subnet)})
response.raise_for_status() response.raise_for_status()
print('=> Finished {0} instance deployment.'.format(acme_type)) print('=> Finished boulder instance deployment.')
except BaseException:
print('Error while setting up {0} instance.'.format(acme_type))
raise
def _prepare_http_proxy(self):
"""Configure and launch an HTTP proxy"""
print('=> Configuring the HTTP proxy...')
mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port)
for node, port in self.acme_xdist['http_port'].items()}
command = [sys.executable, proxy.__file__, str(HTTP_01_PORT), json.dumps(mapping)]
self._launch_process(command)
print('=> Finished configuring the HTTP proxy.')
def _prepare_http_proxy(acme_xdist): def _launch_process(self, command, cwd=os.getcwd(), env=None):
"""Configure and launch an HTTP proxy""" """Launch silently an subprocess OS command"""
print('=> Configuring the HTTP proxy...') if not env:
mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port) env = os.environ
for node, port in acme_xdist['http_port'].items()} process = subprocess.Popen(command, stdout=self._stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env)
command = [sys.executable, proxy.__file__, str(HTTP_01_PORT), json.dumps(mapping)] self._processes.append(process)
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) return process
print('=> Finished configuring the HTTP proxy.')
return process
def _launch_command(command, cwd=os.getcwd()):
"""Launch silently an OS command, output will be displayed in case of failure"""
try:
subprocess.check_output(command, stderr=subprocess.STDOUT, cwd=cwd, universal_newlines=True)
except subprocess.CalledProcessError as e:
sys.stderr.write(e.output)
raise
def main(): def main():
@@ -199,8 +201,7 @@ def main():
raise ValueError('Invalid server value {0}, should be one of {1}' raise ValueError('Invalid server value {0}, should be one of {1}'
.format(server_type, possible_values)) .format(server_type, possible_values))
acme_server = setup_acme_server(server_type, [], False) acme_server = ACMEServer(server_type, [], http_proxy=False, stdout=True)
process = None
try: try:
with acme_server as acme_xdist: with acme_server as acme_xdist:
@@ -208,15 +209,10 @@ def main():
.format(acme_xdist['directory_url'])) .format(acme_xdist['directory_url']))
print('--> Press CTRL+C to stop the ACME server.') print('--> Press CTRL+C to stop the ACME server.')
docker_name = 'pebble_pebble_1' if 'pebble' in server_type else 'boulder_boulder_1'
process = subprocess.Popen(['docker', 'logs', '-f', docker_name])
while True: while True:
time.sleep(3600) time.sleep(3600)
except KeyboardInterrupt: except KeyboardInterrupt:
if process: pass
process.terminate()
process.wait()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -6,12 +6,12 @@ import subprocess
import sys import sys
import os import os
from certbot_integration_tests.utils import misc import certbot_integration_tests
from certbot_integration_tests.utils.constants import * from certbot_integration_tests.utils.constants import *
def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port, def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
config_dir, workspace, force_renew=True): config_dir, workspace, env=None, force_renew=True):
""" """
Invoke the certbot executable available in PATH in a test context for the given args. Invoke the certbot executable available in PATH in a test context for the given args.
The test context consists in running certbot in debug mode, with various flags suitable The test context consists in running certbot in debug mode, with various flags suitable
@@ -23,28 +23,70 @@ def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
:param int tls_alpn_01_port: port for the TLS-ALPN-01 challenges :param int tls_alpn_01_port: port for the TLS-ALPN-01 challenges
:param str config_dir: certbot configuration directory to use :param str config_dir: certbot configuration directory to use
:param str workspace: certbot current directory to use :param str workspace: certbot current directory to use
:param obj env: the environment variables to use (default: None, then current env will be used)
:param bool force_renew: set False to not force renew existing certificates (default: True) :param bool force_renew: set False to not force renew existing certificates (default: True)
:return: stdout as string :return: stdout as string
:rtype: str :rtype: str
""" """
new_environ = env if env else os.environ.copy()
command, env = _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port, command, env = _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
config_dir, workspace, force_renew) config_dir, workspace, force_renew, new_environ)
return subprocess.check_output(command, universal_newlines=True, cwd=workspace, env=env) return subprocess.check_output(command, universal_newlines=True, cwd=workspace, env=env)
def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port, def _prepare_environ(workspace, new_environ):
config_dir, workspace, force_renew): new_environ = new_environ.copy()
new_environ = os.environ.copy()
new_environ['TMPDIR'] = workspace new_environ['TMPDIR'] = workspace
# So, pytest is nice, and a little too nice for our usage.
# In order to help user to call seamlessly any piece of python code without requiring to
# install it as a full-fledged setuptools distribution for instance, it may inject the path
# to the test files into the PYTHONPATH. This allows the python interpreter to import
# as modules any python file available at this path.
# See https://docs.pytest.org/en/3.2.5/pythonpath.html for the explanation and description.
# However this behavior is not good in integration tests, in particular the nginx oldest ones.
# Indeed during these kind of tests certbot is installed as a transitive dependency to
# certbot-nginx. Here is the trick: this certbot version is not necessarily the same as
# the certbot codebase lying in current working directory. For instance in oldest tests
# certbot==0.36.0 may be installed while the codebase corresponds to certbot==0.37.0.dev0.
# Then during a pytest run, PYTHONPATH contains the path to the Certbot codebase, so invoking
# certbot will import the modules from the codebase (0.37.0.dev0), not from the
# required/installed version (0.36.0).
# This will lead to funny and totally incomprehensible errors. To avoid that, we ensure that
# if PYTHONPATH is set, it does not contain the path to the root of the codebase.
if new_environ.get('PYTHONPATH'):
# certbot_integration_tests.__file__ is:
# '/path/to/certbot/certbot-ci/certbot_integration_tests/__init__.pyc'
# ... and we want '/path/to/certbot'
certbot_root = os.path.dirname(os.path.dirname(os.path.dirname(certbot_integration_tests.__file__)))
python_paths = [path for path in new_environ['PYTHONPATH'].split(':') if path != certbot_root]
new_environ['PYTHONPATH'] = ':'.join(python_paths)
return new_environ
def _compute_additional_args(workspace, environ, force_renew):
additional_args = [] additional_args = []
if misc.get_certbot_version() >= LooseVersion('0.30.0'): output = subprocess.check_output(['certbot', '--version'],
universal_newlines=True, stderr=subprocess.STDOUT,
cwd=workspace, env=environ)
version_str = output.split(' ')[1].strip() # Typical response is: output = 'certbot 0.31.0.dev0'
if LooseVersion(version_str) >= LooseVersion('0.30.0'):
additional_args.append('--no-random-sleep-on-renew') additional_args.append('--no-random-sleep-on-renew')
if force_renew: if force_renew:
additional_args.append('--renew-by-default') additional_args.append('--renew-by-default')
return additional_args
def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
config_dir, workspace, force_renew, new_environ):
new_environ = _prepare_environ(workspace, new_environ)
additional_args = _compute_additional_args(workspace, new_environ, force_renew)
command = [ command = [
'certbot', 'certbot',
'--server', directory_url, '--server', directory_url,

View File

@@ -5,3 +5,5 @@ CHALLTESTSRV_PORT = 8055
BOULDER_V1_DIRECTORY_URL = 'http://localhost:4000/directory' BOULDER_V1_DIRECTORY_URL = 'http://localhost:4000/directory'
BOULDER_V2_DIRECTORY_URL = 'http://localhost:4001/directory' BOULDER_V2_DIRECTORY_URL = 'http://localhost:4001/directory'
PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir' PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir'
PEBBLE_MANAGEMENT_URL = 'https://localhost:15000'
MOCK_OCSP_SERVER_PORT = 4002

View File

@@ -3,9 +3,11 @@ Misc module contains stateless functions that could be used during pytest execut
or outside during setup/teardown of the integration tests environment. or outside during setup/teardown of the integration tests environment.
""" """
import contextlib import contextlib
import logging
import errno import errno
import multiprocessing import multiprocessing
import os import os
import re
import shutil import shutil
import stat import stat
import subprocess import subprocess
@@ -23,17 +25,17 @@ from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
from six.moves import socketserver, SimpleHTTPServer from six.moves import socketserver, SimpleHTTPServer
RSA_KEY_TYPE = 'rsa' RSA_KEY_TYPE = 'rsa'
ECDSA_KEY_TYPE = 'ecdsa' ECDSA_KEY_TYPE = 'ecdsa'
def check_until_timeout(url): def check_until_timeout(url, attempts=30):
""" """
Wait and block until given url responds with status 200, or raise an exception Wait and block until given url responds with status 200, or raise an exception
after 150 attempts. after the specified number of attempts.
:param str url: the URL to test :param str url: the URL to test
:raise ValueError: exception raised after 150 unsuccessful attempts to reach the URL :param int attempts: the number of times to try to connect to the URL
:raise ValueError: exception raised if unable to reach the URL
""" """
try: try:
import urllib3 import urllib3
@@ -43,7 +45,7 @@ def check_until_timeout(url):
from requests.packages.urllib3.exceptions import InsecureRequestWarning from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
for _ in range(0, 150): for _ in range(attempts):
time.sleep(1) time.sleep(1)
try: try:
if requests.get(url, verify=False).status_code == 200: if requests.get(url, verify=False).status_code == 200:
@@ -51,7 +53,7 @@ def check_until_timeout(url):
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
pass pass
raise ValueError('Error, url did not respond after 150 attempts: {0}'.format(url)) raise ValueError('Error, url did not respond after {0} attempts: {1}'.format(attempts, url))
class GracefulTCPServer(socketserver.TCPServer): class GracefulTCPServer(socketserver.TCPServer):
@@ -62,6 +64,10 @@ class GracefulTCPServer(socketserver.TCPServer):
allow_reuse_address = True allow_reuse_address = True
def _run_server(port):
GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever()
@contextlib.contextmanager @contextlib.contextmanager
def create_http_server(port): def create_http_server(port):
""" """
@@ -74,10 +80,7 @@ def create_http_server(port):
current_cwd = os.getcwd() current_cwd = os.getcwd()
webroot = tempfile.mkdtemp() webroot = tempfile.mkdtemp()
def run(): process = multiprocessing.Process(target=_run_server, args=(port,))
GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever()
process = multiprocessing.Process(target=run)
try: try:
# SimpleHTTPServer is designed to serve files from the current working directory at the # SimpleHTTPServer is designed to serve files from the current working directory at the
@@ -119,15 +122,9 @@ def generate_test_file_hooks(config_dir, hook_probe):
:param str config_dir: current certbot config directory :param str config_dir: current certbot config directory
:param hook_probe: path to the hook probe to test hook scripts execution :param hook_probe: path to the hook probe to test hook scripts execution
""" """
if sys.platform == 'win32': hook_path = pkg_resources.resource_filename('certbot_integration_tests', 'assets/hook.py')
extension = 'bat'
else:
extension = 'sh'
renewal_hooks_dirs = list_renewal_hooks_dirs(config_dir) for hook_dir in list_renewal_hooks_dirs(config_dir):
renewal_deploy_hook_path = os.path.join(renewal_hooks_dirs[1], 'hook.sh')
for hook_dir in renewal_hooks_dirs:
# We want an equivalent of bash `chmod -p $HOOK_DIR, that does not fail if one folder of # We want an equivalent of bash `chmod -p $HOOK_DIR, that does not fail if one folder of
# the hierarchy already exists. It is not the case of os.makedirs. Python 3 has an # the hierarchy already exists. It is not the case of os.makedirs. Python 3 has an
# optional parameter `exists_ok` to not fail on existing dir, but Python 2.7 does not. # optional parameter `exists_ok` to not fail on existing dir, but Python 2.7 does not.
@@ -137,26 +134,25 @@ def generate_test_file_hooks(config_dir, hook_probe):
except OSError as error: except OSError as error:
if error.errno != errno.EEXIST: if error.errno != errno.EEXIST:
raise raise
hook_path = os.path.join(hook_dir, 'hook.{0}'.format(extension))
if extension == 'sh':
data = '''\
#!/bin/bash -xe
if [ "$0" = "{0}" ]; then
if [ -z "$RENEWED_DOMAINS" -o -z "$RENEWED_LINEAGE" ]; then
echo "Environment variables not properly set!" >&2
exit 1
fi
fi
echo $(basename $(dirname "$0")) >> "{1}"\
'''.format(renewal_deploy_hook_path, hook_probe)
else:
# TODO: Write the equivalent bat file for Windows
data = '''\
''' if os.name != 'nt':
with open(hook_path, 'w') as file: entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.sh')
file.write(data) entrypoint_script = '''\
os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IEXEC) #!/usr/bin/env bash
set -e
"{0}" "{1}" "{2}" "{3}"
'''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)
else:
entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.bat')
entrypoint_script = '''\
@echo off
"{0}" "{1}" "{2}" "{3}"
'''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)
with open(entrypoint_script_path, 'w') as file_h:
file_h.write(entrypoint_script)
os.chmod(entrypoint_script_path, os.stat(entrypoint_script_path).st_mode | stat.S_IEXEC)
@contextlib.contextmanager @contextlib.contextmanager
@@ -193,7 +189,7 @@ for _ in range(0, 10):
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
pass pass
raise ValueError('Error, url did not respond after 10 attempts: {{0}}'.format(url)) raise ValueError('Error, url did not respond after 10 attempts: {{0}}'.format(url))
'''.format(http_server_root, http_port)) '''.format(http_server_root.replace('\\', '\\\\'), http_port))
os.chmod(auth_script_path, 0o755) os.chmod(auth_script_path, 0o755)
cleanup_script_path = os.path.join(tempdir, 'cleanup.py') cleanup_script_path = os.path.join(tempdir, 'cleanup.py')
@@ -204,7 +200,7 @@ import os
import shutil import shutil
well_known = os.path.join('{0}', '.well-known') well_known = os.path.join('{0}', '.well-known')
shutil.rmtree(well_known) shutil.rmtree(well_known)
'''.format(http_server_root)) '''.format(http_server_root.replace('\\', '\\\\')))
os.chmod(cleanup_script_path, 0o755) os.chmod(cleanup_script_path, 0o755)
yield ('{0} {1}'.format(sys.executable, auth_script_path), yield ('{0} {1}'.format(sys.executable, auth_script_path),
@@ -213,18 +209,6 @@ shutil.rmtree(well_known)
shutil.rmtree(tempdir) shutil.rmtree(tempdir)
def get_certbot_version():
"""
Find the version of the certbot available in PATH.
:return str: the certbot version
"""
output = subprocess.check_output(['certbot', '--version'],
universal_newlines=True, stderr=subprocess.STDOUT)
# Typical response is: output = 'certbot 0.31.0.dev0'
version_str = output.split(' ')[1].strip()
return LooseVersion(version_str)
def generate_csr(domains, key_path, csr_path, key_type=RSA_KEY_TYPE): def generate_csr(domains, key_path, csr_path, key_type=RSA_KEY_TYPE):
""" """
Generate a private key, and a CSR for the given domains using this key. Generate a private key, and a CSR for the given domains using this key.
@@ -287,4 +271,32 @@ def load_sample_data_path(workspace):
original = pkg_resources.resource_filename('certbot_integration_tests', 'assets/sample-config') original = pkg_resources.resource_filename('certbot_integration_tests', 'assets/sample-config')
copied = os.path.join(workspace, 'sample-config') copied = os.path.join(workspace, 'sample-config')
shutil.copytree(original, copied, symlinks=True) shutil.copytree(original, copied, symlinks=True)
if os.name == 'nt':
# Fix the symlinks on Windows since GIT is not creating them upon checkout
for lineage in ['a.encryption-example.com', 'b.encryption-example.com']:
current_live = os.path.join(copied, 'live', lineage)
for name in os.listdir(current_live):
if name != 'README':
current_file = os.path.join(current_live, name)
with open(current_file) as file_h:
src = file_h.read()
os.unlink(current_file)
os.symlink(os.path.join(current_live, src), current_file)
return copied return copied
def echo(keyword, path=None):
"""
Generate a platform independent executable command
that echoes the given keyword into the given file.
:param keyword: the keyword to echo (must be a single keyword)
:param path: path to the file were keyword is echoed
:return: the executable command
"""
if not re.match(r'^\w+$', keyword):
raise ValueError('Error, keyword `{0}` is not a single keyword.'
.format(keyword))
return '{0} -c "from __future__ import print_function; print(\'{1}\')"{2}'.format(
os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '')

View File

@@ -0,0 +1,53 @@
import json
import os
import stat
import pkg_resources
import requests
from certbot_integration_tests.utils.constants import MOCK_OCSP_SERVER_PORT
PEBBLE_VERSION = 'v2.2.1'
ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'assets')
def fetch(workspace):
suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe'
pebble_path = _fetch_asset('pebble', suffix)
challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix)
pebble_config_path = _build_pebble_config(workspace)
return pebble_path, challtestsrv_path, pebble_config_path
def _fetch_asset(asset, suffix):
asset_path = os.path.join(ASSETS_PATH, '{0}_{1}_{2}'.format(asset, PEBBLE_VERSION, suffix))
if not os.path.exists(asset_path):
asset_url = ('https://github.com/letsencrypt/pebble/releases/download/{0}/{1}_{2}'
.format(PEBBLE_VERSION, asset, suffix))
response = requests.get(asset_url)
response.raise_for_status()
with open(asset_path, 'wb') as file_h:
file_h.write(response.content)
os.chmod(asset_path, os.stat(asset_path).st_mode | stat.S_IEXEC)
return asset_path
def _build_pebble_config(workspace):
config_path = os.path.join(workspace, 'pebble-config.json')
with open(config_path, 'w') as file_h:
file_h.write(json.dumps({
'pebble': {
'listenAddress': '0.0.0.0:14000',
'managementListenAddress': '0.0.0.0:15000',
'certificate': os.path.join(ASSETS_PATH, 'cert.pem'),
'privateKey': os.path.join(ASSETS_PATH, 'key.pem'),
'httpPort': 5002,
'tlsPort': 5001,
'ocspResponderURL': 'http://127.0.0.1:{0}'.format(MOCK_OCSP_SERVER_PORT),
},
}))
return config_path

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python
"""
This runnable module interfaces itself with the Pebble management interface in order
to serve a mock OCSP responder during integration tests against Pebble.
"""
import datetime
import re
import requests
from dateutil import parser
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography import x509
from cryptography.x509 import ocsp
from six.moves import BaseHTTPServer
from certbot_integration_tests.utils.misc import GracefulTCPServer
from certbot_integration_tests.utils.constants import MOCK_OCSP_SERVER_PORT, PEBBLE_MANAGEMENT_URL
class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_POST(self):
request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0', verify=False)
issuer_key = serialization.load_pem_private_key(request.content, None, default_backend())
request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/0', verify=False)
issuer_cert = x509.load_pem_x509_certificate(request.content, default_backend())
try:
content_len = int(self.headers.getheader('content-length', 0))
except AttributeError:
content_len = int(self.headers.get('Content-Length'))
ocsp_request = ocsp.load_der_ocsp_request(self.rfile.read(content_len))
response = requests.get('{0}/cert-status-by-serial/{1}'.format(
PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')), verify=False)
if not response.ok:
ocsp_response = ocsp.OCSPResponseBuilder.build_unsuccessful(ocsp.OCSPResponseStatus.UNAUTHORIZED)
else:
data = response.json()
now = datetime.datetime.utcnow()
cert = x509.load_pem_x509_certificate(data['Certificate'].encode(), default_backend())
if data['Status'] != 'Revoked':
ocsp_status, revocation_time, revocation_reason = ocsp.OCSPCertStatus.GOOD, None, None
else:
ocsp_status, revocation_reason = ocsp.OCSPCertStatus.REVOKED, x509.ReasonFlags.unspecified
revoked_at = re.sub(r'( \+\d{4}).*$', r'\1', data['RevokedAt']) # "... +0000 UTC" => "+0000"
revocation_time = parser.parse(revoked_at)
ocsp_response = ocsp.OCSPResponseBuilder().add_response(
cert=cert, issuer=issuer_cert, algorithm=hashes.SHA1(),
cert_status=ocsp_status,
this_update=now, next_update=now + datetime.timedelta(hours=1),
revocation_time=revocation_time, revocation_reason=revocation_reason
).responder_id(
ocsp.OCSPResponderEncoding.NAME, issuer_cert
).sign(issuer_key, hashes.SHA256())
self.send_response(200)
self.end_headers()
self.wfile.write(ocsp_response.public_bytes(serialization.Encoding.DER))
if __name__ == '__main__':
try:
GracefulTCPServer(('', MOCK_OCSP_SERVER_PORT), _ProxyHandler).serve_forever()
except KeyboardInterrupt:
pass

View File

@@ -1,5 +1,7 @@
from setuptools import setup import sys
from setuptools import find_packages
from distutils.version import StrictVersion
from setuptools import setup, find_packages, __version__ as setuptools_version
version = '0.32.0.dev0' version = '0.32.0.dev0'
@@ -11,11 +13,23 @@ install_requires = [
'pytest', 'pytest',
'pytest-cov', 'pytest-cov',
'pytest-xdist', 'pytest-xdist',
'python-dateutil',
'pyyaml', 'pyyaml',
'requests', 'requests',
'six', 'six',
] ]
# Add pywin32 on Windows platforms to handle low-level system calls.
# This dependency needs to be added using environment markers to avoid its installation on Linux.
# However environment markers are supported only with setuptools >= 36.2.
# So this dependency is not added for old Linux distributions with old setuptools,
# in order to allow these systems to build certbot from sources.
if StrictVersion(setuptools_version) >= StrictVersion('36.2'):
install_requires.append("pywin32>=224 ; sys_platform == 'win32'")
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Error, you are trying to build certbot wheels using an old version '
'of setuptools. Version 36.2+ of setuptools is required.')
setup( setup(
name='certbot-ci', name='certbot-ci',
version=version, version=version,

View File

@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
install_requires = [ install_requires = [
'certbot', 'certbot',

View File

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

View File

@@ -22,7 +22,9 @@ Credentials
Use of this plugin requires a configuration file containing Cloudflare API Use of this plugin requires a configuration file containing Cloudflare API
credentials, obtained from your Cloudflare credentials, obtained from your Cloudflare
`account page <https://www.cloudflare.com/a/account/my-account>`_. `account page <https://www.cloudflare.com/a/account/my-account>`_. This plugin
does not currently support Cloudflare's "API Tokens", so please ensure you use
the "Global API Key" for authentication.
.. code-block:: ini .. code-block:: ini
:name: credentials.ini :name: credentials.ini

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -3,7 +3,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Please update tox.ini when modifying dependency version requirements # Please update tox.ini when modifying dependency version requirements
install_requires = [ install_requires = [

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -1,7 +1,7 @@
from setuptools import setup from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Please update tox.ini when modifying dependency version requirements # Please update tox.ini when modifying dependency version requirements
install_requires = [ install_requires = [

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -1,3 +1,3 @@
include LICENSE include LICENSE.txt
include README include README
recursive-include docs * recursive-include docs *

View File

@@ -1,7 +1,7 @@
from setuptools import setup from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Please update tox.ini when modifying dependency version requirements # Please update tox.ini when modifying dependency version requirements
install_requires = [ install_requires = [

View File

@@ -2,5 +2,4 @@ include LICENSE.txt
include README.rst include README.rst
recursive-include docs * recursive-include docs *
recursive-include certbot_nginx/tests/testdata * recursive-include certbot_nginx/tests/testdata *
include certbot_nginx/options-ssl-nginx.conf recursive-include certbot_nginx/tls_configs *.conf
include certbot_nginx/options-ssl-nginx-old.conf

View File

@@ -20,7 +20,6 @@ from certbot import crypto_util
from certbot import errors from certbot import errors
from certbot import interfaces from certbot import interfaces
from certbot import util from certbot import util
from certbot.compat import misc
from certbot.compat import os from certbot.compat import os
from certbot.plugins import common from certbot.plugins import common
@@ -128,7 +127,10 @@ class NginxConfigurator(common.Installer):
config_filename = "options-ssl-nginx.conf" config_filename = "options-ssl-nginx.conf"
if self.version < (1, 5, 9): if self.version < (1, 5, 9):
config_filename = "options-ssl-nginx-old.conf" config_filename = "options-ssl-nginx-old.conf"
return pkg_resources.resource_filename("certbot_nginx", config_filename) elif self.version < (1, 13, 0):
config_filename = "options-ssl-nginx-tls12-only.conf"
return pkg_resources.resource_filename(
"certbot_nginx", os.path.join("tls_configs", config_filename))
@property @property
def mod_ssl_conf(self): def mod_ssl_conf(self):
@@ -903,13 +905,9 @@ class NginxConfigurator(common.Installer):
have permissions of root. have permissions of root.
""" """
uid = misc.os_geteuid() util.make_or_verify_dir(self.config.work_dir, core_constants.CONFIG_DIRS_MODE)
util.make_or_verify_dir( util.make_or_verify_dir(self.config.backup_dir, core_constants.CONFIG_DIRS_MODE)
self.config.work_dir, core_constants.CONFIG_DIRS_MODE, uid) util.make_or_verify_dir(self.config.config_dir, core_constants.CONFIG_DIRS_MODE)
util.make_or_verify_dir(
self.config.backup_dir, core_constants.CONFIG_DIRS_MODE, uid)
util.make_or_verify_dir(
self.config.config_dir, core_constants.CONFIG_DIRS_MODE, uid)
def get_version(self): def get_version(self):
"""Return version of Nginx Server. """Return version of Nginx Server.

View File

@@ -23,10 +23,19 @@ UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-nginx-conf-digest.txt"
"""Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`.""" """Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`."""
SSL_OPTIONS_HASHES_NEW = [ SSL_OPTIONS_HASHES_NEW = [
'108c4555058a087496a3893aea5d9e1cee0f20a3085d44a52dc1a66522299ac3',
'd5e021706ecdccc7090111b0ae9a29ef61523e927f020e410caf0a1fd7063981',
]
"""SHA256 hashes of the contents of versions of MOD_SSL_CONF_SRC for nginx >= 1.13.0"""
SSL_OPTIONS_HASHES_MEDIUM = [
'63e2bddebb174a05c9d8a7cf2adf72f7af04349ba59a1a925fe447f73b2f1abf', '63e2bddebb174a05c9d8a7cf2adf72f7af04349ba59a1a925fe447f73b2f1abf',
'2901debc7ecbc10917edd9084c05464c9c5930b463677571eaf8c94bffd11ae2', '2901debc7ecbc10917edd9084c05464c9c5930b463677571eaf8c94bffd11ae2',
'30baca73ed9a5b0e9a69ea40e30482241d8b1a7343aa79b49dc5d7db0bf53b6c',
'02329eb19930af73c54b3632b3165d84571383b8c8c73361df940cb3894dd426',
] ]
"""SHA256 hashes of the contents of versions of MOD_SSL_CONF_SRC for nginx >= 1.5.9""" """SHA256 hashes of the contents of versions of MOD_SSL_CONF_SRC for nginx >= 1.5.9
and nginx < 1.13.0"""
ALL_SSL_OPTIONS_HASHES = [ ALL_SSL_OPTIONS_HASHES = [
'0f81093a1465e3d4eaa8b0c14e77b2a2e93568b0fc1351c2b87893a95f0de87c', '0f81093a1465e3d4eaa8b0c14e77b2a2e93568b0fc1351c2b87893a95f0de87c',
@@ -36,7 +45,8 @@ ALL_SSL_OPTIONS_HASHES = [
'394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d', '394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d',
'4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16', '4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16',
'c052ffff0ad683f43bffe105f7c606b339536163490930e2632a335c8d191cc4', 'c052ffff0ad683f43bffe105f7c606b339536163490930e2632a335c8d191cc4',
] + SSL_OPTIONS_HASHES_NEW '02329eb19930af73c54b3632b3165d84571383b8c8c73361df940cb3894dd426',
] + SSL_OPTIONS_HASHES_MEDIUM + SSL_OPTIONS_HASHES_NEW
"""SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC""" """SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC"""
def os_constant(key): def os_constant(key):

View File

@@ -1,13 +0,0 @@
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";

View File

@@ -1,14 +0,0 @@
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";

View File

@@ -427,11 +427,6 @@ class NginxConfiguratorTest(util.NginxTest):
mock_recovery_routine.side_effect = errors.ReverterError("foo") mock_recovery_routine.side_effect = errors.ReverterError("foo")
self.assertRaises(errors.PluginError, self.config.recovery_routine) self.assertRaises(errors.PluginError, self.config.recovery_routine)
@mock.patch("certbot.reverter.Reverter.view_config_changes")
def test_view_config_changes_throws_error_from_reverter(self, mock_view_config_changes):
mock_view_config_changes.side_effect = errors.ReverterError("foo")
self.assertRaises(errors.PluginError, self.config.view_config_changes)
@mock.patch("certbot.reverter.Reverter.rollback_checkpoints") @mock.patch("certbot.reverter.Reverter.rollback_checkpoints")
def test_rollback_checkpoints_throws_error_from_reverter(self, mock_rollback_checkpoints): def test_rollback_checkpoints_throws_error_from_reverter(self, mock_rollback_checkpoints):
mock_rollback_checkpoints.side_effect = errors.ReverterError("foo") mock_rollback_checkpoints.side_effect = errors.ReverterError("foo")
@@ -968,13 +963,40 @@ class InstallSslOptionsConfTest(util.NginxTest):
"Constants.ALL_SSL_OPTIONS_HASHES must be appended" "Constants.ALL_SSL_OPTIONS_HASHES must be appended"
" with the sha256 hash of self.config.mod_ssl_conf when it is updated.") " with the sha256 hash of self.config.mod_ssl_conf when it is updated.")
def test_old_nginx_version_uses_old_config(self): def test_ssl_config_files_hash_in_all_hashes(self):
"""
It is really critical that all TLS Nginx config files have their SHA256 hash registered in
constants.ALL_SSL_OPTIONS_HASHES. Otherwise Certbot will mistakenly assume that the config
file has been manually edited by the user, and will refuse to update it.
This test ensures that all necessary hashes are present.
"""
from certbot_nginx.constants import ALL_SSL_OPTIONS_HASHES
import pkg_resources
all_files = [
pkg_resources.resource_filename("certbot_nginx", os.path.join("tls_configs", x))
for x in ("options-ssl-nginx.conf",
"options-ssl-nginx-old.conf",
"options-ssl-nginx-tls12-only.conf")
]
self.assertTrue(all_files)
for one_file in all_files:
file_hash = crypto_util.sha256sum(one_file)
self.assertTrue(file_hash in ALL_SSL_OPTIONS_HASHES,
"Constants.ALL_SSL_OPTIONS_HASHES must be appended with the sha256 "
"hash of {0} when it is updated.".format(one_file))
def test_nginx_version_uses_correct_config(self):
self.config.version = (1, 5, 8) self.config.version = (1, 5, 8)
self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src), self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src),
"options-ssl-nginx-old.conf") "options-ssl-nginx-old.conf")
self._call() self._call()
self._assert_current_file() self._assert_current_file()
self.config.version = (1, 5, 9) self.config.version = (1, 5, 9)
self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src),
"options-ssl-nginx-tls12-only.conf")
self._call()
self._assert_current_file()
self.config.version = (1, 13, 0)
self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src), self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src),
"options-ssl-nginx.conf") "options-ssl-nginx.conf")

View File

@@ -30,8 +30,16 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
self.assertEqual(nparser.root, self.config_path) self.assertEqual(nparser.root, self.config_path)
def test_root_absolute(self): def test_root_absolute(self):
nparser = parser.NginxParser(os.path.relpath(self.config_path)) curr_dir = os.getcwd()
self.assertEqual(nparser.root, self.config_path) try:
# On Windows current directory may be on a different drive than self.tempdir.
# However a relative path between two different drives is invalid. So we move to
# self.tempdir to ensure that we stay on the same drive.
os.chdir(self.temp_dir)
nparser = parser.NginxParser(os.path.relpath(self.config_path))
self.assertEqual(nparser.root, self.config_path)
finally:
os.chdir(curr_dir)
def test_root_no_trailing_slash(self): def test_root_no_trailing_slash(self):
nparser = parser.NginxParser(self.config_path + os.path.sep) nparser = parser.NginxParser(self.config_path + os.path.sep)

View File

@@ -3,7 +3,6 @@ import copy
import shutil import shutil
import tempfile import tempfile
import unittest import unittest
import warnings
import josepy as jose import josepy as jose
import mock import mock
@@ -11,6 +10,7 @@ import pkg_resources
import zope.component import zope.component
from certbot import configuration from certbot import configuration
from certbot import util
from certbot.compat import os from certbot.compat import os
from certbot.plugins import common from certbot.plugins import common
from certbot.tests import util as test_util from certbot.tests import util as test_util
@@ -34,20 +34,16 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods
"rsa512_key.pem")) "rsa512_key.pem"))
def tearDown(self): def tearDown(self):
# On Windows we have various files which are not correctly closed at the time of tearDown. # Cleanup opened resources after a test. This is usually done through atexit handlers in
# For know, we log them until a proper file close handling is written. # Certbot, but during tests, atexit will not run registered functions before tearDown is
# Useful for development only, so no warning when we are on a CI process. # called and instead will run them right before the entire test process exits.
def onerror_handler(_, path, excinfo): # It is a problem on Windows, that does not accept to clean resources before closing them.
"""On error handler""" util._release_locks() # pylint: disable=protected-access
if not os.environ.get('APPVEYOR'): # pragma: no cover
message = ('Following error occurred when deleting path {0}'
'during tearDown process: {1}'.format(path, str(excinfo)))
warnings.warn(message)
shutil.rmtree(self.temp_dir, onerror=onerror_handler) shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir, onerror=onerror_handler) shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir, onerror=onerror_handler) shutil.rmtree(self.work_dir)
shutil.rmtree(self.logs_dir, onerror=onerror_handler) shutil.rmtree(self.logs_dir)
def get_data_filename(filename): def get_data_filename(filename):

View File

@@ -0,0 +1,13 @@
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers off;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

View File

@@ -0,0 +1,13 @@
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers off;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

View File

@@ -0,0 +1,13 @@
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

View File

@@ -1,3 +1,3 @@
# Remember to update setup.py to match the package versions below. # Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0 acme[dev]==0.29.0
certbot[dev]==0.34.0 certbot[dev]==0.36.0

View File

@@ -4,13 +4,13 @@ from setuptools.command.test import test as TestCommand
import sys import sys
version = '0.36.0.dev0' version = '0.38.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum # Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version. # acme/certbot version.
install_requires = [ install_requires = [
'acme>=0.29.0', 'acme>=0.29.0',
'certbot>=0.34.0', 'certbot>=0.35.0',
'mock', 'mock',
'PyOpenSSL', 'PyOpenSSL',
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?

View File

@@ -1,4 +1,4 @@
"""Certbot client.""" """Certbot client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2 # version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '0.36.0.dev0' __version__ = '0.38.0.dev0'

View File

@@ -20,7 +20,6 @@ from certbot import constants
from certbot import errors from certbot import errors
from certbot import interfaces from certbot import interfaces
from certbot import util from certbot import util
from certbot.compat import misc
from certbot.compat import os from certbot.compat import os
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -139,8 +138,7 @@ class AccountFileStorage(interfaces.AccountStorage):
""" """
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
util.make_or_verify_dir(config.accounts_dir, 0o700, misc.os_geteuid(), util.make_or_verify_dir(config.accounts_dir, 0o700, self.config.strict_permissions)
self.config.strict_permissions)
def _account_dir_path(self, account_id): def _account_dir_path(self, account_id):
return self._account_dir_path_for_server_path(account_id, self.config.server_path) return self._account_dir_path_for_server_path(account_id, self.config.server_path)
@@ -322,8 +320,7 @@ class AccountFileStorage(interfaces.AccountStorage):
def _save(self, account, acme, regr_only): def _save(self, account, acme, regr_only):
account_dir_path = self._account_dir_path(account.id) account_dir_path = self._account_dir_path(account.id)
util.make_or_verify_dir(account_dir_path, 0o700, misc.os_geteuid(), util.make_or_verify_dir(account_dir_path, 0o700, self.config.strict_permissions)
self.config.strict_permissions)
try: try:
with open(self._regr_path(account_dir_path), "w") as regr_file: with open(self._regr_path(account_dir_path), "w") as regr_file:
regr = account.regr regr = account.regr

View File

@@ -15,7 +15,6 @@ from certbot import interfaces
from certbot import ocsp from certbot import ocsp
from certbot import storage from certbot import storage
from certbot import util from certbot import util
from certbot.compat import misc
from certbot.compat import os from certbot.compat import os
from certbot.display import util as display_util from certbot.display import util as display_util
@@ -106,7 +105,7 @@ def lineage_for_certname(cli_config, certname):
"""Find a lineage object with name certname.""" """Find a lineage object with name certname."""
configs_dir = cli_config.renewal_configs_dir configs_dir = cli_config.renewal_configs_dir
# Verify the directory is there # Verify the directory is there
util.make_or_verify_dir(configs_dir, mode=0o755, uid=misc.os_geteuid()) util.make_or_verify_dir(configs_dir, mode=0o755)
try: try:
renewal_file = storage.renewal_file_for_certname(cli_config, certname) renewal_file = storage.renewal_file_for_certname(cli_config, certname)
except errors.CertStorageError: except errors.CertStorageError:
@@ -375,7 +374,7 @@ def _search_lineages(cli_config, func, initial_rv, *args):
""" """
configs_dir = cli_config.renewal_configs_dir configs_dir = cli_config.renewal_configs_dir
# Verify the directory is there # Verify the directory is there
util.make_or_verify_dir(configs_dir, mode=0o755, uid=misc.os_geteuid()) util.make_or_verify_dir(configs_dir, mode=0o755)
rv = initial_rv rv = initial_rv
for renewal_file in storage.renewal_conf_files(cli_config): for renewal_file in storage.renewal_conf_files(cli_config):

View File

@@ -1418,10 +1418,10 @@ def _plugins_parsing(helpful, plugins):
help="Authenticator plugin name.") help="Authenticator plugin name.")
helpful.add("plugins", "-i", "--installer", default=flag_default("installer"), helpful.add("plugins", "-i", "--installer", default=flag_default("installer"),
help="Installer plugin name (also used to find domains).") help="Installer plugin name (also used to find domains).")
helpful.add(["plugins", "certonly", "run", "install", "config_changes"], helpful.add(["plugins", "certonly", "run", "install"],
"--apache", action="store_true", default=flag_default("apache"), "--apache", action="store_true", default=flag_default("apache"),
help="Obtain and install certificates using Apache") help="Obtain and install certificates using Apache")
helpful.add(["plugins", "certonly", "run", "install", "config_changes"], helpful.add(["plugins", "certonly", "run", "install"],
"--nginx", action="store_true", default=flag_default("nginx"), "--nginx", action="store_true", default=flag_default("nginx"),
help="Obtain and install certificates using Nginx") help="Obtain and install certificates using Nginx")
helpful.add(["plugins", "certonly"], "--standalone", action="store_true", helpful.add(["plugins", "certonly"], "--standalone", action="store_true",

View File

@@ -30,7 +30,6 @@ from certbot import interfaces
from certbot import reverter from certbot import reverter
from certbot import storage from certbot import storage
from certbot import util from certbot import util
from certbot.compat import misc
from certbot.compat import os from certbot.compat import os
from certbot.display import enhancements from certbot.display import enhancements
from certbot.display import ops as display_ops from certbot.display import ops as display_ops
@@ -459,9 +458,7 @@ class Client(object):
""" """
for path in cert_path, chain_path, fullchain_path: for path in cert_path, chain_path, fullchain_path:
util.make_or_verify_dir( util.make_or_verify_dir(os.path.dirname(path), 0o755, self.config.strict_permissions)
os.path.dirname(path), 0o755, misc.os_geteuid(),
self.config.strict_permissions)
cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path)
@@ -627,7 +624,7 @@ class Client(object):
reporter.add_message( reporter.add_message(
"An error occurred and we failed to restore your config and " "An error occurred and we failed to restore your config and "
"restart your server. Please post to " "restart your server. Please post to "
"https://community.letsencrypt.org/c/server-config " "https://community.letsencrypt.org/c/help "
"with details about your configuration and this error you received.", "with details about your configuration and this error you received.",
reporter.HIGH_PRIORITY) reporter.HIGH_PRIORITY)
raise raise

31
certbot/compat/_path.py Normal file
View File

@@ -0,0 +1,31 @@
"""This compat module wraps os.path to forbid some functions."""
# pylint: disable=function-redefined
from __future__ import absolute_import
# First round of wrapping: we import statically all public attributes exposed by the os.path
# module. This allows in particular to have pylint, mypy, IDEs be aware that most of os.path
# members are available in certbot.compat.path.
from os.path import * # type: ignore # pylint: disable=wildcard-import,unused-wildcard-import,redefined-builtin,os-module-forbidden
# Second round of wrapping: we import dynamically all attributes from the os.path module that have
# not yet been imported by the first round (static star import).
import os.path as std_os_path # pylint: disable=os-module-forbidden
import sys as std_sys
ourselves = std_sys.modules[__name__]
for attribute in dir(std_os_path):
# Check if the attribute does not already exist in our module. It could be internal attributes
# of the module (__name__, __doc__), or attributes from standard os.path already imported with
# `from os.path import *`.
if not hasattr(ourselves, attribute):
setattr(ourselves, attribute, getattr(std_os_path, attribute))
# Clean all remaining importables that are not from the core os.path module.
del ourselves, std_os_path, std_sys
# Function os.path.realpath is broken on some versions of Python for Windows.
def realpath(*unused_args, **unused_kwargs):
"""Method os.path.realpath() is forbidden"""
raise RuntimeError('Usage of os.path.realpath() is forbidden. '
'Use certbot.compat.filesystem.realpath() instead.')

View File

@@ -1,12 +1,20 @@
"""Compat module to handle files security on Windows and Linux""" """Compat module to handle files security on Windows and Linux"""
from __future__ import absolute_import from __future__ import absolute_import
import errno
import os # pylint: disable=os-module-forbidden import os # pylint: disable=os-module-forbidden
import stat import stat
try: try:
import ntsecuritycon # pylint: disable=import-error # pylint: disable=import-error
import win32security # pylint: disable=import-error import ntsecuritycon
import win32security
import win32con
import win32api
import win32file
import pywintypes
import winerror
# pylint: enable=import-error
except ImportError: except ImportError:
POSIX_MODE = True POSIX_MODE = True
else: else:
@@ -36,6 +44,209 @@ def chmod(file_path, mode):
_apply_win_mode(file_path, mode) _apply_win_mode(file_path, mode)
# One could ask why there is no copy_ownership() function, or even a reimplementation
# of os.chown() that would modify the ownership of file without touching the mode itself.
# This is because on Windows, it would require recalculating the existing DACL against
# the new owner, since the DACL is composed of ACEs that targets a specific user, not dynamically
# the current owner of a file. This action would be necessary to keep consistency between
# the POSIX mode applied to the file and the current owner of this file.
# Since copying and editing arbitrary DACL is very difficult, and since we actually know
# the mode to apply at the time the owner of a file should change, it is easier to just
# change the owner, then reapply the known mode, as copy_ownership_and_apply_mode() does.
def copy_ownership_and_apply_mode(src, dst, mode, copy_user, copy_group):
# type: (str, str, int, bool, bool) -> None
"""
Copy ownership (user and optionally group on Linux) from the source to the
destination, then apply given mode in compatible way for Linux and Windows.
This replaces the os.chown command.
:param str src: Path of the source file
:param str dst: Path of the destination file
:param int mode: Permission mode to apply on the destination file
:param bool copy_user: Copy user if `True`
:param bool copy_group: Copy group if `True` on Linux (has no effect on Windows)
"""
if POSIX_MODE:
stats = os.stat(src)
user_id = stats.st_uid if copy_user else -1
group_id = stats.st_gid if copy_group else -1
os.chown(dst, user_id, group_id)
elif copy_user:
# There is no group handling in Windows
_copy_win_ownership(src, dst)
chmod(dst, mode)
def check_mode(file_path, mode):
# type: (str, int) -> bool
"""
Check if the given mode matches the permissions of the given file.
On Linux, will make a direct comparison, on Windows, mode will be compared against
the security model.
:param str file_path: Path of the file
:param int mode: POSIX mode to test
:rtype: bool
:return: True if the POSIX mode matches the file permissions
"""
if POSIX_MODE:
return stat.S_IMODE(os.stat(file_path).st_mode) == mode
return _check_win_mode(file_path, mode)
def check_owner(file_path):
# type: (str) -> bool
"""
Check if given file is owned by current user.
:param str file_path: File path to check
:rtype: bool
:return: True if given file is owned by current user, False otherwise.
"""
if POSIX_MODE:
return os.stat(file_path).st_uid == os.getuid()
# Get owner sid of the file
security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION)
user = security.GetSecurityDescriptorOwner()
# Compare sids
return _get_current_user() == user
def check_permissions(file_path, mode):
# type: (str, int) -> bool
"""
Check if given file has the given mode and is owned by current user.
:param str file_path: File path to check
:param int mode: POSIX mode to check
:rtype: bool
:return: True if file has correct mode and owner, False otherwise.
"""
return check_owner(file_path) and check_mode(file_path, mode)
def open(file_path, flags, mode=0o777): # pylint: disable=redefined-builtin
# type: (str, int, int) -> int
"""
Wrapper of original os.open function, that will ensure on Windows that given mode
is correctly applied.
:param str file_path: The file path to open
:param int flags: Flags to apply on file while opened
:param int mode: POSIX mode to apply on file when opened,
Python defaults will be applied if ``None``
:returns: the file descriptor to the opened file
:rtype: int
:raise: OSError(errno.EEXIST) if the file already exists and os.O_CREAT & os.O_EXCL are set,
OSError(errno.EACCES) on Windows if the file already exists and is a directory, and
os.O_CREAT is set.
"""
if POSIX_MODE:
# On Linux, invoke os.open directly.
return os.open(file_path, flags, mode)
# Windows: handle creation of the file atomically with proper permissions.
if flags & os.O_CREAT:
# If os.O_EXCL is set, we will use the "CREATE_NEW", that will raise an exception if
# file exists, matching the API contract of this bit flag. Otherwise, we use
# "CREATE_ALWAYS" that will always create the file whether it exists or not.
disposition = win32con.CREATE_NEW if flags & os.O_EXCL else win32con.CREATE_ALWAYS
attributes = win32security.SECURITY_ATTRIBUTES()
security = attributes.SECURITY_DESCRIPTOR
user = _get_current_user()
dacl = _generate_dacl(user, mode)
# We set second parameter to 0 (`False`) to say that this security descriptor is
# NOT constructed from a default mechanism, but is explicitly set by the user.
# See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-setsecuritydescriptorowner # pylint: disable=line-too-long
security.SetSecurityDescriptorOwner(user, 0)
# We set first parameter to 1 (`True`) to say that this security descriptor contains
# a DACL. Otherwise second and third parameters are ignored.
# We set third parameter to 0 (`False`) to say that this security descriptor is
# NOT constructed from a default mechanism, but is explicitly set by the user.
# See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-setsecuritydescriptordacl # pylint: disable=line-too-long
security.SetSecurityDescriptorDacl(1, dacl, 0)
handle = None
try:
handle = win32file.CreateFile(file_path, win32file.GENERIC_READ,
win32file.FILE_SHARE_READ & win32file.FILE_SHARE_WRITE,
attributes, disposition, 0, None)
except pywintypes.error as err:
# Handle native windows errors into python errors to be consistent with the API
# of os.open in the situation of a file already existing or locked.
if err.winerror == winerror.ERROR_FILE_EXISTS:
raise OSError(errno.EEXIST, err.strerror)
if err.winerror == winerror.ERROR_SHARING_VIOLATION:
raise OSError(errno.EACCES, err.strerror)
raise err
finally:
if handle:
handle.Close()
# At this point, the file that did not exist has been created with proper permissions,
# so os.O_CREAT and os.O_EXCL are not needed anymore. We remove them from the flags to
# avoid a FileExists exception before calling os.open.
return os.open(file_path, flags ^ os.O_CREAT ^ os.O_EXCL)
# Windows: general case, we call os.open, let exceptions be thrown, then chmod if all is fine.
handle = os.open(file_path, flags)
chmod(file_path, mode)
return handle
def makedirs(file_path, mode=0o777):
# type: (str, int) -> None
"""
Rewrite of original os.makedirs function, that will ensure on Windows that given mode
is correctly applied.
:param str file_path: The file path to open
:param int mode: POSIX mode to apply on leaf directory when created, Python defaults
will be applied if ``None``
"""
if POSIX_MODE:
return os.makedirs(file_path, mode)
orig_mkdir_fn = os.mkdir
try:
# As we know that os.mkdir is called internally by os.makedirs, we will swap the function in
# os module for the time of makedirs execution on Windows.
os.mkdir = mkdir # type: ignore
return os.makedirs(file_path, mode)
finally:
os.mkdir = orig_mkdir_fn
def mkdir(file_path, mode=0o777):
# type: (str, int) -> None
"""
Rewrite of original os.mkdir function, that will ensure on Windows that given mode
is correctly applied.
:param str file_path: The file path to open
:param int mode: POSIX mode to apply on directory when created, Python defaults
will be applied if ``None``
"""
if POSIX_MODE:
return os.mkdir(file_path, mode)
attributes = win32security.SECURITY_ATTRIBUTES()
security = attributes.SECURITY_DESCRIPTOR
user = _get_current_user()
dacl = _generate_dacl(user, mode)
security.SetSecurityDescriptorOwner(user, False)
security.SetSecurityDescriptorDacl(1, dacl, 0)
try:
win32file.CreateDirectory(file_path, attributes)
except pywintypes.error as err:
# Handle native windows error into python error to be consistent with the API
# of os.mkdir in the situation of a directory already existing.
if err.winerror == winerror.ERROR_ALREADY_EXISTS:
raise OSError(errno.EEXIST, err.strerror, file_path, err.winerror)
raise err
return None
def replace(src, dst): def replace(src, dst):
# type: (str, str) -> None # type: (str, str) -> None
""" """
@@ -52,13 +263,22 @@ def replace(src, dst):
os.rename(src, dst) os.rename(src, dst)
def _apply_win_mode(file_path, mode): def realpath(file_path):
""" """
This function converts the given POSIX mode into a Windows ACL list, and applies it to the Find the real path for the given path. This method resolves symlinks, including
file given its path. If the given path is a symbolic link, it will resolved to apply the recursive symlinks, and is protected against symlinks that creates an infinite loop.
mode on the targeted file.
""" """
original_path = file_path original_path = file_path
if POSIX_MODE:
path = os.path.realpath(file_path)
if os.path.islink(path):
# If path returned by realpath is still a link, it means that it failed to
# resolve the symlink because of a loop.
# See realpath code: https://github.com/python/cpython/blob/master/Lib/posixpath.py
raise RuntimeError('Error, link {0} is a loop!'.format(original_path))
return path
inspected_paths = [] # type: List[str] inspected_paths = [] # type: List[str]
while os.path.islink(file_path): while os.path.islink(file_path):
link_path = file_path link_path = file_path
@@ -68,6 +288,53 @@ def _apply_win_mode(file_path, mode):
if file_path in inspected_paths: if file_path in inspected_paths:
raise RuntimeError('Error, link {0} is a loop!'.format(original_path)) raise RuntimeError('Error, link {0} is a loop!'.format(original_path))
inspected_paths.append(file_path) inspected_paths.append(file_path)
return os.path.abspath(file_path)
# On Windows is_executable run from an unprivileged shell may claim that a path is
# executable when it is excutable only if run from a privileged shell. This result
# is due to the fact that GetEffectiveRightsFromAcl calculate effective rights
# without taking into consideration if the target user has currently required the
# elevated privileges or not. However this is not a problem since certbot always
# requires to be run under a privileged shell, so the user will always benefit
# from the highest (privileged one) set of permissions on a given file.
def is_executable(path):
"""
Is path an executable file?
:param str path: path to test
:returns: True if path is an executable file
:rtype: bool
"""
if POSIX_MODE:
return os.path.isfile(path) and os.access(path, os.X_OK)
return _win_is_executable(path)
def _win_is_executable(path):
if not os.path.isfile(path):
return False
security = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
mode = dacl.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': _get_current_user(),
})
return mode & ntsecuritycon.FILE_GENERIC_EXECUTE == ntsecuritycon.FILE_GENERIC_EXECUTE
def _apply_win_mode(file_path, mode):
"""
This function converts the given POSIX mode into a Windows ACL list, and applies it to the
file given its path. If the given path is a symbolic link, it will resolved to apply the
mode on the targeted file.
"""
file_path = realpath(file_path)
# Get owner sid of the file # Get owner sid of the file
security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION) security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION)
user = security.GetSecurityDescriptorOwner() user = security.GetSecurityDescriptorOwner()
@@ -129,6 +396,18 @@ def _analyze_mode(mode):
} }
def _copy_win_ownership(src, dst):
security_src = win32security.GetFileSecurity(src, win32security.OWNER_SECURITY_INFORMATION)
user_src = security_src.GetSecurityDescriptorOwner()
security_dst = win32security.GetFileSecurity(dst, win32security.OWNER_SECURITY_INFORMATION)
# Second parameter indicates, if `False`, that the owner of the file is not provided by some
# default mechanism, but is explicitly set instead. This is obviously what we are doing here.
security_dst.SetSecurityDescriptorOwner(user_src, False)
win32security.SetFileSecurity(dst, win32security.OWNER_SECURITY_INFORMATION, security_dst)
def _generate_windows_flags(rights_desc): def _generate_windows_flags(rights_desc):
# Some notes about how each POSIX right is interpreted. # Some notes about how each POSIX right is interpreted.
# #
@@ -166,6 +445,28 @@ def _generate_windows_flags(rights_desc):
return flag return flag
def _check_win_mode(file_path, mode):
# Resolve symbolic links
file_path = realpath(file_path)
# Get current dacl file
security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION
| win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
# Get current file owner sid
user = security.GetSecurityDescriptorOwner()
if not dacl:
# No DACL means full control to everyone
# This is not a deterministic permissions set.
return False
# Calculate the target dacl
ref_dacl = _generate_dacl(user, mode)
return _compare_dacls(dacl, ref_dacl)
def _compare_dacls(dacl1, dacl2): def _compare_dacls(dacl1, dacl2):
""" """
This method compare the two given DACLs to check if they are identical. This method compare the two given DACLs to check if they are identical.
@@ -173,3 +474,17 @@ def _compare_dacls(dacl1, dacl2):
""" """
return ([dacl1.GetAce(index) for index in range(0, dacl1.GetAceCount())] == return ([dacl1.GetAce(index) for index in range(0, dacl1.GetAceCount())] ==
[dacl2.GetAce(index) for index in range(0, dacl2.GetAceCount())]) [dacl2.GetAce(index) for index in range(0, dacl2.GetAceCount())])
def _get_current_user():
"""
Return the pySID corresponding to the current user.
"""
account_name = win32api.GetUserNameEx(win32api.NameSamCompatible)
# LookupAccountName() expects the system name as first parameter. By passing None to it,
# we instruct Windows to first search the matching account in the machine local accounts,
# then into the primary domain accounts, if the machine has joined a domain, then finally
# into the trusted domains accounts. This is the preferred lookup mechanism to use in Windows
# if there is no reason to use a specific lookup mechanism.
# See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-lookupaccountnamea
return win32security.LookupAccountName(None, account_name)[0]

View File

@@ -17,6 +17,23 @@ except ImportError: # pragma: no cover
from certbot import errors from certbot import errors
from certbot.compat import os from certbot.compat import os
# MASK_FOR_PRIVATE_KEY_PERMISSIONS defines what are the permissions flags to keep
# when transferring the permissions from an old private key to a new one.
if POSIX_MODE:
# On Linux, we keep read/write/execute permissions
# for group and read permissions for everybody.
MASK_FOR_PRIVATE_KEY_PERMISSIONS = stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH
else:
# On Windows, the mode returned by os.stat is not reliable,
# so we do not keep any permission from the previous private key.
MASK_FOR_PRIVATE_KEY_PERMISSIONS = 0
# For Linux: define OS specific standard binary directories
STANDARD_BINARY_DIRS = ["/usr/sbin", "/usr/local/bin", "/usr/local/sbin"] if POSIX_MODE else []
def raise_for_non_administrative_windows_rights(): def raise_for_non_administrative_windows_rights():
# type: () -> None # type: () -> None
""" """
@@ -29,22 +46,6 @@ def raise_for_non_administrative_windows_rights():
raise errors.Error('Error, certbot must be run on a shell with administrative rights.') raise errors.Error('Error, certbot must be run on a shell with administrative rights.')
def os_geteuid():
"""
Get current user uid
:returns: The current user uid.
:rtype: int
"""
try:
# Linux specific
return os.geteuid()
except AttributeError:
# Windows specific
return 0
def readline_with_timeout(timeout, prompt): def readline_with_timeout(timeout, prompt):
# type: (float, str) -> str # type: (float, str) -> str
""" """
@@ -75,16 +76,6 @@ def readline_with_timeout(timeout, prompt):
return sys.stdin.readline() return sys.stdin.readline()
def compare_file_modes(mode1, mode2):
"""Return true if the two modes can be considered as equals for this platform"""
if os.name != 'nt':
# Linux specific: standard compare
return oct(stat.S_IMODE(mode1)) == oct(stat.S_IMODE(mode2))
# Windows specific: most of mode bits are ignored on Windows. Only check user R/W rights.
return (stat.S_IMODE(mode1) & stat.S_IREAD == stat.S_IMODE(mode2) & stat.S_IREAD
and stat.S_IMODE(mode1) & stat.S_IWRITE == stat.S_IMODE(mode2) & stat.S_IWRITE)
WINDOWS_DEFAULT_FOLDERS = { WINDOWS_DEFAULT_FOLDERS = {
'config': 'C:\\Certbot', 'config': 'C:\\Certbot',
'work': 'C:\\Certbot\\lib', 'work': 'C:\\Certbot\\lib',

View File

@@ -17,6 +17,7 @@ from os import * # type: ignore # pylint: disable=wildcard-import,unused-wildc
# and so not in `from os import *`. # and so not in `from os import *`.
import os as std_os # pylint: disable=os-module-forbidden import os as std_os # pylint: disable=os-module-forbidden
import sys as std_sys import sys as std_sys
ourselves = std_sys.modules[__name__] ourselves = std_sys.modules[__name__]
for attribute in dir(std_os): for attribute in dir(std_os):
# Check if the attribute does not already exist in our module. It could be internal attributes # Check if the attribute does not already exist in our module. It could be internal attributes
@@ -25,7 +26,9 @@ for attribute in dir(std_os):
if not hasattr(ourselves, attribute): if not hasattr(ourselves, attribute):
setattr(ourselves, attribute, getattr(std_os, attribute)) setattr(ourselves, attribute, getattr(std_os, attribute))
# Similar to os.path, allow certbot.compat.os.path to behave as a module # Import our internal path module, then allow certbot.compat.os.path
# to behave as a module (similarly to os.path).
from certbot.compat import _path as path # type: ignore # pylint: disable=wrong-import-position
std_sys.modules[__name__ + '.path'] = path std_sys.modules[__name__ + '.path'] = path
# Clean all remaining importables that are not from the core os module. # Clean all remaining importables that are not from the core os module.
@@ -45,12 +48,51 @@ del ourselves, std_os, std_sys
# Basically, it states that appropriate permissions will be set for the owner, nothing for the # Basically, it states that appropriate permissions will be set for the owner, nothing for the
# group, appropriate permissions for the "Everyone" group, and all permissions to the # group, appropriate permissions for the "Everyone" group, and all permissions to the
# "Administrators" group + "System" user, as they can do everything anyway. # "Administrators" group + "System" user, as they can do everything anyway.
def chmod(*unused_args, **unused_kwargs): # pylint: disable=function-redefined def chmod(*unused_args, **unused_kwargs):
"""Method os.chmod() is forbidden""" """Method os.chmod() is forbidden"""
raise RuntimeError('Usage of os.chmod() is forbidden. ' raise RuntimeError('Usage of os.chmod() is forbidden. '
'Use certbot.compat.filesystem.chmod() instead.') 'Use certbot.compat.filesystem.chmod() instead.')
# Because uid is not a concept on Windows, chown is useless. In fact, it is not even available
# on Python for Windows. So to be consistent on both platforms for Certbot, this method is
# always forbidden.
def chown(*unused_args, **unused_kwargs):
"""Method os.chown() is forbidden"""
raise RuntimeError('Usage of os.chown() is forbidden.'
'Use certbot.compat.filesystem.copy_ownership_and_apply_mode() instead.')
# The os.open function on Windows has the same effect as a call to os.chown concerning the file
# modes: these modes lack the correct control over the permissions given to the file. Instead,
# filesystem.open invokes the Windows native API `CreateFile` to ensure that permissions are
# atomically set in case of file creation, or invokes filesystem.chmod to properly set the
# permissions for the other cases.
def open(*unused_args, **unused_kwargs):
"""Method os.open() is forbidden"""
raise RuntimeError('Usage of os.open() is forbidden. '
'Use certbot.compat.filesystem.open() instead.')
# Very similarly to os.open, os.mkdir has the same effects on Windows and creates an unsecured
# folder. So a similar mitigation to security.chmod is provided on this platform.
def mkdir(*unused_args, **unused_kwargs):
"""Method os.mkdir() is forbidden"""
raise RuntimeError('Usage of os.mkdir() is forbidden. '
'Use certbot.compat.filesystem.mkdir() instead.')
# As said above, os.makedirs would call the original os.mkdir function recursively on Windows,
# creating the same flaws for every actual folder created. This method is modified to ensure
# that our modified os.mkdir is called on Windows, by monkey patching temporarily the mkdir method
# on the original os module, executing the modified logic to correctly protect newly created
# folders, then restoring original mkdir method in the os module.
def makedirs(*unused_args, **unused_kwargs):
"""Method os.makedirs() is forbidden"""
raise RuntimeError('Usage of os.makedirs() is forbidden. '
'Use certbot.compat.filesystem.makedirs() instead.')
# Because of the blocking strategy on file handlers on Windows, rename does not behave as expected # Because of the blocking strategy on file handlers on Windows, rename does not behave as expected
# with POSIX systems: an exception will be raised if dst already exists. # with POSIX systems: an exception will be raised if dst already exists.
def rename(*unused_args, **unused_kwargs): def rename(*unused_args, **unused_kwargs):
@@ -65,3 +107,12 @@ def replace(*unused_args, **unused_kwargs):
"""Method os.replace() is forbidden""" """Method os.replace() is forbidden"""
raise RuntimeError('Usage of os.replace() is forbidden. ' raise RuntimeError('Usage of os.replace() is forbidden. '
'Use certbot.compat.filesystem.replace() instead.') 'Use certbot.compat.filesystem.replace() instead.')
# Results given by os.access are inconsistent or partial on Windows, because this platform is not
# following the POSIX approach.
def access(*unused_args, **unused_kwargs):
"""Method os.access() is forbidden"""
raise RuntimeError('Usage of os.access() is forbidden. '
'Use certbot.compat.filesystem.check_mode() or '
'certbot.compat.filesystem.is_executable() instead.')

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