Compare commits

...

161 Commits

Author SHA1 Message Date
Erik Rose
0a43883d81 Switch to the USTC mirror because it doesn't require SNI.
Old Python versions don't understand SNI and raise SNIMissingWarning on connecting.
2017-10-04 09:59:29 -04:00
Erik Rose
32b0727a32 Teach certbot about certbot-auto's new --pypi-mirror option so it doesn't throw an unknown-arg error. 2017-10-03 14:46:37 -04:00
Erik Rose
65bd307905 Let certbot-auto work behind the Great Firewall of China. Fix #3222. Ref #4371.
...without needing to get pip >= 8 by hand first and without needing to muck with pip.conf.

We do it by upgrading to a new version of pipstrap which understands pip's own PIP_INDEX_URL env var for specifying a mirror. That done, certbot-auto simply sets that var to a known Chinese mirror in response to a CLI flag, and off we go.

This could be made automatic in the future: see 2nd-last paragraph of https://github.com/erikrose/pipstrap/pull/10#issuecomment-333637736. But there's no reason not to land this simple version first.
2017-10-02 17:27:23 -04:00
Brad Warren
cad7d4c8ed Update master to reflect 0.18.2 (#5127)
* Release 0.18.2

(cherry picked from commit d031c42b98)

* Bump version to 0.19.0
2017-09-27 16:02:40 -07:00
Joona Hoikkala
ba84b7ab49 Add test to prevent regressions of #4183 (#5134) 2017-09-27 15:51:28 -07:00
ohemorange
7412099567 Allow multiple interactive certname selections in certbot delete (#5133) 2017-09-27 15:47:40 -07:00
r5d
85deca588f Stop using print in certbot.main module. (#5121)
* Stop using print in `certbot.main` module.

* Update certbot.main.plugins_cmd` function.

* Update test methods `test_plugins_no_args`,
`test_plugins_no_args_unprivileged`, `test_plugins_init` and
`test_plugins_prepare` in `cerbot.tests.MainTest` class.

Addresses #3720.

* certbot: Add `patch_get_utility_with_stdout` function.

* Add functions `certbot.tests.util.patch_get_utility_with_stdout`
  and `certbot.tests.util._create_get_utility_mock_with_stdout`.

* certbot: tests: Update tests in MainTest.

* Update methods `test_plugins_no_args`,
`test_plugins_no_args_unprivileged`, and `test_plugins_init`,
`test_plugins_prepare` to use `patch_get_utility_with_stdout`.

* certbot: tests: Update _create_get_utility_mock_with_stdout.

* Update certbot.tests.util._create_get_utility_mock_with_stdout
  function. The mock function for all IDisplay methods, except
  `notification` method, calls _write_msg and _assert_valid_call.

* certbot: tests: Update `patch_get_utility_with_stdout`

* Update doc string.
* Argument stdout's default value is None now.

* certbot: tests: Update util._create_get_utility_mock_with_stdout.
2017-09-25 18:42:31 -07:00
Brad Warren
8b7d6c4ea3 Update changelog for 0.18.2 (#5128) 2017-09-25 16:46:04 -07:00
Christian Becker
36d5221bac certbot-dns-google: enable automatic credential lookup on google cloud (#5117)
- when no credentials are passed it will try to get valid credentials
using the google metadata service
- this is a feature of the google SDK, so we don't need to handle that
explicitly
- previous behaviour with a credentials file is retained
2017-09-25 12:17:15 -07:00
Joona Hoikkala
1ce813c3cc Do not parse disabled configuration files from under sites-available on Debian / Ubuntu (#4104)
This changes the apache plugin behaviour to only parse enabled configuration files and respecting the --apache-vhost-root CLI parameter for new SSL vhost creation. If --apache-vhost-root isn't defined, or doesn't exist, the SSL vhost will be created to originating non-SSL vhost directory.

This PR also implements actual check for vhost enabled state, and makes sure parser.parse_file() does not discard changes in Augeas DOM, by doing an autosave.

Also handles enabling the new SSL vhost, if it's on a path that's not parsed by Apache.

Fixes: #1328
Fixes: #3545
Fixes: #3791
Fixes: #4523
Fixes: #4837
Fixes: #4905

* First changes

* Handle rest of the errors

* Test fixes

* Final fixes

* Make parse_files accessible and fix linter problems

* Activate vhost at later time

* Cleanup

* Add a new test case, and fix old

* Enable site later in deploy_cert

* Make apache-conf-test default dummy configuration enabled

* Remove is_sites_available as obsolete

* Cleanup

* Brought back conditional vhost_path parsing

* Parenthesis

* Fix merge leftovers

* Fix to work with the recent changes to new file creation

* Added fix and tests for non-symlink vhost in sites-enabled

* Made vhostroot parameter for ApacheParser optional, and removed extra_path

* Respect vhost-root, and add Include statements to root configuration if needed

* Fixed site enabling order to prevent apache restart error while enabling mod_ssl

* Don't exclude Ubuntu / Debian vhost-root cli argument

* Changed the SSL vhost directory selection priority

* Requested fixes for paths and vhost discovery

* Make sure the Augeas DOM is written to disk before loading new files

* Actual checking for if the file is parsed within existing Apache configuration

* Fix the order of dummy SSL directives addition and enabling modules

* Restructured site_enabled checks

* Enabling vhost correctly for non-debian systems
2017-09-25 12:03:09 -07:00
Noah Swartz
ade01d618b add info about -d (#5097) 2017-09-21 08:52:01 -07:00
Michał Zegan
5a4028c763 fix dns-rfc2136 plugin not respecting cnames (#5101)
* fix dns-rfc2136 plugin not respecting cnames

The plugin does not work if the domain of a certificate is found to have a cname record in dns.
That is because when plugin tries to find zone boundary, it searches from the domain up for the SOA record, and each DNS response is checked for the answer being empty, assuming that empty answer means no SOA record is present and the higher level domain has to be checked, and non empty answer section means that this domain is a zone root.
However, if the initial domain, or any upper level domain except the zone root has a cname record pointing to the zone root, then the server will, instead of returning an empty answer, return one containing two records, first a cname pointing to the zone root, then the SOA record of zone root, and that will make the check fail and use a wrong domain as a zone name during update.
Fix that by replacing a check for empty answer with explicitly searching in response's answer section for a SOA record matching the domain that is being checked.

* dns-rfc2136: fix lint errors
2017-09-20 11:29:48 -07:00
yomna
48fd7ee260 Updating the AWS letstest documentation (#5091)
* Better documentation for working w/ AWS.

* Addressing feedback.

* profile name -> key name
2017-09-19 10:25:36 -07:00
Brad Warren
6aabb31eb5 Merge pull request #5118 from erikrose/certbot-auto-timeout
Certbot auto timeout
2017-09-18 15:56:26 -07:00
Noah Swartz
3acde31ed3 Merge pull request #5096 from certbot/0.18.1-release-notes
Add 0.18.1 release notes
2017-09-18 13:45:01 -07:00
Erik Rose
e7884898ec Simplify and stop repeating knowledge by hard-coding timeout into HttpsGetter.get().
Also, switch timeout to 30 so it has every opportunity to actually work, even in bad network weather. (I posit that people are used to 30-second timeouts.)

Stop catching URLError explicitly, since it's a subclass of the already-caught IOError.
2017-09-18 09:55:16 -04:00
Chris J
9be4fedeec Add timeout to certbot-auto HTTPS fetches. Fix #4473. 2017-09-18 09:52:17 -04:00
Chris Julian
f0caf5b04f #4435. CLI Argument Default Organization (#5037)
* Enhancement #4435. Organizing defaults in prepare_and_parse_args()

* Playing fast and loose with tox.

Discovered screwy case involving flag_default returning empty list (domains)

* Setting defaults for more low-hanging fruit. Some caveats remain.

* key_path default to None

* Applying PR feedback: explicit defaults even where redundant

* Obsessive quote consistency

* Set testing config path arguments to a 'certonly' default

* Copy the default domains list rather than get reference

* Build a testing Config from CLI_DEFAULTS

* Update some email tests for use with defaults in config.

config.email and config.noninteractive_mode in these tests
used to be magic-mock'd, so were True-ish. The default
email is now None and default noninteractive_mode is
False, so update in tests accordingly.

* Lint...

* Copy anything retrieved using flag_defaults. Apply this to test_cli_ini_domains too.

* Put those quotes back. Backslashes are just the worst.

* Remove vestigial line

* A test to ensure no regressions around modifying CLI_DEFAULTS
2017-09-15 17:10:43 -07:00
Seong-ho Cho
f6be07da74 fix #5111 AttributeError occured with >=pyOpenSSL-17.2.0 (#5112) 2017-09-15 16:57:10 -07:00
r5d
7c16e0da26 certbot: Let plugins_cmd be run as un-priviliged user. (#5103)
* certbot: Let plugins_cmd be run as un-priviliged user.

* certbot/main.py (main): Update function.

Addresses issue #4350.

* * Add test certbot.tests.main_testMainTest.test_plugins_no_args_unpriviliged
2017-09-15 16:55:05 -07:00
Noah Swartz
03624fa9db add domain name when having issues in the warn output (#5105) 2017-09-15 16:51:06 -07:00
Noah Swartz
d3a00a97a3 fix NAME to CERTNAME (#5114) 2017-09-15 16:47:08 -07:00
Brad Warren
4bc0c83ca7 Add --no-self-upgrade to test farm test. (#5095) 2017-09-14 17:33:32 -07:00
Brad Warren
7d0a77ffcf Release 0.18.1 (#5093)
* Release 0.18.1

(cherry picked from commit 8010822a0b)

* Bump version to 0.19.0
2017-09-14 17:32:45 -07:00
Noah Swartz
837f691992 Merge pull request #5108 from certbot/issue_5107
add a help output for cert-name
2017-09-13 16:39:54 -07:00
Noah Swartz
174a006d9c add renew to existing doc 2017-09-13 11:37:07 -07:00
Noah Swartz
b529250535 add a help output for cert-name 2017-09-12 10:52:51 -07:00
Brad Warren
134d499b07 Add 0.18.1 release notes 2017-09-08 13:33:47 -07:00
Brad Warren
68283940cd Test farm improvements (#5088)
* prevent regressions of #5082

* Fix test_leauto_upgrades.sh

test_leauto_upgrades.sh has been incorrectly been succeeding because while peep
doesn't work with newer versions of pip and letsencrypt-auto would crash,
the output included the version number so we reported the test as passing.
This updates letsencrypt-auto to the oldest version that still works for the
purpose of the test and sets pipefail so errors are properly reported.

* Test symlink creation in test_leauto_upgrades.sh

* Pin dependencies in test_sdists.sh.

* Fix permissions errors in test_tests.sh
2017-09-07 17:54:40 -07:00
Brad Warren
82d0ff1df2 Fix permissions error when upgrading certbot-auto. (#5086)
Now we always check if we have root access if --cb-auto-has-root is not given
on the command line. This allows certbot-auto to properly acquire root when
upgrading from an older version. People upgrading from 0.18.0 to 0.18.1 may
check for root access twice, however, if root's user ID is 0, this check is
essentially a noop. If root's user ID is not 0, we'll request root access a 2nd
time during this upgrade.
2017-09-07 17:23:57 -07:00
Brad Warren
d4fe812508 Update changelog to reflect 0.18.0 (#5081) 2017-09-07 16:06:07 -07:00
Brad Warren
6988491b67 Merge pull request #5080 from certbot/candidate-0.18.0
Release 0.18.0
2017-09-07 05:57:12 -07:00
Brad Warren
1a79f82082 Also check new path when determining cli_command (#5082) 2017-09-06 20:22:27 -07:00
yomna
9fb132ba69 Merge pull request #5075 from certbot/specify-min-six-version
Specify the minimum six version in ACME
2017-09-05 17:49:42 -07:00
Brad Warren
a7267b0fcd Bump version to 0.19.0 2017-09-05 16:07:03 -07:00
Brad Warren
756c44f7af Release 0.18.0 2017-09-05 16:06:43 -07:00
Brad Warren
d710c441e2 Specify the minimum six version in acme 2017-09-05 10:07:32 -07:00
ohemorange
8ad18cbe6e Use ffdhe2048 Nginx DH params to fix Weak-DH bug (#4973)
* Rename plugins.common.install_ssl_options_conf to plugins.common.install_version_controlled_file

* Install ssl_dhparams file

* Add installation test

* Add ssl_dhparam option when making a server block ssl

* add install_ssl_dhparams to Installer common plugin class

* Remove redundant code and tests

* update MANIFEST.in
2017-09-01 07:57:30 -07:00
Noah Swartz
c6bdad4ffb mention that revoke doesn't effect rate limit (#5070)
fixes #2720
2017-09-01 07:57:07 -07:00
Noah Swartz
39696456db Link to changelog from readme (#5069)
fixes #3420
2017-09-01 07:56:49 -07:00
Brad Warren
bbf397a9f9 Fix documentation build failures (#5068)
* sphinxify error_handler docs
2017-08-31 16:35:53 -07:00
r5d
7cb8c1264f certbot: Update renew command output in quiet mode. (#5062)
* certbot: Update `renew` command output in quiet mode.

* certbot/renewal.py (_renew_describe_results): Update function.
* certbot/tests/main_test.py (_test_renewal_common): Update method.
  Add optional arg `stdout`; Modify `mock_get_utilitiy`, `stdout`.
  (test_quiet_renew): Update method.
2017-08-31 11:01:15 -07:00
Noah Swartz
b6f8a477b8 Merge pull request #5064 from certbot/letsencrypt-travis
Don't send IRC notifications from forks
2017-08-31 11:19:29 -05:00
Brad Warren
06dd645e85 encrypt channel 2017-08-30 10:34:05 -07:00
Chris Julian
2bfc92e58d #4071 Mixin to prevent setting return_value after initializing certain Mock objects (#4963)
* Addressing #4071 Wrote an ImmutableReturnMixin to prevent developers overriding return_value in certain Mock objects

* Language

* Loosening the assumption that underlying _mock objects need to be Immutable-like simplifies implementation

* Addressing #4071

* Ensure side_effects and return_values are pushed down to the underlying _mock in FreezableMocks. And IDisplay mocks are no longer frozen in _create_get_utility_mock()

* Edit a handful of tests to not override the mock_get_utility return_value

* Brief explainer of FreezableMock.__setattr__

* Incorporating PR feedback and some compatibility

* FreezableMock __getattr__ needs a shortcut in case of return_value or side_effect

* Changing return_value only forbidden if set before freezing

* Remove unnecessary else block

* Expanded doc strings

* Bring a couple new tests in line with patch_get_utility() norms
2017-08-30 09:52:45 -07:00
Brad Warren
ae0be73b53 Make common Installer base class (#5055)
* Add installer class

* Add wrapped reverter methods to common.Installer.

* Use Installer class in Apache plugin

* Use Installer class in Nginx plugin

* Don't create reverter in Apache and Nginx plugins
2017-08-28 17:06:09 -07:00
Noah Swartz
8d362d4469 Merge pull request #5029 from certbot/issue_4792
expand nginx no name error
2017-08-28 14:04:56 -07:00
Ted Marynicz
133f636817 Update install.rst (#5057)
Minor typo fix in VE para
2017-08-28 11:59:08 -07:00
Noah Swartz
df71ec33b3 switch from triple quotes to single quotes 2017-08-28 13:45:42 -05:00
Josh Soref
b2b3285bf5 Add the word instead to renew error (#5053)
Closes #4118
2017-08-25 10:25:59 -07:00
r5d
b43bf8f94a Stop using print in certbot.cli module. (#5028)
* Update cerbot.tests.util.patch_get_utility (#3720)

* Add new arg `stdout_notification` to
  `cerbot.tests.util.patch_get_utility` function.

  If `stdout_notification` is True, then the mock
  interfaces.IDisplay.notification function will print out to stdout.

* Add new arg `stdout_notification` to _create_get_utility_mock
  function.

* Add new function `_stdout_notification`.

* Stop using print in certbot.cli (#3720)

* certbot/cli.py (HelpfulArgumentParser._usage_string)
(HelpfulArgumentParser.__init__): Update methods.
* certbot/tests/cli_test.py (test_cli_ini_domains, test_no_args)
(test_install_abspath, test_help, test_help_no_dashes)
(test_parse_domains, test_preferred_challenges, test_server_flag)
(test_must_staple_flag, test_no_gui, test_staging_flag)
(test_dry_run_flag, test_option_was_set)
(test_encode_revocation_reason, test_force_interactive)
(test_deploy_hook_conflict, test_deploy_hook_matches_renew_hook)
(test_deploy_hook_sets_renew_hook, test_renew_hook_conflict)
(test_renew_hook_matches_deploy_hook)
(test_renew_hook_does_not_set_renew_hook, test_max_log_backups_error)
(test_max_log_backups_success, test_webroot_map)
(test_report_config_interaction_str)
(test_report_config_interaction_iterable): Update tests.
* certbot/tests/main_test.py (test_certificates)
(test_certonly_abspath, test_certonly_bad_args)
(test_agree_dev_preview_config): Update tests.

* certbot: Refactor cli_test.ParseTest.

* certbot/tests/cli_test.py (ParseTest._unmocked_parse): Rename parse
to _unmocked_parse.
(parse): New method.
(ParseTest._help_output, ParseTest.test_cli_ini_domains)
(ParseTest.test_no_args, ParseTest.test_install_abspath)
(ParseTest.test_help, ParseTest.test_help_no_dashes)
(ParseTest.test_parse_domains, ParseTest.test_preferred_challenges)
(ParseTest.test_server_flag, ParseTest.test_must_staple_flag)
(ParseTest.test_no_gui, ParseTest.test_staging_flag)
(ParseTest.test_dry_run_flag, ParseTest.test_option_was_set)
(ParseTest.test_encode_revocation_reason)
(ParseTest.test_force_interactive)
(ParseTest.test_deploy_hook_conflict)
(ParseTest.test_deploy_hook_matches_renew_hook)
(ParseTest.test_deploy_hook_sets_renew_hook)
(ParseTest.test_renew_hook_conflict)
(ParseTest.test_renew_hook_matches_deploy_hook)
(ParseTest.test_renew_hook_does_not_set_renew_hook)
(ParseTest.test_max_log_backups_error)
(ParseTest.test_max_log_backups_success): Update methods.

* certbot: Refactor cli_test.SetByCliTest

* certbot/tests/cli_test.py (SetByCliTest.test_webroot_map)
(SetByCliTest.test_report_config_interaction_str)
(SetByCliTest.test_report_config_interaction_iteratable)
(_call_set_by_cli): Update methods.

* certbot: cli: Fix style.

* certbot/cli.py (HelpfulArgumentParser.__init__): Update method.

* certbot: Revert changes to tests.util.patch_get_utility

* certbot/tests/util.py (patch_get_utility): Remove
`stdout_notification` arg.
(_creat_get_utility_mock): Remove `stdout_notification` arg.
(_stdout_notification): Remove function.

* certbot: Revert changes to MainTest.

* certbot/tests/main_test.py
(MainTest.test_certificates, MainTest.test_certonly_abspath)
(MainTest.test_certonly_bad_args): Update methods.

* certbot: cli_test.py: Remove 'pylint: disable' lines.

* certbot/tests/cli_test.py (ParseTest.parse): Update method.
(_call_set_by_cli): Update function.
2017-08-25 10:05:58 -07:00
Brad Warren
a5fae7eab5 certbot-auto OS dependency update system (#4971)
* Add version number to bootstrap scripts.

* Always determine Bootstrap function and version.

* Write bootstrap version into venv.

* Add PrevBootstrapVersion function.

* Add OS bootstrapping check to phase 2.

* Differentiate -n and renew when rebootstrapping.

* Quote all environment variables.

* Correct test condition

* Add loud warning about hardcoded version list.

* s/VENV_BOOTSTRAP_VERSION/BOOTSTRAP_VERSION_PATH

* Properly handle noop bootstrap functions.
2017-08-23 11:01:20 -07:00
Winston Smith
8ca36a0b62 Organize + document certbot/tests/testdata directory (#4983)
* wp organize keys documentation start

* oganized testdata directory + readme

* clean up doc

* del acme change
2017-08-23 10:32:27 -07:00
Noah Swartz
c33ee0e2df add warnings and clarity to config documentation (#4991) 2017-08-21 12:30:04 -07:00
Brad Warren
56db211367 Change certbot-auto's installation path to /opt (#4970)
* Update comment about root usage.

* run all of certbot-auto as root

* remove other $SUDO uses from template

* remove $SUDO usage from bootstrappers

* default venv path = /opt/eff.org/certbot/venv

* Create symlinks from old default venvs

* Delete old venv path when it exists.

Also, quote expansion of paths.

* fix typo

* Separate venv_dir and le_auto_path

* Deduplicate code with test_dirs()

* Ignore cleanup errors.

This is caused by subdirectories being owned by root.

* Split test into test_phase2_upgrade.

* Rename test_dirs to temp_paths for clarity.

* Check both venvs before bootstrapping again.

* Use OLD_VENV_PATH/bin

* Preserve environment with sudo.

* Remove "esp. under sudo" comment.

* Export *VENV_PATH.

* Change check for OLD_VENV installation.

This approach better handles manually set VENV_PATH values.

* Remove SUDO_ENV.

* Print message before requesting root privileges.

* Make a function for selecting root auth method.

* Address @erikrose's feedback.
2017-08-21 12:23:09 -07:00
Nicolas Duchon
cd2e70e9cd Redirect to zenhack/simp_le (#5025)
Kuba's simp_le has been unmaintained for more than a year and is starting to break.

zenhack's fork is actively maintained and available through PyPI.
2017-08-21 11:27:24 -07:00
Noah Swartz
2c9e072a9f link to nginx documentation 2017-08-15 17:39:23 -07:00
Noah Swartz
4d72fa42e3 expand nginx no name error 2017-08-15 17:00:03 -07:00
Winston Smith
16ed141301 Include plugin selection at the info logging level (#5010)
* Plugin selection on INFO

* Fixed lint errors

* Fixed lint errors

* record_chosen_plugin log plugin
2017-08-10 16:51:19 -07:00
Noah Swartz
af322a6068 Merge pull request #4896 from certbot/order-matters2
Explain --domains and --cert-name usage
2017-08-10 09:51:19 -07:00
cj-dev
48c890be61 #4434 Test Config Base Class (#4974)
* Addressing #4434 by implementing ConfigTestCase which mocks out a NamespaceConfig for consistent config use across tests

* Refactor account_test.py for use with ConfigTestCase

* Remove superfluous setup/teardown

* Pylint oops.

* Fix redundant inheritance class definitions

* Separate ConfigTestCase's mocked directories

* Module import style consistency

* Refactor log_test.py for use with ConfigTestCase

* Refactor eff_test.py for use with ConfigTestCase. Also tweak for import style consistency

* Refactor reverter_test.py for use with ConfigTestCase

* Refactor renewal_test.py for use with ConfigTestCase

* Refactor main_test.py for use with ConfigTestCase

* Refactor storage_test.py for use with ConfigTestCase

* Refactor cert_manager_test.py for use with ConfigTestCase

* Refactor client_test.py for use with ConfigTestCase

* Refactor configuration_test.py for use with ConfigTestCase

* Pylint!

* Incorporating PR feedback

* Remove comment
2017-08-09 13:19:43 -07:00
Bob Strecansky
5e58580d13 [#4966] - Fedora 26 doesn't have development tools handy (#4997) 2017-08-09 09:12:36 -07:00
Brad Warren
6c52cc49a7 Pin dependencies in compatibility tests. (#5004)
We now use tools/pip_install_editable.sh which installs our packages using the
pinned versions from certbot-auto.

We also use letsencrypt-auto-source/letsencrypt-auto instead of certbot-auto in
the root to:

1. Make sure OS bootstrappers are up to date with master.
2. Copy letsencrypt-auto-source into our tree so it can be used by
tools/pip_install_editable.sh later.
2017-08-08 15:31:41 -07:00
Brad Warren
47b3d19170 Increase pinned configargparse version to 0.12.0. (#4995) 2017-08-08 12:03:48 -07:00
Noah Swartz
32de7303dd Merge pull request #4990 from certbot/fix-arch
Fix space in quiet check in BootstrapArchCommon
2017-08-08 09:13:54 -07:00
Noah Swartz
f9ed53e698 Revocation reason (#4987)
* fix revocation reason help text

* add it to the docs

* move and expand revoke reason example
2017-08-07 17:13:27 -07:00
Brad Warren
f31cb5f812 Put the minimum dep version in Google DNS setup.py (#5002) 2017-08-07 17:12:58 -07:00
Brad Warren
0ac21e47c7 Use #letsencrypt-dev instead of #letsencrypt (#4998) 2017-08-07 17:12:49 -07:00
Brad Warren
f68fba2be2 Fix oldest tests by pinning Google DNS deps (#5000) 2017-08-07 14:57:56 -07:00
Brad Warren
d4676610e9 Unhide Nginx (#4969)
* Be careful with new interaction from enabling nginx

* Fix py3 compataibility & better docs

* Make minor changes to @pde's PR to unhide nginx

* unhide nginx plugin

* Only protect against nginx interaction in cb-auto
2017-08-07 08:36:41 -07:00
Brad Warren
0b94d6aa18 Merge pull request #4981 from certbot/candidate-0.17.0
Update versions and *autos from 0.17.0 release
2017-08-03 15:06:30 -07:00
Noah Swartz
e6c9e2a868 Merge pull request #4982 from certbot/0.17.0-changelog
Update changelog for 0.17.0
2017-08-03 14:56:01 -07:00
Brad Warren
5508d1dd12 Fix space in quiet check in BootstrapArchCommon 2017-08-03 13:45:55 -07:00
Brad Warren
c0d10bba5a typos and grammar 2017-08-02 12:11:19 -07:00
Brad Warren
4f74b8eb7a Update changelog for 0.17.0 2017-08-02 12:06:02 -07:00
Brad Warren
744c993040 Bump version to 0.18.0 2017-08-01 17:01:19 -07:00
Brad Warren
912d235466 Release 0.17.0 2017-08-01 17:01:07 -07:00
Zach Shepherd
314f4bbe22 client: allow callers to add information to the user agent (#4690)
This change introduces a new flag to allow callers to add information to
the user agent without replacing it entirely.

This allows people re-packaging or wrapping Certbot to influence its user
agent string. They may which to do this so that stats/metrics related to
their distribution are available to boulder.

This is beneficial for both the Certbot team and the party re-packaging
Certbot as it allows the custom user agent to match the Certbot user
agent as closely as possible, allowing data about use of the re-packaged
version to be collected along side or separately from vanilla certbot.
2017-08-01 11:34:35 -07:00
Brad Warren
7461bdbffd Update pipstrap to version 1.3.0 (#4978)
* Update pipstrap to version 1.2.0.

* Update pipstrap to include Python 2.6 fix.

* Bump pipstrap to 1.3.0.
2017-08-01 10:18:11 -07:00
Noah Swartz
9288cddfe9 Merge pull request #4960 from certbot/easy-install
Explicitly advise against using easy_install.
2017-07-31 19:04:59 -07:00
Brad Warren
d6a7e2d1fe Bump cryptography to 2.0.2 (#4972) 2017-07-28 09:55:18 -07:00
ohemorange
0321c0cb4c Change add_server_directives replace=True behavior to attempt to replace, but append on failure to find. (#4956)
* Change add_server_directives replace=True behavior to attempt to replace, but append on failure to find.

* Remove try/except around add_server_directives
2017-07-26 13:57:25 -07:00
r5d
142ced234b Add cert name in renewal error messages (#4932) (#4957)
* Add cert name in renewal error messages (#4932)

* certbot: Fix error message in `handle_renewal_request`.
2017-07-26 13:16:20 -07:00
Brad Warren
43bea2edb3 Explicitly advise against using easy_install. 2017-07-25 11:43:23 -07:00
Brad Warren
5845f186ed Merge pull request #4948 from certbot/challenge_docs
Challenge docs
2017-07-20 17:33:25 -07:00
Brad Warren
c314ec0474 Correct --cert-name and --domains usage.
* Revert "Mention that the domain is used to choose filename"

This reverts commit 1c06144e18.

* Correct --cert-name and --domains usage.

* Clarify which paths --domains affects
2017-07-20 17:26:05 -07:00
Noah Swartz
447d3d867d fix a few nits 2017-07-20 11:16:48 -07:00
Peter Conrad
df8b374916 Adding 'What Is a Certificate' section
- adding what.rst to index.rst

- Bigger link to instruction generator in intro.rst, some edits to what.rst in response to comments on What is a Certificate? section first draft #4370

- Responding to St_Ranger's comment on 4370

- Edits to using.rst related to --expand

- Initial edit pass through challenges.rst

- Edits to what.rst and challenges.rst to resolve #3664 and #4153

- Incorpoprating feedback from #4370

- Finally going after those last few comments before the restructuring of the plugin stuff (coming soon)

- Fixing --expand example in using.rst and adding to Apache/NGINX bullet in challenges.rst
2017-07-20 11:14:08 -07:00
Noah Swartz
7d5ccd006b Merge pull request #4904 from certbot/improve-apache-error-message2
Update Apache error message and comment
2017-07-20 11:02:08 -07:00
Noah Swartz
bb6a22b985 Merge pull request #4943 from certbot/issue_4520
add deprecation warning
2017-07-19 15:51:51 -07:00
Noah Swartz
7f19ac5e3d Merge pull request #4941 from certbot/issue_4866
add disable pip version check flag
2017-07-19 15:36:29 -07:00
Noah Swartz
9878f15966 leave macos unchanged 2017-07-19 11:55:53 -07:00
Noah Swartz
1cf8c5a586 add changes to template 2017-07-18 16:37:03 -07:00
Noah Swartz
56ce87db27 Merge branch 'master' into issue_4866 2017-07-18 16:34:35 -07:00
Noah Swartz
fa74a32245 updated letsencrypt-auto.template 2017-07-18 16:27:33 -07:00
Jacob Hoffman-Andrews
b3004fe6cf Handle critical SAN extension. (#4931)
* Handle critical SAN extension.

* Add testdata/critical-san.pem.
2017-07-18 15:57:00 -07:00
Noah Swartz
278194fe6d add log rotation info (#4942) 2017-07-18 15:50:23 -07:00
Jacob Hoffman-Andrews
d327c1c28f Tweak non-root error message. (#4944)
For most people, the right answer will be "run as root," so we should emphasize
that over the "how to run as non-root" instructions.
2017-07-18 12:47:10 -07:00
yomna
7d0e4d7bad Merge pull request #4915 from Baeumla/master
Check keys if revoke certificate by private key
2017-07-17 15:54:47 -07:00
Noah Swartz
d962b2605a add deprecation warning 2017-07-17 15:27:48 -07:00
Brad Warren
c779be8b88 Merge pull request #4863 from certbot/bmw-nginx-compatibility-tests
Nginx compatibility test
2017-07-17 14:32:39 -07:00
Noah Swartz
f5580598cd add disable pip version check flag 2017-07-17 13:08:52 -07:00
Noah Swartz
7ad41dc3ef Merge pull request #4939 from cj-dev/4893
Enhancement #4893. More copypastable output, including private key location on disk
2017-07-17 10:58:11 -07:00
Chris J
a8b5dfc76c Enhancement #4893. More copypastable output, including private key location on disk 2017-07-16 22:38:21 -04:00
Baime
0f30f9e96f Added test to check revoke cert by key mismatch 2017-07-15 15:17:25 +02:00
Noah Swartz
6090fe9651 Merge pull request #4899 from jonasbn/master
Added missing rst files as described in issue #4736
2017-07-14 18:47:31 -07:00
Noah Swartz
de1f9c4fe3 Merge pull request #4876 from tchollingsworth/combining-plugins-docs
docs: explain how to combine plugins
2017-07-14 18:42:06 -07:00
jonasbn
7ed2e91cc7 Removed null.rst as by request 2017-07-14 12:52:25 +02:00
r5d
331d12ed50 certbot: Update storage.get_link_target (#4750) (#4923)
* certbot: Update storage.get_link_target (#4750)

* The `get_link_target` function raises `errors.CertStorageError` when
  link does not exists.

* certbot: Fix typo in storage.get_link_target.
2017-07-13 10:13:59 -07:00
Brad Warren
29d80f334f Merge pull request #4907 from certbot/backup-count
Make Certbot's log rotation configurable
2017-07-13 08:56:24 -05:00
Noah Swartz
6ede309c6a Merge pull request #4865 from certbot/irc-notifications
Turn on IRC notifications for Travis failures in master
2017-07-12 13:03:37 -07:00
Noah Swartz
7efa213b22 Merge pull request #4814 from certbot/improve-apache-error-message
Correct message about vhost ambiguity
2017-07-12 07:53:36 -07:00
Brad Warren
a7a8e060e3 Finish adding configurable log rotation
* Update log backupCount name and description.

* Add additional error handling to --log-backups

* test --log-backups flag

* Pass log_backups value to RotatingFileHandler

* Test that log_backups is properly used

* add _test_success_common

* Add test_success_with_rollover

* Add test_success_without_rollover

* mock stderr in cli tests

* Set log_backups in PostArgParseSetupTest

* Rename "log backups" to "max log backups"
2017-07-11 21:14:18 -05:00
yomna
bc3765d6d0 No longer mask failed challenge errors with encoding errors (#4867)
*     no longer masker failed challenge errors with encoding errors

* simplifying through type-checking

* bytes
2017-07-10 21:05:52 -05:00
John Harlan
fad1a4b576 Add flags to configure log rotation
* Add & enable --disable-log-rotation

* Add --max-log-count

* Correct max-log-count: remove action=store_true add type=int

* Add logging to cli argument groups.

* Add logging group to HELP_TOPICS in __init__

* Adjust line length

* test simplifying to one argument
2017-07-10 19:43:28 -05:00
Baime
368beee8bf Use correct key/cert for revoke by key test 2017-07-09 12:10:35 +02:00
Baime
3a9150a7ba Fix for revoke cert by key process 2017-07-08 19:36:39 +02:00
Baime
ab286e0887 fixed path errors in revoking by key process 2017-07-08 18:00:33 +02:00
Baime
62bdf663f2 Check keys if revoke certificate by private key 2017-07-08 16:20:12 +02:00
Florian Mutter
48ef16ab0d Align domain names output to command line input (#4874)
The command line takes a comma separated list of domain names. To be able to use the list of existing domain names it would be helpful to get a list that is also comma separated.

Sample use case: If you would like to add a new domain to an existing certificate you need to list all existing domain names.

Makes certbot certificates use comma-separated domain names instead of space-separated.
2017-07-07 12:46:30 -07:00
Felix Yan
d0ecf739bd Add new DNS authenticator plugins in 0.16 (#4911) 2017-07-07 07:46:09 -04:00
ohemorange
57e56cc97b Candidate 0.16.0 (#4908)
* Release 0.16.0

* Bump version to 0.17.0
2017-07-06 15:57:11 -07:00
yomna
1f3b028398 Merge pull request #4895 from certbot/deploy-hook
Deploy hook
2017-07-06 15:56:55 -07:00
Brad Warren
b23384438f update changelog for 0.16.0 release (#4906) 2017-07-06 15:46:21 -04:00
Brad Warren
c3c1609fa0 no more renew(al) hook(s) 2017-07-06 14:59:28 -04:00
Brad Warren
f314ea1d33 s/renew-hook/deploy-hook docs/using.rst 2017-07-06 10:30:29 -04:00
Felix Yan
6bb95c6596 Fix a typo: enviroment -> environment (#4898) 2017-07-05 12:59:23 -04:00
Brad Warren
72b1a6f9cd Update outdated comment 2017-07-05 10:03:02 -04:00
Brad Warren
bf763cbbc6 remove outdated error message 2017-07-05 10:00:14 -04:00
Brad Warren
5318945267 Hide exceptions that occur during session.close() (#4891)
* Hide exceptions that occur during session.close()

This fixes #4840. Exceptions that are raised out of __del__ methods are caught
and printed to stderr. By catching any exceptions that occur, we now prevent
this from happening.

Alternative solutions to this would have been either not calling
session.close() at all or adding a close() method to acme.client.ClientNetwork,
acme.client.Client, and certbot.client.Client and using certbot.client.Client
in a context manager to ensure close() is called. The former means that users
of the ACME library never properly close their connections until their program
exits and the latter adds a lot of complexity and nesting of client code for
little benefit.

* Only catch Exceptions
2017-07-05 09:25:44 -04:00
jonasbn
72c480ef18 Removed files with test in name after review comment from @ynasser 2017-07-04 23:52:24 +02:00
jonasbn
054873034c Added missed rst file 2017-07-02 00:31:58 +02:00
jonasbn
d118acf524 Correction to module name 2017-07-02 00:16:15 +02:00
jonasbn
ec35828b9a Added missing rst files after doing an inspection of the file structure 2017-07-02 00:12:16 +02:00
ohemorange
97b22da1b6 Replace the easy v. secure prompt with more clear choices (#4897)
* Replace the easy v. secure prompt with more clear choices
2017-06-30 17:12:09 -07:00
Peter Eckersley
595745e044 Clarify domain name <-> cert name docs 2017-06-30 13:58:18 -04:00
Alex Dehnert
1c06144e18 Mention that the domain is used to choose filename
The cert filename is chosen based on the first domain listed. With certs with overlapping domains or where some domains are less canonical, it's therefore useful to put the most canonical/unique domain first. This updates the help text to inform users of this fact.
2017-06-30 13:57:59 -04:00
Brad Warren
62327b49c3 Test hook validation order 2017-06-30 11:40:34 -04:00
Brad Warren
4c19d19cf5 Test that deploy and renew hooks are saved right
It is important that both renew and deploy hooks are saved as renew_hook in
renewal configuration files to preserve forwards compatibility.
2017-06-30 11:30:21 -04:00
Brad Warren
6dedfa62b6 Test renew and deploy hooks are run properly 2017-06-30 11:06:51 -04:00
Brad Warren
8a664622ea Call deploy_hook during certonly and run 2017-06-30 10:33:49 -04:00
Brad Warren
1b65ba88d8 test hooks.deploy_hook 2017-06-30 10:30:33 -04:00
Brad Warren
e94ee31a6f add hooks.deploy_hook 2017-06-30 10:24:00 -04:00
Brad Warren
32fa3b1d04 test deploy-hook and renew-hook match 2017-06-30 09:59:19 -04:00
Brad Warren
ad4ed22932 test --renew-hook 2017-06-30 09:56:17 -04:00
Brad Warren
feffeb275b add --renew-hook error handling 2017-06-30 09:56:17 -04:00
Brad Warren
4243db1525 test --renew-hook is hidden 2017-06-30 09:56:17 -04:00
Brad Warren
220d486190 remove --renew-hook from help output 2017-06-30 09:56:17 -04:00
Brad Warren
ed4be4117c hide --renew-hook 2017-06-30 09:56:17 -04:00
Brad Warren
5cf82e4843 test --deploy-hook 2017-06-30 09:56:13 -04:00
Brad Warren
af354e9099 add --deploy-hook parsing 2017-06-30 09:18:07 -04:00
Brad Warren
d57e8bfaa3 add --deploy-hook 2017-06-30 09:11:51 -04:00
Brad Warren
828363b21a Fix nginx --dry-run (#4889)
* Revert "Don't save keys/csr on dry run (#4380)"

This reverts commit e034b50363.

* Don't save CSRs and keys during dry run

* Factor out _test_obtain_certificate_common

* Add test_obtain_certificate_dry_run

* Wrap key from make_key in util.Key

* Wrap result from make_csr in util.CSR
2017-06-30 08:10:55 -04:00
T.C. Hollingsworth
7d17919527 docs: remove errant parenthesis 2017-06-27 18:12:05 -07:00
T.C. Hollingsworth
33306de90b docs: explain how to combine plugins 2017-06-26 18:57:20 -07:00
Brad Warren
f4094e4d3f Finish oldest tests (#4857)
* Pin oldest version of packaged python deps

* Install security extras in oldest tests

* Revert "bump requests requirement to >=2.10 (#4248)"

This reverts commit 402ad8b353.

* Use create=True when patching open on module
2017-06-23 09:40:59 -07:00
Brad Warren
6aa21d1db6 Fix and speed up compatibility-tests
* Fix nginx-compatibility tests

* sleep is overrated

* Reduce verbosity of nginx tests
2017-06-21 13:29:00 -07:00
Brad Warren
bdf02c9fcc Turn on IRC notifications for Travis failures in master 2017-06-21 11:46:30 -07:00
Erica Portnoy
15c6c1388e Have validator only test domains without existing redirects 2017-06-20 17:15:00 -07:00
Seth Schoen
be457ffa95 Test more in nginx compatibility tests
* Highlight failures more with asterisks

* Filter out wildcard names from all_names

* Only test -ai, not -aie (no redirects)

* Modified versions of almost all of 79 configs corpus

* Re-enable now-working stanza with 301 redirect

* Change another redirect to go to :443
2017-06-20 17:14:22 -07:00
Brad Warren
d3549e18a7 Correct message about vhost ambiguity.
When our Apache plugin is unable to determine which virtual host to use in
non-interactive mode, it raises an error about vhost ambiguity with
instructions on how to fix the problem. These instructions stated that we
require one vhost per file which is no longer accurate since #4706 so I removed
this part of the error message.
2017-06-09 12:48:59 -07:00
167 changed files with 4839 additions and 2280 deletions

View File

@@ -169,8 +169,7 @@ notifications:
email: false
irc:
channels:
- "chat.freenode.net#letsencrypt"
- secure: "SGWZl3ownKx9xKVV2VnGt7DqkTmutJ89oJV9tjKhSs84kLijU6EYdPnllqISpfHMTxXflNZuxtGo0wTDYHXBuZL47w1O32W6nzuXdra5zC+i4sYQwYULUsyfOv9gJX8zWAULiK0Z3r0oho45U+FR5ZN6TPCidi8/eGU+EEPwaAw="
on_success: never
on_failure: always
use_notice: true
skip_join: true

View File

@@ -2,6 +2,150 @@
Certbot adheres to [Semantic Versioning](http://semver.org/).
## 0.18.2 - 2017-09-20
### Fixed
* An issue where Certbot's ACME module would raise an AttributeError trying to
create self-signed certificates when used with pyOpenSSL 17.3.0 has been
resolved. For Certbot users with this version of pyOpenSSL, this caused
Certbot to crash when performing a TLS SNI challenge or when the Nginx plugin
tried to create an SSL server block.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/46?closed=1
## 0.18.1 - 2017-09-08
### Fixed
* If certbot-auto was running as an unprivileged user and it upgraded from
0.17.0 to 0.18.0, it would crash with a permissions error and would need to
be run again to successfully complete the upgrade. This has been fixed and
certbot-auto should upgrade cleanly to 0.18.1.
* Certbot usually uses "certbot-auto" or "letsencrypt-auto" in error messages
and the User-Agent string instead of "certbot" when you are using one of
these wrapper scripts. Proper detection of this was broken with Certbot's new
installation path in /opt in 0.18.0 but this problem has been resolved.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/45?closed=1
## 0.18.0 - 2017-09-06
### Added
* The Nginx plugin now configures Nginx to use 2048-bit Diffie-Hellman
parameters. Java 6 clients do not support Diffie-Hellman parameters larger
than 1024 bits, so if you need to support these clients you will need to
manually modify your Nginx configuration after using the Nginx installer.
### Changed
* certbot-auto now installs Certbot in directories under `/opt/eff.org`. If you
had an existing installation from certbot-auto, a symlink is created to the
new directory. You can configure certbot-auto to use a different path by
setting the environment variable VENV_PATH.
* The Nginx plugin can now be selected in Certbot's interactive output.
* Output verbosity of renewal failures when running with `--quiet` has been
reduced.
* The default revocation reason shown in Certbot help output now is a human
readable string instead of a numerical code.
* Plugin selection is now included in normal terminal output.
### Fixed
* A newer version of ConfigArgParse is now installed when using certbot-auto
causing values set to false in a Certbot INI configuration file to be handled
intuitively. Setting a boolean command line flag to false is equivalent to
not including it in the configuration file at all.
* New naming conventions preventing certbot-auto from installing OS
dependencies on Fedora 26 have been resolved.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/42?closed=1
## 0.17.0 - 2017-08-02
### Added
* Support in our nginx plugin for modifying SSL server blocks that do
not contain certificate or key directives.
* A `--max-log-backups` flag to allow users to configure or even completely
disable Certbot's built in log rotation.
* A `--user-agent-comment` flag to allow people who build tools around Certbot
to differentiate their user agent string by adding a comment to its default
value.
### Changed
* Due to some awesome work by
[cryptography project](https://github.com/pyca/cryptography), compilation can
now be avoided on most systems when using certbot-auto. This eliminates many
problems people have had in the past such as running out of memory, having
invalid headers/libraries, and changes to the OS packages on their system
after compilation breaking Certbot.
* The `--renew-hook` flag has been hidden in favor of `--deploy-hook`. This new
flag works exactly the same way except it is always run when a certificate is
issued rather than just when it is renewed.
* We have started printing deprecation warnings in certbot-auto for
experimentally supported systems with OS packages available.
* A certificate lineage's name is included in error messages during renewal.
### Fixed
* Encoding errors that could occur when parsing error messages from the ACME
server containing Unicode have been resolved.
* certbot-auto no longer prints misleading messages about there being a newer
pip version available when installation fails.
* Certbot's ACME library now properly extracts domains from critical SAN
extensions.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.17.0+is%3Aclosed
## 0.16.0 - 2017-07-05
### Added
* A plugin for performing DNS challenges using dynamic DNS updates as defined
in RFC 2316. This plugin is packaged separately from Certbot and is available
at https://pypi.python.org/pypi/certbot-dns-rfc2136. It supports Python 2.6,
2.7, and 3.3+. At this time, there isn't a good way to install this plugin
when using certbot-auto, but this should change in the near future.
* Plugins for performing DNS challenges for the providers
[DNS Made Easy](https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy) and
[LuaDNS](https://pypi.python.org/pypi/certbot-dns-luadns). These plugins are
packaged separately from Certbot and support Python 2.7 and 3.3+. Currently,
there isn't a good way to install these plugins when using certbot-auto,
but that should change soon.
* Support for performing TLS-SNI-01 challenges when using the manual plugin.
* Automatic detection of Arch Linux in the Apache plugin providing better
default settings for the plugin.
### Changed
* The text of the interactive question about whether a redirect from HTTP to
HTTPS should be added by Certbot has been rewritten to better explain the
choices to the user.
* Simplified HTTP challenge instructions in the manual plugin.
### Fixed
* Problems performing a dry run when using the Nginx plugin have been fixed.
* Resolved an issue where certbot-dns-digitalocean's test suite would sometimes
fail when ran using Python 3.
* On some systems, previous versions of certbot-auto would error out with a
message about a missing hash for setuptools. This has been fixed.
* A bug where Certbot would sometimes not print a space at the end of an
interactive prompt has been resolved.
* Nonfatal tracebacks are no longer shown in rare cases where Certbot
encounters an exception trying to close its TCP connection with the ACME
server.
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.16.0+is%3Aclosed
## 0.15.0 - 2017-06-08
### Added

View File

@@ -6,3 +6,4 @@ include linter_plugin.py
recursive-include docs *
recursive-include examples *
recursive-include certbot/tests/testdata *
include certbot/ssl-dhparams.pem

View File

@@ -15,6 +15,9 @@ protocol) that can automate the tasks of obtaining certificates and
configuring webservers to use them. This client runs on Unix-based operating
systems.
To see the changes made to Certbot between versions please refer to our
`changelog <https://github.com/certbot/certbot/blob/master/CHANGELOG.md>`_.
Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto``,
depending on install method. Instructions on the Internet, and some pieces of the
software, may still refer to this older name.

View File

@@ -519,7 +519,12 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
self._default_timeout = timeout
def __del__(self):
self.session.close()
# Try to close the session, but don't show exceptions to the
# user if the call to close() fails. See #4840.
try:
self.session.close()
except Exception: # pylint: disable=broad-except
pass
def _wrap_in_jws(self, obj, nonce):
"""Wrap `JSONDeSerializable` object in JWS.

View File

@@ -600,12 +600,19 @@ class ClientNetworkTest(unittest.TestCase):
mock.ANY, mock.ANY, verify=mock.ANY, headers=mock.ANY,
timeout=45)
def test_del(self):
def test_del(self, close_exception=None):
sess = mock.MagicMock()
if close_exception is not None:
sess.close.side_effect = close_exception
self.net.session = sess
del self.net
sess.close.assert_called_once_with()
def test_del_error(self):
self.test_del(ReferenceError)
@mock.patch('acme.client.requests')
def test_requests_error_passthrough(self, mock_requests):
mock_requests.exceptions = requests.exceptions

View File

@@ -2,6 +2,7 @@
import binascii
import contextlib
import logging
import os
import re
import socket
import sys
@@ -218,7 +219,7 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
# WARNING: this function does not support multiple SANs extensions.
# Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
match = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text)
match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
# WARNING: this function assumes that no SAN can include
# parts_separator, hence the split!
sans_parts = [] if match is None else match.group(1).split(parts_separator)
@@ -243,7 +244,7 @@ def gen_ss_cert(key, domains, not_before=None,
"""
assert domains, "Must provide one or more hostnames for the cert."
cert = OpenSSL.crypto.X509()
cert.set_serial_number(int(binascii.hexlify(OpenSSL.rand.bytes(16)), 16))
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
cert.set_version(2)
extensions = [

View File

@@ -131,6 +131,11 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
self.assertEqual(self._call_csr('csr-idnsans.pem'),
self._get_idn_names())
def test_critical_san(self):
self.assertEqual(self._call_cert('critical-san.pem'),
['chicago-cubs.venafi.example', 'cubs.venafi.example'])
class RandomSnTest(unittest.TestCase):
"""Test for random certificate serial numbers."""

View File

@@ -1,5 +1,6 @@
"""ACME protocol messages."""
import collections
import six
from acme import challenges
from acme import errors
@@ -36,9 +37,13 @@ ERROR_TYPE_DESCRIPTIONS.update(dict( # add errors with old prefix, deprecate me
def is_acme_error(err):
"""Check if argument is an ACME error."""
return (ERROR_PREFIX in str(err)) or (OLD_ERROR_PREFIX in str(err))
if isinstance(err, Error) and (err.typ is not None):
return (ERROR_PREFIX in err.typ) or (OLD_ERROR_PREFIX in err.typ)
else:
return False
@six.python_2_unicode_compatible
class Error(jose.JSONObjectWithFields, errors.Error):
"""ACME error.
@@ -92,10 +97,10 @@ class Error(jose.JSONObjectWithFields, errors.Error):
return code
def __str__(self):
return ' :: '.join(
part for part in
return b' :: '.join(
part.encode('ascii', 'backslashreplace') for part in
(self.typ, self.description, self.detail, self.title)
if part is not None)
if part is not None).decode()
class _Constant(jose.JSONDeSerializable, collections.Hashable): # type: ignore

View File

@@ -26,6 +26,7 @@ class ErrorTest(unittest.TestCase):
'type': ERROR_PREFIX + 'malformed',
}
self.error_custom = Error(typ='custom', detail='bar')
self.empty_error = Error()
self.jobj_custom = {'type': 'custom', 'detail': 'bar'}
def test_default_typ(self):
@@ -45,12 +46,6 @@ class ErrorTest(unittest.TestCase):
'The request message was malformed', self.error.description)
self.assertTrue(self.error_custom.description is None)
def test_str(self):
self.assertEqual(
'urn:ietf:params:acme:error:malformed :: The request message was '
'malformed :: foo :: title', str(self.error))
self.assertEqual('custom :: bar', str(self.error_custom))
def test_code(self):
from acme.messages import Error
self.assertEqual('malformed', self.error.code)
@@ -60,8 +55,16 @@ class ErrorTest(unittest.TestCase):
def test_is_acme_error(self):
from acme.messages import is_acme_error
self.assertTrue(is_acme_error(self.error))
self.assertTrue(is_acme_error(str(self.error)))
self.assertFalse(is_acme_error(self.error_custom))
self.assertFalse(is_acme_error(self.empty_error))
self.assertFalse(is_acme_error("must pet all the {dogs|rabbits}"))
def test_unicode_error(self):
from acme.messages import Error, ERROR_PREFIX, is_acme_error
arabic_error = Error(
detail=u'\u0639\u062f\u0627\u0644\u0629', typ=ERROR_PREFIX + 'malformed',
title='title')
self.assertTrue(is_acme_error(arabic_error))
def test_with_code(self):
from acme.messages import Error, is_acme_error

28
acme/acme/testdata/critical-san.pem vendored Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG
A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5
MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g
U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN
MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M
0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV
EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU
0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl
XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF
fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD
VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl
bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV
HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo
dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw
SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0
dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ
KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv
DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w
DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L
ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs
OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst
bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS
yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be
n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv
KA==
-----END CERTIFICATE-----

View File

@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.16.0.dev0'
version = '0.19.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
@@ -16,15 +16,11 @@ install_requires = [
'PyOpenSSL>=0.13',
'pyrfc3339',
'pytz',
# requests>=2.10 is required to fix
# https://github.com/shazow/urllib3/issues/556. This requirement can be
# relaxed to 'requests[security]>=2.4.1', however, less useful errors
# will be raised for some network/SSL errors.
'requests[security]>=2.10',
'requests[security]>=2.4.1', # security extras added in 2.4.1
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'six',
'six>=1.9.0', # needed for python_2_unicode_compatible
]
# env markers cause problems with older pip and setuptools

View File

@@ -3,7 +3,6 @@ import logging
from certbot import errors
from certbot import reverter
from certbot.plugins import common
from certbot_apache import constants
@@ -11,7 +10,7 @@ from certbot_apache import constants
logger = logging.getLogger(__name__)
class AugeasConfigurator(common.Plugin):
class AugeasConfigurator(common.Installer):
"""Base Augeas Configurator class.
:ivar config: Configuration.
@@ -33,11 +32,6 @@ class AugeasConfigurator(common.Plugin):
self.save_notes = ""
# See if any temporary changes need to be recovered
# This needs to occur before VirtualHost objects are setup...
# because this will change the underlying configuration and potential
# vhosts
self.reverter = reverter.Reverter(self.config)
def init_augeas(self):
""" Initialize the actual Augeas instance """
@@ -50,6 +44,10 @@ class AugeasConfigurator(common.Plugin):
flags=(augeas.Augeas.NONE |
augeas.Augeas.NO_MODL_AUTOLOAD |
augeas.Augeas.ENABLE_SPAN))
# See if any temporary changes need to be recovered
# This needs to occur before VirtualHost objects are setup...
# because this will change the underlying configuration and potential
# vhosts
self.recovery_routine()
def check_parsing_errors(self, lens):
@@ -78,26 +76,26 @@ class AugeasConfigurator(common.Plugin):
self.aug.get(path + "/message")))
raise errors.PluginError(msg)
# TODO: Cleanup this function
def save(self, title=None, temporary=False):
"""Saves all changes to the configuration files.
def ensure_augeas_state(self):
"""Makes sure that all Augeas dom changes are written to files to avoid
loss of configuration directives when doing additional augeas parsing,
causing a possible augeas.load() resulting dom reset
"""
This function first checks for save errors, if none are found,
all configuration changes made will be saved. According to the
function parameters. If an exception is raised, a new checkpoint
was not created.
if self.unsaved_files():
self.save_notes += "(autosave)"
self.save()
:param str title: The title of the save. If a title is given, the
configuration will be saved as a new checkpoint and put in a
timestamped directory.
:param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (ie. challenges)
def unsaved_files(self):
"""Lists files that have modified Augeas DOM but the changes have not
been written to the filesystem yet, used by `self.save()` and
ApacheConfigurator to check the file state.
:raises .errors.PluginError: If there was an error in Augeas, in
an attempt to save the configuration, or an error creating a
checkpoint
:returns: `set` of unsaved files
"""
save_state = self.aug.get("/augeas/save")
self.aug.set("/augeas/save", "noop")
@@ -113,30 +111,41 @@ class AugeasConfigurator(common.Plugin):
raise errors.PluginError(
"Error saving files, check logs for more info.")
# Return the original save method
self.aug.set("/augeas/save", save_state)
# Retrieve list of modified files
# Note: Noop saves can cause the file to be listed twice, I used a
# set to remove this possibility. This is a known augeas 0.10 error.
save_paths = self.aug.match("/augeas/events/saved")
# If the augeas tree didn't change, no files were saved and a backup
# should not be created
save_files = set()
if save_paths:
for path in save_paths:
save_files.add(self.aug.get(path)[6:])
return save_files
try:
# Create Checkpoint
if temporary:
self.reverter.add_to_temp_checkpoint(
save_files, self.save_notes)
else:
self.reverter.add_to_checkpoint(save_files,
self.save_notes)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
def save(self, title=None, temporary=False):
"""Saves all changes to the configuration files.
This function first checks for save errors, if none are found,
all configuration changes made will be saved. According to the
function parameters. If an exception is raised, a new checkpoint
was not created.
:param str title: The title of the save. If a title is given, the
configuration will be saved as a new checkpoint and put in a
timestamped directory.
:param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (ie. challenges)
"""
save_files = self.unsaved_files()
if save_files:
self.add_to_checkpoint(save_files,
self.save_notes, temporary=temporary)
self.aug.set("/augeas/save", save_state)
self.save_notes = ""
self.aug.save()
@@ -147,10 +156,7 @@ class AugeasConfigurator(common.Plugin):
self.aug.remove("/files/"+sf)
self.aug.load()
if title and not temporary:
try:
self.reverter.finalize_checkpoint(title)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.finalize_checkpoint(title)
def _log_save_errors(self, ex_errs):
"""Log errors due to bad Augeas save.
@@ -175,10 +181,7 @@ class AugeasConfigurator(common.Plugin):
:raises .errors.PluginError: If unable to recover the configuration
"""
try:
self.reverter.recovery_routine()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
super(AugeasConfigurator, self).recovery_routine()
# Need to reload configuration after these changes take effect
self.aug.load()
@@ -188,10 +191,7 @@ class AugeasConfigurator(common.Plugin):
:raises .errors.PluginError: If unable to revert the challenge config.
"""
try:
self.reverter.revert_temporary_config()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.revert_temporary_config()
self.aug.load()
def rollback_checkpoints(self, rollback=1):
@@ -203,20 +203,5 @@ class AugeasConfigurator(common.Plugin):
the function is unable to correctly revert the configuration
"""
try:
self.reverter.rollback_checkpoints(rollback)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
super(AugeasConfigurator, self).rollback_checkpoints(rollback)
self.aug.load()
def view_config_changes(self):
"""Show all of the configuration changes that have taken place.
:raises .errors.PluginError: If there is a problem while processing
the checkpoints directories.
"""
try:
self.reverter.view_config_changes()
except errors.ReverterError as err:
raise errors.PluginError(str(err))

View File

@@ -1,6 +1,5 @@
"""Apache Configuration based off of Augeas Configurator."""
# pylint: disable=too-many-lines
import filecmp
import fnmatch
import logging
import os
@@ -96,7 +95,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
help="SSL vhost configuration extension.")
add("server-root", default=constants.os_constant("server_root"),
help="Apache server root directory.")
add("vhost-root", default=constants.os_constant("vhost_root"),
add("vhost-root", default=None,
help="Apache server VirtualHost configuration root")
add("logs-root", default=constants.os_constant("logs_root"),
help="Apache server logs directory")
@@ -134,6 +133,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser = None
self.version = version
self.vhosts = None
self.vhostroot = None
self._enhance_func = {"redirect": self._enable_redirect,
"ensure-http-header": self._set_http_header,
"staple-ocsp": self._enable_ocsp_stapling}
@@ -190,9 +190,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"version 1.2.0 or higher, please make sure you have you have "
"those installed.")
# Parse vhost-root if defined on cli
if not self.conf("vhost-root"):
self.vhostroot = constants.os_constant("vhost_root")
else:
self.vhostroot = os.path.abspath(self.conf("vhost-root"))
self.parser = parser.ApacheParser(
self.aug, self.conf("server-root"), self.conf("vhost-root"),
self.version)
self.version, configurator=self)
# Check for errors in parsing files with Augeas
self.check_parsing_errors("httpd.aug")
@@ -242,13 +248,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
a lack of directives
"""
# Choose vhost before (possible) enabling of mod_ssl, to keep the
# vhost choice namespace similar with the pre-validation one.
vhost = self.choose_vhost(domain)
self._clean_vhost(vhost)
# This is done first so that ssl module is enabled and cert_path,
# cert_key... can all be parsed appropriately
self.prepare_server_https("443")
# Add directives and remove duplicates
self._add_dummy_ssl_directives(vhost.path)
self._clean_vhost(vhost)
path = {"cert_path": self.parser.find_dir("SSLCertificateFile",
None, vhost.path),
"cert_key": self.parser.find_dir("SSLCertificateKeyFile",
@@ -290,6 +301,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.aug.set(path["cert_path"][-1], fullchain_path)
self.aug.set(path["cert_key"][-1], key_path)
# Enable the new vhost if needed
if not vhost.enabled:
self.enable_site(vhost)
# Save notes about the transaction that took place
self.save_notes += ("Changed vhost at %s with addresses of %s\n"
"\tSSLCertificateFile %s\n"
@@ -300,11 +315,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if chain_path is not None:
self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path
# Make sure vhost is enabled if distro with enabled / available
if self.conf("handle-sites"):
if not vhost.enabled:
self.enable_site(vhost)
def choose_vhost(self, target_name, temp=False):
"""Chooses a virtual host based on the given domain name.
@@ -347,9 +357,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
vhost = display_ops.select_vhost(target_name, self.vhosts)
if vhost is None:
logger.error(
"No vhost exists with servername or alias of: %s "
"(or it's in a file with multiple vhosts, which Certbot "
"can't parse yet). "
"No vhost exists with servername or alias of %s. "
"No vhost was selected. Please specify ServerName or ServerAlias "
"in the Apache config, or split vhosts into separate files.",
target_name)
@@ -581,17 +589,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if filename is None:
return None
if self.conf("handle-sites"):
is_enabled = self.is_site_enabled(filename)
else:
is_enabled = True
macro = False
if "/macro/" in path.lower():
macro = True
vhost_enabled = self.parser.parsed_in_original(filename)
vhost = obj.VirtualHost(filename, path, addrs, is_ssl,
is_enabled, modmacro=macro)
vhost_enabled, modmacro=macro)
self._add_servernames(vhost)
return vhost
@@ -646,7 +651,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
elif internal_path not in internal_paths[realpath]:
internal_paths[realpath].add(internal_path)
vhs.append(new_vhost)
return vhs
def is_name_vhost(self, target_addr):
@@ -857,14 +861,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
vh_p = self._get_new_vh_path(orig_matches, new_matches)
if not vh_p:
raise errors.PluginError(
"Could not reverse map the HTTPS VirtualHost to the original")
# The vhost was not found on the currently parsed paths
# Make Augeas aware of the new vhost
self.parser.parse_file(ssl_fp)
# Try to search again
new_matches = self.aug.match(
"/files%s//* [label()=~regexp('%s')]" %
(self._escape(ssl_fp),
parser.case_i("VirtualHost")))
vh_p = self._get_new_vh_path(orig_matches, new_matches)
if not vh_p:
raise errors.PluginError(
"Could not reverse map the HTTPS VirtualHost to the original")
# Update Addresses
self._update_ssl_vhosts_addrs(vh_p)
# Add directives
self._add_dummy_ssl_directives(vh_p)
self.save()
# Log actions and create save notes
logger.info("Created an SSL vhost at %s", ssl_fp)
@@ -875,6 +887,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Create the Vhost object
ssl_vhost = self._create_vhost(vh_p)
ssl_vhost.ancestor = nonssl_vhost
self.vhosts.append(ssl_vhost)
# NOTE: Searches through Augeas seem to ruin changes to directives
@@ -903,11 +916,29 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return None
def _get_ssl_vhost_path(self, non_ssl_vh_fp):
# Get filepath of new ssl_vhost
if non_ssl_vh_fp.endswith(".conf"):
return non_ssl_vh_fp[:-(len(".conf"))] + self.conf("le_vhost_ext")
""" Get a file path for SSL vhost, uses user defined path as priority,
but if the value is invalid or not defined, will fall back to non-ssl
vhost filepath.
:param str non_ssl_vh_fp: Filepath of non-SSL vhost
:returns: Filepath for SSL vhost
:rtype: str
"""
if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
# Defined by user on CLI
fp = os.path.join(os.path.realpath(self.vhostroot),
os.path.basename(non_ssl_vh_fp))
else:
return non_ssl_vh_fp + self.conf("le_vhost_ext")
# Use non-ssl filepath
fp = os.path.realpath(non_ssl_vh_fp)
if fp.endswith(".conf"):
return fp[:-(len(".conf"))] + self.conf("le_vhost_ext")
else:
return fp + self.conf("le_vhost_ext")
def _sift_rewrite_rule(self, line):
"""Decides whether a line should be copied to a SSL vhost.
@@ -972,6 +1003,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# The content does not include the closing tag, so add it
new_file.write("</VirtualHost>\n")
new_file.write("</IfModule>\n")
# Add new file to augeas paths if we're supposed to handle
# activation (it's not included as default)
if not self.parser.parsed_in_current(ssl_fp):
self.parser.parse_file(ssl_fp)
except IOError:
logger.fatal("Error writing/reading to file in make_vhost_ssl")
raise errors.PluginError("Unable to write/read in make_vhost_ssl")
@@ -1612,7 +1647,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.name
redirect_filepath = os.path.join(self.conf("vhost-root"),
redirect_filepath = os.path.join(self.vhostroot,
redirect_filename)
# Register the new file that will be created
@@ -1623,6 +1658,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Write out file
with open(redirect_filepath, "w") as redirect_file:
redirect_file.write(text)
# Add new include to configuration if it doesn't exist yet
if not self.parser.parsed_in_current(redirect_filepath):
self.parser.parse_file(redirect_filepath)
logger.info("Created redirect file: %s", redirect_filename)
return redirect_filepath
@@ -1662,32 +1702,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return redirects
def is_site_enabled(self, avail_fp):
"""Checks to see if the given site is enabled.
.. todo:: fix hardcoded sites-enabled, check os.path.samefile
:param str avail_fp: Complete file path of available site
:returns: Success
:rtype: bool
"""
enabled_dir = os.path.join(self.parser.root, "sites-enabled")
if not os.path.isdir(enabled_dir):
error_msg = ("Directory '{0}' does not exist. Please ensure "
"that the values for --apache-handle-sites and "
"--apache-server-root are correct for your "
"environment.".format(enabled_dir))
raise errors.ConfigurationError(error_msg)
for entry in os.listdir(enabled_dir):
try:
if filecmp.cmp(avail_fp, os.path.join(enabled_dir, entry)):
return True
except OSError:
pass
return False
def enable_site(self, vhost):
"""Enables an available site, Apache reload required.
@@ -1707,21 +1721,40 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
supported.
"""
if self.is_site_enabled(vhost.filep):
if vhost.enabled:
return
if "/sites-available/" in vhost.filep:
enabled_path = ("%s/sites-enabled/%s" %
(self.parser.root, os.path.basename(vhost.filep)))
self.reverter.register_file_creation(False, enabled_path)
# Handle non-debian systems
if not self.conf("handle-sites"):
if not self.parser.parsed_in_original(vhost.filep):
# Add direct include to root conf
self.parser.add_include(self.parser.loc["default"], vhost.filep)
vhost.enabled = True
return
enabled_path = ("%s/sites-enabled/%s" %
(self.parser.root, os.path.basename(vhost.filep)))
self.reverter.register_file_creation(False, enabled_path)
try:
os.symlink(vhost.filep, enabled_path)
vhost.enabled = True
logger.info("Enabling available site: %s", vhost.filep)
self.save_notes += "Enabled site %s\n" % vhost.filep
else:
raise errors.NotSupportedError(
"Unsupported filesystem layout. "
"sites-available/enabled expected.")
except OSError as err:
if os.path.islink(enabled_path) and os.path.realpath(
enabled_path) == vhost.filep:
# Already in shape
vhost.enabled = True
return
else:
logger.warning(
"Could not symlink %s to %s, got error: %s", enabled_path,
vhost.filep, err.strerror)
errstring = ("Encountered error while trying to enable a " +
"newly created VirtualHost located at {0} by " +
"linking to it from {1}")
raise errors.NotSupportedError(errstring.format(vhost.filep,
enabled_path))
vhost.enabled = True
logger.info("Enabling available site: %s", vhost.filep)
self.save_notes += "Enabled site %s\n" % vhost.filep
def enable_mod(self, mod_name, temp=False):
"""Enables module in Apache.
@@ -1991,5 +2024,5 @@ def install_ssl_options_conf(options_ssl, options_ssl_digest):
# XXX if we ever try to enforce a local privilege boundary (eg, running
# certbot for unprivileged users via setuid), this function will need
# to be modified.
return common.install_ssl_options_conf(options_ssl, options_ssl_digest,
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
constants.os_constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)

View File

@@ -85,10 +85,12 @@ def _vhost_menu(domain, vhosts):
"vhosts are not yet supported)".format(domain, os.linesep),
choices, force_interactive=True)
except errors.MissingCommandlineFlag:
msg = ("Encountered vhost ambiguity but unable to ask for user guidance in "
"non-interactive mode. Currently Certbot needs each vhost to be "
"in its own conf file, and may need vhosts to be explicitly "
"labelled with ServerName or ServerAlias directives.")
msg = (
"Encountered vhost ambiguity when trying to find a vhost for "
"{0} but was unable to ask for user "
"guidance in non-interactive mode. Certbot may need "
"vhosts to be explicitly labelled with ServerName or "
"ServerAlias directives.".format(domain))
logger.warning(msg)
raise errors.MissingCommandlineFlag(msg)

View File

@@ -1,4 +1,5 @@
"""ApacheParser is a member object of the ApacheConfigurator class."""
import copy
import fnmatch
import logging
import os
@@ -30,9 +31,15 @@ class ApacheParser(object):
arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
fnmatch_chars = set(["*", "?", "\\", "[", "]"])
def __init__(self, aug, root, vhostroot, version=(2, 4)):
def __init__(self, aug, root, vhostroot=None, version=(2, 4),
configurator=None):
# Note: Order is important here.
# Needed for calling save() with reverter functionality that resides in
# AugeasConfigurator superclass of ApacheConfigurator. This resolves
# issues with aug.load() after adding new files / defines to parse tree
self.configurator = configurator
# This uses the binary, so it can be done first.
# https://httpd.apache.org/docs/2.4/mod/core.html#define
# https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine
@@ -46,9 +53,7 @@ class ApacheParser(object):
# Find configuration root and make sure augeas can parse it.
self.root = os.path.abspath(root)
self.loc = {"root": self._find_config_root()}
self._parse_file(self.loc["root"])
self.vhostroot = os.path.abspath(vhostroot)
self.parse_file(self.loc["root"])
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
@@ -62,15 +67,42 @@ class ApacheParser(object):
# Set up rest of locations
self.loc.update(self._set_locations())
# Must also attempt to parse virtual host root
self._parse_file(self.vhostroot + "/" +
constants.os_constant("vhost_files"))
self.existing_paths = copy.deepcopy(self.parser_paths)
# Must also attempt to parse additional virtual host root
if vhostroot:
self.parse_file(os.path.abspath(vhostroot) + "/" +
constants.os_constant("vhost_files"))
# check to see if there were unparsed define statements
if version < (2, 4):
if self.find_dir("Define", exclude=False):
raise errors.PluginError("Error parsing runtime variables")
def add_include(self, main_config, inc_path):
"""Add Include for a new configuration file if one does not exist
:param str main_config: file path to main Apache config file
:param str inc_path: path of file to include
"""
if len(self.find_dir(case_i("Include"), inc_path)) == 0:
logger.debug("Adding Include %s to %s",
inc_path, get_aug_path(main_config))
self.add_dir(
get_aug_path(main_config),
"Include", inc_path)
# Add new path to parser paths
new_dir = os.path.dirname(inc_path)
new_file = os.path.basename(inc_path)
if new_dir in self.existing_paths.keys():
# Add to existing path
self.existing_paths[new_dir].append(new_file)
else:
# Create a new path
self.existing_paths[new_dir] = [new_file]
def init_modules(self):
"""Iterates on the configuration until no new modules are loaded.
@@ -428,9 +460,9 @@ class ApacheParser(object):
# Attempts to add a transform to the file if one does not already exist
if os.path.isdir(arg):
self._parse_file(os.path.join(arg, "*"))
self.parse_file(os.path.join(arg, "*"))
else:
self._parse_file(arg)
self.parse_file(arg)
# Argument represents an fnmatch regular expression, convert it
# Split up the path and convert each into an Augeas accepted regex
@@ -470,7 +502,7 @@ class ApacheParser(object):
# Since Python 3.6, it returns a different pattern like (?s:.*\.load)\Z
return fnmatch.translate(clean_fn_match)[4:-3]
def _parse_file(self, filepath):
def parse_file(self, filepath):
"""Parse file with Augeas
Checks to see if file_path is parsed by Augeas
@@ -480,6 +512,10 @@ class ApacheParser(object):
"""
use_new, remove_old = self._check_path_actions(filepath)
# Ensure that we have the latest Augeas DOM state on disk before
# calling aug.load() which reloads the state from disk
if self.configurator:
self.configurator.ensure_augeas_state()
# Test if augeas included file for Httpd.lens
# Note: This works for augeas globs, ie. *.conf
if use_new:
@@ -494,6 +530,39 @@ class ApacheParser(object):
self._add_httpd_transform(filepath)
self.aug.load()
def parsed_in_current(self, filep):
"""Checks if the file path is parsed by current Augeas parser config
ie. returns True if the file is found on a path that's found in live
Augeas configuration.
:param str filep: Path to match
:returns: True if file is parsed in existing configuration tree
:rtype: bool
"""
return self._parsed_by_parser_paths(filep, self.parser_paths)
def parsed_in_original(self, filep):
"""Checks if the file path is parsed by existing Apache config.
ie. returns True if the file is found on a path that matches Include or
IncludeOptional statement in the Apache configuration.
:param str filep: Path to match
:returns: True if file is parsed in existing configuration tree
:rtype: bool
"""
return self._parsed_by_parser_paths(filep, self.existing_paths)
def _parsed_by_parser_paths(self, filep, paths):
"""Helper function that searches through provided paths and returns
True if file path is found in the set"""
for directory in paths.keys():
for filename in paths[directory]:
if fnmatch.fnmatch(filep, os.path.join(directory, filename)):
return True
return False
def _check_path_actions(self, filepath):
"""Determine actions to take with a new augeas path
@@ -622,7 +691,6 @@ class ApacheParser(object):
for name in location:
if os.path.isfile(os.path.join(self.root, name)):
return os.path.join(self.root, name)
raise errors.NoInstallationError("Could not find configuration root")

View File

@@ -26,6 +26,7 @@ function Setup() {
ErrorLog /tmp/error.log
CustomLog /tmp/requests.log combined
</VirtualHost>" | sudo tee $EA/sites-available/throwaway-example.conf >/dev/null
sudo ln -sf $EA/sites-available/throwaway-example.conf $EA/sites-enabled/throwaway-example.conf
else
TMP="/tmp/`basename \"$APPEND_APACHECONF\"`.$$"
sudo cp -a "$APPEND_APACHECONF" "$TMP"
@@ -37,6 +38,7 @@ function Cleanup() {
if [ "$APPEND_APACHECONF" = "" ] ; then
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
sudo rm $EA/sites-available/throwaway-example.conf
sudo rm $EA/sites-enabled/throwaway-example.conf
else
sudo mv "$TMP" "$APPEND_APACHECONF"
fi

View File

@@ -31,7 +31,7 @@ class AugeasConfiguratorTest(util.ApacheTest):
def test_bad_parse(self):
# pylint: disable=protected-access
self.config.parser._parse_file(os.path.join(
self.config.parser.parse_file(os.path.join(
self.config.parser.root, "conf-available", "bad_conf_file.conf"))
self.assertRaises(
errors.PluginError, self.config.check_parsing_errors, "httpd.aug")

View File

@@ -8,6 +8,7 @@ import unittest
import mock
# six is used in mock.patch()
import six # pylint: disable=unused-import
import tempfile
from acme import challenges
@@ -34,9 +35,20 @@ class MultipleVhostsTest(util.ApacheTest):
def setUp(self): # pylint: disable=arguments-differ
super(MultipleVhostsTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.config = self.mock_deploy_cert(self.config)
from certbot_apache.constants import os_constant
orig_os_constant = os_constant
def mock_os_constant(key, vhost_path=self.vhost_path):
"""Mock default vhost path"""
if key == "vhost_root":
return vhost_path
else:
return orig_os_constant(key)
with mock.patch("certbot_apache.constants.os_constant") as mock_c:
mock_c.side_effect = mock_os_constant
self.config = util.get_apache_configurator(
self.config_path, None, self.config_dir, self.work_dir)
self.config = self.mock_deploy_cert(self.config)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
@@ -121,19 +133,20 @@ class MultipleVhostsTest(util.ApacheTest):
@certbot_util.patch_get_utility()
def test_get_all_names(self, mock_getutility):
mock_getutility.notification = mock.MagicMock(return_value=True)
mock_utility = mock_getutility()
mock_utility.notification = mock.MagicMock(return_value=True)
names = self.config.get_all_names()
self.assertEqual(names, set(
["certbot.demo", "ocspvhost.com", "encryption-example.demo"]
["certbot.demo", "ocspvhost.com", "encryption-example.demo",
"nonsym.link", "vhost.in.rootconf"]
))
@certbot_util.patch_get_utility()
@mock.patch("certbot_apache.configurator.socket.gethostbyaddr")
def test_get_all_names_addrs(self, mock_gethost, mock_getutility):
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
notification = mock.Mock()
notification.notification = mock.Mock(return_value=True)
mock_getutility.return_value = notification
mock_utility = mock_getutility()
mock_utility.notification.return_value = True
vhost = obj.VirtualHost(
"fp", "ap",
set([obj.Addr(("8.8.8.8", "443")),
@@ -145,7 +158,7 @@ class MultipleVhostsTest(util.ApacheTest):
names = self.config.get_all_names()
# Names get filtered, only 5 are returned
self.assertEqual(len(names), 5)
self.assertEqual(len(names), 7)
self.assertTrue("zombo.com" in names)
self.assertTrue("google.com" in names)
self.assertTrue("certbot.demo" in names)
@@ -159,7 +172,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_get_aug_internal_path(self):
from certbot_apache.configurator import get_internal_aug_path
internal_paths = [
"VirtualHost", "IfModule/VirtualHost", "VirtualHost", "VirtualHost",
"Virtualhost", "IfModule/VirtualHost", "VirtualHost", "VirtualHost",
"Macro/VirtualHost", "IfModule/VirtualHost", "VirtualHost",
"IfModule/VirtualHost"]
@@ -185,14 +198,9 @@ class MultipleVhostsTest(util.ApacheTest):
self.vh_truth[2].get_names(), set(["*.le.co", "ip-172-30-0-17"]))
def test_get_virtual_hosts(self):
"""Make sure all vhosts are being properly found.
.. note:: If test fails, only finding 1 Vhost... it is likely that
it is a problem with is_enabled. If finding only 3, likely is_ssl
"""
"""Make sure all vhosts are being properly found."""
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 8)
self.assertEqual(len(vhs), 10)
found = 0
for vhost in vhs:
@@ -203,7 +211,7 @@ class MultipleVhostsTest(util.ApacheTest):
else:
raise Exception("Missed: %s" % vhost) # pragma: no cover
self.assertEqual(found, 8)
self.assertEqual(found, 10)
# Handle case of non-debian layout get_virtual_hosts
with mock.patch(
@@ -211,7 +219,7 @@ class MultipleVhostsTest(util.ApacheTest):
) as mock_conf:
mock_conf.return_value = False
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 8)
self.assertEqual(len(vhs), 10)
@mock.patch("certbot_apache.display_ops.select_vhost")
def test_choose_vhost_none_avail(self, mock_select):
@@ -226,8 +234,10 @@ class MultipleVhostsTest(util.ApacheTest):
self.vh_truth[1], self.config.choose_vhost("none.com"))
@mock.patch("certbot_apache.display_ops.select_vhost")
def test_choose_vhost_select_vhost_non_ssl(self, mock_select):
@mock.patch("certbot_apache.obj.VirtualHost.conflicts")
def test_choose_vhost_select_vhost_non_ssl(self, mock_conf, mock_select):
mock_select.return_value = self.vh_truth[0]
mock_conf.return_value = False
chosen_vhost = self.config.choose_vhost("none.com")
self.vh_truth[0].aliases.add("none.com")
self.assertEqual(
@@ -237,6 +247,15 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertFalse(self.vh_truth[0].ssl)
self.assertTrue(chosen_vhost.ssl)
@mock.patch("certbot_apache.configurator.ApacheConfigurator._find_best_vhost")
@mock.patch("certbot_apache.parser.ApacheParser.add_dir")
def test_choose_vhost_and_servername_addition(self, mock_add, mock_find):
ret_vh = self.vh_truth[8]
ret_vh.enabled = False
mock_find.return_value = self.vh_truth[8]
self.config.choose_vhost("whatever.com")
self.assertTrue(mock_add.called)
@mock.patch("certbot_apache.display_ops.select_vhost")
def test_choose_vhost_select_vhost_with_temp(self, mock_select):
mock_select.return_value = self.vh_truth[0]
@@ -288,9 +307,9 @@ class MultipleVhostsTest(util.ApacheTest):
# Assume only the two default vhosts.
self.config.vhosts = [
vh for vh in self.config.vhosts
if vh.name not in ["certbot.demo",
if vh.name not in ["certbot.demo", "nonsym.link",
"encryption-example.demo",
"ocspvhost.com"]
"ocspvhost.com", "vhost.in.rootconf"]
and "*.blue.purple.com" not in vh.aliases
]
self.assertEqual(
@@ -299,26 +318,7 @@ class MultipleVhostsTest(util.ApacheTest):
def test_non_default_vhosts(self):
# pylint: disable=protected-access
self.assertEqual(len(self.config._non_default_vhosts()), 6)
def test_is_site_enabled(self):
"""Test if site is enabled.
.. note:: This test currently fails for hard links
(which may happen if you move dirs incorrectly)
.. warning:: This test does not work when running using the
unittest.main() function. It incorrectly copies symlinks.
"""
self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep))
self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep))
with mock.patch("os.path.isdir") as mock_isdir:
mock_isdir.return_value = False
self.assertRaises(errors.ConfigurationError,
self.config.is_site_enabled,
"irrelevant")
self.assertEqual(len(self.config._non_default_vhosts()), 8)
@mock.patch("certbot.util.run_script")
@mock.patch("certbot.util.exe_exists")
@@ -345,21 +345,59 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertRaises(
errors.MisconfigurationError, self.config.enable_mod, "ssl")
def test_enable_site(self):
# Default 443 vhost
self.assertFalse(self.vh_truth[1].enabled)
self.config.enable_site(self.vh_truth[1])
def test_enable_site_already_enabled(self):
self.assertTrue(self.vh_truth[1].enabled)
# Go again to make sure nothing fails
self.config.enable_site(self.vh_truth[1])
def test_enable_site_failure(self):
self.config.parser.root = "/tmp/nonexistent"
self.assertRaises(
errors.NotSupportedError,
self.config.enable_site,
obj.VirtualHost("asdf", "afsaf", set(), False, False))
def test_enable_site_nondebian(self):
mock_c = "certbot_apache.configurator.ApacheConfigurator.conf"
def conf_side_effect(arg):
""" Mock function for ApacheConfigurator.conf """
confvars = {"handle-sites": False}
if arg in confvars:
return confvars[arg]
inc_path = "/path/to/whereever"
vhost = self.vh_truth[0]
with mock.patch(mock_c) as mock_conf:
mock_conf.side_effect = conf_side_effect
vhost.enabled = False
vhost.filep = inc_path
self.assertFalse(self.config.parser.find_dir("Include", inc_path))
self.assertFalse(
os.path.dirname(inc_path) in self.config.parser.existing_paths)
self.config.enable_site(vhost)
self.assertTrue(self.config.parser.find_dir("Include", inc_path))
self.assertTrue(
os.path.dirname(inc_path) in self.config.parser.existing_paths)
self.assertTrue(
os.path.basename(inc_path) in self.config.parser.existing_paths[
os.path.dirname(inc_path)])
def test_deploy_cert_enable_new_vhost(self):
# Create
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.assertFalse(ssl_vhost.enabled)
self.config.deploy_cert(
"encryption-example.demo", "example/cert.pem", "example/key.pem",
"example/cert_chain.pem", "example/fullchain.pem")
self.assertTrue(ssl_vhost.enabled)
# Make sure that we don't error out if symlink already exists
ssl_vhost.enabled = False
self.assertFalse(ssl_vhost.enabled)
self.config.deploy_cert(
"encryption-example.demo", "example/cert.pem", "example/key.pem",
"example/cert_chain.pem", "example/fullchain.pem")
self.assertTrue(ssl_vhost.enabled)
def test_deploy_cert_newssl(self):
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir,
@@ -388,12 +426,14 @@ class MultipleVhostsTest(util.ApacheTest):
# Verify one directive was found in the correct file
self.assertEqual(len(loc_cert), 1)
self.assertEqual(configurator.get_file_path(loc_cert[0]),
self.vh_truth[1].filep)
self.assertEqual(
configurator.get_file_path(loc_cert[0]),
self.vh_truth[1].filep)
self.assertEqual(len(loc_key), 1)
self.assertEqual(configurator.get_file_path(loc_key[0]),
self.vh_truth[1].filep)
self.assertEqual(
configurator.get_file_path(loc_key[0]),
self.vh_truth[1].filep)
def test_deploy_cert_newssl_no_fullchain(self):
self.config = util.get_apache_configurator(
@@ -427,10 +467,75 @@ class MultipleVhostsTest(util.ApacheTest):
"random.demo", "example/cert.pem",
"example/key.pem"))
def test_deploy_cert_not_parsed_path(self):
# Make sure that we add include to root config for vhosts when
# handle-sites is false
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
tmp_path = os.path.realpath(tempfile.mkdtemp("vhostroot"))
os.chmod(tmp_path, 0o755)
mock_p = "certbot_apache.configurator.ApacheConfigurator._get_ssl_vhost_path"
mock_a = "certbot_apache.parser.ApacheParser.add_include"
mock_c = "certbot_apache.configurator.ApacheConfigurator.conf"
orig_conf = self.config.conf
def conf_side_effect(arg):
""" Mock function for ApacheConfigurator.conf """
confvars = {"handle-sites": False}
if arg in confvars:
return confvars[arg]
else:
return orig_conf("arg")
with mock.patch(mock_c) as mock_conf:
mock_conf.side_effect = conf_side_effect
with mock.patch(mock_p) as mock_path:
mock_path.return_value = os.path.join(tmp_path, "whatever.conf")
with mock.patch(mock_a) as mock_add:
self.config.deploy_cert(
"encryption-example.demo",
"example/cert.pem", "example/key.pem",
"example/cert_chain.pem")
# Test that we actually called add_include
self.assertTrue(mock_add.called)
shutil.rmtree(tmp_path)
def test_deploy_cert(self):
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
# Patch _add_dummy_ssl_directives to make sure we write them correctly
# pylint: disable=protected-access
orig_add_dummy = self.config._add_dummy_ssl_directives
def mock_add_dummy_ssl(vhostpath):
"""Mock method for _add_dummy_ssl_directives"""
def find_args(path, directive):
"""Return list of arguments in requested directive at path"""
f_args = []
dirs = self.config.parser.find_dir(directive, None,
path)
for d in dirs:
f_args.append(self.config.parser.get_arg(d))
return f_args
# Verify that the dummy directives do not exist
self.assertFalse(
"insert_cert_file_path" in find_args(vhostpath,
"SSLCertificateFile"))
self.assertFalse(
"insert_key_file_path" in find_args(vhostpath,
"SSLCertificateKeyFile"))
orig_add_dummy(vhostpath)
# Verify that the dummy directives exist
self.assertTrue(
"insert_cert_file_path" in find_args(vhostpath,
"SSLCertificateFile"))
self.assertTrue(
"insert_key_file_path" in find_args(vhostpath,
"SSLCertificateKeyFile"))
# pylint: disable=protected-access
self.config._add_dummy_ssl_directives = mock_add_dummy_ssl
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
self.config.deploy_cert(
@@ -452,16 +557,19 @@ class MultipleVhostsTest(util.ApacheTest):
# Verify one directive was found in the correct file
self.assertEqual(len(loc_cert), 1)
self.assertEqual(configurator.get_file_path(loc_cert[0]),
self.vh_truth[1].filep)
self.assertEqual(
configurator.get_file_path(loc_cert[0]),
self.vh_truth[1].filep)
self.assertEqual(len(loc_key), 1)
self.assertEqual(configurator.get_file_path(loc_key[0]),
self.vh_truth[1].filep)
self.assertEqual(
configurator.get_file_path(loc_key[0]),
self.vh_truth[1].filep)
self.assertEqual(len(loc_chain), 1)
self.assertEqual(configurator.get_file_path(loc_chain[0]),
self.vh_truth[1].filep)
self.assertEqual(
configurator.get_file_path(loc_chain[0]),
self.vh_truth[1].filep)
# One more time for chain directive setting
self.config.deploy_cert(
@@ -614,6 +722,30 @@ class MultipleVhostsTest(util.ApacheTest):
mock_span.return_value = return_value
self.test_make_vhost_ssl()
def test_make_vhost_ssl_nonsymlink(self):
ssl_vhost_slink = self.config.make_vhost_ssl(self.vh_truth[8])
self.assertTrue(ssl_vhost_slink.ssl)
self.assertTrue(ssl_vhost_slink.enabled)
self.assertEqual(ssl_vhost_slink.name, "nonsym.link")
def test_make_vhost_ssl_nonexistent_vhost_path(self):
def conf_side_effect(arg):
""" Mock function for ApacheConfigurator.conf """
confvars = {
"vhost-root": "/tmp/nonexistent",
"le_vhost_ext": "-le-ssl.conf",
"handle-sites": True}
return confvars[arg]
with mock.patch(
"certbot_apache.configurator.ApacheConfigurator.conf"
) as mock_conf:
mock_conf.side_effect = conf_side_effect
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
self.assertEqual(os.path.dirname(ssl_vhost.filep),
os.path.dirname(os.path.realpath(
self.vh_truth[1].filep)))
def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
@@ -623,22 +755,17 @@ class MultipleVhostsTest(util.ApacheTest):
"encryption-example-le-ssl.conf"))
self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
"/files" + ssl_vhost.filep + "/IfModule/Virtualhost")
self.assertEqual(len(ssl_vhost.addrs), 1)
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
self.assertEqual(ssl_vhost.name, "encryption-example.demo")
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateFile", None, ssl_vhost.path, False))
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateKeyFile", None, ssl_vhost.path, False))
self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]),
self.config.is_name_vhost(ssl_vhost))
self.assertEqual(len(self.config.vhosts), 9)
self.assertEqual(len(self.config.vhosts), 11)
def test_clean_vhost_ssl(self):
# pylint: disable=protected-access
@@ -688,17 +815,17 @@ class MultipleVhostsTest(util.ApacheTest):
DIRECTIVES = ["Foo", "Bar"]
for directive in DIRECTIVES:
for _ in range(10):
self.config.parser.add_dir(self.vh_truth[1].path,
self.config.parser.add_dir(self.vh_truth[2].path,
directive, ["baz"])
self.config.save()
self.config._remove_directives(self.vh_truth[1].path, DIRECTIVES)
self.config._remove_directives(self.vh_truth[2].path, DIRECTIVES)
self.config.save()
for directive in DIRECTIVES:
self.assertEqual(
len(self.config.parser.find_dir(
directive, None, self.vh_truth[1].path, False)), 0)
directive, None, self.vh_truth[2].path, False)), 0)
def test_make_vhost_ssl_bad_write(self):
mock_open = mock.mock_open()
@@ -717,10 +844,10 @@ class MultipleVhostsTest(util.ApacheTest):
def test_add_name_vhost_if_necessary(self):
# pylint: disable=protected-access
self.config.save = mock.Mock()
self.config.add_name_vhost = mock.Mock()
self.config.version = (2, 2)
self.config._add_name_vhost_if_necessary(self.vh_truth[0])
self.assertTrue(self.config.save.called)
self.assertTrue(self.config.add_name_vhost.called)
new_addrs = set()
for addr in self.vh_truth[0].addrs:
@@ -728,7 +855,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.vh_truth[0].addrs = new_addrs
self.config._add_name_vhost_if_necessary(self.vh_truth[0])
self.assertEqual(self.config.save.call_count, 2)
self.assertEqual(self.config.add_name_vhost.call_count, 2)
@mock.patch("certbot_apache.configurator.tls_sni_01.ApacheTlsSni01.perform")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@@ -915,7 +1042,6 @@ class MultipleVhostsTest(util.ApacheTest):
"SSLUseStapling", "on", ssl_vhost.path)
self.assertEqual(len(ssl_use_stapling_aug_path), 1)
ssl_vhost_aug_path = parser.get_aug_path(ssl_vhost.filep)
stapling_cache_aug_path = self.config.parser.find_dir('SSLStaplingCache',
"shmcb:/var/run/apache2/stapling_cache(128000)",
@@ -1177,7 +1303,7 @@ class MultipleVhostsTest(util.ApacheTest):
# pylint: disable=protected-access
self.config._enable_redirect(self.vh_truth[1], "")
self.assertEqual(len(self.config.vhosts), 9)
self.assertEqual(len(self.config.vhosts), 11)
def test_create_own_redirect_for_old_apache_version(self):
self.config.parser.modules.add("rewrite_module")
@@ -1188,7 +1314,7 @@ class MultipleVhostsTest(util.ApacheTest):
# pylint: disable=protected-access
self.config._enable_redirect(self.vh_truth[1], "")
self.assertEqual(len(self.config.vhosts), 9)
self.assertEqual(len(self.config.vhosts), 11)
def test_sift_rewrite_rule(self):
# pylint: disable=protected-access
@@ -1285,13 +1411,17 @@ class AugeasVhostsTest(util.ApacheTest):
for name in names:
self.assertFalse(name in self.config.choose_vhost(name).aliases)
def test_choose_vhost_without_matching_wildcard(self):
@mock.patch("certbot_apache.obj.VirtualHost.conflicts")
def test_choose_vhost_without_matching_wildcard(self, mock_conflicts):
mock_conflicts.return_value = False
mock_path = "certbot_apache.display_ops.select_vhost"
with mock.patch(mock_path, lambda _, vhosts: vhosts[0]):
for name in ("a.example.net", "other.example.net"):
self.assertTrue(name in self.config.choose_vhost(name).aliases)
def test_choose_vhost_wildcard_not_found(self):
@mock.patch("certbot_apache.obj.VirtualHost.conflicts")
def test_choose_vhost_wildcard_not_found(self, mock_conflicts):
mock_conflicts.return_value = False
mock_path = "certbot_apache.display_ops.select_vhost"
names = (
"abc.example.net", "not.there.tld", "aa.wildcard.tld"
@@ -1358,10 +1488,6 @@ class MultiVhostsTest(util.ApacheTest):
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateFile", None, ssl_vhost.path, False))
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateKeyFile", None, ssl_vhost.path, False))
self.assertEqual(self.config.is_name_vhost(self.vh_truth[1]),
self.config.is_name_vhost(ssl_vhost))
@@ -1497,7 +1623,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
with mock.patch("certbot.plugins.common.logger") as mock_logger:
self._call()
self.assertEqual(mock_logger.warning.call_args[0][0],
"%s has been manually modified; updated ssl configuration options "
"%s has been manually modified; updated file "
"saved to %s. We recommend updating %s for security purposes.")
self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")),
self._current_ssl_options_hash())

View File

@@ -38,7 +38,7 @@ class BasicParserTest(util.ParserTest):
file_path = os.path.join(
self.config_path, "not-parsed-by-default", "certbot.conf")
self.parser._parse_file(file_path) # pylint: disable=protected-access
self.parser.parse_file(file_path) # pylint: disable=protected-access
# search for the httpd incl
matches = self.parser.aug.match(
@@ -52,7 +52,7 @@ class BasicParserTest(util.ParserTest):
test2 = self.parser.find_dir("documentroot")
self.assertEqual(len(test), 1)
self.assertEqual(len(test2), 4)
self.assertEqual(len(test2), 7)
def test_add_dir(self):
aug_default = "/files" + self.parser.loc["default"]

View File

@@ -0,0 +1 @@
../sites-available/another_wildcard.conf

View File

@@ -0,0 +1 @@
../sites-available/old,default.conf

View File

@@ -0,0 +1 @@
../sites-available/wildcard.conf

View File

@@ -193,4 +193,15 @@ IncludeOptional conf-enabled/*.conf
# Include the virtual host configurations:
IncludeOptional sites-enabled/*.conf
<VirtualHost *:80>
ServerName vhost.in.rootconf
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View File

@@ -1,4 +1,4 @@
<VirtualHost *:80>
<Virtualhost *:80>
ServerName encryption-example.demo
ServerAdmin webmaster@localhost
@@ -39,4 +39,4 @@
Allow from 127.0.0.0/255.0.0.0 ::1/128
</Directory>
</VirtualHost>
</Virtualhost>

View File

@@ -0,0 +1 @@
../sites-available/default-ssl-port-only.conf

View File

@@ -0,0 +1 @@
../sites-available/default-ssl.conf

View File

@@ -0,0 +1,9 @@
<VirtualHost *:80>
ServerName nonsym.link
ServerAdmin webmaster@localhost
DocumentRoot /var/www-certbot-reworld/static/
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

View File

@@ -0,0 +1 @@
../sites-available/wildcard.conf

View File

@@ -45,6 +45,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
return
for vhost_basename in os.listdir(sites_enabled):
# Keep the one non-symlink test vhost in place
if vhost_basename == "non-symlink.conf":
continue
vhost = os.path.join(sites_enabled, vhost_basename)
if not os.path.islink(vhost): # pragma: no cover
os.remove(vhost)
@@ -115,18 +118,20 @@ def get_vh_truth(temp_dir, config_name):
"""Return the ground truth for the specified directory."""
if config_name == "debian_apache_2_4/multiple_vhosts":
prefix = os.path.join(
temp_dir, config_name, "apache2/sites-available")
temp_dir, config_name, "apache2/sites-enabled")
aug_pre = "/files" + prefix
vh_truth = [
obj.VirtualHost(
os.path.join(prefix, "encryption-example.conf"),
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
os.path.join(aug_pre, "encryption-example.conf/Virtualhost"),
set([obj.Addr.fromstring("*:80")]),
False, True, "encryption-example.demo"),
obj.VirtualHost(
os.path.join(prefix, "default-ssl.conf"),
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
set([obj.Addr.fromstring("_default_:443")]), True, False),
os.path.join(aug_pre,
"default-ssl.conf/IfModule/VirtualHost"),
set([obj.Addr.fromstring("_default_:443")]), True, True),
obj.VirtualHost(
os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"),
@@ -148,17 +153,34 @@ def get_vh_truth(temp_dir, config_name):
os.path.join(prefix, "default-ssl-port-only.conf"),
os.path.join(aug_pre, ("default-ssl-port-only.conf/"
"IfModule/VirtualHost")),
set([obj.Addr.fromstring("_default_:443")]), True, False),
set([obj.Addr.fromstring("_default_:443")]), True, True),
obj.VirtualHost(
os.path.join(prefix, "wildcard.conf"),
os.path.join(aug_pre, "wildcard.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, False,
set([obj.Addr.fromstring("*:80")]), False, True,
"ip-172-30-0-17", aliases=["*.blue.purple.com"]),
obj.VirtualHost(
os.path.join(prefix, "ocsp-ssl.conf"),
os.path.join(aug_pre, "ocsp-ssl.conf/IfModule/VirtualHost"),
set([obj.Addr.fromstring("10.2.3.4:443")]), True, True,
"ocspvhost.com")]
"ocspvhost.com"),
obj.VirtualHost(
os.path.join(prefix, "non-symlink.conf"),
os.path.join(aug_pre, "non-symlink.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
"nonsym.link"),
obj.VirtualHost(
os.path.join(prefix, "default-ssl-port-only.conf"),
os.path.join(aug_pre,
"default-ssl-port-only.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), True, True, ""),
obj.VirtualHost(
os.path.join(temp_dir, config_name,
"apache2/apache2.conf"),
"/files" + os.path.join(temp_dir, config_name,
"apache2/apache2.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
"vhost.in.rootconf")]
return vh_truth
if config_name == "debian_apache_2_4/multi_vhosts":
prefix = os.path.join(

View File

@@ -7,7 +7,6 @@ from certbot.plugins import common
from certbot.errors import PluginError, MissingCommandlineFlag
from certbot_apache import obj
from certbot_apache import parser
logger = logging.getLogger(__name__)
@@ -105,7 +104,8 @@ class ApacheTlsSni01(common.TLSSNI01):
config_text += "</IfModule>\n"
self._conf_include_check(self.configurator.parser.loc["default"])
self.configurator.parser.add_include(
self.configurator.parser.loc["default"], self.challenge_conf)
self.configurator.reverter.register_file_creation(
True, self.challenge_conf)
@@ -126,9 +126,8 @@ class ApacheTlsSni01(common.TLSSNI01):
vhost = self.configurator.choose_vhost(achall.domain, temp=True)
except (PluginError, MissingCommandlineFlag):
# We couldn't find the virtualhost for this domain, possibly
# because it's a new vhost that's not configured yet (GH #677),
# or perhaps because there were multiple <VirtualHost> sections
# in the config file (GH #1042). See also GH #2600.
# because it's a new vhost that's not configured yet
# (GH #677). See also GH #2600.
logger.warning("Falling back to default vhost %s...", default_addr)
addrs.add(default_addr)
return addrs
@@ -143,24 +142,6 @@ class ApacheTlsSni01(common.TLSSNI01):
return addrs
def _conf_include_check(self, main_config):
"""Add TLS-SNI-01 challenge conf file into configuration.
Adds TLS-SNI-01 challenge include file if it does not already exist
within mainConfig
:param str main_config: file path to main user apache config file
"""
if len(self.configurator.parser.find_dir(
parser.case_i("Include"), self.challenge_conf)) == 0:
# print "Including challenge virtual host(s)"
logger.debug("Adding Include %s to %s",
self.challenge_conf, parser.get_aug_path(main_config))
self.configurator.parser.add_dir(
parser.get_aug_path(main_config),
"Include", self.challenge_conf)
def _get_config_text(self, achall, ip_addrs):
"""Chocolate virtual server configuration text

View File

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

View File

@@ -23,12 +23,15 @@ fi
if [ -z "$XDG_DATA_HOME" ]; then
XDG_DATA_HOME=~/.local/share
fi
VENV_NAME="letsencrypt"
if [ -z "$VENV_PATH" ]; then
VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
# We export these values so they are preserved properly if this script is
# rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value.
export OLD_VENV_PATH="$XDG_DATA_HOME/letsencrypt"
export VENV_PATH="/opt/eff.org/certbot/venv"
fi
VENV_BIN="$VENV_PATH/bin"
LE_AUTO_VERSION="0.15.0"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="0.18.2"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -49,6 +52,7 @@ Help for certbot itself cannot be provided until it is installed.
implies --non-interactive
All arguments are accepted and forwarded to the Certbot client when run."
export CERTBOT_AUTO="$0"
for arg in "$@" ; do
case "$arg" in
@@ -77,7 +81,7 @@ for arg in "$@" ; do
h)
HELP=1;;
n)
ASSUME_YES=1;;
NONINTERACTIVE=1;;
q)
QUIET=1;;
v)
@@ -93,8 +97,8 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
HELP=0
fi
# Set ASSUME_YES to 1 if QUIET (i.e. --quiet implies --non-interactive)
if [ "$QUIET" = 1 ]; then
# Set ASSUME_YES to 1 if QUIET or NONINTERACTIVE
if [ "$QUIET" = 1 -o "$NONINTERACTIVE" = 1 ]; then
ASSUME_YES=1
fi
@@ -119,16 +123,18 @@ else
exit 1
fi
# certbot-auto needs root access to bootstrap OS dependencies, and
# certbot itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
# this script *can* be run as root (not recommended), or fall back to using
# `su`. Auto-detection can be overridden by explicitly setting the
# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
# Certbot itself needs root access for almost all modes of operation.
# certbot-auto needs root access to bootstrap OS dependencies and install
# Certbot at a protected path so it can be safely run as root. To accomplish
# this, this script will attempt to run itself as root if it doesn't have the
# necessary privileges by using `sudo` or falling back to `su` if it is not
# available. The mechanism used to obtain root access can be set explicitly by
# setting the environment variable LE_AUTO_SUDO to 'sudo', 'su', 'su_sudo',
# 'SuSudo', or '' as used below.
# Because the parameters in `su -c` has to be a string,
# we need to properly escape it.
su_sudo() {
SuSudo() {
args=""
# This `while` loop iterates over all parameters given to this function.
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
@@ -147,37 +153,57 @@ su_sudo() {
su root -c "$args"
}
SUDO_ENV=""
export CERTBOT_AUTO="$0"
if [ -n "${LE_AUTO_SUDO+x}" ]; then
case "$LE_AUTO_SUDO" in
su_sudo|su)
SUDO=su_sudo
;;
sudo)
SUDO=sudo
SUDO_ENV="CERTBOT_AUTO=$0"
;;
'') ;; # Nothing to do for plain root method.
*)
error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
exit 1
esac
say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
else
if test "`id -u`" -ne "0" ; then
if $EXISTS sudo 1>/dev/null 2>&1; then
SUDO=sudo
SUDO_ENV="CERTBOT_AUTO=$0"
else
say \"sudo\" is not available, will use \"su\" for installation steps...
SUDO=su_sudo
fi
# Sets the environment variable SUDO to be the name of the program or function
# to call to get root access. If this script already has root privleges, SUDO
# is set to an empty string. The value in SUDO should be run with the command
# to called with root privileges as arguments.
SetRootAuthMechanism() {
SUDO=""
if [ -n "${LE_AUTO_SUDO+x}" ]; then
case "$LE_AUTO_SUDO" in
SuSudo|su_sudo|su)
SUDO=SuSudo
;;
sudo)
SUDO="sudo -E"
;;
'') ;; # Nothing to do for plain root method.
*)
error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
exit 1
esac
say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
else
SUDO=
if test "`id -u`" -ne "0" ; then
if $EXISTS sudo 1>/dev/null 2>&1; then
SUDO="sudo -E"
else
say \"sudo\" is not available, will use \"su\" for installation steps...
SUDO=SuSudo
fi
fi
fi
}
if [ "$1" = "--cb-auto-has-root" ]; then
shift 1
else
SetRootAuthMechanism
if [ -n "$SUDO" ]; then
echo "Requesting to rerun $0 with root privileges..."
$SUDO "$0" --cb-auto-has-root "$@"
exit 0
fi
fi
# Runs this script again with the given arguments. --cb-auto-has-root is added
# to the command line arguments to ensure we don't try to acquire root a
# second time. After the script is rerun, we exit the current script.
RerunWithArgs() {
"$0" --cb-auto-has-root "$@"
exit 0
}
BootstrapMessage() {
# Arguments: Platform name
say "Bootstrapping dependencies for $1... (you can skip this with --no-bootstrap)"
@@ -200,6 +226,25 @@ ExperimentalBootstrap() {
fi
}
DeprecationBootstrap() {
# Arguments: Platform name, bootstrap function name
if [ "$DEBUG" = 1 ]; then
if [ "$2" != "" ]; then
BootstrapMessage $1
$2
fi
else
error "WARNING: certbot-auto support for this $1 is DEPRECATED!"
error "Please visit certbot.eff.org to learn how to download a version of"
error "Certbot that is packaged for your system. While an existing version"
error "of certbot-auto may work currently, we have stopped supporting updating"
error "system packages for your system. Please switch to a packaged version"
error "as soon as possible."
exit 1
fi
}
DeterminePythonVersion() {
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
@@ -219,6 +264,10 @@ DeterminePythonVersion() {
fi
}
# If new packages are installed by BootstrapDebCommon below, this version
# number must be increased.
BOOTSTRAP_DEB_COMMON_VERSION=1
BootstrapDebCommon() {
# Current version tested with:
#
@@ -242,7 +291,7 @@ BootstrapDebCommon() {
QUIET_FLAG='-qq'
fi
$SUDO apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
# virtualenv binary can be found in different packages depending on
# distro version (#346)
@@ -292,13 +341,13 @@ BootstrapDebCommon() {
esac
fi
if [ "$add_backports" = 1 ]; then
$SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
$SUDO apt-get $QUIET_FLAG update
sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
apt-get $QUIET_FLAG update
fi
fi
fi
if [ "$add_backports" != 0 ]; then
$SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
augeas_pkg=
fi
}
@@ -317,7 +366,7 @@ BootstrapDebCommon() {
# XXX add a case for ubuntu PPAs
fi
$SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
python \
python-dev \
$virtualenv \
@@ -335,6 +384,10 @@ BootstrapDebCommon() {
fi
}
# If new packages are installed by BootstrapRpmCommon below, this version
# number must be increased.
BOOTSTRAP_RPM_COMMON_VERSION=1
BootstrapRpmCommon() {
# Tested with:
# - Fedora 20, 21, 22, 23 (x64)
@@ -361,9 +414,9 @@ BootstrapRpmCommon() {
QUIET_FLAG='--quiet'
fi
if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then
if ! $tool list *virtualenv >/dev/null 2>&1; then
echo "To use Certbot, packages from the EPEL repository need to be installed."
if ! $SUDO $tool list epel-release >/dev/null 2>&1; then
if ! $tool list epel-release >/dev/null 2>&1; then
error "Enable the EPEL repository and try running Certbot again."
exit 1
fi
@@ -375,7 +428,7 @@ BootstrapRpmCommon() {
/bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..."
sleep 1s
fi
if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then
if ! $tool install $yes_flag $QUIET_FLAG epel-release; then
error "Could not enable EPEL. Aborting bootstrap!"
exit 1
fi
@@ -391,9 +444,8 @@ BootstrapRpmCommon() {
ca-certificates
"
# Some distros and older versions of current distros use a "python27"
# instead of "python" naming convention. Try both conventions.
if $SUDO $tool list python >/dev/null 2>&1; then
# Most RPM distros use the "python" or "python-" naming convention. Let's try that first.
if $tool list python >/dev/null 2>&1; then
pkgs="$pkgs
python
python-devel
@@ -401,6 +453,20 @@ BootstrapRpmCommon() {
python-tools
python-pip
"
# Fedora 26 starts to use the prefix python2 for python2 based packages.
# this elseif is theoretically for any Fedora over version 26:
elif $tool list python2 >/dev/null 2>&1; then
pkgs="$pkgs
python2
python2-libs
python2-setuptools
python2-devel
python2-virtualenv
python2-tools
python2-pip
"
# Some distros and older versions of current distros use a "python27"
# instead of the "python" or "python-" naming convention.
else
pkgs="$pkgs
python27
@@ -411,18 +477,22 @@ BootstrapRpmCommon() {
"
fi
if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then
if $tool list installed "httpd" >/dev/null 2>&1; then
pkgs="$pkgs
mod_ssl
"
fi
if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then
if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then
error "Could not install OS dependencies. Aborting bootstrap!"
exit 1
fi
}
# If new packages are installed by BootstrapSuseCommon below, this version
# number must be increased.
BOOTSTRAP_SUSE_COMMON_VERSION=1
BootstrapSuseCommon() {
# SLE12 don't have python-virtualenv
@@ -435,7 +505,7 @@ BootstrapSuseCommon() {
QUIET_FLAG='-qq'
fi
$SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \
zypper $QUIET_FLAG $zypper_flags in $install_flags \
python \
python-devel \
python-virtualenv \
@@ -446,6 +516,10 @@ BootstrapSuseCommon() {
ca-certificates
}
# If new packages are installed by BootstrapArchCommon below, this version
# number must be increased.
BOOTSTRAP_ARCH_COMMON_VERSION=1
BootstrapArchCommon() {
# Tested with:
# - ArchLinux (x86_64)
@@ -466,21 +540,25 @@ BootstrapArchCommon() {
"
# pacman -T exits with 127 if there are missing dependencies
missing=$($SUDO pacman -T $deps) || true
missing=$(pacman -T $deps) || true
if [ "$ASSUME_YES" = 1 ]; then
noconfirm="--noconfirm"
fi
if [ "$missing" ]; then
if [ "$QUIET" = 1]; then
$SUDO pacman -S --needed $missing $noconfirm > /dev/null
if [ "$QUIET" = 1 ]; then
pacman -S --needed $missing $noconfirm > /dev/null
else
$SUDO pacman -S --needed $missing $noconfirm
pacman -S --needed $missing $noconfirm
fi
fi
}
# If new packages are installed by BootstrapGentooCommon below, this version
# number must be increased.
BOOTSTRAP_GENTOO_COMMON_VERSION=1
BootstrapGentooCommon() {
PACKAGES="
dev-lang/python:2.7
@@ -498,29 +576,37 @@ BootstrapGentooCommon() {
case "$PACKAGE_MANAGER" in
(paludis)
$SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
$SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
(portage|*)
$SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
;;
esac
}
# If new packages are installed by BootstrapFreeBsd below, this version number
# must be increased.
BOOTSTRAP_FREEBSD_VERSION=1
BootstrapFreeBsd() {
if [ "$QUIET" = 1 ]; then
QUIET_FLAG="--quiet"
fi
$SUDO pkg install -Ay $QUIET_FLAG \
pkg install -Ay $QUIET_FLAG \
python \
py27-virtualenv \
augeas \
libffi
}
# If new packages are installed by BootstrapMac below, this version number must
# be increased.
BOOTSTRAP_MAC_VERSION=1
BootstrapMac() {
if hash brew 2>/dev/null; then
say "Using Homebrew to install dependencies..."
@@ -529,7 +615,7 @@ BootstrapMac() {
elif hash port 2>/dev/null; then
say "Using MacPorts to install dependencies..."
pkgman=port
pkgcmd="$SUDO port install"
pkgcmd="port install"
else
say "No Homebrew/MacPorts; installing Homebrew..."
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
@@ -549,8 +635,8 @@ BootstrapMac() {
# Workaround for _dlopen not finding augeas on macOS
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
say "Applying augeas workaround"
$SUDO mkdir -p /usr/local/lib/
$SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
mkdir -p /usr/local/lib/
ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
fi
if ! hash pip 2>/dev/null; then
@@ -566,17 +652,25 @@ BootstrapMac() {
fi
}
# If new packages are installed by BootstrapSmartOS below, this version number
# must be increased.
BOOTSTRAP_SMARTOS_VERSION=1
BootstrapSmartOS() {
pkgin update
pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv'
}
# If new packages are installed by BootstrapMageiaCommon below, this version
# number must be increased.
BOOTSTRAP_MAGEIA_COMMON_VERSION=1
BootstrapMageiaCommon() {
if [ "$QUIET" = 1 ]; then
QUIET_FLAG='--quiet'
fi
if ! $SUDO urpmi --force $QUIET_FLAG \
if ! urpmi --force $QUIET_FLAG \
python \
libpython-devel \
python-virtualenv
@@ -585,7 +679,7 @@ BootstrapMageiaCommon() {
exit 1
fi
if ! $SUDO urpmi --force $QUIET_FLAG \
if ! urpmi --force $QUIET_FLAG \
git \
gcc \
python-augeas \
@@ -599,23 +693,41 @@ BootstrapMageiaCommon() {
}
# Install required OS packages:
Bootstrap() {
if [ "$NO_BOOTSTRAP" = 1 ]; then
return
elif [ -f /etc/debian_version ]; then
# Set Bootstrap to the function that installs OS dependencies on this system
# and BOOTSTRAP_VERSION to the unique identifier for the current version of
# that function. If Bootstrap is set to a function that doesn't install any
# packages (either because --no-bootstrap was included on the command line or
# we don't know how to bootstrap on this system), BOOTSTRAP_VERSION is not set.
if [ "$NO_BOOTSTRAP" = 1 ]; then
Bootstrap() {
:
}
elif [ -f /etc/debian_version ]; then
Bootstrap() {
BootstrapMessage "Debian-based OSes"
BootstrapDebCommon
elif [ -f /etc/mageia-release ]; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
}
BOOTSTRAP_VERSION="BootstrapDebCommon $BOOTSTRAP_DEB_COMMON_VERSION"
elif [ -f /etc/mageia-release ]; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
Bootstrap() {
ExperimentalBootstrap "Mageia" BootstrapMageiaCommon
elif [ -f /etc/redhat-release ]; then
}
BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION"
elif [ -f /etc/redhat-release ]; then
Bootstrap() {
BootstrapMessage "RedHat-based OSes"
BootstrapRpmCommon
elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
Bootstrap() {
BootstrapMessage "openSUSE-based OSes"
BootstrapSuseCommon
elif [ -f /etc/arch-release ]; then
}
BOOTSTRAP_VERSION="BootstrapSuseCommon $BOOTSTRAP_SUSE_COMMON_VERSION"
elif [ -f /etc/arch-release ]; then
Bootstrap() {
if [ "$DEBUG" = 1 ]; then
BootstrapMessage "Archlinux"
BootstrapArchCommon
@@ -627,25 +739,76 @@ Bootstrap() {
error "--debug flag."
exit 1
fi
elif [ -f /etc/manjaro-release ]; then
}
BOOTSTRAP_VERSION="BootstrapArchCommon $BOOTSTRAP_ARCH_COMMON_VERSION"
elif [ -f /etc/manjaro-release ]; then
Bootstrap() {
ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon
elif [ -f /etc/gentoo-release ]; then
ExperimentalBootstrap "Gentoo" BootstrapGentooCommon
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
elif uname | grep -iq Darwin ; then
ExperimentalBootstrap "macOS" BootstrapMac
elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
}
BOOTSTRAP_VERSION="BootstrapArchCommon $BOOTSTRAP_ARCH_COMMON_VERSION"
elif [ -f /etc/gentoo-release ]; then
Bootstrap() {
DeprecationBootstrap "Gentoo" BootstrapGentooCommon
}
BOOTSTRAP_VERSION="BootstrapGentooCommon $BOOTSTRAP_GENTOO_COMMON_VERSION"
elif uname | grep -iq FreeBSD ; then
Bootstrap() {
DeprecationBootstrap "FreeBSD" BootstrapFreeBsd
}
BOOTSTRAP_VERSION="BootstrapFreeBsd $BOOTSTRAP_FREEBSD_VERSION"
elif uname | grep -iq Darwin ; then
Bootstrap() {
DeprecationBootstrap "macOS" BootstrapMac
}
BOOTSTRAP_VERSION="BootstrapMac $BOOTSTRAP_MAC_VERSION"
elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
Bootstrap() {
ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon
elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
}
BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION"
elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
Bootstrap() {
ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS
else
}
BOOTSTRAP_VERSION="BootstrapSmartOS $BOOTSTRAP_SMARTOS_VERSION"
else
Bootstrap() {
error "Sorry, I don't know how to bootstrap Certbot on your operating system!"
error
error "You will need to install OS dependencies, configure virtualenv, and run pip install manually."
error "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
error "for more info."
exit 1
}
fi
# Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used
# to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set
# if it is unknown how OS dependencies were installed on this system.
SetPrevBootstrapVersion() {
if [ -f $BOOTSTRAP_VERSION_PATH ]; then
PREV_BOOTSTRAP_VERSION=$(cat "$BOOTSTRAP_VERSION_PATH")
# The list below only contains bootstrap version strings that existed before
# we started writing them to disk.
#
# DO NOT MODIFY THIS LIST UNLESS YOU KNOW WHAT YOU'RE DOING!
elif grep -Fqx "$BOOTSTRAP_VERSION" << "UNLIKELY_EOF"
BootstrapDebCommon 1
BootstrapMageiaCommon 1
BootstrapRpmCommon 1
BootstrapSuseCommon 1
BootstrapArchCommon 1
BootstrapGentooCommon 1
BootstrapFreeBsd 1
BootstrapMac 1
BootstrapSmartOS 1
UNLIKELY_EOF
then
# If there's no bootstrap version saved to disk, but the currently selected
# bootstrap script is from before we started saving the version number,
# return the currently selected version to prevent us from rebootstrapping
# unnecessarily.
PREV_BOOTSTRAP_VERSION="$BOOTSTRAP_VERSION"
fi
}
@@ -659,18 +822,38 @@ if [ "$1" = "--le-auto-phase2" ]; then
# Phase 2: Create venv, install LE, and run.
shift 1 # the --le-auto-phase2 arg
if [ -f "$VENV_BIN/letsencrypt" ]; then
# --version output ran through grep due to python-cryptography DeprecationWarnings
# grep for both certbot and letsencrypt until certbot and shim packages have been released
INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2)
if [ -z "$INSTALLED_VERSION" ]; then
error "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2
"$VENV_BIN/letsencrypt" --version
exit 1
SetPrevBootstrapVersion
INSTALLED_VERSION="none"
if [ -d "$VENV_PATH" ]; then
# If the selected Bootstrap function isn't a noop and it differs from the
# previously used version
if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then
# if non-interactive mode or stdin and stdout are connected to a terminal
if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then
rm -rf "$VENV_PATH"
RerunWithArgs "$@"
else
error "Skipping upgrade because new OS dependencies may need to be installed."
error
error "To upgrade to a newer version, please run this script again manually so you can"
error "approve changes or with --non-interactive on the command line to automatically"
error "install any required packages."
# Set INSTALLED_VERSION to be the same so we don't update the venv
INSTALLED_VERSION="$LE_AUTO_VERSION"
fi
elif [ -f "$VENV_BIN/letsencrypt" ]; then
# --version output ran through grep due to python-cryptography DeprecationWarnings
# grep for both certbot and letsencrypt until certbot and shim packages have been released
INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2)
if [ -z "$INSTALLED_VERSION" ]; then
error "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2
"$VENV_BIN/letsencrypt" --version
exit 1
fi
fi
else
INSTALLED_VERSION="none"
fi
if [ "$LE_AUTO_VERSION" != "$INSTALLED_VERSION" ]; then
say "Creating virtual environment..."
DeterminePythonVersion
@@ -681,6 +864,12 @@ if [ "$1" = "--le-auto-phase2" ]; then
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null
fi
if [ -n "$BOOTSTRAP_VERSION" ]; then
echo "$BOOTSTRAP_VERSION" > "$BOOTSTRAP_VERSION_PATH"
elif [ -n "$PREV_BOOTSTRAP_VERSION" ]; then
echo "$PREV_BOOTSTRAP_VERSION" > "$BOOTSTRAP_VERSION_PATH"
fi
say "Installing Python packages..."
TEMP_DIR=$(TempDir)
trap 'rm -rf "$TEMP_DIR"' EXIT
@@ -710,54 +899,78 @@ pycparser==2.14 \
asn1crypto==0.22.0 \
--hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \
--hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a
cffi==1.4.2 \
--hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \
--hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \
--hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \
--hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \
--hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \
--hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \
--hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \
--hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \
--hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \
--hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \
--hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \
--hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \
--hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \
--hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \
--hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \
--hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998
ConfigArgParse==0.10.0 \
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
cffi==1.10.0 \
--hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \
--hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \
--hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \
--hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \
--hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \
--hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \
--hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \
--hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \
--hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \
--hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \
--hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \
--hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \
--hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \
--hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \
--hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \
--hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \
--hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \
--hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \
--hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \
--hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \
--hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \
--hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \
--hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \
--hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \
--hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \
--hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \
--hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \
--hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \
--hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \
--hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \
--hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \
--hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \
--hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \
--hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \
--hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \
--hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5
ConfigArgParse==0.12.0 \
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
cryptography==1.8.2 \
--hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \
--hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \
--hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \
--hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \
--hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \
--hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \
--hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \
--hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \
--hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \
--hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \
--hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \
--hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \
--hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \
--hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \
--hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \
--hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \
--hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \
--hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \
--hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \
--hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \
--hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \
--hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \
--hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \
--hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \
--hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \
--hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda
cryptography==2.0.2 \
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
--hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \
--hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \
--hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \
--hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \
--hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \
--hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \
--hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \
--hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \
--hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \
--hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \
--hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \
--hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \
--hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \
--hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \
--hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \
--hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \
--hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \
--hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \
--hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \
--hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \
--hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \
--hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \
--hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \
--hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \
--hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@@ -864,18 +1077,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.15.0 \
--hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \
--hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb
acme==0.15.0 \
--hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \
--hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87
certbot-apache==0.15.0 \
--hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \
--hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca
certbot-nginx==0.15.0 \
--hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \
--hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234
certbot==0.18.2 \
--hash=sha256:e1d1fcd6248cdbdba92e3a769dc40567dbffc717bff86ef3431fadf31419a6b6 \
--hash=sha256:0122e6ee3fc1167fa91883b9f2ea1897f3d0d4612739f175827b5a3e8f259c9a
acme==0.18.2 \
--hash=sha256:67e1270318a2f8ca5f309b22fc50669abfdb38c875133b20e8a1358e96591a7a \
--hash=sha256:718efa9b3a7e6defcc7179e214a838d43a2bcef7dbd77667a2e505504e453ef6
certbot-apache==0.18.2 \
--hash=sha256:54d9a16a7ec87df0538a094a28aabf5415d92bda3e80742dfd0b3d1ed82d8760 \
--hash=sha256:07856f49db28593eb58ed970da2f6520b0d0d7402ac7757a5ea41fcb6a24a9ea
certbot-nginx==0.18.2 \
--hash=sha256:7fac4ec1be4966ef828f2aca32a04db303a9b49453589961f2fb5867b70823c5 \
--hash=sha256:d35aeda6f2ce6213d84ecbbc658ca6cf13c98961c475867c70740552d05ae3e3
UNLIKELY_EOF
# -------------------------------------------------------------------------
@@ -903,6 +1116,7 @@ anything goes wrong, it will exit with a non-zero status code.
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
from __future__ import print_function
from distutils.version import StrictVersion
from hashlib import sha256
from os.path import join
from pipes import quote
@@ -937,12 +1151,14 @@ except ImportError:
from urllib.parse import urlparse # 3.4
__version__ = 1, 1, 1
__version__ = 1, 3, 0
PIP_VERSION = '9.0.1'
# wheel has a conditional dependency on argparse:
maybe_argparse = (
[('https://pypi.python.org/packages/source/a/argparse/'
[('https://pypi.python.org/packages/18/dd/'
'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/'
'argparse-1.4.0.tar.gz',
'62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')]
if version_info < (2, 7, 0) else [])
@@ -950,13 +1166,19 @@ maybe_argparse = (
PACKAGES = maybe_argparse + [
# Pip has no dependencies, as it vendors everything:
('https://pypi.python.org/packages/source/p/pip/pip-8.0.3.tar.gz',
'30f98b66f3fe1069c529a491597d34a1c224a68640c82caf2ade5f88aa1405e8'),
('https://pypi.python.org/packages/11/b6/'
'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/'
'pip-{0}.tar.gz'
.format(PIP_VERSION),
'09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'),
# This version of setuptools has only optional dependencies:
('https://pypi.python.org/packages/source/s/setuptools/'
('https://pypi.python.org/packages/69/65/'
'4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/'
'setuptools-20.2.2.tar.gz',
'24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'),
('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz',
('https://pypi.python.org/packages/c9/1d/'
'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/'
'wheel-0.29.0.tar.gz',
'1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648')
]
@@ -1006,11 +1228,21 @@ def hashed_download(url, temp, digest):
def main():
pip_version = StrictVersion(check_output(['pip', '--version'])
.decode('utf-8').split()[1])
min_pip_version = StrictVersion(PIP_VERSION)
if pip_version >= min_pip_version:
return 0
has_pip_cache = pip_version >= StrictVersion('6.0')
temp = mkdtemp(prefix='pipstrap-')
try:
downloads = [hashed_download(url, temp, digest)
for url, digest in PACKAGES]
check_output('pip install --no-index --no-deps -U ' +
# Disable cache since we're not using it and it otherwise
# sometimes throws permission warnings:
('--no-cache-dir ' if has_pip_cache else '') +
' '.join(quote(d) for d in downloads),
shell=True)
except HashError as exc:
@@ -1033,9 +1265,9 @@ UNLIKELY_EOF
PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py"
set +e
if [ "$VERBOSE" = 1 ]; then
"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt"
"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt"
else
PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1`
PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1`
fi
PIP_STATUS=$?
set -e
@@ -1069,20 +1301,15 @@ UNLIKELY_EOF
rm -rf "$VENV_PATH"
exit 1
fi
if [ -d "$OLD_VENV_PATH" -a ! -L "$OLD_VENV_PATH" ]; then
rm -rf "$OLD_VENV_PATH"
ln -s "$VENV_PATH" "$OLD_VENV_PATH"
fi
say "Installation succeeded."
fi
if [ -n "$SUDO" ]; then
# SUDO is su wrapper or sudo
say "Requesting root privileges to run certbot..."
say " $VENV_BIN/letsencrypt" "$@"
fi
if [ -z "$SUDO_ENV" ] ; then
# SUDO is su wrapper / noop
$SUDO "$VENV_BIN/letsencrypt" "$@"
else
# sudo
$SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@"
fi
"$VENV_BIN/letsencrypt" "$@"
else
# Phase 1: Upgrade certbot-auto if necessary, then self-invoke.
@@ -1093,12 +1320,14 @@ else
# package). Phase 2 checks the version of the locally installed certbot.
if [ ! -f "$VENV_BIN/letsencrypt" ]; then
if [ "$HELP" = 1 ]; then
echo "$USAGE"
exit 0
if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then
if [ "$HELP" = 1 ]; then
echo "$USAGE"
exit 0
fi
# If it looks like we've never bootstrapped before, bootstrap:
Bootstrap
fi
# If it looks like we've never bootstrapped before, bootstrap:
Bootstrap
fi
if [ "$OS_PACKAGES_ONLY" = 1 ]; then
say "OS packages installed."
@@ -1258,15 +1487,15 @@ UNLIKELY_EOF
say "Replacing certbot-auto..."
# Clone permissions with cp. chmod and chown don't have a --reference
# option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
$SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
# Using mv rather than cp leaves the old file descriptor pointing to the
# original copy so the shell can continue to read it unmolested. mv across
# filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the
# cp is unlikely to fail (esp. under sudo) if the rm doesn't.
$SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
# cp is unlikely to fail if the rm doesn't.
mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
fi # A newer version is available.
fi # Self-upgrading is allowed.
"$0" --le-auto-phase2 "$@"
RerunWithArgs --le-auto-phase2 "$@"
fi

View File

@@ -8,8 +8,8 @@ MAINTAINER Brad Warren <bmw@eff.org>
# TODO: Install non-default Python versions for tox.
# TODO: Install Apache/Nginx for plugin development.
COPY certbot-auto /opt/certbot/src/certbot-auto
RUN /opt/certbot/src/certbot-auto -n --os-packages-only
COPY letsencrypt-auto-source /opt/certbot/src/letsencrypt-auto-source
RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only
# the above is not likely to change, so by putting it further up the
# Dockerfile we make sure we cache as much as possible
@@ -29,16 +29,18 @@ COPY acme /opt/certbot/src/acme/
COPY certbot-apache /opt/certbot/src/certbot-apache/
COPY certbot-nginx /opt/certbot/src/certbot-nginx/
COPY certbot-compatibility-test /opt/certbot/src/certbot-compatibility-test/
COPY tools /opt/certbot/src/tools
RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \
/opt/certbot/venv/bin/pip install -U setuptools && \
/opt/certbot/venv/bin/pip install -U pip && \
/opt/certbot/venv/bin/pip install \
-e /opt/certbot/src/acme \
-e /opt/certbot/src \
-e /opt/certbot/src/certbot-apache \
-e /opt/certbot/src/certbot-nginx \
-e /opt/certbot/src/certbot-compatibility-test
/opt/certbot/venv/bin/pip install -U pip
ENV PATH /opt/certbot/venv/bin:$PATH
RUN /opt/certbot/src/tools/pip_install_editable.sh \
/opt/certbot/src/acme \
/opt/certbot/src \
/opt/certbot/src/certbot-apache \
/opt/certbot/src/certbot-nginx \
/opt/certbot/src/certbot-compatibility-test
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/certbot/src" (it's stays in the underlaying image);
@@ -46,5 +48,3 @@ RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \
# bash" and investigate, apply patches, etc.
WORKDIR /opt/certbot/src/certbot-compatibility-test/certbot_compatibility_test/testdata
ENV PATH /opt/certbot/venv/bin:$PATH

View File

@@ -79,6 +79,8 @@ def _get_names(config):
if line.strip().startswith("server_name"):
names = line.partition("server_name")[2].rpartition(";")[0]
for n in names.split():
all_names.add(n)
# Filter out wildcards in both all_names and test_names
if not n.startswith("*."):
all_names.add(n)
non_ip_names = set(n for n in all_names if not util.IP_REGEX.match(n))
return all_names, non_ip_names

View File

@@ -1,7 +1,6 @@
"""Tests Certbot plugins against different server configurations."""
import argparse
import filecmp
import functools
import logging
import os
import shutil
@@ -64,17 +63,17 @@ def test_authenticator(plugin, config, temp_dir):
type(achalls[i]), achalls[i].domain, config)
success = False
elif isinstance(responses[i], challenges.TLSSNI01Response):
verify = functools.partial(responses[i].simple_verify, achalls[i].chall,
achalls[i].domain,
util.JWK.public_key(),
host="127.0.0.1",
port=plugin.https_port)
if _try_until_true(verify):
verified = responses[i].simple_verify(achalls[i].chall,
achalls[i].domain,
util.JWK.public_key(),
host="127.0.0.1",
port=plugin.https_port)
if verified:
logger.info(
"tls-sni-01 verification for %s succeeded", achalls[i].domain)
else:
logger.error(
"tls-sni-01 verification for %s in %s failed",
"**** tls-sni-01 verification for %s in %s failed",
achalls[i].domain, config)
success = False
@@ -122,7 +121,7 @@ def test_installer(args, plugin, config, temp_dir):
if names_match:
logger.info("get_all_names test succeeded")
else:
logger.error("get_all_names test failed for config %s", config)
logger.error("**** get_all_names test failed for config %s", config)
domains = list(plugin.get_testable_domain_names())
success = test_deploy_cert(plugin, temp_dir, domains)
@@ -147,7 +146,7 @@ def test_deploy_cert(plugin, temp_dir, domains):
plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path)
plugin.save() # Needed by the Apache plugin
except le_errors.Error as error:
logger.error("Plugin failed to deploy certificate for %s:", domain)
logger.error("**** Plugin failed to deploy certificate for %s:", domain)
logger.exception(error)
return False
@@ -155,11 +154,12 @@ def test_deploy_cert(plugin, temp_dir, domains):
return False
success = True
time.sleep(3)
for domain in domains:
verify = functools.partial(validator.Validator().certificate, cert,
domain, "127.0.0.1", plugin.https_port)
if not _try_until_true(verify):
logger.error("Could not verify certificate for domain %s", domain)
verified = validator.Validator().certificate(
cert, domain, "127.0.0.1", plugin.https_port)
if not verified:
logger.error("**** Could not verify certificate for domain %s", domain)
success = False
if success:
@@ -177,16 +177,21 @@ def test_enhancements(plugin, domains):
"enhancements")
return False
for domain in domains:
domains_and_info = [(domain, []) for domain in domains]
for domain, info in domains_and_info:
try:
previous_redirect = validator.Validator().any_redirect(
"localhost", plugin.http_port, headers={"Host": domain})
info.append(previous_redirect)
plugin.enhance(domain, "redirect")
plugin.save() # Needed by the Apache plugin
except le_errors.PluginError as error:
# Don't immediately fail because a redirect may already be enabled
logger.warning("Plugin failed to enable redirect for %s:", domain)
logger.warning("*** Plugin failed to enable redirect for %s:", domain)
logger.warning("%s", error)
except le_errors.Error as error:
logger.error("An error occurred while enabling redirect for %s:",
logger.error("*** An error occurred while enabling redirect for %s:",
domain)
logger.exception(error)
@@ -194,12 +199,14 @@ def test_enhancements(plugin, domains):
return False
success = True
for domain in domains:
verify = functools.partial(validator.Validator().redirect, "localhost",
plugin.http_port, headers={"Host": domain})
if not _try_until_true(verify):
logger.error("Improper redirect for domain %s", domain)
success = False
for domain, info in domains_and_info:
previous_redirect = info[0]
if not previous_redirect:
verified = validator.Validator().redirect(
"localhost", plugin.http_port, headers={"Host": domain})
if not verified:
logger.error("*** Improper redirect for domain %s", domain)
success = False
if success:
logger.info("Enhancements test succeeded")
@@ -207,17 +214,6 @@ def test_enhancements(plugin, domains):
return success
def _try_until_true(func, max_tries=5, sleep_time=0.5):
"""Calls func up to max_tries times until it returns True"""
for _ in xrange(0, max_tries):
if func():
return True
else:
time.sleep(sleep_time)
return False
def _save_and_restart(plugin, title=None):
"""Saves and restart the plugin, returning True if no errors occurred"""
try:
@@ -225,7 +221,7 @@ def _save_and_restart(plugin, title=None):
plugin.restart()
return True
except le_errors.Error as error:
logger.error("Plugin failed to save and restart server:")
logger.error("*** Plugin failed to save and restart server:")
logger.exception(error)
return False
@@ -235,12 +231,12 @@ def test_rollback(plugin, config, backup):
try:
plugin.rollback_checkpoints(1337)
except le_errors.Error as error:
logger.error("Plugin raised an exception during rollback:")
logger.error("*** Plugin raised an exception during rollback:")
logger.exception(error)
return False
if _dirs_are_unequal(config, backup):
logger.error("Rollback failed for config `%s`", config)
logger.error("*** Rollback failed for config `%s`", config)
return False
else:
logger.info("Rollback succeeded")

View File

@@ -45,19 +45,12 @@ class Validator(object):
else:
response = requests.get(url, allow_redirects=False)
if response.status_code not in (301, 303):
return False
redirect_location = response.headers.get("location", "")
# We're checking that the redirect we added behaves correctly.
# It's okay for some server configuration to redirect to an
# http URL, as long as it's on some other domain.
if not redirect_location.startswith("https://"):
if not redirect_location.startswith("http://"):
return False
else:
if redirect_location[len("http://"):] == name:
return False
return False
if response.status_code != 301:
logger.error("Server did not redirect with permanent code")
@@ -65,6 +58,16 @@ class Validator(object):
return True
def any_redirect(self, name, port=80, headers=None):
"""Test whether webserver redirects."""
url = "http://{0}:{1}".format(name, port)
if headers:
response = requests.get(url, headers=headers, allow_redirects=False)
else:
response = requests.get(url, allow_redirects=False)
return response.status_code in xrange(300, 309)
def hsts(self, name):
"""Test for HTTP Strict Transport Security header"""
headers = requests.get("https://" + name).headers

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ Named Arguments
======================================== =====================================
``--dns-google-credentials`` Google Cloud Platform credentials_
JSON file.
(Required)
(Required - Optional on Google Compute Engine)
``--dns-google-propagation-seconds`` The number of seconds to wait for DNS
to propagate before asking the ACME
server to verify the DNS record.
@@ -21,8 +21,8 @@ Named Arguments
Credentials
-----------
Use of this plugin requires a configuration file containing Google Cloud
Platform API credentials for an account with the following permissions:
Use of this plugin requires Google Cloud Platform API credentials
for an account with the following permissions:
* ``dns.changes.create``
* ``dns.changes.get``
@@ -33,7 +33,12 @@ Platform API credentials for an account with the following permissions:
Google provides instructions for `creating a service account <https://developers
.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount>`_ and
`information about the required permissions <https://cloud.google.com/dns/access
-control#permissions_and_roles>`_.
-control#permissions_and_roles>`_. If you're running on Google Compute Engine,
you can `assign the service account to the instance <https://cloud.google.com/
compute/docs/access/create-enable-service-accounts-for-instances>`_ which
is running certbot. A credentials file is not required in this case, as they
are automatically obtained by certbot through the `metadata service
<https://cloud.google.com/compute/docs/storing-retrieving-metadata>`_ .
.. code-block:: json
:name: credentials.json

View File

@@ -2,6 +2,7 @@
import json
import logging
import httplib2
import zope.interface
from googleapiclient import discovery
from googleapiclient import errors as googleapiclient_errors
@@ -15,6 +16,8 @@ logger = logging.getLogger(__name__)
ACCT_URL = 'https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount'
PERMISSIONS_URL = 'https://cloud.google.com/dns/access-control#permissions_and_roles'
METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/'
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
@zope.interface.implementer(interfaces.IAuthenticator)
@@ -39,16 +42,29 @@ class Authenticator(dns_common.DNSAuthenticator):
add('credentials',
help=('Path to Google Cloud DNS service account JSON file. (See {0} for' +
'information about creating a service account and {1} for information about the' +
'required permissions.)').format(ACCT_URL, PERMISSIONS_URL))
'required permissions.)').format(ACCT_URL, PERMISSIONS_URL),
default=None)
def more_info(self): # pylint: disable=missing-docstring,no-self-use
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
'the Google Cloud DNS API.'
def _setup_credentials(self):
self._configure_file('credentials', 'path to Google Cloud DNS service account JSON file')
if self.conf('credentials') is None:
try:
# use project_id query to check for availability of google metadata server
# we won't use the result but know we're not on GCP when an exception is thrown
_GoogleClient.get_project_id()
except (ValueError, httplib2.ServerNotFoundError):
raise errors.PluginError('Unable to get Google Cloud Metadata and no credentials'
' specified. Automatic credential lookup is only '
'available on Google Cloud Platform. Please configure'
' credentials using --dns-google-credentials <file>')
else:
self._configure_file('credentials',
'path to Google Cloud DNS service account JSON file')
dns_common.validate_file_permissions(self.conf('credentials'))
dns_common.validate_file_permissions(self.conf('credentials'))
def _perform(self, domain, validation_name, validation):
self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl)
@@ -65,13 +81,18 @@ class _GoogleClient(object):
Encapsulates all communication with the Google Cloud DNS API.
"""
def __init__(self, account_json):
def __init__(self, account_json=None):
scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']
credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)
if account_json is not None:
credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)
with open(account_json) as account:
self.project_id = json.load(account)['project_id']
else:
credentials = None
self.project_id = self.get_project_id()
self.dns = discovery.build('dns', 'v1', credentials=credentials, cache_discovery=False)
with open(account_json) as account:
self.project_id = json.load(account)['project_id']
def add_txt_record(self, domain, record_name, record_content, record_ttl):
"""
@@ -183,3 +204,24 @@ class _GoogleClient(object):
raise errors.PluginError('Unable to determine managed zone for {0} using zone names: {1}.'
.format(domain, zone_dns_name_guesses))
@staticmethod
def get_project_id():
"""
Query the google metadata service for the current project ID
This only works on Google Cloud Platform
:raises ServerNotFoundError: Not running on Google Compute or DNS not available
:raises ValueError: Server is found, but response code is not 200
:returns: project id
"""
url = '{0}project/project-id'.format(METADATA_URL)
# Request an access token from the metadata server.
http = httplib2.Http()
r, content = http.request(url, headers=METADATA_HEADERS)
if r.status != 200:
raise ValueError("Invalid status code: {0}".format(r))
return content

View File

@@ -5,8 +5,10 @@ import unittest
import mock
from googleapiclient.errors import Error
from httplib2 import ServerNotFoundError
from certbot import errors
from certbot.errors import PluginError
from certbot.plugins import dns_test_common
from certbot.plugins.dns_test_common import DOMAIN
from certbot.tests import util as test_util
@@ -50,6 +52,11 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
self.assertEqual(expected, self.mock_client.mock_calls)
@mock.patch('httplib2.Http.request', side_effect=ServerNotFoundError)
def test_without_auth(self, unused_mock):
self.config.google_credentials = None
self.assertRaises(PluginError, self.auth.perform, [self.achall])
class GoogleClientTest(unittest.TestCase):
record_name = "foo"
@@ -74,11 +81,24 @@ class GoogleClientTest(unittest.TestCase):
return client, mock_changes
@mock.patch('googleapiclient.discovery.build')
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google._GoogleClient.get_project_id')
def test_client_without_credentials(self, get_project_id_mock, credential_mock,
unused_discovery_mock):
from certbot_dns_google.dns_google import _GoogleClient
_GoogleClient(None)
self.assertFalse(credential_mock.called)
self.assertTrue(get_project_id_mock.called)
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
def test_add_txt_record(self, unused_credential_mock):
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
@mock.patch('certbot_dns_google.dns_google._GoogleClient.get_project_id')
def test_add_txt_record(self, get_project_id_mock, credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
credential_mock.assert_called_once_with('/not/a/real/path.json', mock.ANY)
self.assertFalse(get_project_id_mock.called)
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
@@ -101,7 +121,7 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_add_txt_record_and_poll(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
changes.create.return_value.execute.return_value = {'status': 'pending', 'id': self.change}
@@ -119,7 +139,7 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_add_txt_record_error_during_zone_lookup(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(API_ERROR)
@@ -128,7 +148,7 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_add_txt_record_zone_not_found(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock([{'managedZones': []},
{'managedZones': []}])
@@ -138,7 +158,7 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_add_txt_record_error_during_add(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
changes.create.side_effect = API_ERROR
@@ -148,7 +168,7 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
@@ -173,7 +193,7 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record_error_during_zone_lookup(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock(API_ERROR)
@@ -181,7 +201,7 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record_zone_not_found(self, unused_credential_mock):
client, unused_changes = self._setUp_client_with_mock([{'managedZones': []},
{'managedZones': []}])
@@ -190,13 +210,41 @@ class GoogleClientTest(unittest.TestCase):
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
@mock.patch('certbot_dns_google.dns_google.open',
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True)
def test_del_txt_record_error_during_delete(self, unused_credential_mock):
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
changes.create.side_effect = API_ERROR
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
def test_get_project_id(self):
from certbot_dns_google.dns_google import _GoogleClient
response = DummyResponse()
response.status = 200
with mock.patch('httplib2.Http.request', return_value=(response, 1234)):
project_id = _GoogleClient.get_project_id()
self.assertEqual(project_id, 1234)
failed_response = DummyResponse()
failed_response.status = 404
with mock.patch('httplib2.Http.request',
return_value=(failed_response, "some detailed http error response")):
self.assertRaises(ValueError, _GoogleClient.get_project_id)
with mock.patch('httplib2.Http.request', side_effect=ServerNotFoundError):
self.assertRaises(ServerNotFoundError, _GoogleClient.get_project_id)
class DummyResponse(object):
"""
Dummy object to create a fake HTTPResponse (the actual one requires a socket and we only
need the status attribute)
"""
def __init__(self):
self.status = 200
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -4,19 +4,23 @@ from setuptools import setup
from setuptools import find_packages
version = '0.16.0.dev0'
version = '0.19.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'acme=={0}'.format(version),
'certbot=={0}'.format(version),
'google-api-python-client',
# 1.5 is the first version that supports oauth2client>=2.0
'google-api-python-client>=1.5',
'mock',
'oauth2client',
# for oauth2client.service_account.ServiceAccountCredentials
'oauth2client>=2.0',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'zope.interface',
# already a dependency of google-api-python-client, but added for consistency
'httplib2'
]
docs_extras = [

View File

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

View File

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

View File

@@ -208,8 +208,8 @@ class _RFC2136Client(object):
rcode = response.rcode()
# Authoritative Answer bit should be set
if (rcode == dns.rcode.NOERROR and len(response.answer) > 0 and
response.flags & dns.flags.AA):
if (rcode == dns.rcode.NOERROR and response.get_rrset(response.answer,
domain, dns.rdataclass.IN, dns.rdatatype.SOA) and response.flags & dns.flags.AA):
logger.debug('Received authoritative SOA response for %s', domain_name)
return True

View File

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

View File

@@ -3,7 +3,7 @@ import sys
from distutils.core import setup
from setuptools import find_packages
version = '0.16.0.dev0'
version = '0.19.0.dev0'
install_requires = [
'acme=={0}'.format(version),

View File

@@ -19,7 +19,6 @@ from certbot import crypto_util
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot import reverter
from certbot.plugins import common
@@ -63,7 +62,7 @@ TEST_REDIRECT_COMMENT_BLOCK = [
@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
@zope.interface.provider(interfaces.IPluginFactory)
class NginxConfigurator(common.Plugin):
class NginxConfigurator(common.Installer):
# pylint: disable=too-many-instance-attributes,too-many-public-methods
"""Nginx configurator.
@@ -87,8 +86,6 @@ class NginxConfigurator(common.Plugin):
description = "Nginx Web Server plugin - Alpha"
hidden = True
DEFAULT_LISTEN_PORT = '80'
@classmethod
@@ -129,8 +126,6 @@ class NginxConfigurator(common.Plugin):
self._enhance_func = {"redirect": self._enable_redirect,
"staple-ocsp": self._enable_ocsp_stapling}
# Set up reverter
self.reverter = reverter.Reverter(self.config)
self.reverter.recovery_routine()
@property
@@ -162,6 +157,8 @@ class NginxConfigurator(common.Plugin):
install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest)
self.install_ssl_dhparams()
# Set Version
if self.version is None:
self.version = self.get_version()
@@ -198,16 +195,10 @@ class NginxConfigurator(common.Plugin):
cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path],
['\n', 'ssl_certificate_key', ' ', key_path]]
try:
self.parser.add_server_directives(vhost,
cert_directives, replace=True)
logger.info("Deployed Certificate to VirtualHost %s for %s",
vhost.filep, vhost.names)
except errors.MisconfigurationError as error:
logger.debug(error)
# Presumably break here so that the virtualhost is not modified
raise errors.PluginError("Cannot find a cert or key directive in {0} for {1}. "
"VirtualHost was not modified.".format(vhost.filep, vhost.names))
self.parser.add_server_directives(vhost,
cert_directives, replace=True)
logger.info("Deployed Certificate to VirtualHost %s for %s",
vhost.filep, vhost.names)
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
(vhost.filep,
@@ -244,7 +235,11 @@ class NginxConfigurator(common.Plugin):
if not vhost:
# No matches. Raise a misconfiguration error.
raise errors.MisconfigurationError(
"Cannot find a VirtualHost matching domain %s." % (target_name))
("Cannot find a VirtualHost matching domain %s. "
"In order for Certbot to correctly perform the challenge "
"please add a corresponding server_name directive to your "
"nginx configuration: "
"https://nginx.org/en/docs/http/server_names.html") % (target_name))
else:
# Note: if we are enhancing with ocsp, vhost should already be ssl.
if not vhost.ssl:
@@ -456,11 +451,13 @@ class NginxConfigurator(common.Plugin):
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
ssl_block = (
[['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)],
['\n ', 'ssl_certificate', ' ', snakeoil_cert],
['\n ', 'ssl_certificate_key', ' ', snakeoil_key],
['\n ', 'include', ' ', self.mod_ssl_conf]])
ssl_block = ([
['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)],
['\n ', 'ssl_certificate', ' ', snakeoil_cert],
['\n ', 'ssl_certificate_key', ' ', snakeoil_key],
['\n ', 'include', ' ', self.mod_ssl_conf],
['\n ', 'ssl_dhparam', ' ', self.ssl_dhparams],
])
self.parser.add_server_directives(
vhost, ssl_block, replace=False)
@@ -701,31 +698,13 @@ class NginxConfigurator(common.Plugin):
"""
save_files = set(self.parser.parsed.keys())
try: # TODO: make a common base for Apache and Nginx plugins
# Create Checkpoint
if temporary:
self.reverter.add_to_temp_checkpoint(
save_files, self.save_notes)
# how many comments does it take
else:
self.reverter.add_to_checkpoint(save_files,
self.save_notes)
# to confuse a linter?
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.add_to_checkpoint(save_files, self.save_notes, temporary)
self.save_notes = ""
# Change 'ext' to something else to not override existing conf files
self.parser.filedump(ext='')
if title and not temporary:
try:
self.reverter.finalize_checkpoint(title)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
return True
self.finalize_checkpoint(title)
def recovery_routine(self):
"""Revert all previously modified files.
@@ -735,10 +714,7 @@ class NginxConfigurator(common.Plugin):
:raises .errors.PluginError: If unable to recover the configuration
"""
try:
self.reverter.recovery_routine()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
super(NginxConfigurator, self).recovery_routine()
self.parser.load()
def revert_challenge_config(self):
@@ -747,10 +723,7 @@ class NginxConfigurator(common.Plugin):
:raises .errors.PluginError: If unable to revert the challenge config.
"""
try:
self.reverter.revert_temporary_config()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.revert_temporary_config()
self.parser.load()
def rollback_checkpoints(self, rollback=1):
@@ -762,24 +735,9 @@ class NginxConfigurator(common.Plugin):
the function is unable to correctly revert the configuration
"""
try:
self.reverter.rollback_checkpoints(rollback)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
super(NginxConfigurator, self).rollback_checkpoints(rollback)
self.parser.load()
def view_config_changes(self):
"""Show all of the configuration changes that have taken place.
:raises .errors.PluginError: If there is a problem while processing
the checkpoints directories.
"""
try:
self.reverter.view_config_changes()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
###########################################################################
# Challenges Section for IAuthenticator
###########################################################################
@@ -868,5 +826,5 @@ def nginx_restart(nginx_ctl, nginx_conf):
def install_ssl_options_conf(options_ssl, options_ssl_digest):
"""Copy Certbot's SSL options file into the system's config dir if required."""
return common.install_ssl_options_conf(options_ssl, options_ssl_digest,
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
constants.MOD_SSL_CONF_SRC, constants.ALL_SSL_OPTIONS_HASHES)

View File

@@ -278,8 +278,8 @@ class NginxParser(object):
This method modifies vhost to be fully consistent with the new directives.
..note :: If replace is True, this raises a misconfiguration error
if the directive does not already exist.
..note :: If replace is True and the directive already exists, the first
instance will be replaced. Otherwise, the directive is added.
..note :: If replace is False nothing gets added if an identical
block exists already.
@@ -464,8 +464,9 @@ def _add_directives(block, directives, replace):
When replace=False, it's an error to try and add a directive that already
exists in the config block with a conflicting value.
When replace=True, a directive with the same name MUST already exist in the
config block, and the first instance will be replaced.
When replace=True and a directive with the same name already exists in the
config block, the first instance will be replaced. Otherwise, the directive
will be added to the config block.
..todo :: Find directives that are in included files.
@@ -547,49 +548,46 @@ def _add_directive(block, directive, replace):
location = find_location(directive)
if replace:
if location is None:
raise errors.MisconfigurationError(
'expected directive for {0} in the Nginx '
'config but did not find it.'.format(directive[0]))
block[location] = directive
_comment_directive(block, location)
else:
# Append directive. Fail if the name is not a repeatable directive name,
# and there is already a copy of that directive with a different value
# in the config file.
if location is not None:
block[location] = directive
_comment_directive(block, location)
return
# Append directive. Fail if the name is not a repeatable directive name,
# and there is already a copy of that directive with a different value
# in the config file.
# handle flat include files
# handle flat include files
directive_name = directive[0]
def can_append(loc, dir_name):
""" Can we append this directive to the block? """
return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES)
directive_name = directive[0]
def can_append(loc, dir_name):
""" Can we append this directive to the block? """
return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES)
err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".'
err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".'
# Give a better error message about the specific directive than Nginx's "fail to restart"
if directive_name == INCLUDE:
# in theory, we might want to do this recursively, but in practice, that's really not
# necessary because we know what file we're talking about (and if we don't recurse, we
# just give a worse error message)
included_directives = _parse_ssl_options(directive[1])
# Give a better error message about the specific directive than Nginx's "fail to restart"
if directive_name == INCLUDE:
# in theory, we might want to do this recursively, but in practice, that's really not
# necessary because we know what file we're talking about (and if we don't recurse, we
# just give a worse error message)
included_directives = _parse_ssl_options(directive[1])
for included_directive in included_directives:
included_dir_loc = find_location(included_directive)
included_dir_name = included_directive[0]
if not is_whitespace_or_comment(included_directive) \
and not can_append(included_dir_loc, included_dir_name):
if block[included_dir_loc] != included_directive:
raise errors.MisconfigurationError(err_fmt.format(included_directive,
block[included_dir_loc]))
else:
_comment_out_directive(block, included_dir_loc, directive[1])
for included_directive in included_directives:
included_dir_loc = find_location(included_directive)
included_dir_name = included_directive[0]
if not is_whitespace_or_comment(included_directive) \
and not can_append(included_dir_loc, included_dir_name):
if block[included_dir_loc] != included_directive:
raise errors.MisconfigurationError(err_fmt.format(included_directive,
block[included_dir_loc]))
else:
_comment_out_directive(block, included_dir_loc, directive[1])
if can_append(location, directive_name):
block.append(directive)
_comment_directive(block, len(block) - 1)
elif block[location] != directive:
raise errors.MisconfigurationError(err_fmt.format(directive, block[location]))
if can_append(location, directive_name):
block.append(directive)
_comment_directive(block, len(block) - 1)
elif block[location] != directive:
raise errors.MisconfigurationError(err_fmt.format(directive, block[location]))
def _apply_global_addr_ssl(addr_to_ssl, parsed_server):
"""Apply global sslishness information to the parsed server block

View File

@@ -226,8 +226,9 @@ class NginxConfiguratorTest(util.NginxTest):
['listen', '5001', 'ssl'],
['ssl_certificate', 'example/fullchain.pem'],
['ssl_certificate_key', 'example/key.pem'],
['include', self.config.mod_ssl_conf]]
]],
['include', self.config.mod_ssl_conf],
['ssl_dhparam', self.config.ssl_dhparams],
]]],
parsed_example_conf)
self.assertEqual([['server_name', 'somename', 'alias', 'another.alias']],
parsed_server_conf)
@@ -244,8 +245,9 @@ class NginxConfiguratorTest(util.NginxTest):
['listen', '5001', 'ssl'],
['ssl_certificate', '/etc/nginx/fullchain.pem'],
['ssl_certificate_key', '/etc/nginx/key.pem'],
['include', self.config.mod_ssl_conf]]
],
['include', self.config.mod_ssl_conf],
['ssl_dhparam', self.config.ssl_dhparams],
]],
2))
def test_deploy_cert_add_explicit_listen(self):
@@ -268,8 +270,9 @@ class NginxConfiguratorTest(util.NginxTest):
['listen', '5001', 'ssl'],
['ssl_certificate', 'summer/fullchain.pem'],
['ssl_certificate_key', 'summer/key.pem'],
['include', self.config.mod_ssl_conf]]
],
['include', self.config.mod_ssl_conf],
['ssl_dhparam', self.config.ssl_dhparams],
]],
parsed_migration_conf[0])
@mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform")
@@ -601,7 +604,7 @@ class InstallSslOptionsConfTest(util.NginxTest):
with mock.patch("certbot.plugins.common.logger") as mock_logger:
self._call()
self.assertEqual(mock_logger.warning.call_args[0][0],
"%s has been manually modified; updated ssl configuration options "
"%s has been manually modified; updated file "
"saved to %s. We recommend updating %s for security purposes.")
self.assertEqual(crypto_util.sha256sum(constants.MOD_SSL_CONF_SRC),
self._current_ssl_options_hash())

View File

@@ -273,11 +273,16 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
['server_name', 'example.*'], []
]]])
mock_vhost.names = set(['foobar.com', 'example.*'])
self.assertRaises(errors.MisconfigurationError,
nparser.add_server_directives,
mock_vhost,
[['ssl_certificate', 'cert.pem']],
replace=True)
nparser.add_server_directives(
mock_vhost, [['ssl_certificate', 'cert.pem']], replace=True)
self.assertEqual(
nparser.parsed[filep],
[[['server'], [['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
['server_name', 'foobar.com'], ['#', COMMENT],
['server_name', 'example.*'], [],
['ssl_certificate', 'cert.pem'], ['#', COMMENT], [],
]]])
def test_get_best_match(self):
target_name = 'www.eff.org'

View File

@@ -65,7 +65,6 @@ def get_nginx_configurator(
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
server="https://acme-server.org:443/new",
tls_sni_01_port=5001,
dry_run=False,
),
name="nginx",
version=version)

View File

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

View File

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

View File

@@ -28,7 +28,6 @@ logger = logging.getLogger(__name__)
# pylint: disable=too-few-public-methods
class AnnotatedChallenge(jose.ImmutableMap):
"""Client annotated challenge.

View File

@@ -45,7 +45,7 @@ def rename_lineage(config):
"""
disp = zope.component.getUtility(interfaces.IDisplay)
certname = _get_certname(config, "rename")
certname = _get_certnames(config, "rename")[0]
new_certname = config.new_certname
if not new_certname:
@@ -87,11 +87,12 @@ def certificates(config):
def delete(config):
"""Delete Certbot files associated with a certificate lineage."""
certname = _get_certname(config, "delete")
storage.delete_files(config, certname)
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("Deleted all files relating to certificate {0}."
.format(certname), pause=False)
certnames = _get_certnames(config, "delete", allow_multiple=True)
for certname in certnames:
storage.delete_files(config, certname)
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("Deleted all files relating to certificate {0}."
.format(certname), pause=False)
###################
# Public Helpers
@@ -146,23 +147,34 @@ def find_duplicative_certs(config, domains):
# Private Helpers
###################
def _get_certname(config, verb):
def _get_certnames(config, verb, allow_multiple=False):
"""Get certname from flag, interactively, or error out.
"""
certname = config.certname
if not certname:
if certname:
certnames = [certname]
else:
disp = zope.component.getUtility(interfaces.IDisplay)
filenames = storage.renewal_conf_files(config)
choices = [storage.lineagename_for_filename(name) for name in filenames]
if not choices:
raise errors.Error("No existing certificates found.")
code, index = disp.menu("Which certificate would you like to {0}?".format(verb),
choices, flag="--cert-name",
force_interactive=True)
if code != display_util.OK or not index in range(0, len(choices)):
raise errors.Error("User ended interaction.")
certname = choices[index]
return certname
if allow_multiple:
code, certnames = disp.checklist(
"Which certificate(s) would you like to {0}?".format(verb),
choices, cli_flag="--cert-name",
force_interactive=True)
if code != display_util.OK:
raise errors.Error("User ended interaction.")
else:
code, index = disp.menu("Which certificate would you like to {0}?".format(verb),
choices, cli_flag="--cert-name",
force_interactive=True)
if code != display_util.OK or index not in range(0, len(choices)):
raise errors.Error("User ended interaction.")
certnames = [choices[index]]
return certnames
def _report_lines(msgs):
"""Format a results report for a category of single-line renewal outcomes"""
@@ -205,7 +217,7 @@ def _report_human_readable(config, parsed_certs):
" Certificate Path: {3}\n"
" Private Key Path: {4}".format(
cert.lineagename,
" ".join(cert.names()),
",".join(cert.names()),
valid_string,
cert.fullchain,
cert.privkey))

View File

@@ -11,6 +11,9 @@ import sys
import configargparse
import six
import zope.component
from zope.interface import interfaces as zope_interfaces
from acme import challenges
@@ -23,6 +26,7 @@ from certbot import hooks
from certbot import interfaces
from certbot import util
from certbot.display import util as display_util
from certbot.plugins import disco as plugins_disco
import certbot.plugins.selection as plugin_selection
@@ -45,8 +49,13 @@ if "CERTBOT_AUTO" in os.environ:
# user saved the script under a different name
LEAUTO = os.path.basename(os.environ["CERTBOT_AUTO"])
fragment = os.path.join(".local", "share", "letsencrypt")
cli_command = LEAUTO if fragment in sys.argv[0] else "certbot"
old_path_fragment = os.path.join(".local", "share", "letsencrypt")
new_path_prefix = os.path.abspath(os.path.join(os.sep, "opt",
"eff.org", "certbot", "venv"))
if old_path_fragment in sys.argv[0] or sys.argv[0].startswith(new_path_prefix):
cli_command = LEAUTO
else:
cli_command = "certbot"
# Argparse's help formatting has a lot of unhelpful peculiarities, so we want
# to replace as much of it as we can...
@@ -273,7 +282,7 @@ def flag_default(name):
# argparse has been set up; it is not accurate for all flags. Call it
# with caution. Plugin defaults are missing, and some things are using
# defaults defined in this file, not in constants.py :(
return constants.CLI_DEFAULTS[name]
return copy.deepcopy(constants.CLI_DEFAULTS[name])
def config_help(name, hidden=False):
@@ -347,7 +356,7 @@ VERB_HELP = [
" before and after renewal; see"
" https://certbot.eff.org/docs/using.html#renewal for more"
" information on these."),
"usage": "\n\n certbot renew [--cert-name NAME] [options]\n\n"
"usage": "\n\n certbot renew [--cert-name CERTNAME] [options]\n\n"
}),
("certificates", {
"short": "List certificates managed by Certbot",
@@ -439,9 +448,18 @@ class HelpfulArgumentParser(object):
"delete": main.delete,
}
# Get notification function for printing
try:
self.notify = zope.component.getUtility(
interfaces.IDisplay).notification
except zope_interfaces.ComponentLookupError:
self.notify = display_util.NoninteractiveDisplay(
sys.stdout).notification
# List of topics for which additional help can be provided
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
HELP_TOPICS += self.COMMANDS_TOPICS + ["manage"]
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"]
HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + ["manage"]
plugin_names = list(plugins)
self.help_topics = HELP_TOPICS + plugin_names + [None]
@@ -510,10 +528,10 @@ class HelpfulArgumentParser(object):
usage = SHORT_USAGE
if help_arg == True:
print(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE)
self.notify(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE)
sys.exit(0)
elif help_arg in self.COMMANDS_TOPICS:
print(usage + self._list_subcommands())
self.notify(usage + self._list_subcommands())
sys.exit(0)
elif help_arg == "all":
# if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at
@@ -848,36 +866,58 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"e.g. -vvv.")
helpful.add(
None, "-t", "--text", dest="text_mode", action="store_true",
help=argparse.SUPPRESS)
default=flag_default("text_mode"), help=argparse.SUPPRESS)
helpful.add(
None, "--max-log-backups", type=nonnegative_int,
default=flag_default("max_log_backups"),
help="Specifies the maximum number of backup logs that should "
"be kept by Certbot's built in log rotation. Setting this "
"flag to 0 disables log rotation entirely, causing "
"Certbot to always append to the same log file.")
helpful.add(
[None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive",
dest="noninteractive_mode", action="store_true",
default=flag_default("noninteractive_mode"),
help="Run without ever asking for user input. This may require "
"additional command line flags; the client will try to explain "
"which ones are required if it finds one missing")
helpful.add(
[None, "register", "run", "certonly"],
constants.FORCE_INTERACTIVE_FLAG, action="store_true",
default=flag_default("force_interactive"),
help="Force Certbot to be interactive even if it detects it's not "
"being run in a terminal. This flag cannot be used with the "
"renew subcommand.")
helpful.add(
[None, "run", "certonly", "certificates"],
"-d", "--domains", "--domain", dest="domains",
metavar="DOMAIN", action=_DomainsAction, default=[],
metavar="DOMAIN", action=_DomainsAction,
default=flag_default("domains"),
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
"as a parameter. (default: Ask)")
"as a parameter. The first domain provided will be the "
"subject CN of the certificate, and all domains will be "
"Subject Alternative Names on the certificate. "
"The first domain will also be used in "
"some software user interfaces and as the file paths for the "
"certificate and related material unless otherwise "
"specified or you already have a certificate with the same "
"name. In the case of a name collision it will append a number "
"like 0001 to the file path name. (default: Ask)")
helpful.add(
[None, "run", "certonly", "manage", "delete", "certificates"],
[None, "run", "certonly", "manage", "delete", "certificates", "renew"],
"--cert-name", dest="certname",
metavar="CERTNAME", default=None,
help="Certificate name to apply. Only one certificate name can be used "
"per Certbot run. To see certificate names, run 'certbot certificates'. "
"When creating a new certificate, specifies the new certificate's name.")
metavar="CERTNAME", default=flag_default("certname"),
help="Certificate name to apply. This name is used by Certbot for housekeeping "
"and in file paths; it doesn't affect the content of the certificate itself. "
"To see certificate names, run 'certbot certificates'. "
"When creating a new certificate, specifies the new certificate's name. "
"(default: the first provided domain or the name of an existing "
"certificate on your system for the same domains)")
helpful.add(
[None, "testing", "renew", "certonly"],
"--dry-run", action="store_true", dest="dry_run",
default=flag_default("dry_run"),
help="Perform a test run of the client, obtaining test (invalid) certificates"
" but not saving them to disk. This can currently only be used"
" with the 'certonly' and 'renew' subcommands. \nNote: Although --dry-run"
@@ -887,9 +927,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" in order to obtain test certificates, and reloads webservers to deploy and then"
" roll back those changes. It also calls --pre-hook and --post-hook commands"
" if they are defined because they may be necessary to accurately simulate"
" renewal. --renew-hook commands are not called.")
" renewal. --deploy-hook commands are not called.")
helpful.add(
["register", "automation"], "--register-unsafely-without-email", action="store_true",
default=flag_default("register_unsafely_without_email"),
help="Specifying this flag enables registering an account with no "
"email address. This is strongly discouraged, because in the "
"event of key loss or account compromise you will irrevocably "
@@ -900,27 +941,29 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"update to the web site.")
helpful.add(
"register", "--update-registration", action="store_true",
default=flag_default("update_registration"),
help="With the register verb, indicates that details associated "
"with an existing registration, such as the e-mail address, "
"should be updated, rather than registering a new account.")
helpful.add(
["register", "unregister", "automation"], "-m", "--email",
default=flag_default("email"),
help=config_help("email"))
helpful.add(["register", "automation"], "--eff-email", action="store_true",
default=None, dest="eff_email",
default=flag_default("eff_email"), dest="eff_email",
help="Share your e-mail address with EFF")
helpful.add(["register", "automation"], "--no-eff-email", action="store_false",
default=None, dest="eff_email",
default=flag_default("eff_email"), dest="eff_email",
help="Don't share your e-mail address with EFF")
helpful.add(
["automation", "certonly", "run"],
"--keep-until-expiring", "--keep", "--reinstall",
dest="reinstall", action="store_true",
dest="reinstall", action="store_true", default=flag_default("reinstall"),
help="If the requested certificate matches an existing certificate, always keep the "
"existing one until it is due for renewal (for the "
"'run' subcommand this means reinstall the existing certificate). (default: Ask)")
helpful.add(
"automation", "--expand", action="store_true",
"automation", "--expand", action="store_true", default=flag_default("expand"),
help="If an existing certificate is a strict subset of the requested names, "
"always expand and replace it with the additional names. (default: Ask)")
helpful.add(
@@ -929,21 +972,24 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
help="show program's version number and exit")
helpful.add(
["automation", "renew"],
"--force-renewal", "--renew-by-default",
action="store_true", dest="renew_by_default", help="If a certificate "
"--force-renewal", "--renew-by-default", dest="renew_by_default",
action="store_true", default=flag_default("renew_by_default"),
help="If a certificate "
"already exists for the requested domains, renew it now, "
"regardless of whether it is near expiry. (Often "
"--keep-until-expiring is more appropriate). Also implies "
"--expand.")
helpful.add(
"automation", "--renew-with-new-domains",
action="store_true", dest="renew_with_new_domains", help="If a "
"automation", "--renew-with-new-domains", dest="renew_with_new_domains",
action="store_true", default=flag_default("renew_with_new_domains"),
help="If a "
"certificate already exists for the requested certificate name "
"but does not match the requested domains, renew it now, "
"regardless of whether it is near expiry.")
helpful.add(
["automation", "renew", "certonly"],
"--allow-subset-of-names", action="store_true",
default=flag_default("allow_subset_of_names"),
help="When performing domain validation, do not consider it a failure "
"if authorizations can not be obtained for a strict subset of "
"the requested domains. This may be useful for allowing renewals for "
@@ -951,39 +997,51 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"at this system. This option cannot be used with --csr.")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
default=flag_default("tos"),
help="Agree to the ACME Subscriber Agreement (default: Ask)")
helpful.add(
["unregister", "automation"], "--account", metavar="ACCOUNT_ID",
default=flag_default("account"),
help="Account ID to use")
helpful.add(
"automation", "--duplicate", dest="duplicate", action="store_true",
default=flag_default("duplicate"),
help="Allow making a certificate lineage that duplicates an existing one "
"(both can be renewed in parallel)")
helpful.add(
"automation", "--os-packages-only", action="store_true",
default=flag_default("os_packages_only"),
help="(certbot-auto only) install OS package dependencies and then stop")
helpful.add(
"automation", "--no-self-upgrade", action="store_true",
default=flag_default("no_self_upgrade"),
help="(certbot-auto only) prevent the certbot-auto script from"
" upgrading itself to newer released versions (default: Upgrade"
" automatically)")
helpful.add(
"automation", "--no-bootstrap", action="store_true",
default=flag_default("no_bootstrap"),
help="(certbot-auto only) prevent the certbot-auto script from"
" installing OS-level dependencies (default: Prompt to install "
" OS-wide dependencies, but exit if the user says 'No')")
helpful.add(
"automation", "--pypi-mirror", action="store_true",
default=flag_default("pypi_mirror"),
help="(certbot-auto only) use an alternative Python package server "
"that is reachable from China")
helpful.add(
["automation", "renew", "certonly", "run"],
"-q", "--quiet", dest="quiet", action="store_true",
default=flag_default("quiet"),
help="Silence all output except errors. Useful for automation via cron."
" Implies --non-interactive.")
# overwrites server, handled in HelpfulArgumentParser.parse_args()
helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
action='store_true', dest='staging',
help='Use the staging server to obtain or revoke test (invalid) certificates; equivalent'
' to --server ' + constants.STAGING_URI)
dest="staging", action="store_true", default=flag_default("staging"),
help="Use the staging server to obtain or revoke test (invalid) certificates; equivalent"
" to --server " + constants.STAGING_URI)
helpful.add(
"testing", "--debug", action="store_true",
"testing", "--debug", action="store_true", default=flag_default("debug"),
help="Show tracebacks in case of errors, and allow certbot-auto "
"execution on experimental platforms")
helpful.add(
@@ -1013,6 +1071,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
default=flag_default("http01_address"), help=config_help("http01_address"))
helpful.add(
"testing", "--break-my-certs", action="store_true",
default=flag_default("break_my_certs"),
help="Be willing to replace or renew valid certificates with invalid "
"(testing/staging) certificates")
helpful.add(
@@ -1020,47 +1079,51 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
default=flag_default("rsa_key_size"), help=config_help("rsa_key_size"))
helpful.add(
"security", "--must-staple", action="store_true",
help=config_help("must_staple"), dest="must_staple", default=False)
dest="must_staple", default=flag_default("must_staple"),
help=config_help("must_staple"))
helpful.add(
"security", "--redirect", action="store_true",
"security", "--redirect", action="store_true", dest="redirect",
default=flag_default("redirect"),
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
"authenticated vhost. (default: Ask)", dest="redirect", default=None)
"authenticated vhost. (default: Ask)")
helpful.add(
"security", "--no-redirect", action="store_false",
"security", "--no-redirect", action="store_false", dest="redirect",
default=flag_default("redirect"),
help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
"authenticated vhost. (default: Ask)", dest="redirect", default=None)
"authenticated vhost. (default: Ask)")
helpful.add(
"security", "--hsts", action="store_true",
"security", "--hsts", action="store_true", dest="hsts", default=flag_default("hsts"),
help="Add the Strict-Transport-Security header to every HTTP response."
" Forcing browser to always use SSL for the domain."
" Defends against SSL Stripping.", dest="hsts", default=False)
" Defends against SSL Stripping.")
helpful.add(
"security", "--no-hsts", action="store_false",
help=argparse.SUPPRESS, dest="hsts", default=False)
"security", "--no-hsts", action="store_false", dest="hsts",
default=flag_default("hsts"), help=argparse.SUPPRESS)
helpful.add(
"security", "--uir", action="store_true",
help="Add the \"Content-Security-Policy: upgrade-insecure-requests\""
" header to every HTTP response. Forcing the browser to use"
" https:// for every http:// resource.", dest="uir", default=None)
"security", "--uir", action="store_true", dest="uir", default=flag_default("uir"),
help='Add the "Content-Security-Policy: upgrade-insecure-requests"'
' header to every HTTP response. Forcing the browser to use'
' https:// for every http:// resource.')
helpful.add(
"security", "--no-uir", action="store_false",
help=argparse.SUPPRESS, dest="uir", default=None)
"security", "--no-uir", action="store_false", dest="uir", default=flag_default("uir"),
help=argparse.SUPPRESS)
helpful.add(
"security", "--staple-ocsp", action="store_true",
"security", "--staple-ocsp", action="store_true", dest="staple",
default=flag_default("staple"),
help="Enables OCSP Stapling. A valid OCSP response is stapled to"
" the certificate that the server offers during TLS.",
dest="staple", default=None)
" the certificate that the server offers during TLS.")
helpful.add(
"security", "--no-staple-ocsp", action="store_false",
help=argparse.SUPPRESS, dest="staple", default=None)
"security", "--no-staple-ocsp", action="store_false", dest="staple",
default=flag_default("staple"), help=argparse.SUPPRESS)
helpful.add(
"security", "--strict-permissions", action="store_true",
default=flag_default("strict_permissions"),
help="Require that all configuration files are owned by the current "
"user; only needed if your config is somewhere unsafe like /tmp/")
helpful.add(
["manual", "standalone", "certonly", "renew"],
"--preferred-challenges", dest="pref_challs",
action=_PrefChallAction, default=[],
action=_PrefChallAction, default=flag_default("pref_challs"),
help='A sorted, comma delimited list of the preferred challenge to '
'use during authorization with the most preferred challenge '
'listed first (Eg, "dns" or "tls-sni-01,http,dns"). '
@@ -1085,24 +1148,29 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" run if an attempt was made to obtain/renew a certificate. If"
" multiple renewed certificates have identical post-hooks, only"
" one will be run.")
helpful.add("renew", "--renew-hook",
action=_RenewHookAction, help=argparse.SUPPRESS)
helpful.add(
"renew", "--renew-hook",
help="Command to be run in a shell once for each successfully renewed"
" certificate. For this command, the shell variable $RENEWED_LINEAGE"
" will point to the config live subdirectory (for example,"
" \"/etc/letsencrypt/live/example.com\") containing the new certificates"
" and keys; the shell variable $RENEWED_DOMAINS will contain a"
" space-delimited list of renewed certificate domains (for example,"
" \"example.com www.example.com\"")
"renew", "--deploy-hook", action=_DeployHookAction,
help='Command to be run in a shell once for each successfully'
' issued certificate. For this command, the shell variable'
' $RENEWED_LINEAGE will point to the config live subdirectory'
' (for example, "/etc/letsencrypt/live/example.com") containing'
' the new certificates and keys; the shell variable'
' $RENEWED_DOMAINS will contain a space-delimited list of'
' renewed certificate domains (for example, "example.com'
' www.example.com"')
helpful.add(
"renew", "--disable-hook-validation",
action='store_false', dest='validate_hooks', default=True,
action="store_false", dest="validate_hooks",
default=flag_default("validate_hooks"),
help="Ordinarily the commands specified for"
" --pre-hook/--post-hook/--renew-hook will be checked for validity, to"
" see if the programs being run are in the $PATH, so that mistakes can"
" be caught early, even when the hooks aren't being run just yet. The"
" validation is rather simplistic and fails if you use more advanced"
" shell constructs, so you can use this switch to disable it."
" --pre-hook/--post-hook/--deploy-hook will be checked for"
" validity, to see if the programs being run are in the $PATH,"
" so that mistakes can be caught early, even when the hooks"
" aren't being run just yet. The validation is rather"
" simplistic and fails if you use more advanced shell"
" constructs, so you can use this switch to disable it."
" (default: False)")
helpful.add_deprecated_argument("--agree-dev-preview", 0)
@@ -1121,42 +1189,53 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
def _create_subparsers(helpful):
helpful.add("config_changes", "--num", type=int,
helpful.add("config_changes", "--num", type=int, default=flag_default("num"),
help="How many past revisions you want to be displayed")
from certbot.client import sample_user_agent # avoid import loops
helpful.add(
None, "--user-agent", default=None,
help="Set a custom user agent string for the client. User agent strings allow "
"the CA to collect high level statistics about success rates by OS, "
"plugin and use case, and to know when to deprecate support for past Python "
None, "--user-agent", default=flag_default("user_agent"),
help='Set a custom user agent string for the client. User agent strings allow '
'the CA to collect high level statistics about success rates by OS, '
'plugin and use case, and to know when to deprecate support for past Python '
"versions and flags. If you wish to hide this information from the Let's "
'Encrypt server, set this to "". '
'(default: {0}). The flags encoded in the user agent are: '
'--duplicate, --force-renew, --allow-subset-of-names, -n, and '
'whether any hooks are set.'.format(sample_user_agent()))
helpful.add(
None, "--user-agent-comment", default=flag_default("user_agent_comment"),
type=_user_agent_comment_type,
help="Add a comment to the default user agent string. May be used when repackaging Certbot "
"or calling it from another tool to allow additional statistical data to be collected."
" Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0)")
helpful.add("certonly",
"--csr", type=read_file,
"--csr", default=flag_default("csr"), type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
" Currently --csr only works with the 'certonly' subcommand.")
helpful.add("revoke",
"--reason", dest="reason",
choices=CaseInsensitiveList(constants.REVOCATION_REASONS.keys()),
action=_EncodeReasonAction, default=0,
help="Specify reason for revoking certificate.")
choices=CaseInsensitiveList(sorted(constants.REVOCATION_REASONS,
key=constants.REVOCATION_REASONS.get)),
action=_EncodeReasonAction, default=flag_default("reason"),
help="Specify reason for revoking certificate. (default: unspecified)")
helpful.add("rollback",
"--checkpoints", type=int, metavar="N",
default=flag_default("rollback_checkpoints"),
help="Revert configuration N number of checkpoints.")
helpful.add("plugins",
"--init", action="store_true", help="Initialize plugins.")
"--init", action="store_true", default=flag_default("init"),
help="Initialize plugins.")
helpful.add("plugins",
"--prepare", action="store_true", help="Initialize and prepare plugins.")
"--prepare", action="store_true", default=flag_default("prepare"),
help="Initialize and prepare plugins.")
helpful.add("plugins",
"--authenticators", action="append_const", dest="ifaces",
default=flag_default("ifaces"),
const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
helpful.add("plugins",
"--installers", action="append_const", dest="ifaces",
default=flag_default("ifaces"),
const=interfaces.IInstaller, help="Limit to installer plugins only.")
@@ -1222,53 +1301,68 @@ def _plugins_parsing(helpful, plugins):
"a particular plugin by setting options provided below. Running "
"--help <plugin_name> will list flags specific to that plugin.")
helpful.add("plugins", "--configurator",
helpful.add("plugins", "--configurator", default=flag_default("configurator"),
help="Name of the plugin that is both an authenticator and an installer."
" Should not be used together with --authenticator or --installer. "
"(default: Ask)")
helpful.add("plugins", "-a", "--authenticator", help="Authenticator plugin name.")
helpful.add("plugins", "-i", "--installer",
helpful.add("plugins", "-a", "--authenticator", default=flag_default("authenticator"),
help="Authenticator plugin name.")
helpful.add("plugins", "-i", "--installer", default=flag_default("installer"),
help="Installer plugin name (also used to find domains).")
helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
"--apache", action="store_true",
"--apache", action="store_true", default=flag_default("apache"),
help="Obtain and install certificates using Apache")
helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
"--nginx", action="store_true", help="Obtain and install certificates using Nginx")
"--nginx", action="store_true", default=flag_default("nginx"),
help="Obtain and install certificates using Nginx")
helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
default=flag_default("standalone"),
help='Obtain certificates using a "standalone" webserver.')
helpful.add(["plugins", "certonly"], "--manual", action="store_true",
help='Provide laborious manual instructions for obtaining a certificate')
default=flag_default("manual"),
help="Provide laborious manual instructions for obtaining a certificate")
helpful.add(["plugins", "certonly"], "--webroot", action="store_true",
help='Obtain certificates by placing files in a webroot directory.')
default=flag_default("webroot"),
help="Obtain certificates by placing files in a webroot directory.")
helpful.add(["plugins", "certonly"], "--dns-cloudflare", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are '
'using Cloudflare for DNS).'))
default=flag_default("dns_cloudflare"),
help=("Obtain certificates using a DNS TXT record (if you are "
"using Cloudflare for DNS)."))
helpful.add(["plugins", "certonly"], "--dns-cloudxns", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are '
'using CloudXNS for DNS).'))
default=flag_default("dns_cloudxns"),
help=("Obtain certificates using a DNS TXT record (if you are "
"using CloudXNS for DNS)."))
helpful.add(["plugins", "certonly"], "--dns-digitalocean", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are '
'using DigitalOcean for DNS).'))
default=flag_default("dns_digitalocean"),
help=("Obtain certificates using a DNS TXT record (if you are "
"using DigitalOcean for DNS)."))
helpful.add(["plugins", "certonly"], "--dns-dnsimple", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are '
'using DNSimple for DNS).'))
default=flag_default("dns_dnsimple"),
help=("Obtain certificates using a DNS TXT record (if you are "
"using DNSimple for DNS)."))
helpful.add(["plugins", "certonly"], "--dns-dnsmadeeasy", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are'
'using DNS Made Easy for DNS).'))
default=flag_default("dns_dnsmadeeasy"),
help=("Obtain certificates using a DNS TXT record (if you are"
"using DNS Made Easy for DNS)."))
helpful.add(["plugins", "certonly"], "--dns-google", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are '
'using Google Cloud DNS).'))
default=flag_default("dns_google"),
help=("Obtain certificates using a DNS TXT record (if you are "
"using Google Cloud DNS)."))
helpful.add(["plugins", "certonly"], "--dns-luadns", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are '
'using LuaDNS for DNS).'))
default=flag_default("dns_luadns"),
help=("Obtain certificates using a DNS TXT record (if you are "
"using LuaDNS for DNS)."))
helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are '
'using NS1 for DNS).'))
default=flag_default("dns_nsone"),
help=("Obtain certificates using a DNS TXT record (if you are "
"using NS1 for DNS)."))
helpful.add(["plugins", "certonly"], "--dns-rfc2136", action="store_true",
help='Obtain certificates using a DNS TXT record (if you are using BIND for DNS).')
default=flag_default("dns_rfc2136"),
help="Obtain certificates using a DNS TXT record (if you are using BIND for DNS).")
helpful.add(["plugins", "certonly"], "--dns-route53", action="store_true",
help=('Obtain certificates using a DNS TXT record (if you are using Route53 for '
'DNS).'))
default=flag_default("dns_route53"),
help=("Obtain certificates using a DNS TXT record (if you are using Route53 for "
"DNS)."))
# things should not be reorder past/pre this comment:
# plugins_group should be displayed in --help before plugin
@@ -1349,3 +1443,53 @@ def parse_preferred_challenges(pref_challs):
raise errors.Error(
"Unrecognized challenges: {0}".format(unrecognized))
return challs
def _user_agent_comment_type(value):
if "(" in value or ")" in value:
raise argparse.ArgumentTypeError("may not contain parentheses")
return value
class _DeployHookAction(argparse.Action):
"""Action class for parsing deploy hooks."""
def __call__(self, parser, namespace, values, option_string=None):
renew_hook_set = namespace.deploy_hook != namespace.renew_hook
if renew_hook_set and namespace.renew_hook != values:
raise argparse.ArgumentError(
self, "conflicts with --renew-hook value")
namespace.deploy_hook = namespace.renew_hook = values
class _RenewHookAction(argparse.Action):
"""Action class for parsing renew hooks."""
def __call__(self, parser, namespace, values, option_string=None):
deploy_hook_set = namespace.deploy_hook is not None
if deploy_hook_set and namespace.deploy_hook != values:
raise argparse.ArgumentError(
self, "conflicts with --deploy-hook value")
namespace.renew_hook = values
def nonnegative_int(value):
"""Converts value to an int and checks that it is not negative.
This function should used as the type parameter for argparse
arguments.
:param str value: value provided on the command line
:returns: integer representation of value
:rtype: int
:raises argparse.ArgumentTypeError: if value isn't a non-negative integer
"""
try:
int_value = int(value)
except ValueError:
raise argparse.ArgumentTypeError("value must be an integer")
if int_value < 0:
raise argparse.ArgumentTypeError("value must be non-negative")
return int_value

View File

@@ -9,6 +9,7 @@ import OpenSSL
import zope.component
from acme import client as acme_client
from acme import crypto_util as acme_crypto_util
from acme import errors as acme_errors
from acme import jose
from acme import messages
@@ -57,11 +58,12 @@ def determine_user_agent(config):
# policy, talk to a core Certbot team member before making any
# changes here.
if config.user_agent is None:
ua = ("CertbotACMEClient/{0} ({1}; {2}) Authenticator/{3} Installer/{4} "
ua = ("CertbotACMEClient/{0} ({1}; {2}{8}) Authenticator/{3} Installer/{4} "
"({5}; flags: {6}) Py/{7}")
ua = ua.format(certbot.__version__, cli.cli_command, util.get_os_info_ua(),
config.authenticator, config.installer, config.verb,
ua_flags(config), platform.python_version())
ua_flags(config), platform.python_version(),
"; " + config.user_agent_comment if config.user_agent_comment else "")
else:
ua = config.user_agent
return ua
@@ -319,9 +321,17 @@ class Client(object):
domains = [d for d in domains if d in auth_domains]
# Create CSR from names
key = crypto_util.init_save_key(
self.config.rsa_key_size, self.config.key_dir)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
if self.config.dry_run:
key = util.Key(file=None,
pem=crypto_util.make_key(self.config.rsa_key_size))
csr = util.CSR(file=None, form="pem",
data=acme_crypto_util.make_csr(
key.pem, domains, self.config.must_staple))
else:
key = crypto_util.init_save_key(
self.config.rsa_key_size, self.config.key_dir)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
certr, chain = self.obtain_certificate_from_csr(
domains, csr, authzr=authzr)

View File

@@ -1,6 +1,7 @@
"""Certbot constants."""
import os
import logging
import os
import pkg_resources
from acme import challenges
@@ -18,24 +19,92 @@ CLI_DEFAULTS = dict(
os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"),
"letsencrypt", "cli.ini"),
],
dry_run=False,
# Main parser
verbose_count=-int(logging.INFO / 10),
server="https://acme-v01.api.letsencrypt.org/directory",
text_mode=False,
max_log_backups=1000,
noninteractive_mode=False,
force_interactive=False,
domains=[],
certname=None,
dry_run=False,
register_unsafely_without_email=False,
update_registration=False,
email=None,
eff_email=None,
reinstall=False,
expand=False,
renew_by_default=False,
renew_with_new_domains=False,
allow_subset_of_names=False,
tos=False,
account=None,
duplicate=False,
os_packages_only=False,
no_self_upgrade=False,
no_bootstrap=False,
pypi_mirror=False,
quiet=False,
staging=False,
debug=False,
debug_challenges=False,
no_verify_ssl=False,
tls_sni_01_port=challenges.TLSSNI01Response.PORT,
tls_sni_01_address="",
http01_port=challenges.HTTP01Response.PORT,
http01_address="",
break_my_certs=False,
rsa_key_size=2048,
must_staple=False,
redirect=None,
hsts=None,
uir=None,
staple=None,
strict_permissions=False,
pref_challs=[],
validate_hooks=True,
# Subparsers
num=None,
user_agent=None,
user_agent_comment=None,
csr=None,
reason=0,
rollback_checkpoints=1,
init=False,
prepare=False,
ifaces=None,
# Path parsers
auth_cert_path="./cert.pem",
auth_chain_path="./chain.pem",
key_path=None,
config_dir="/etc/letsencrypt",
work_dir="/var/lib/letsencrypt",
logs_dir="/var/log/letsencrypt",
no_verify_ssl=False,
http01_port=challenges.HTTP01Response.PORT,
http01_address="",
tls_sni_01_port=challenges.TLSSNI01Response.PORT,
tls_sni_01_address="",
server="https://acme-v01.api.letsencrypt.org/directory",
# Plugins parsers
configurator=None,
authenticator=None,
installer=None,
apache=False,
nginx=False,
standalone=False,
manual=False,
webroot=False,
dns_cloudflare=False,
dns_cloudxns=False,
dns_digitalocean=False,
dns_dnsimple=False,
dns_dnsmadeeasy=False,
dns_google=False,
dns_luadns=False,
dns_nsone=False,
dns_rfc2136=False,
dns_route53=False
auth_cert_path="./cert.pem",
auth_chain_path="./chain.pem",
strict_permissions=False,
debug_challenges=False,
)
STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
@@ -113,3 +182,18 @@ FORCE_INTERACTIVE_FLAG = "--force-interactive"
EFF_SUBSCRIBE_URI = "https://supporters.eff.org/subscribe/certbot"
"""EFF URI used to submit the e-mail address of users who opt-in."""
SSL_DHPARAMS_DEST = "ssl-dhparams.pem"
"""Name of the ssl_dhparams file as saved in `IConfig.config_dir`."""
SSL_DHPARAMS_SRC = pkg_resources.resource_filename(
"certbot", "ssl-dhparams.pem")
"""Path to the nginx ssl_dhparams file found in the Certbot distribution."""
UPDATED_SSL_DHPARAMS_DIGEST = ".updated-ssl-dhparams-pem-digest.txt"
"""Name of the hash of the updated or informed ssl_dhparams as saved in `IConfig.config_dir`."""
ALL_SSL_DHPARAMS_HASHES = [
'9ba6429597aeed2d8617a7705b56e96d044f64b07971659382e426675105654b',
]
"""SHA256 hashes of the contents of all versions of SSL_DHPARAMS_SRC"""

View File

@@ -55,15 +55,11 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
# Save file
util.make_or_verify_dir(key_dir, 0o700, os.geteuid(),
config.strict_permissions)
if config.dry_run:
key_path = None
logger.debug("Generating key (%d bits), not saving to file", key_size)
else:
key_f, key_path = util.unique_file(
os.path.join(key_dir, keyname), 0o600, "wb")
with key_f:
key_f.write(key_pem)
logger.debug("Generating key (%d bits): %s", key_size, key_path)
key_f, key_path = util.unique_file(
os.path.join(key_dir, keyname), 0o600, "wb")
with key_f:
key_f.write(key_pem)
logger.debug("Generating key (%d bits): %s", key_size, key_path)
return util.Key(key_path, key_pem)
@@ -90,15 +86,11 @@ def init_save_csr(privkey, names, path):
# Save CSR
util.make_or_verify_dir(path, 0o755, os.geteuid(),
config.strict_permissions)
if config.dry_run:
csr_filename = None
logger.debug("Creating CSR: not saving to file")
else:
csr_f, csr_filename = util.unique_file(
os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
with csr_f:
csr_f.write(csr_pem)
logger.debug("Creating CSR: %s", csr_filename)
csr_f, csr_filename = util.unique_file(
os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
with csr_f:
csr_f.write(csr_pem)
logger.debug("Creating CSR: %s", csr_filename)
return util.CSR(csr_filename, csr_pem, "pem")
@@ -222,7 +214,7 @@ def verify_renewable_cert(renewable_cert):
"""
verify_renewable_cert_sig(renewable_cert)
verify_fullchain(renewable_cert)
verify_cert_matches_priv_key(renewable_cert)
verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey)
def verify_renewable_cert_sig(renewable_cert):
@@ -246,27 +238,24 @@ def verify_renewable_cert_sig(renewable_cert):
raise errors.Error(error_str)
def verify_cert_matches_priv_key(renewable_cert):
def verify_cert_matches_priv_key(cert_path, key_path):
""" Verifies that the private key and cert match.
:param `.storage.RenewableCert` renewable_cert: cert to verify
:param str cert_path: path to a cert in PEM format
:param str key_path: path to a private key file
:raises errors.Error: If they don't match.
"""
try:
with open(renewable_cert.cert) as cert:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.read())
with open(renewable_cert.privkey) as privkey:
privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey.read())
context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
context.use_privatekey(privkey)
context.use_certificate(cert)
context.use_certificate_file(cert_path)
context.use_privatekey_file(key_path)
context.check_privatekey()
except (IOError, OpenSSL.SSL.Error) as e:
error_str = "verifying the cert located at {0} matches the \
private key located at {1} has failed. \
Details: {2}".format(renewable_cert.cert,
renewable_cert.privkey, e)
Details: {2}".format(cert_path,
key_path, e)
logger.exception(error_str)
raise errors.Error(error_str)

View File

@@ -42,12 +42,14 @@ def redirect_by_default():
"""
choices = [
("Easy", "Allow both HTTP and HTTPS access to these sites"),
("Secure", "Make all requests redirect to secure HTTPS access"),
("No redirect", "Make no further changes to the webserver configuration."),
("Redirect", "Make all requests redirect to secure HTTPS access. "
"Choose this for new sites, or if you're confident your site works on HTTPS. "
"You can undo this change by editing your web server's configuration."),
]
code, selection = util(interfaces.IDisplay).menu(
"Please choose whether HTTPS access is required or optional.",
"Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.",
choices, default=0,
cli_flag="--redirect / --no-redirect", force_interactive=True)

View File

@@ -29,18 +29,19 @@ class ErrorHandler(object):
"""Context manager for running code that must be cleaned up on failure.
The context manager allows you to register functions that will be called
when an exception (excluding SystemExit) or signal is encountered. Usage:
when an exception (excluding SystemExit) or signal is encountered.
Usage::
handler = ErrorHandler(cleanup1_func, *cleanup1_args, **cleanup1_kwargs)
handler.register(cleanup2_func, *cleanup2_args, **cleanup2_kwargs)
handler = ErrorHandler(cleanup1_func, *cleanup1_args, **cleanup1_kwargs)
handler.register(cleanup2_func, *cleanup2_args, **cleanup2_kwargs)
with handler:
do_something()
with handler:
do_something()
Or for one cleanup function:
Or for one cleanup function::
with ErrorHandler(func, args, kwargs):
do_something()
with ErrorHandler(func, args, kwargs):
do_something()
If an exception is raised out of do_something, the cleanup functions will
be called in last in first out order. Then the exception is raised.
@@ -84,7 +85,7 @@ class ErrorHandler(object):
return retval
def register(self, func, *args, **kwargs):
"""Sets func to be called with *args and **kwargs during cleanup
"""Sets func to be run with the given arguments during cleanup.
:param function func: function to be called in case of an error

View File

@@ -18,6 +18,7 @@ def validate_hooks(config):
"""Check hook commands are executable."""
validate_hook(config.pre_hook, "pre")
validate_hook(config.post_hook, "post")
validate_hook(config.deploy_hook, "deploy")
validate_hook(config.renew_hook, "renew")
@@ -95,16 +96,30 @@ def run_saved_post_hooks():
_run_hook(cmd)
def deploy_hook(config, domains, lineage_path):
"""Run post-issuance hook if defined.
:param configuration.NamespaceConfig config: Certbot settings
:param domains: domains in the obtained certificate
:type domains: `list` of `str`
:param str lineage_path: live directory path for the new cert
"""
if config.deploy_hook:
renew_hook(config, domains, lineage_path)
def renew_hook(config, domains, lineage_path):
"""Run post-renewal hook if defined."""
if config.renew_hook:
if not config.dry_run:
os.environ["RENEWED_DOMAINS"] = " ".join(domains)
os.environ["RENEWED_LINEAGE"] = lineage_path
logger.info("Running renew-hook command: %s", config.renew_hook)
logger.info("Running deploy-hook command: %s", config.renew_hook)
_run_hook(config.renew_hook)
else:
logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook)
logger.warning(
"Dry run: skipping deploy hook command: %s", config.renew_hook)
def _run_hook(shell_cmd):

View File

@@ -138,7 +138,8 @@ def setup_log_file_handler(config, logfile, fmt):
log_file_path = os.path.join(config.logs_dir, logfile)
try:
handler = logging.handlers.RotatingFileHandler(
log_file_path, maxBytes=2 ** 20, backupCount=1000)
log_file_path, maxBytes=2 ** 20,
backupCount=config.max_log_backups)
except IOError as error:
raise errors.Error(util.PERM_ERR_FMT.format(error))
# rotate on each invocation, rollover only possible when maxBytes

View File

@@ -1,5 +1,6 @@
"""Certbot main entry point."""
from __future__ import print_function
import functools
import logging.handlers
import os
import sys
@@ -82,6 +83,8 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N
lineage = le_client.obtain_and_enroll_certificate(domains, certname)
if lineage is False:
raise errors.Error("Certificate could not be obtained")
elif lineage is not None:
hooks.deploy_hook(config, lineage.names(), lineage.live_dir)
finally:
hooks.post_hook(config)
@@ -291,11 +294,12 @@ def _find_domains_or_certname(config, installer):
return domains, certname
def _report_new_cert(config, cert_path, fullchain_path):
def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
"""Reports the creation of a new certificate to the user.
:param str cert_path: path to cert
:param str fullchain_path: path to full chain
:param str key_path: path to private key, if available
"""
if config.dry_run:
@@ -310,13 +314,17 @@ def _report_new_cert(config, cert_path, fullchain_path):
# (Nginx and Apache2.4) will want.
verbswitch = ' with the "certonly" option' if config.verb == "run" else ""
privkey_statement = 'Your key file has been saved at:{br}{0}{br}'.format(
key_path, br=os.linesep) if key_path else ""
# XXX Perhaps one day we could detect the presence of known old webservers
# and say something more informative here.
msg = ('Congratulations! Your certificate and chain have been saved at {0}.'
' Your cert will expire on {1}. To obtain a new or tweaked version of this '
'certificate in the future, simply run {2} again{3}. '
'To non-interactively renew *all* of your certificates, run "{2} renew"'
.format(fullchain_path, expiry, cli.cli_command, verbswitch))
msg = ('Congratulations! Your certificate and chain have been saved at:{br}'
'{0}{br}{1}'
'Your cert will expire on {2}. To obtain a new or tweaked version of this '
'certificate in the future, simply run {3} again{4}. '
'To non-interactively renew *all* of your certificates, run "{3} renew"'
.format(fullchain_path, privkey_statement, expiry, cli.cli_command, verbswitch,
br=os.linesep))
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
@@ -485,7 +493,7 @@ def install(config, plugins):
_install_cert(config, le_client, domains)
def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print
def plugins_cmd(config, plugins):
"""List server software plugins."""
logger.debug("Expected interfaces: %s", config.ifaces)
@@ -493,8 +501,10 @@ def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print
filtered = plugins.visible().ifaces(ifaces)
logger.debug("Filtered plugins: %r", filtered)
notify = functools.partial(zope.component.getUtility(
interfaces.IDisplay).notification, pause=False)
if not config.init and not config.prepare:
print(str(filtered))
notify(str(filtered))
return
filtered.init(config)
@@ -502,13 +512,13 @@ def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print
logger.debug("Verified plugins: %r", verified)
if not config.prepare:
print(str(verified))
notify(str(verified))
return
verified.prepare()
available = verified.available()
logger.debug("Prepared plugins: %s", available)
print(str(available))
notify(str(available))
def rollback(config, plugins):
@@ -560,6 +570,7 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config
if config.key_path is not None: # revocation by cert key
logger.debug("Revoking %s using cert key %s",
config.cert_path[0], config.key_path[0])
crypto_util.verify_cert_matches_priv_key(config.cert_path[0], config.key_path[0])
key = jose.JWK.load(config.key_path[1])
else: # revocation by account key
logger.debug("Revoking %s using Account Key", config.cert_path[0])
@@ -599,7 +610,8 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
cert_path = new_lineage.cert_path if new_lineage else None
fullchain_path = new_lineage.fullchain_path if new_lineage else None
_report_new_cert(config, cert_path, fullchain_path)
key_path = new_lineage.key_path if new_lineage else None
_report_new_cert(config, cert_path, fullchain_path, key_path)
_install_cert(config, le_client, domains, new_lineage)
@@ -636,6 +648,7 @@ def renew_cert(config, plugins, lineage):
except errors.PluginSelectionError as e:
logger.info("Could not choose appropriate plugin: %s", e)
raise
le_client = _init_le_client(config, auth, installer)
_get_and_save_cert(le_client, config, lineage=lineage)
@@ -664,6 +677,7 @@ def certonly(config, plugins):
except errors.PluginSelectionError as e:
logger.info("Could not choose appropriate plugin: %s", e)
raise
le_client = _init_le_client(config, auth, installer)
if config.csr:
@@ -684,7 +698,8 @@ def certonly(config, plugins):
cert_path = lineage.cert_path if lineage else None
fullchain_path = lineage.fullchain_path if lineage else None
_report_new_cert(config, cert_path, fullchain_path)
key_path = lineage.key_path if lineage else None
_report_new_cert(config, cert_path, fullchain_path, key_path)
_suggest_donation_if_appropriate(config)
def renew(config, unused_plugins):
@@ -731,8 +746,14 @@ def main(cli_args=sys.argv[1:]):
config = configuration.NamespaceConfig(args)
zope.component.provideUtility(config)
log.post_arg_parse_setup(config)
make_or_verify_needed_dirs(config)
try:
log.post_arg_parse_setup(config)
make_or_verify_needed_dirs(config)
except errors.Error:
# Let plugins_cmd be run as un-privileged user.
if config.func != plugins_cmd:
raise
set_displayer(config)
# Reporter

View File

@@ -13,7 +13,9 @@ from acme.jose import util as jose_util
from certbot import constants
from certbot import crypto_util
from certbot import errors
from certbot import interfaces
from certbot import reverter
from certbot import util
logger = logging.getLogger(__name__)
@@ -100,6 +102,120 @@ class Plugin(object):
# other
class Installer(Plugin):
"""An installer base class with reverter and ssl_dhparam methods defined.
Installer plugins do not have to inherit from this class.
"""
def __init__(self, *args, **kwargs):
super(Installer, self).__init__(*args, **kwargs)
self.reverter = reverter.Reverter(self.config)
def add_to_checkpoint(self, save_files, save_notes, temporary=False):
"""Add files to a checkpoint.
:param set save_files: set of filepaths to save
:param str save_notes: notes about changes during the save
:param bool temporary: True if the files should be added to a
temporary checkpoint rather than a permanent one. This is
usually used for changes that will soon be reverted.
:raises .errors.PluginError: when unable to add to checkpoint
"""
if temporary:
checkpoint_func = self.reverter.add_to_temp_checkpoint
else:
checkpoint_func = self.reverter.add_to_checkpoint
try:
checkpoint_func(save_files, save_notes)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
def finalize_checkpoint(self, title):
"""Timestamp and save changes made through the reverter.
:param str title: Title describing checkpoint
:raises .errors.PluginError: when an error occurs
"""
try:
self.reverter.finalize_checkpoint(title)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
def recovery_routine(self):
"""Revert all previously modified files.
Reverts all modified files that have not been saved as a checkpoint
:raises .errors.PluginError: If unable to recover the configuration
"""
try:
self.reverter.recovery_routine()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
def revert_temporary_config(self):
"""Rollback temporary checkpoint.
:raises .errors.PluginError: when unable to revert config
"""
try:
self.reverter.revert_temporary_config()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
def rollback_checkpoints(self, rollback=1):
"""Rollback saved checkpoints.
:param int rollback: Number of checkpoints to revert
:raises .errors.PluginError: If there is a problem with the input or
the function is unable to correctly revert the configuration
"""
try:
self.reverter.rollback_checkpoints(rollback)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
def view_config_changes(self):
"""Show all of the configuration changes that have taken place.
:raises .errors.PluginError: If there is a problem while processing
the checkpoints directories.
"""
try:
self.reverter.view_config_changes()
except errors.ReverterError as err:
raise errors.PluginError(str(err))
@property
def ssl_dhparams(self):
"""Full absolute path to ssl_dhparams file."""
return os.path.join(self.config.config_dir, constants.SSL_DHPARAMS_DEST)
@property
def updated_ssl_dhparams_digest(self):
"""Full absolute path to digest of updated ssl_dhparams file."""
return os.path.join(self.config.config_dir, constants.UPDATED_SSL_DHPARAMS_DIGEST)
def install_ssl_dhparams(self):
"""Copy Certbot's ssl_dhparams file into the system's config dir if required."""
return install_version_controlled_file(
self.ssl_dhparams,
self.updated_ssl_dhparams_digest,
constants.SSL_DHPARAMS_SRC,
constants.ALL_SSL_DHPARAMS_HASHES)
class Addr(object):
r"""Represents an virtual host address.
@@ -270,51 +386,50 @@ class TLSSNI01(object):
return response
def install_ssl_options_conf(options_ssl, options_ssl_digest, mod_ssl_conf_src,
all_ssl_options_hashes):
"""Copy Certbot's SSL options file into the system's config dir if required.
def install_version_controlled_file(dest_path, digest_path, src_path, all_hashes):
"""Copy a file into an active location (likely the system's config dir) if required.
:param str options_ssl: destination path for file containing ssl options
:param str options_ssl_digest: path to save a digest of options_ssl in
:param str mod_ssl_conf_src: path to file containing ssl options found in distribution
:param list all_ssl_options_hashes: hashes of every released version of options_ssl
:param str dest_path: destination path for version controlled file
:param str digest_path: path to save a digest of the file in
:param str src_path: path to version controlled file found in distribution
:param list all_hashes: hashes of every released version of the file
"""
current_ssl_options_hash = crypto_util.sha256sum(mod_ssl_conf_src)
current_hash = crypto_util.sha256sum(src_path)
def _write_current_hash():
with open(options_ssl_digest, "w") as f:
f.write(current_ssl_options_hash)
with open(digest_path, "w") as f:
f.write(current_hash)
def _install_current_file():
shutil.copyfile(mod_ssl_conf_src, options_ssl)
shutil.copyfile(src_path, dest_path)
_write_current_hash()
# Check to make sure options-ssl.conf is installed
if not os.path.isfile(options_ssl):
if not os.path.isfile(dest_path):
_install_current_file()
return
# there's already a file there. if it's up to date, do nothing. if it's not but
# it matches a known file hash, we can update it.
# otherwise, print a warning once per new version.
active_file_digest = crypto_util.sha256sum(options_ssl)
if active_file_digest == current_ssl_options_hash: # already up to date
active_file_digest = crypto_util.sha256sum(dest_path)
if active_file_digest == current_hash: # already up to date
return
elif active_file_digest in all_ssl_options_hashes: # safe to update
elif active_file_digest in all_hashes: # safe to update
_install_current_file()
else: # has been manually modified, not safe to update
# did they modify the current version or an old version?
if os.path.isfile(options_ssl_digest):
with open(options_ssl_digest, "r") as f:
if os.path.isfile(digest_path):
with open(digest_path, "r") as f:
saved_digest = f.read()
# they modified it after we either installed or told them about this version, so return
if saved_digest == current_ssl_options_hash:
if saved_digest == current_hash:
return
# there's a new version but we couldn't update the file, or they deleted the digest.
# save the current digest so we only print this once, and print a warning
_write_current_hash()
logger.warning("%s has been manually modified; updated ssl configuration options "
logger.warning("%s has been manually modified; updated file "
"saved to %s. We recommend updating %s for security purposes.",
options_ssl, mod_ssl_conf_src, options_ssl)
dest_path, src_path, dest_path)
# test utils used by certbot_apache/certbot_nginx (hence

View File

@@ -1,4 +1,5 @@
"""Tests for certbot.plugins.common."""
import functools
import os
import shutil
import tempfile
@@ -12,6 +13,7 @@ from acme import jose
from certbot import achallenges
from certbot import crypto_util
from certbot import errors
from certbot.tests import acme_util
from certbot.tests import util as test_util
@@ -77,6 +79,107 @@ class PluginTest(unittest.TestCase):
"--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None)
class InstallerTest(test_util.ConfigTestCase):
"""Tests for certbot.plugins.common.Installer."""
def setUp(self):
super(InstallerTest, self).setUp()
os.mkdir(self.config.config_dir)
from certbot.plugins.common import Installer
with mock.patch("certbot.plugins.common.reverter.Reverter"):
self.installer = Installer(config=self.config,
name="Installer")
self.reverter = self.installer.reverter
def test_add_to_real_checkpoint(self):
files = set(("foo.bar", "baz.qux",))
save_notes = "foo bar baz qux"
self._test_wrapped_method("add_to_checkpoint", files, save_notes)
def test_add_to_real_checkpoint2(self):
self._test_add_to_checkpoint_common(False)
def test_add_to_temporary_checkpoint(self):
self._test_add_to_checkpoint_common(True)
def _test_add_to_checkpoint_common(self, temporary):
files = set(("foo.bar", "baz.qux",))
save_notes = "foo bar baz qux"
installer_func = functools.partial(self.installer.add_to_checkpoint,
temporary=temporary)
if temporary:
reverter_func = self.reverter.add_to_temp_checkpoint
else:
reverter_func = self.reverter.add_to_checkpoint
self._test_adapted_method(
installer_func, reverter_func, files, save_notes)
def test_finalize_checkpoint(self):
self._test_wrapped_method("finalize_checkpoint", "foo")
def test_recovery_routine(self):
self._test_wrapped_method("recovery_routine")
def test_revert_temporary_config(self):
self._test_wrapped_method("revert_temporary_config")
def test_rollback_checkpoints(self):
self._test_wrapped_method("rollback_checkpoints", 42)
def test_view_config_changes(self):
self._test_wrapped_method("view_config_changes")
def _test_wrapped_method(self, name, *args, **kwargs):
"""Test a wrapped reverter method.
:param str name: name of the method to test
:param tuple args: position arguments to method
:param dict kwargs: keyword arguments to method
"""
installer_func = getattr(self.installer, name)
reverter_func = getattr(self.reverter, name)
self._test_adapted_method(
installer_func, reverter_func, *args, **kwargs)
def _test_adapted_method(self, installer_func,
reverter_func, *passed_args, **passed_kwargs):
"""Test an adapted reverter method
:param callable installer_func: installer method to test
:param mock.MagicMock reverter_func: mocked adapated
reverter method
:param tuple passed_args: positional arguments passed from
installer method to the reverter method
:param dict passed_kargs: keyword arguments passed from
installer method to the reverter method
"""
installer_func(*passed_args, **passed_kwargs)
reverter_func.assert_called_once_with(*passed_args, **passed_kwargs)
reverter_func.side_effect = errors.ReverterError
self.assertRaises(
errors.PluginError, installer_func, *passed_args, **passed_kwargs)
def test_install_ssl_dhparams(self):
self.installer.install_ssl_dhparams()
self.assertTrue(os.path.isfile(self.installer.ssl_dhparams))
def _current_ssl_dhparams_hash(self):
from certbot.constants import SSL_DHPARAMS_SRC
return crypto_util.sha256sum(SSL_DHPARAMS_SRC)
def test_current_file_hash_in_all_hashes(self):
from certbot.constants import ALL_SSL_DHPARAMS_HASHES
self.assertTrue(self._current_ssl_dhparams_hash() in ALL_SSL_DHPARAMS_HASHES,
"Constants.ALL_SSL_DHPARAMS_HASHES must be appended"
" with the sha256 hash of self.config.ssl_dhparams when it is updated.")
class AddrTest(unittest.TestCase):
"""Tests for certbot.client.plugins.common.Addr."""
@@ -202,7 +305,7 @@ class TLSSNI01Test(unittest.TestCase):
achall.chall.encode.return_value = "token"
key = test_util.load_pyopenssl_private_key("rsa512_key.pem")
achall.response_and_validation.return_value = (
response, (test_util.load_cert("cert.pem"), key))
response, (test_util.load_cert("cert_512.pem"), key))
with mock.patch("certbot.plugins.common.open",
mock_open, create=True):
@@ -215,7 +318,7 @@ class TLSSNI01Test(unittest.TestCase):
# pylint: disable=no-member
mock_open.assert_called_once_with(self.sni.get_cert_path(achall), "wb")
mock_open.return_value.write.assert_called_once_with(
test_util.load_vector("cert.pem"))
test_util.load_vector("cert_512.pem"))
mock_safe_open.assert_called_once_with(
self.sni.get_key_path(achall), "wb", chmod=0o400)
mock_safe_open.return_value.write.assert_called_once_with(
@@ -227,11 +330,11 @@ class TLSSNI01Test(unittest.TestCase):
achall.response(achall.account_key).z_domain.decode("utf-8"))
class InstallSslOptionsConfTest(test_util.TempDirTestCase):
"""Tests for certbot.plugins.common.install_ssl_options_conf."""
class InstallVersionControlledFileTest(test_util.TempDirTestCase):
"""Tests for certbot.plugins.common.install_version_controlled_file."""
def setUp(self):
super(InstallSslOptionsConfTest, self).setUp()
super(InstallVersionControlledFileTest, self).setUp()
self.hashes = ["someotherhash"]
self.dest_path = os.path.join(self.tempdir, "options-ssl-dest.conf")
self.hash_path = os.path.join(self.tempdir, ".options-ssl-conf.txt")
@@ -243,19 +346,19 @@ class InstallSslOptionsConfTest(test_util.TempDirTestCase):
self.hashes.append(crypto_util.sha256sum(path))
def _call(self):
from certbot.plugins.common import install_ssl_options_conf
install_ssl_options_conf(self.dest_path,
self.hash_path,
self.source_path,
self.hashes)
from certbot.plugins.common import install_version_controlled_file
install_version_controlled_file(self.dest_path,
self.hash_path,
self.source_path,
self.hashes)
def _current_ssl_options_hash(self):
def _current_file_hash(self):
return crypto_util.sha256sum(self.source_path)
def _assert_current_file(self):
self.assertTrue(os.path.isfile(self.dest_path))
self.assertEqual(crypto_util.sha256sum(self.dest_path),
self._current_ssl_options_hash())
self._current_file_hash())
def test_no_file(self):
self.assertFalse(os.path.isfile(self.dest_path))
@@ -282,9 +385,9 @@ class InstallSslOptionsConfTest(test_util.TempDirTestCase):
self.assertFalse(mock_logger.warning.called)
self.assertTrue(os.path.isfile(self.dest_path))
self.assertEqual(crypto_util.sha256sum(self.source_path),
self._current_ssl_options_hash())
self._current_file_hash())
self.assertNotEqual(crypto_util.sha256sum(self.dest_path),
self._current_ssl_options_hash())
self._current_file_hash())
def test_manually_modified_past_file_warns(self):
with open(self.dest_path, "a") as mod_ssl_conf:
@@ -294,10 +397,10 @@ class InstallSslOptionsConfTest(test_util.TempDirTestCase):
with mock.patch("certbot.plugins.common.logger") as mock_logger:
self._call()
self.assertEqual(mock_logger.warning.call_args[0][0],
"%s has been manually modified; updated ssl configuration options "
"%s has been manually modified; updated file "
"saved to %s. We recommend updating %s for security purposes.")
self.assertEqual(crypto_util.sha256sum(self.source_path),
self._current_ssl_options_hash())
self._current_file_hash())
# only print warning once
with mock.patch("certbot.plugins.common.logger") as mock_logger:
self._call()

View File

@@ -108,11 +108,19 @@ def choose_plugin(prepared, question):
opts = [plugin_ep.description_with_name +
(" [Misconfigured]" if plugin_ep.misconfigured else "")
for plugin_ep in prepared]
names = set(plugin_ep.name for plugin_ep in prepared)
while True:
disp = z_util(interfaces.IDisplay)
code, index = disp.menu(
question, opts, force_interactive=True)
if "CERTBOT_AUTO" in os.environ and names == set(("apache", "nginx")):
# The possibility of being offered exactly apache and nginx here
# is new interactivity brought by https://github.com/certbot/certbot/issues/4079,
# so set apache as a default for those kinds of non-interactive use
# (the user will get a warning to set --non-interactive or --force-interactive)
apache_idx = [n for n, p in enumerate(prepared) if p.name == "apache"][0]
code, index = disp.menu(question, opts, default=apache_idx)
else:
code, index = disp.menu(question, opts, force_interactive=True)
if code == display_util.OK:
plugin_ep = prepared[index]
@@ -134,6 +142,8 @@ def record_chosen_plugins(config, plugins, auth, inst):
"Update the config entries to reflect the plugins we actually selected."
config.authenticator = plugins.find_init(auth).name if auth else "None"
config.installer = plugins.find_init(inst).name if inst else "None"
logger.info("Plugins selected: Authenticator %s, Installer %s",
config.authenticator, config.installer)
def choose_configurator_plugins(config, plugins, verb):

View File

@@ -1,4 +1,5 @@
"""Tests for letsencrypt.plugins.selection"""
import os
import sys
import unittest
@@ -115,6 +116,7 @@ class ChoosePluginTest(unittest.TestCase):
False))
self.mock_apache = mock.Mock(
description_with_name="a", misconfigured=True)
self.mock_apache.name = "apache"
self.mock_stand = mock.Mock(
description_with_name="s", misconfigured=False)
self.mock_stand.init().more_info.return_value = "standalone"
@@ -146,3 +148,26 @@ class ChoosePluginTest(unittest.TestCase):
def test_no_choice(self, mock_util):
mock_util().menu.return_value = (display_util.CANCEL, 0)
self.assertTrue(self._call() is None)
@test_util.patch_get_utility("certbot.plugins.selection.z_util")
def test_new_interaction_avoidance(self, mock_util):
mock_nginx = mock.Mock(
description_with_name="n", misconfigured=False)
mock_nginx.init().more_info.return_value = "nginx plugin"
mock_nginx.name = "nginx"
self.plugins[1] = mock_nginx
mock_util().menu.return_value = (display_util.CANCEL, 0)
unset_cb_auto = os.environ.get("CERTBOT_AUTO") is None
if unset_cb_auto:
os.environ["CERTBOT_AUTO"] = "foo"
try:
self._call()
finally:
if unset_cb_auto:
del os.environ["CERTBOT_AUTO"]
self.assertTrue("default" in mock_util().menu.call_args[1])
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -158,10 +158,11 @@ class AuthenticatorTest(unittest.TestCase):
@test_util.patch_get_utility()
def test_perform_eaddrinuse_retry(self, mock_get_utility):
mock_utility = mock_get_utility()
errno = socket.errno.EADDRINUSE
error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1)
self.auth.servers.run.side_effect = [error] + 2 * [mock.MagicMock()]
mock_yesno = mock_get_utility.return_value.yesno
mock_yesno = mock_utility.yesno
mock_yesno.return_value = True
self.test_perform()
@@ -169,7 +170,8 @@ class AuthenticatorTest(unittest.TestCase):
@test_util.patch_get_utility()
def test_perform_eaddrinuse_no_retry(self, mock_get_utility):
mock_yesno = mock_get_utility.return_value.yesno
mock_utility = mock_get_utility()
mock_yesno = mock_utility.yesno
mock_yesno.return_value = False
errno = socket.errno.EADDRINUSE

View File

@@ -320,6 +320,12 @@ def _renew_describe_results(config, renew_successes, renew_failures,
out = []
notify = out.append
disp = zope.component.getUtility(interfaces.IDisplay)
def notify_error(err):
"""Notify and log errors."""
notify(err)
logger.error(err)
if config.dry_run:
notify("** DRY RUN: simulating 'certbot renew' close to cert expiry")
@@ -338,14 +344,14 @@ def _renew_describe_results(config, renew_successes, renew_failures,
"have been renewed:")
notify(report(renew_successes, "success"))
elif renew_failures and not renew_successes:
notify("All renewal attempts failed. The following certs could not be "
"renewed:")
notify(report(renew_failures, "failure"))
notify_error("All renewal attempts failed. The following certs could "
"not be renewed:")
notify_error(report(renew_failures, "failure"))
elif renew_failures and renew_successes:
notify("The following certs were successfully renewed:")
notify(report(renew_successes, "success"))
notify("\nThe following certs could not be renewed:")
notify(report(renew_failures, "failure"))
notify(report(renew_successes, "success") + "\n")
notify_error("The following certs could not be renewed:")
notify_error(report(renew_failures, "failure"))
if parse_failures:
notify("\nAdditionally, the following renewal configuration files "
@@ -356,9 +362,7 @@ def _renew_describe_results(config, renew_successes, renew_failures,
notify("** DRY RUN: simulating 'certbot renew' close to cert expiry")
notify("** (The test certificates above have not been saved.)")
if config.quiet and not (renew_failures or parse_failures):
return
print("\n".join(out))
disp.notification("\n".join(out), wrap=False)
def handle_renewal_request(config):
@@ -372,8 +376,8 @@ def handle_renewal_request(config):
"renewing all installed certificates that are due "
"to be renewed or renewing a single certificate specified "
"by its name. If you would like to renew specific "
"certificates by their domains, use the certonly "
"command. The renew verb may provide other options "
"certificates by their domains, use the certonly command "
"instead. The renew verb may provide other options "
"for selecting certificates to renew in the future.")
if config.certname:
@@ -389,14 +393,16 @@ def handle_renewal_request(config):
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("Processing " + renewal_file, pause=False)
lineage_config = copy.deepcopy(config)
lineagename = storage.lineagename_for_filename(renewal_file)
# Note that this modifies config (to add back the configuration
# elements from within the renewal configuration file).
try:
renewal_candidate = _reconstitute(lineage_config, renewal_file)
except Exception as e: # pylint: disable=broad-except
logger.warning("Renewal configuration file %s produced an "
"unexpected error: %s. Skipping.", renewal_file, e)
logger.warning("Renewal configuration file %s (cert: %s) "
"produced an unexpected error: %s. Skipping.",
renewal_file, lineagename, e)
logger.debug("Traceback was:\n%s", traceback.format_exc())
parse_failures.append(renewal_file)
continue
@@ -422,8 +428,9 @@ def handle_renewal_request(config):
renew_skipped.append(renewal_candidate.fullchain)
except Exception as e: # pylint: disable=broad-except
# obtain_cert (presumably) encountered an unanticipated problem.
logger.warning("Attempting to renew cert from %s produced an "
"unexpected error: %s. Skipping.", renewal_file, e)
logger.warning("Attempting to renew cert (%s) from %s produced an "
"unexpected error: %s. Skipping.", lineagename,
renewal_file, e)
logger.debug("Traceback was:\n%s", traceback.format_exc())
renew_failures.append(renewal_candidate.fullchain)

8
certbot/ssl-dhparams.pem Normal file
View File

@@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----

View File

@@ -186,8 +186,15 @@ def get_link_target(link):
:returns: Absolute path to the target of link
:rtype: str
:raises .CertStorageError: If link does not exists.
"""
target = os.readlink(link)
try:
target = os.readlink(link)
except OSError:
raise errors.CertStorageError(
"Expected {0} to be a symlink".format(link))
if not os.path.isabs(target):
target = os.path.join(os.path.dirname(link), target)
return os.path.abspath(target)

View File

@@ -14,12 +14,10 @@ from acme import messages
from certbot import errors
from certbot.tests import util
from certbot.tests.util import TempDirTestCase
import certbot.tests.util as test_util
KEY = jose.JWKRSA.load(util.load_vector("rsa512_key_2.pem"))
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
class AccountTest(unittest.TestCase):
@@ -48,22 +46,19 @@ class AccountTest(unittest.TestCase):
def test_id(self):
self.assertEqual(
self.acc.id, "bca5889f66457d5b62fbba7b25f9ab6f")
self.acc.id, "7adac10320f585ddf118429c0c4af2cd")
def test_slug(self):
self.assertEqual(
self.acc.slug, "test.certbot.org@2015-07-04T14:04:10Z (bca5)")
self.acc.slug, "test.certbot.org@2015-07-04T14:04:10Z (7ada)")
def test_repr(self):
self.assertTrue(repr(self.acc).startswith(
"<Account(i_am_a_regr, bca5889f66457d5b62fbba7b25f9ab6f, Meta("))
"<Account(i_am_a_regr, 7adac10320f585ddf118429c0c4af2cd, Meta("))
class ReportNewAccountTest(unittest.TestCase):
class ReportNewAccountTest(test_util.ConfigTestCase):
"""Tests for certbot.account.report_new_account."""
def setUp(self):
self.config = mock.MagicMock(config_dir="/etc/letsencrypt")
def _call(self):
from certbot.account import report_new_account
report_new_account(self.config)
@@ -98,14 +93,12 @@ class AccountMemoryStorageTest(unittest.TestCase):
self.assertEqual([account], self.storage.find_all())
class AccountFileStorageTest(TempDirTestCase):
class AccountFileStorageTest(test_util.ConfigTestCase):
"""Tests for certbot.account.AccountFileStorage."""
def setUp(self):
super(AccountFileStorageTest, self).setUp()
self.config = mock.MagicMock(
accounts_dir=os.path.join(self.tempdir, "accounts"))
from certbot.account import AccountFileStorage
self.storage = AccountFileStorage(self.config)

View File

@@ -1,3 +1,4 @@
"""Tests for certbot.cert_manager."""
# pylint: disable=protected-access
import os
@@ -18,58 +19,50 @@ from certbot.storage import ALL_FOUR
from certbot.tests import storage_test
from certbot.tests import util as test_util
from certbot.tests.util import TempDirTestCase
class BaseCertManagerTest(TempDirTestCase):
class BaseCertManagerTest(test_util.ConfigTestCase):
"""Base class for setting up Cert Manager tests.
"""
def setUp(self):
super(BaseCertManagerTest, self).setUp()
os.makedirs(os.path.join(self.tempdir, "renewal"))
self.cli_config = configuration.NamespaceConfig(mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
logs_dir=self.tempdir,
quiet=False,
))
self.config.quiet = False
os.makedirs(self.config.renewal_configs_dir)
self.domains = {
"example.org": None,
"other.com": os.path.join(self.tempdir, "specialarchive")
"other.com": os.path.join(self.config.config_dir, "specialarchive")
}
self.configs = dict((domain, self._set_up_config(domain, self.domains[domain]))
self.config_files = dict((domain, self._set_up_config(domain, self.domains[domain]))
for domain in self.domains)
# We also create a file that isn't a renewal config in the same
# location to test that logic that reads in all-and-only renewal
# configs will ignore it and NOT attempt to parse it.
junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w")
junk = open(os.path.join(self.config.renewal_configs_dir, "IGNORE.THIS"), "w")
junk.write("This file should be ignored!")
junk.close()
def _set_up_config(self, domain, custom_archive):
# TODO: maybe provide NamespaceConfig.make_dirs?
# TODO: main() should create those dirs, c.f. #902
os.makedirs(os.path.join(self.tempdir, "live", domain))
config = configobj.ConfigObj()
os.makedirs(os.path.join(self.config.live_dir, domain))
config_file = configobj.ConfigObj()
if custom_archive is not None:
os.makedirs(custom_archive)
config["archive_dir"] = custom_archive
config_file["archive_dir"] = custom_archive
else:
os.makedirs(os.path.join(self.tempdir, "archive", domain))
os.makedirs(os.path.join(self.config.default_archive_dir, domain))
for kind in ALL_FOUR:
config[kind] = os.path.join(self.tempdir, "live", domain,
config_file[kind] = os.path.join(self.config.live_dir, domain,
kind + ".pem")
config.filename = os.path.join(self.tempdir, "renewal",
config_file.filename = os.path.join(self.config.renewal_configs_dir,
domain + ".conf")
config.write()
return config
config_file.write()
return config_file
class UpdateLiveSymlinksTest(BaseCertManagerTest):
@@ -86,27 +79,27 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest):
if custom_archive is not None:
archive_dir_path = custom_archive
else:
archive_dir_path = os.path.join(self.tempdir, "archive", domain)
archive_dir_path = os.path.join(self.config.default_archive_dir, domain)
archive_paths[domain] = dict((kind,
os.path.join(archive_dir_path, kind + "1.pem")) for kind in ALL_FOUR)
for kind in ALL_FOUR:
live_path = self.configs[domain][kind]
live_path = self.config_files[domain][kind]
archive_path = archive_paths[domain][kind]
open(archive_path, 'a').close()
# path is incorrect but base must be correct
os.symlink(os.path.join(self.tempdir, kind + "1.pem"), live_path)
os.symlink(os.path.join(self.config.config_dir, kind + "1.pem"), live_path)
# run update symlinks
cert_manager.update_live_symlinks(self.cli_config)
cert_manager.update_live_symlinks(self.config)
# check that symlinks go where they should
prev_dir = os.getcwd()
try:
for domain in self.domains:
for kind in ALL_FOUR:
os.chdir(os.path.dirname(self.configs[domain][kind]))
os.chdir(os.path.dirname(self.config_files[domain][kind]))
self.assertEqual(
os.path.realpath(os.readlink(self.configs[domain][kind])),
os.path.realpath(os.readlink(self.config_files[domain][kind])),
os.path.realpath(archive_paths[domain][kind]))
finally:
os.chdir(prev_dir)
@@ -115,16 +108,45 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest):
class DeleteTest(storage_test.BaseRenewableCertTest):
"""Tests for certbot.cert_manager.delete
"""
def _call(self):
from certbot import cert_manager
cert_manager.delete(self.config)
@test_util.patch_get_utility()
@mock.patch('certbot.cert_manager.lineage_for_certname')
@mock.patch('certbot.storage.delete_files')
def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility):
def test_delete_from_config(self, mock_delete_files, mock_lineage_for_certname,
unused_get_utility):
"""Test delete"""
mock_lineage_for_certname.return_value = self.test_rc
self.cli_config.certname = "example.org"
from certbot import cert_manager
cert_manager.delete(self.cli_config)
self.assertTrue(mock_delete_files.called)
self.config.certname = "example.org"
self._call()
mock_delete_files.assert_called_once_with(self.config, "example.org")
@test_util.patch_get_utility()
@mock.patch('certbot.cert_manager.lineage_for_certname')
@mock.patch('certbot.storage.delete_files')
def test_delete_interactive_single(self, mock_delete_files, mock_lineage_for_certname,
mock_util):
"""Test delete"""
mock_lineage_for_certname.return_value = self.test_rc
mock_util().checklist.return_value = (display_util.OK, ["example.org"])
self._call()
mock_delete_files.assert_called_once_with(self.config, "example.org")
@test_util.patch_get_utility()
@mock.patch('certbot.cert_manager.lineage_for_certname')
@mock.patch('certbot.storage.delete_files')
def test_delete_interactive_multiple(self, mock_delete_files, mock_lineage_for_certname,
mock_util):
"""Test delete"""
mock_lineage_for_certname.return_value = self.test_rc
mock_util().checklist.return_value = (display_util.OK, ["example.org", "other.org"])
self._call()
mock_delete_files.assert_any_call(self.config, "example.org")
mock_delete_files.assert_any_call(self.config, "other.org")
self.assertEqual(mock_delete_files.call_count, 2)
class CertificatesTest(BaseCertManagerTest):
@@ -137,15 +159,15 @@ class CertificatesTest(BaseCertManagerTest):
@mock.patch('certbot.cert_manager.logger')
@test_util.patch_get_utility()
def test_certificates_parse_fail(self, mock_utility, mock_logger):
self._certificates(self.cli_config)
self._certificates(self.config)
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
self.assertTrue(mock_utility.called)
@mock.patch('certbot.cert_manager.logger')
@test_util.patch_get_utility()
def test_certificates_quiet(self, mock_utility, mock_logger):
self.cli_config.quiet = True
self._certificates(self.cli_config)
self.config.quiet = True
self._certificates(self.config)
self.assertFalse(mock_utility.notification.called)
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
@@ -158,7 +180,7 @@ class CertificatesTest(BaseCertManagerTest):
mock_utility, mock_logger, mock_verifier):
mock_verifier.return_value = None
mock_report.return_value = ""
self._certificates(self.cli_config)
self._certificates(self.config)
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
self.assertTrue(mock_report.called)
self.assertTrue(mock_utility.called)
@@ -167,20 +189,19 @@ class CertificatesTest(BaseCertManagerTest):
@mock.patch('certbot.cert_manager.logger')
@test_util.patch_get_utility()
def test_certificates_no_files(self, mock_utility, mock_logger):
tempdir = tempfile.mkdtemp()
cli_config = configuration.NamespaceConfig(mock.MagicMock(
config_dir=tempdir,
work_dir=tempdir,
logs_dir=tempdir,
quiet=False,
empty_tempdir = tempfile.mkdtemp()
empty_config = configuration.NamespaceConfig(mock.MagicMock(
config_dir=os.path.join(empty_tempdir, "config"),
work_dir=os.path.join(empty_tempdir, "work"),
logs_dir=os.path.join(empty_tempdir, "logs"),
quiet=False
))
os.makedirs(os.path.join(tempdir, "renewal"))
self._certificates(cli_config)
os.makedirs(empty_config.renewal_configs_dir)
self._certificates(empty_config)
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
self.assertTrue(mock_utility.called)
shutil.rmtree(tempdir)
shutil.rmtree(empty_tempdir)
@mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked')
def test_report_human_readable(self, mock_revoked):
@@ -261,7 +282,7 @@ class SearchLineagesTest(BaseCertManagerTest):
mock_renewable_cert.side_effect = errors.CertStorageError
from certbot import cert_manager
# pylint: disable=protected-access
self.assertEqual(cert_manager._search_lineages(self.cli_config, lambda x: x, "check"),
self.assertEqual(cert_manager._search_lineages(self.config, lambda x: x, "check"),
"check")
self.assertTrue(mock_make_or_verify_dir.called)
@@ -278,7 +299,7 @@ class LineageForCertnameTest(BaseCertManagerTest):
mock_match = mock.Mock(lineagename="example.com")
mock_renewable_cert.return_value = mock_match
from certbot import cert_manager
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
self.assertEqual(cert_manager.lineage_for_certname(self.config, "example.com"),
mock_match)
self.assertTrue(mock_make_or_verify_dir.called)
@@ -288,7 +309,7 @@ class LineageForCertnameTest(BaseCertManagerTest):
mock_make_or_verify_dir):
mock_renewal_conf_file.return_value = "other.com.conf"
from certbot import cert_manager
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
self.assertEqual(cert_manager.lineage_for_certname(self.config, "example.com"),
None)
self.assertTrue(mock_make_or_verify_dir.called)
@@ -298,7 +319,7 @@ class LineageForCertnameTest(BaseCertManagerTest):
mock_make_or_verify_dir):
mock_renewal_conf_file.side_effect = errors.CertStorageError()
from certbot import cert_manager
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
self.assertEqual(cert_manager.lineage_for_certname(self.config, "example.com"),
None)
self.assertTrue(mock_make_or_verify_dir.called)
@@ -317,7 +338,7 @@ class DomainsForCertnameTest(BaseCertManagerTest):
mock_match.names.return_value = domains
mock_renewable_cert.return_value = mock_match
from certbot import cert_manager
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "example.com"),
self.assertEqual(cert_manager.domains_for_certname(self.config, "example.com"),
domains)
self.assertTrue(mock_make_or_verify_dir.called)
@@ -327,7 +348,7 @@ class DomainsForCertnameTest(BaseCertManagerTest):
mock_make_or_verify_dir):
mock_renewal_conf_file.return_value = "somefile.conf"
from certbot import cert_manager
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"),
self.assertEqual(cert_manager.domains_for_certname(self.config, "other.com"),
None)
self.assertTrue(mock_make_or_verify_dir.called)
@@ -337,15 +358,8 @@ class RenameLineageTest(BaseCertManagerTest):
def setUp(self):
super(RenameLineageTest, self).setUp()
self.mock_config = configuration.NamespaceConfig(
namespace=mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
logs_dir=self.tempdir,
certname="example.org",
new_certname="after",
)
)
self.config.certname = "example.org"
self.config.new_certname = "after"
def _call(self, *args, **kwargs):
from certbot import cert_manager
@@ -354,81 +368,76 @@ class RenameLineageTest(BaseCertManagerTest):
@mock.patch('certbot.storage.renewal_conf_files')
@test_util.patch_get_utility()
def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
mock_config = mock.Mock(certname=None, new_certname="two")
self.config.certname = None
self.config.new_certname = "two"
# if not choices
mock_renewal_conf_files.return_value = []
self.assertRaises(errors.Error, self._call, mock_config)
self.assertRaises(errors.Error, self._call, self.config)
mock_renewal_conf_files.return_value = ["one.conf"]
util_mock = mock.Mock()
util_mock = mock_get_utility()
util_mock.menu.return_value = (display_util.CANCEL, 0)
mock_get_utility.return_value = util_mock
self.assertRaises(errors.Error, self._call, mock_config)
self.assertRaises(errors.Error, self._call, self.config)
util_mock.menu.return_value = (display_util.OK, -1)
self.assertRaises(errors.Error, self._call, mock_config)
self.assertRaises(errors.Error, self._call, self.config)
@test_util.patch_get_utility()
def test_no_new_certname(self, mock_get_utility):
mock_config = mock.Mock(certname="one", new_certname=None)
self.config.certname = "one"
self.config.new_certname = None
util_mock = mock.Mock()
util_mock = mock_get_utility()
util_mock.input.return_value = (display_util.CANCEL, "name")
mock_get_utility.return_value = util_mock
self.assertRaises(errors.Error, self._call, mock_config)
self.assertRaises(errors.Error, self._call, self.config)
util_mock = mock.Mock()
util_mock.input.return_value = (display_util.OK, None)
mock_get_utility.return_value = util_mock
self.assertRaises(errors.Error, self._call, mock_config)
self.assertRaises(errors.Error, self._call, self.config)
@test_util.patch_get_utility()
@mock.patch('certbot.cert_manager.lineage_for_certname')
def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility):
mock_config = mock.Mock(certname="one", new_certname="two")
self.config.certname = "one"
self.config.new_certname = "two"
mock_lineage_for_certname.return_value = None
self.assertRaises(errors.ConfigurationError,
self._call, mock_config)
self._call, self.config)
@test_util.patch_get_utility()
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
def test_rename_cert(self, mock_check, unused_get_utility):
mock_check.return_value = True
mock_config = self.mock_config
self._call(mock_config)
self._call(self.config)
from certbot import cert_manager
updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
updated_lineage = cert_manager.lineage_for_certname(self.config, self.config.new_certname)
self.assertTrue(updated_lineage is not None)
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
self.assertEqual(updated_lineage.lineagename, self.config.new_certname)
@test_util.patch_get_utility()
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility):
mock_check.return_value = True
mock_config = self.mock_config
mock_config.certname = None
util_mock = mock.Mock()
self.config.certname = None
util_mock = mock_get_utility()
util_mock.menu.return_value = (display_util.OK, 0)
mock_get_utility.return_value = util_mock
self._call(mock_config)
self._call(self.config)
from certbot import cert_manager
updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname)
updated_lineage = cert_manager.lineage_for_certname(self.config, self.config.new_certname)
self.assertTrue(updated_lineage is not None)
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
self.assertEqual(updated_lineage.lineagename, self.config.new_certname)
@test_util.patch_get_utility()
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility):
mock_check.return_value = True
mock_config = self.mock_config
# for example, don't rename to existing certname
mock_config.new_certname = "example.org"
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
self.config.new_certname = "example.org"
self.assertRaises(errors.ConfigurationError, self._call, self.config)
mock_config.new_certname = "one{0}two".format(os.path.sep)
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
self.config.new_certname = "one{0}two".format(os.path.sep)
self.assertRaises(errors.ConfigurationError, self._call, self.config)
class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
@@ -436,36 +445,36 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
def setUp(self):
super(DuplicativeCertsTest, self).setUp()
self.config.write()
self.config_file.write()
self._write_out_ex_kinds()
@mock.patch('certbot.util.make_or_verify_dir')
def test_find_duplicative_names(self, unused_makedir):
from certbot.cert_manager import find_duplicative_certs
test_cert = test_util.load_vector('cert-san.pem')
test_cert = test_util.load_vector('cert-san_512.pem')
with open(self.test_rc.cert, 'wb') as f:
f.write(test_cert)
# No overlap at all
result = find_duplicative_certs(
self.cli_config, ['wow.net', 'hooray.org'])
self.config, ['wow.net', 'hooray.org'])
self.assertEqual(result, (None, None))
# Totally identical
result = find_duplicative_certs(
self.cli_config, ['example.com', 'www.example.com'])
self.config, ['example.com', 'www.example.com'])
self.assertTrue(result[0].configfile.filename.endswith('example.org.conf'))
self.assertEqual(result[1], None)
# Superset
result = find_duplicative_certs(
self.cli_config, ['example.com', 'www.example.com', 'something.new'])
self.config, ['example.com', 'www.example.com', 'something.new'])
self.assertEqual(result[0], None)
self.assertTrue(result[1].configfile.filename.endswith('example.org.conf'))
# Partial overlap doesn't count
result = find_duplicative_certs(
self.cli_config, ['example.com', 'something.new'])
self.config, ['example.com', 'something.new'])
self.assertEqual(result, (None, None))

View File

@@ -3,6 +3,7 @@ import argparse
import unittest
import os
import tempfile
import copy
import mock
import six
@@ -15,6 +16,8 @@ from certbot import constants
from certbot import errors
from certbot.plugins import disco
import certbot.tests.util as test_util
from certbot.tests.util import TempDirTestCase
PLUGINS = disco.PluginsRegistry.find_all()
@@ -40,7 +43,7 @@ class TestReadFile(TempDirTestCase):
class ParseTest(unittest.TestCase):
class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods
'''Test the cli args entrypoint'''
_multiprocess_can_split_ = True
@@ -49,24 +52,41 @@ class ParseTest(unittest.TestCase):
reload_module(cli)
@staticmethod
def parse(*args, **kwargs):
def _unmocked_parse(*args, **kwargs):
"""Get result of cli.prepare_and_parse_args."""
return cli.prepare_and_parse_args(PLUGINS, *args, **kwargs)
@staticmethod
def parse(*args, **kwargs):
"""Mocks zope.component.getUtility and calls _unmocked_parse."""
with test_util.patch_get_utility():
return ParseTest._unmocked_parse(*args, **kwargs)
def _help_output(self, args):
"Run a command, and return the output string for scrutiny"
output = six.StringIO()
def write_msg(message, *args, **kwargs): # pylint: disable=missing-docstring,unused-argument
output.write(message)
with mock.patch('certbot.main.sys.stdout', new=output):
with mock.patch('certbot.main.sys.stderr'):
self.assertRaises(SystemExit, self.parse, args, output)
with test_util.patch_get_utility() as mock_get_utility:
mock_get_utility().notification.side_effect = write_msg
with mock.patch('certbot.main.sys.stderr'):
self.assertRaises(SystemExit, self._unmocked_parse, args, output)
return output.getvalue()
@mock.patch("certbot.cli.flag_default")
def test_cli_ini_domains(self, mock_flag_default):
tmp_config = tempfile.NamedTemporaryFile()
# use a shim to get ConfigArgParse to pick up tmp_config
shim = lambda v: constants.CLI_DEFAULTS[v] if v != "config_files" else [tmp_config.name]
shim = (
lambda v: copy.deepcopy(constants.CLI_DEFAULTS[v])
if v != "config_files"
else [tmp_config.name]
)
mock_flag_default.side_effect = shim
namespace = self.parse(["certonly"])
@@ -109,6 +129,7 @@ class ParseTest(unittest.TestCase):
self.assertTrue("--dialog" not in out)
self.assertTrue("%s" not in out)
self.assertTrue("{0}" not in out)
self.assertTrue("--renew-hook" not in out)
out = self._help_output(['-h', 'nginx'])
if "nginx" in PLUGINS:
@@ -323,6 +344,70 @@ class ParseTest(unittest.TestCase):
self.assertRaises(
errors.Error, self.parse, "-n --force-interactive".split())
def test_deploy_hook_conflict(self):
with mock.patch("certbot.cli.sys.stderr"):
self.assertRaises(SystemExit, self.parse,
"--renew-hook foo --deploy-hook bar".split())
def test_deploy_hook_matches_renew_hook(self):
value = "foo"
namespace = self.parse(["--renew-hook", value,
"--deploy-hook", value,
"--disable-hook-validation"])
self.assertEqual(namespace.deploy_hook, value)
self.assertEqual(namespace.renew_hook, value)
def test_deploy_hook_sets_renew_hook(self):
value = "foo"
namespace = self.parse(
["--deploy-hook", value, "--disable-hook-validation"])
self.assertEqual(namespace.deploy_hook, value)
self.assertEqual(namespace.renew_hook, value)
def test_renew_hook_conflict(self):
with mock.patch("certbot.cli.sys.stderr"):
self.assertRaises(SystemExit, self.parse,
"--deploy-hook foo --renew-hook bar".split())
def test_renew_hook_matches_deploy_hook(self):
value = "foo"
namespace = self.parse(["--deploy-hook", value,
"--renew-hook", value,
"--disable-hook-validation"])
self.assertEqual(namespace.deploy_hook, value)
self.assertEqual(namespace.renew_hook, value)
def test_renew_hook_does_not_set_renew_hook(self):
value = "foo"
namespace = self.parse(
["--renew-hook", value, "--disable-hook-validation"])
self.assertEqual(namespace.deploy_hook, None)
self.assertEqual(namespace.renew_hook, value)
def test_max_log_backups_error(self):
with mock.patch('certbot.cli.sys.stderr'):
self.assertRaises(
SystemExit, self.parse, "--max-log-backups foo".split())
self.assertRaises(
SystemExit, self.parse, "--max-log-backups -42".split())
def test_max_log_backups_success(self):
value = "42"
namespace = self.parse(["--max-log-backups", value])
self.assertEqual(namespace.max_log_backups, int(value))
def test_unchanging_defaults(self):
namespace = self.parse([])
self.assertEqual(namespace.domains, [])
self.assertEqual(namespace.pref_challs, [])
namespace.pref_challs = [challenges.HTTP01.typ]
namespace.domains = ['example.com']
namespace = self.parse([])
self.assertEqual(namespace.domains, [])
self.assertEqual(namespace.pref_challs, [])
class DefaultTest(unittest.TestCase):
"""Tests for certbot.cli._Default."""
@@ -397,9 +482,10 @@ class SetByCliTest(unittest.TestCase):
def _call_set_by_cli(var, args, verb):
with mock.patch('certbot.cli.helpful_parser') as mock_parser:
mock_parser.args = args
mock_parser.verb = verb
return cli.set_by_cli(var)
with test_util.patch_get_utility():
mock_parser.args = args
mock_parser.verb = verb
return cli.set_by_cli(var)
if __name__ == '__main__':

View File

@@ -18,23 +18,17 @@ import certbot.tests.util as test_util
KEY = test_util.load_vector("rsa512_key.pem")
CSR_SAN = test_util.load_vector("csr-san.pem")
CSR_SAN = test_util.load_vector("csr-san_512.pem")
class ConfigHelper(object):
"""Creates a dummy object to imitate a namespace object
Example: cfg = ConfigHelper(redirect=True, hsts=False, uir=False)
will result in: cfg.redirect=True, cfg.hsts=False, etc.
"""
def __init__(self, **kwds):
self.__dict__.update(kwds)
class RegisterTest(unittest.TestCase):
class RegisterTest(test_util.ConfigTestCase):
"""Tests for certbot.client.register."""
def setUp(self):
self.config = mock.MagicMock(rsa_key_size=1024, register_unsafely_without_email=False)
super(RegisterTest, self).setUp()
self.config.rsa_key_size = 1024
self.config.register_unsafely_without_email = False
self.config.email = "alias@example.com"
self.account_storage = account.AccountMemoryStorage()
self.tos_cb = mock.MagicMock()
@@ -82,6 +76,7 @@ class RegisterTest(unittest.TestCase):
@mock.patch("certbot.account.report_new_account")
def test_email_invalid_noninteractive(self, _rep):
from acme import messages
self.config.noninteractive_mode = True
msg = "DNS problem: NXDOMAIN looking up MX for example.com"
mx_err = messages.Error.with_code('invalidContact', detail=msg)
with mock.patch("certbot.client.acme_client.Client") as mock_client:
@@ -116,14 +111,12 @@ class RegisterTest(unittest.TestCase):
self.assertFalse(mock_handle.called)
class ClientTestCommon(unittest.TestCase):
class ClientTestCommon(test_util.ConfigTestCase):
"""Common base class for certbot.client.Client tests."""
def setUp(self):
self.config = mock.MagicMock(
no_verify_ssl=False,
config_dir="/etc/letsencrypt",
work_dir="/var/lib/letsencrypt",
allow_subset_of_names=False)
super(ClientTestCommon, self).setUp()
self.config.no_verify_ssl = False
self.config.allow_subset_of_names = False
# pylint: disable=star-args
self.account = mock.MagicMock(**{"key.pem": KEY})
@@ -143,7 +136,7 @@ class ClientTest(ClientTestCommon):
super(ClientTest, self).setUp()
self.config.allow_subset_of_names = False
self.config.config_dir = "/etc/letsencrypt"
self.config.dry_run = False
self.eg_domains = ["example.com", "www.example.com"]
def test_init_acme_verify_ssl(self):
@@ -241,15 +234,37 @@ class ClientTest(ClientTestCommon):
self.assertEqual(1, mock_get_utility().notification.call_count)
@mock.patch("certbot.client.crypto_util")
@test_util.patch_get_utility()
def test_obtain_certificate(self, unused_mock_get_utility,
mock_crypto_util):
self._mock_obtain_certificate()
def test_obtain_certificate(self, mock_crypto_util):
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
mock_crypto_util.init_save_csr.return_value = csr
mock_crypto_util.init_save_key.return_value = mock.sentinel.key
domains = ["example.com", "www.example.com"]
self._test_obtain_certificate_common(mock.sentinel.key, csr)
mock_crypto_util.init_save_key.assert_called_once_with(
self.config.rsa_key_size, self.config.key_dir)
mock_crypto_util.init_save_csr.assert_called_once_with(
mock.sentinel.key, self.eg_domains, self.config.csr_dir)
@mock.patch("certbot.client.crypto_util")
@mock.patch("certbot.client.acme_crypto_util")
def test_obtain_certificate_dry_run(self, mock_acme_crypto, mock_crypto):
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
mock_acme_crypto.make_csr.return_value = CSR_SAN
mock_crypto.make_key.return_value = mock.sentinel.key_pem
key = util.Key(file=None, pem=mock.sentinel.key_pem)
self.client.config.dry_run = True
self._test_obtain_certificate_common(key, csr)
mock_crypto.make_key.assert_called_once_with(self.config.rsa_key_size)
mock_acme_crypto.make_csr.assert_called_once_with(
mock.sentinel.key_pem, self.eg_domains, self.config.must_staple)
mock_crypto.init_save_key.assert_not_called()
mock_crypto.init_save_csr.assert_not_called()
def _test_obtain_certificate_common(self, key, csr):
self._mock_obtain_certificate()
# return_value is essentially set to (None, None) in
# _mock_obtain_certificate(), which breaks this test.
@@ -258,7 +273,7 @@ class ClientTest(ClientTestCommon):
authzr = []
# domain ordering should not be affected by authorization order
for domain in reversed(domains):
for domain in reversed(self.eg_domains):
authzr.append(
mock.MagicMock(
body=mock.MagicMock(
@@ -267,14 +282,12 @@ class ClientTest(ClientTestCommon):
self.client.auth_handler.get_authorizations.return_value = authzr
self.assertEqual(
self.client.obtain_certificate(domains),
(mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr))
with test_util.patch_get_utility():
result = self.client.obtain_certificate(self.eg_domains)
mock_crypto_util.init_save_key.assert_called_once_with(
self.config.rsa_key_size, self.config.key_dir)
mock_crypto_util.init_save_csr.assert_called_once_with(
mock.sentinel.key, domains, self.config.csr_dir)
self.assertEqual(
result,
(mock.sentinel.certr, mock.sentinel.chain, key, csr))
self._check_obtain_certificate()
@mock.patch('certbot.client.Client.obtain_certificate')
@@ -301,14 +314,14 @@ class ClientTest(ClientTestCommon):
@mock.patch("certbot.cli.helpful_parser")
def test_save_certificate(self, mock_parser):
# pylint: disable=too-many-locals
certs = ["matching_cert.pem", "cert.pem", "cert-san.pem"]
certs = ["cert_512.pem", "cert-san_512.pem"]
tmp_path = tempfile.mkdtemp()
os.chmod(tmp_path, 0o755) # TODO: really??
certr = mock.MagicMock(body=test_util.load_comparable_cert(certs[0]))
chain_cert = [test_util.load_comparable_cert(certs[1]),
test_util.load_comparable_cert(certs[2])]
candidate_cert_path = os.path.join(tmp_path, "certs", "cert.pem")
chain_cert = [test_util.load_comparable_cert(certs[0]),
test_util.load_comparable_cert(certs[1])]
candidate_cert_path = os.path.join(tmp_path, "certs", "cert_512.pem")
candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem")
candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem")
mock_parser.verb = "certonly"
@@ -333,8 +346,8 @@ class ClientTest(ClientTestCommon):
with open(chain_path, "rb") as chain_file:
chain_contents = chain_file.read()
self.assertEqual(chain_contents, test_util.load_vector(certs[1]) +
test_util.load_vector(certs[2]))
self.assertEqual(chain_contents, test_util.load_vector(certs[0]) +
test_util.load_vector(certs[1]))
shutil.rmtree(tmp_path)

View File

@@ -6,33 +6,32 @@ import mock
from certbot import errors
from certbot.tests import util as test_util
class NamespaceConfigTest(unittest.TestCase):
class NamespaceConfigTest(test_util.ConfigTestCase):
"""Tests for certbot.configuration.NamespaceConfig."""
def setUp(self):
self.namespace = mock.MagicMock(
config_dir='/tmp/config', work_dir='/tmp/foo',
logs_dir="/tmp/bar", foo='bar',
server='https://acme-server.org:443/new',
tls_sni_01_port=1234, http01_port=4321)
from certbot.configuration import NamespaceConfig
self.config = NamespaceConfig(self.namespace)
super(NamespaceConfigTest, self).setUp()
self.config.foo = 'bar'
self.config.server = 'https://acme-server.org:443/new'
self.config.tls_sni_01_port = 1234
self.config.http01_port = 4321
def test_init_same_ports(self):
self.namespace.tls_sni_01_port = 4321
self.config.namespace.tls_sni_01_port = 4321
from certbot.configuration import NamespaceConfig
self.assertRaises(errors.Error, NamespaceConfig, self.namespace)
self.assertRaises(errors.Error, NamespaceConfig, self.config.namespace)
def test_proxy_getattr(self):
self.assertEqual(self.config.foo, 'bar')
self.assertEqual(self.config.work_dir, '/tmp/foo')
self.assertEqual(self.config.work_dir, os.path.join(self.tempdir, 'work'))
def test_server_path(self):
self.assertEqual(['acme-server.org:443', 'new'],
self.config.server_path.split(os.path.sep))
self.namespace.server = ('http://user:pass@acme.server:443'
self.config.namespace.server = ('http://user:pass@acme.server:443'
'/p/a/t/h;parameters?query#fragment')
self.assertEqual(['user:pass@acme.server:443', 'p', 'a', 't', 'h'],
self.config.server_path.split(os.path.sep))
@@ -48,12 +47,18 @@ class NamespaceConfigTest(unittest.TestCase):
constants.TEMP_CHECKPOINT_DIR = 't'
self.assertEqual(
self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new')
self.assertEqual(self.config.backup_dir, '/tmp/foo/backups')
self.assertEqual(self.config.csr_dir, '/tmp/config/csr')
self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p')
self.assertEqual(self.config.key_dir, '/tmp/config/keys')
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
self.config.accounts_dir, os.path.join(
self.config.config_dir, 'acc/acme-server.org:443/new'))
self.assertEqual(
self.config.backup_dir, os.path.join(self.config.work_dir, 'backups'))
self.assertEqual(
self.config.csr_dir, os.path.join(self.config.config_dir, 'csr'))
self.assertEqual(
self.config.in_progress_dir, os.path.join(self.config.work_dir, '../p'))
self.assertEqual(
self.config.key_dir, os.path.join(self.config.config_dir, 'keys'))
self.assertEqual(
self.config.temp_checkpoint_dir, os.path.join(self.config.work_dir, 't'))
def test_absolute_paths(self):
from certbot.configuration import NamespaceConfig
@@ -95,10 +100,13 @@ class NamespaceConfigTest(unittest.TestCase):
constants.LIVE_DIR = 'l'
constants.RENEWAL_CONFIGS_DIR = 'renewal_configs'
self.assertEqual(self.config.default_archive_dir, '/tmp/config/a')
self.assertEqual(self.config.live_dir, '/tmp/config/l')
self.assertEqual(
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
self.config.default_archive_dir, os.path.join(self.config.config_dir, 'a'))
self.assertEqual(
self.config.live_dir, os.path.join(self.config.config_dir, 'l'))
self.assertEqual(
self.config.renewal_configs_dir, os.path.join(
self.config.config_dir, 'renewal_configs'))
def test_renewal_absolute_paths(self):
from certbot.configuration import NamespaceConfig

View File

@@ -17,11 +17,10 @@ RSA256_KEY = test_util.load_vector('rsa256_key.pem')
RSA256_KEY_PATH = test_util.vector_path('rsa256_key.pem')
RSA512_KEY = test_util.load_vector('rsa512_key.pem')
RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem')
CERT_PATH = test_util.vector_path('cert.pem')
CERT = test_util.load_vector('cert.pem')
SAN_CERT = test_util.load_vector('cert-san.pem')
SS_CERT_PATH = test_util.vector_path('self_signed_cert.pem')
SS_CERT = test_util.load_vector('self_signed_cert.pem')
CERT_PATH = test_util.vector_path('cert_512.pem')
CERT = test_util.load_vector('cert_512.pem')
SS_CERT_PATH = test_util.vector_path('cert_2048.pem')
SS_CERT = test_util.load_vector('cert_2048.pem')
class InitSaveKeyTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.init_save_key."""
@@ -30,8 +29,7 @@ class InitSaveKeyTest(test_util.TempDirTestCase):
logging.disable(logging.CRITICAL)
zope.component.provideUtility(
mock.Mock(strict_permissions=True, dry_run=False),
interfaces.IConfig)
mock.Mock(strict_permissions=True), interfaces.IConfig)
def tearDown(self):
super(InitSaveKeyTest, self).tearDown()
@@ -51,16 +49,6 @@ class InitSaveKeyTest(test_util.TempDirTestCase):
self.assertTrue('key-certbot.pem' in key.file)
self.assertTrue(os.path.exists(os.path.join(self.tempdir, key.file)))
@mock.patch('certbot.crypto_util.make_key')
def test_success_dry_run(self, mock_make):
zope.component.provideUtility(
mock.Mock(strict_permissions=True, dry_run=True),
interfaces.IConfig)
mock_make.return_value = b'key_pem'
key = self._call(1024, self.tempdir)
self.assertEqual(key.pem, b'key_pem')
self.assertTrue(key.file is None)
@mock.patch('certbot.crypto_util.make_key')
def test_key_failure(self, mock_make):
mock_make.side_effect = ValueError
@@ -74,12 +62,11 @@ class InitSaveCSRTest(test_util.TempDirTestCase):
super(InitSaveCSRTest, self).setUp()
zope.component.provideUtility(
mock.Mock(strict_permissions=True, dry_run=False),
interfaces.IConfig)
mock.Mock(strict_permissions=True), interfaces.IConfig)
@mock.patch('acme.crypto_util.make_csr')
@mock.patch('certbot.crypto_util.util.make_or_verify_dir')
def test_success(self, unused_mock_verify, mock_csr):
def test_it(self, unused_mock_verify, mock_csr):
from certbot.crypto_util import init_save_csr
mock_csr.return_value = b'csr_pem'
@@ -90,22 +77,6 @@ class InitSaveCSRTest(test_util.TempDirTestCase):
self.assertEqual(csr.data, b'csr_pem')
self.assertTrue('csr-certbot.pem' in csr.file)
@mock.patch('acme.crypto_util.make_csr')
@mock.patch('certbot.crypto_util.util.make_or_verify_dir')
def test_success_dry_run(self, unused_mock_verify, mock_csr):
from certbot.crypto_util import init_save_csr
zope.component.provideUtility(
mock.Mock(strict_permissions=True, dry_run=True),
interfaces.IConfig)
mock_csr.return_value = b'csr_pem'
csr = init_save_csr(
mock.Mock(pem='dummy_key'), 'example.com', self.tempdir)
self.assertEqual(csr.data, b'csr_pem')
self.assertTrue(csr.file is None)
class ValidCSRTest(unittest.TestCase):
"""Tests for certbot.crypto_util.valid_csr."""
@@ -116,13 +87,13 @@ class ValidCSRTest(unittest.TestCase):
return valid_csr(csr)
def test_valid_pem_true(self):
self.assertTrue(self._call(test_util.load_vector('csr.pem')))
self.assertTrue(self._call(test_util.load_vector('csr_512.pem')))
def test_valid_pem_san_true(self):
self.assertTrue(self._call(test_util.load_vector('csr-san.pem')))
self.assertTrue(self._call(test_util.load_vector('csr-san_512.pem')))
def test_valid_der_false(self):
self.assertFalse(self._call(test_util.load_vector('csr.der')))
self.assertFalse(self._call(test_util.load_vector('csr_512.der')))
def test_empty_false(self):
self.assertFalse(self._call(''))
@@ -141,11 +112,11 @@ class CSRMatchesPubkeyTest(unittest.TestCase):
def test_valid_true(self):
self.assertTrue(self._call(
test_util.load_vector('csr.pem'), RSA512_KEY))
test_util.load_vector('csr_512.pem'), RSA512_KEY))
def test_invalid_false(self):
self.assertFalse(self._call(
test_util.load_vector('csr.pem'), RSA256_KEY))
test_util.load_vector('csr_512.pem'), RSA256_KEY))
class ImportCSRFileTest(unittest.TestCase):
@@ -157,9 +128,9 @@ class ImportCSRFileTest(unittest.TestCase):
return import_csr_file(*args, **kwargs)
def test_der_csr(self):
csrfile = test_util.vector_path('csr.der')
data = test_util.load_vector('csr.der')
data_pem = test_util.load_vector('csr.pem')
csrfile = test_util.vector_path('csr_512.der')
data = test_util.load_vector('csr_512.der')
data_pem = test_util.load_vector('csr_512.pem')
self.assertEqual(
(OpenSSL.crypto.FILETYPE_PEM,
@@ -170,8 +141,8 @@ class ImportCSRFileTest(unittest.TestCase):
self._call(csrfile, data))
def test_pem_csr(self):
csrfile = test_util.vector_path('csr.pem')
data = test_util.load_vector('csr.pem')
csrfile = test_util.vector_path('csr_512.pem')
data = test_util.load_vector('csr_512.pem')
self.assertEqual(
(OpenSSL.crypto.FILETYPE_PEM,
@@ -183,8 +154,8 @@ class ImportCSRFileTest(unittest.TestCase):
def test_bad_csr(self):
self.assertRaises(errors.Error, self._call,
test_util.vector_path('cert.pem'),
test_util.load_vector('cert.pem'))
test_util.vector_path('cert_512.pem'),
test_util.load_vector('cert_512.pem'))
class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods
@@ -207,7 +178,7 @@ class VerifyCertSetup(unittest.TestCase):
self.renewable_cert.cert = SS_CERT_PATH
self.renewable_cert.chain = SS_CERT_PATH
self.renewable_cert.privkey = RSA2048_KEY_PATH
self.renewable_cert.fullchain = test_util.vector_path('self_signed_fullchain.pem')
self.renewable_cert.fullchain = test_util.vector_path('cert_fullchain_2048.pem')
self.bad_renewable_cert = mock.MagicMock()
self.bad_renewable_cert.chain = SS_CERT_PATH
@@ -247,7 +218,7 @@ class VerifyRenewableCertSigTest(VerifyCertSetup):
self.assertEqual(None, self._call(self.renewable_cert))
def test_cert_sig_mismatch(self):
self.bad_renewable_cert.cert = test_util.vector_path('self_signed_cert_bad.pem')
self.bad_renewable_cert.cert = test_util.vector_path('cert_512_bad.pem')
self.assertRaises(errors.Error, self._call, self.bad_renewable_cert)
@@ -280,9 +251,11 @@ class VerifyCertMatchesPrivKeyTest(VerifyCertSetup):
def _call(self, renewable_cert):
from certbot.crypto_util import verify_cert_matches_priv_key
return verify_cert_matches_priv_key(renewable_cert)
return verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey)
def test_cert_priv_key_match(self):
self.renewable_cert.cert = SS_CERT_PATH
self.renewable_cert.privkey = RSA2048_KEY_PATH
self.assertEqual(None, self._call(self.renewable_cert))
def test_cert_priv_key_mismatch(self):
@@ -301,7 +274,7 @@ class ValidPrivkeyTest(unittest.TestCase):
return valid_privkey(privkey)
def test_valid_true(self):
self.assertTrue(self._call(RSA256_KEY))
self.assertTrue(self._call(RSA512_KEY))
def test_empty_false(self):
self.assertFalse(self._call(''))
@@ -319,12 +292,12 @@ class GetSANsFromCertTest(unittest.TestCase):
return get_sans_from_cert(*args, **kwargs)
def test_single(self):
self.assertEqual([], self._call(test_util.load_vector('cert.pem')))
self.assertEqual([], self._call(test_util.load_vector('cert_512.pem')))
def test_san(self):
self.assertEqual(
['example.com', 'www.example.com'],
self._call(test_util.load_vector('cert-san.pem')))
self._call(test_util.load_vector('cert-san_512.pem')))
class GetNamesFromCertTest(unittest.TestCase):
@@ -338,19 +311,19 @@ class GetNamesFromCertTest(unittest.TestCase):
def test_single(self):
self.assertEqual(
['example.com'],
self._call(test_util.load_vector('cert.pem')))
self._call(test_util.load_vector('cert_512.pem')))
def test_san(self):
self.assertEqual(
['example.com', 'www.example.com'],
self._call(test_util.load_vector('cert-san.pem')))
self._call(test_util.load_vector('cert-san_512.pem')))
def test_common_name_sans_order(self):
# Tests that the common name comes first
# followed by the SANS in alphabetical order
self.assertEqual(
['example.com'] + ['{0}.example.com'.format(c) for c in 'abcd'],
self._call(test_util.load_vector('cert-5sans.pem')))
self._call(test_util.load_vector('cert-5sans_512.pem')))
def test_parse_non_cert(self):
self.assertRaises(OpenSSL.crypto.Error, self._call, "hello there")

View File

@@ -310,10 +310,11 @@ class ChooseNamesTest(unittest.TestCase):
@test_util.patch_get_utility("certbot.display.ops.z_util")
def test_choose_manually(self, mock_util):
from certbot.display.ops import _choose_names_manually
utility_mock = mock_util()
# No retry
mock_util().yesno.return_value = False
utility_mock.yesno.return_value = False
# IDN and no retry
mock_util().input.return_value = (display_util.OK,
utility_mock.input.return_value = (display_util.OK,
"uniçodé.com")
self.assertEqual(_choose_names_manually(), [])
# IDN exception with previous mocks
@@ -324,7 +325,7 @@ class ChooseNamesTest(unittest.TestCase):
mock_sli.side_effect = unicode_error
self.assertEqual(_choose_names_manually(), [])
# Valid domains
mock_util().input.return_value = (display_util.OK,
utility_mock.input.return_value = (display_util.OK,
("example.com,"
"under_score.example.com,"
"justtld,"
@@ -332,14 +333,17 @@ class ChooseNamesTest(unittest.TestCase):
self.assertEqual(_choose_names_manually(),
["example.com", "under_score.example.com",
"justtld", "valid.example.com"])
@test_util.patch_get_utility("certbot.display.ops.z_util")
def test_choose_manually_retry(self, mock_util):
from certbot.display.ops import _choose_names_manually
utility_mock = mock_util()
# Three iterations
mock_util().input.return_value = (display_util.OK,
utility_mock.input.return_value = (display_util.OK,
"uniçodé.com")
yn = mock.MagicMock()
yn.side_effect = [True, True, False]
mock_util().yesno = yn
utility_mock.yesno.side_effect = [True, True, False]
_choose_names_manually()
self.assertEqual(mock_util().yesno.call_count, 3)
self.assertEqual(utility_mock.yesno.call_count, 3)
class SuccessInstallationTest(unittest.TestCase):

View File

@@ -4,20 +4,22 @@ import unittest
import mock
from certbot import constants
from certbot.tests import util
import certbot.tests.util as test_util
class HandleSubscriptionTest(unittest.TestCase):
class HandleSubscriptionTest(test_util.ConfigTestCase):
"""Tests for certbot.eff.handle_subscription."""
def setUp(self):
super(HandleSubscriptionTest, self).setUp()
self.email = 'certbot@example.org'
self.config = mock.Mock(email=self.email, eff_email=None)
self.config.email = self.email
self.config.eff_email = None
def _call(self):
from certbot.eff import handle_subscription
return handle_subscription(self.config)
@util.patch_get_utility()
@test_util.patch_get_utility()
@mock.patch('certbot.eff.subscribe')
def test_failure(self, mock_subscribe, mock_get_utility):
self.config.email = None
@@ -32,12 +34,12 @@ class HandleSubscriptionTest(unittest.TestCase):
@mock.patch('certbot.eff.subscribe')
def test_no_subscribe_with_no_prompt(self, mock_subscribe):
self.config.eff_email = False
with util.patch_get_utility() as mock_get_utility:
with test_util.patch_get_utility() as mock_get_utility:
self._call()
self.assertFalse(mock_subscribe.called)
self._assert_no_get_utility_calls(mock_get_utility)
@util.patch_get_utility()
@test_util.patch_get_utility()
@mock.patch('certbot.eff.subscribe')
def test_subscribe_with_no_prompt(self, mock_subscribe, mock_get_utility):
self.config.eff_email = True
@@ -49,7 +51,7 @@ class HandleSubscriptionTest(unittest.TestCase):
self.assertFalse(mock_get_utility().yesno.called)
self.assertFalse(mock_get_utility().add_message.called)
@util.patch_get_utility()
@test_util.patch_get_utility()
@mock.patch('certbot.eff.subscribe')
def test_subscribe_with_prompt(self, mock_subscribe, mock_get_utility):
mock_get_utility().yesno.return_value = True
@@ -62,7 +64,7 @@ class HandleSubscriptionTest(unittest.TestCase):
self.assertTrue(mock_subscribe.called)
self.assertEqual(mock_subscribe.call_args[0][0], self.email)
@util.patch_get_utility()
@test_util.patch_get_utility()
@mock.patch('certbot.eff.subscribe')
def test_no_subscribe_with_prompt(self, mock_subscribe, mock_get_utility):
mock_get_utility().yesno.return_value = False
@@ -105,7 +107,7 @@ class SubscribeTest(unittest.TestCase):
self.assertFalse(data is None)
self.assertEqual(data.get('email'), self.email)
@util.patch_get_utility()
@test_util.patch_get_utility()
def test_bad_status(self, mock_get_utility):
self.json['status'] = False
self._call() # pylint: disable=no-value-for-parameter
@@ -113,7 +115,7 @@ class SubscribeTest(unittest.TestCase):
expected_part = 'because your e-mail address appears to be invalid.'
self.assertTrue(expected_part in actual)
@util.patch_get_utility()
@test_util.patch_get_utility()
def test_not_ok(self, mock_get_utility):
self.response.ok = False
self._call() # pylint: disable=no-value-for-parameter
@@ -125,7 +127,7 @@ class SubscribeTest(unittest.TestCase):
self.assertTrue(mock_get_utility().add_message.called)
return mock_get_utility().add_message.call_args[0][0]
@util.patch_get_utility()
@test_util.patch_get_utility()
def test_subscribe(self, mock_get_utility):
self._call() # pylint: disable=no-value-for-parameter
self.assertFalse(mock_get_utility.called)

View File

@@ -23,6 +23,17 @@ class FailedChallengesTest(unittest.TestCase):
self.assertTrue(str(self.error).startswith(
"Failed authorization procedure. example.com (dns-01): tls"))
def test_unicode(self):
from certbot.errors import FailedChallenges
arabic_detail = u'\u0639\u062f\u0627\u0644\u0629'
arabic_error = FailedChallenges(set([achallenges.DNS(
domain="example.com", challb=messages.ChallengeBody(
chall=acme_util.DNS01, uri=None,
error=messages.Error(typ="tls", detail=arabic_detail)))]))
self.assertTrue(str(arabic_error).startswith(
"Failed authorization procedure. example.com (dns-01): tls"))
class StandaloneBindErrorTest(unittest.TestCase):
"""Tests for certbot.errors.StandaloneBindError."""

View File

@@ -16,7 +16,8 @@ class HookTest(unittest.TestCase):
@mock.patch('certbot.hooks._prog')
def test_validate_hooks(self, mock_prog):
config = mock.MagicMock(pre_hook="", post_hook="ls -lR", renew_hook="uptime")
config = mock.MagicMock(deploy_hook=None, pre_hook="",
post_hook="ls -lR", renew_hook="uptime")
hooks.validate_hooks(config)
self.assertEqual(mock_prog.call_count, 2)
self.assertEqual(mock_prog.call_args_list[1][0][0], 'uptime')
@@ -25,6 +26,19 @@ class HookTest(unittest.TestCase):
config = mock.MagicMock(pre_hook="explodinator", post_hook="", renew_hook="")
self.assertRaises(errors.HookCommandNotFound, hooks.validate_hooks, config)
@mock.patch('certbot.hooks.validate_hook')
def test_validation_order(self, mock_validate_hook):
# This ensures error messages are about deploy hook when appropriate
config = mock.Mock(deploy_hook=None, pre_hook=None,
post_hook=None, renew_hook=None)
hooks.validate_hooks(config)
order = [call[0][1] for call in mock_validate_hook.call_args_list]
self.assertTrue('pre' in order)
self.assertTrue('post' in order)
self.assertTrue('deploy' in order)
self.assertEqual(order[-1], 'renew')
@mock.patch('certbot.hooks.util.exe_exists')
@mock.patch('certbot.hooks.plug_util.path_surgery')
def test_prog(self, mock_ps, mock_exe_exists):
@@ -35,6 +49,19 @@ class HookTest(unittest.TestCase):
self.assertEqual(hooks._prog("funky"), None)
self.assertEqual(mock_ps.call_count, 1)
@mock.patch('certbot.hooks.renew_hook')
def test_deploy_hook(self, mock_renew_hook):
args = (mock.Mock(deploy_hook='foo'), ['example.org'], 'path',)
# pylint: disable=star-args
hooks.deploy_hook(*args)
mock_renew_hook.assert_called_once_with(*args)
@mock.patch('certbot.hooks.renew_hook')
def test_no_deploy_hook(self, mock_renew_hook):
args = (mock.Mock(deploy_hook=None), ['example.org'], 'path',)
hooks.deploy_hook(*args) # pylint: disable=star-args
mock_renew_hook.assert_not_called()
def _test_a_hook(self, config, hook_function, calls_expected, **kwargs):
with mock.patch('certbot.hooks.logger') as mock_logger:
mock_logger.warning = mock.MagicMock()

View File

@@ -55,7 +55,7 @@ class PreArgParseSetupTest(unittest.TestCase):
memory_handler, 1, 2, 3, debug=True, log_path=mock.ANY)
class PostArgParseSetupTest(test_util.TempDirTestCase):
class PostArgParseSetupTest(test_util.ConfigTestCase):
"""Tests for certbot.log.post_arg_parse_setup."""
@classmethod
@@ -65,9 +65,10 @@ class PostArgParseSetupTest(test_util.TempDirTestCase):
def setUp(self):
super(PostArgParseSetupTest, self).setUp()
self.config = mock.MagicMock(
debug=False, logs_dir=self.tempdir, quiet=False,
verbose_count=constants.CLI_DEFAULTS['verbose_count'])
self.config.debug = False
self.config.max_log_backups = 1000
self.config.quiet = False
self.config.verbose_count = constants.CLI_DEFAULTS['verbose_count']
self.devnull = open(os.devnull, 'w')
from certbot.log import ColoredStreamHandler
@@ -102,7 +103,7 @@ class PostArgParseSetupTest(test_util.TempDirTestCase):
self.assertFalse(os.path.exists(self.temp_path))
mock_sys.excepthook(1, 2, 3)
mock_except_hook.assert_called_once_with(
1, 2, 3, debug=self.config.debug, log_path=self.tempdir)
1, 2, 3, debug=self.config.debug, log_path=self.config.logs_dir)
level = self.stream_handler.level
if self.config.quiet:
@@ -119,7 +120,7 @@ class PostArgParseSetupTest(test_util.TempDirTestCase):
self.test_common()
class SetupLogFileHandlerTest(test_util.TempDirTestCase):
class SetupLogFileHandlerTest(test_util.ConfigTestCase):
"""Tests for certbot.log.setup_log_file_handler."""
@classmethod
@@ -129,7 +130,7 @@ class SetupLogFileHandlerTest(test_util.TempDirTestCase):
def setUp(self):
super(SetupLogFileHandlerTest, self).setUp()
self.config = mock.MagicMock(logs_dir=self.tempdir)
self.config.max_log_backups = 42
@mock.patch('certbot.main.logging.handlers.RotatingFileHandler')
def test_failure(self, mock_handler):
@@ -142,15 +143,32 @@ class SetupLogFileHandlerTest(test_util.TempDirTestCase):
else: # pragma: no cover
self.fail('Error not raised.')
def test_success(self):
def test_success_with_rollover(self):
self._test_success_common(should_rollover=True)
def test_success_without_rollover(self):
self.config.max_log_backups = 0
self._test_success_common(should_rollover=False)
def _test_success_common(self, should_rollover):
log_file = 'test.log'
handler, log_path = self._call(self.config, log_file, '%(message)s')
handler.close()
self.assertEqual(handler.level, logging.DEBUG)
self.assertEqual(handler.formatter.converter, time.gmtime)
expected_path = os.path.join(self.config.logs_dir, log_file)
self.assertEqual(log_path, expected_path)
handler.close()
backup_path = os.path.join(self.config.logs_dir, log_file + '.1')
self.assertEqual(os.path.exists(backup_path), should_rollover)
@mock.patch('certbot.log.logging.handlers.RotatingFileHandler')
def test_max_log_backups_used(self, mock_handler):
self._call(self.config, 'test.log', '%(message)s')
backup_count = mock_handler.call_args[1]['backupCount']
self.assertEqual(self.config.max_log_backups, backup_count)
class ColoredStreamHandlerTest(unittest.TestCase):

View File

@@ -23,8 +23,6 @@ from certbot import configuration
from certbot import crypto_util
from certbot import errors
from certbot import main
from certbot import renewal
from certbot import storage
from certbot import util
from certbot.plugins import disco
@@ -32,11 +30,13 @@ from certbot.plugins import manual
import certbot.tests.util as test_util
CERT_PATH = test_util.vector_path('cert.pem')
CERT = test_util.vector_path('cert.pem')
CSR = test_util.vector_path('csr.der')
CERT_PATH = test_util.vector_path('cert_512.pem')
CERT = test_util.vector_path('cert_512.pem')
CSR = test_util.vector_path('csr_512.der')
KEY = test_util.vector_path('rsa256_key.pem')
JWK = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem"))
JWK = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem')
SS_CERT_PATH = test_util.vector_path('cert_2048.pem')
class TestHandleIdenticalCerts(unittest.TestCase):
@@ -164,9 +164,7 @@ class CertonlyTest(unittest.TestCase):
self.assertTrue(mock_report_cert.call_count == 2)
# error in _ask_user_to_confirm_new_names
util_mock = mock.Mock()
util_mock.yesno.return_value = False
self.mock_get_utility.return_value = util_mock
self.mock_get_utility().yesno.return_value = False
self.assertRaises(errors.ConfigurationError, self._call,
('certonly --webroot -d example.com -d test.com --cert-name example.com').split())
@@ -225,7 +223,7 @@ class RevokeTest(test_util.TempDirTestCase):
shutil.copy(CERT_PATH, self.tempdir)
self.tmp_cert_path = os.path.abspath(os.path.join(self.tempdir,
'cert.pem'))
'cert_512.pem'))
self.patches = [
mock.patch('acme.client.Client', autospec=True),
@@ -287,16 +285,14 @@ class RevokeTest(test_util.TempDirTestCase):
self.mock_success_revoke.assert_not_called()
class DetermineAccountTest(unittest.TestCase):
class DetermineAccountTest(test_util.ConfigTestCase):
"""Tests for certbot.main._determine_account."""
def setUp(self):
self.args = mock.MagicMock(account=None, email=None,
config_dir="unused_config",
logs_dir="unused_logs",
work_dir="unused_work",
register_unsafely_without_email=False)
self.config = configuration.NamespaceConfig(self.args)
super(DetermineAccountTest, self).setUp()
self.config.account = None
self.config.email = None
self.config.register_unsafely_without_email = False
self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')]
self.account_storage = account.AccountMemoryStorage()
# For use in saving accounts: fake out the new_authz URL.
@@ -357,19 +353,16 @@ class DetermineAccountTest(unittest.TestCase):
self.assertEqual('other email', self.config.email)
class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-methods
class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-methods
"""Tests for different commands."""
def setUp(self):
super(MainTest, self).setUp()
self.config_dir = os.path.join(self.tempdir, 'config')
self.work_dir = os.path.join(self.tempdir, 'work')
self.logs_dir = os.path.join(self.tempdir, 'logs')
os.mkdir(self.logs_dir)
self.standard_args = ['--config-dir', self.config_dir,
'--work-dir', self.work_dir,
'--logs-dir', self.logs_dir, '--text']
os.mkdir(self.config.logs_dir)
self.standard_args = ['--config-dir', self.config.config_dir,
'--work-dir', self.config.work_dir,
'--logs-dir', self.config.logs_dir, '--text']
def tearDown(self):
# Reset globals in cli
@@ -546,7 +539,32 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
_, stdout, _, _ = self._call(['plugins'])
stdout = six.StringIO()
with test_util.patch_get_utility_with_stdout(stdout=stdout):
_, stdout, _, _ = self._call(['plugins'], stdout)
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
self.assertEqual(stdout.getvalue().strip(), str(filtered))
@mock.patch('certbot.main.plugins_disco')
@mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_no_args_unprivileged(self, _det, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
def throw_error(directory, mode, uid, strict):
"""Raises error.Error."""
_, _, _, _ = directory, mode, uid, strict
raise errors.Error()
stdout = six.StringIO()
with mock.patch('certbot.util.set_up_core_dir') as mock_set_up_core_dir:
with test_util.patch_get_utility_with_stdout(stdout=stdout):
mock_set_up_core_dir.side_effect = throw_error
_, stdout, _, _ = self._call(['plugins'], stdout)
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
@@ -558,7 +576,10 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
_, stdout, _, _ = self._call(['plugins', '--init'])
stdout = six.StringIO()
with test_util.patch_get_utility_with_stdout(stdout=stdout):
_, stdout, _, _ = self._call(['plugins', '--init'], stdout)
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
@@ -572,7 +593,11 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
def test_plugins_prepare(self, _det, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
stdout = six.StringIO()
with test_util.patch_get_utility_with_stdout(stdout=stdout):
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'], stdout)
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
@@ -645,7 +670,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
self.assertRaises(
errors.Error, self._call,
'certonly --csr {0}'.format(
test_util.vector_path('csr-nonames.pem')).split())
test_util.vector_path('csr-nonames_512.pem')).split())
def test_csr_with_inconsistent_domains(self):
self.assertRaises(
@@ -678,11 +703,12 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
@test_util.patch_get_utility()
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
cert_path = '/etc/letsencrypt/live/foo.bar'
key_path = '/etc/letsencrypt/live/baz.qux'
date = '1970-01-01'
mock_notAfter().date.return_value = date
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path,
fullchain_path=cert_path)
fullchain_path=cert_path, key_path=key_path)
mock_client = mock.MagicMock()
mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
self._certonly_new_request_common(mock_client)
@@ -691,6 +717,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
cert_msg = mock_get_utility().add_message.call_args_list[0][0][0]
self.assertTrue(cert_path in cert_msg)
self.assertTrue(date in cert_msg)
self.assertTrue(key_path in cert_msg)
self.assertTrue(
'donate' in mock_get_utility().add_message.call_args[0][0])
@@ -701,9 +728,10 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
self._certonly_new_request_common, mock_client)
def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,
args=None, should_renew=True, error_expected=False):
args=None, should_renew=True, error_expected=False,
quiet_mode=False):
# pylint: disable=too-many-locals,too-many-arguments
cert_path = test_util.vector_path('cert.pem')
cert_path = test_util.vector_path('cert_512.pem')
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path,
cert_path=cert_path, fullchain_path=chain_path)
@@ -713,15 +741,23 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
mock_certr = mock.MagicMock()
mock_key = mock.MagicMock(pem='pem_key')
mock_client = mock.MagicMock()
stdout = None
stdout = six.StringIO()
mock_client.obtain_certificate.return_value = (mock_certr, 'chain',
mock_key, 'csr')
def write_msg(message, *args, **kwargs):
"""Write message to stdout."""
_, _ = args, kwargs
stdout.write(message)
try:
with mock.patch('certbot.cert_manager.find_duplicative_certs') as mock_fdc:
mock_fdc.return_value = (mock_lineage, None)
with mock.patch('certbot.main._init_le_client') as mock_init:
mock_init.return_value = mock_client
with test_util.patch_get_utility() as mock_get_utility:
if not quiet_mode:
mock_get_utility().notification.side_effect = write_msg
with mock.patch('certbot.main.renewal.OpenSSL') as mock_ssl:
mock_latest = mock.MagicMock()
mock_latest.get_issuer.return_value = "Fake fake"
@@ -732,7 +768,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
if extra_args:
args += extra_args
try:
ret, stdout, _, _ = self._call(args)
ret, stdout, _, _ = self._call(args, stdout)
if ret:
print("Returned", ret)
raise AssertionError(ret)
@@ -752,7 +788,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
raise
finally:
if log_out:
with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
with open(os.path.join(self.config.logs_dir, "letsencrypt.log")) as lf:
self.assertTrue(log_out in lf.read())
return mock_lineage, mock_get_utility, stdout
@@ -784,59 +820,45 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
def _dump_log(self):
print("Logs:")
log_path = os.path.join(self.logs_dir, "letsencrypt.log")
log_path = os.path.join(self.config.logs_dir, "letsencrypt.log")
if os.path.exists(log_path):
with open(log_path) as lf:
print(lf.read())
def test_renew_verb(self):
test_util.make_lineage(self.config_dir, 'sample-renewal.conf')
test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
args = ["renew", "--dry-run", "-tvv"]
self._test_renewal_common(True, [], args=args, should_renew=True)
def test_quiet_renew(self):
test_util.make_lineage(self.config_dir, 'sample-renewal.conf')
test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
args = ["renew", "--dry-run"]
_, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
out = stdout.getvalue()
self.assertTrue("renew" in out)
args = ["renew", "--dry-run", "-q"]
_, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
_, _, stdout = self._test_renewal_common(True, [], args=args,
should_renew=True, quiet_mode=True)
out = stdout.getvalue()
self.assertEqual("", out)
def test_renew_hook_validation(self):
test_util.make_lineage(self.config_dir, 'sample-renewal.conf')
test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
args = ["renew", "--dry-run", "--post-hook=no-such-command"]
self._test_renewal_common(True, [], args=args, should_renew=False,
error_expected=True)
def test_renew_no_hook_validation(self):
test_util.make_lineage(self.config_dir, 'sample-renewal.conf')
test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
args = ["renew", "--dry-run", "--post-hook=no-such-command",
"--disable-hook-validation"]
with mock.patch("certbot.hooks.post_hook"):
self._test_renewal_common(True, [], args=args, should_renew=True,
error_expected=False)
@mock.patch("certbot.cli.set_by_cli")
def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
mock_set_by_cli.return_value = False
rc_path = test_util.make_lineage(
self.config_dir, 'sample-renewal-ancient.conf')
args = mock.MagicMock(account=None, config_dir=self.config_dir,
logs_dir=self.logs_dir, work_dir=self.work_dir,
email=None, webroot_path=None)
config = configuration.NamespaceConfig(args)
lineage = storage.RenewableCert(rc_path, config)
renewalparams = lineage.configuration["renewalparams"]
# pylint: disable=protected-access
renewal._restore_webroot_config(config, renewalparams)
self.assertEqual(config.webroot_path, ["/var/www/"])
def test_renew_verb_empty_config(self):
rd = os.path.join(self.config_dir, 'renewal')
rd = os.path.join(self.config.config_dir, 'renewal')
if not os.path.exists(rd):
os.makedirs(rd)
with open(os.path.join(rd, 'empty.conf'), 'w'):
@@ -845,7 +867,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True)
def test_renew_with_certname(self):
test_util.make_lineage(self.config_dir, 'sample-renewal.conf')
test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
self._test_renewal_common(True, [], should_renew=True,
args=['renew', '--dry-run', '--cert-name', 'sample-renewal'])
@@ -855,7 +877,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
error_expected=True)
def _make_dummy_renewal_config(self):
renewer_configs_dir = os.path.join(self.config_dir, 'renewal')
renewer_configs_dir = os.path.join(self.config.config_dir, 'renewal')
os.makedirs(renewer_configs_dir)
with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f:
f.write("My contents don't matter")
@@ -973,7 +995,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
chain = 'chain'
mock_client = mock.MagicMock()
mock_client.obtain_certificate_from_csr.return_value = (certr, chain)
cert_path = '/etc/letsencrypt/live/example.com/cert.pem'
cert_path = '/etc/letsencrypt/live/example.com/cert_512.pem'
full_path = '/etc/letsencrypt/live/example.com/fullchain.pem'
mock_client.save_certificate.return_value = cert_path, None, full_path
with mock.patch('certbot.main._init_le_client') as mock_init:
@@ -1000,6 +1022,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
mock_get_utility = self._test_certonly_csr_common()
cert_msg = mock_get_utility().add_message.call_args_list[0][0][0]
self.assertTrue('fullchain.pem' in cert_msg)
self.assertFalse('Your key file has been saved at' in cert_msg)
self.assertTrue(
'donate' in mock_get_utility().add_message.call_args[0][0])
@@ -1012,18 +1035,24 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me
@mock.patch('certbot.main.client.acme_client')
def test_revoke_with_key(self, mock_acme_client):
server = 'foo.bar'
self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY,
self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH,
'--server', server, 'revoke'])
with open(KEY, 'rb') as f:
with open(RSA2048_KEY_PATH, 'rb') as f:
mock_acme_client.Client.assert_called_once_with(
server, key=jose.JWK.load(f.read()), net=mock.ANY)
with open(CERT, 'rb') as f:
with open(SS_CERT_PATH, 'rb') as f:
cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
mock_revoke = mock_acme_client.Client().revoke
mock_revoke.assert_called_once_with(
jose.ComparableX509(cert),
mock.ANY)
def test_revoke_with_key_mismatch(self):
server = 'foo.bar'
self.assertRaises(errors.Error, self._call_no_clientmock,
['--cert-path', CERT, '--key-path', KEY,
'--server', server, 'revoke'])
@mock.patch('certbot.main._determine_account')
def test_revoke_without_key(self, mock_determine_account):
mock_determine_account.return_value = (mock.MagicMock(), None)
@@ -1126,7 +1155,7 @@ class UnregisterTest(unittest.TestCase):
def test_abort_unregister(self):
self.mocks['account'].AccountFileStorage.return_value = mock.Mock()
util_mock = self.mocks['get_utility'].return_value
util_mock = self.mocks['get_utility']()
util_mock.yesno.return_value = False
config = mock.Mock()

View File

@@ -1,5 +1,4 @@
"""Tests for certbot.renewal"""
import os
import mock
import unittest
@@ -9,24 +8,22 @@ from certbot import configuration
from certbot import errors
from certbot import storage
from certbot.tests import util
import certbot.tests.util as test_util
class RenewalTest(util.TempDirTestCase):
class RenewalTest(test_util.ConfigTestCase):
def setUp(self):
super(RenewalTest, self).setUp()
self.config_dir = os.path.join(self.tempdir, 'config')
@mock.patch('certbot.cli.set_by_cli')
def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
mock_set_by_cli.return_value = False
rc_path = util.make_lineage(
self.config_dir, 'sample-renewal-ancient.conf')
args = mock.MagicMock(account=None, config_dir=self.config_dir,
logs_dir="logs", work_dir="work",
email=None, webroot_path=None)
config = configuration.NamespaceConfig(args)
rc_path = test_util.make_lineage(
self.config.config_dir, 'sample-renewal-ancient.conf')
self.config.account = None
self.config.email = None
self.config.webroot_path = None
config = configuration.NamespaceConfig(self.config)
lineage = storage.RenewableCert(rc_path, config)
renewalparams = lineage.configuration['renewalparams']
# pylint: disable=protected-access
@@ -35,10 +32,10 @@ class RenewalTest(util.TempDirTestCase):
self.assertEqual(config.webroot_path, ['/var/www/'])
class RestoreRequiredConfigElementsTest(unittest.TestCase):
class RestoreRequiredConfigElementsTest(test_util.ConfigTestCase):
"""Tests for certbot.renewal.restore_required_config_elements."""
def setUp(self):
self.config = mock.MagicMock()
super(RestoreRequiredConfigElementsTest, self).setUp()
@classmethod
def _call(cls, *args, **kwargs):

View File

@@ -14,16 +14,16 @@ from certbot import errors
from certbot.tests import util as test_util
class ReverterCheckpointLocalTest(unittest.TestCase):
class ReverterCheckpointLocalTest(test_util.ConfigTestCase):
# pylint: disable=too-many-instance-attributes, too-many-public-methods
"""Test the Reverter Class."""
def setUp(self):
super(ReverterCheckpointLocalTest, self).setUp()
from certbot.reverter import Reverter
# Disable spurious errors... we are trying to test for them
logging.disable(logging.CRITICAL)
self.config = setup_work_direc()
self.reverter = Reverter(self.config)
tup = setup_test_files()
@@ -277,15 +277,15 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
self.assertEqual(read_in(self.config2), "directive-dir2")
class TestFullCheckpointsReverter(unittest.TestCase):
class TestFullCheckpointsReverter(test_util.ConfigTestCase):
# pylint: disable=too-many-instance-attributes
"""Tests functions having to deal with full checkpoints."""
def setUp(self):
super(TestFullCheckpointsReverter, self).setUp()
from certbot.reverter import Reverter
# Disable spurious errors...
logging.disable(logging.CRITICAL)
self.config = setup_work_direc()
self.reverter = Reverter(self.config)
tup = setup_test_files()
@@ -439,21 +439,6 @@ class TestFullCheckpointsReverter(unittest.TestCase):
return config3
def setup_work_direc():
"""Setup directories.
:returns: Mocked :class:`certbot.interfaces.IConfig`
"""
work_dir = tempfile.mkdtemp("work")
backup_dir = os.path.join(work_dir, "backup")
return mock.MagicMock(
work_dir=work_dir, backup_dir=backup_dir,
temp_checkpoint_dir=os.path.join(work_dir, "temp"),
in_progress_dir=os.path.join(backup_dir, "in_progress_dir"))
def setup_test_files():
"""Setup sample configuration files."""
dir1 = tempfile.mkdtemp("dir1")

View File

@@ -13,14 +13,14 @@ import six
import certbot
from certbot import cli
from certbot import configuration
from certbot import errors
from certbot.storage import ALL_FOUR
from certbot.tests import util
import certbot.tests.util as test_util
CERT = util.load_cert('cert.pem')
CERT = test_util.load_cert('cert_512.pem')
def unlink_all(rc_object):
@@ -36,7 +36,7 @@ def fill_with_sample_data(rc_object):
f.write(kind)
class BaseRenewableCertTest(util.TempDirTestCase):
class BaseRenewableCertTest(test_util.ConfigTestCase):
"""Base class for setting up Renewable Cert tests.
.. note:: It may be required to write out self.config for
@@ -50,39 +50,31 @@ class BaseRenewableCertTest(util.TempDirTestCase):
super(BaseRenewableCertTest, self).setUp()
self.cli_config = configuration.NamespaceConfig(
namespace=mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
logs_dir=self.tempdir,
)
)
# TODO: maybe provide NamespaceConfig.make_dirs?
# TODO: main() should create those dirs, c.f. #902
os.makedirs(os.path.join(self.tempdir, "live", "example.org"))
archive_path = os.path.join(self.tempdir, "archive", "example.org")
os.makedirs(os.path.join(self.config.config_dir, "live", "example.org"))
archive_path = os.path.join(self.config.config_dir, "archive", "example.org")
os.makedirs(archive_path)
os.makedirs(os.path.join(self.tempdir, "renewal"))
os.makedirs(os.path.join(self.config.config_dir, "renewal"))
config = configobj.ConfigObj()
config_file = configobj.ConfigObj()
for kind in ALL_FOUR:
kind_path = os.path.join(self.tempdir, "live", "example.org",
kind_path = os.path.join(self.config.config_dir, "live", "example.org",
kind + ".pem")
config[kind] = kind_path
with open(os.path.join(self.tempdir, "live", "example.org",
config_file[kind] = kind_path
with open(os.path.join(self.config.config_dir, "live", "example.org",
"README"), 'a'):
pass
config["archive"] = archive_path
config.filename = os.path.join(self.tempdir, "renewal",
config_file["archive"] = archive_path
config_file.filename = os.path.join(self.config.config_dir, "renewal",
"example.org.conf")
config.write()
self.config = config
config_file.write()
self.config_file = config_file
# We also create a file that isn't a renewal config in the same
# location to test that logic that reads in all-and-only renewal
# configs will ignore it and NOT attempt to parse it.
junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w")
junk = open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w")
junk.write("This file should be ignored!")
junk.close()
@@ -90,7 +82,7 @@ class BaseRenewableCertTest(util.TempDirTestCase):
with mock.patch("certbot.storage.RenewableCert._check_symlinks") as check:
check.return_value = True
self.test_rc = storage.RenewableCert(config.filename, self.cli_config)
self.test_rc = storage.RenewableCert(config_file.filename, self.config)
def _write_out_kind(self, kind, ver, value=None):
link = getattr(self.test_rc, kind)
@@ -117,7 +109,7 @@ class RenewableCertTests(BaseRenewableCertTest):
for kind in ALL_FOUR:
self.assertEqual(
getattr(self.test_rc, kind), os.path.join(
self.tempdir, "live", "example.org", kind + ".pem"))
self.config.config_dir, "live", "example.org", kind + ".pem"))
def test_renewal_bad_config(self):
"""Test that the RenewableCert constructor will complain if
@@ -125,14 +117,14 @@ class RenewableCertTests(BaseRenewableCertTest):
"""
from certbot import storage
broken = os.path.join(self.tempdir, "broken.conf")
broken = os.path.join(self.config.config_dir, "broken.conf")
with open(broken, "w") as f:
f.write("[No closing bracket for you!")
self.assertRaises(errors.CertStorageError, storage.RenewableCert,
broken, self.cli_config)
broken, self.config)
os.unlink(broken)
self.assertRaises(errors.CertStorageError, storage.RenewableCert,
"fun", self.cli_config)
"fun", self.config)
def test_renewal_incomplete_config(self):
"""Test that the RenewableCert constructor will complain if
@@ -143,30 +135,30 @@ class RenewableCertTests(BaseRenewableCertTest):
# Here the required privkey is missing.
config["chain"] = "imaginary_chain.pem"
config["fullchain"] = "imaginary_fullchain.pem"
config.filename = os.path.join(self.tempdir, "imaginary_config.conf")
config.filename = os.path.join(self.config.config_dir, "imaginary_config.conf")
config.write()
self.assertRaises(errors.CertStorageError, storage.RenewableCert,
config.filename, self.cli_config)
config.filename, self.config)
def test_no_renewal_version(self):
from certbot import storage
self._write_out_ex_kinds()
self.assertTrue("version" not in self.config)
self.assertTrue("version" not in self.config_file)
with mock.patch("certbot.storage.logger") as mock_logger:
storage.RenewableCert(self.config.filename, self.cli_config)
storage.RenewableCert(self.config_file.filename, self.config)
self.assertFalse(mock_logger.warning.called)
def test_renewal_newer_version(self):
from certbot import storage
self._write_out_ex_kinds()
self.config["version"] = "99.99.99"
self.config.write()
self.config_file["version"] = "99.99.99"
self.config_file.write()
with mock.patch("certbot.storage.logger") as mock_logger:
storage.RenewableCert(self.config.filename, self.cli_config)
storage.RenewableCert(self.config_file.filename, self.config)
self.assertTrue(mock_logger.warning.called)
self.assertTrue("version" in mock_logger.warning.call_args[0][0])
@@ -191,7 +183,7 @@ class RenewableCertTests(BaseRenewableCertTest):
unlink_all(self.test_rc)
# Items must point to desired place if they are absolute
for kind in ALL_FOUR:
os.symlink(os.path.join(self.tempdir, kind + "17.pem"),
os.symlink(os.path.join(self.config.config_dir, kind + "17.pem"),
getattr(self.test_rc, kind))
self.assertFalse(self.test_rc._consistent())
unlink_all(self.test_rc)
@@ -216,17 +208,17 @@ class RenewableCertTests(BaseRenewableCertTest):
# Relative path logic
self._write_out_kind("cert", 17)
self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"),
os.path.join(self.tempdir, "archive",
os.path.join(self.config.config_dir, "archive",
"example.org",
"cert17.pem")))
# Absolute path logic
os.unlink(self.test_rc.cert)
os.symlink(os.path.join(self.tempdir, "archive", "example.org",
os.symlink(os.path.join(self.config.config_dir, "archive", "example.org",
"cert17.pem"), self.test_rc.cert)
with open(self.test_rc.cert, "w") as f:
f.write("cert")
self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"),
os.path.join(self.tempdir, "archive",
os.path.join(self.config.config_dir, "archive",
"example.org",
"cert17.pem")))
@@ -369,18 +361,21 @@ class RenewableCertTests(BaseRenewableCertTest):
def test_names(self):
# Trying the current version
self._write_out_kind("cert", 12, util.load_vector("cert-san.pem"))
self._write_out_kind("cert", 12, test_util.load_vector("cert-san_512.pem"))
self.assertEqual(self.test_rc.names(),
["example.com", "www.example.com"])
# Trying a non-current version
self._write_out_kind("cert", 15, util.load_vector("cert.pem"))
self._write_out_kind("cert", 15, test_util.load_vector("cert_512.pem"))
self.assertEqual(self.test_rc.names(12),
["example.com", "www.example.com"])
# Testing common name is listed first
self._write_out_kind(
"cert", 12, util.load_vector("cert-5sans.pem"))
"cert", 12, test_util.load_vector("cert-5sans_512.pem"))
self.assertEqual(
self.test_rc.names(12),
["example.com"] + ["{0}.example.com".format(c) for c in "abcd"])
@@ -393,7 +388,8 @@ class RenewableCertTests(BaseRenewableCertTest):
def test_time_interval_judgments(self, mock_datetime):
"""Test should_autodeploy() and should_autorenew() on the basis
of expiry time windows."""
test_cert = util.load_vector("cert.pem")
test_cert = test_util.load_vector("cert_512.pem")
self._write_out_ex_kinds()
self.test_rc.update_all_links_to(12)
@@ -491,7 +487,7 @@ class RenewableCertTests(BaseRenewableCertTest):
self.test_rc.update_all_links_to(3)
self.assertEqual(
6, self.test_rc.save_successor(3, b'new cert', None,
b'new chain', self.cli_config))
b'new chain', self.config))
with open(self.test_rc.version("cert", 6)) as f:
self.assertEqual(f.read(), "new cert")
with open(self.test_rc.version("chain", 6)) as f:
@@ -504,10 +500,10 @@ class RenewableCertTests(BaseRenewableCertTest):
# Let's try two more updates
self.assertEqual(
7, self.test_rc.save_successor(6, b'again', None,
b'newer chain', self.cli_config))
b'newer chain', self.config))
self.assertEqual(
8, self.test_rc.save_successor(7, b'hello', None,
b'other chain', self.cli_config))
b'other chain', self.config))
# All of the subsequent versions should link directly to the original
# privkey.
for i in (6, 7, 8):
@@ -522,14 +518,14 @@ class RenewableCertTests(BaseRenewableCertTest):
self.test_rc.update_all_links_to(8)
self.assertEqual(
9, self.test_rc.save_successor(8, b'last', None,
b'attempt', self.cli_config))
b'attempt', self.config))
for kind in ALL_FOUR:
self.assertEqual(self.test_rc.available_versions(kind),
list(six.moves.range(1, 10)))
self.assertEqual(self.test_rc.current_version(kind), 8)
with open(self.test_rc.version("fullchain", 9)) as f:
self.assertEqual(f.read(), "last" + "attempt")
temp_config_file = os.path.join(self.cli_config.renewal_configs_dir,
temp_config_file = os.path.join(self.config.renewal_configs_dir,
self.test_rc.lineagename) + ".conf.new"
with open(temp_config_file, "w") as f:
f.write("We previously crashed while writing me :(")
@@ -537,7 +533,7 @@ class RenewableCertTests(BaseRenewableCertTest):
# be saved in a new file rather than creating a new symlink.
self.assertEqual(
10, self.test_rc.save_successor(9, b'with', b'a',
b'key', self.cli_config))
b'key', self.config))
self.assertTrue(os.path.exists(self.test_rc.version("privkey", 10)))
self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10)))
self.assertFalse(os.path.exists(temp_config_file))
@@ -597,38 +593,38 @@ class RenewableCertTests(BaseRenewableCertTest):
from certbot import storage
result = storage.RenewableCert.new_lineage(
"the-lineage.com", b"cert", b"privkey", b"chain", self.cli_config)
"the-lineage.com", b"cert", b"privkey", b"chain", self.config)
# This consistency check tests most relevant properties about the
# newly created cert lineage.
# pylint: disable=protected-access
self.assertTrue(result._consistent())
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
self.config.renewal_configs_dir, "the-lineage.com.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "the-lineage.com", "README")))
self.config.live_dir, "the-lineage.com", "README")))
with open(result.fullchain, "rb") as f:
self.assertEqual(f.read(), b"cert" + b"chain")
# Let's do it again and make sure it makes a different lineage
result = storage.RenewableCert.new_lineage(
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config)
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf")))
self.config.renewal_configs_dir, "the-lineage.com-0001.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "the-lineage.com-0001", "README")))
self.config.live_dir, "the-lineage.com-0001", "README")))
# Now trigger the detection of already existing files
os.mkdir(os.path.join(
self.cli_config.live_dir, "the-lineage.com-0002"))
self.config.live_dir, "the-lineage.com-0002"))
self.assertRaises(errors.CertStorageError,
storage.RenewableCert.new_lineage, "the-lineage.com",
b"cert3", b"privkey3", b"chain3", self.cli_config)
os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com"))
b"cert3", b"privkey3", b"chain3", self.config)
os.mkdir(os.path.join(self.config.default_archive_dir, "other-example.com"))
self.assertRaises(errors.CertStorageError,
storage.RenewableCert.new_lineage,
"other-example.com", b"cert4",
b"privkey4", b"chain4", self.cli_config)
b"privkey4", b"chain4", self.config)
# Make sure it can accept renewal parameters
result = storage.RenewableCert.new_lineage(
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config)
# TODO: Conceivably we could test that the renewal parameters actually
# got saved
@@ -640,19 +636,19 @@ class RenewableCertTests(BaseRenewableCertTest):
mock_rv.side_effect = lambda x: x
from certbot import storage
shutil.rmtree(self.cli_config.renewal_configs_dir)
shutil.rmtree(self.cli_config.default_archive_dir)
shutil.rmtree(self.cli_config.live_dir)
shutil.rmtree(self.config.renewal_configs_dir)
shutil.rmtree(self.config.default_archive_dir)
shutil.rmtree(self.config.live_dir)
storage.RenewableCert.new_lineage(
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
"the-lineage.com", b"cert2", b"privkey2", b"chain2", self.config)
self.assertTrue(os.path.exists(
os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
self.config.renewal_configs_dir, "the-lineage.com.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "the-lineage.com", "privkey.pem")))
self.config.live_dir, "the-lineage.com", "privkey.pem")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.default_archive_dir, "the-lineage.com", "privkey1.pem")))
self.config.default_archive_dir, "the-lineage.com", "privkey1.pem")))
@mock.patch("certbot.storage.util.unique_lineage_name")
def test_invalid_config_filename(self, mock_uln):
@@ -660,7 +656,7 @@ class RenewableCertTests(BaseRenewableCertTest):
mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes"
self.assertRaises(errors.CertStorageError,
storage.RenewableCert.new_lineage, "example.com",
"cert", "privkey", "chain", self.cli_config)
"cert", "privkey", "chain", self.config)
def test_bad_kind(self):
self.assertRaises(
@@ -744,18 +740,18 @@ class RenewableCertTests(BaseRenewableCertTest):
from certbot import storage
self.assertRaises(errors.CertStorageError,
storage.RenewableCert,
self.config.filename, self.cli_config)
os.symlink("missing", self.config[ALL_FOUR[0]])
self.config_file.filename, self.config)
os.symlink("missing", self.config_file[ALL_FOUR[0]])
self.assertRaises(errors.CertStorageError,
storage.RenewableCert,
self.config.filename, self.cli_config)
self.config_file.filename, self.config)
def test_write_renewal_config(self):
# Mostly tested by the process of creating and updating lineages,
# but we can test that this successfully creates files, removes
# unneeded items, and preserves comments.
temp = os.path.join(self.tempdir, "sample-file")
temp2 = os.path.join(self.tempdir, "sample-file.new")
temp = os.path.join(self.config.config_dir, "sample-file")
temp2 = os.path.join(self.config.config_dir, "sample-file.new")
with open(temp, "w") as f:
f.write("[renewalparams]\nuseful = value # A useful value\n"
"useless = value # Not needed\n")
@@ -785,17 +781,17 @@ class RenewableCertTests(BaseRenewableCertTest):
def test_update_symlinks(self):
from certbot import storage
archive_dir_path = os.path.join(self.tempdir, "archive", "example.org")
archive_dir_path = os.path.join(self.config.config_dir, "archive", "example.org")
for kind in ALL_FOUR:
live_path = self.config[kind]
live_path = self.config_file[kind]
basename = kind + "1.pem"
archive_path = os.path.join(archive_dir_path, basename)
open(archive_path, 'a').close()
os.symlink(os.path.join(self.tempdir, basename), live_path)
os.symlink(os.path.join(self.config.config_dir, basename), live_path)
self.assertRaises(errors.CertStorageError,
storage.RenewableCert, self.config.filename,
self.cli_config)
storage.RenewableCert(self.config.filename, self.cli_config,
storage.RenewableCert, self.config_file.filename,
self.config)
storage.RenewableCert(self.config_file.filename, self.config,
update_symlinks=True)
class DeleteFilesTest(BaseRenewableCertTest):
@@ -804,88 +800,88 @@ class DeleteFilesTest(BaseRenewableCertTest):
super(DeleteFilesTest, self).setUp()
for kind in ALL_FOUR:
kind_path = os.path.join(self.tempdir, "live", "example.org",
kind_path = os.path.join(self.config.config_dir, "live", "example.org",
kind + ".pem")
with open(kind_path, 'a'):
pass
self.config.write()
self.config_file.write()
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "example.org.conf")))
self.config.renewal_configs_dir, "example.org.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.config.live_dir, "example.org")))
self.assertTrue(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
self.config.config_dir, "archive", "example.org")))
def _call(self):
from certbot import storage
with mock.patch("certbot.storage.logger"):
storage.delete_files(self.cli_config, "example.org")
storage.delete_files(self.config, "example.org")
def test_delete_all_files(self):
self._call()
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "example.org.conf")))
self.config.renewal_configs_dir, "example.org.conf")))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
self.config.config_dir, "archive", "example.org")))
def test_bad_renewal_config(self):
with open(self.config.filename, 'a') as config_file:
with open(self.config_file.filename, 'a') as config_file:
config_file.write("asdfasfasdfasdf")
self.assertRaises(errors.CertStorageError, self._call)
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "example.org.conf")))
self.config.renewal_configs_dir, "example.org.conf")))
def test_no_renewal_config(self):
os.remove(self.config.filename)
os.remove(self.config_file.filename)
self.assertRaises(errors.CertStorageError, self._call)
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(self.config.filename))
self.config.live_dir, "example.org")))
self.assertFalse(os.path.exists(self.config_file.filename))
def test_no_cert_file(self):
os.remove(os.path.join(
self.cli_config.live_dir, "example.org", "cert.pem"))
self.config.live_dir, "example.org", "cert.pem"))
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertFalse(os.path.exists(self.config_file.filename))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
self.config.config_dir, "archive", "example.org")))
def test_no_readme_file(self):
os.remove(os.path.join(
self.cli_config.live_dir, "example.org", "README"))
self.config.live_dir, "example.org", "README"))
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertFalse(os.path.exists(self.config_file.filename))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
self.config.config_dir, "archive", "example.org")))
def test_livedir_not_empty(self):
with open(os.path.join(
self.cli_config.live_dir, "example.org", "other_file"), 'a'):
self.config.live_dir, "example.org", "other_file"), 'a'):
pass
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertFalse(os.path.exists(self.config_file.filename))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
self.config.config_dir, "archive", "example.org")))
def test_no_archive(self):
archive_dir = os.path.join(self.tempdir, "archive", "example.org")
archive_dir = os.path.join(self.config.config_dir, "archive", "example.org")
os.rmdir(archive_dir)
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertFalse(os.path.exists(self.config_file.filename))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.config.live_dir, "example.org")))
self.assertFalse(os.path.exists(archive_dir))

11
certbot/tests/testdata/README vendored Normal file
View File

@@ -0,0 +1,11 @@
The following command has been used to generate test keys:
for x in 256 512 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
and for the CSR PEM (Certificate Signing Request):
openssl req -new -out csr-Xsans_X.pem -key rsa512_key.pem [-config csr-Xsans_X.conf | -subj '/CN=example.com'] [-outform DER > csr_X.der]
and for the certificate:
openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der]

View File

@@ -1 +0,0 @@
MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMndfk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o

Binary file not shown.

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