Compare commits

...

296 Commits

Author SHA1 Message Date
m0namon
f9749f64d8 temp testfixes 2020-01-21 17:17:16 -08:00
sydneyli
c0633ec3ac Implement ApacheBlockNode.delete_child 2020-01-21 16:20:57 -08:00
sydneyli
c9edb49267 Implement ApacheBlockNode.add_child_* 2020-01-21 16:20:32 -08:00
sydneyli
9cc15da90f Implement ApacheBlockNode constructor 2020-01-21 16:19:58 -08:00
m0namon
69bb92d698 Bump apacheconfig dependency to latest version and install dev version of apache for coverage tests 2020-01-21 16:02:17 -08:00
m0namon
bafa9e2eb0 Load apacheconfig dependency, gate behind flag 2020-01-15 10:29:36 -08:00
sydneyli
0e78436b05 [Apache v2] Add apacheconfig as a dependency (#7643)
* Add apacheconfig as a dependency.

* Change apacheconfig to a dev dependency

* Bump apacheconfig dep to 0.3.1
2020-01-07 09:57:43 -08:00
ohemorange
9b5b27597c Merge pull request #7618 from certbot/ap2_merge_master
[Apache v2] Merge changes from master to apache-parser-v2
2020-01-06 17:54:29 -08:00
Joona Hoikkala
3b065238b3 Modifications needed for merging to master 2020-01-06 17:19:33 +02:00
Joona Hoikkala
0f5bda4ff9 Merge remote-tracking branch 'origin/master' into ap2_merge_master 2020-01-06 17:17:31 +02:00
Joona Hoikkala
70be256c66 Fix gating to ensure that no parsernode functionality is run unless explicitly requested (#7654) 2020-01-02 15:44:11 -08:00
Adrien Ferrand
fda655370a Update CHANGELOG.md (#7659) 2020-01-02 23:44:16 +01:00
Adrien Ferrand
887d72fd5d Remove POST-as-GET fallback to GET (#6994) 2020-01-02 12:48:55 -08:00
Brad Warren
6d527bcc42 Include header files for compilation. (#7650) 2019-12-19 14:02:24 -08:00
Barbz
6ca80b7ce8 How to uninstall certbot-auto (#7648) 2019-12-19 13:30:13 -08:00
Joona Hoikkala
4401eacaac Implement get_virtual_hosts() for ParserNode interfaces (#7564) 2019-12-19 10:51:41 +02:00
Brad Warren
f520d482fd Remove other 3.8-dev references. (#7646) 2019-12-18 23:00:49 +01:00
Adrien Ferrand
b5a31bec03 Add docker-compose as a requirement of certbot-ci (#7120)
Fixes #7110 

This PR declares docker-compose as a requirement for certbot-ci. This way, a recent version of docker-compose is installed in the standard virtual environment set up by `tools/venv.py` and `tools/venv3.py`, and so is available to pytest integration tests from `tox` or in the virtual environment enabled.

* Add docker-compose as a dev dependency and declares it in certbot-ci requirements

* Update docker-compose 1.25.0
2019-12-18 13:21:54 -08:00
Brad Warren
6ac7aabaf7 Remove warning about dev preview (#7640) 2019-12-18 11:14:58 -08:00
Brad Warren
24fdea5fd8 discourage dns plugins (#7639) 2019-12-18 11:13:57 -08:00
Adrien Ferrand
4a906484ee Execute Windows installer integration tests on several Windows versions (#7641)
This PRs extends the installer tests on Azure Pipeline, in order to run the integration tests on a certbot instance installed with the Windows installer for several Windows versions, corresponding to the scope of supported versions on Certbot:
* Windows Server 2012 R2
* Windows Server 2016
* Windows Server 2019

One can see the result on: https://dev.azure.com/adferrand/certbot/_build/results?buildId=311

* Try specific installer-build step

* Install Python manually

* Add tests on windows 2019
2019-12-16 16:03:39 -08:00
Adrien Ferrand
9e5bca4bbf Lint certbot code on Python 3, and update Pylint to the latest version (#7551)
Part of #7550

This PR makes appropriate corrections to run pylint on Python 3.

Why not keeping the dependencies unchanged and just run pylint on Python 3?
Because the old version of pylint breaks horribly on Python 3 because of unsupported version of astroid.

Why updating pylint + astroid to the latest version ?
Because this version only fixes some internal errors occuring during the lint of Certbot code, and is also ready to run gracefully on Python 3.8.

Why upgrading mypy ?
Because the old version does not support the new version of astroid required to run pylint correctly.

Why not upgrading mypy to its latest version ?
Because this latest version includes a new typshed version, that adds a lot of new type definitions, and brings dozens of new errors on the Certbot codebase. I would like to fix that in a future PR.

That said so, the work has been to find the correct set of new dependency versions, then configure pylint for sane configuration errors in our situation, disable irrelevant lintings errors, then fixing (or ignoring for good reason) the remaining mypy errors.

I also made PyLint and MyPy checks run correctly on Windows.

* Start configuration

* Reconfigure travis

* Suspend a check specific to python 3. Start fixing code.

* Repair call_args

* Fix return + elif lints

* Reconfigure development to run mainly on python3

* Remove incompatible Python 3.4 jobs

* Suspend pylint in some assertions

* Remove pylint in dev

* Take first mypy that supports typed-ast>=1.4.0 to limit the migration path

* Various return + else lint errors

* Find a set of deps that is working with current mypy version

* Update local oldest requirements

* Remove all current pylint errors

* Rebuild letsencrypt-auto

* Update mypy to fix pylint with new astroid version, and fix mypy issues

* Explain type: ignore

* Reconfigure tox, fix none path

* Simplify pinning

* Remove useless directive

* Remove debugging code

* Remove continue

* Update requirements

* Disable unsubscriptable-object check

* Disable one check, enabling two more

* Plug certbot dev version for oldest requirements

* Remove useless disable directives

* Remove useless no-member disable

* Remove no-else-* checks. Use elif in symetric branches.

* Add back assertion

* Add new line

* Remove unused pylint disable

* Remove other pylint disable
2019-12-10 14:12:50 -08:00
Joona Hoikkala
5c588a6f8d [Apache v2] Implement parsed_files (#7562)
* Implement parsed_files

* Add parsed_files stub to ApacheParserNodes and fix assertions

* Update certbot-apache/certbot_apache/interfaces.py

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

* Add more descriptive comments

* Update certbot-apache/certbot_apache/augeasparser.py

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

* Update certbot-apache/certbot_apache/dualparser.py

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

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: ohemorange <ebportnoy@gmail.com>
2019-12-10 13:20:00 -05:00
Adrien Ferrand
e048da1e38 Reorganize imports (#7616)
* Isort execution

* Fix pylint, adapt coverage

* New isort

* Fix magic_typing lint

* Second round

* Fix pylint

* Third round. Store isort configuration

* Fix latest mistakes

* Other fixes

* Add newline

* Fix lint errors
2019-12-09 15:50:20 -05:00
Brad Warren
34b568f366 Don't list adding type annotations as a PR req. (#7627) 2019-12-04 20:22:10 +01:00
ohemorange
b99bfe8ab7 Merge pull request #7622 from certbot/candidate-1.0.0
Release 1.0
2019-12-04 14:15:49 -05:00
Brad Warren
5da61564d9 Don't list DNS plugins as alpha quality. (#7624)
They should be considered production quality like our other packaged code.
2019-12-03 19:56:16 -08:00
Brad Warren
b45f79d0ab fix bad links in docs (#7623)
This PR fixes the failures at https://travis-ci.com/certbot/website/builds/139193502#L1316.

Once this PR lands, I'll update certbot/website#508 to include this commit.
2019-12-03 11:05:23 -08:00
Brad Warren
3cfa63483d Add full API documentation (#7614)
A lot of Certbot's files don't have API documentation which is fixed by this PR. To do this, from the top level certbot directory I ran:
```
sphinx-apidoc -Me -o docs/api certbot
```
I then merged the resulting `modules.rst` file with `docs/api.rst`.
2019-12-03 09:54:37 -08:00
Brad Warren
27d6f62a96 update external plugin (#7604)
The old plugin at https://github.com/marcan/certbot-external says it's obsolete and points people to https://github.com/EnigmaBridge/certbot-external-auth. The new plugin is also an installer.

I also removed the reference to #2782 about us adding similar functionality since that's been done for a long time. We could reference our manual plugin instead, but I think that devalues their plugin a bit which I don't think is necessary or correct as it has different features.
2019-12-03 09:52:05 -08:00
Brad Warren
e32033f1ec document main (#7610)
I deleted the exceptions because I think it's not feasible to document the possible exceptions raised by all of Certbot.
2019-12-03 09:51:43 -08:00
Brad Warren
d2bad803f3 Bump version to 1.1.0 2019-12-03 09:27:30 -08:00
Brad Warren
5debf7af7e Add contents to certbot/CHANGELOG.md for next version 2019-12-03 09:27:30 -08:00
Brad Warren
6102cc440b Release 1.0.0 2019-12-03 09:27:28 -08:00
Brad Warren
bc80195a58 Update changelog for 1.0.0 release 2019-12-03 09:20:30 -08:00
Felix Schwarz
2008e3cc77 acme/setup.py: comment refers to "PyOpenSSL" not "mock" (#7619) 2019-12-03 01:16:41 +01:00
Joona Hoikkala
6148e5c355 [Apache v2] Move the apachectl parsing to apache_util (#7569)
* Move the Apache CLI parsing to apache_util

* Fix test mocks

* Address review comments

* Fix the parsernode metadata dictionary
2019-12-02 18:00:53 -05:00
Adrien Ferrand
4c652b9c82 Upgrade to pywin32>=227 (#7615)
Current version of pywin32 used in certbot (225) does not have wheels available for Python 3.8. Installing certbot for development in this case requires to build from source. On Windows, this implies a Visual Studio C++ environment up and ready, which is absolutely not fun.

Let's upgrade to pywin32 227, that provides these wheels for all Python versions from 3.5 up to current dev status of 3.9.
2019-12-02 13:39:31 -08:00
Joona Hoikkala
06fdbf2a55 [Apache v2] Implement find_ancestors (#7561)
* Implement find_ancestors

* Create the node properly and add assertions

* Update certbot-apache/certbot_apache/augeasparser.py

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

* Remove comment
2019-12-02 16:25:39 +02:00
Brad Warren
84b770b56e Defines the RenewableCert API (#7603)
This is my proposed fix for #7540. I would ideally like this to be included in our 1.0 release.

I came up with this design by adding all attributes used either in our own plugins, 3rd party plugins listed at https://certbot.eff.org/docs/using.html#third-party-plugins, or our public API code.

Despite me thinking that zope is unneeded nowadays, I initially tried to use it to define this interface since we have it and it gives us a way to define expected attributes, but it doesn't work because zope interface objects also have a method called `names` which conflict with the API.

I talked about this with Adrien out of band and did some of my own research and there are some minor benefits with this new approach of using properties:

1. It's more conventional.
2. If you also change the implementation to inherit from the class, Python will error if all properties aren't defined.
3. The PEP 526 style type annotations with mypy seem to (currently) only be used to validate code using the class, not the class implementation itself. You can add a type annotation saying the class needs to have this attribute, never define it, and mypy won't complain.

With this new approach, I had to fix `names` because pylint was complaining that the arguments differed, however, we never used the optional parameter to `names` outside of tests so I just deleted the code altogether.

* fixes #7540

* move to properties
2019-11-27 11:32:00 -08:00
ohemorange
6c1dfe43c7 Refactor tests out of packaged module for apache plugin (#7607)
Part of #7593.

* Refactor tests out of packaged module for apache plugin

* Exclude pycache and .py[cod]

* Change tests path in tox.ini
2019-11-27 09:57:35 -08:00
ohemorange
a8e711d281 Refactor tests out of packaged module for nginx plugin (#7606)
* Refactor tests out of packaged module for nginx plugin

* Exclude pycache and .py[cod]
2019-11-26 17:45:18 -08:00
ohemorange
f36b93267c Exclude pycache and .py[cod] from certbot package (#7608) 2019-11-26 17:45:07 -08:00
ohemorange
d2b65b47f2 Refactor tests out of packaged module for acme plugin (#7600)
* Move acme tests to tests/ directory outside of acme module

* Fix call to messages_test in client_test

* Move test_util.py and testdata/ into tests/

* Update manifest to package tests

* Exclude pycache and .py[cod]
2019-11-26 15:25:41 -08:00
ohemorange
b624172f68 Refactor tests out of packaged module for dns plugins (#7599)
* Refactor tests out of module for certbot-dns-cloudflare

* Refactor tests out of module for certbot-dns-cloudxns

* Refactor tests out of module for certbot-dns-digitalocean

* Refactor tests out of module for certbot-dns-dnsimple

* Refactor tests out of module for certbot-dns-dnsmadeeasy

* Refactor tests out of module for certbot-dns-gehirn

* Refactor tests out of module for certbot-dns-google

* Refactor tests out of module for certbot-dns-linode

* Refactor tests out of module for certbot-dns-luadns

* Refactor tests out of module for certbot-dns-nsone

* Refactor tests out of module for certbot-dns-ovh

* Refactor tests out of module for certbot-dns-rfc2136

* Refactor tests out of module for certbot-dns-sakuracloud

* Refactor tests out of module for certbot-dns-route53

* Move certbot-dns-google testdata/ under tests/

* Use pytest for dns plugins

* Exclude pycache and .py[cod]
2019-11-26 15:25:28 -08:00
ohemorange
6d1472bf8c Implement redirect by default (#7595)
* Change redirect default to yes so that it happens automatically in noninteractive mode

* Update changelog
2019-11-25 18:53:20 -08:00
ohemorange
5c8083851a Fix refactor (#7597)
Clean up some places missed by #7544.

Found this when running test farm tests. They were working as of 5d90544, and I will truly shocked if subsequent changes (all to the windows installer) made them stop working.

* Release script needs to target new CHANGELOG location

* Clean up various other CHANGELOG path references

* Update windows paths for new certbot location

* Add certbot to packages list for windows installer
2019-11-25 18:24:20 -08:00
ohemorange
345bdb46e0 Update pull_request_template.md (#7596)
* Update pull_request_template.md

* Remove line breaks

Github seems to be keeping the line breaks rather than ignoring them, making it be formatted weirdly, so remove them.
2019-11-25 15:42:01 -08:00
ohemorange
e023f889ff Make the contents of the nginx plugin private (#7589)
Part of #5775.

* Create _internal folder certbot-nginx

* Move configurator.py to _internal

* Move constants.py to _internal

* Move display_ops.py to _internal

* Move http_01.py to _internal

* Move nginxparser.py to _internal

* Move obj.py to _internal

* Move parser_obj.py to _internal

* Move parser.py to _internal

* Update location and references for tls_configs

* exclude parser_obj from coverage
2019-11-25 14:30:24 -08:00
ohemorange
4abd81e218 Refactor certbot/ and certbot/tests/ to use the same structure as the other packages (#7544)
Summary of changes in this PR:
- Refactor files involved in the `certbot` module to be of a similar structure to every other package; that is, inside a directory inside the main repo root (see below).
- Make repo root README symlink to `certbot` README.
- Pull tests outside of the distributed module.
- Make `certbot/tests` not be a module so that `certbot` isn't added to Python's path for module discovery.
- Remove `--pyargs` from test calls, and make sure to call tests from repo root since without `--pyargs`, `pytest` takes directory names rather than package names as arguments.
- Replace mentions of `.` with `certbot` when referring to packages to install, usually editably.
- Clean up some unused code around executing tests in a different directory.
- Create public shim around main and make that the entry point.

New directory structure summary:
```
repo root ("certbot", probably, but for clarity all files I mention are relative to here)
├── certbot
│   ├── setup.py
│   ├── certbot
│   │   ├── __init__.py
│   │   ├── achallenges.py
│   │   ├── _internal
│   │   │   ├── __init__.py
│   │   │   ├── account.py
│   │   │   ├── ...
│   │   ├── ...
│   ├── tests
│   │   ├── account_test.py
│   │   ├── display
│   │   │   ├── __init__.py
│   │   │   ├── ...
│   │   ├── ... # note no __init__.py at this level
│   ├── ...
├── acme
│   ├── ...
├── certbot-apache
│   ├── ...
├── ...
```

* refactor certbot/ and certbot/tests/ to use the same structure as the other packages

* git grep -lE "\-e(\s+)\." | xargs sed -i -E "s/\-e(\s+)\./-e certbot/g"

* git grep -lE "\.\[dev\]" | xargs sed -i -E "s/\.\[dev\]/certbot[dev]/g"

* git grep -lE "\.\[dev3\]" | xargs sed -i -E "s/\.\[dev3\]/certbot[dev3]/g"

* Remove replacement of certbot into . in install_and_test.py

* copy license back out to main folder

* remove linter_plugin.py and CONTRIBUTING.md from certbot/MANIFEST.in because these files are not under certbot/

* Move README back into main folder, and make the version inside certbot/ a symlink

* symlink certbot READMEs the other way around

* move testdata into the public api certbot zone

* update source_paths in tox.ini to certbot/certbot to find the right subfolder for tests

* certbot version has been bumped down a directory level

* make certbot tests directory not a package and import sibling as module

* Remove unused script cruft

* change . to certbot in test_sdists

* remove outdated comment referencing a command that doesn't work

* Install instructions should reference an existing file

* update file paths in Dockerfile

* some package named in tox.ini were manually specified, change those to certbot

* new directory format doesn't work easily with pyargs according to http://doc.pytest.org/en/latest/goodpractices.html#tests-as-part-of-application-code

* remove other instance of pyargs

* fix up some references in _release.sh by searching for ' . ' and manual check

* another stray . in tox.ini

* fix paths in tools/_release.sh

* Remove final --pyargs call, and now-unnecessary call to modules instead of local files, since that's fixed by certbot's code being one layer deeper

* Create public shim around main and make that the entry point

* without pyargs, tests cannot be run from an empty directory

* Remove cruft for running certbot directly from main

* Have main shim take real arg

* add docs/api file for main, and fix up main comment

* Update certbot/docs/install.rst

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

* Fix comments in readthedocs requirements files to refer to current package

* Update .[docs] reference in contributing.rst

* Move plugins tests to certbot tests directory

* add certbot tests to MANIFEST.in so packagers can run python setup.py test

* move examples directory inside certbot/

* Move CHANGELOG into certbot, and create a top-level symlink

* Remove unused sys and logging from main shim

* nginx http01 test no longer relies on certbot plugins common test
2019-11-25 14:28:05 -08:00
ohemorange
d56cd4ef01 Make the contents of the DNS plugins private (#7580)
Part of #5775.

```
modify_item () {
    mkdir certbot-dns-$1/certbot_dns_$1/_internal
    git grep -l "from certbot_dns_$1 import dns_$1" | xargs sed -i "s/from certbot_dns_$1 import dns_$1/from certbot_dns_$1._internal import dns_$1/g"
    git grep -l "certbot_dns_$1\.dns_$1" | xargs sed -i "s/certbot_dns_$1\.dns_$1/certbot_dns_$1._internal.dns_$1/g"
    git checkout -- certbot-dns-$1/certbot_dns_$1/__init__.py
    echo '"""Internal implementation of \`~certbot_dns_$1.dns_$1\` plugin."""' > certbot-dns-$1/certbot_dns_$1/_internal/__init__.py
    mv certbot-dns-$1/certbot_dns_$1/dns_$1.py certbot-dns-$1/certbot_dns_$1/_internal
    git checkout -- CHANGELOG.md
    git status
    git add -A
    git commit -m "Move certbot-dns-$1 to _internal structure"
}
```

Structure now looks like this:
```
certbot-dns-cloudflare/
├── certbot_dns_cloudflare
│   ├── dns_cloudflare_test.py
│   ├── __init__.py
│   └── _internal
│       ├── dns_cloudflare.py
│       └── __init__.py
```

* Move certbot-dns-cloudflare to _internal structure

* Move certbot-dns-cloudxns to _internal structure

* Move certbot-dns-digitalocean to _internal structure

* Move certbot-dns-dnsimple to _internal structure

* Move certbot-dns-dnsmadeeasy to _internal structure

* Move certbot-dns-gehirn to _internal structure

* Move certbot-dns-google to _internal structure

* Move certbot-dns-linode to _internal structure

* Move certbot-dns-luadns to _internal structure

* Move certbot-dns-nsone to _internal structure

* Move certbot-dns-ovh to _internal structure

* Move certbot-dns-rfc2136 to _internal structure

* Move certbot-dns-sakuracloud to _internal structure

* Init file comments need to be comments

* Move certbot-dns-route53 to _internal structure

* Fix comment in route53 init
2019-11-25 10:26:05 -08:00
ohemorange
8139689d4c Make the contents of the apache plugin private (#7579)
Part of #5775.

Tree:
```
certbot-apache/certbot_apache
├── __init__.py
├── _internal
│   ├── apache_util.py
│   ├── augeas_lens
│   │   ├── httpd.aug
│   │   └── README
│   ├── centos-options-ssl-apache.conf
│   ├── configurator.py
│   ├── constants.py
│   ├── display_ops.py
│   ├── entrypoint.py
│   ├── http_01.py
│   ├── __init__.py
│   ├── obj.py
│   ├── options-ssl-apache.conf
│   ├── override_arch.py
│   ├── override_centos.py
│   ├── override_darwin.py
│   ├── override_debian.py
│   ├── override_fedora.py
│   ├── override_gentoo.py
│   ├── override_suse.py
│   └── parser.py
└── tests
    ├── ...
```

* Create _internal folder for certbot_apache

* Move apache_util.py to _internal

* Move display_ops.py to _internal

* Move override_centos.py to _internal

* Move override_gentoo.py to _internal

* Move override_darwin.py to _internal

* Move override_suse.py to _internal

* Move override_debian.py to _internal

* Move override_fedora.py to _internal

* Move override_arch.py to _internal

* Move parser.py to _internal

* Move obj.py to _internal

* Move http_01.py to _internal

* Move entrypoint.py to _internal

* Move constants.py to _internal

* Move configurator.py to _internal

* Move augeas_lens to _internal

* Move options-ssl-apache.conf files to _internal

* move augeas_lens in MANIFEST

* Clean up some stray references to certbot_apache that could use _internal

* Correct imports and lint
2019-11-25 09:44:40 -08:00
ohemorange
a27b1137a5 Remove unused nginx docs (#7576)
Part of #5775. We don't use these docs anywhere, so delete them.

Removes:
- `certbot-nginx/readthedocs.org.requirements.txt`
- `certbot-nginx/docs/` folder
- docs include in `MANIFEST.in`
- docs dependencies in `setup.py`

* Remove unused nginx docs

* Add changelog entry about the removal
2019-11-25 09:18:12 -08:00
Brad Warren
5809aa6a2c remove unused route53 tools (#7586) 2019-11-22 22:24:51 +01:00
ohemorange
d8ca555eed Remove DNS plugin API docs. (#7578)
Replace DNS plugins' API documentation with a note that plugins adhere to certbot's plugin interface.
2019-11-22 12:58:06 -08:00
ohemorange
bd35e71b5c Remove unused certbot-compatibility-test docs (#7577)
Part of #5775. We don't use these docs anywhere, so delete them.

Removes:
- `certbot-compatibility-test/readthedocs.org.requirements.txt`
- `certbot-compatibility-test/docs/` folder
- docs include in `MANIFEST.in`
- docs dependencies in `setup.py`
2019-11-22 12:54:18 -08:00
ohemorange
70e4cb7853 Remove unused apache docs (#7575)
Part of #5775. We don't use these docs anywhere, so delete them.

Removes:
- `certbot-apache/readthedocs.org.requirements.txt`
- `certbot-apache/docs/` folder
- docs include in `MANIFEST.in`
- docs dependencies in `setup.py`
2019-11-22 12:50:01 -08:00
Joona Hoikkala
ac1a60ff0b Implement add_child_comment (#7518) 2019-11-18 21:21:51 +02:00
sydneyli
b70f9c4744 [Apache v2] Initial ApacheParser skeleton (#7559)
* Fix metadata & primary references in Augeas tests.

When performing actions only on one of the trees in DualNodeParser, the two
trees get out-of-sync. Similarly, we can't expect that the metadata between
the two trees will remain the same.

Did a pass over the tests to re-wire metadata and primary usage.

* Add ApacheParser skeleton.

Fix plumbing in configurator & dualparser to initialize ApacheParser
alongside AugeasParser.

* Silence coverage reports for now
2019-11-15 12:22:18 +02:00
Brad Warren
4f3010ef3f fixes #7553 (#7560) 2019-11-14 14:26:01 -08:00
Brad Warren
2692b862d2 Update pinned dependencies (#7558)
Fixes #7184.

I updated #7358 to track the issue of unpinning all of these dependencies.

* pin back configargparse

* Pin back zope packages.

* update deps

* Add changelog entry.

* run build.py
2019-11-13 13:44:36 -08:00
ohemorange
4d4c83d4d8 Internalize modules called by internal plugins (#7543)
* Move hooks.py to _internal

* Move cli.py to _internal
2019-11-13 11:14:46 -08:00
Brad Warren
57148b7593 Fix shebang in rebuild_deps (#7557)
When you try to run this script, it crashes with:
```
standard_init_linux.go:211: exec user process caused "exec format error"
```
This is caused by the script being written to have the contents:
```
\
#!/bin/sh
set -e
...
```
This fixes the problem by removing the slash and moving the shebang to the first line of the string.
2019-11-13 11:14:26 -08:00
Amjad Mashaal
4a8ede2562 Deprecate certbot register --update-registration (#7556)
Closes #7452.
2019-11-13 10:24:37 -08:00
ohemorange
46d5f7a860 Move configuration.py to _internal (#7542)
Part of #5775. Methodology similar to #7528. Also refactors NGINX test util to use certbot.tests.util.ConfigTestCase.

* refactor nginx tests to no longer rely on certbot.configuration internals

* Move configuration.py to _internal
2019-11-13 10:19:27 -08:00
Adrien Ferrand
595b1b212e [Windows] Avoid letsencrypt.log permissions error during scheduled certbot renew task (#7537)
While coding for #7536, I ran into another issue. It appears that Certbot logs generated during the scheduled task execution have wrong permissions that make them almost unusable: they do not have an owner, and their ACL contains nonsense values (non existant accounts name).

The class `logging.handler.RotatingFileHandler` is responsible for these logs, and become mad when it is in a Python process run under a scheduled task owned by `SYSTEM`. This is precisely our case here.

This PR avoids (but not fix) the issue, by changing the owner of the scheduled task from `SYSTEM` to the `Administrators` group, that appears to work fine.

* Use Administrators group instead of SYSTEM to run the certbot renew task
2019-11-13 10:04:45 -08:00
Adrien Ferrand
75acdeb645 [Windows] Fix certbot renew task failure under NT AUTHORITY\SYSTEM account (#7536)
Turned out that the scheduled task that runs `certbot renew` twice a day, is failing. Without any kind of log of course, otherwise it would not be fun.

It can be revealed by opening a powershell under the `NT AUTHORITY\SYSTEM` account, under which the scheduled task is run. Under theses circumstances, the bug is revealed: Certbot breaks when trying to invoke `certbot.compat.filesystem._get_current_user()`. Indeed the logic there implied to call `win32api.GetUserNameEx(win32api.NameSamCompatible)` and this function does not return always a useful value.

For normal account, it will be typically `DOMAIN_OR_MACHINE_NAME\YOUR_USER_NAME` (e.g. `My Machine\Adrien Ferrand`). But for the account `NT AUTHORITY\SYSTEM`, it will return `MACHINE_NAME\DOMAIN$`, which is a nonsense and makes fail the resolution of the actual SID of the account at the end of `_get_current_user()`.

This PR fixes this behavior by using an explicit construction of the account name that works both for normal users and `SYSTEM`.

* Use a different way to resolve current user account, that works both for normal users and SYSTEM.

* Add a comment to run Certbot under NT AUTHORITY\SYSTEM
2019-11-13 09:43:50 -08:00
Joona Hoikkala
517ff5cb19 [Apache v2] Implement delete_child() (#7521)
* Implement delete_child

* Fix linter
2019-11-12 14:19:35 -08:00
Joona Hoikkala
d14eec9ecf [Apache v2] Implement save() and unsaved_files() (#7520)
* Implement save() and unsaved_files()

* Linter fix
2019-11-12 14:19:21 -08:00
Joona Hoikkala
bdf24d2bed Implement add_child_directive (#7517) 2019-11-12 14:19:02 -08:00
Adrien Ferrand
c26d459d0f Remove python2 and certbot-auto references in how to set up a Certbot build environment. (#7549)
Fixes #7548.

This PR udpdates installation instructions to get rid of python2 and certbot-auto in the how-to explaining the Certbot development environment setup.

Instead, Python 3 is used, and appropriate instructions for APT and RPM based distributions are provided.
2019-11-12 13:52:44 -08:00
ohemorange
4792e1ee21 Move constants.py to _internal (#7534)
* Don't call core constants from nginx plugin

* Move constants.py to _internal/

* Move ENHANCEMENTS from now-internal constants to public plugins.enhancements

* Update display.enhancements.ask from its 2015 comment
2019-11-11 15:41:40 -08:00
ohemorange
08c1de34bd Move items in certbot/plugins to _internal (#7533)
* Create and initialize _internal/plugins

* Move plugins/manual.py to _internal/

* Move plugins/disco.py to _internal/

* Move plugins/selection.py to _internal/

* Move plugins/webroot.py to _internal/

* Move plugins/null.py to _internal/

* Move plugins/standalone.py to _internal/

* add missed internalization

* shorten line

* Update outdated init comment
2019-11-11 15:14:18 -08:00
Brad Warren
641b60b8f0 Remove TLS-SNI objects in ACME (#7535)
* fixes #7214

* update changelog

* remove unused import
2019-11-11 23:04:26 +01:00
Joona Hoikkala
578ca1c6af [Apache v2] Adding nodes 1/3 : add_child_block() (#7497)
* Implement add_child_block()

* Add comments and example

* Check augas path inconsistencies in initialization
2019-11-11 11:33:14 -08:00
ohemorange
d290fe464e Move eff.py to _internal (#7530)
* Move eff.py to _internal

* missed a few certbot.effs in tests

* remove sublime autocompletion

* fix messy scripting
2019-11-11 10:20:05 -08:00
ohemorange
e38aa65cae Move items in certbot/display to _internal (#7532)
* Move display/completer.py to _internal/

* Move display/dummy_readline.py to _internal/

* Move display/enhancements.py to _internal/

* Create __init__.py in _internal/display
2019-11-11 10:19:28 -08:00
ohemorange
8fb9e9adde Move log.py to _internal (#7531)
Part of #5775. Methodology similar to #7528, but slightly more manual.
2019-11-11 10:17:16 -08:00
ohemorange
96e02d614b Make uncomplicated modules private (#7528)
* Create _internal package for Certbot's non-public modules

* Move account.py to _internal

* Move auth_handler.py to _internal

* Move cert_manager.py to _internal

* Move client.py to _internal

* Move error_handler.py to _internal

* Move lock.py to _internal

* Move main.py to _internal

* Move notify.py to _internal

* Move ocsp.py to _internal

* Move renewal.py to _internal

* Move reporter.py to _internal

* Move storage.py to _internal

* Move updater.py to _internal

* update apache and nginx oldest requirements

* Keep the lock file as certbot.lock

* nginx oldest tests still need to rely on newer certbot

* python doesn't have good dependency resolution, so specify the transitive dependency

* update required minimum versions in nginx setup.py
2019-11-08 16:19:21 -08:00
Brad Warren
0a48d7bf7e remove get_systemd_os_info (#7526)
Fixes #7500.
2019-11-08 11:11:03 -08:00
Brad Warren
4b488614cf Remove tls sni common (#7527)
* fixes #7478

* add changelog entry
2019-11-08 15:11:09 +01:00
Shell Chen
f4f16605ed dns-rfc2136: use TCP to query SOA records (#7503)
* Use tcp query on dns-rfc2136 plugin

To improve network robust; fixes #7502.

* Update CHANGELOG.md

* Fix dns-rfc2136 test cases

* Add UDP fallback to dns-rfc2136
2019-11-07 18:37:11 +01:00
ohemorange
b84edfd39a Merge pull request #7515 from certbot/candidate-0.40.1
Candidate 0.40.1
2019-11-06 12:01:59 -08:00
ohemorange
88d9a31cf9 Merge branch 'master' into candidate-0.40.1 2019-11-06 11:47:28 -08:00
Amjad Mashaal
1dff022d05 Deprecate config_changes (#7469)
Closes #7454

* Deprecate config_changes

* Error on config_changes

* Fix tests for main.py

* Fix CHANGELOG entry

* Remove remnants of config_changes

* Fix CHANGELOG and add removed functions
2019-11-06 11:29:07 -08:00
Brad Warren
2b4c2a7f55 Match our Travis logic in Azure. (#7514)
In Travis, the full test suite doesn't run on PRs for point release branches, just on commits for them. I think this behavior makes sense because what we actually want to test before a point release is the exact commit we want to release after any squashing/merging has been done. This PR modifies Azure to match this behavior.

After this PR lands, I need to update the tests required to pass on GitHub.
2019-11-06 10:45:17 -08:00
Joona Hoikkala
19de05c72f [Apache v2] Implement set_parameters() (#7461)
* find_comments implementation and AugeasCommentNode creation

* set_parameters implementation

* Change parameters to a property

* Remove parameters property setter

* More pythonic iteration handling
2019-11-06 10:33:24 -08:00
Adrien Ferrand
baf43a2dbc Pin all build dependencies for the Windows installer (#7504)
This PR uses pipstrap to bootstrap the venv used to build Windows installers. This effectively pin all build dependencies, since pynsist is already installed through pip_install.py script.

* Use pipstrap

* Pin also NSIS version
2019-11-06 10:17:53 -08:00
Brad Warren
ebce0adb5a Merge branch 'master' into candidate-0.40.1 2019-11-05 19:49:27 -08:00
Erica Portnoy
61f77c35c0 Bump version to 1.0.0 2019-11-05 18:32:22 -08:00
Erica Portnoy
1b76faada6 Add contents to CHANGELOG.md for next version 2019-11-05 18:32:22 -08:00
Erica Portnoy
b79bcd0bf2 Release 0.40.1 2019-11-05 18:32:20 -08:00
Erica Portnoy
5f6ab47a7b Update changelog for 0.40.1 release 2019-11-05 18:24:52 -08:00
Brad Warren
d87c905c06 Add back Python 3.4 support (#7510) (#7511)
* Revert "Deprecation warnings for Python 3.4 (#7378)"

This reverts commit 6fcdfb0e50.

* Revert "Migrate certbot-auto users on CentOS 6 to Python 3.6 (#7268)"

This reverts commit e19b2e04c7.

* add changelog entry

* keep mona in authors

(cherry picked from commit 9b848b1d65)
2019-11-05 17:11:23 -08:00
Brad Warren
9b848b1d65 Add back Python 3.4 support (#7510)
* Revert "Deprecation warnings for Python 3.4 (#7378)"

This reverts commit 6fcdfb0e50.

* Revert "Migrate certbot-auto users on CentOS 6 to Python 3.6 (#7268)"

This reverts commit e19b2e04c7.

* add changelog entry

* keep mona in authors
2019-11-05 16:45:08 -08:00
Brad Warren
f555e4bf1f Merge pull request #7505 from certbot/candidate-0.40.0
Release 0.40.0
2019-11-05 13:55:40 -08:00
Erica Portnoy
0de2645a8f Bump version to 0.41.0 2019-11-05 12:53:16 -08:00
Erica Portnoy
fcecdfbcc5 Add contents to CHANGELOG.md for next version 2019-11-05 12:53:16 -08:00
Erica Portnoy
73cd5aa81c Release 0.40.0 2019-11-05 12:52:26 -08:00
Erica Portnoy
3d9d212040 Update changelog for 0.40.0 release 2019-11-05 12:35:33 -08:00
Brad Warren
78deca4f60 Don't use --agree-dev-preview in tests. (#7501) 2019-11-05 17:34:46 +01:00
Adrien Ferrand
3c24ff88cc Build Windows installers with pinned dependencies (#7498)
* Consume constraints file

* Independent pywin32 dependency definition in setup.py and construct.py
2019-11-04 14:20:42 -08:00
alexzorin
08d91b456b Use fresh authorizations in dry runs (#7442)
* acme: re-populate uri in deactivate_authorization

* Use fresh authorizations in dry runs

--dry-run now deactivates 'valid' authorizations if it encounters them
when creating a new order.

Resolves #5116.

* remove unused code

* typo in local-oldest-requirements

* better error handling

* certbot-ci: AUTHREUSE to 100 + unskip dry-run test

* improve test coverage for error cases

* restore newline to local-oldest-requirements.txt
2019-11-04 21:23:25 +01:00
Brandon Moore
1c05b9bd07 Dropped deprecated flags from commands (#7482)
This pull request addresses #7451 by removing the deprecated flags.

* Dropped deprecated flags from commands

* Updated changelog for dropped flags and deleted outdated tests

* removed init-script part of apache test
2019-11-04 09:50:57 -08:00
Brad Warren
fffa74edb2 Clarify when the changelog should be modified (#7491) 2019-11-01 16:40:31 -07:00
Brad Warren
8956de6bee Describe distributed Certbot components. (#7493) 2019-11-01 15:05:41 -07:00
Brad Warren
9bc4286a27 Deprecate more code related to TLS-SNI-01 (#7483)
I tried to finish up #7214 by removing the code in acme but we can't really do that until #7478 is resolved which we cannot do until we release 0.40.0.

Since we have to wait, this PR adds deprecation warnings for code that uses the TLS-SNI-01 code or was only used by the long deprecated TLS-SNI-01 code.

I'd like this PR to land before our next release.

* Deprecate more code related to TLS-SNI-01.

* Assert about warning message.
2019-11-01 15:00:22 -07:00
Brad Warren
3e848b8fce Remove changelog entry about unpackaged scripts. (#7490)
We don't package rebuild_dependencies.py so I don't think we need to mention changes to it in our changelog which is primarily read by users and packagers.
2019-11-01 13:59:17 -07:00
Joona Hoikkala
fb1aafb5d2 Use distro library for all OS version detection (#7467)
This pull request ensures that we use distro package in all the distribution version detection. It also replaces the custom systemd /etc/os-release parsing and adds a few version fingerprints to Apache override selection.

Fixes: #7405

* Revert "Try to use platform.linux_distribution() before distro equivalent (#7403)"

This reverts commit ca3077d034.

* Use distro for all os detection code

* Address review comments

* Add changelog entry

* Added tests

* Fix tests to return a consistent os name

* Do not crash on non-linux systems

* Minor fixes to distro compatibility checks

* Make the tests OS independent

* Update certbot/util.py

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

* Skip linux specific tests on other platforms

* Test fixes

* Better test state handling

* Lower the coverage target for Windows tests
2019-11-01 10:51:21 -07:00
Brad Warren
f8ff881d23 Don't use acme.test_util outside of acme. (#7484)
`certbot-compatibility-test` is using code in `acme` that I proposed making private and not trivially importable in https://github.com/certbot/certbot/issues/5775.

To fix it, I switched to using Certbot's test utilities which I proposed keeping public to help with writing tests for plugins. When doing this I had to change the name of the key because `rsa1024_key.pem` doesn't exist in Certbot.

I also deleted the keys in `certbot-compatibility-test`'s testdata because because they are unused.
2019-11-01 10:06:32 -07:00
Brad Warren
ef3f8888b5 Don't use dev version of 3.8. (#7485)
Now that Python 3.8 is out, we don't need to use the development version.
2019-11-01 10:06:10 -07:00
Mickaël Schoentgen
a45efcd40d Fix invalid escape sequence \. rebuild_dependencies.py (#7486)
Signed-off-by: Mickaël Schoentgen <contact@tiger-222.fr>
2019-11-01 17:27:18 +01:00
Brad Warren
63d673a3e0 Remove references to TLS-SNI-01 outside of ACME (#7479)
This is a big part of #7214. It removes all references to TLS-SNI-01 outside of acme (and pytest.ini). Those changes will come in a subsequent PR. I thought this one was getting big enough.

* Remove references to TLS-SNI-01 in Apache plugin

* Remove references to TLS-SNI-01 from certbot-nginx

* Remove references to TLS-SNI from Certbot.

* Remove TLS-SNI reference from docs

* add certbot changelog

* Clarify test behavior
2019-10-31 10:17:29 -07:00
Brad Warren
9796128fee Polish changelog (#7476)
I wanted to polish the changelog a bit. Changes made are:

* We don't ship our test farm tests so including info about them in our changelog seems unnecessary.
* I combined and expanded the info about the deprecation of Python 3.4.
2019-10-30 17:07:36 -07:00
Brad Warren
de6b56bec0 Deprecate certbot.plugins.common.TLSSNI01 (#7477)
While working on #7214, I noticed that certbot.plugins.common.TLSSNI01 wasn't printing a deprecation warning and it was still being used in our Apache plugin. This PR fixes that.
2019-10-30 15:19:38 -07:00
James Renken
6f711d9ae8 change random sleep to use fractional seconds (#7473)
If we use fractional instead of whole seconds for the random sleep before renewing, it will reduce bunching of requests to Let's Encrypt's API.
2019-10-30 12:06:30 -07:00
Joona Hoikkala
d645574839 [Apache v2] AugeasBlockNode find_comments() implementation (#7457)
* find_comments implementation and AugeasCommentNode creation

* Use dummy value for ancestor

* Add NotImplementedError when calling find_comments with exact parameter

* Remove parameter 'exact' from find_comments interface

* Fix comment
2019-10-30 11:03:23 -07:00
sydneyli
6fcdfb0e50 Deprecation warnings for Python 3.4 (#7378)
Fixes #7367

* Deprecation warnings for Python 3.4 users

* CHANGELOG.md and AUTHORS.md

* double equals typo
2019-10-30 10:57:46 -07:00
Adrien Ferrand
e19b2e04c7 Migrate certbot-auto users on CentOS 6 to Python 3.6 (#7268)
Fixes #7007

Python 3.4 is [EOL](https://www.python.org/dev/peps/pep-0429/), and only Python 3.x version available for CentOS 6 through EPEL is this version, and so is used by `certbot-auto`, the only official way to install Certbot on this platform.

This unpleasant situation becomes a little more uncomfortable, considering that the newest `pip` version (19.2) [just dropped Python 3.4 support](https://github.com/pypa/pip/issues/6685) and will refuse to start on this Python version. We can expect a lot of dependencies to follow this path now.

One direct result of this situation is that a fix to support correctly the ARM platforms requires to upgrade `pip` to 19.2 for `certbot-auto`. So this is not possible right now.

Then, let's upgrade Certbot instances on CentOS 6 to a supported version of Python 3.

This PR proposes a new bootstrap approach for CentOS 6 platform, `BootstrapRpmPython3Legacy`, that will install Python 3.6 from [SCL](https://www.softwarecollections.org) (the latest one available for now on CentOS 6). In term of Python 3 specific bootstrap methods, I take the occasion here to completely separate the bootstrap of CentOS 6 as a legacy system, from the RPM-based newest systems (like Fedora 29+) that are simply dropping support for Python 2.x. This is in prevision of future migration for all systems on Python 3.x, that is a different problematic than supporting old systems.

* Add logic

* Rebuilt letsencrypt-auto

* Fix logic

* Focus on specific packages

* Maintain PATH for further invocations of letsencrypt-auto after bootstrap.

* Various corrections

* Fix farm test for RHEL6

* Working centos6 letsencrypt-auto self tests

* Fix test_sdist for CentOS 6

* Corrections

* Work in progress

* Working configuration

* Fix typo

* Remove EPEL. Add a test.

* Update letsencrypt-auto-source/letsencrypt-auto.template

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

* Improvements after review

* Improvements

* Add a comment

* Add a test

* Update a test

* Corrections

* Update function return

* Work in progress

* Correct behavior on oracle linux 6.

* Corrections

* Rebuild script

* Add letsencrypt-auto tests for oraclelinux6

* Update tox.ini

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

* Update letsencrypt-auto-source/letsencrypt-auto

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

* Update letsencrypt-auto-source/tests/oraclelinux6_tests.sh

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

* Update letsencrypt-auto-source/letsencrypt-auto.template

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

* Update letsencrypt-auto-source/letsencrypt-auto

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

* Update letsencrypt-auto-source/letsencrypt-auto

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

* Update letsencrypt-auto-source/letsencrypt-auto.template

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

* Update letsencrypt-auto-source/tests/oraclelinux6_tests.sh

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

* Remove specific code for scientific linux

* Change some variables names

* Update letsencrypt-auto-source/tests/oraclelinux6_tests.sh

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

* Various corrections

* Fix tests

* Add a comment

* Update message

* Fix test message

* Update letsencrypt-auto-source/letsencrypt-auto.template

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

* Update letsencrypt-auto-source/letsencrypt-auto

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

* Update letsencrypt-auto-source/letsencrypt-auto

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

* Update scripts

* More focused assertion

* Add back a test

* Update script

* Update letsencrypt-auto-source/letsencrypt-auto.template

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

* Update letsencrypt-auto-source/letsencrypt-auto.template

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

* Check quiet mode

* Add changelog

* Update letsencrypt-auto-source/tests/oraclelinux6_tests.sh

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-10-30 10:39:45 -07:00
Adrien Ferrand
2dbe47f3a7 Create a release pipeline on Azure for Windows installer (#7441)
This PR creates a pipeline triggered on tag push matching v0.* (eg. v0.40.0).

Once triggered, this pipeline will build the windows installer, and run integration tests on it, like for the pipeline run nightly.

I also add a simple script to extract from CHANGELOG.md file to extract the relevant part to put it in the body of the GitHub release. I believe it makes things nicer.

* Create release pipeline

* Relax condition on tags

* Put beta keyword

* Update job name

* Fix release pipeline
2019-10-30 10:19:10 -07:00
Joona Hoikkala
9b2322a573 Use dummy values for ancestor (#7462) 2019-10-25 14:54:28 -07:00
Joona Hoikkala
79caaa8e6f Merge pull request #7466 from certbot/update-apache-parser-v2-v2
Update the apache-parser-v2 branch
2019-10-25 18:56:47 +03:00
Brad Warren
8620dcf06f Merge branch 'master' into apache-parser-v2 2019-10-24 09:59:53 -07:00
Brad Warren
0f31d9b7ac Remove skip_unless cruft (#7410)
* Remove skip_unless cruft.

* remove unused import
2019-10-24 14:46:55 +02:00
Brad Warren
60673e8a81 Remove AppVeyor. (#7440) 2019-10-24 12:48:00 +02:00
Brad Warren
3132c32c26 Update pluggy pinning. (#7459) 2019-10-23 10:50:18 +02:00
Brad Warren
db46326e95 Run at 4:00AM UTC not 0:04AM UTC. (#7460)
Fixes [cron syntax](https://docs.microsoft.com/en-us/azure/devops/pipelines/build/triggers?view=azure-devops&tabs=yaml#supported-cron-syntax) to get the behavior I had in mind in https://github.com/certbot/certbot/pull/7377#discussion_r331295897.
2019-10-21 14:42:51 -07:00
Brad Warren
44cc8d7a3c Require newer versions of oauth2client (#7458)
Over the weekend, nightly tests on Windows failed for certbot-dns-google: https://dev.azure.com/certbot/web/build.aspx?pcguid=74ef9c03-9faf-405b-9d03-9acf8c43e8d6&builduri=vstfs%3a%2f%2f%2fBuild%2fBuild%2f72

The error occurred inside `oauth2client`'s locking code and the failure seems spurious as it did not reproduce this morning: https://dev.azure.com/certbot/certbot/_build/results?buildId=73

I could not find a relevant changelog entry in `oauth2client` saying they've fixed the problem, but the problematic code no longer exists in `oauth2client>=4.0`. This PR updates our minimum dependency required in an attempt to avoid spurious failures for us in the future. The only downside I am aware of is it'll make it harder for certbot-dns-google to be packaged in Debian Old Stable or Ubuntu 16.04, but I don't expect either of those things to happen anytime soon.

* bump oauth2client dep

* Update dev_constraints.txt.

* Add changelog entry for packagers.
2019-10-21 13:54:17 -07:00
Joona Hoikkala
3f36298716 [Apache v2] find_blocks and find_directives implementation (#7443)
* Implement AugeasDirectiveNode and AugeasBlockNode find and create functions

* Add tests for _aug_get_block_name
2019-10-21 12:52:00 -07:00
Brad Warren
f8e097a061 Remove warning about rename. (#7453) 2019-10-19 08:09:08 +02:00
Brad Warren
37b3c22dee Run nightly on Azure even if no commits landed. (#7455) 2019-10-19 08:06:37 +02:00
Victor Shih
032178bea0 Clarify possible existence of /etc/letsencrypt/cli.ini (#7449) 2019-10-18 13:36:45 -07:00
Adrien Ferrand
63c7dd109c Merge pull request #7435 from certbot/add-azure-pipelines
Add azure pipelines
2019-10-10 19:38:32 +02:00
alexzorin
118cb3c9b1 cli: allow --dry-run to be combined with --server (#7436)
The value of --server will now be respected, except when it is the
default value, in which case it will be changed to the staging server,
preserving Certbot's existing behavior.
2019-10-10 00:09:25 +02:00
Brad Warren
717afebcff Upload coverage for integration tests (#7433)
* Upload coverage for integration tests.

* Use in not containsValue.
2019-10-09 23:39:49 +02:00
Brad Warren
8f6fc67378 Merge branch 'master' into add-azure-pipelines 2019-10-08 17:05:12 -07:00
Adrien Ferrand
ec3ec9068c Upgrade to pywin32>=225 and fix unit tests (#7429)
Fixes #7426
2019-10-08 16:17:08 -07:00
Brad Warren
f755cfef48 Add final newlines to files. (#7432)
More conventional and makes it nicer when doing things like running cat to quickly look at the file like I was doing when I noticed this.
2019-10-08 16:16:04 -07:00
Brad Warren
c1f4b86d34 Use shared variable group (#7431)
When setting up Azure Pipelines, I didn't like having to define codecov_token for each pipeline. This works around it by using a shared variable group.

You can see this working successfully at https://dev.azure.com/certbot/certbot/_build/results?buildId=3.

* Use certbot-common.

* update instructions
2019-10-08 16:12:02 -07:00
Adrien Ferrand
fcc398831b Create a new CI for Certbot on Windows using Azure Pipelines (#7377)
This PR defines pipelines that can be run on Azure Pipelines. Currently there are two:
* `.azure-pipelines/main.yml` is the main one, executed on PRs for master, and pushes to master,
* `.azure-pipelines/advanced.yml` add installer testing on top of the main pipeline, and is executed for `test-*` branches, release branches, and nightly run for master.

These two pipelines covers all existing stuff done by AppVeyor currently, and so AppVeyor can be decommissioned once Azure Pipelines is operational.

You can see working pipeline in my fork:
* a PR for `master` (so using main pipeline): https://github.com/adferrand/certbot/pull/65
* a PR for `test-something` (so using advanced pipeline): https://github.com/adferrand/certbot/pull/66
* uploaded coverage from Azure Pipelines: 499aa2cbf2/build

Once this PR is merged, we need to enable Azure Pipelines for Certbot. Instructions are written in `azure-pipelines/INSTALL.md`. This document also references all access rights required to Azure Pipelines onto GitHub to make the CI process work.

Future work for future PRs:
* create a CD pipeline for the releases that will push the installer to GitHub releases
* implement a solution to generate notification on IRC or Mattermost when a nightly build fails

* Define pipelines

* Update locations

* Update nightly

* Use x86

* Update nightly.yml for Azure Pipelines

* Run script

* Use script

* Update install

* Use local installation

* Register warnings

* Fix pywin32 loading

* Clean context

* Enable coverage publication

* Consume codecov token

* Document installation

* Update tool to upload coverage

* Prepare pipeline artifacts

* Update artifact ignore

* Protect against codecov failures

* Add a comment about codecov

* Add a comment on RW access asked by Azure

* Add instructions

* Rename pipeline file

* Update instructions

* Update .azure-pipelines/templates/tests-suite.yml

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

* Update .azure-pipelines/INSTALL.md

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

* Modified scheduled pipeline

* Add comment

* Remove dynamic version-based installer name
2019-10-08 14:40:17 -07:00
Andrzej Górski
9da07590bd Remove --fast from the test farm tests (#7427) 2019-10-08 21:24:55 +02:00
Brad Warren
0cfedbc5f5 Add test farm tests for Debian 10 (#7421)
Fixes #7225.

I got the AMI ID from https://wiki.debian.org/Cloud/AmazonEC2Image/Buster.

You can see all test farm tests including test_tests.sh passing with these changes at https://travis-ci.com/certbot/certbot/builds/130318446.
2019-10-03 15:08:24 -07:00
Jacob Hoffman-Andrews
3608abb01a Remove unnecessary account ID match check. (#7416)
* Remove unnecessary account ID match check.

Right now the Account object calculates an ID using md5. This is
unnecessary and causes problems on FIPS systems that forbid md5. It's
just as good to pick a random series of bytes for the ID, since the ID
gets read out of renewal/foo.conf.

However, if we switched the algorithm right now, we could wind up
breaking forward compatibility / downgradeability, since older versions
would run into this check.

Removing this check now lays the ground to change the ID-calculation
algorithm in the future.

Related to #1948 and
https://github.com/certbot/certbot/pull/1013#issuecomment-149983479.

* Remove test.

* Remove unused import.
2019-10-02 14:44:25 -07:00
Brad Warren
4739a0616d Merge pull request #7415 from certbot/candidate-0.39.0
Candidate 0.39.0
2019-10-01 13:34:47 -07:00
Erica Portnoy
6e38ad9cce Bump version to 0.40.0 2019-10-01 13:04:10 -07:00
Erica Portnoy
4599aff07f Add contents to CHANGELOG.md for next version 2019-10-01 13:04:10 -07:00
Erica Portnoy
0b605333d9 Release 0.39.0 2019-10-01 13:04:08 -07:00
Erica Portnoy
9c18de993d Update changelog for 0.39.0 release 2019-10-01 12:48:40 -07:00
Brad Warren
e3dbd9ce4a Keep compatibility with IE11 in the Nginx plugin (#7414)
As discussed at https://github.com/mozilla/server-side-tls/issues/263, Mozilla's current intermediate recommendations drop support for some non-EOL'd versions of IE. [Their TLS recommendations were updated to suggest a couple possible workarounds for people who need this support](https://github.com/mozilla/server-side-tls/pull/264) and [April suggested that we make this change in Certbot](https://github.com/mozilla/server-side-tls/issues/263#issuecomment-537085728).

We know `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA` translates to `ECDHE-RSA-AES128-SHA` because [nginx uses the same cipher format as OpenSSL](https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers) and the translation is shown in the table at https://github.com/mozilla/server-side-tls/blob/gh-pages/Cipher_Suites.mediawiki.

The risk of regressions making this change is low as we always had this ciphersuite enabled just a few releases ago: https://github.com/certbot/certbot/tree/v0.36.0/certbot-nginx/certbot_nginx

* Keep compatibility with IE11

* update changelog
2019-10-01 10:34:11 -07:00
Brad Warren
c2480b29f7 Add CentOS 8 support to certbot-auto. (#7406)
Fixes #7396.
2019-09-30 09:19:05 -07:00
Brad Warren
6ac8633363 Remove listing for broken icecast plugin. (#7408)
The repo description for the [3rd party Icecast plugin](https://github.com/e00E/lets-encrypt-icecast) says that the plugin isn't currently working and the repository hasn't been updated since 2017. Since it seems broken and unmaintained, let's remove it from the list of third party plugins.

I would happily add it again to the list of third party plugins if people fix and maintain it.
2019-09-27 12:47:12 -07:00
Brad Warren
8a4c2f505f Remove listing for broken heroku plugin (#7409)
The README for the [3rd party heroku plugin](https://github.com/gboudreau/certbot-heroku) says it has been deprecated. Because of this, let's remove it from the list of third party plugins.
2019-09-27 12:46:56 -07:00
Joona Hoikkala
ca3077d034 Try to use platform.linux_distribution() before distro equivalent (#7403)
Try to primarily fall back to using `platform.linux_distribution()` if `/etc/os-release` isn't available. Only use `distro.linux_distribution()` on Python >= 3.8.

* Try to use platform.linux_distribution() before distro equivalent

* Fix tests for py38

* Added changelog entry
2019-09-27 09:50:38 -07:00
Kenichi Maehashi
6c89aa5227 Fix to run with Apache on RHEL 6 (#7401)
This PR fixes a regression in #7337 (0.38.0) that certbot cannot run with Apache on RHEL 6.

In RHEL 6, `distro.linux_distribution()` returns `RedHatEnterpriseServer`.

In RHEL 6:

```py
>>> import distro
>>> distro.linux_distribution()
(u'RedHatEnterpriseServer', u'6.10', u'Santiago')

>>> import platform
>>> platform.linux_distribution()
('Red Hat Enterprise Linux Server', '6.10', 'Santiago')
```

In RHEL 7:

```py
>>> import distro
>>> distro.linux_distribution()
('Red Hat Enterprise Linux Server', '7.6', 'Maipo')

>>> import platform
>>> platform.linux_distribution()
('Red Hat Enterprise Linux Server', '7.6', 'Maipo')
```

* fix to run with Apache on RHEL 6

* fix docs
2019-09-26 13:25:48 -07:00
Joona Hoikkala
a156d37ee1 Merge pull request #7400 from certbot/update-apache-parser-v2
Update apache parser v2
2019-09-26 10:49:36 +03:00
Brad Warren
1756ef8620 Merge branch 'master' into update-apache-parser-v2 2019-09-25 11:55:33 -07:00
Brad Warren
8cb57566c0 List support for Python 3.8 (#7392)
Fixes #7368.

When updating the changelog, I replaced the line about running tests on Python 3.8 because I personally think that support for Python 3.8 is the most relevant information for our users/packagers about our changes in this area.

* List support for Python 3.8.

* Update changelog.
2019-09-24 11:38:38 -07:00
ohemorange
18e6c6c2a8 Don't send OCSP requests for expired certificates (#7387)
Fixes #7152.

* don't check ocsp if cert is expired when getting cert information

* don't check ocsp if the cert is expired in ocsp_revoked

* update tests

* update changelog

* move pytz import to the top of the test file
2019-09-23 17:20:11 -07:00
Joona Hoikkala
feacbe9671 [Apache v2] DualParserNode implementation 3/3 (#7376)
* DualParserNode, DualCommentNode and DualDirectiveNode implementations

* Add DualBlockNode

* DualBlockNode find_ methods

* Address review comments

* Address review comments

* Simplify isPass

* Add explanation to _create_matching_list pydoc

* Remove unnecessary conditional block

* Address review comments
2019-09-23 16:27:48 -04:00
Adrien Ferrand
e402993c34 [Windows] Create a certbot renew scheduled task using the installer (#7371)
This PR implements the item "register a scheduled task for certificate renewal" from the list of requirements described in #7365.

This PR adds required instructions in the NSIS installer for Certbot to create a task, named "Certbot Renew Task" in the Windows Scheduler. This task is run twice a day, to execute the command certbot renew and keep the certificates up-to-date.

Uninstalling Certbot will also remove this scheduled task.

* Implementation

* Corrections

* Update template.nsi

* Improve scripts

* Add a random delay of 12 hours

* Synchronize template against default one in pynsist 2.4

* Clean config of scheduled task

* Install only in AllUsers mode

* Add comments

* Remove the logic of single user install
2019-09-23 12:29:18 -07:00
Brad Warren
754c34c120 Fix Windows sdist. (#7384) 2019-09-20 17:21:07 +02:00
Joona Hoikkala
c224340330 [Apache v2] DualParserNode implementation 2/3 (#7375)
* DualParserNode, DualCommentNode and DualDirectiveNode implementations

* Add DualBlockNode

* Address review comments

* Address review comments

* Call the right assertion after name change

* Simplify isPass

* Add explanation to _create_matching_list pydoc

* Break when match was found
2019-09-19 17:44:50 -04:00
Joona Hoikkala
23fb6d2877 [Apache v2] DualParserNode implementation 1/3 (#7374)
* DualParserNode, DualCommentNode and DualDirectiveNode implementations

* Address review comments

* Address review comments

* Simplify isPass
2019-09-18 16:31:44 -04:00
Brad Warren
2883ca839e Use xenial globally (#7380)
As described at https://github.com/certbot/certbot/pull/7372#discussion_r323592366, Travis is transitioning people to Xenial, but it seems this transition still may not be complete as some of our jobs ran on Trusty with all references to `dist` removed as seen at https://travis-ci.com/certbot/certbot/builds/127960999.

This PR sets `dist: xenial` globally and overrides it as needed for the oldest tests.

* Set xenial globally.

* Use trusty in all oldest tests.
2019-09-17 15:24:53 -07:00
ohemorange
fb6aad28bd Get integration tests working on python 3.8 (#7372)
* Get integration tests working on python 3.8

* Run unit tests on py38

* Update coveragercs to use coverage 4.5+ format

* remove line added to tox.ini

* update changelog

* xenial is the new travis default; no need to specify in .travis.yml
2019-09-16 14:14:26 -04:00
Joona Hoikkala
9620cc75d4 [Apache v2] Allow initialization of ParserNode instances using metadata dictionary instead of required arguments (#7366)
Add metadata keyword argument to the ParserNode interface, allowing the initialization of the object from contents of the metadata - if the implementation allows it. As an example, Augeas implementation needs nothing more than the Augeas DOM path of a configuration directive to be able to populate the ParserNode instance with all data relevant to the DirectiveNode.

The checks also allow skipping the otherwise required keyword arguments if metadata is provided.

* Allow creating ParserNode instances using information from metadata dictionary

* Update certbot-apache/certbot_apache/interfaces.py

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

* Update certbot-apache/certbot_apache/interfaces.py

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

* Address review comments

* Fix filepath comment

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-09-09 12:09:09 -07:00
Adrien Ferrand
ab76834100 [Windows|Linux] Forbid os.stat and os.fstat (#7325)
Fixes #7212

This PR forbid os.stat and os.fstat, and fix or provide alternatives to avoid its usage in certbot outside of certbot.compat.filesystem.

* Reimplement private key mode propagation

* Remove other os.stat

* Remove last call of os.stat in certbot package

* Forbid stat and fstat

* Implement mode comparison checks

* Add unit tests

* Update certbot/compat/filesystem.py

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

* Update certbot/compat/filesystem.py

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

* Handle case where multiple ace concerns a given SID in has_min_permissions

* Add a new test scenario

* Add a simple test for has_same_ownership

* Fix name function

* Add a comment explaining an ACE structure

* Move a test in its dedicated class

* Improve a message error

* Calculate has_min_permission result using effective permission rights to be more generic.

* Change an exception message

* Add comments, avoid to skip a test.

* Update certbot/compat/filesystem.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-09-06 14:30:25 -07:00
Brad Warren
ada2f5c767 Simplify testing of RHEL 8. (#7323) 2019-09-06 08:59:24 +02:00
ohemorange
e4af1f3319 Disable TLS session tickets in Nginx (#7355)
* Find OpenSSL version

* Create and update various config files

* Update logic to use new version constraints

* SSL_OPTIONS_HASHES_NEW and SSL_OPTIONS_HASHES_MEDIUM were just being used for testing, and maintaining them is becoming untenable, so remove them.

* if we don't know the openssl version, we can't turn off session tickets

* add unit test for _get_openssl_version

* add unit tests

* placate lint

* Fix docs and tests and clean up code

* use python correctly

* update changelog

* Lint

* make comment a comment
2019-09-05 13:51:56 -07:00
ohemorange
ab0e382829 Merge pull request #7364 from certbot/candidate-0.38.0
Update files from 0.38.0 release
2019-09-04 18:23:50 -04:00
Adrien Ferrand
ed0b8e4af5 [Windows] Create an installer for Certbot (#7324)
This PR is the first step to create an official distribution channel of Certbot for Windows. It consists essentially in creating a proper Certbot Windows installer.

Usually distributing an application requires, in a way or another, to stabilize the application logic and its dependencies around a given version. On Windows, this usually takes the form of a freezed application, that vendors its dependencies into a single executable.

There are two well-known solutions to create an executable shipping a Python application on Windows: [py2exe](http://www.py2exe.org/) and [pyinstaller](https://www.pyinstaller.org/). However these solutions create self-executable `.EXE` files: you run the `.EXE` file that launches immediately the software.

This is not a end-user solution. Indeed when a Windows user wants to install a piece of software, he expects to find and download an installer. When run the installer would interface with Windows to setup configuration entries in the Registry, update the environment variable, add shortcuts in the Start Menu, and declare a uninstaller entry into the Uninstaller Manager. Quite similarly, this is what you would get from a `.deb` or `.rpm` package.

A solution that builds proper installers is [pynsis](https://pynsist.readthedocs.io/en/latest/). It is a Python project that constructs installers for Python software using [NSIS](https://sourceforge.net/projects/nsis/), the most known free Windows installer builder solution.

This PR uses pynsist to build a Windows installer. The Python script to launch the installer build is `.\windows-installer\construct.py`. Once finished, the installer is located in `.\windows-installer\build\nsis`.

This installer will do the following operations during the installation:
* copy in the install path a full python distribution used exclusively for Certbot
* copy all Python requirements gathered from the `setup.py` of relevant certbot projects
* copy `certbot` and `acme`
* pre-build python binary assets
* register the existence of the application correctly in Windows Registry
* prepare a procedure to uninstall Certbot
* and of course, expose `certbot` executable to the Windows command line, like on Linux, to be able to launch it as any CLI application from Batch or Powershell

This installer support updates: downloading a new version of it and running it on a Windows with existing installation of Certbot will replace it with the new version.

Future capabilities not included in this PR:
* auto-update of Certbot when a new release is available
* online documentation for Windows
* register a scheduled task for certificate renewal
* installer distribution (continuous deployment + distribution channels)
* method to check the downloaded installer is untampered

* Setup config

* Fix shortcut

* Various improvments

* Update windows-installer/construct.py

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

* Split into several method

* Change installer name

* Remove DNS plugins for now

* Add a comment about administrator privileges

* Update welcome

* Control python version

* Control bitness

* Update windows-installer/construct.py

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

* Update windows-installer/construct.py

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

* Update windows-installer/construct.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-09-03 16:30:13 -07:00
Brad Warren
8a570b18e9 Bump version to 0.39.0 2019-09-03 12:49:30 -07:00
Brad Warren
deb0168c09 Add contents to CHANGELOG.md for next version 2019-09-03 12:49:29 -07:00
Brad Warren
46a12d0127 Release 0.38.0 2019-09-03 12:49:28 -07:00
Brad Warren
6d4baec955 Update changelog for 0.38.0 release 2019-09-03 12:42:35 -07:00
Joona Hoikkala
af1c66b28f [Apache v2] Modifications to ParserNode interfaces (#7330)
This PR contains the changes requested in initial pre-review comments of #7308

Move properties to class pydocs in interfaces.py
Prefer class ABC register() functionality instead of class inheritance for interface classes
Add apache implementation specific functions to interfaces

* Move class argument definitions to class pydoc

* Add apache specific functionality to the interface

* Bring inheritance back

* Define initialization for different ParserNode classes

* Add parsernode utils to check keyword arguments and document the defaults in pydoc

* Fix pydocs and make BlockNode a child of DirectiveNode

* Refine docs, and remove unused __init__ from BlockNode

* Split parsernode util tests to their own respective file

* Skip cover for dummy calls to super

* Add types to method documentation

* Add documentation for children
2019-08-30 13:42:18 -07:00
Brad Warren
4eaa06d58e list py37 support (#7360)
These plugins also support Python 3.7. You can see tests passing at https://travis-ci.com/certbot/certbot/jobs/228820500.
2019-08-28 11:10:13 -07:00
ohemorange
0fe28a6459 Replace platform.linux_distribution with distro.linux_distribution (#7337)
Smallest possible fix for #7106 

* Replace platform.linux_dependencies with distro.linux_dependencies

* run build.py

* Add minimum version of 1.0.1

* Pin back requests package

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

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

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

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

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

* Remove certbot root from PYTHONPATH during integration tests

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

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

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

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

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

* Stop turning session tickets off in Nginx

* update changelog

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

* Stop turning session tickets off in Nginx

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

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

* Update .travis.yml

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

* Fix lint

* Remove useless tearDown

* Clean pylint
2019-08-16 11:08:42 +02:00
Adrien Ferrand
9a047a6996 Clean travis config (#7328)
This PR removes some useless capabilities in .travis.yml that are associated to the jobs. This concerns mainly sudo and docker.
2019-08-15 16:41:51 -07:00
Matthias Bilger
a8bd839223 Added DNS plugin for ISPConfig to list (#7332) 2019-08-15 14:43:14 -07:00
Joona Hoikkala
270754deff [Apache v2] New ParserNode interface abstraction (#7246)
* New ParserNode interface abstraction

* Add the test assertions, and fix interface

* Update certbot-apache/certbot_apache/interfaces.py

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

* Update certbot-apache/certbot_apache/interfaces.py

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

* Update certbot-apache/certbot_apache/interfaces.py

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

* Add dummy tests and change arguments to properties

* Add more context to comment property docstring

* Add documentation to the main docstring

* Streamline the parameter naming

* Update certbot-apache/certbot_apache/interfaces.py

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

* Add explicit instructions how whitespaces are treated in set_parameters

* Add the information about lookups being case insensitive.

* Add context about whitespacing to add_ - methods

* Update certbot-apache/certbot_apache/interfaces.py

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

* Update certbot-apache/certbot_apache/interfaces.py

Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-08-13 09:22:51 -07:00
Joona Hoikkala
a83f9eb4e4 Merge pull request #7321 from certbot/fix-apache-parser-v2-cover
Fix apache-parser-v2 tests
2019-08-13 15:28:24 +03:00
tyborr
a1aef4c15c Fix Certbot's Apache plugin doesn't work on Scientific Linux (#7294)
This PR adds OVERRIDE_CLASS in certbot-apache/entrypoint.py for Scientific Linux. Fixes #7248.

* add OVERRIDE_CLASS for Scientific Linux os name

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

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

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

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

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

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

This reverts commit 9174c631d9.

* Keep hashes with TLS session tickets disabled.

* dont delete changelog entries

* add changelog entry

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

This reverts commit f4d17d9a6b.

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

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

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

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

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

This reverts commit 9174c631d9.

* Keep hashes with TLS session tickets disabled.

* dont delete changelog entries

* add changelog entry

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

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

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

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

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

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

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

* Use latest certbot-auto to bootstrap.

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

* Fix hook executable test

* Remove the temporary decorator @broken_on_windows

* Fix util_test

* No broken unit test on Windows anymore

* More elegant mock

* Fix context manager

* Fix lint

* Fix mypy

* Adapt coverage

* Corrections

* Fix lint

* Adapt coverage

* Update certbot/tests/compat/filesystem_test.py

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

* Update util_test.py

* Fix pylint

* Forbid os.access

* Update os_test.py

* Update os.py

* Fix lint

* Update filesystem.py

* Update filesystem.py

* Update filesystem.py

* Update os.py

* Start fixing tests

* Platform independent hooks

* Fix probe fd close

* Add broken_on_windows for integration tests

* Fix a lot of tests

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

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

* New tests fixed

* Test for permissions on Windows

* Permissions comparison for Windows

* No broken tests in certbot core anymore

* Change mode

* Specific config for appveyor

* Use forked pebble for now

* Various fixes

* Assert file permissions for world on private keys

* Clean code

* Fix several things

* Add integration target

* Optimize integration env

* Re-enable all AppVeyor envs

* Use again official pebble

* Update pebble_artifacts.py

* Set PYTEST_ADDOPTS silently

* Update appveyor.yml

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

* Remove injection of nginx in PATH

* Clean debug code

* Various cleanup, ensure to remove workspace after tests

* Update tox target

* Improve assertions. Control the keyword echoed in hooks

* Fix for virtualenv on Python 3.7.4 for Windows

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

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

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

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

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

* Move tls configuration files in a specific folder

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

* Add tests and fix if statement

* Update CHANGELOG.md

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

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

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

* Complete process

* Fix nginx cert path

* Check conditionnally docker

* Update gitignore, fix apacheconftest

* Full object

* Carriage return

* Work in progress

* Move to official v2.1.0 of pebble

* Fix name

* Update acme_server.py

* Link things together with new version of pebble

* Plug the logic to tests

* Update config

* Reinitiate config

* Add OCSP config to pebble

* Working.

* Simplify logic

* Clean code

* Use forked pebble for now

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

* Move full logic of mock at the acme server config

* Continue work

* Finish fixing the date parsing

* Update module name

* Use again official pebble

* Activate mock OCSP server

* Clean code

* Update pebble_artifacts.py

* Remove OCSP stale test

* Add executable permissions

* Clean code

* Update setup.py

* Simplify code

* On-demand import of pebble_ocsp_server

* Revert "Remove OCSP stale test"

This reverts commit 2e4c985b427120cc15526bbcfd15806d02a6f3fc.

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

* Fix for virtualenv on Python 3.7.4 for Windows

* Update acme_server.py
2019-08-02 11:46:12 -07:00
Brad Warren
31a8d086fc Merge pull request #7289 from certbot/fix-apache-parser-v2
Fix AppVeyor on the apache-parser-v2 branch
2019-08-02 11:22:53 -07:00
Adrien Ferrand
36b4c312c6 Upgrade virtualenv in dev/tests environments (#7287)
AppVeyor recently upgrade the Python 3.7.x installed in their VM to 3.7.4. However, virtualenv 16.6.1 is broken on that specific version of Python for Windows.

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

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

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

* Fix account_tests

* Fix hook executable test

* Remove the temporary decorator @broken_on_windows

* Fix util_test

* No broken unit test on Windows anymore

* More elegant mock

* Fix context manager

* Adapt coverage

* Corrections

* Adapt coverage

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

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

* Update tests

* Fix lint and changelog

* Update configurator.py

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

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

* Corrections after review

* Improve a test

* Remove commented useless lines in TLS configs

* Add a nice warning. Because I am nice.

* Fix lint

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

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

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

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

* Remove duplicate, failing oldest tests.

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

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

* Implement copy_ownership

* Apply copy_ownership

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

* Correct lint and mypy

* Ensure to apply mode in makedirs

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

* Ensure streamHandler has 0600 on Windows

* Reactivate a test on windows

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

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

* Administrative privileges are always required.

* Correct security implementation (not the logic yet)

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

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

* Add a test for windows about default paths

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

* Fix lint and mypy

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

* Apply suggestions from code review

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

* Add more details in a comment

* Retrigger build.

* Add documentation.

* Fix a test

* Correct RW clear down

* Update util.py

* Remove unused code

* Fix code style

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

* Various optimizations around file owner and file mode

* Fix last error

* Fix copy_ownership_and_apply_mode

* Fix lint

* Correct mypy

* Extract out first part from windows-file-permissions

* Ignore new_compat in coverage for now

* Create test package for compat

* Add unit tests for security module.

* Add pywin32

* Adapt linux coverages to the windows-specific LOCs added

* Clean imports

* Correct import

* Trigger CI

* Reactivate a test

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

* Clean comment

* Add doc

* Fix lint

* Correct mypy

* Add executable permissions

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

* Load certbot.compat.os instead of os

* Move existing compat test

* Update local oldest requirements

* Import sys

* Fix some mocks

* Update account_test.py

* Update os.py

* Update os.py

* Update local oldest requirements

* Implement the new linter_plugin

* Fix remaining linting errors

* Fix local oldest for nginx

* Remove custom check in favor of pylint plugin

* Remove check coding style

* Update linter_plugin.py

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

* Add several comments

* Update the setup.py

* Add documentation

* Update acme dependencies

* Update certbot/compat/os.py

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

* Update certbot/compat/os.py

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

* Update certbot/compat/os.py

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

* Update docs/contributing.rst

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

* Update linter_plugin.py

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

* Update linter_plugin.py

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

* Update docs/contributing.rst

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

* Update docs/contributing.rst

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

* Corrections

* Handle os.path. Simplify checker.

* Add a comment to a reference implementation

* Update changelog

* Fix module registering

* Update docs/contributing.rst

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

* Update docs/contributing.rst

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

* Update docs/contributing.rst

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

* Update config and changelog

* Correction

* Correct os

* Fix merge

* Disable pylint checks

* Normalize imports

* Simplify security

* Corrections

* Reorganize module

* Clean code

* Clean code

* Remove coverage

* No cover

* Implement security.chmod

* Disable a test for now

* Disable hard error for now

* Add a first test. Remove unused import

* Recalibrate coverage

* Modifications for misc

* Correct function call

* Add some types

* Remove newline

* Use os_rename

* Implement security.open

* Revert to windows-files-permissions approach

* Fix lint

* Implement security.mkdir and security.makedirs

* Fix lint

* Clean lint

* Clean lint

* Revert "Clean lint"

This reverts commit 83bf81960ac6bf3f76c286ca065a5ac850c6870b.

* Correct mock

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

* Fix separator

* Fix separator

* Rename security into filesystem

* Change module security to filesystem

* Move rename into filesystem

* Rename security into filesystem

* Rename security into filesystem

* Rerun CI

* Fix import

* Fix pylint

* Implement copy_ownership_and_apply_mode

* Fix pylint

* Update certbot/compat/os.py

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

* Remove default values

* Rewrite a comment.

* Relaunch CI

* Pass as keyword arguments

* Update certbot/compat/filesystem.py

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

* Update certbot/compat/filesystem.py

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

* Update certbot/compat/filesystem.py

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

* Make the private key permissions transfer platform specific

* Update certbot/compat/filesystem.py

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

* Rename variable

* Fix comment0

* Add unit test for copy_ownership_and_apply_mode

* Adapt coverage

* Implement new methods.

* Remove the old method

* Reimplement make_or_verify_dir

* Finish migration

* Start to fix tests

* Fix ownership when creating a file with filesystem.open

* Fix security on TempHandler

* Fix validation path permissions

* Fix owner on mkdir

* Use a proper workdir for crypto tests

* Fix pylint

* Adapt coverage

* Update storage_test.py

* Update util_test.py

* Clean code

* Update certbot/compat/filesystem.py

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

* Add comment

* Update certbot/compat/filesystem.py

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

* Check permissions

* Change test mode

* Add unit test for filesystem.check_* functions

* Update filesystem_test.py

* Better logic for TempHandler

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

* acme: Implement authz deactivation

Resolves #4945

* update AUTHORS and CHANGELOG

* typos in mypy annotations

* formatting: missing newline

* improve test_deactivate_authorization

* improve deactivate_authorization

* test: s/STATUS_INVALID/STATUS_DEACTIVATED/

* simplify dict to keyword argument

* acme: add UpdateAuthorization

* acme: use UpdateAuthorization in deactivate_authz

and add mypy annotation

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

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

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

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

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

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

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

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

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

* Forbid os.path.realpath

* Finish implementation

* Use filesystem.realpath

* Control symlink loops also for Linux

* Add a test for forbidden method

* Import a new object from os.path module

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

* Correct errors

* Fix dependencies

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

But this test is not done on Windows.

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

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

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

* Run tests on apache-parser-v2.

* add comment

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

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

* Implement copy_ownership_and_apply_mode

* Update certbot/compat/os.py

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

* Remove default values

* Rewrite a comment.

* Relaunch CI

* Pass as keyword arguments

* Update certbot/compat/filesystem.py

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

* Update certbot/compat/filesystem.py

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

* Update certbot/compat/filesystem.py

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

* Make the private key permissions transfer platform specific

* Update certbot/compat/filesystem.py

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

* Rename variable

* Fix comment0

* Add unit test for copy_ownership_and_apply_mode

* Adapt coverage

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

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

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

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

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

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

* Complete process

* Fix nginx cert path

* Check conditionnally docker

* Update gitignore, fix apacheconftest

* Full object

* Carriage return

* Move to official v2.1.0 of pebble

* Fix name

* Update acme_server.py

* Relaunch CI

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

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

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

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

* Update docstring

* Update documentation

* Configure a stdout to ACMEServer

* Map all process through defined stdout

* Remove unused variable

* Handle using signals

* Use failsafe entering context

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

* Fix lint

* Correct mock

* Rename security into filesystem

* Update apache and nginx plugins requirements

* Update certbot/plugins/webroot.py

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

* Reenable pylint here

* Move code

* Reimplement mkdir

* Control errors on eexist, remove superfluous chmod for makedirs

* Add proper skip for windows only tests

* Fix lint

* Fix mypy

* Clean code

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

* Add forbiden functions to tests

* Update certbot/compat/os.py

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

* Simplify code

* Sync _get_current_user with part3c

* Use the simpliest implementation

* Remove exist_ok, simplify code.

* Simplify inline comment

* Update filesystem_test.py

* Update certbot/compat/os.py

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

* Update certbot/plugins/webroot.py

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

* Update certbot/plugins/webroot.py

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

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

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

* Deprecate certbot_config changes.

* Document config_changes deprecation.

* Remove view_config_changes as IInstaller method.

* Remove view_config_changes from plugins.

* Add view_config_changes warnings.

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

* Clean lint

* Rename security into filesystem

* Update certbot/compat/filesystem.py

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

* Update certbot/util.py

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

* Update certbot/lock.py

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

* Update certbot/compat/os.py

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

* Update certbot/lock.py

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

* Update certbot/compat/os.py

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

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

* Secure implementation preventing race conditions

* Revert "Secure implementation preventing race conditions"

This reverts commit dbb85492195122020ca0b4a685ddb4836fdc6d12.

* Simplify the logic on Windows.

* Implement os.open to prevent race conditions

* Add unit tests

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

* Improve comments

* Use CREATE_ALWAYS

* Adapt coverage threshold to new Windows specific LOCs.

* Update certbot/compat/os.py

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

* Update certbot/compat/os.py

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

* Update certbot/compat/os.py

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

* Update certbot/compat/filesystem.py

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

* Add some comments

* Fix pylint

* Improve docstring

* Added test cases

* Improve docstring

* Update certbot/lock.py

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

* Update certbot/lock.py

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

* Fix lint

* Adapt coverage

* Adapt coverage
2019-07-02 16:21:24 -07:00
Brad Warren
448d159223 Install Python3 only dev tools with tools/venv3.py (#7215)
These packages can be useful and I found that they aren't being installed in our Python 3 development environment. Let's fix that.
2019-07-02 13:45:57 -07:00
Brad Warren
3e872627d8 Pin/upgrade virtualenv in our tests (#7211)
* Update virtualenv to the latest version.

* Use venv from pip and pin more packages.

* Pin codecov.

* update appveyor config

* Write the path separator backwards.

* s/pip_install.py install/pip_install.py

* Prefix tools\\pip_install.py with python exe.

* Upgrade py to fix AppVeyor failures.

* add back comment

* Update virtualenv with CERTBOT_NO_PIN.

* Pass -U to upgrade tox and deps.

* Upgrade virtualenv.
2019-07-02 20:02:00 +03:00
Brad Warren
76b7eb0628 Document certbot-auto's code freeze. (#7207)
Inspired by #7194, this PR adds a note to our documentation that we're not accepting most changes to certbot-auto.
2019-06-28 15:53:56 -07:00
dkp
4fc30f2ecb Replace Some Platform-Specific Line Separation (#7203)
os.linesep isn't supposed to be used when writing to files opened in
text mode, where '\n' is escaped to the platform-specific ASCII
sequence.  For example, on Windows, os.linesep is '\r\n' and in text
mode is escaped to ASCII sequence CR CR LF rather than just CR LF.
This is also true for the default logger and IDisplay notifications.

Replacing os.linesep with '\n' ensures the right sequence is escaped.

Resolves: 6899
2019-06-28 13:06:52 -07:00
sydneyli
1c75b6dacd Update Nginx conf file to match Mozilla's security recommendations (#7163)
Fixes #7089
2019-06-28 12:16:51 -07:00
Joona Hoikkala
c08a4dec2d Refactor augeas_configurator.py functionality to configurator.py and parser.py accordingly. (#7181)
This pull request moves the functionality within `AugeasConfigurator` that previously existed as a parent class of `ApacheConfigurator` to `ApacheConfigurator` and `ApacheParser` accordingly.

Most of the methods were moved as-is, and one (`recovery_routine()`) was completely removed. Few of the methods had to be split between the configurator and parser, good example of this is `save()`.

The Augeas object now lives completely within the `ApacheParser`.

* Remove augeasconfigurator

* Fix references

* Adjust tests accordingly

* Simplify test

* Address review comments

* Address review comments

* Move test_recovery_routine_reload
2019-06-28 08:39:13 -07:00
Brad Warren
4fc0ef0fbe certbot-plugin-gandi is not an installer. (#7201)
This [plugin](https://github.com/obynio/certbot-plugin-gandi) is an authenticator but not an installer. It's a DNS authenticator plugin.
2019-06-27 15:17:45 -07:00
Brad Warren
26a1eddd89 Remove plesk from the list of 3rd party plugins. (#7200)
Our link for the Plesk plugin goes to https://github.com/plesk/letsencrypt-plesk which refers you to https://ext.plesk.com/packages/f6847e61-33a7-4104-8dc9-d26a0183a8dd-letsencrypt and in their changelog for 2.0.0 it says "Replaced Python-based certbot with PHP-based client".
2019-06-27 15:17:31 -07:00
Brad Warren
1c6210ee00 Fix certbot config_changes (#7197)
* Remove for_logging parameter.

* Remove broken/unused --num parameter.

* update changelog
2019-06-26 17:46:51 -07:00
Brad Warren
a27f3ebd4f s/for for/for (#7196) 2019-06-26 17:24:04 -07:00
Brad Warren
a778b50403 Run le_auto_xenial on every PR. (#7195)
https://github.com/certbot/certbot/pull/7190/files removed our only le_auto_* tests on PRs. This PR fixes that by running le_auto_xenial on every PR which also includes running modification-check.py like we used to for Trusty.
2019-06-26 14:54:08 -07:00
Brad Warren
f2ab6a338c Remove files for old Docker image. (#7188) 2019-06-26 11:54:02 +02:00
Hunter
0d5bad6c8c dns-cloudflare: update URL for obtaining API keys (#7052)
Updated the ACCOUNT_URL in the Cloudflare-DNS plugin.
This uses the new "dash.cloudflare.com" scheme and future-proofs this URL for an upcoming change to Cloudflare API keys (this is not public yet, so no other changes related to this).
2019-06-25 17:53:31 -07:00
Brad Warren
dc0cfa21c9 Drop support for Ubuntu Trusty (#7190)
* Remove references and tests for Ubuntu Trusty.

* Mention dropped support in changelog.
2019-06-25 14:04:25 -07:00
Brad Warren
a37a4486cf Add Debian ARM AMI. (#7189)
Inspired by the number of ARM users we have (and because I want to rip out the only 32 bit test we have which without this PR would remove all tests we have on non-x86_64 architectures), this test adds an ARM image to the test farm tests. The image ID was taken from https://wiki.debian.org/Cloud/AmazonEC2Image/Stretch, you can see tests passing at https://travis-ci.com/certbot/certbot/builds/116857897, and I ran test_tests.sh locally and it passed.
2019-06-25 14:03:45 -07:00
Brad Warren
776e939a4c Drop support for quay.io. (#7187)
In this spirt of cleaning up some low hanging cruft, this fixes #4343.

There are no (recent) release tags on quay.io and the builds are just following master. See https://quay.io/repository/letsencrypt/letsencrypt?tab=tags.

Once this lands, I can disable the automated builds on quay.io and we can delete Dockerfile-old and tools/docker-warning.sh.
2019-06-25 11:05:28 -07:00
Brad Warren
69cf64079c Mention dropped support in changelog. 2019-06-25 10:18:39 -07:00
Brad Warren
9962cf0b8e Upgrade compatibility tests to stretch. (#7185)
Inspired by #7180, there's no reason for these tests to be running on old stable. This upgrades them to the latest stable version of Debian.

You can see tests passing with these changes at https://travis-ci.com/certbot/certbot/builds/116844923.
2019-06-25 10:13:57 -07:00
Brad Warren
4c95b687ae Remove references and tests for Ubuntu Trusty. 2019-06-25 10:10:14 -07:00
Brad Warren
a3bbdd52e7 Improve issue closing behavior. (#7178) 2019-06-24 16:39:45 -07:00
Siilike
2e3c1d7c77 Add reference to the Standalone DNS Authenticator (#7137)
Updated documentation to add a reference to the Standalone DNS Authenticator, https://github.com/siilike/certbot-dns-standalone
2019-06-24 12:47:50 -07:00
Adrien Ferrand
249af5c4cd Fix integration tests with Pebble v2.1.0 + (#7175)
Since Pebble v2.1.0, new controls have been added on ACME specs compliance on Pebble with strict mode enabled. These controls are described here: letsencrypt/pebble@3a2ce1c

Currently Certbot is not compliant enough to pass these new controls. One part of the work to do is described here: #7171

As a consequence, our CI is currently broken, both on PR builds and nightly builds.

This PR disables the strict mode during integration tests, fixing temporarily our CI. This will give us some time to fix theses deviations, and add back the strict mode in a future PR once it is merged.

* Remove -strict mode on Pebble for now.

* Refer to relevant Certbot PR

* Clean code
2019-06-24 12:03:24 -07:00
Adrien Ferrand
9a60f6df78 Fix codecov quality gate since flags have been removed (#7173)
Because some users were complaining about staled workflow when flags (https://docs.codecov.io/docs/flags) are enabled, Codecov decided to remove them when calculating the coverage on branches until they improved this functionality.

See: https://docs.codecov.io/docs/flags#section-flags-in-the-codecov-ui

The flags are still taken into account on PR builds, but not on based branch.

This is a problem for us, because we use the flags to compare specifically the coverage of a PR against its base branch for Windows on one side, and Linux on the other side. Without flags taken into account on the base branch, the CI fails because the coverage on Windows is too low.

As a temporary fix until the situation is clarified on Codecov side, this PR replaces the validation condition, that was a comparison against the base branch, to a fixed coverage registered in the local .codecov.yml file in Certbot repository.

This way, the coverage on PR builds, that takes into account the flags, is validated against an appropriate value.

This is a temporary solution, that will require an explicit update of .codecov.yml in the mean time if the coverage significantly increases, or decreases on some developments. But until the situation is fixed, this will allow to have a functional quality gate.
2019-06-21 12:00:03 -07:00
Adrien Ferrand
e9bcaaa576 [Windows] Security model for files permissions - STEP 3a (#6964)
This PR implements the filesystem.chmod method from #6497.

* Implement filesystem.chmod

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

* Update apache plugin requirements

* Use a try/except import approach similar to lock

* Add comments about well-known SIDs

* Add main command

* Call filesystem.chmod in tests, remove one test

* Add test for os module

* Update environment marker

* Ensure we are not building wheels using an old version of setuptools

* Added a link to list of NTFS rights

* Simplify sid comparison

* Enable coverage

* Sometimes, double-quote is the solution

* Add entrypoint

* Add unit tests to filesystem

* Resolve recursively the link, add doc

* Move imports to the top of the file

* Remove string conversion of the ACL, fix setup

* Ensure admins have all permissions

* Simplify dacl comparison

* Conditionally raise for windows temporary workaround

* Add a test to check filesystem.chown is protected against symlink loops
2019-06-20 10:52:43 -07:00
Brad Warren
5078b58de9 Upgrade to the latest macOS image (#7167)
This fixes the test failures we saw last night at https://travis-ci.com/certbot/certbot/builds/116073070.

The problem is that the Homebrew installation included in the Travis image is outdated and when it tries to install packages, it fails. You can see this at https://travis-ci.com/certbot/certbot/jobs/209185570#L83. There is a thread in Travis' community froum about this at https://travis-ci.community/t/xcode-8-3-homebrew-outdated-error/3798.

To fix this, we could either upgrade Hombrew which can be a slow process according to both Travis and the original poster of the issue or we could upgrade to a newer version of macOS. I chose the latter to avoid the speed problems and picked the latest version available.

You can see tests passing with these changes at https://travis-ci.com/certbot/certbot/builds/116186095.
2019-06-19 14:09:30 -07:00
schoen
03cf5d15a6 Merge pull request #6894 from suqld/fix-google-dns-private-zones
Detect private DNS zones in Google and skip them
2019-06-19 13:21:22 -07:00
David Drobner
8efe3fb19a RFC8555 Messages (#7131)
Add new error types and descriptions from RFC 8555 to acme (#7116)
2019-06-18 17:29:53 -07:00
Brad Warren
9863c2d18e Update Ubuntu 18.04 AMI to fix blocking on input (#7166) 2019-06-18 12:07:45 -07:00
timwsuqld
6172821d90 Merge branch 'master' into fix-google-dns-private-zones 2019-06-18 14:04:21 +10:00
Brad Warren
dde16df778 Fixes #3400. (#7162)
The person who wrote this code no longer works on Certbot and regardless of
what the intended behavior was, let's document the actual behavior.
2019-06-17 15:56:06 -07:00
Adrien Ferrand
1df778859b [Windows|Linux] Use builtin Python proxy capabilities for Certbot-CI (#7156)
This PR is a part of the actions necessary to make Certbot-CI work on Windows, in order to execute the integration tests on this platform.

I initially used the fully-fledged HTTP proxy [Traefik](https://docs.traefik.io/) to distribute HTTP challenges among several pytest nodes, and so parallelize the integration tests. Traefik for this purpose is overkill. We just want to redirect the ACME server to a pytest node depending on the `Host` header, and we use here a production-grade HTTP proxy for that.

However it was not a problem on Linux, as soon as you can have Docker, because this instance is deployed through it.

But this becomes a problem for Windows, where Docker is not available everywhere, very compelling on its setup, and limited by the implemented network drivers. See my comments here https://github.com/letsencrypt/pebble/pull/240 for more details.

Hopefully Python ships with everything needed to implement a simple HTTP proxy, with strictly what we need for the parallelization of integration tests.

This PR implements this kind of HTTP proxy, and remove the coupling to Traefik.

This PR has been tested successfully with integration tests on Pebble under Linux for Python 2.x and Python 3.x, and the proxy alone has been also tested successfully on Windows (no integration tests can be run for now on this platform).

* Create a python proxy

* Refactor proxy config

* Working logic

* Resolve from the path

* Give proxy process to the ACMEServer context manager
2019-06-14 16:28:14 -07:00
Brad Warren
20ca47dec6 Bump stale threshold to 1 year. (#7149)
While I expect stale bot will close out 150 - 250 issues, that'll still leave us with 400+ open issues. My concern is that with a threshold of 6 months, most of these 400 issues will be in the same state 6 months from now and stale bot will annoy people by asking them if their issue is still valid too frequently.

Doubling the stale threshold to 1 year should mitigate this problem a bit I think.
2019-06-14 15:51:15 -07:00
sydneyli
6c53f5d8ed Turn off session tickets for versions of Nginx that support it (#7092)
* Turn off session tickets for versions of Nginx that support it

In line with Mozilla's security recommendations.

* Changelog.

* Set version before installing config files

* lint: remove unused import

* windows testfix

* another windows testfix?

* Testing path of updating src file with old nginx

* Fix windows, and make config update tests fail if update doesn't happen
2019-06-14 13:44:50 -07:00
Tim White
352218510a Update changelog 2019-04-26 09:34:17 +10:00
Tim White
463d089407 Detect private DNS zones in Google and skip them until we get to a public zone 2019-04-26 09:31:39 +10:00
874 changed files with 14029 additions and 11154 deletions

119
.azure-pipelines/INSTALL.md Normal file
View File

@@ -0,0 +1,119 @@
# Configuring Azure Pipelines with Certbot
Let's begin. All pipelines are defined in `.azure-pipelines`. Currently there are two:
* `.azure-pipelines/main.yml` is the main one, executed on PRs for master, and pushes to master,
* `.azure-pipelines/advanced.yml` add installer testing on top of the main pipeline, and is executed for `test-*` branches, release branches, and nightly run for master.
Several templates are defined in `.azure-pipelines/templates`. These YAML files aggregate common jobs configuration that can be reused in several pipelines.
Unlike Travis, where CodeCov is working without any action required, CodeCov supports Azure Pipelines
using the coverage-bash utility (not python-coverage for now) only if you provide the Codecov repo token
using the `CODECOV_TOKEN` environment variable. So `CODECOV_TOKEN` needs to be set as a secured
environment variable to allow the main pipeline to publish coverage reports to CodeCov.
This INSTALL.md file explains how to configure Azure Pipelines with Certbot in order to execute the CI/CD logic defined in `.azure-pipelines` folder with it.
During this installation step, warnings describing user access and legal comitments will be displayed like this:
```
!!! ACCESS REQUIRED !!!
```
This document suppose that the Azure DevOps organization is named _certbot_, and the Azure DevOps project is also _certbot_.
## Useful links
* https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema
* https://www.azuredevopslabs.com/labs/azuredevops/github-integration/
* https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/python?view=azure-devops
## Prerequisites
### Having a GitHub account
Use your GitHub user for a normal GitHub account, or a user that has administrative rights to the GitHub organization if relevant.
### Having an Azure DevOps account
- Go to https://dev.azure.com/, click "Start free with GitHub"
- Login to GitHub
```
!!! ACCESS REQUIRED !!!
Personal user data (email + profile info, in read-only)
```
- Microsoft will create a Live account using the email referenced for the GitHub account. This account is also linked to GitHub account (meaning you can log it using GitHub authentication)
- Proceed with account registration (birth date, country), add details about name and email contact
```
!!! ACCESS REQUIRED !!!
Microsoft proposes to send commercial links to this mail
Azure DevOps terms of service need to be accepted
```
_Logged to Azure DevOps, account is ready._
### Installing Azure Pipelines to GitHub
- On GitHub, go to Marketplace
- Select Azure Pipeline, and "Set up a plan"
- Select Free, then "Install it for free"
- Click "Complete order and begin installation"
```
!!! ACCESS !!!
Azure Pipeline needs RW on code, RO on metadata, RW on checks, commit statuses, deployments, issues, pull requests.
RW access here is required to allow update of the pipelines YAML files from Azure DevOps interface, and to
update the status of builds and PRs on GitHub side when Azure Pipelines are triggered.
Note however that no admin access is defined here: this means that Azure Pipelines cannot do anything with
protected branches, like master, and cannot modify the security context around this on GitHub.
Access can be defined for all or only selected repositories, which is nice.
```
- Redirected to Azure DevOps, select the account created in _Having an Azure DevOps account_ section.
- Select the organization, and click "Create a new project" (let's name it the same than the targetted github repo)
- The Visibility is public, to profit from 10 parallel jobs
```
!!! ACCESS !!!
Azure Pipelines needs access to the GitHub account (in term of beeing able to check it is valid), and the Resources shared between the GitHub account and Azure Pipelines.
```
_Done. We can move to pipelines configuration._
## Import an existing pipelines from `.azure-pipelines` folder
- On Azure DevOps, go to your organization (eg. _certbot_) then your project (eg. _certbot_)
- Click "Pipelines" tab
- Click "New pipeline"
- Where is your code?: select "__Use the classic editor__"
__Warning: Do not choose the GitHub option in Where is your code? section. Indeed, this option will trigger an OAuth
grant permissions from Azure Pipelines to GitHub in order to setup a GitHub OAuth Application. The permissions asked
then are way too large (admin level on almost everything), while the classic approach does not add any more
permissions, and works perfectly well.__
- Select GitHub in "Select your repository section", choose certbot/certbot in Repository, master in default branch.
- Click on YAML option for "Select a template"
- Choose a name for the pipeline (eg. test-pipeline), and browse to the actual pipeline YAML definition in the
"YAML file path" input (eg. `.azure-pipelines/test-pipeline.yml`)
- Click "Save & queue", choose the master branch to build the first pipeline, and click "Save and run" button.
_Done. Pipeline is operational. Repeat to add more pipelines from existing YAML files in `.azure-pipelines`._
## Add a secret variable to a pipeline (like `CODECOV_TOKEN`)
__NB: Following steps suppose that you already setup the YAML pipeline file to
consume the secret variable that these steps will create as an environment variable.
For a variable named `CODECOV_TOKEN` consuming the variable `codecov_token`,
in the YAML file this setup would take the form of the following:
```
steps:
- script: ./do_something_that_consumes_CODECOV_TOKEN # Eg. `codecov -F windows`
env:
CODECOV_TOKEN: $(codecov_token)
```
To set up a variable that is shared between pipelines, follow the instructions
at
https://docs.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups.
When adding variables to a group, don't forget to tick "Keep this value secret"
if it shouldn't be shared publcily.

View File

@@ -0,0 +1,20 @@
# Advanced pipeline for isolated checks and release purpose
trigger:
- test-*
- '*.x'
pr:
- test-*
# This pipeline is also nightly run on master
schedules:
- cron: "0 4 * * *"
displayName: Nightly build
branches:
include:
- master
always: true
jobs:
# Any addition here should be reflected in the release pipeline.
# It is advised to declare all jobs here as templates to improve maintainability.
- template: templates/tests-suite.yml
- template: templates/installer-tests.yml

12
.azure-pipelines/main.yml Normal file
View File

@@ -0,0 +1,12 @@
trigger:
# apache-parser-v2 is a temporary branch for doing work related to
# rewriting the parser in the Apache plugin.
- apache-parser-v2
- master
pr:
- apache-parser-v2
- master
- '*.x'
jobs:
- template: templates/tests-suite.yml

View File

@@ -0,0 +1,13 @@
# Release pipeline to build and deploy Certbot for Windows for GitHub release tags
trigger:
tags:
include:
- v*
pr: none
jobs:
# Any addition here should be reflected in the advanced pipeline.
# It is advised to declare all jobs here as templates to improve maintainability.
- template: templates/tests-suite.yml
- template: templates/installer-tests.yml
- template: templates/changelog.yml

View File

@@ -0,0 +1,14 @@
jobs:
- job: changelog
pool:
vmImage: vs2017-win2016
steps:
- bash: |
CERTBOT_VERSION="$(cd certbot && python -c "import certbot; print(certbot.__version__)" && cd ~-)"
"${BUILD_REPOSITORY_LOCALPATH}\tools\extract_changelog.py" "${CERTBOT_VERSION}" >> "${BUILD_ARTIFACTSTAGINGDIRECTORY}/release_notes.md"
displayName: Prepare changelog
- task: PublishPipelineArtifact@1
inputs:
path: $(Build.ArtifactStagingDirectory)
artifact: changelog
displayName: Publish changelog

View File

@@ -0,0 +1,54 @@
jobs:
- job: installer_build
pool:
vmImage: vs2017-win2016
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.7
architecture: x86
addToPath: true
- script: python windows-installer/construct.py
displayName: Build Certbot installer
- task: CopyFiles@2
inputs:
sourceFolder: $(System.DefaultWorkingDirectory)/windows-installer/build/nsis
contents: '*.exe'
targetFolder: $(Build.ArtifactStagingDirectory)
- task: PublishPipelineArtifact@1
inputs:
path: $(Build.ArtifactStagingDirectory)
artifact: windows-installer
displayName: Publish Windows installer
- job: installer_run
dependsOn: installer_build
strategy:
matrix:
win2019:
imageName: windows-2019
win2016:
imageName: vs2017-win2016
win2012r2:
imageName: vs2015-win2012r2
pool:
vmImage: $(imageName)
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: windows-installer
path: $(Build.SourcesDirectory)/bin
displayName: Retrieve Windows installer
- script: $(Build.SourcesDirectory)\bin\certbot-beta-installer-win32.exe /S
displayName: Install Certbot
- powershell: Invoke-WebRequest https://www.python.org/ftp/python/3.8.0/python-3.8.0-amd64-webinstall.exe -OutFile C:\py3-setup.exe
displayName: Get Python
- script: C:\py3-setup.exe /quiet PrependPath=1 InstallAllUsers=1 Include_launcher=1 InstallLauncherAllUsers=1 Include_test=0 Include_doc=0 Include_dev=1 Include_debug=0 Include_tcltk=0 TargetDir=C:\py3
displayName: Install Python
- script: |
py -3 -m venv venv
venv\Scripts\python tools\pip_install.py -e certbot-ci
displayName: Prepare Certbot-CI
- script: |
set PATH=%ProgramFiles(x86)%\Certbot\bin;%PATH%
venv\Scripts\python -m pytest certbot-ci\certbot_integration_tests\certbot_tests -n 4
displayName: Run integration tests

View File

@@ -0,0 +1,38 @@
jobs:
- job: test
pool:
vmImage: vs2017-win2016
strategy:
matrix:
py35:
PYTHON_VERSION: 3.5
TOXENV: py35
py37-cover:
PYTHON_VERSION: 3.7
TOXENV: py37-cover
integration-certbot:
PYTHON_VERSION: 3.7
TOXENV: integration-certbot
PYTEST_ADDOPTS: --numprocesses 4
variables:
- group: certbot-common
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: $(PYTHON_VERSION)
addToPath: true
- script: python tools/pip_install.py -U tox coverage
displayName: Install dependencies
- script: python -m tox
displayName: Run tox
# We do not require codecov report upload to succeed. So to avoid to break the pipeline if
# something goes wrong, each command is suffixed with a command that hides any non zero exit
# codes and echoes an informative message instead.
- bash: |
curl -s https://codecov.io/bash -o codecov-bash || echo "Failed to download codecov-bash"
chmod +x codecov-bash || echo "Failed to apply execute permissions on codecov-bash"
./codecov-bash -F windows || echo "Codecov did not collect coverage reports"
condition: in(variables['TOXENV'], 'py37-cover', 'integration-certbot')
env:
CODECOV_TOKEN: $(codecov_token)
displayName: Publish coverage

View File

@@ -4,11 +4,15 @@ coverage:
default: off
linux:
flags: linux
target: auto
# Fixed target instead of auto set by #7173, can
# be removed when flags in Codecov are added back.
target: 97.4
threshold: 0.1
base: auto
windows:
flags: windows
target: auto
# Fixed target instead of auto set by #7173, can
# be removed when flags in Codecov are added back.
target: 97.4
threshold: 0.1
base: auto

View File

@@ -1,2 +1,5 @@
[run]
omit = */setup.py
[report]
omit = */setup.py

9
.github/stale.yml vendored
View File

@@ -1,11 +1,12 @@
# Configuration for https://github.com/marketplace/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 180
daysUntilStale: 365
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# When changing this value, be sure to also update markComment below.
daysUntilClose: 30
# Ignore issues with an assignee (defaults to false)
exemptAssignees: true
@@ -18,8 +19,8 @@ markComment: >
We've made a lot of changes to Certbot since this issue was opened. If you
still have this issue with an up-to-date version of Certbot, can you please
add a comment letting us know? This helps us to better see what issues are
still affecting our users. If there is no further activity, this issue will
be automatically closed.
still affecting our users. If there is no activity in the next 30 days, this
issue will be automatically closed.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >

2
.gitignore vendored
View File

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

7
.isort.cfg Normal file
View File

@@ -0,0 +1,7 @@
[settings]
skip_glob=venv*
skip=letsencrypt-auto-source
force_sort_within_sections=True
force_single_line=True
order_by_type=False
line_length=400

View File

@@ -24,6 +24,11 @@ persistent=yes
# usually to register additional checkers.
load-plugins=linter_plugin
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=pywintypes,win32api,win32file,win32security
[MESSAGES CONTROL]
@@ -41,10 +46,14 @@ load-plugins=linter_plugin
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=fixme,locally-disabled,locally-enabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
# abstract-class-not-used cannot be disabled locally (at least in
# pylint 1.4.1), same for abstract-class-little-used
# CERTBOT COMMENT
# 1) Once certbot codebase is claimed to be compatible exclusively with Python 3,
# the useless-object-inheritance check can be enabled again, and code fixed accordingly.
# 2) Check unsubscriptable-object tends to create a lot of false positives. Let's disable it.
# See https://github.com/PyCQA/pylint/issues/1498.
# 3) Same as point 2 for no-value-for-parameter.
# See https://github.com/PyCQA/pylint/issues/2820.
disable=fixme,locally-disabled,locally-enabled,bad-continuation,no-self-use,invalid-name,cyclic-import,duplicate-code,design,import-outside-toplevel,useless-object-inheritance,unsubscriptable-object,no-value-for-parameter,no-else-return,no-else-raise,no-else-break,no-else-continue
[REPORTS]
@@ -297,40 +306,6 @@ valid-classmethod-first-arg=cls
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# Maximum number of arguments for function / method
max-args=6
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=(unused)?_.*|dummy
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=12
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to

View File

@@ -1,4 +1,5 @@
language: python
dist: xenial
cache:
directories:
@@ -8,6 +9,8 @@ before_script:
- 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ulimit -n 1024 ; fi'
# On Travis, the fastest parallelization for integration tests has proved to be 4.
- 'if [[ "$TOXENV" == *"integration"* ]]; then export PYTEST_ADDOPTS="--numprocesses 4"; fi'
# Use Travis retry feature for farm tests since they are flaky
- 'if [[ "$TOXENV" == "travis-test-farm"* ]]; then export TRAVIS_RETRY=travis_retry; fi'
- export TOX_TESTENV_PASSENV=TRAVIS
# Only build pushes to the master branch, PRs, and branches beginning with
@@ -16,6 +19,9 @@ before_script:
# is a cap of on the number of simultaneous runs.
branches:
only:
# apache-parser-v2 is a temporary branch for doing work related to
# rewriting the parser in the Apache plugin.
- apache-parser-v2
- master
- /^\d+\.\d+\.x$/
- /^test-.*$/
@@ -24,47 +30,43 @@ branches:
not-on-master: &not-on-master
if: NOT (type = push AND branch = master)
# Jobs for the extended test suite are executed for cron jobs and pushes on non-master branches.
# Jobs for the extended test suite are executed for cron jobs and pushes to
# non-development branches. See the explanation for apache-parser-v2 above.
extended-test-suite: &extended-test-suite
if: type = cron OR (type = push AND branch != master)
if: type = cron OR (type = push AND branch NOT IN (apache-parser-v2, master))
matrix:
include:
# Main test suite
- python: "2.7"
env: ACME_SERVER=pebble TOXENV=integration
sudo: required
services: docker
<<: *not-on-master
# This job is always executed, including on master
- python: "2.7"
env: TOXENV=py27-cover FYI="py27 tests + code coverage"
- python: "2.7"
- python: "3.7"
env: TOXENV=lint
<<: *not-on-master
- python: "3.4"
env: TOXENV=mypy
<<: *not-on-master
- python: "3.5"
env: TOXENV=mypy
<<: *not-on-master
- python: "2.7"
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
sudo: required
services: docker
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
env: TOXENV='py27-{acme,apache,apache-v2,certbot,dns,nginx}-oldest'
<<: *not-on-master
- python: "3.4"
env: TOXENV=py34
sudo: required
services: docker
<<: *not-on-master
- python: "3.7"
dist: xenial
env: TOXENV=py37
sudo: required
services: docker
<<: *not-on-master
- python: "3.8"
env: TOXENV=py38
<<: *not-on-master
- sudo: required
env: TOXENV=apache_compat
@@ -73,15 +75,11 @@ matrix:
addons:
<<: *not-on-master
- sudo: required
env: TOXENV=le_auto_trusty
env: TOXENV=le_auto_xenial
services: docker
before_install:
addons:
<<: *not-on-master
- python: "2.7"
env: TOXENV=apacheconftest-with-pebble
sudo: required
services: docker
<<: *not-on-master
- python: "2.7"
env: TOXENV=nginxroundtrip
@@ -117,7 +115,6 @@ matrix:
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite
- python: "3.7"
dist: xenial
env: TOXENV=py37 CERTBOT_NO_PIN=1
<<: *extended-test-suite
- python: "2.7"
@@ -130,29 +127,39 @@ matrix:
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: TOXENV=py27-certbot-oldest
<<: *extended-test-suite
- python: "2.7"
env: TOXENV=py27-nginx-oldest
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration-certbot-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration-certbot-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration-nginx-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
- python: "2.7"
env: ACME_SERVER=boulder-v2 TOXENV=integration-nginx-oldest
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
sudo: required
services: docker
<<: *extended-test-suite
@@ -166,9 +173,11 @@ matrix:
env: TOXENV=py36
<<: *extended-test-suite
- python: "3.7"
dist: xenial
env: TOXENV=py37
<<: *extended-test-suite
- python: "3.8"
env: TOXENV=py38
<<: *extended-test-suite
- python: "3.4"
env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
@@ -200,20 +209,20 @@ matrix:
services: docker
<<: *extended-test-suite
- python: "3.7"
dist: xenial
env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- python: "3.7"
dist: xenial
env: ACME_SERVER=boulder-v2 TOXENV=integration
sudo: required
services: docker
<<: *extended-test-suite
- sudo: required
env: TOXENV=le_auto_xenial
services: docker
- python: "3.8"
env: ACME_SERVER=boulder-v1 TOXENV=integration
<<: *extended-test-suite
- python: "3.8"
env: ACME_SERVER=boulder-v2 TOXENV=integration
<<: *extended-test-suite
- sudo: required
env: TOXENV=le_auto_jessie
@@ -234,6 +243,9 @@ matrix:
- language: generic
env: TOXENV=py27
os: osx
# Using this osx_image is a workaround for
# https://travis-ci.community/t/xcode-8-3-homebrew-outdated-error/3798.
osx_image: xcode10.2
addons:
homebrew:
packages:
@@ -243,6 +255,9 @@ matrix:
- language: generic
env: TOXENV=py3
os: osx
# Using this osx_image is a workaround for
# https://travis-ci.community/t/xcode-8-3-homebrew-outdated-error/3798.
osx_image: xcode10.2
addons:
homebrew:
packages:
@@ -257,7 +272,6 @@ addons:
apt:
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
- python-dev
- python-virtualenv
- gcc
- libaugeas0
- libssl-dev
@@ -267,8 +281,17 @@ addons:
- nginx-light
- openssl
install: "$(command -v pip || command -v pip3) install codecov tox"
script: tox
# tools/pip_install.py is used to pin packages to a known working version
# except in tests where the environment variable CERTBOT_NO_PIN is set.
# virtualenv is listed here explicitly to make sure it is upgraded when
# CERTBOT_NO_PIN is set to work around failures we've seen when using an older
# version of virtualenv.
install: 'tools/pip_install.py -U codecov tox virtualenv'
# Most of the time TRAVIS_RETRY is an empty string, and has no effect on the
# script command. It is set only to `travis_retry` during farm tests, in
# order to trigger the Travis retry feature, and compensate the inherent
# flakiness of these specific tests.
script: '$TRAVIS_RETRY tox'
after_success: '[ "$TOXENV" == "py27-cover" ] && codecov -F linux'

View File

@@ -15,8 +15,10 @@ Authors
* [Alex Gaynor](https://github.com/alex)
* [Alex Halderman](https://github.com/jhalderm)
* [Alex Jordan](https://github.com/strugee)
* [Alex Zorin](https://github.com/alexzorin)
* [Amjad Mashaal](https://github.com/TheNavigat)
* [Andrew Murray](https://github.com/radarhere)
* [Andrzej Górski](https://github.com/andrzej3393)
* [Anselm Levskaya](https://github.com/levskaya)
* [Antoine Jacoutot](https://github.com/ajacoutot)
* [asaph](https://github.com/asaph)
@@ -126,6 +128,7 @@ Authors
* [Joubin Jabbari](https://github.com/joubin)
* [Juho Juopperi](https://github.com/jkjuopperi)
* [Kane York](https://github.com/riking)
* [Kenichi Maehashi](https://github.com/kmaehashi)
* [Kenneth Skovhede](https://github.com/kenkendk)
* [Kevin Burke](https://github.com/kevinburke)
* [Kevin London](https://github.com/kevinlondon)
@@ -161,8 +164,10 @@ Authors
* [Michael Schumacher](https://github.com/schumaml)
* [Michael Strache](https://github.com/Jarodiv)
* [Michael Sverdlin](https://github.com/sveder)
* [Michael Watters](https://github.com/blackknight36)
* [Michal Moravec](https://github.com/https://github.com/Majkl578)
* [Michal Papis](https://github.com/mpapis)
* [Mickaël Schoentgen](https://github.com/BoboTiG)
* [Minn Soe](https://github.com/MinnSoe)
* [Min RK](https://github.com/minrk)
* [Miquel Ruiz](https://github.com/miquelruiz)
@@ -226,6 +231,7 @@ Authors
* [Stavros Korokithakis](https://github.com/skorokithakis)
* [Stefan Weil](https://github.com/stweil)
* [Steve Desmond](https://github.com/stevedesmond-ca)
* [sydneyli](https://github.com/sydneyli)
* [Tan Jay Jun](https://github.com/jayjun)
* [Tapple Gao](https://github.com/tapple)
* [Telepenin Nikolay](https://github.com/telepenin)

File diff suppressed because it is too large Load Diff

1
CHANGELOG.md Symbolic link
View File

@@ -0,0 +1 @@
certbot/CHANGELOG.md

View File

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

View File

@@ -1,21 +1,20 @@
# This Dockerfile builds an image for development.
FROM ubuntu:xenial
FROM debian:buster
# Note: this only exposes the port to other docker containers.
EXPOSE 80 443
WORKDIR /opt/certbot/src
# TODO: Install Apache/Nginx for plugin development.
COPY . .
RUN apt-get update && \
apt-get install apache2 git nginx-light -y && \
letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
apt-get install apache2 git python3-dev python3-venv gcc libaugeas0 \
libssl-dev libffi-dev ca-certificates openssl nginx-light -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
/var/tmp/*
RUN VENV_NAME="../venv" python tools/venv.py
RUN VENV_NAME="../venv3" python3 tools/venv3.py
ENV PATH /opt/certbot/venv/bin:$PATH
ENV PATH /opt/certbot/venv3/bin:$PATH

View File

@@ -1,75 +0,0 @@
# https://github.com/letsencrypt/letsencrypt/pull/431#issuecomment-103659297
# it is more likely developers will already have ubuntu:trusty rather
# than e.g. debian:jessie and image size differences are negligible
FROM ubuntu:trusty
MAINTAINER Jakub Warmuz <jakub@warmuz.org>
MAINTAINER William Budington <bill@eff.org>
# Note: this only exposes the port to other docker containers. You
# still have to bind to 443@host at runtime, as per the ACME spec.
EXPOSE 443
# TODO: make sure --config-dir and --work-dir cannot be changed
# through the CLI (certbot-docker wrapper that uses standalone
# authenticator and text mode only?)
VOLUME /etc/letsencrypt /var/lib/letsencrypt
WORKDIR /opt/certbot
# no need to mkdir anything:
# https://docs.docker.com/reference/builder/#copy
# If <dest> doesn't exist, it is created along with all missing
# directories in its path.
ENV DEBIAN_FRONTEND=noninteractive
COPY letsencrypt-auto-source/letsencrypt-auto /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto
RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
/var/tmp/*
# the above is not likely to change, so by putting it further up the
# Dockerfile we make sure we cache as much as possible
COPY setup.py README.rst CHANGELOG.md MANIFEST.in letsencrypt-auto-source/pieces/pipstrap.py /opt/certbot/src/
# all above files are necessary for setup.py and venv setup, however,
# package source code directory has to be copied separately to a
# subdirectory...
# https://docs.docker.com/reference/builder/#copy: "If <src> is a
# directory, the entire contents of the directory are copied,
# including filesystem metadata. Note: The directory itself is not
# copied, just its contents." Order again matters, three files are far
# more likely to be cached than the whole project directory
COPY certbot /opt/certbot/src/certbot/
COPY acme /opt/certbot/src/acme/
COPY certbot-apache /opt/certbot/src/certbot-apache/
COPY certbot-nginx /opt/certbot/src/certbot-nginx/
RUN VIRTUALENV_NO_DOWNLOAD=1 virtualenv --no-site-packages -p python2 /opt/certbot/venv
# PATH is set now so pipstrap upgrades the correct (v)env
ENV PATH /opt/certbot/venv/bin:$PATH
RUN /opt/certbot/venv/bin/python /opt/certbot/src/pipstrap.py && \
/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
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/certbot/src" (it's stays in the underlaying image);
# this might also help in debugging: you can "docker run --entrypoint
# bash" and investigate, apply patches, etc.
# set up certbot/letsencrypt wrapper to warn people about Dockerfile changes
COPY tools/docker-warning.sh /opt/certbot/bin/certbot
RUN ln -s /opt/certbot/bin/certbot /opt/certbot/bin/letsencrypt
ENV PATH /opt/certbot/bin:$PATH
ENTRYPOINT [ "certbot" ]

View File

@@ -1,131 +0,0 @@
.. This file contains a series of comments that are used to include sections of this README in other files. Do not modify these comments unless you know what you are doing. tag:intro-begin
Certbot is part of EFFs effort to encrypt the entire Internet. Secure communication over the Web relies on HTTPS, which requires the use of a digital certificate that lets browsers verify the identity of web servers (e.g., is that really google.com?). Web servers obtain their certificates from trusted third parties called certificate authorities (CAs). Certbot is an easy-to-use client that fetches a certificate from Lets Encrypt—an open certificate authority launched by the EFF, Mozilla, and others—and deploys it to a web server.
Anyone who has gone through the trouble of setting up a secure website knows what a hassle getting and maintaining a certificate is. Certbot and Lets Encrypt can automate away the pain and let you turn on and manage HTTPS with simple commands. Using Certbot and Let's Encrypt is free, so theres no need to arrange payment.
How you use Certbot depends on the configuration of your web server. The best way to get started is to use our `interactive guide <https://certbot.eff.org>`_. It generates instructions based on your configuration settings. In most cases, youll need `root or administrator access <https://certbot.eff.org/faq/#does-certbot-require-root-administrator-privileges>`_ to your web server to run Certbot.
Certbot is meant to be run directly on your web server, not on your personal computer. If youre using a hosted service and dont have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Lets Encrypt.
Certbot is a fully-featured, extensible client for the Let's
Encrypt CA (or any other CA that speaks the `ACME
<https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md>`_
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.
Contributing
------------
If you'd like to contribute to this project please read `Developer Guide
<https://certbot.eff.org/docs/contributing.html>`_.
This project is governed by `EFF's Public Projects Code of Conduct <https://www.eff.org/pages/eppcode>`_.
.. _installation:
How to run the client
---------------------
The easiest way to install and run Certbot is by visiting `certbot.eff.org`_,
where you can find the correct instructions for many web server and OS
combinations. For more information, see `Get Certbot
<https://certbot.eff.org/docs/install.html>`_.
.. _certbot.eff.org: https://certbot.eff.org/
Understanding the client in more depth
--------------------------------------
To understand what the client is doing in detail, it's important to
understand the way it uses plugins. Please see the `explanation of
plugins <https://certbot.eff.org/docs/using.html#plugins>`_ in
the User Guide.
Links
=====
.. Do not modify this comment unless you know what you're doing. tag:links-begin
Documentation: https://certbot.eff.org/docs
Software project: https://github.com/certbot/certbot
Notes for developers: https://certbot.eff.org/docs/contributing.html
Main Website: https://certbot.eff.org
Let's Encrypt Website: https://letsencrypt.org
Community: https://community.letsencrypt.org
ACME spec: http://ietf-wg-acme.github.io/acme/
ACME working area in github: https://github.com/ietf-wg-acme/acme
|build-status| |coverage| |docs| |container|
.. |build-status| image:: https://travis-ci.com/certbot/certbot.svg?branch=master
:target: https://travis-ci.com/certbot/certbot
:alt: Travis CI status
.. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg
:target: https://codecov.io/gh/certbot/certbot
:alt: Coverage status
.. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/
:target: https://readthedocs.org/projects/letsencrypt/
:alt: Documentation status
.. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status
:target: https://quay.io/repository/letsencrypt/letsencrypt
:alt: Docker Repository on Quay.io
.. Do not modify this comment unless you know what you're doing. tag:links-end
System Requirements
===================
See https://certbot.eff.org/docs/install.html#system-requirements.
.. Do not modify this comment unless you know what you're doing. tag:intro-end
.. Do not modify this comment unless you know what you're doing. tag:features-begin
Current Features
=====================
* Supports multiple web servers:
- apache/2.x
- nginx/0.8.48+
- webroot (adds files to webroot directories in order to prove control of
domains and obtain certs)
- standalone (runs its own simple webserver to prove you control a domain)
- other server software via `third party plugins <https://certbot.eff.org/docs/using.html#third-party-plugins>`_
* The private key is generated locally on your system.
* Can talk to the Let's Encrypt CA or optionally to other ACME
compliant services.
* Can get domain-validated (DV) certificates.
* Can revoke certificates.
* Adjustable RSA key bit-length (2048 (default), 4096, ...).
* Can optionally install a http -> https redirect, so your site effectively
runs https only (Apache only)
* Fully automated.
* Configuration changes are logged and can be reverted.
* Supports an interactive text UI, or can be driven entirely from the
command line.
* Free and Open Source Software, made with Python.
.. Do not modify this comment unless you know what you're doing. tag:features-end
For extensive documentation on using and contributing to Certbot, go to https://certbot.eff.org/docs. If you would like to contribute to the project or run the latest code from git, you should read our `developer guide <https://certbot.eff.org/docs/contributing.html>`_.

1
README.rst Symbolic link
View File

@@ -0,0 +1 @@
certbot/README.rst

View File

@@ -3,4 +3,6 @@ include README.rst
include pytest.ini
recursive-include docs *
recursive-include examples *
recursive-include acme/testdata *
recursive-include tests *
global-exclude __pycache__
global-exclude *.py[cod]

View File

@@ -13,7 +13,6 @@ import warnings
#
# It is based on
# https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py
import josepy as jose
for mod in list(sys.modules):
@@ -21,30 +20,3 @@ for mod in list(sys.modules):
# preserved (acme.jose.* is josepy.*)
if mod == 'josepy' or mod.startswith('josepy.'):
sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod]
# This class takes a similar approach to the cryptography project to deprecate attributes
# in public modules. See the _ModuleWithDeprecation class here:
# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129
class _TLSSNI01DeprecationModule(object):
"""
Internal class delegating to a module, and displaying warnings when
attributes related to TLS-SNI-01 are accessed.
"""
def __init__(self, module):
self.__dict__['_module'] = module
def __getattr__(self, attr):
if 'TLSSNI01' in attr:
warnings.warn('{0} attribute is deprecated, and will be removed soon.'.format(attr),
DeprecationWarning, stacklevel=2)
return getattr(self._module, attr)
def __setattr__(self, attr, value): # pragma: no cover
setattr(self._module, attr, value)
def __delattr__(self, attr): # pragma: no cover
delattr(self._module, attr)
def __dir__(self): # pragma: no cover
return ['_module'] + dir(self._module)

View File

@@ -3,19 +3,13 @@ import abc
import functools
import hashlib
import logging
import socket
import sys
from cryptography.hazmat.primitives import hashes # type: ignore
import josepy as jose
import OpenSSL
import requests
import six
from acme import errors
from acme import crypto_util
from acme import fields
from acme import _TLSSNI01DeprecationModule
logger = logging.getLogger(__name__)
@@ -60,8 +54,7 @@ class UnrecognizedChallenge(Challenge):
object.__setattr__(self, "jobj", jobj)
def to_partial_json(self):
# pylint: disable=no-member
return self.jobj
return self.jobj # pylint: disable=no-member
@classmethod
def from_json(cls, jobj):
@@ -119,7 +112,7 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
:rtype: bool
"""
parts = self.key_authorization.split('.') # pylint: disable=no-member
parts = self.key_authorization.split('.')
if len(parts) != 2:
logger.debug("Key authorization (%r) is not well formed",
self.key_authorization)
@@ -237,7 +230,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse):
return verified
@Challenge.register # pylint: disable=too-many-ancestors
@Challenge.register
class DNS01(KeyAuthorizationChallenge):
"""ACME dns-01 challenge."""
response_cls = DNS01Response
@@ -327,7 +320,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
return True
@Challenge.register # pylint: disable=too-many-ancestors
@Challenge.register
class HTTP01(KeyAuthorizationChallenge):
"""ACME http-01 challenge."""
response_cls = HTTP01Response
@@ -367,148 +360,6 @@ class HTTP01(KeyAuthorizationChallenge):
return self.key_authorization(account_key)
@ChallengeResponse.register
class TLSSNI01Response(KeyAuthorizationChallengeResponse):
"""ACME tls-sni-01 challenge response."""
typ = "tls-sni-01"
DOMAIN_SUFFIX = b".acme.invalid"
"""Domain name suffix."""
PORT = 443
"""Verification port as defined by the protocol.
You can override it (e.g. for testing) by passing ``port`` to
`simple_verify`.
"""
@property
def z(self): # pylint: disable=invalid-name
"""``z`` value used for verification.
:rtype bytes:
"""
return hashlib.sha256(
self.key_authorization.encode("utf-8")).hexdigest().lower().encode()
@property
def z_domain(self):
"""Domain name used for verification, generated from `z`.
:rtype bytes:
"""
return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX
def gen_cert(self, key=None, bits=2048):
"""Generate tls-sni-01 certificate.
:param OpenSSL.crypto.PKey key: Optional private key used in
certificate generation. If not provided (``None``), then
fresh key will be generated.
:param int bits: Number of bits for newly generated key.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
if key is None:
key = OpenSSL.crypto.PKey()
key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
return crypto_util.gen_ss_cert(key, [
# z_domain is too big to fit into CN, hence first dummy domain
'dummy', self.z_domain.decode()], force_san=True), key
def probe_cert(self, domain, **kwargs):
"""Probe tls-sni-01 challenge certificate.
:param unicode domain:
"""
# TODO: domain is not necessary if host is provided
if "host" not in kwargs:
host = socket.gethostbyname(domain)
logger.debug('%s resolved to %s', domain, host)
kwargs["host"] = host
kwargs.setdefault("port", self.PORT)
kwargs["name"] = self.z_domain
# TODO: try different methods?
return crypto_util.probe_sni(**kwargs)
def verify_cert(self, cert):
"""Verify tls-sni-01 challenge certificate.
:param OpensSSL.crypto.X509 cert: Challenge certificate.
:returns: Whether the certificate was successfully verified.
:rtype: bool
"""
# pylint: disable=protected-access
sans = crypto_util._pyopenssl_cert_or_req_san(cert)
logger.debug('Certificate %s. SANs: %s', cert.digest('sha256'), sans)
return self.z_domain.decode() in sans
def simple_verify(self, chall, domain, account_public_key,
cert=None, **kwargs):
"""Simple verify.
Verify ``validation`` using ``account_public_key``, optionally
probe tls-sni-01 certificate and check using `verify_cert`.
:param .challenges.TLSSNI01 chall: Corresponding challenge.
:param str domain: Domain name being validated.
:param JWK account_public_key:
:param OpenSSL.crypto.X509 cert: Optional certificate. If not
provided (``None``) certificate will be retrieved using
`probe_cert`.
:param int port: Port used to probe the certificate.
:returns: ``True`` iff client's control of the domain has been
verified.
:rtype: bool
"""
if not self.verify(chall, account_public_key):
logger.debug("Verification of key authorization in response failed")
return False
if cert is None:
try:
cert = self.probe_cert(domain=domain, **kwargs)
except errors.Error as error:
logger.debug(str(error), exc_info=True)
return False
return self.verify_cert(cert)
@Challenge.register # pylint: disable=too-many-ancestors
class TLSSNI01(KeyAuthorizationChallenge):
"""ACME tls-sni-01 challenge."""
response_cls = TLSSNI01Response
typ = response_cls.typ
# boulder#962, ietf-wg-acme#22
#n = jose.Field("n", encoder=int, decoder=int)
def validation(self, account_key, **kwargs):
"""Generate validation.
:param JWK account_key:
:param OpenSSL.crypto.PKey cert_key: Optional private key used
in certificate generation. If not provided (``None``), then
fresh key will be generated.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
@ChallengeResponse.register
class TLSALPN01Response(KeyAuthorizationChallengeResponse):
"""ACME TLS-ALPN-01 challenge response.
@@ -520,7 +371,7 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
typ = "tls-alpn-01"
@Challenge.register # pylint: disable=too-many-ancestors
@Challenge.register
class TLSALPN01(KeyAuthorizationChallenge):
"""ACME tls-alpn-01 challenge.
@@ -617,7 +468,3 @@ class DNSResponse(ChallengeResponse):
"""
return chall.check_validation(self.validation, account_public_key)
# Patching ourselves to warn about TLS-SNI challenge deprecation and removal.
sys.modules[__name__] = _TLSSNI01DeprecationModule(sys.modules[__name__])

View File

@@ -5,25 +5,26 @@ import datetime
from email.utils import parsedate_tz
import heapq
import logging
import time
import re
import sys
import time
import six
from six.moves import http_client # pylint: disable=import-error
import josepy as jose
import OpenSSL
import requests
from requests.adapters import HTTPAdapter
from requests_toolbelt.adapters.source import SourceAddressAdapter
import six
from six.moves import http_client # pylint: disable=import-error
from acme import crypto_util
from acme import errors
from acme import jws
from acme import messages
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict, List, Set, Text
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Text # pylint: disable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
@@ -33,7 +34,6 @@ logger = logging.getLogger(__name__)
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if sys.version_info < (2, 7, 9): # pragma: no cover
try:
# pylint: disable=no-member
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() # type: ignore
except AttributeError:
import urllib3.contrib.pyopenssl # pylint: disable=import-error
@@ -44,7 +44,7 @@ DEFAULT_NETWORK_TIMEOUT = 45
DER_CONTENT_TYPE = 'application/pkix-cert'
class ClientBase(object): # pylint: disable=too-many-instance-attributes
class ClientBase(object):
"""ACME client base object.
:ivar messages.Directory directory:
@@ -123,6 +123,22 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
"""
return self.update_registration(regr, update={'status': 'deactivated'})
def deactivate_authorization(self, authzr):
# type: (messages.AuthorizationResource) -> messages.AuthorizationResource
"""Deactivate authorization.
:param messages.AuthorizationResource authzr: The Authorization resource
to be deactivated.
:returns: The Authorization resource that was deactivated.
:rtype: `.AuthorizationResource`
"""
body = messages.UpdateAuthorization(status='deactivated')
response = self._post(authzr.uri, body)
return self._authzr_from_response(response,
authzr.body.identifier, authzr.uri)
def _authzr_from_response(self, response, identifier=None, uri=None):
authzr = messages.AuthorizationResource(
body=messages.Authorization.from_json(response.json()),
@@ -238,7 +254,6 @@ class Client(ClientBase):
URI from which the resource will be downloaded.
"""
# pylint: disable=too-many-arguments
self.key = key
if net is None:
net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl)
@@ -264,7 +279,6 @@ class Client(ClientBase):
assert response.status_code == http_client.CREATED
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
return self._regr_from_response(response)
def query_registration(self, regr):
@@ -419,7 +433,6 @@ class Client(ClientBase):
was marked by the CA as invalid
"""
# pylint: disable=too-many-locals
assert max_attempts > 0
attempts = collections.defaultdict(int) # type: Dict[messages.AuthorizationResource, int]
exhausted = set()
@@ -450,7 +463,6 @@ class Client(ClientBase):
updated[authzr] = updated_authzr
attempts[authzr] += 1
# pylint: disable=no-member
if updated_authzr.body.status not in (
messages.STATUS_VALID, messages.STATUS_INVALID):
if attempts[authzr] < max_attempts:
@@ -591,7 +603,6 @@ class ClientV2(ClientBase):
if response.status_code == 200 and 'Location' in response.headers:
raise errors.ConflictError(response.headers.get('Location'))
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
regr = self._regr_from_response(response)
self.net.account = regr
return regr
@@ -715,7 +726,7 @@ class ClientV2(ClientBase):
for authzr in responses:
if authzr.body.status != messages.STATUS_VALID:
for chall in authzr.body.challenges:
if chall.error != None:
if chall.error is not None:
failed.append(authzr)
if failed:
raise errors.ValidationError(failed)
@@ -765,29 +776,13 @@ class ClientV2(ClientBase):
def _post_as_get(self, *args, **kwargs):
"""
Send GET request using the POST-as-GET protocol if needed.
The request will be first issued using POST-as-GET for ACME v2. If the ACME CA servers do
not support this yet and return an error, request will be retried using GET.
For ACME v1, only GET request will be tried, as POST-as-GET is not supported.
Send GET request using the POST-as-GET protocol.
:param args:
:param kwargs:
:return:
"""
if self.acme_version >= 2:
# We add an empty payload for POST-as-GET requests
new_args = args[:1] + (None,) + args[1:]
try:
return self._post(*new_args, **kwargs)
except messages.Error as error:
if error.code == 'malformed':
logger.debug('Error during a POST-as-GET request, '
'your ACME CA server may not support it:\n%s', error)
logger.debug('Retrying request with GET.')
else: # pragma: no cover
raise
# If POST-as-GET is not supported yet, we use a GET instead.
return self.net.get(*args, **kwargs)
new_args = args[:1] + (None,) + args[1:]
return self._post(*new_args, **kwargs)
class BackwardsCompatibleClientV2(object):
@@ -931,7 +926,7 @@ class BackwardsCompatibleClientV2(object):
return self.client.external_account_required()
class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
class ClientNetwork(object):
"""Wrapper around requests that signs POSTs for authentication.
Also adds user agent, and handles Content-Type.
@@ -957,7 +952,6 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
def __init__(self, key, account=None, alg=jose.RS256, verify_ssl=True,
user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT,
source_address=None):
# pylint: disable=too-many-arguments
self.key = key
self.account = account
self.alg = alg
@@ -1065,7 +1059,6 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
return response
def _send_request(self, method, url, *args, **kwargs):
# pylint: disable=too-many-locals
"""Send HTTP request.
Makes sure that `verify_ssl` is respected. Logs request and
@@ -1112,10 +1105,9 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
err_regex = r".*host='(\S*)'.*Max retries exceeded with url\: (\/\w*).*(\[Errno \d+\])([A-Za-z ]*)"
m = re.match(err_regex, str(e))
if m is None:
raise # pragma: no cover
else:
host, path, _err_no, err_msg = m.groups()
raise ValueError("Requesting {0}{1}:{2}".format(host, path, err_msg))
raise # pragma: no cover
host, path, _err_no, err_msg = m.groups()
raise ValueError("Requesting {0}{1}:{2}".format(host, path, err_msg))
# If content is DER, log the base64 of it instead of raw bytes, to keep
# binary data out of the logs.
@@ -1181,8 +1173,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
if error.code == 'badNonce':
logger.debug('Retrying request after error:\n%s', error)
return self._post_once(*args, **kwargs)
else:
raise
raise
def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE,
acme_version=1, **kwargs):

View File

@@ -6,15 +6,15 @@ import os
import re
import socket
from OpenSSL import crypto
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
import josepy as jose
from OpenSSL import crypto
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
from acme import errors
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Callable, Union, Tuple, Optional
# pylint: enable=unused-import, no-name-in-module
from acme.magic_typing import Callable # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Optional # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Tuple # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Union # pylint: disable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
@@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
_DEFAULT_SSL_METHOD = SSL.SSLv23_METHOD # type: ignore
class SSLSocket(object): # pylint: disable=too-few-public-methods
class SSLSocket(object):
"""SSL wrapper for sockets.
:ivar socket sock: Original wrapped socket.
@@ -74,7 +74,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
class FakeConnection(object):
"""Fake OpenSSL.SSL.Connection."""
# pylint: disable=too-few-public-methods,missing-docstring
# pylint: disable=missing-docstring
def __init__(self, connection):
self._wrapped = connection

View File

@@ -29,7 +29,12 @@ class NonceError(ClientError):
class BadNonce(NonceError):
"""Bad nonce error."""
def __init__(self, nonce, error, *args, **kwargs):
super(BadNonce, self).__init__(*args, **kwargs)
# MyPy complains here that there is too many arguments for BaseException constructor.
# This is an error fixed in typeshed, see https://github.com/python/mypy/issues/4183
# The fix is included in MyPy>=0.740, but upgrading it would bring dozen of errors due to
# new types definitions. So we ignore the error until the code base is fixed to match
# with MyPy>=0.740 referential.
super(BadNonce, self).__init__(*args, **kwargs) # type: ignore
self.nonce = nonce
self.error = error
@@ -48,7 +53,8 @@ class MissingNonce(NonceError):
"""
def __init__(self, response, *args, **kwargs):
super(MissingNonce, self).__init__(*args, **kwargs)
# See comment in BadNonce constructor above for an explanation of type: ignore here.
super(MissingNonce, self).__init__(*args, **kwargs) # type: ignore
self.response = response
def __str__(self):
@@ -83,6 +89,7 @@ class PollError(ClientError):
return '{0}(exhausted={1!r}, updated={2!r})'.format(
self.__class__.__name__, self.exhausted, self.updated)
class ValidationError(Error):
"""Error for authorization failures. Contains a list of authorization
resources, each of which is invalid and should have an error field.
@@ -91,9 +98,11 @@ class ValidationError(Error):
self.failed_authzrs = failed_authzrs
super(ValidationError, self).__init__()
class TimeoutError(Error):
class TimeoutError(Error): # pylint: disable=redefined-builtin
"""Error for when polling an authorization or an order times out."""
class IssuanceError(Error):
"""Error sent by the server after requesting issuance of a certificate."""
@@ -105,6 +114,7 @@ class IssuanceError(Error):
self.error = error
super(IssuanceError, self).__init__()
class ConflictError(ClientError):
"""Error for when the server returns a 409 (Conflict) HTTP status.

View File

@@ -4,7 +4,6 @@ import logging
import josepy as jose
import pyrfc3339
logger = logging.getLogger(__name__)

View File

@@ -40,10 +40,10 @@ class Signature(jose.Signature):
class JWS(jose.JWS):
"""ACME-specific JWS. Includes none, url, and kid in protected header."""
signature_cls = Signature
__slots__ = jose.JWS._orig_slots # pylint: disable=no-member
__slots__ = jose.JWS._orig_slots
@classmethod
# pylint: disable=arguments-differ,too-many-arguments
# pylint: disable=arguments-differ
def sign(cls, payload, key, alg, nonce, url=None, kid=None):
# Per ACME spec, jwk and kid are mutually exclusive, so only include a
# jwk field if kid is not provided.

View File

@@ -1,6 +1,7 @@
"""Shim class to not have to depend on typing module in prod."""
import sys
class TypingClass(object):
"""Ignore import errors by getting anything"""
def __getattr__(self, name):

View File

@@ -1,37 +1,55 @@
"""ACME protocol messages."""
import json
import josepy as jose
import six
from acme import challenges
from acme import errors
from acme import fields
from acme import jws
from acme import util
try:
from collections.abc import Hashable # pylint: disable=no-name-in-module
except ImportError: # pragma: no cover
from collections import Hashable
import josepy as jose
from acme import challenges
from acme import errors
from acme import fields
from acme import util
from acme import jws
OLD_ERROR_PREFIX = "urn:acme:error:"
ERROR_PREFIX = "urn:ietf:params:acme:error:"
ERROR_CODES = {
'accountDoesNotExist': 'The request specified an account that does not exist',
'alreadyRevoked': 'The request specified a certificate to be revoked that has' \
' already been revoked',
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
'badNonce': 'The client sent an unacceptable anti-replay nonce',
'badPublicKey': 'The JWS was signed by a public key the server does not support',
'badRevocationReason': 'The revocation reason provided is not allowed by the server',
'badSignatureAlgorithm': 'The JWS was signed with an algorithm the server does not support',
'caa': 'Certification Authority Authorization (CAA) records forbid the CA from issuing' \
' a certificate',
'compound': 'Specific error conditions are indicated in the "subproblems" array',
'connection': ('The server could not connect to the client to verify the'
' domain'),
'dns': 'There was a problem with a DNS query during identifier validation',
'dnssec': 'The server could not validate a DNSSEC signed domain',
'incorrectResponse': 'Response recieved didn\'t match the challenge\'s requirements',
# deprecate invalidEmail
'invalidEmail': 'The provided email for a registration was invalid',
'invalidContact': 'The provided contact URI was invalid',
'malformed': 'The request message was malformed',
'rejectedIdentifier': 'The server will not issue certificates for the identifier',
'orderNotReady': 'The request attempted to finalize an order that is not ready to be finalized',
'rateLimited': 'There were too many requests of a given type',
'serverInternal': 'The server experienced an internal error',
'tls': 'The server experienced a TLS error during domain verification',
'unauthorized': 'The client lacks sufficient authorization',
'unsupportedContact': 'A contact URL for an account used an unsupported protocol scheme',
'unknownHost': 'The server could not resolve a domain name',
'unsupportedIdentifier': 'An identifier is of an unsupported type',
'externalAccountRequired': 'The server requires external account binding',
}
@@ -128,7 +146,7 @@ class _Constant(jose.JSONDeSerializable, Hashable): # type: ignore
if jobj not in cls.POSSIBLE_NAMES: # pylint: disable=unsupported-membership-test
raise jose.DeserializationError(
'{0} not recognized'.format(cls.__name__))
return cls.POSSIBLE_NAMES[jobj] # pylint: disable=unsubscriptable-object
return cls.POSSIBLE_NAMES[jobj]
def __repr__(self):
return '{0}({1})'.format(self.__class__.__name__, self.name)
@@ -153,6 +171,7 @@ STATUS_VALID = Status('valid')
STATUS_INVALID = Status('invalid')
STATUS_REVOKED = Status('revoked')
STATUS_READY = Status('ready')
STATUS_DEACTIVATED = Status('deactivated')
class IdentifierType(_Constant):
@@ -456,7 +475,7 @@ class Authorization(ResourceBody):
:ivar datetime.datetime expires:
"""
identifier = jose.Field('identifier', decoder=Identifier.from_json)
identifier = jose.Field('identifier', decoder=Identifier.from_json, omitempty=True)
challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True)
@@ -486,6 +505,12 @@ class NewAuthorization(Authorization):
resource = fields.Resource(resource_type)
class UpdateAuthorization(Authorization):
"""Update authorization."""
resource_type = 'authz'
resource = fields.Resource(resource_type)
class AuthorizationResource(ResourceWithURI):
"""Authorization Resource.

View File

@@ -1,29 +1,22 @@
"""Support for standalone client challenge solvers. """
import argparse
import collections
import functools
import logging
import os
import socket
import sys
import threading
from six.moves import BaseHTTPServer # type: ignore # pylint: disable=import-error
from six.moves import http_client # pylint: disable=import-error
from six.moves import socketserver # type: ignore # pylint: disable=import-error
import OpenSSL
from acme import challenges
from acme import crypto_util
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from acme import _TLSSNI01DeprecationModule
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
# six.moves.* | pylint: disable=no-member,attribute-defined-outside-init
# pylint: disable=too-few-public-methods,no-init
# pylint: disable=no-init
class TLSServer(socketserver.TCPServer):
@@ -51,7 +44,7 @@ class TLSServer(socketserver.TCPServer):
return socketserver.TCPServer.server_bind(self)
class ACMEServerMixin: # pylint: disable=old-style-class
class ACMEServerMixin:
"""ACME server common settings mixin."""
# TODO: c.f. #858
server_version = "ACME client standalone challenge solver"
@@ -112,7 +105,6 @@ class BaseDualNetworkedServers(object):
"""Wraps socketserver.TCPServer.serve_forever"""
for server in self.servers:
thread = threading.Thread(
# pylint: disable=no-member
target=server.serve_forever)
thread.start()
self.threads.append(thread)
@@ -132,35 +124,6 @@ class BaseDualNetworkedServers(object):
self.threads = []
class TLSSNI01Server(TLSServer, ACMEServerMixin):
"""TLSSNI01 Server."""
def __init__(self, server_address, certs, ipv6=False):
TLSServer.__init__(
self, server_address, BaseRequestHandlerWithLogging, certs=certs, ipv6=ipv6)
class TLSSNI01DualNetworkedServers(BaseDualNetworkedServers):
"""TLSSNI01Server Wrapper. Tries everything for both. Failures for one don't
affect the other."""
def __init__(self, *args, **kwargs):
BaseDualNetworkedServers.__init__(self, TLSSNI01Server, *args, **kwargs)
class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler):
"""BaseRequestHandler with logging."""
def log_message(self, format, *args): # pylint: disable=redefined-builtin
"""Log arbitrary message."""
logger.debug("%s - - %s", self.client_address[0], format % args)
def handle(self):
"""Handle request."""
self.log_message("Incoming request")
socketserver.BaseRequestHandler.handle(self)
class HTTPServer(BaseHTTPServer.HTTPServer):
"""Generic HTTP Server."""
@@ -263,43 +226,3 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""
return functools.partial(
cls, simple_http_resources=simple_http_resources)
def simple_tls_sni_01_server(cli_args, forever=True):
"""Run simple standalone TLSSNI01 server."""
logging.basicConfig(level=logging.DEBUG)
parser = argparse.ArgumentParser()
parser.add_argument(
"-p", "--port", default=0, help="Port to serve at. By default "
"picks random free port.")
args = parser.parse_args(cli_args[1:])
certs = {}
_, hosts, _ = next(os.walk('.')) # type: ignore # https://github.com/python/mypy/issues/465
for host in hosts:
with open(os.path.join(host, "cert.pem")) as cert_file:
cert_contents = cert_file.read()
with open(os.path.join(host, "key.pem")) as key_file:
key_contents = key_file.read()
certs[host.encode()] = (
OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, key_contents),
OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert_contents))
server = TLSSNI01Server(('', int(args.port)), certs=certs)
logger.info("Serving at https://%s:%s...", *server.socket.getsockname()[:2])
if forever: # pragma: no cover
server.serve_forever()
else:
server.handle_request()
# Patching ourselves to warn about TLS-SNI challenge deprecation and removal.
sys.modules[__name__] = _TLSSNI01DeprecationModule(sys.modules[__name__])
if __name__ == "__main__":
sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover

View File

@@ -12,10 +12,9 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
import shlex
import sys
here = os.path.abspath(os.path.dirname(__file__))

View File

@@ -26,8 +26,10 @@ Workflow:
- Deactivate Account
"""
from contextlib import contextmanager
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
import josepy as jose
import OpenSSL
from acme import challenges
@@ -36,7 +38,6 @@ from acme import crypto_util
from acme import errors
from acme import messages
from acme import standalone
import josepy as jose
# Constants:

View File

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

View File

@@ -1,9 +1,10 @@
from setuptools import setup
from setuptools import find_packages
from setuptools.command.test import test as TestCommand
import sys
version = '0.36.0.dev0'
from setuptools import find_packages
from setuptools import setup
from setuptools.command.test import test as TestCommand
version = '1.1.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
@@ -14,8 +15,8 @@ install_requires = [
# 1.1.0+ is required to avoid the warnings described at
# https://github.com/certbot/josepy/issues/13.
'josepy>=1.1.0',
# Connection.set_tlsext_host_name (>=0.13)
'mock',
# Connection.set_tlsext_host_name (>=0.13)
'PyOpenSSL>=0.13.1',
'pyrfc3339',
'pytz',
@@ -73,6 +74,7 @@ setup(
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],

View File

@@ -3,13 +3,10 @@ import unittest
import josepy as jose
import mock
import OpenSSL
import requests
from six.moves.urllib import parse as urllib_parse
from six.moves.urllib import parse as urllib_parse # pylint: disable=relative-import
from acme import errors
from acme import test_util
import test_util
CERT = test_util.load_comparable_cert('cert.pem')
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
@@ -21,7 +18,6 @@ class ChallengeTest(unittest.TestCase):
from acme.challenges import Challenge
from acme.challenges import UnrecognizedChallenge
chall = UnrecognizedChallenge({"type": "foo"})
# pylint: disable=no-member
self.assertEqual(chall, Challenge.from_json(chall.jobj))
@@ -77,7 +73,6 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase):
class DNS01ResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import DNS01Response
@@ -149,7 +144,6 @@ class DNS01Test(unittest.TestCase):
class HTTP01ResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import HTTP01Response
@@ -259,152 +253,7 @@ class HTTP01Test(unittest.TestCase):
self.msg.update(token=b'..').good_token)
class TLSSNI01ResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import TLSSNI01
self.chall = TLSSNI01(
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
self.response = self.chall.response(KEY)
self.jmsg = {
'resource': 'challenge',
'type': 'tls-sni-01',
'keyAuthorization': self.response.key_authorization,
}
# pylint: disable=invalid-name
label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f'
label2 = b'b7793728f084394f2a1afd459556bb5c'
self.z = label1 + label2
self.z_domain = label1 + b'.' + label2 + b'.acme.invalid'
self.domain = 'foo.com'
def test_z_and_domain(self):
self.assertEqual(self.z, self.response.z)
self.assertEqual(self.z_domain, self.response.z_domain)
def test_to_partial_json(self):
self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'},
self.response.to_partial_json())
def test_from_json(self):
from acme.challenges import TLSSNI01Response
self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import TLSSNI01Response
hash(TLSSNI01Response.from_json(self.jmsg))
@mock.patch('acme.challenges.socket.gethostbyname')
@mock.patch('acme.challenges.crypto_util.probe_sni')
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
mock_gethostbyname.return_value = '127.0.0.1'
self.response.probe_cert('foo.com')
mock_gethostbyname.assert_called_once_with('foo.com')
mock_probe_sni.assert_called_once_with(
host='127.0.0.1', port=self.response.PORT,
name=self.z_domain)
self.response.probe_cert('foo.com', host='8.8.8.8')
mock_probe_sni.assert_called_with(
host='8.8.8.8', port=mock.ANY, name=mock.ANY)
self.response.probe_cert('foo.com', port=1234)
mock_probe_sni.assert_called_with(
host=mock.ANY, port=1234, name=mock.ANY)
self.response.probe_cert('foo.com', bar='baz')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz')
self.response.probe_cert('foo.com', name=b'xxx')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY,
name=self.z_domain)
def test_gen_verify_cert(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.response.gen_cert(key1)
self.assertEqual(key1, key2)
self.assertTrue(self.response.verify_cert(cert))
def test_gen_verify_cert_gen_key(self):
cert, key = self.response.gen_cert()
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
self.assertTrue(self.response.verify_cert(cert))
def test_verify_bad_cert(self):
self.assertFalse(self.response.verify_cert(
test_util.load_cert('cert.pem')))
def test_simple_verify_bad_key_authorization(self):
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
self.response.simple_verify(self.chall, "local", key2.public_key())
@mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True)
def test_simple_verify(self, mock_verify_cert):
mock_verify_cert.return_value = mock.sentinel.verification
self.assertEqual(
mock.sentinel.verification, self.response.simple_verify(
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert))
mock_verify_cert.assert_called_once_with(
self.response, mock.sentinel.cert)
@mock.patch('acme.challenges.TLSSNI01Response.probe_cert')
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
mock_probe_cert.side_effect = errors.Error
self.assertFalse(self.response.simple_verify(
self.chall, self.domain, KEY.public_key()))
class TLSSNI01Test(unittest.TestCase):
def setUp(self):
self.jmsg = {
'type': 'tls-sni-01',
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
}
from acme.challenges import TLSSNI01
self.msg = TLSSNI01(
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import TLSSNI01
self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import TLSSNI01
hash(TLSSNI01.from_json(self.jmsg))
def test_from_json_invalid_token_length(self):
from acme.challenges import TLSSNI01
self.jmsg['token'] = jose.encode_b64jose(b'abcd')
self.assertRaises(
jose.DeserializationError, TLSSNI01.from_json, self.jmsg)
@mock.patch('acme.challenges.TLSSNI01Response.gen_cert')
def test_validation(self, mock_gen_cert):
mock_gen_cert.return_value = ('cert', 'key')
self.assertEqual(('cert', 'key'), self.msg.validation(
KEY, cert_key=mock.sentinel.cert_key))
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
def test_deprecation_message(self):
with mock.patch('acme.warnings.warn') as mock_warn:
from acme.challenges import TLSSNI01
assert TLSSNI01
self.assertEqual(mock_warn.call_count, 1)
self.assertTrue('deprecated' in mock_warn.call_args[0][0])
class TLSALPN01ResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import TLSALPN01Response

View File

@@ -5,21 +5,19 @@ import datetime
import json
import unittest
from six.moves import http_client # pylint: disable=import-error
import josepy as jose
import mock
import OpenSSL
import requests
from six.moves import http_client # pylint: disable=import-error
from acme import challenges
from acme import errors
from acme import jws as acme_jws
from acme import messages
from acme import messages_test
from acme import test_util
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
import messages_test
import test_util
CERT_DER = test_util.load_vector('cert.der')
CERT_SAN_PEM = test_util.load_vector('cert-san.pem')
@@ -63,7 +61,7 @@ class ClientTestBase(unittest.TestCase):
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages.Registration(
contact=self.contact, key=KEY.public_key())
the_arg = dict(reg) # type: Dict
the_arg = dict(reg) # type: Dict
self.new_reg = messages.NewRegistration(**the_arg)
self.regr = messages.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1')
@@ -318,7 +316,6 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
class ClientTest(ClientTestBase):
"""Tests for acme.client.Client."""
# pylint: disable=too-many-instance-attributes,too-many-public-methods
def setUp(self):
super(ClientTest, self).setUp()
@@ -637,6 +634,14 @@ class ClientTest(ClientTestBase):
errors.PollError, self.client.poll_and_request_issuance,
csr, authzrs, mintime=mintime, max_attempts=2)
def test_deactivate_authorization(self):
authzb = self.authzr.body.update(status=messages.STATUS_DEACTIVATED)
self.response.json.return_value = authzb.to_json()
authzr = self.client.deactivate_authorization(self.authzr)
self.assertEqual(authzb, authzr.body)
self.assertEqual(self.client.net.post.call_count, 1)
self.assertTrue(self.authzr.uri in self.net.post.call_args_list[0][0])
def test_check_cert(self):
self.response.headers['Location'] = self.certr.uri
self.response.content = CERT_DER
@@ -880,19 +885,6 @@ class ClientV2Test(ClientTestBase):
new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce')
self.client.net.get.assert_not_called()
class FakeError(messages.Error): # pylint: disable=too-many-ancestors
"""Fake error to reproduce a malformed request ACME error"""
def __init__(self): # pylint: disable=super-init-not-called
pass
@property
def code(self):
return 'malformed'
self.client.net.post.side_effect = FakeError()
self.client.poll(self.authzr2) # pylint: disable=protected-access
self.client.net.get.assert_called_once_with(self.authzr2.uri)
class MockJSONDeSerializable(jose.JSONDeSerializable):
# pylint: disable=missing-docstring
@@ -909,7 +901,6 @@ class MockJSONDeSerializable(jose.JSONDeSerializable):
class ClientNetworkTest(unittest.TestCase):
"""Tests for acme.client.ClientNetwork."""
# pylint: disable=too-many-public-methods
def setUp(self):
self.verify_ssl = mock.MagicMock()
@@ -959,8 +950,8 @@ class ClientNetworkTest(unittest.TestCase):
def test_check_response_not_ok_jobj_error(self):
self.response.ok = False
self.response.json.return_value = messages.Error(
detail='foo', typ='serverInternal', title='some title').to_json()
self.response.json.return_value = messages.Error.with_code(
'serverInternal', detail='foo', title='some title').to_json()
# pylint: disable=protected-access
self.assertRaises(
messages.Error, self.net._check_response, self.response)
@@ -985,7 +976,7 @@ class ClientNetworkTest(unittest.TestCase):
self.response.json.side_effect = ValueError
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
self.response.headers['Content-Type'] = response_ct
# pylint: disable=protected-access,no-value-for-parameter
# pylint: disable=protected-access
self.assertEqual(
self.response, self.net._check_response(self.response))
@@ -999,7 +990,7 @@ class ClientNetworkTest(unittest.TestCase):
self.response.json.return_value = {}
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
self.response.headers['Content-Type'] = response_ct
# pylint: disable=protected-access,no-value-for-parameter
# pylint: disable=protected-access
self.assertEqual(
self.response, self.net._check_response(self.response))
@@ -1115,7 +1106,6 @@ class ClientNetworkTest(unittest.TestCase):
class ClientNetworkWithMockedResponseTest(unittest.TestCase):
"""Tests for acme.client.ClientNetwork which mock out response."""
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.client import ClientNetwork
@@ -1125,8 +1115,8 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.response.headers = {}
self.response.links = {}
self.response.checked = False
self.acmev1_nonce_response = mock.MagicMock(ok=False,
status_code=http_client.METHOD_NOT_ALLOWED)
self.acmev1_nonce_response = mock.MagicMock(
ok=False, status_code=http_client.METHOD_NOT_ALLOWED)
self.acmev1_nonce_response.headers = {}
self.obj = mock.MagicMock()
self.wrapped_obj = mock.MagicMock()

View File

@@ -5,15 +5,14 @@ import threading
import time
import unittest
import six
from six.moves import socketserver #type: ignore # pylint: disable=import-error
import josepy as jose
import OpenSSL
import six
from six.moves import socketserver # type: ignore # pylint: disable=import-error
from acme import errors
from acme import test_util
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
import test_util
class SSLSocketAndProbeSNITest(unittest.TestCase):
@@ -30,7 +29,6 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
class _TestServer(socketserver.TCPServer):
# pylint: disable=too-few-public-methods
# six.moves.* | pylint: disable=attribute-defined-outside-init,no-init
def server_bind(self): # pylint: disable=missing-docstring
@@ -40,7 +38,6 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
self.server = _TestServer(('', 0), socketserver.BaseRequestHandler)
self.port = self.server.socket.getsockname()[1]
self.server_thread = threading.Thread(
# pylint: disable=no-member
target=self.server.handle_request)
def tearDown(self):
@@ -67,7 +64,7 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
def test_probe_connection_error(self):
# pylint has a hard time with six
self.server.server_close() # pylint: disable=no-member
self.server.server_close()
original_timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(1)

View File

@@ -2,6 +2,7 @@
import importlib
import unittest
class JoseTest(unittest.TestCase):
"""Tests for acme.jose shim."""
@@ -20,11 +21,10 @@ class JoseTest(unittest.TestCase):
# We use the imports below with eval, but pylint doesn't
# understand that.
# pylint: disable=eval-used,unused-variable
import acme
import josepy
acme_jose_mod = eval(acme_jose_path)
josepy_mod = eval(josepy_path)
import acme # pylint: disable=unused-import
import josepy # pylint: disable=unused-import
acme_jose_mod = eval(acme_jose_path) # pylint: disable=eval-used
josepy_mod = eval(josepy_path) # pylint: disable=eval-used
self.assertIs(acme_jose_mod, josepy_mod)
self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute))

View File

@@ -3,8 +3,7 @@ import unittest
import josepy as jose
from acme import test_util
import test_util
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))

View File

@@ -5,9 +5,8 @@ import josepy as jose
import mock
from acme import challenges
from acme import test_util
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
import test_util
CERT = test_util.load_comparable_cert('cert.der')
CSR = test_util.load_comparable_csr('csr.der')
@@ -19,8 +18,7 @@ class ErrorTest(unittest.TestCase):
def setUp(self):
from acme.messages import Error, ERROR_PREFIX
self.error = Error(
detail='foo', typ=ERROR_PREFIX + 'malformed', title='title')
self.error = Error.with_code('malformed', detail='foo', title='title')
self.jobj = {
'detail': 'foo',
'title': 'some title',
@@ -28,7 +26,6 @@ class ErrorTest(unittest.TestCase):
}
self.error_custom = Error(typ='custom', detail='bar')
self.empty_error = Error()
self.jobj_custom = {'type': 'custom', 'detail': 'bar'}
def test_default_typ(self):
from acme.messages import Error
@@ -43,8 +40,7 @@ class ErrorTest(unittest.TestCase):
hash(Error.from_json(self.error.to_json()))
def test_description(self):
self.assertEqual(
'The request message was malformed', self.error.description)
self.assertEqual('The request message was malformed', self.error.description)
self.assertTrue(self.error_custom.description is None)
def test_code(self):
@@ -54,17 +50,17 @@ class ErrorTest(unittest.TestCase):
self.assertEqual(None, Error().code)
def test_is_acme_error(self):
from acme.messages import is_acme_error
from acme.messages import is_acme_error, Error
self.assertTrue(is_acme_error(self.error))
self.assertFalse(is_acme_error(self.error_custom))
self.assertFalse(is_acme_error(Error()))
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')
from acme.messages import Error, is_acme_error
arabic_error = Error.with_code(
'malformed', detail=u'\u0639\u062f\u0627\u0644\u0629', title='title')
self.assertTrue(is_acme_error(arabic_error))
def test_with_code(self):
@@ -305,8 +301,7 @@ class ChallengeBodyTest(unittest.TestCase):
from acme.messages import Error
from acme.messages import STATUS_INVALID
self.status = STATUS_INVALID
error = Error(typ='urn:ietf:params:acme:error:serverInternal',
detail='Unable to communicate with DNS server')
error = Error.with_code('serverInternal', detail='Unable to communicate with DNS server')
self.challb = ChallengeBody(
uri='http://challb', chall=self.chall, status=self.status,
error=error)

View File

@@ -1,26 +1,17 @@
"""Tests for acme.standalone."""
import multiprocessing
import os
import shutil
import socket
import threading
import tempfile
import unittest
import time
from contextlib import closing
from six.moves import http_client # pylint: disable=import-error
from six.moves import socketserver # type: ignore # pylint: disable=import-error
import josepy as jose
import mock
import requests
from six.moves import http_client # pylint: disable=import-error
from six.moves import socketserver # type: ignore # pylint: disable=import-error
from acme import challenges
from acme import crypto_util
from acme import errors
from acme import test_util
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
import test_util
class TLSServerTest(unittest.TestCase):
@@ -41,32 +32,6 @@ class TLSServerTest(unittest.TestCase):
server.server_close()
class TLSSNI01ServerTest(unittest.TestCase):
"""Test for acme.standalone.TLSSNI01Server."""
def setUp(self):
self.certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
test_util.load_cert('rsa2048_cert.pem'),
)}
from acme.standalone import TLSSNI01Server
self.server = TLSSNI01Server(('localhost', 0), certs=self.certs)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.start()
def tearDown(self):
self.server.shutdown()
self.thread.join()
def test_it(self):
host, port = self.server.socket.getsockname()[:2]
cert = crypto_util.probe_sni(
b'localhost', host=host, port=port, timeout=1)
self.assertEqual(jose.ComparableX509(cert),
jose.ComparableX509(self.certs[b'localhost'][1]))
class HTTP01ServerTest(unittest.TestCase):
"""Tests for acme.standalone.HTTP01Server."""
@@ -170,33 +135,6 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
prev_port = port
class TLSSNI01DualNetworkedServersTest(unittest.TestCase):
"""Test for acme.standalone.TLSSNI01DualNetworkedServers."""
def setUp(self):
self.certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
test_util.load_cert('rsa2048_cert.pem'),
)}
from acme.standalone import TLSSNI01DualNetworkedServers
self.servers = TLSSNI01DualNetworkedServers(('localhost', 0), certs=self.certs)
self.servers.serve_forever()
def tearDown(self):
self.servers.shutdown_and_server_close()
def test_connect(self):
socknames = self.servers.getsocknames()
# connect to all addresses
for sockname in socknames:
host, port = sockname[:2]
cert = crypto_util.probe_sni(
b'localhost', host=host, port=port, timeout=1)
self.assertEqual(jose.ComparableX509(cert),
jose.ComparableX509(self.certs[b'localhost'][1]))
class HTTP01DualNetworkedServersTest(unittest.TestCase):
"""Tests for acme.standalone.HTTP01DualNetworkedServers."""
@@ -247,60 +185,5 @@ class HTTP01DualNetworkedServersTest(unittest.TestCase):
self.assertFalse(self._test_http01(add=False))
class TestSimpleTLSSNI01Server(unittest.TestCase):
"""Tests for acme.standalone.simple_tls_sni_01_server."""
def setUp(self):
# mirror ../examples/standalone
self.test_cwd = tempfile.mkdtemp()
localhost_dir = os.path.join(self.test_cwd, 'localhost')
os.makedirs(localhost_dir)
shutil.copy(test_util.vector_path('rsa2048_cert.pem'),
os.path.join(localhost_dir, 'cert.pem'))
shutil.copy(test_util.vector_path('rsa2048_key.pem'),
os.path.join(localhost_dir, 'key.pem'))
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
sock.bind(('', 0))
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.port = sock.getsockname()[1]
from acme.standalone import simple_tls_sni_01_server
self.process = multiprocessing.Process(target=simple_tls_sni_01_server,
args=(['path', '-p', str(self.port)],))
self.old_cwd = os.getcwd()
os.chdir(self.test_cwd)
def tearDown(self):
os.chdir(self.old_cwd)
if self.process.is_alive():
self.process.terminate()
self.process.join(timeout=5)
# Check that we didn't timeout waiting for the process to
# terminate.
self.assertNotEqual(self.process.exitcode, None)
shutil.rmtree(self.test_cwd)
@mock.patch('acme.standalone.TLSSNI01Server.handle_request')
def test_mock(self, handle):
from acme.standalone import simple_tls_sni_01_server
simple_tls_sni_01_server(cli_args=['path', '-p', str(self.port)], forever=False)
self.assertEqual(handle.call_count, 1)
def test_live(self):
self.process.start()
cert = None
for _ in range(50):
time.sleep(0.1)
try:
cert = crypto_util.probe_sni(b'localhost', b'127.0.0.1', self.port)
break
except errors.Error: # pragma: no cover
pass
self.assertEqual(jose.ComparableX509(cert),
test_util.load_comparable_cert('rsa2048_cert.pem'))
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -4,19 +4,12 @@
"""
import os
import unittest
import pkg_resources
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import josepy as jose
from OpenSSL import crypto
def vector_path(*names):
"""Path to a test vector."""
return pkg_resources.resource_filename(
__name__, os.path.join('testdata', *names))
import pkg_resources
def load_vector(*names):
@@ -32,8 +25,7 @@ def _guess_loader(filename, loader_pem, loader_der):
return loader_pem
elif ext.lower() == '.der':
return loader_der
else: # pragma: no cover
raise ValueError("Loader could not be recognized based on extension")
raise ValueError("Loader could not be recognized based on extension") # pragma: no cover
def load_cert(*names):
@@ -73,23 +65,3 @@ def load_pyopenssl_private_key(*names):
loader = _guess_loader(
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_privatekey(loader, load_vector(*names))
def skip_unless(condition, reason): # pragma: no cover
"""Skip tests unless a condition holds.
This implements the basic functionality of unittest.skipUnless
which is only available on Python 2.7+.
:param bool condition: If ``False``, the test will be skipped
:param str reason: the reason for skipping the test
:rtype: callable
:returns: decorator that hides tests unless condition is ``True``
"""
if hasattr(unittest, "skipUnless"):
return unittest.skipUnless(condition, reason)
elif condition:
return lambda cls: cls
return lambda cls: None

View File

@@ -1,40 +0,0 @@
image: Visual Studio 2015
environment:
matrix:
- TOXENV: py35
- TOXENV: py37-cover
branches:
only:
- master
- /^\d+\.\d+\.x$/ # Version branches like X.X.X
- /^test-.*$/
init:
# Since master can receive only commits from PR that have already been tested, following
# condition avoid to launch all jobs except the coverage one for commits pushed to master.
- ps: |
if (-Not $Env:APPVEYOR_PULL_REQUEST_NUMBER -And $Env:APPVEYOR_REPO_BRANCH -Eq 'master' `
-And -Not ($Env:TOXENV -Like '*-cover'))
{ $Env:APPVEYOR_SKIP_FINALIZE_ON_EXIT = 'true'; Exit-AppVeyorBuild }
install:
# Use Python 3.7 by default
- "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%"
# Check env
- "python --version"
# Upgrade pip to avoid warnings
- "python -m pip install --upgrade pip"
# Ready to install tox and coverage
- "pip install tox codecov"
build: off
test_script:
- set TOX_TESTENV_PASSENV=APPVEYOR
# Test env is set by TOXENV env variable
- tox
on_success:
- if exist .coverage codecov -F windows

View File

@@ -1,7 +1,8 @@
include LICENSE.txt
include README.rst
recursive-include docs *
recursive-include certbot_apache/tests/testdata *
include certbot_apache/centos-options-ssl-apache.conf
include certbot_apache/options-ssl-apache.conf
recursive-include certbot_apache/augeas_lens *.aug
recursive-include tests *
include certbot_apache/_internal/centos-options-ssl-apache.conf
include certbot_apache/_internal/options-ssl-apache.conf
recursive-include certbot_apache/_internal/augeas_lens *.aug
global-exclude __pycache__
global-exclude *.py[cod]

View File

@@ -0,0 +1 @@
"""Certbot Apache plugin."""

View File

@@ -0,0 +1,243 @@
""" Utility functions for certbot-apache plugin """
import binascii
import fnmatch
import logging
import re
import subprocess
from certbot import errors
from certbot import util
from certbot.compat import os
logger = logging.getLogger(__name__)
def get_mod_deps(mod_name):
"""Get known module dependencies.
.. note:: This does not need to be accurate in order for the client to
run. This simply keeps things clean if the user decides to revert
changes.
.. warning:: If all deps are not included, it may cause incorrect parsing
behavior, due to enable_mod's shortcut for updating the parser's
currently defined modules (`.ApacheParser.add_mod`)
This would only present a major problem in extremely atypical
configs that use ifmod for the missing deps.
"""
deps = {
"ssl": ["setenvif", "mime"]
}
return deps.get(mod_name, [])
def get_file_path(vhost_path):
"""Get file path from augeas_vhost_path.
Takes in Augeas path and returns the file name
:param str vhost_path: Augeas virtual host path
:returns: filename of vhost
:rtype: str
"""
if not vhost_path or not vhost_path.startswith("/files/"):
return None
return _split_aug_path(vhost_path)[0]
def get_internal_aug_path(vhost_path):
"""Get the Augeas path for a vhost with the file path removed.
:param str vhost_path: Augeas virtual host path
:returns: Augeas path to vhost relative to the containing file
:rtype: str
"""
return _split_aug_path(vhost_path)[1]
def _split_aug_path(vhost_path):
"""Splits an Augeas path into a file path and an internal path.
After removing "/files", this function splits vhost_path into the
file path and the remaining Augeas path.
:param str vhost_path: Augeas virtual host path
:returns: file path and internal Augeas path
:rtype: `tuple` of `str`
"""
# Strip off /files
file_path = vhost_path[6:]
internal_path = []
# Remove components from the end of file_path until it becomes valid
while not os.path.exists(file_path):
file_path, _, internal_path_part = file_path.rpartition("/")
internal_path.append(internal_path_part)
return file_path, "/".join(reversed(internal_path))
def parse_define_file(filepath, varname):
""" Parses Defines from a variable in configuration file
:param str filepath: Path of file to parse
:param str varname: Name of the variable
:returns: Dict of Define:Value pairs
:rtype: `dict`
"""
return_vars = {}
# Get list of words in the variable
a_opts = util.get_var_from_file(varname, filepath).split()
for i, v in enumerate(a_opts):
# Handle Define statements and make sure it has an argument
if v == "-D" and len(a_opts) >= i+2:
var_parts = a_opts[i+1].partition("=")
return_vars[var_parts[0]] = var_parts[2]
elif len(v) > 2 and v.startswith("-D"):
# Found var with no whitespace separator
var_parts = v[2:].partition("=")
return_vars[var_parts[0]] = var_parts[2]
return return_vars
def unique_id():
""" Returns an unique id to be used as a VirtualHost identifier"""
return binascii.hexlify(os.urandom(16)).decode("utf-8")
def included_in_paths(filepath, paths):
"""
Returns true if the filepath is included in the list of paths
that may contain full paths or wildcard paths that need to be
expanded.
:param str filepath: Filepath to check
:params list paths: List of paths to check against
:returns: True if included
:rtype: bool
"""
return any([fnmatch.fnmatch(filepath, path) for path in paths])
def parse_defines(apachectl):
"""
Gets Defines from httpd process and returns a dictionary of
the defined variables.
:param str apachectl: Path to apachectl executable
:returns: dictionary of defined variables
:rtype: dict
"""
variables = dict()
define_cmd = [apachectl, "-t", "-D",
"DUMP_RUN_CFG"]
matches = parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
try:
matches.remove("DUMP_RUN_CFG")
except ValueError:
return {}
for match in matches:
if match.count("=") > 1:
logger.error("Unexpected number of equal signs in "
"runtime config dump.")
raise errors.PluginError(
"Error parsing Apache runtime variables")
parts = match.partition("=")
variables[parts[0]] = parts[2]
return variables
def parse_includes(apachectl):
"""
Gets Include directives from httpd process and returns a list of
their values.
:param str apachectl: Path to apachectl executable
:returns: list of found Include directive values
:rtype: list of str
"""
inc_cmd = [apachectl, "-t", "-D",
"DUMP_INCLUDES"]
return parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
def parse_modules(apachectl):
"""
Get loaded modules from httpd process, and return the list
of loaded module names.
:param str apachectl: Path to apachectl executable
:returns: list of found LoadModule module names
:rtype: list of str
"""
mod_cmd = [apachectl, "-t", "-D",
"DUMP_MODULES"]
return parse_from_subprocess(mod_cmd, r"(.*)_module")
def parse_from_subprocess(command, regexp):
"""Get values from stdout of subprocess command
:param list command: Command to run
:param str regexp: Regexp for parsing
:returns: list parsed from command output
:rtype: list
"""
stdout = _get_runtime_cfg(command)
return re.compile(regexp).findall(stdout)
def _get_runtime_cfg(command):
"""
Get runtime configuration info.
:param command: Command to run
:returns: stdout from command
"""
try:
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logger.error(
"Error running command %s for runtime parameters!%s",
command, os.linesep)
raise errors.MisconfigurationError(
"Error accessing loaded Apache parameters: {0}".format(
command))
# Small errors that do not impede
if proc.returncode != 0:
logger.warning("Error in checking parameter list: %s", stderr)
raise errors.MisconfigurationError(
"Apache is unable to check whether or not the module is "
"loaded because Apache is misconfigured.")
return stdout

View File

@@ -0,0 +1,242 @@
""" apacheconfig implementation of the ParserNode interfaces """
from functools import partial
from certbot import errors
from certbot_apache._internal import assertions
from certbot_apache._internal import interfaces
from certbot_apache._internal import parsernode_util as util
class ApacheParserNode(interfaces.ParserNode):
""" apacheconfig implementation of ParserNode interface.
Expects metadata `ac_ast` to be passed in, where `ac_ast` is the AST provided
by parsing the equivalent configuration text using the apacheconfig library.
"""
def __init__(self, **kwargs):
ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs) # pylint: disable=unused-variable
super(ApacheParserNode, self).__init__(**kwargs)
self.ancestor = ancestor
self.filepath = filepath
self.dirty = dirty
self.metadata = metadata
self._raw = self.metadata["ac_ast"]
def save(self, msg): # pragma: no cover
pass
def find_ancestors(self, name): # pylint: disable=unused-variable
"""Find ancestor BlockNodes with a given name"""
return [ApacheBlockNode(name=assertions.PASS,
parameters=assertions.PASS,
ancestor=self,
filepath=assertions.PASS,
metadata=self.metadata)]
class ApacheCommentNode(ApacheParserNode):
""" apacheconfig implementation of CommentNode interface """
def __init__(self, **kwargs):
comment, kwargs = util.commentnode_kwargs(kwargs) # pylint: disable=unused-variable
super(ApacheCommentNode, self).__init__(**kwargs)
self.comment = comment
def __eq__(self, other): # pragma: no cover
if isinstance(other, self.__class__):
return (self.comment == other.comment and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata and
self.filepath == other.filepath)
return False
class ApacheDirectiveNode(ApacheParserNode):
""" apacheconfig implementation of DirectiveNode interface """
def __init__(self, **kwargs):
name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
super(ApacheDirectiveNode, self).__init__(**kwargs)
self.name = name
self.parameters = parameters
self.enabled = enabled
self.include = None
def __eq__(self, other): # pragma: no cover
if isinstance(other, self.__class__):
return (self.name == other.name and
self.filepath == other.filepath and
self.parameters == other.parameters and
self.enabled == other.enabled and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
return False
def set_parameters(self, _parameters):
"""Sets the parameters for DirectiveNode"""
return
def _parameters_from_string(text):
text = text.strip()
words = []
word = ""
quote = None
escape = False
for c in text:
if c.isspace() and not quote:
if word:
words.append(word)
word = ""
else:
word += c
if not escape:
if not quote and c in "\"\'":
quote = c
elif c == quote:
words.append(word[1:-1])
word = ""
quote = None
escape = c == "\\"
if word:
words.append(word)
return tuple(words)
class ApacheBlockNode(ApacheDirectiveNode):
""" apacheconfig implementation of BlockNode interface """
def __init__(self, **kwargs):
super(ApacheBlockNode, self).__init__(**kwargs)
self._raw_children = self._raw
children = []
for raw_node in self._raw_children:
metadata = self.metadata.copy()
metadata['ac_ast'] = raw_node
if raw_node.typestring == "comment":
node = ApacheCommentNode(comment=raw_node.name[2:],
metadata=metadata, ancestor=self,
filepath=self.filepath)
elif raw_node.typestring == "block":
parameters = _parameters_from_string(raw_node.arguments)
node = ApacheBlockNode(name=raw_node.tag, parameters=parameters,
metadata=metadata, ancestor=self,
filepath=self.filepath, enabled=self.enabled)
else:
parameters = ()
if raw_node.value:
parameters = _parameters_from_string(raw_node.value)
node = ApacheDirectiveNode(name=raw_node.name, parameters=parameters,
metadata=metadata, ancestor=self,
filepath=self.filepath, enabled=self.enabled)
children.append(node)
self.children = tuple(children)
def __eq__(self, other): # pragma: no cover
if isinstance(other, self.__class__):
return (self.name == other.name and
self.filepath == other.filepath and
self.parameters == other.parameters and
self.children == other.children and
self.enabled == other.enabled and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
return False
def _add_child_thing(self, raw_string, partial_node, position):
position = len(self._raw_children) if not position else position
# Cap position to length to mimic AugeasNode behavior. TODO: document that this happens
position = min(len(self._raw_children), position)
raw_ast = self._raw_children.add(position, raw_string)
metadata = self.metadata.copy()
metadata['ac_ast'] = raw_ast
new_node = partial_node(ancestor=self, metadata=metadata, filepath=self.filepath)
# Update metadata
children = list(self.children)
children.insert(position, new_node)
self.children = tuple(children)
return new_node
def add_child_block(self, name, parameters=None, position=None):
"""Adds a new BlockNode to the sequence of children"""
parameters_str = " " + " ".join(parameters) if parameters else ""
if not parameters:
parameters = []
partial_block = partial(ApacheBlockNode, name=name,
parameters=tuple(parameters), enabled=self.enabled)
return self._add_child_thing("\n<%s%s>\n</%s>" % (name, parameters_str, name),
partial_block, position)
def add_child_directive(self, name, parameters=None, position=None):
"""Adds a new DirectiveNode to the sequence of children"""
parameters_str = " " + " ".join(parameters) if parameters else ""
if not parameters: # TODO (mona): test
parameters = [] # pragma: no cover
partial_block = partial(ApacheDirectiveNode, name=name,
parameters=tuple(parameters), enabled=self.enabled)
return self._add_child_thing("\n%s%s" % (name, parameters_str), partial_block, position)
def add_child_comment(self, comment="", position=None):
"""Adds a new CommentNode to the sequence of children"""
partial_comment = partial(ApacheCommentNode, comment=comment)
return self._add_child_thing(comment, partial_comment, position)
def find_blocks(self, name, exclude=True): # pylint: disable=unused-argument
"""Recursive search of BlockNodes from the sequence of children"""
return [ApacheBlockNode(name=assertions.PASS,
parameters=assertions.PASS,
ancestor=self,
filepath=assertions.PASS,
metadata=self.metadata)]
def find_directives(self, name, exclude=True): # pylint: disable=unused-argument
"""Recursive search of DirectiveNodes from the sequence of children"""
return [ApacheDirectiveNode(name=assertions.PASS,
parameters=assertions.PASS,
ancestor=self,
filepath=assertions.PASS,
metadata=self.metadata)]
def find_comments(self, comment, exact=False): # pylint: disable=unused-argument
"""Recursive search of DirectiveNodes from the sequence of children"""
return [ApacheCommentNode(comment=assertions.PASS,
ancestor=self,
filepath=assertions.PASS,
metadata=self.metadata)]
# TODO (mona): test
def delete_child(self, child): # pragma: no cover
"""Deletes a ParserNode from the sequence of children"""
index = -1
i = None
for i, elem in enumerate(self.children):
if elem == child:
index = i
break
if index < 0:
raise errors.PluginError("Could not find child node to delete")
children_list = list(self.children)
thing = children_list.pop(i)
self.children = tuple(children_list)
self._raw_children.remove(i)
return thing
def unsaved_files(self): # pragma: no cover
"""Returns a list of unsaved filepaths"""
return [assertions.PASS]
def parsed_paths(self): # pragma: no cover
"""Returns a list of parsed configuration file paths"""
return [assertions.PASS]
interfaces.CommentNode.register(ApacheCommentNode)
interfaces.DirectiveNode.register(ApacheDirectiveNode)
interfaces.BlockNode.register(ApacheBlockNode)

View File

@@ -0,0 +1,142 @@
"""Dual parser node assertions"""
import fnmatch
from certbot_apache._internal import interfaces
PASS = "CERTBOT_PASS_ASSERT"
def assertEqual(first, second):
""" Equality assertion """
if isinstance(first, interfaces.CommentNode):
assertEqualComment(first, second)
elif isinstance(first, interfaces.DirectiveNode):
assertEqualDirective(first, second)
# Do an extra interface implementation assertion, as the contents were
# already checked for BlockNode in the assertEqualDirective
if isinstance(first, interfaces.BlockNode):
assert isinstance(second, interfaces.BlockNode)
# Skip tests if filepath includes the pass value. This is done
# because filepath is variable of the base ParserNode interface, and
# unless the implementation is actually done, we cannot assume getting
# correct results from boolean assertion for dirty
if not isPass(first.filepath) and not isPass(second.filepath):
assert first.dirty == second.dirty
# We might want to disable this later if testing with two separate
# (but identical) directory structures.
assert first.filepath == second.filepath
def assertEqualComment(first, second): # pragma: no cover
""" Equality assertion for CommentNode """
assert isinstance(first, interfaces.CommentNode)
assert isinstance(second, interfaces.CommentNode)
if not isPass(first.comment) and not isPass(second.comment): # type: ignore
assert first.comment == second.comment # type: ignore
def _assertEqualDirectiveComponents(first, second): # pragma: no cover
""" Handles assertion for instance variables for DirectiveNode and BlockNode"""
# Enabled value cannot be asserted, because Augeas implementation
# is unable to figure that out.
# assert first.enabled == second.enabled
if not isPass(first.name) and not isPass(second.name):
assert first.name == second.name
if not isPass(first.parameters) and not isPass(second.parameters):
assert first.parameters == second.parameters
def assertEqualDirective(first, second):
""" Equality assertion for DirectiveNode """
assert isinstance(first, interfaces.DirectiveNode)
assert isinstance(second, interfaces.DirectiveNode)
_assertEqualDirectiveComponents(first, second)
def isPass(value): # pragma: no cover
"""Checks if the value is set to PASS"""
if isinstance(value, bool):
return True
return PASS in value
def isPassDirective(block):
""" Checks if BlockNode or DirectiveNode should pass the assertion """
if isPass(block.name):
return True
if isPass(block.parameters): # pragma: no cover
return True
if isPass(block.filepath): # pragma: no cover
return True
return False
def isPassComment(comment):
""" Checks if CommentNode should pass the assertion """
if isPass(comment.comment):
return True
if isPass(comment.filepath): # pragma: no cover
return True
return False
def isPassNodeList(nodelist): # pragma: no cover
""" Checks if a ParserNode in the nodelist should pass the assertion,
this function is used for results of find_* methods. Unimplemented find_*
methods should return a sequence containing a single ParserNode instance
with assertion pass string."""
try:
node = nodelist[0]
except IndexError:
node = None
if not node: # pragma: no cover
return False
if isinstance(node, interfaces.DirectiveNode):
return isPassDirective(node)
return isPassComment(node)
def assertEqualSimple(first, second):
""" Simple assertion """
if not isPass(first) and not isPass(second):
assert first == second
def isEqualVirtualHost(first, second):
"""
Checks that two VirtualHost objects are similar. There are some built
in differences with the implementations: VirtualHost created by ParserNode
implementation doesn't have "path" defined, as it was used for Augeas path
and that cannot obviously be used in the future. Similarly the legacy
version lacks "node" variable, that has a reference to the BlockNode for the
VirtualHost.
"""
return (
first.name == second.name and
first.aliases == second.aliases and
first.filep == second.filep and
first.addrs == second.addrs and
first.ssl == second.ssl and
first.enabled == second.enabled and
first.modmacro == second.modmacro and
first.ancestor == second.ancestor
)
def assertEqualPathsList(first, second): # pragma: no cover
"""
Checks that the two lists of file paths match. This assertion allows for wildcard
paths.
"""
if any([isPass(path) for path in first]):
return
if any([isPass(path) for path in second]):
return
for fpath in first:
assert any([fnmatch.fnmatch(fpath, spath) for spath in second])
for spath in second:
assert any([fnmatch.fnmatch(fpath, spath) for fpath in first])

View File

@@ -0,0 +1,538 @@
"""
Augeas implementation of the ParserNode interfaces.
Augeas works internally by using XPATH notation. The following is a short example
of how this all works internally, to better understand what's going on under the
hood.
A configuration file /etc/apache2/apache2.conf with the following content:
# First comment line
# Second comment line
WhateverDirective whatevervalue
<ABlock>
DirectiveInABlock dirvalue
</ABlock>
SomeDirective somedirectivevalue
<ABlock>
AnotherDirectiveInABlock dirvalue
</ABlock>
# Yet another comment
Translates over to Augeas path notation (of immediate children), when calling
for example: aug.match("/files/etc/apache2/apache2.conf/*")
[
"/files/etc/apache2/apache2.conf/#comment[1]",
"/files/etc/apache2/apache2.conf/#comment[2]",
"/files/etc/apache2/apache2.conf/directive[1]",
"/files/etc/apache2/apache2.conf/ABlock[1]",
"/files/etc/apache2/apache2.conf/directive[2]",
"/files/etc/apache2/apache2.conf/ABlock[2]",
"/files/etc/apache2/apache2.conf/#comment[3]"
]
Regardless of directives name, its key in the Augeas tree is always "directive",
with index where needed of course. Comments work similarly, while blocks
have their own key in the Augeas XPATH notation.
It's important to note that all of the unique keys have their own indices.
Augeas paths are case sensitive, while Apache configuration is case insensitive.
It looks like this:
<block>
directive value
</block>
<Block>
Directive Value
</Block>
<block>
directive value
</block>
<bLoCk>
DiReCtiVe VaLuE
</bLoCk>
Translates over to:
[
"/files/etc/apache2/apache2.conf/block[1]",
"/files/etc/apache2/apache2.conf/Block[1]",
"/files/etc/apache2/apache2.conf/block[2]",
"/files/etc/apache2/apache2.conf/bLoCk[1]",
]
"""
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import assertions
from certbot_apache._internal import interfaces
from certbot_apache._internal import parser
from certbot_apache._internal import parsernode_util as util
class AugeasParserNode(interfaces.ParserNode):
""" Augeas implementation of ParserNode interface """
def __init__(self, **kwargs):
ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs) # pylint: disable=unused-variable
super(AugeasParserNode, self).__init__(**kwargs)
self.ancestor = ancestor
self.filepath = filepath
self.dirty = dirty
self.metadata = metadata
self.parser = self.metadata.get("augeasparser")
try:
if self.metadata["augeaspath"].endswith("/"):
raise errors.PluginError(
"Augeas path: {} has a trailing slash".format(
self.metadata["augeaspath"]
)
)
except KeyError:
raise errors.PluginError("Augeas path is required")
def save(self, msg):
self.parser.save(msg)
def find_ancestors(self, name):
"""
Searches for ancestor BlockNodes with a given name.
:param str name: Name of the BlockNode parent to search for
:returns: List of matching ancestor nodes.
:rtype: list of AugeasBlockNode
"""
ancestors = []
parent = self.metadata["augeaspath"]
while True:
# Get the path of ancestor node
parent = parent.rpartition("/")[0]
# Root of the tree
if not parent or parent == "/files":
break
anc = self._create_blocknode(parent)
if anc.name.lower() == name.lower():
ancestors.append(anc)
return ancestors
def _create_blocknode(self, path):
"""
Helper function to create a BlockNode from Augeas path. This is used by
AugeasParserNode.find_ancestors and AugeasBlockNode.
and AugeasBlockNode.find_blocks
"""
name = self._aug_get_name(path)
metadata = {"augeasparser": self.parser, "augeaspath": path}
# Check if the file was included from the root config or initial state
enabled = self.parser.parsed_in_original(
apache_util.get_file_path(path)
)
return AugeasBlockNode(name=name,
enabled=enabled,
ancestor=assertions.PASS,
filepath=apache_util.get_file_path(path),
metadata=metadata)
def _aug_get_name(self, path):
"""
Helper function to get name of a configuration block or variable from path.
"""
# Remove the ending slash if any
if path[-1] == "/": # pragma: no cover
path = path[:-1]
# Get the block name
name = path.split("/")[-1]
# remove [...], it's not allowed in Apache configuration and is used
# for indexing within Augeas
name = name.split("[")[0]
return name
class AugeasCommentNode(AugeasParserNode):
""" Augeas implementation of CommentNode interface """
def __init__(self, **kwargs):
comment, kwargs = util.commentnode_kwargs(kwargs) # pylint: disable=unused-variable
super(AugeasCommentNode, self).__init__(**kwargs)
# self.comment = comment
self.comment = comment
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.comment == other.comment and
self.filepath == other.filepath and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
return False
class AugeasDirectiveNode(AugeasParserNode):
""" Augeas implementation of DirectiveNode interface """
def __init__(self, **kwargs):
name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
super(AugeasDirectiveNode, self).__init__(**kwargs)
self.name = name
self.enabled = enabled
if parameters:
self.set_parameters(parameters)
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.name == other.name and
self.filepath == other.filepath and
self.parameters == other.parameters and
self.enabled == other.enabled and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
return False
def set_parameters(self, parameters):
"""
Sets parameters of a DirectiveNode or BlockNode object.
:param list parameters: List of all parameters for the node to set.
"""
orig_params = self._aug_get_params(self.metadata["augeaspath"])
# Clear out old parameters
for _ in orig_params:
# When the first parameter is removed, the indices get updated
param_path = "{}/arg[1]".format(self.metadata["augeaspath"])
self.parser.aug.remove(param_path)
# Insert new ones
for pi, param in enumerate(parameters):
param_path = "{}/arg[{}]".format(self.metadata["augeaspath"], pi+1)
self.parser.aug.set(param_path, param)
@property
def parameters(self):
"""
Fetches the parameters from Augeas tree, ensuring that the sequence always
represents the current state
:returns: Tuple of parameters for this DirectiveNode
:rtype: tuple:
"""
return tuple(self._aug_get_params(self.metadata["augeaspath"]))
def _aug_get_params(self, path):
"""Helper function to get parameters for DirectiveNodes and BlockNodes"""
arg_paths = self.parser.aug.match(path + "/arg")
return [self.parser.get_arg(apath) for apath in arg_paths]
class AugeasBlockNode(AugeasDirectiveNode):
""" Augeas implementation of BlockNode interface """
def __init__(self, **kwargs):
super(AugeasBlockNode, self).__init__(**kwargs)
self.children = ()
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.name == other.name and
self.filepath == other.filepath and
self.parameters == other.parameters and
self.children == other.children and
self.enabled == other.enabled and
self.dirty == other.dirty and
self.ancestor == other.ancestor and
self.metadata == other.metadata)
return False
# pylint: disable=unused-argument
def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
"""Adds a new BlockNode to the sequence of children"""
insertpath, realpath, before = self._aug_resolve_child_position(
name,
position
)
new_metadata = {"augeasparser": self.parser, "augeaspath": realpath}
# Create the new block
self.parser.aug.insert(insertpath, name, before)
# Check if the file was included from the root config or initial state
enabled = self.parser.parsed_in_original(
apache_util.get_file_path(realpath)
)
# Parameters will be set at the initialization of the new object
new_block = AugeasBlockNode(name=name,
parameters=parameters,
enabled=enabled,
ancestor=assertions.PASS,
filepath=apache_util.get_file_path(realpath),
metadata=new_metadata)
return new_block
# pylint: disable=unused-argument
def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover
"""Adds a new DirectiveNode to the sequence of children"""
if not parameters:
raise errors.PluginError("Directive requires parameters and none were set.")
insertpath, realpath, before = self._aug_resolve_child_position(
"directive",
position
)
new_metadata = {"augeasparser": self.parser, "augeaspath": realpath}
# Create the new directive
self.parser.aug.insert(insertpath, "directive", before)
# Set the directive key
self.parser.aug.set(realpath, name)
# Check if the file was included from the root config or initial state
enabled = self.parser.parsed_in_original(
apache_util.get_file_path(realpath)
)
new_dir = AugeasDirectiveNode(name=name,
parameters=parameters,
enabled=enabled,
ancestor=assertions.PASS,
filepath=apache_util.get_file_path(realpath),
metadata=new_metadata)
return new_dir
def add_child_comment(self, comment="", position=None):
"""Adds a new CommentNode to the sequence of children"""
insertpath, realpath, before = self._aug_resolve_child_position(
"#comment",
position
)
new_metadata = {"augeasparser": self.parser, "augeaspath": realpath}
# Create the new comment
self.parser.aug.insert(insertpath, "#comment", before)
# Set the comment content
self.parser.aug.set(realpath, comment)
new_comment = AugeasCommentNode(comment=comment,
ancestor=assertions.PASS,
filepath=apache_util.get_file_path(realpath),
metadata=new_metadata)
return new_comment
def find_blocks(self, name, exclude=True):
"""Recursive search of BlockNodes from the sequence of children"""
nodes = list()
paths = self._aug_find_blocks(name)
if exclude:
paths = self.parser.exclude_dirs(paths)
for path in paths:
nodes.append(self._create_blocknode(path))
return nodes
def find_directives(self, name, exclude=True):
"""Recursive search of DirectiveNodes from the sequence of children"""
nodes = list()
ownpath = self.metadata.get("augeaspath")
directives = self.parser.find_dir(name, start=ownpath, exclude=exclude)
already_parsed = set() # type: Set[str]
for directive in directives:
# Remove the /arg part from the Augeas path
directive = directive.partition("/arg")[0]
# find_dir returns an object for each _parameter_ of a directive
# so we need to filter out duplicates.
if directive not in already_parsed:
nodes.append(self._create_directivenode(directive))
already_parsed.add(directive)
return nodes
def find_comments(self, comment):
"""
Recursive search of DirectiveNodes from the sequence of children.
:param str comment: Comment content to search for.
"""
nodes = list()
ownpath = self.metadata.get("augeaspath")
comments = self.parser.find_comments(comment, start=ownpath)
for com in comments:
nodes.append(self._create_commentnode(com))
return nodes
def delete_child(self, child):
"""
Deletes a ParserNode from the sequence of children, and raises an
exception if it's unable to do so.
:param AugeasParserNode: child: A node to delete.
"""
if not self.parser.aug.remove(child.metadata["augeaspath"]):
raise errors.PluginError(
("Could not delete child node, the Augeas path: {} doesn't " +
"seem to exist.").format(child.metadata["augeaspath"])
)
def unsaved_files(self):
"""Returns a list of unsaved filepaths"""
return self.parser.unsaved_files()
def parsed_paths(self):
"""
Returns a list of file paths that have currently been parsed into the parser
tree. The returned list may include paths with wildcard characters, for
example: ['/etc/apache2/conf.d/*.load']
This is typically called on the root node of the ParserNode tree.
:returns: list of file paths of files that have been parsed
"""
res_paths = []
paths = self.parser.existing_paths
for directory in paths:
for filename in paths[directory]:
res_paths.append(os.path.join(directory, filename))
return res_paths
def _create_commentnode(self, path):
"""Helper function to create a CommentNode from Augeas path"""
comment = self.parser.aug.get(path)
metadata = {"augeasparser": self.parser, "augeaspath": path}
# Because of the dynamic nature of AugeasParser and the fact that we're
# not populating the complete node tree, the ancestor has a dummy value
return AugeasCommentNode(comment=comment,
ancestor=assertions.PASS,
filepath=apache_util.get_file_path(path),
metadata=metadata)
def _create_directivenode(self, path):
"""Helper function to create a DirectiveNode from Augeas path"""
name = self.parser.get_arg(path)
metadata = {"augeasparser": self.parser, "augeaspath": path}
# Check if the file was included from the root config or initial state
enabled = self.parser.parsed_in_original(
apache_util.get_file_path(path)
)
return AugeasDirectiveNode(name=name,
ancestor=assertions.PASS,
enabled=enabled,
filepath=apache_util.get_file_path(path),
metadata=metadata)
def _aug_find_blocks(self, name):
"""Helper function to perform a search to Augeas DOM tree to search
configuration blocks with a given name"""
# The code here is modified from configurator.get_virtual_hosts()
blk_paths = set()
for vhost_path in list(self.parser.parser_paths):
paths = self.parser.aug.match(
("/files%s//*[label()=~regexp('%s')]" %
(vhost_path, parser.case_i(name))))
blk_paths.update([path for path in paths if
name.lower() in os.path.basename(path).lower()])
return blk_paths
def _aug_resolve_child_position(self, name, position):
"""
Helper function that iterates through the immediate children and figures
out the insertion path for a new AugeasParserNode.
Augeas also generalizes indices for directives and comments, simply by
using "directive" or "comment" respectively as their names.
This function iterates over the existing children of the AugeasBlockNode,
returning their insertion path, resulting Augeas path and if the new node
should be inserted before or after the returned insertion path.
Note: while Apache is case insensitive, Augeas is not, and blocks like
Nameofablock and NameOfABlock have different indices.
:param str name: Name of the AugeasBlockNode to insert, "directive" for
AugeasDirectiveNode or "comment" for AugeasCommentNode
:param int position: The position to insert the child AugeasParserNode to
:returns: Tuple of insert path, resulting path and a boolean if the new
node should be inserted before it.
:rtype: tuple of str, str, bool
"""
# Default to appending
before = False
all_children = self.parser.aug.match("{}/*".format(
self.metadata["augeaspath"])
)
# Calculate resulting_path
# Augeas indices start at 1. We use counter to calculate the index to
# be used in resulting_path.
counter = 1
for i, child in enumerate(all_children):
if position is not None and i >= position:
# We're not going to insert the new node to an index after this
break
childname = self._aug_get_name(child)
if name == childname:
counter += 1
resulting_path = "{}/{}[{}]".format(
self.metadata["augeaspath"],
name,
counter
)
# Form the correct insert_path
# Inserting the only child and appending as the last child work
# similarly in Augeas.
append = not all_children or position is None or position >= len(all_children)
if append:
insert_path = "{}/*[last()]".format(
self.metadata["augeaspath"]
)
elif position == 0:
# Insert as the first child, before the current first one.
insert_path = all_children[0]
before = True
else:
insert_path = "{}/*[{}]".format(
self.metadata["augeaspath"],
position
)
return (insert_path, resulting_path, before)
interfaces.CommentNode.register(AugeasCommentNode)
interfaces.DirectiveNode.register(AugeasDirectiveNode)
interfaces.BlockNode.register(AugeasBlockNode)

View File

@@ -1,5 +1,6 @@
"""Apache Configuration based off of Augeas Configurator."""
"""Apache Configurator."""
# pylint: disable=too-many-lines
from collections import defaultdict
import copy
import fnmatch
import logging
@@ -7,34 +8,39 @@ import re
import socket
import time
from collections import defaultdict
import pkg_resources
import six
import zope.component
import zope.interface
try:
import apacheconfig
HAS_APACHECONFIG = True
except ImportError: # pragma: no cover
HAS_APACHECONFIG = False
from acme import challenges
from acme.magic_typing import DefaultDict, Dict, List, Set, Union # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import DefaultDict # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Union # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
from certbot.compat import filesystem
from certbot.compat import os
from certbot.plugins import common
from certbot.plugins.util import path_surgery
from certbot.plugins.enhancements import AutoHSTSEnhancement
from certbot_apache import apache_util
from certbot_apache import augeas_configurator
from certbot_apache import constants
from certbot_apache import display_ops
from certbot_apache import http_01
from certbot_apache import obj
from certbot_apache import parser
from certbot.plugins.util import path_surgery
from certbot_apache._internal import apache_util
from certbot_apache._internal import assertions
from certbot_apache._internal import constants
from certbot_apache._internal import display_ops
from certbot_apache._internal import dualparser
from certbot_apache._internal import http_01
from certbot_apache._internal import obj
from certbot_apache._internal import parser
logger = logging.getLogger(__name__)
@@ -70,22 +76,18 @@ logger = logging.getLogger(__name__)
@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
@zope.interface.provider(interfaces.IPluginFactory)
class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# pylint: disable=too-many-instance-attributes,too-many-public-methods
class ApacheConfigurator(common.Installer):
"""Apache configurator.
State of Configurator: This code has been been tested and built for Ubuntu
14.04 Apache 2.4 and it works for Ubuntu 12.04 Apache 2.2
:ivar config: Configuration.
:type config: :class:`~certbot.interfaces.IConfig`
:ivar parser: Handles low level parsing
:type parser: :class:`~certbot_apache.parser`
:type parser: :class:`~certbot_apache._internal.parser`
:ivar tup version: version of Apache
:ivar list vhosts: All vhosts found in the configuration
(:class:`list` of :class:`~certbot_apache.obj.VirtualHost`)
(:class:`list` of :class:`~certbot_apache._internal.obj.VirtualHost`)
:ivar dict assoc: Mapping between domains and vhosts
@@ -114,7 +116,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
handle_sites=False,
challenge_location="/etc/apache2",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def option(self, key):
@@ -177,8 +179,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"(Only Ubuntu/Debian currently)")
add("ctl", default=DEFAULTS["ctl"],
help="Full path to Apache control script")
util.add_deprecated_argument(
add, argument_name="init-script", nargs=1)
def __init__(self, *args, **kwargs):
"""Initialize an Apache Configurator.
@@ -188,6 +188,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
version = kwargs.pop("version", None)
use_parsernode = kwargs.pop("use_parsernode", False)
super(ApacheConfigurator, self).__init__(*args, **kwargs)
# Add name_server association dict
@@ -201,10 +202,17 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self._enhanced_vhosts = defaultdict(set) # type: DefaultDict[str, Set[obj.VirtualHost]]
# Temporary state for AutoHSTS enhancement
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
# Reverter save notes
self.save_notes = ""
# Should we use ParserNode implementation instead of the old behavior
self.USE_PARSERNODE = use_parsernode
# Saves the list of file paths that were parsed initially, and
# not added to parser tree by self.conf("vhost-root") for example.
self.parsed_paths = [] # type: List[str]
# These will be set in the prepare function
self._prepared = False
self.parser = None
self.parser_root = None
self.version = version
self.vhosts = None
self.options = copy.deepcopy(self.OS_DEFAULTS)
@@ -231,12 +239,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:raises .errors.PluginError: If there is any other error
"""
# Perform the actual Augeas initialization to be able to react
try:
self.init_augeas()
except ImportError:
raise errors.NoInstallationError("Problem in Augeas installation")
self._prepare_options()
# Verify Apache is installed
@@ -254,16 +256,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
raise errors.NotSupportedError(
"Apache Version {0} not supported.".format(str(self.version)))
if not self._check_aug_version():
raise errors.NotSupportedError(
"Apache plugin support requires libaugeas0 and augeas-lenses "
"version 1.2.0 or higher, please make sure you have you have "
"those installed.")
# Recover from previous crash before Augeas initialization to have the
# correct parse tree from the get go.
self.recovery_routine()
# Perform the actual Augeas initialization to be able to react
self.parser = self.get_parser()
# Set up ParserNode root
pn_meta = {"augeasparser": self.parser,
"augeaspath": self.parser.get_root_augpath(),
"ac_ast": None}
if self.USE_PARSERNODE and HAS_APACHECONFIG:
self.parser_root = self.get_parsernode_root(pn_meta)
self.parsed_paths = self.parser_root.parsed_paths()
# Check for errors in parsing files with Augeas
self.check_parsing_errors("httpd.aug")
self.parser.check_parsing_errors("httpd.aug")
# Get all of the available vhosts
self.vhosts = self.get_virtual_hosts()
@@ -282,6 +290,67 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
" Apache configuration?".format(self.option("server_root")))
self._prepared = True
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.parser.unsaved_files()
if save_files:
self.add_to_checkpoint(save_files,
self.save_notes, temporary=temporary)
# Handle the parser specific tasks
self.parser.save(save_files)
if title and not temporary:
self.finalize_checkpoint(title)
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
"""
super(ApacheConfigurator, self).recovery_routine()
# Reload configuration after these changes take effect if needed
# ie. ApacheParser has been initialized.
if self.parser:
# TODO: wrap into non-implementation specific parser interface
self.parser.aug.load()
def revert_challenge_config(self):
"""Used to cleanup challenge configurations.
:raises .errors.PluginError: If unable to revert the challenge config.
"""
self.revert_temporary_config()
self.parser.aug.load()
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
"""
super(ApacheConfigurator, self).rollback_checkpoints(rollback)
self.parser.aug.load()
def _verify_exe_availability(self, exe):
"""Checks availability of Apache executable"""
if not util.exe_exists(exe):
@@ -289,28 +358,34 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
raise errors.NoInstallationError(
'Cannot find Apache executable {0}'.format(exe))
def _check_aug_version(self):
""" Checks that we have recent enough version of libaugeas.
If augeas version is recent enough, it will support case insensitive
regexp matching"""
self.aug.set("/test/path/testing/arg", "aRgUMeNT")
try:
matches = self.aug.match(
"/test//*[self::arg=~regexp('argument', 'i')]")
except RuntimeError:
self.aug.remove("/test/path")
return False
self.aug.remove("/test/path")
return matches
def get_parser(self):
"""Initializes the ApacheParser"""
# If user provided vhost_root value in command line, use it
return parser.ApacheParser(
self.aug, self.option("server_root"), self.conf("vhost-root"),
self.option("server_root"), self.conf("vhost-root"),
self.version, configurator=self)
def get_parsernode_root(self, metadata):
"""Initializes the ParserNode parser root instance."""
apache_vars = dict()
apache_vars["defines"] = apache_util.parse_defines(self.option("ctl"))
apache_vars["includes"] = apache_util.parse_includes(self.option("ctl"))
apache_vars["modules"] = apache_util.parse_modules(self.option("ctl"))
metadata["apache_vars"] = apache_vars
with open(self.parser.loc["root"]) as f:
with apacheconfig.make_loader(writable=True,
**apacheconfig.flavors.NATIVE_APACHE) as loader:
metadata["ac_ast"] = loader.loads(f.read())
return dualparser.DualBlockNode(
name=assertions.PASS,
ancestor=None,
filepath=self.parser.loc["root"],
metadata=metadata
)
def _wildcard_domain(self, domain):
"""
Checks if domain is a wildcard domain
@@ -357,7 +432,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
counterpart, should one get created
:returns: List of VirtualHosts or None
:rtype: `list` of :class:`~certbot_apache.obj.VirtualHost`
:rtype: `list` of :class:`~certbot_apache._internal.obj.VirtualHost`
"""
if self._wildcard_domain(domain):
@@ -416,7 +491,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
filtered_vhosts[name] = vhost
# Only unique VHost objects
dialog_input = set([vhost for vhost in filtered_vhosts.values()])
dialog_input = set(filtered_vhosts.values())
# Ask the user which of names to enable, expect list of names back
dialog_output = display_ops.select_vhost_multiple(list(dialog_input))
@@ -487,8 +562,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# install SSLCertificateFile, SSLCertificateKeyFile,
# and SSLCertificateChainFile directives
set_cert_path = cert_path
self.aug.set(path["cert_path"][-1], cert_path)
self.aug.set(path["cert_key"][-1], key_path)
self.parser.aug.set(path["cert_path"][-1], cert_path)
self.parser.aug.set(path["cert_key"][-1], key_path)
if chain_path is not None:
self.parser.add_dir(vhost.path,
"SSLCertificateChainFile", chain_path)
@@ -500,8 +575,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
raise errors.PluginError("Please provide the --fullchain-path "
"option pointing to your full chain file")
set_cert_path = fullchain_path
self.aug.set(path["cert_path"][-1], fullchain_path)
self.aug.set(path["cert_key"][-1], key_path)
self.parser.aug.set(path["cert_path"][-1], fullchain_path)
self.parser.aug.set(path["cert_key"][-1], key_path)
# Enable the new vhost if needed
if not vhost.enabled:
@@ -532,7 +607,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
counterpart, should one get created
:returns: vhost associated with name
:rtype: :class:`~certbot_apache.obj.VirtualHost`
:rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
:raises .errors.PluginError: If no vhost is available or chosen
@@ -567,9 +642,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"in the Apache config.",
target_name)
raise errors.PluginError("No vhost selected")
elif temp:
if temp:
return vhost
elif not vhost.ssl:
if not vhost.ssl:
addrs = self._get_proposed_addrs(vhost, "443")
# TODO: Conflicts is too conservative
if not any(vhost.enabled and vhost.conflicts(addrs) for
@@ -635,7 +710,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str target_name: domain handled by the desired vhost
:param vhosts: vhosts to consider
:type vhosts: `collections.Iterable` of :class:`~certbot_apache.obj.VirtualHost`
:type vhosts: `collections.Iterable` of :class:`~certbot_apache._internal.obj.VirtualHost`
:param bool filter_defaults: whether a vhost with a _default_
addr is acceptable
@@ -777,7 +852,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Helper function for get_virtual_hosts().
:param host: In progress vhost whose names will be added
:type host: :class:`~certbot_apache.obj.VirtualHost`
:type host: :class:`~certbot_apache._internal.obj.VirtualHost`
"""
@@ -796,12 +871,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str path: Augeas path to virtual host
:returns: newly created vhost
:rtype: :class:`~certbot_apache.obj.VirtualHost`
:rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
"""
addrs = set()
try:
args = self.aug.match(path + "/arg")
args = self.parser.aug.match(path + "/arg")
except RuntimeError:
logger.warning("Encountered a problem while parsing file: %s, skipping", path)
return None
@@ -819,7 +894,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
is_ssl = True
filename = apache_util.get_file_path(
self.aug.get("/augeas/files%s/path" % apache_util.get_file_path(path)))
self.parser.aug.get("/augeas/files%s/path" % apache_util.get_file_path(path)))
if filename is None:
return None
@@ -835,9 +910,32 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return vhost
def get_virtual_hosts(self):
"""
Temporary wrapper for legacy and ParserNode version for
get_virtual_hosts. This should be replaced with the ParserNode
implementation when ready.
"""
v1_vhosts = self.get_virtual_hosts_v1()
if self.USE_PARSERNODE and HAS_APACHECONFIG:
v2_vhosts = self.get_virtual_hosts_v2()
for v1_vh in v1_vhosts:
found = False
for v2_vh in v2_vhosts:
if assertions.isEqualVirtualHost(v1_vh, v2_vh):
found = True
break
if not found:
raise AssertionError("Equivalent for {} was not found".format(v1_vh.path))
return v2_vhosts
return v1_vhosts
def get_virtual_hosts_v1(self):
"""Returns list of virtual hosts found in the Apache configuration.
:returns: List of :class:`~certbot_apache.obj.VirtualHost`
:returns: List of :class:`~certbot_apache._internal.obj.VirtualHost`
objects found in configuration
:rtype: list
@@ -849,7 +947,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Make a list of parser paths because the parser_paths
# dictionary may be modified during the loop.
for vhost_path in list(self.parser.parser_paths):
paths = self.aug.match(
paths = self.parser.aug.match(
("/files%s//*[label()=~regexp('%s')]" %
(vhost_path, parser.case_i("VirtualHost"))))
paths = [path for path in paths if
@@ -859,7 +957,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if not new_vhost:
continue
internal_path = apache_util.get_internal_aug_path(new_vhost.path)
realpath = os.path.realpath(new_vhost.filep)
realpath = filesystem.realpath(new_vhost.filep)
if realpath not in file_paths:
file_paths[realpath] = new_vhost.filep
internal_paths[realpath].add(internal_path)
@@ -887,6 +985,80 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
vhs.append(new_vhost)
return vhs
def get_virtual_hosts_v2(self):
"""Returns list of virtual hosts found in the Apache configuration using
ParserNode interface.
:returns: List of :class:`~certbot_apache.obj.VirtualHost`
objects found in configuration
:rtype: list
"""
vhs = []
vhosts = self.parser_root.find_blocks("VirtualHost", exclude=False)
for vhblock in vhosts:
vhs.append(self._create_vhost_v2(vhblock))
return vhs
def _create_vhost_v2(self, node):
"""Used by get_virtual_hosts_v2 to create vhost objects using ParserNode
interfaces.
:param interfaces.BlockNode node: The BlockNode object of VirtualHost block
:returns: newly created vhost
:rtype: :class:`~certbot_apache.obj.VirtualHost`
"""
addrs = set()
for param in node.parameters:
addrs.add(obj.Addr.fromstring(param))
is_ssl = False
# Exclusion to match the behavior in get_virtual_hosts_v2
sslengine = node.find_directives("SSLEngine", exclude=False)
if sslengine:
for directive in sslengine:
if directive.parameters[0].lower() == "on":
is_ssl = True
break
# "SSLEngine on" might be set outside of <VirtualHost>
# Treat vhosts with port 443 as ssl vhosts
for addr in addrs:
if addr.get_port() == "443":
is_ssl = True
enabled = apache_util.included_in_paths(node.filepath, self.parsed_paths)
macro = False
# Check if the VirtualHost is contained in a mod_macro block
if node.find_ancestors("Macro"):
macro = True
vhost = obj.VirtualHost(
node.filepath, None, addrs, is_ssl, enabled, modmacro=macro, node=node
)
self._populate_vhost_names_v2(vhost)
return vhost
def _populate_vhost_names_v2(self, vhost):
"""Helper function that populates the VirtualHost names.
:param host: In progress vhost whose names will be added
:type host: :class:`~certbot_apache.obj.VirtualHost`
"""
servername_match = vhost.node.find_directives("ServerName",
exclude=False)
serveralias_match = vhost.node.find_directives("ServerAlias",
exclude=False)
servername = None
if servername_match:
servername = servername_match[-1].parameters[-1]
if not vhost.modmacro:
for alias in serveralias_match:
for serveralias in alias.parameters:
vhost.aliases.add(serveralias)
vhost.name = servername
def is_name_vhost(self, target_addr):
"""Returns if vhost is a name based vhost
@@ -894,7 +1066,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
now NameVirtualHosts. If version is earlier than 2.4, check if addr
has a NameVirtualHost directive in the Apache config
:param certbot_apache.obj.Addr target_addr: vhost address
:param certbot_apache._internal.obj.Addr target_addr: vhost address
:returns: Success
:rtype: bool
@@ -912,19 +1084,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Adds NameVirtualHost directive for given address.
:param addr: Address that will be added as NameVirtualHost directive
:type addr: :class:`~certbot_apache.obj.Addr`
:type addr: :class:`~certbot_apache._internal.obj.Addr`
"""
loc = parser.get_aug_path(self.parser.loc["name"])
if addr.get_port() == "443":
path = self.parser.add_dir_to_ifmodssl(
self.parser.add_dir_to_ifmodssl(
loc, "NameVirtualHost", [str(addr)])
else:
path = self.parser.add_dir(loc, "NameVirtualHost", [str(addr)])
self.parser.add_dir(loc, "NameVirtualHost", [str(addr)])
msg = ("Setting %s to be NameBasedVirtualHost\n"
"\tDirective added to %s\n" % (addr, path))
msg = "Setting {0} to be NameBasedVirtualHost\n".format(addr)
logger.debug(msg)
self.save_notes += msg
@@ -1081,7 +1252,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if "ssl_module" not in self.parser.modules:
self.enable_mod("ssl", temp=temp)
def make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals
def make_vhost_ssl(self, nonssl_vhost):
"""Makes an ssl_vhost version of a nonssl_vhost.
Duplicates vhost and adds default ssl options
@@ -1091,10 +1262,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. note:: This function saves the configuration
:param nonssl_vhost: Valid VH that doesn't have SSLEngine on
:type nonssl_vhost: :class:`~certbot_apache.obj.VirtualHost`
:type nonssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:returns: SSL vhost
:rtype: :class:`~certbot_apache.obj.VirtualHost`
:rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
:raises .errors.PluginError: If more than one virtual host is in
the file or if plugin is unable to write/read vhost files.
@@ -1103,16 +1274,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
avail_fp = nonssl_vhost.filep
ssl_fp = self._get_ssl_vhost_path(avail_fp)
orig_matches = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
orig_matches = self.parser.aug.match("/files%s//* [label()=~regexp('%s')]" %
(self._escape(ssl_fp),
parser.case_i("VirtualHost")))
self._copy_create_ssl_vhost_skeleton(nonssl_vhost, ssl_fp)
# Reload augeas to take into account the new vhost
self.aug.load()
self.parser.aug.load()
# Get Vhost augeas path for new vhost
new_matches = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
new_matches = self.parser.aug.match("/files%s//* [label()=~regexp('%s')]" %
(self._escape(ssl_fp),
parser.case_i("VirtualHost")))
@@ -1123,7 +1294,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Make Augeas aware of the new vhost
self.parser.parse_file(ssl_fp)
# Try to search again
new_matches = self.aug.match(
new_matches = self.parser.aug.match(
"/files%s//* [label()=~regexp('%s')]" %
(self._escape(ssl_fp),
parser.case_i("VirtualHost")))
@@ -1185,11 +1356,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
fp = os.path.join(os.path.realpath(self.option("vhost_root")),
fp = os.path.join(filesystem.realpath(self.option("vhost_root")),
os.path.basename(non_ssl_vh_fp))
else:
# Use non-ssl filepath
fp = os.path.realpath(non_ssl_vh_fp)
fp = filesystem.realpath(non_ssl_vh_fp)
if fp.endswith(".conf"):
return fp[:-(len(".conf"))] + self.option("le_vhost_ext")
@@ -1273,8 +1444,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"vhost for your HTTPS site located at {1} because they have "
"the potential to create redirection loops.".format(
vhost.filep, ssl_fp), reporter.MEDIUM_PRIORITY)
self.aug.set("/augeas/files%s/mtime" % (self._escape(ssl_fp)), "0")
self.aug.set("/augeas/files%s/mtime" % (self._escape(vhost.filep)), "0")
self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(ssl_fp)), "0")
self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(vhost.filep)), "0")
def _sift_rewrite_rules(self, contents):
""" Helper function for _copy_create_ssl_vhost_skeleton to prepare the
@@ -1332,12 +1503,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
result.append(comment)
sift = True
result.append('\n'.join(
['# ' + l for l in chunk]))
continue
result.append('\n'.join(['# ' + l for l in chunk]))
else:
result.append('\n'.join(chunk))
continue
return result, sift
def _get_vhost_block(self, vhost):
@@ -1349,7 +1517,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
span_val = self.aug.span(vhost.path)
span_val = self.parser.aug.span(vhost.path)
except ValueError:
logger.critical("Error while reading the VirtualHost %s from "
"file %s", vhost.name, vhost.filep, exc_info=True)
@@ -1384,13 +1552,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
def _update_ssl_vhosts_addrs(self, vh_path):
ssl_addrs = set()
ssl_addr_p = self.aug.match(vh_path + "/arg")
ssl_addr_p = self.parser.aug.match(vh_path + "/arg")
for addr in ssl_addr_p:
old_addr = obj.Addr.fromstring(
str(self.parser.get_arg(addr)))
ssl_addr = old_addr.get_addr_obj("443")
self.aug.set(addr, str(ssl_addr))
self.parser.aug.set(addr, str(ssl_addr))
ssl_addrs.add(ssl_addr)
return ssl_addrs
@@ -1409,14 +1577,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
vh_path, False)) > 1:
directive_path = self.parser.find_dir(directive, None,
vh_path, False)
self.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
self.parser.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
def _remove_directives(self, vh_path, directives):
for directive in directives:
while self.parser.find_dir(directive, None, vh_path, False):
directive_path = self.parser.find_dir(directive, None,
vh_path, False)
self.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
self.parser.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
def _add_dummy_ssl_directives(self, vh_path):
self.parser.add_dir(vh_path, "SSLCertificateFile",
@@ -1455,7 +1623,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
matches = self.parser.find_dir(
"ServerAlias", start=vh_path, exclude=False)
aliases = (self.aug.get(match) for match in matches)
aliases = (self.parser.aug.get(match) for match in matches)
return self.domain_in_names(aliases, target_name)
def _add_name_vhost_if_necessary(self, vhost):
@@ -1465,7 +1633,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
https://httpd.apache.org/docs/2.2/mod/core.html#namevirtualhost
:param vhost: New virtual host that was recently created.
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
"""
need_to_save = False
@@ -1500,7 +1668,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str id_str: Id string for matching
:returns: The matched VirtualHost or None
:rtype: :class:`~certbot_apache.obj.VirtualHost` or None
:rtype: :class:`~certbot_apache._internal.obj.VirtualHost` or None
:raises .errors.PluginError: If no VirtualHost is found
"""
@@ -1517,7 +1685,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
used for keeping track of VirtualHost directive over time.
:param vhost: Virtual host to add the id
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:returns: The unique ID or None
:rtype: str or None
@@ -1539,7 +1707,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
If ID already exists, returns that instead.
:param vhost: Virtual host to add or find the id
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:returns: The unique ID for vhost
:rtype: str or None
@@ -1577,9 +1745,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str domain: domain to enhance
:param str enhancement: enhancement type defined in
:const:`~certbot.constants.ENHANCEMENTS`
:const:`~certbot.plugins.enhancements.ENHANCEMENTS`
:param options: options for the enhancement
See :const:`~certbot.constants.ENHANCEMENTS`
See :const:`~certbot.plugins.enhancements.ENHANCEMENTS`
documentation for appropriate parameter.
:raises .errors.PluginError: If Enhancement is not supported, or if
@@ -1617,7 +1785,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Increase the AutoHSTS max-age value
:param vhost: Virtual host object to modify
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:param str id_str: The unique ID string of VirtualHost
@@ -1638,7 +1806,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if header_path:
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
for match in header_path:
if re.search(pat, self.aug.get(match).lower()):
if re.search(pat, self.parser.aug.get(match).lower()):
hsts_dirpath = match
if not hsts_dirpath:
err_msg = ("Certbot was unable to find the existing HSTS header "
@@ -1652,7 +1820,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Our match statement was for string strict-transport-security, but
# we need to update the value instead. The next index is for the value
hsts_dirpath = hsts_dirpath.replace("arg[3]", "arg[4]")
self.aug.set(hsts_dirpath, hsts_maxage)
self.parser.aug.set(hsts_dirpath, hsts_maxage)
note_msg = ("Increasing HSTS max-age value to {0} for VirtualHost "
"in {1}\n".format(nextstep_value, vhost.filep))
logger.debug(note_msg)
@@ -1701,13 +1869,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. note:: This function saves the configuration
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost`
:type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:param unused_options: Not currently used
:type unused_options: Not Available
:returns: Success, general_vhost (HTTP vhost)
:rtype: (bool, :class:`~certbot_apache.obj.VirtualHost`)
:rtype: (bool, :class:`~certbot_apache._internal.obj.VirtualHost`)
"""
min_apache_ver = (2, 3, 3)
@@ -1734,7 +1902,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# We'll simply delete the directive, so that we'll have a
# consistent OCSP cache path.
if stapling_cache_aug_path:
self.aug.remove(
self.parser.aug.remove(
re.sub(r"/\w*$", "", stapling_cache_aug_path[0]))
self.parser.add_dir_to_ifmodssl(ssl_vhost_aug_path,
@@ -1757,14 +1925,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. note:: This function saves the configuration
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost`
:type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:param header_substring: string that uniquely identifies a header.
e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
:type str
:returns: Success, general_vhost (HTTP vhost)
:rtype: (bool, :class:`~certbot_apache.obj.VirtualHost`)
:rtype: (bool, :class:`~certbot_apache._internal.obj.VirtualHost`)
:raises .errors.PluginError: If no viable HTTP host can be created or
set with header header_substring.
@@ -1792,7 +1960,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
contains the string header_substring.
:param ssl_vhost: vhost to check
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:param header_substring: string that uniquely identifies a header.
e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
@@ -1811,7 +1979,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# "Existing Header directive for virtualhost"
pat = '(?:[ "]|^)(%s)(?:[ "]|$)' % (header_substring.lower())
for match in header_path:
if re.search(pat, self.aug.get(match).lower()):
if re.search(pat, self.parser.aug.get(match).lower()):
raise errors.PluginEnhancementAlreadyPresent(
"Existing %s header" % (header_substring))
@@ -1829,7 +1997,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. note:: This function saves the configuration
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost`
:type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:param unused_options: Not currently used
:type unused_options: Not Available
@@ -1914,7 +2082,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
delete certbot's old rewrite rules and set the new one instead.
:param vhost: vhost to check
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:raises errors.PluginEnhancementAlreadyPresent: When the exact
certbot redirection WriteRule exists in virtual host.
@@ -1938,11 +2106,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
constants.REWRITE_HTTPS_ARGS_WITH_END]
for dir_path, args_paths in rewrite_args_dict.items():
arg_vals = [self.aug.get(x) for x in args_paths]
arg_vals = [self.parser.aug.get(x) for x in args_paths]
# Search for past redirection rule, delete it, set the new one
if arg_vals in constants.OLD_REWRITE_HTTPS_ARGS:
self.aug.remove(dir_path)
self.parser.aug.remove(dir_path)
self._set_https_redirection_rewrite_rule(vhost)
self.save()
raise errors.PluginEnhancementAlreadyPresent(
@@ -1956,7 +2124,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Checks if there exists a RewriteRule directive in vhost
:param vhost: vhost to check
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:returns: True if a RewriteRule directive exists.
:rtype: bool
@@ -1970,7 +2138,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Checks if a RewriteEngine directive is on
:param vhost: vhost to check
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
"""
rewrite_engine_path_list = self.parser.find_dir("RewriteEngine", "on",
@@ -1987,10 +2155,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Creates an http_vhost specifically to redirect for the ssl_vhost.
:param ssl_vhost: ssl vhost
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost`
:type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:returns: tuple of the form
(`success`, :class:`~certbot_apache.obj.VirtualHost`)
(`success`, :class:`~certbot_apache._internal.obj.VirtualHost`)
:rtype: tuple
"""
@@ -1998,7 +2166,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
redirect_filepath = self._write_out_redirect(ssl_vhost, text)
self.aug.load()
self.parser.aug.load()
# Make a new vhost data structure and add it to the lists
new_vhost = self._create_vhost(parser.get_aug_path(self._escape(redirect_filepath)))
self.vhosts.append(new_vhost)
@@ -2116,7 +2284,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
of this method where available.
:param vhost: vhost to enable
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:raises .errors.NotSupportedError: If filesystem layout is not
supported.
@@ -2310,7 +2478,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
Enable the AutoHSTS enhancement for defined domains
:param _unused_lineage: Certificate lineage object, unused
:type _unused_lineage: certbot.storage.RenewableCert
:type _unused_lineage: certbot._internal.storage.RenewableCert
:param domains: List of domains in certificate to enhance
:type domains: str
@@ -2353,7 +2521,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Do the initial AutoHSTS deployment to a vhost
:param ssl_vhost: The VirtualHost object to deploy the AutoHSTS
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost` or None
:type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost` or None
:raises errors.PluginEnhancementAlreadyPresent: When already enhanced
@@ -2435,7 +2603,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
and changes the HSTS max-age to a high value.
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
:type lineage: certbot._internal.storage.RenewableCert
"""
self._autohsts_fetch_state()
if not self._autohsts:
@@ -2480,4 +2648,4 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self._autohsts_save_state()
AutoHSTSEnhancement.register(ApacheConfigurator) # pylint: disable=no-member
AutoHSTSEnhancement.register(ApacheConfigurator)

View File

@@ -1,6 +1,7 @@
"""Apache plugin constants."""
import pkg_resources
from certbot.compat import os
MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
"""Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
@@ -9,6 +10,7 @@ MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-apache-conf-digest.txt"
"""Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`."""
# NEVER REMOVE A SINGLE HASH FROM THIS LIST UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!
ALL_SSL_OPTIONS_HASHES = [
'2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a',
'4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a',
@@ -18,11 +20,15 @@ ALL_SSL_OPTIONS_HASHES = [
'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b',
'80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791',
'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082',
'717b0a89f5e4c39b09a42813ac6e747cfbdeb93439499e73f4f70a1fe1473f20',
'0fcdc81280cd179a07ec4d29d3595068b9326b455c488de4b09f585d5dafc137',
'86cc09ad5415cd6d5f09a947fe2501a9344328b1e8a8b458107ea903e80baa6c',
'06675349e457eae856120cdebb564efe546f0b87399f2264baeb41e442c724c7',
]
"""SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC"""
AUGEAS_LENS_DIR = pkg_resources.resource_filename(
"certbot_apache", "augeas_lens")
"certbot_apache", os.path.join("_internal", "augeas_lens"))
"""Path to the Augeas lens directory"""
REWRITE_HTTPS_ARGS = [

View File

@@ -3,10 +3,10 @@ import logging
import zope.component
import certbot.display.util as display_util
from certbot import errors
from certbot import interfaces
from certbot.compat import os
import certbot.display.util as display_util
logger = logging.getLogger(__name__)
@@ -77,7 +77,7 @@ def _vhost_menu(domain, vhosts):
if free_chars < 2:
logger.debug("Display size is too small for "
"certbot_apache.display_ops._vhost_menu()")
"certbot_apache._internal.display_ops._vhost_menu()")
# This runs the edge off the screen, but it doesn't cause an "error"
filename_size = 1
disp_name_size = 1

View File

@@ -0,0 +1,306 @@
""" Dual ParserNode implementation """
from certbot_apache._internal import assertions
from certbot_apache._internal import augeasparser
from certbot_apache._internal import apacheparser
class DualNodeBase(object):
""" Dual parser interface for in development testing. This is used as the
base class for dual parser interface classes. This class handles runtime
attribute value assertions."""
def save(self, msg): # pragma: no cover
""" Call save for both parsers """
self.primary.save(msg)
self.secondary.save(msg)
def __getattr__(self, aname):
""" Attribute value assertion """
firstval = getattr(self.primary, aname)
secondval = getattr(self.secondary, aname)
exclusions = [
# Metadata will inherently be different, as ApacheParserNode does
# not have Augeas paths and so on.
aname == "metadata",
callable(firstval)
]
if not any(exclusions):
assertions.assertEqualSimple(firstval, secondval)
return firstval
def find_ancestors(self, name):
""" Traverses the ancestor tree and returns ancestors matching name """
return self._find_helper(DualBlockNode, "find_ancestors", name)
def _find_helper(self, nodeclass, findfunc, search, **kwargs):
"""A helper for find_* functions. The function specific attributes should
be passed as keyword arguments.
:param interfaces.ParserNode nodeclass: The node class for results.
:param str findfunc: Name of the find function to call
:param str search: The search term
"""
primary_res = getattr(self.primary, findfunc)(search, **kwargs)
secondary_res = getattr(self.secondary, findfunc)(search, **kwargs)
# The order of search results for Augeas implementation cannot be
# assured.
pass_primary = assertions.isPassNodeList(primary_res)
pass_secondary = assertions.isPassNodeList(secondary_res)
new_nodes = list()
if pass_primary and pass_secondary:
# Both unimplemented
new_nodes.append(nodeclass(primary=primary_res[0],
secondary=secondary_res[0])) # pragma: no cover
elif pass_primary:
for c in secondary_res:
new_nodes.append(nodeclass(primary=primary_res[0],
secondary=c))
elif pass_secondary:
for c in primary_res:
new_nodes.append(nodeclass(primary=c,
secondary=secondary_res[0]))
else:
assert len(primary_res) == len(secondary_res)
matches = self._create_matching_list(primary_res, secondary_res)
for p, s in matches:
new_nodes.append(nodeclass(primary=p, secondary=s))
return new_nodes
class DualCommentNode(DualNodeBase):
""" Dual parser implementation of CommentNode interface """
def __init__(self, **kwargs):
""" This initialization implementation allows ordinary initialization
of CommentNode objects as well as creating a DualCommentNode object
using precreated or fetched CommentNode objects if provided as optional
arguments primary and secondary.
Parameters other than the following are from interfaces.CommentNode:
:param CommentNode primary: Primary pre-created CommentNode, mainly
used when creating new DualParser nodes using add_* methods.
:param CommentNode secondary: Secondary pre-created CommentNode
"""
kwargs.setdefault("primary", None)
kwargs.setdefault("secondary", None)
primary = kwargs.pop("primary")
secondary = kwargs.pop("secondary")
if primary or secondary:
assert primary and secondary
self.primary = primary
self.secondary = secondary
else:
self.primary = augeasparser.AugeasCommentNode(**kwargs)
self.secondary = apacheparser.ApacheCommentNode(**kwargs)
assertions.assertEqual(self.primary, self.secondary)
class DualDirectiveNode(DualNodeBase):
""" Dual parser implementation of DirectiveNode interface """
def __init__(self, **kwargs):
""" This initialization implementation allows ordinary initialization
of DirectiveNode objects as well as creating a DualDirectiveNode object
using precreated or fetched DirectiveNode objects if provided as optional
arguments primary and secondary.
Parameters other than the following are from interfaces.DirectiveNode:
:param DirectiveNode primary: Primary pre-created DirectiveNode, mainly
used when creating new DualParser nodes using add_* methods.
:param DirectiveNode secondary: Secondary pre-created DirectiveNode
"""
kwargs.setdefault("primary", None)
kwargs.setdefault("secondary", None)
primary = kwargs.pop("primary")
secondary = kwargs.pop("secondary")
if primary or secondary:
assert primary and secondary
self.primary = primary
self.secondary = secondary
else:
self.primary = augeasparser.AugeasDirectiveNode(**kwargs)
self.secondary = apacheparser.ApacheDirectiveNode(**kwargs)
assertions.assertEqual(self.primary, self.secondary)
def set_parameters(self, parameters):
""" Sets parameters and asserts that both implementation successfully
set the parameter sequence """
self.primary.set_parameters(parameters)
self.secondary.set_parameters(parameters)
assertions.assertEqual(self.primary, self.secondary)
class DualBlockNode(DualNodeBase):
""" Dual parser implementation of BlockNode interface """
def __init__(self, **kwargs):
""" This initialization implementation allows ordinary initialization
of BlockNode objects as well as creating a DualBlockNode object
using precreated or fetched BlockNode objects if provided as optional
arguments primary and secondary.
Parameters other than the following are from interfaces.BlockNode:
:param BlockNode primary: Primary pre-created BlockNode, mainly
used when creating new DualParser nodes using add_* methods.
:param BlockNode secondary: Secondary pre-created BlockNode
"""
kwargs.setdefault("primary", None)
kwargs.setdefault("secondary", None)
primary = kwargs.pop("primary")
secondary = kwargs.pop("secondary")
if primary or secondary:
assert primary and secondary
self.primary = primary
self.secondary = secondary
else:
self.primary = augeasparser.AugeasBlockNode(**kwargs)
self.secondary = apacheparser.ApacheBlockNode(**kwargs)
assertions.assertEqual(self.primary, self.secondary)
def add_child_block(self, name, parameters=None, position=None):
""" Creates a new child BlockNode, asserts that both implementations
did it in a similar way, and returns a newly created DualBlockNode object
encapsulating both of the newly created objects """
primary_new = self.primary.add_child_block(name, parameters, position)
secondary_new = self.secondary.add_child_block(name, parameters, position)
assertions.assertEqual(primary_new, secondary_new)
new_block = DualBlockNode(primary=primary_new, secondary=secondary_new)
return new_block
def add_child_directive(self, name, parameters=None, position=None):
""" Creates a new child DirectiveNode, asserts that both implementations
did it in a similar way, and returns a newly created DualDirectiveNode
object encapsulating both of the newly created objects """
primary_new = self.primary.add_child_directive(name, parameters, position)
secondary_new = self.secondary.add_child_directive(name, parameters, position)
assertions.assertEqual(primary_new, secondary_new)
new_dir = DualDirectiveNode(primary=primary_new, secondary=secondary_new)
return new_dir
def add_child_comment(self, comment="", position=None):
""" Creates a new child CommentNode, asserts that both implementations
did it in a similar way, and returns a newly created DualCommentNode
object encapsulating both of the newly created objects """
primary_new = self.primary.add_child_comment(comment, position)
secondary_new = self.secondary.add_child_comment(comment, position)
assertions.assertEqual(primary_new, secondary_new)
new_comment = DualCommentNode(primary=primary_new, secondary=secondary_new)
return new_comment
def _create_matching_list(self, primary_list, secondary_list):
""" Matches the list of primary_list to a list of secondary_list and
returns a list of tuples. This is used to create results for find_
methods.
This helper function exists, because we cannot ensure that the list of
search results returned by primary.find_* and secondary.find_* are ordered
in a same way. The function pairs the same search results from both
implementations to a list of tuples.
"""
matched = list()
for p in primary_list:
match = None
for s in secondary_list:
try:
assertions.assertEqual(p, s)
match = s
break
except AssertionError:
continue
if match:
matched.append((p, match))
else:
raise AssertionError("Could not find a matching node.")
return matched
def find_blocks(self, name, exclude=True):
"""
Performs a search for BlockNodes using both implementations and does simple
checks for results. This is built upon the assumption that unimplemented
find_* methods return a list with a single assertion passing object.
After the assertion, it creates a list of newly created DualBlockNode
instances that encapsulate the pairs of returned BlockNode objects.
"""
return self._find_helper(DualBlockNode, "find_blocks", name,
exclude=exclude)
def find_directives(self, name, exclude=True):
"""
Performs a search for DirectiveNodes using both implementations and
checks the results. This is built upon the assumption that unimplemented
find_* methods return a list with a single assertion passing object.
After the assertion, it creates a list of newly created DualDirectiveNode
instances that encapsulate the pairs of returned DirectiveNode objects.
"""
return self._find_helper(DualDirectiveNode, "find_directives", name,
exclude=exclude)
def find_comments(self, comment):
"""
Performs a search for CommentNodes using both implementations and
checks the results. This is built upon the assumption that unimplemented
find_* methods return a list with a single assertion passing object.
After the assertion, it creates a list of newly created DualCommentNode
instances that encapsulate the pairs of returned CommentNode objects.
"""
return self._find_helper(DualCommentNode, "find_comments", comment)
def delete_child(self, child):
"""Deletes a child from the ParserNode implementations. The actual
ParserNode implementations are used here directly in order to be able
to match a child to the list of children."""
self.primary.delete_child(child.primary)
self.secondary.delete_child(child.secondary)
def unsaved_files(self):
""" Fetches the list of unsaved file paths and asserts that the lists
match """
primary_files = self.primary.unsaved_files()
secondary_files = self.secondary.unsaved_files()
assertions.assertEqualSimple(primary_files, secondary_files)
return primary_files
def parsed_paths(self):
"""
Returns a list of file paths that have currently been parsed into the parser
tree. The returned list may include paths with wildcard characters, for
example: ['/etc/apache2/conf.d/*.load']
This is typically called on the root node of the ParserNode tree.
:returns: list of file paths of files that have been parsed
"""
primary_paths = self.primary.parsed_paths()
secondary_paths = self.secondary.parsed_paths()
assertions.assertEqualPathsList(primary_paths, secondary_paths)
return primary_paths

View File

@@ -4,18 +4,18 @@
from distutils.version import LooseVersion # pylint: disable=no-name-in-module,import-error
from certbot import util
from certbot_apache import configurator
from certbot_apache import override_arch
from certbot_apache import override_fedora
from certbot_apache import override_darwin
from certbot_apache import override_debian
from certbot_apache import override_centos
from certbot_apache import override_gentoo
from certbot_apache import override_suse
from certbot_apache._internal import configurator
from certbot_apache._internal import override_arch
from certbot_apache._internal import override_centos
from certbot_apache._internal import override_darwin
from certbot_apache._internal import override_debian
from certbot_apache._internal import override_fedora
from certbot_apache._internal import override_gentoo
from certbot_apache._internal import override_suse
OVERRIDE_CLASSES = {
"arch": override_arch.ArchConfigurator,
"cloudlinux": override_centos.CentOSConfigurator,
"darwin": override_darwin.DarwinConfigurator,
"debian": override_debian.DebianConfigurator,
"ubuntu": override_debian.DebianConfigurator,
@@ -23,7 +23,10 @@ OVERRIDE_CLASSES = {
"centos linux": override_centos.CentOSConfigurator,
"fedora_old": override_centos.CentOSConfigurator,
"fedora": override_fedora.FedoraConfigurator,
"linuxmint": override_debian.DebianConfigurator,
"ol": override_centos.CentOSConfigurator,
"oracle": override_centos.CentOSConfigurator,
"redhatenterpriseserver": override_centos.CentOSConfigurator,
"red hat enterprise linux server": override_centos.CentOSConfigurator,
"rhel": override_centos.CentOSConfigurator,
"amazon": override_centos.CentOSConfigurator,
@@ -31,6 +34,9 @@ OVERRIDE_CLASSES = {
"gentoo base system": override_gentoo.GentooConfigurator,
"opensuse": override_suse.OpenSUSEConfigurator,
"suse": override_suse.OpenSUSEConfigurator,
"sles": override_suse.OpenSUSEConfigurator,
"scientific": override_centos.CentOSConfigurator,
"scientific linux": override_centos.CentOSConfigurator,
}

View File

@@ -1,19 +1,19 @@
"""A class that performs HTTP-01 challenges for Apache"""
import logging
from acme.magic_typing import List, Set # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot.compat import filesystem
from certbot.compat import os
from certbot.plugins import common
from certbot_apache.obj import VirtualHost # pylint: disable=unused-import
from certbot_apache.parser import get_aug_path
from certbot_apache._internal.obj import VirtualHost # pylint: disable=unused-import
from certbot_apache._internal.parser import get_aug_path
logger = logging.getLogger(__name__)
class ApacheHttp01(common.TLSSNI01):
class ApacheHttp01(common.ChallengePerformer):
"""Class that performs HTTP-01 challenges within the Apache configurator."""
CONFIG_TEMPLATE22_PRE = """\
@@ -168,8 +168,7 @@ class ApacheHttp01(common.TLSSNI01):
def _set_up_challenges(self):
if not os.path.isdir(self.challenge_dir):
os.makedirs(self.challenge_dir)
os.chmod(self.challenge_dir, 0o755)
filesystem.makedirs(self.challenge_dir, 0o755)
responses = []
for achall in self.achalls:
@@ -185,7 +184,7 @@ class ApacheHttp01(common.TLSSNI01):
self.configurator.reverter.register_file_creation(True, name)
with open(name, 'wb') as f:
f.write(validation.encode())
os.chmod(name, 0o644)
filesystem.chmod(name, 0o644)
return response
@@ -195,8 +194,8 @@ class ApacheHttp01(common.TLSSNI01):
if vhost not in self.moded_vhosts:
logger.debug(
"Adding a temporary challenge validation Include for name: %s " +
"in: %s", vhost.name, vhost.filep)
"Adding a temporary challenge validation Include for name: %s in: %s",
vhost.name, vhost.filep)
self.configurator.parser.add_dir_beginning(
vhost.path, "Include", self.challenge_conf_pre)
self.configurator.parser.add_dir(

View File

@@ -0,0 +1,516 @@
"""ParserNode interface for interacting with configuration tree.
General description
-------------------
The ParserNode interfaces are designed to be able to contain all the parsing logic,
while allowing their users to interact with the configuration tree in a Pythonic
and well structured manner.
The structure allows easy traversal of the tree of ParserNodes. Each ParserNode
stores a reference to its ancestor and immediate children, allowing the user to
traverse the tree using built in interface methods as well as accessing the interface
properties directly.
ParserNode interface implementation should stand between the actual underlying
parser functionality and the business logic within Configurator code, interfacing
with both. The ParserNode tree is a result of configuration parsing action.
ParserNode tree will be in charge of maintaining the parser state and hence the
abstract syntax tree (AST). Interactions between ParserNode tree and underlying
parser should involve only parsing the configuration files to this structure, and
writing it back to the filesystem - while preserving the format including whitespaces.
For some implementations (Apache for example) it's important to keep track of and
to use state information while parsing conditional blocks and directives. This
allows the implementation to set a flag to parts of the parsed configuration
structure as not being in effect in a case of unmatched conditional block. It's
important to store these blocks in the tree as well in order to not to conduct
destructive actions (failing to write back parts of the configuration) while writing
the AST back to the filesystem.
The ParserNode tree is in charge of maintaining the its own structure while every
child node fetched with find - methods or by iterating its list of children can be
changed in place. When making changes the affected nodes should be flagged as "dirty"
in order for the parser implementation to figure out the parts of the configuration
that need to be written back to disk during the save() operation.
Metadata
--------
The metadata holds all the implementation specific attributes of the ParserNodes -
things like the positional information related to the AST, file paths, whitespacing,
and any other information relevant to the underlying parser engine.
Access to the metadata should be handled by implementation specific methods, allowing
the Configurator functionality to access the underlying information where needed.
For some implementations the node can be initialized using the information carried
in metadata alone. This is useful especially when populating the ParserNode tree
while parsing the configuration.
Apache implementation
---------------------
The Apache implementation of ParserNode interface requires some implementation
specific functionalities that are not described by the interface itself.
Initialization
When the user of a ParserNode class is creating these objects, they must specify
the parameters as described in the documentation for the __init__ methods below.
When these objects are created internally, however, some parameters may not be
needed because (possibly more detailed) information is included in the metadata
parameter. In this case, implementations can deviate from the required parameters
from __init__, however, they should still behave the same when metadata is not
provided.
For consistency internally, if an argument is provided directly in the ParserNode
initialization parameters as well as within metadata it's recommended to establish
clear behavior around this scenario within the implementation.
Conditional blocks
Apache configuration can have conditional blocks, for example: <IfModule ...>,
resulting the directives and subblocks within it being either enabled or disabled.
While find_* interface methods allow including the disabled parts of the configuration
tree in searches a special care needs to be taken while parsing the structure in
order to reflect the active state of configuration.
Whitespaces
Each ParserNode object is responsible of storing its prepending whitespace characters
in order to be able to write the AST back to filesystem like it was, preserving the
format, this applies for parameters of BlockNode and DirectiveNode as well.
When parameters of ParserNode are changed, the pre-existing whitespaces in the
parameter sequence are discarded, as the general reason for storing them is to
maintain the ability to write the configuration back to filesystem exactly like
it was. This loses its meaning when we have to change the directives or blocks
parameters for other reasons.
Searches and matching
Apache configuration is largely case insensitive, so the Apache implementation of
ParserNode interface needs to provide the user means to match block and directive
names and parameters in case insensitive manner. This does not apply to everything
however, for example the parameters of a conditional statement may be case sensitive.
For this reason the internal representation of data should not ignore the case.
"""
import abc
import six
from acme.magic_typing import Any, Dict, Optional, Tuple # pylint: disable=unused-import, no-name-in-module
@six.add_metaclass(abc.ABCMeta)
class ParserNode(object):
"""
ParserNode is the basic building block of the tree of such nodes,
representing the structure of the configuration. It is largely meant to keep
the structure information intact and idiomatically accessible.
The root node as well as the child nodes of it should be instances of ParserNode.
Nodes keep track of their differences to on-disk representation of configuration
by marking modified ParserNodes as dirty to enable partial write-to-disk for
different files in the configuration structure.
While for the most parts the usage and the child types are obvious, "include"-
and similar directives are an exception to this rule. This is because of the
nature of include directives - which unroll the contents of another file or
configuration block to their place. While we could unroll the included nodes
to the parent tree, it remains important to keep the context of include nodes
separate in order to write back the original configuration as it was.
For parsers that require the implementation to keep track of the whitespacing,
it's responsibility of each ParserNode object itself to store its prepending
whitespaces in order to be able to reconstruct the complete configuration file
as it was when originally read from the disk.
ParserNode objects should have the following attributes:
# Reference to ancestor node, or None if the node is the root node of the
# configuration tree.
ancestor: Optional[ParserNode]
# True if this node has been modified since last save.
dirty: bool
# Filepath of the file where the configuration element for this ParserNode
# object resides. For root node, the value for filepath is the httpd root
# configuration file. Filepath can be None if a configuration directive is
# defined in for example the httpd command line.
filepath: Optional[str]
# Metadata dictionary holds all the implementation specific key-value pairs
# for the ParserNode instance.
metadata: Dict[str, Any]
"""
@abc.abstractmethod
def __init__(self, **kwargs):
"""
Initializes the ParserNode instance, and sets the ParserNode specific
instance variables. This is not meant to be used directly, but through
specific classes implementing ParserNode interface.
:param ancestor: BlockNode ancestor for this CommentNode. Required.
:type ancestor: BlockNode or None
:param filepath: Filesystem path for the file where this CommentNode
does or should exist in the filesystem. Required.
:type filepath: str or None
:param dirty: Boolean flag for denoting if this CommentNode has been
created or changed after the last save. Default: False.
:type dirty: bool
:param metadata: Dictionary of metadata values for this ParserNode object.
Metadata information should be used only internally in the implementation.
Default: {}
:type metadata: dict
"""
@abc.abstractmethod
def save(self, msg):
"""
Save traverses the children, and attempts to write the AST to disk for
all the objects that are marked dirty. The actual operation of course
depends on the underlying implementation. save() shouldn't be called
from the Configurator outside of its designated save() method in order
to ensure that the Reverter checkpoints are created properly.
Note: this approach of keeping internal structure of the configuration
within the ParserNode tree does not represent the file inclusion structure
of actual configuration files that reside in the filesystem. To handle
file writes properly, the file specific temporary trees should be extracted
from the full ParserNode tree where necessary when writing to disk.
:param str msg: Message describing the reason for the save.
"""
@abc.abstractmethod
def find_ancestors(self, name):
"""
Traverses the ancestor tree up, searching for BlockNodes with a specific
name.
:param str name: Name of the ancestor BlockNode to search for
:returns: A list of ancestor BlockNodes that match the name
:rtype: list of BlockNode
"""
# Linter rule exclusion done because of https://github.com/PyCQA/pylint/issues/179
@six.add_metaclass(abc.ABCMeta) # pylint: disable=abstract-method
class CommentNode(ParserNode):
"""
CommentNode class is used for representation of comments within the parsed
configuration structure. Because of the nature of comments, it is not able
to have child nodes and hence it is always treated as a leaf node.
CommentNode stores its contents in class variable 'comment' and does not
have a specific name.
CommentNode objects should have the following attributes in addition to
the ones described in ParserNode:
# Contains the contents of the comment without the directive notation
# (typically # or /* ... */).
comment: str
"""
@abc.abstractmethod
def __init__(self, **kwargs):
"""
Initializes the CommentNode instance and sets its instance variables.
:param comment: Contents of the comment. Required.
:type comment: str
:param ancestor: BlockNode ancestor for this CommentNode. Required.
:type ancestor: BlockNode or None
:param filepath: Filesystem path for the file where this CommentNode
does or should exist in the filesystem. Required.
:type filepath: str or None
:param dirty: Boolean flag for denoting if this CommentNode has been
created or changed after the last save. Default: False.
:type dirty: bool
"""
super(CommentNode, self).__init__(ancestor=kwargs['ancestor'],
dirty=kwargs.get('dirty', False),
filepath=kwargs['filepath'],
metadata=kwargs.get('metadata', {})) # pragma: no cover
@six.add_metaclass(abc.ABCMeta)
class DirectiveNode(ParserNode):
"""
DirectiveNode class represents a configuration directive within the configuration.
It can have zero or more parameters attached to it. Because of the nature of
single directives, it is not able to have child nodes and hence it is always
treated as a leaf node.
If a this directive was defined on the httpd command line, the ancestor instance
variable for this DirectiveNode should be None, and it should be inserted to the
beginning of root BlockNode children sequence.
DirectiveNode objects should have the following attributes in addition to
the ones described in ParserNode:
# True if this DirectiveNode is enabled and False if it is inside of an
# inactive conditional block.
enabled: bool
# Name, or key of the configuration directive. If BlockNode subclass of
# DirectiveNode is the root configuration node, the name should be None.
name: Optional[str]
# Tuple of parameters of this ParserNode object, excluding whitespaces.
parameters: Tuple[str, ...]
"""
@abc.abstractmethod
def __init__(self, **kwargs):
"""
Initializes the DirectiveNode instance and sets its instance variables.
:param name: Name or key of the DirectiveNode object. Required.
:type name: str or None
:param tuple parameters: Tuple of str parameters for this DirectiveNode.
Default: ().
:type parameters: tuple
:param ancestor: BlockNode ancestor for this DirectiveNode, or None for
root configuration node. Required.
:type ancestor: BlockNode or None
:param filepath: Filesystem path for the file where this DirectiveNode
does or should exist in the filesystem, or None for directives introduced
in the httpd command line. Required.
:type filepath: str or None
:param dirty: Boolean flag for denoting if this DirectiveNode has been
created or changed after the last save. Default: False.
:type dirty: bool
:param enabled: True if this DirectiveNode object is parsed in the active
configuration of the httpd. False if the DirectiveNode exists within a
unmatched conditional configuration block. Default: True.
:type enabled: bool
"""
super(DirectiveNode, self).__init__(ancestor=kwargs['ancestor'],
dirty=kwargs.get('dirty', False),
filepath=kwargs['filepath'],
metadata=kwargs.get('metadata', {})) # pragma: no cover
@abc.abstractmethod
def set_parameters(self, parameters):
"""
Sets the sequence of parameters for this ParserNode object without
whitespaces. While the whitespaces for parameters are discarded when using
this method, the whitespacing preceeding the ParserNode itself should be
kept intact.
:param list parameters: sequence of parameters
"""
@six.add_metaclass(abc.ABCMeta)
class BlockNode(DirectiveNode):
"""
BlockNode class represents a block of nested configuration directives, comments
and other blocks as its children. A BlockNode can have zero or more parameters
attached to it.
Configuration blocks typically consist of one or more child nodes of all possible
types. Because of this, the BlockNode class has various discovery and structure
management methods.
Lists of parameters used as an optional argument for some of the methods should
be lists of strings that are applicable parameters for each specific BlockNode
or DirectiveNode type. As an example, for a following configuration example:
<VirtualHost *:80>
...
</VirtualHost>
The node type would be BlockNode, name would be 'VirtualHost' and its parameters
would be: ['*:80'].
While for the following example:
LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
The node type would be DirectiveNode, name would be 'LoadModule' and its
parameters would be: ['alias_module', '/usr/lib/apache2/modules/mod_alias.so']
The applicable parameters are dependent on the underlying configuration language
and its grammar.
BlockNode objects should have the following attributes in addition to
the ones described in DirectiveNode:
# Tuple of direct children of this BlockNode object. The order of children
# in this tuple retain the order of elements in the parsed configuration
# block.
children: Tuple[ParserNode, ...]
"""
@abc.abstractmethod
def add_child_block(self, name, parameters=None, position=None):
"""
Adds a new BlockNode child node with provided values and marks the callee
BlockNode dirty. This is used to add new children to the AST. The preceeding
whitespaces should not be added based on the ancestor or siblings for the
newly created object. This is to match the current behavior of the legacy
parser implementation.
:param str name: The name of the child node to add
:param list parameters: list of parameters for the node
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: BlockNode instance of the created child block
"""
@abc.abstractmethod
def add_child_directive(self, name, parameters=None, position=None):
"""
Adds a new DirectiveNode child node with provided values and marks the
callee BlockNode dirty. This is used to add new children to the AST. The
preceeding whitespaces should not be added based on the ancestor or siblings
for the newly created object. This is to match the current behavior of the
legacy parser implementation.
:param str name: The name of the child node to add
:param list parameters: list of parameters for the node
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: DirectiveNode instance of the created child directive
"""
@abc.abstractmethod
def add_child_comment(self, comment="", position=None):
"""
Adds a new CommentNode child node with provided value and marks the
callee BlockNode dirty. This is used to add new children to the AST. The
preceeding whitespaces should not be added based on the ancestor or siblings
for the newly created object. This is to match the current behavior of the
legacy parser implementation.
:param str comment: Comment contents
:param int position: Position in the list of children to add the new child
node to. Defaults to None, which appends the newly created node to the list.
If an integer is given, the child is inserted before that index in the
list similar to list().insert.
:returns: CommentNode instance of the created child comment
"""
@abc.abstractmethod
def find_blocks(self, name, exclude=True):
"""
Find a configuration block by name. This method walks the child tree of
ParserNodes under the instance it was called from. This way it is possible
to search for the whole configuration tree, when starting from root node or
to do a partial search when starting from a specified branch. The lookup
should be case insensitive.
:param str name: The name of the directive to search for
:param bool exclude: If the search results should exclude the contents of
ParserNode objects that reside within conditional blocks and because
of current state are not enabled.
:returns: A list of found BlockNode objects.
"""
@abc.abstractmethod
def find_directives(self, name, exclude=True):
"""
Find a directive by name. This method walks the child tree of ParserNodes
under the instance it was called from. This way it is possible to search
for the whole configuration tree, when starting from root node, or to do
a partial search when starting from a specified branch. The lookup should
be case insensitive.
:param str name: The name of the directive to search for
:param bool exclude: If the search results should exclude the contents of
ParserNode objects that reside within conditional blocks and because
of current state are not enabled.
:returns: A list of found DirectiveNode objects.
"""
@abc.abstractmethod
def find_comments(self, comment):
"""
Find comments with value containing the search term.
This method walks the child tree of ParserNodes under the instance it was
called from. This way it is possible to search for the whole configuration
tree, when starting from root node, or to do a partial search when starting
from a specified branch. The lookup should be case sensitive.
:param str comment: The content of comment to search for
:returns: A list of found CommentNode objects.
"""
@abc.abstractmethod
def delete_child(self, child):
"""
Remove a specified child node from the list of children of the called
BlockNode object.
:param ParserNode child: Child ParserNode object to remove from the list
of children of the callee.
"""
@abc.abstractmethod
def unsaved_files(self):
"""
Returns a list of file paths that have been changed since the last save
(or the initial configuration parse). The intended use for this method
is to tell the Reverter which files need to be included in a checkpoint.
This is typically called for the root of the ParserNode tree.
:returns: list of file paths of files that have been changed but not yet
saved to disk.
"""
@abc.abstractmethod
def parsed_paths(self):
"""
Returns a list of file paths that have currently been parsed into the parser
tree. The returned list may include paths with wildcard characters, for
example: ['/etc/apache2/conf.d/*.load']
This is typically called on the root node of the ParserNode tree.
:returns: list of file paths of files that have been parsed
"""

View File

@@ -1,7 +1,7 @@
"""Module contains classes used by the Apache Configurator."""
import re
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot.plugins import common
@@ -24,7 +24,7 @@ class Addr(common.Addr):
return not self.__eq__(other)
def __repr__(self):
return "certbot_apache.obj.Addr(" + repr(self.tup) + ")"
return "certbot_apache._internal.obj.Addr(" + repr(self.tup) + ")"
def __hash__(self): # pylint: disable=useless-super-delegation
# Python 3 requires explicit overridden for __hash__ if __eq__ or
@@ -98,7 +98,7 @@ class Addr(common.Addr):
return self.get_addr_obj(port)
class VirtualHost(object): # pylint: disable=too-few-public-methods
class VirtualHost(object):
"""Represents an Apache Virtualhost.
:ivar str filep: file path of VH
@@ -124,9 +124,8 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
aliases=None, modmacro=False, ancestor=None):
aliases=None, modmacro=False, ancestor=None, node=None):
# pylint: disable=too-many-arguments
"""Initialize a VH."""
self.filep = filep
self.path = path
@@ -137,6 +136,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
self.enabled = enabled
self.modmacro = modmacro
self.ancestor = ancestor
self.node = node
def get_names(self):
"""Return a set of all names."""

View File

@@ -1,11 +1,11 @@
""" Distribution specific override class for Arch Linux """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import configurator
from certbot_apache import configurator
@zope.interface.provider(interfaces.IPluginFactory)
class ArchConfigurator(configurator.ApacheConfigurator):
@@ -27,5 +27,5 @@ class ArchConfigurator(configurator.ApacheConfigurator):
handle_sites=False,
challenge_location="/etc/httpd/conf",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)

View File

@@ -4,17 +4,15 @@ import logging
import pkg_resources
import zope.interface
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.compat import os
from certbot.errors import MisconfigurationError
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from certbot_apache import apache_util
from certbot_apache import configurator
from certbot_apache import parser
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal import parser
logger = logging.getLogger(__name__)
@@ -40,7 +38,7 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
handle_sites=False,
challenge_location="/etc/httpd/conf.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "centos-options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "centos-options-ssl-apache.conf"))
)
def config_test(self):
@@ -86,7 +84,7 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
def get_parser(self):
"""Initializes the ApacheParser"""
return CentOSParser(
self.aug, self.option("server_root"), self.option("vhost_root"),
self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
def _deploy_cert(self, *args, **kwargs): # pylint: disable=arguments-differ
@@ -155,7 +153,7 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
for loadmod_path in loadmod_paths:
nodir_path = loadmod_path.split("/directive")[0]
# Remove the old LoadModule directive
self.aug.remove(loadmod_path)
self.parser.aug.remove(loadmod_path)
# Create a new IfModule !mod_ssl.c if not already found on path
ssl_ifmod = self.parser.get_ifmod(nodir_path, "!mod_ssl.c",

View File

@@ -1,11 +1,11 @@
""" Distribution specific override class for macOS """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import configurator
from certbot_apache import configurator
@zope.interface.provider(interfaces.IPluginFactory)
class DarwinConfigurator(configurator.ApacheConfigurator):
@@ -27,5 +27,5 @@ class DarwinConfigurator(configurator.ApacheConfigurator):
handle_sites=False,
challenge_location="/etc/apache2/other",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)

View File

@@ -7,10 +7,10 @@ import zope.interface
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.compat import filesystem
from certbot.compat import os
from certbot_apache import apache_util
from certbot_apache import configurator
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
logger = logging.getLogger(__name__)
@@ -35,7 +35,7 @@ class DebianConfigurator(configurator.ApacheConfigurator):
handle_sites=True,
challenge_location="/etc/apache2",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def enable_site(self, vhost):
@@ -45,7 +45,7 @@ class DebianConfigurator(configurator.ApacheConfigurator):
modules are enabled appropriately.
:param vhost: vhost to enable
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
:raises .errors.NotSupportedError: If filesystem layout is not
supported.
@@ -65,20 +65,19 @@ class DebianConfigurator(configurator.ApacheConfigurator):
try:
os.symlink(vhost.filep, enabled_path)
except OSError as err:
if os.path.islink(enabled_path) and os.path.realpath(
if os.path.islink(enabled_path) and filesystem.realpath(
enabled_path) == vhost.filep:
# Already in shape
vhost.enabled = True
return None
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))
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

View File

@@ -5,10 +5,10 @@ import zope.interface
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot_apache import apache_util
from certbot_apache import configurator
from certbot_apache import parser
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal import parser
@zope.interface.provider(interfaces.IPluginFactory)
@@ -33,7 +33,7 @@ class FedoraConfigurator(configurator.ApacheConfigurator):
challenge_location="/etc/httpd/conf.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
# TODO: eventually newest version of Fedora will need their own config
"certbot_apache", "centos-options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "centos-options-ssl-apache.conf"))
)
def config_test(self):
@@ -51,7 +51,7 @@ class FedoraConfigurator(configurator.ApacheConfigurator):
def get_parser(self):
"""Initializes the ApacheParser"""
return FedoraParser(
self.aug, self.option("server_root"), self.option("vhost_root"),
self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
def _try_restart_fedora(self):

View File

@@ -1,13 +1,13 @@
""" Distribution specific override class for Gentoo Linux """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal import parser
from certbot_apache import apache_util
from certbot_apache import configurator
from certbot_apache import parser
@zope.interface.provider(interfaces.IPluginFactory)
class GentooConfigurator(configurator.ApacheConfigurator):
@@ -30,7 +30,7 @@ class GentooConfigurator(configurator.ApacheConfigurator):
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)
def _prepare_options(self):
@@ -44,7 +44,7 @@ class GentooConfigurator(configurator.ApacheConfigurator):
def get_parser(self):
"""Initializes the ApacheParser"""
return GentooParser(
self.aug, self.option("server_root"), self.option("vhost_root"),
self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
@@ -70,6 +70,6 @@ class GentooParser(parser.ApacheParser):
def update_modules(self):
"""Get loaded modules from httpd process, and add them to DOM"""
mod_cmd = [self.configurator.option("ctl"), "modules"]
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
matches = apache_util.parse_from_subprocess(mod_cmd, r"(.*)_module")
for mod in matches:
self.add_mod(mod.strip())

View File

@@ -1,11 +1,11 @@
""" Distribution specific override class for OpenSUSE """
import pkg_resources
import zope.interface
from certbot import interfaces
from certbot.compat import os
from certbot_apache._internal import configurator
from certbot_apache import configurator
@zope.interface.provider(interfaces.IPluginFactory)
class OpenSUSEConfigurator(configurator.ApacheConfigurator):
@@ -27,5 +27,5 @@ class OpenSUSEConfigurator(configurator.ApacheConfigurator):
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
"certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
)

View File

@@ -3,21 +3,22 @@ import copy
import fnmatch
import logging
import re
import subprocess
import sys
import six
from acme.magic_typing import Dict, List, Set # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot.compat import os
from certbot_apache._internal import apache_util
from certbot_apache._internal import constants
logger = logging.getLogger(__name__)
class ApacheParser(object):
# pylint: disable=too-many-public-methods
"""Class handles the fine details of parsing the Apache Configuration.
.. todo:: Make parsing general... remove sites-available etc...
@@ -32,7 +33,7 @@ class ApacheParser(object):
arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
fnmatch_chars = set(["*", "?", "\\", "[", "]"])
def __init__(self, aug, root, vhostroot=None, version=(2, 4),
def __init__(self, root, vhostroot=None, version=(2, 4),
configurator=None):
# Note: Order is important here.
@@ -41,11 +42,20 @@ class ApacheParser(object):
# issues with aug.load() after adding new files / defines to parse tree
self.configurator = configurator
# Initialize augeas
self.aug = None
self.init_augeas()
if not self.check_aug_version():
raise errors.NotSupportedError(
"Apache plugin support requires libaugeas0 and augeas-lenses "
"version 1.2.0 or higher, please make sure you have you have "
"those installed.")
self.modules = set() # type: Set[str]
self.parser_paths = {} # type: Dict[str, List[str]]
self.variables = {} # type: Dict[str, str]
self.aug = aug
# Find configuration root and make sure augeas can parse it.
self.root = os.path.abspath(root)
self.loc = {"root": self._find_config_root()}
@@ -77,6 +87,146 @@ class ApacheParser(object):
if self.find_dir("Define", exclude=False):
raise errors.PluginError("Error parsing runtime variables")
def init_augeas(self):
""" Initialize the actual Augeas instance """
try:
import augeas
except ImportError: # pragma: no cover
raise errors.NoInstallationError("Problem in Augeas installation")
self.aug = augeas.Augeas(
# specify a directory to load our preferred lens from
loadpath=constants.AUGEAS_LENS_DIR,
# Do not save backup (we do it ourselves), do not load
# anything by default
flags=(augeas.Augeas.NONE |
augeas.Augeas.NO_MODL_AUTOLOAD |
augeas.Augeas.ENABLE_SPAN))
def check_parsing_errors(self, lens):
"""Verify Augeas can parse all of the lens files.
:param str lens: lens to check for errors
:raises .errors.PluginError: If there has been an error in parsing with
the specified lens.
"""
error_files = self.aug.match("/augeas//error")
for path in error_files:
# Check to see if it was an error resulting from the use of
# the httpd lens
lens_path = self.aug.get(path + "/lens")
# As aug.get may return null
if lens_path and lens in lens_path:
msg = (
"There has been an error in parsing the file {0} on line {1}: "
"{2}".format(
# Strip off /augeas/files and /error
path[13:len(path) - 6],
self.aug.get(path + "/line"),
self.aug.get(path + "/message")))
raise errors.PluginError(msg)
def check_aug_version(self):
""" Checks that we have recent enough version of libaugeas.
If augeas version is recent enough, it will support case insensitive
regexp matching"""
self.aug.set("/test/path/testing/arg", "aRgUMeNT")
try:
matches = self.aug.match(
"/test//*[self::arg=~regexp('argument', 'i')]")
except RuntimeError:
self.aug.remove("/test/path")
return False
self.aug.remove("/test/path")
return matches
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")
# Existing Errors
ex_errs = self.aug.match("/augeas//error")
try:
# This is a noop save
self.aug.save()
except (RuntimeError, IOError):
self._log_save_errors(ex_errs)
# Erase Save Notes
self.configurator.save_notes = ""
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")
save_files = set()
if save_paths:
for path in save_paths:
save_files.add(self.aug.get(path)[6:])
return save_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
"""
if self.unsaved_files():
self.configurator.save_notes += "(autosave)"
self.configurator.save()
def save(self, save_files):
"""Saves all changes to the configuration files.
save() is called from ApacheConfigurator to handle the parser specific
tasks of saving.
:param list save_files: list of strings of file paths that we need to save.
"""
self.configurator.save_notes = ""
self.aug.save()
# Force reload if files were modified
# This is needed to recalculate augeas directive span
if save_files:
for sf in save_files:
self.aug.remove("/files/"+sf)
self.aug.load()
def _log_save_errors(self, ex_errs):
"""Log errors due to bad Augeas save.
:param list ex_errs: Existing errors before save
"""
# Check for the root of save problems
new_errs = self.aug.match("/augeas//error")
# logger.error("During Save - %s", mod_conf)
logger.error("Unable to save files: %s. Attempted Save Notes: %s",
", ".join(err[13:len(err) - 6] for err in new_errs
# Only new errors caused by recent save
if err not in ex_errs), self.configurator.save_notes)
def add_include(self, main_config, inc_path):
"""Add Include for a new configuration file if one does not exist
@@ -134,38 +284,21 @@ class ApacheParser(object):
mods.add(mod_name)
mods.add(os.path.basename(mod_filename)[:-2] + "c")
else:
logger.debug("Could not read LoadModule directive from " +
"Augeas path: %s", match_name[6:])
logger.debug("Could not read LoadModule directive from Augeas path: %s",
match_name[6:])
self.modules.update(mods)
def update_runtime_variables(self):
"""Update Includes, Defines and Includes from httpd config dump data"""
self.update_defines()
self.update_includes()
self.update_modules()
def update_defines(self):
"""Get Defines from httpd process"""
"""Updates the dictionary of known variables in the configuration"""
variables = dict()
define_cmd = [self.configurator.option("ctl"), "-t", "-D",
"DUMP_RUN_CFG"]
matches = self.parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
try:
matches.remove("DUMP_RUN_CFG")
except ValueError:
return
for match in matches:
if match.count("=") > 1:
logger.error("Unexpected number of equal signs in "
"runtime config dump.")
raise errors.PluginError(
"Error parsing Apache runtime variables")
parts = match.partition("=")
variables[parts[0]] = parts[2]
self.variables = variables
self.variables = apache_util.parse_defines(self.configurator.option("ctl"))
def update_includes(self):
"""Get includes from httpd process, and add them to DOM if needed"""
@@ -175,9 +308,7 @@ class ApacheParser(object):
# configuration files
_ = self.find_dir("Include")
inc_cmd = [self.configurator.option("ctl"), "-t", "-D",
"DUMP_INCLUDES"]
matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
matches = apache_util.parse_includes(self.configurator.option("ctl"))
if matches:
for i in matches:
if not self.parsed_in_current(i):
@@ -186,56 +317,10 @@ class ApacheParser(object):
def update_modules(self):
"""Get loaded modules from httpd process, and add them to DOM"""
mod_cmd = [self.configurator.option("ctl"), "-t", "-D",
"DUMP_MODULES"]
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
matches = apache_util.parse_modules(self.configurator.option("ctl"))
for mod in matches:
self.add_mod(mod.strip())
def parse_from_subprocess(self, command, regexp):
"""Get values from stdout of subprocess command
:param list command: Command to run
:param str regexp: Regexp for parsing
:returns: list parsed from command output
:rtype: list
"""
stdout = self._get_runtime_cfg(command)
return re.compile(regexp).findall(stdout)
def _get_runtime_cfg(self, command): # pylint: disable=no-self-use
"""Get runtime configuration info.
:param command: Command to run
:returns: stdout from command
"""
try:
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logger.error(
"Error running command %s for runtime parameters!%s",
command, os.linesep)
raise errors.MisconfigurationError(
"Error accessing loaded Apache parameters: {0}".format(
command))
# Small errors that do not impede
if proc.returncode != 0:
logger.warning("Error in checking parameter list: %s", stderr)
raise errors.MisconfigurationError(
"Apache is unable to check whether or not the module is "
"loaded because Apache is misconfigured.")
return stdout
def filter_args_num(self, matches, args): # pylint: disable=no-self-use
"""Filter out directives with specific number of arguments.
@@ -462,7 +547,7 @@ class ApacheParser(object):
"%s//*[self::directive=~regexp('%s')]" % (start, regex))
if exclude:
matches = self._exclude_dirs(matches)
matches = self.exclude_dirs(matches)
if arg is None:
arg_suffix = "/arg"
@@ -475,7 +560,7 @@ class ApacheParser(object):
# https://httpd.apache.org/docs/2.4/mod/core.html#include
for match in matches:
dir_ = self.aug.get(match).lower()
if dir_ == "include" or dir_ == "includeoptional":
if dir_ in ("include", "includeoptional"):
ordered_matches.extend(self.find_dir(
directive, arg,
self._get_include_path(self.get_arg(match + "/arg")),
@@ -515,8 +600,7 @@ class ApacheParser(object):
# e.g. strip now, not later
if not value:
return None
else:
value = value.strip("'\"")
value = value.strip("'\"")
variables = ApacheParser.arg_var_interpreter.findall(value)
@@ -529,7 +613,13 @@ class ApacheParser(object):
return value
def _exclude_dirs(self, matches):
def get_root_augpath(self):
"""
Returns the Augeas path of root configuration.
"""
return get_aug_path(self.loc["root"])
def exclude_dirs(self, matches):
"""Exclude directives that are not loaded into the configuration."""
filters = [("ifmodule", self.modules), ("ifdefine", self.variables)]
@@ -658,8 +748,7 @@ 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()
self.ensure_augeas_state()
# Test if augeas included file for Httpd.lens
# Note: This works for augeas globs, ie. *.conf
if use_new:

View File

@@ -0,0 +1,129 @@
"""ParserNode utils"""
def validate_kwargs(kwargs, required_names):
"""
Ensures that the kwargs dict has all the expected values. This function modifies
the kwargs dictionary, and hence the returned dictionary should be used instead
in the caller function instead of the original kwargs.
:param dict kwargs: Dictionary of keyword arguments to validate.
:param list required_names: List of required parameter names.
"""
validated_kwargs = dict()
for name in required_names:
try:
validated_kwargs[name] = kwargs.pop(name)
except KeyError:
raise TypeError("Required keyword argument: {} undefined.".format(name))
# Raise exception if unknown key word arguments are found.
if kwargs:
unknown = ", ".join(kwargs.keys())
raise TypeError("Unknown keyword argument(s): {}".format(unknown))
return validated_kwargs
def parsernode_kwargs(kwargs):
"""
Validates keyword arguments for ParserNode. This function modifies the kwargs
dictionary, and hence the returned dictionary should be used instead in the
caller function instead of the original kwargs.
If metadata is provided, the otherwise required argument "filepath" may be
omitted if the implementation is able to extract its value from the metadata.
This usecase is handled within this function. Filepath defaults to None.
:param dict kwargs: Keyword argument dictionary to validate.
:returns: Tuple of validated and prepared arguments.
"""
# As many values of ParserNode instances can be derived from the metadata,
# (ancestor being a common exception here) make sure we permit it here as well.
if "metadata" in kwargs:
# Filepath can be derived from the metadata in Augeas implementation.
# Default is None, as in this case the responsibility of populating this
# variable lies on the implementation.
kwargs.setdefault("filepath", None)
kwargs.setdefault("dirty", False)
kwargs.setdefault("metadata", {})
kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "metadata"])
return kwargs["ancestor"], kwargs["dirty"], kwargs["filepath"], kwargs["metadata"]
def commentnode_kwargs(kwargs):
"""
Validates keyword arguments for CommentNode and sets the default values for
optional kwargs. This function modifies the kwargs dictionary, and hence the
returned dictionary should be used instead in the caller function instead of
the original kwargs.
If metadata is provided, the otherwise required argument "comment" may be
omitted if the implementation is able to extract its value from the metadata.
This usecase is handled within this function.
:param dict kwargs: Keyword argument dictionary to validate.
:returns: Tuple of validated and prepared arguments and ParserNode kwargs.
"""
# As many values of ParserNode instances can be derived from the metadata,
# (ancestor being a common exception here) make sure we permit it here as well.
if "metadata" in kwargs:
kwargs.setdefault("comment", None)
# Filepath can be derived from the metadata in Augeas implementation.
# Default is None, as in this case the responsibility of populating this
# variable lies on the implementation.
kwargs.setdefault("filepath", None)
kwargs.setdefault("dirty", False)
kwargs.setdefault("metadata", {})
kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "comment",
"metadata"])
comment = kwargs.pop("comment")
return comment, kwargs
def directivenode_kwargs(kwargs):
"""
Validates keyword arguments for DirectiveNode and BlockNode and sets the
default values for optional kwargs. This function modifies the kwargs
dictionary, and hence the returned dictionary should be used instead in the
caller function instead of the original kwargs.
If metadata is provided, the otherwise required argument "name" may be
omitted if the implementation is able to extract its value from the metadata.
This usecase is handled within this function.
:param dict kwargs: Keyword argument dictionary to validate.
:returns: Tuple of validated and prepared arguments and ParserNode kwargs.
"""
# As many values of ParserNode instances can be derived from the metadata,
# (ancestor being a common exception here) make sure we permit it here as well.
if "metadata" in kwargs:
kwargs.setdefault("name", None)
# Filepath can be derived from the metadata in Augeas implementation.
# Default is None, as in this case the responsibility of populating this
# variable lies on the implementation.
kwargs.setdefault("filepath", None)
kwargs.setdefault("dirty", False)
kwargs.setdefault("enabled", True)
kwargs.setdefault("parameters", ())
kwargs.setdefault("metadata", {})
kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "name",
"parameters", "enabled", "metadata"])
name = kwargs.pop("name")
parameters = kwargs.pop("parameters")
enabled = kwargs.pop("enabled")
return name, parameters, enabled, kwargs

View File

@@ -1,107 +0,0 @@
""" Utility functions for certbot-apache plugin """
import binascii
from certbot import util
from certbot.compat import os
def get_mod_deps(mod_name):
"""Get known module dependencies.
.. note:: This does not need to be accurate in order for the client to
run. This simply keeps things clean if the user decides to revert
changes.
.. warning:: If all deps are not included, it may cause incorrect parsing
behavior, due to enable_mod's shortcut for updating the parser's
currently defined modules (`.ApacheParser.add_mod`)
This would only present a major problem in extremely atypical
configs that use ifmod for the missing deps.
"""
deps = {
"ssl": ["setenvif", "mime"]
}
return deps.get(mod_name, [])
def get_file_path(vhost_path):
"""Get file path from augeas_vhost_path.
Takes in Augeas path and returns the file name
:param str vhost_path: Augeas virtual host path
:returns: filename of vhost
:rtype: str
"""
if not vhost_path or not vhost_path.startswith("/files/"):
return None
return _split_aug_path(vhost_path)[0]
def get_internal_aug_path(vhost_path):
"""Get the Augeas path for a vhost with the file path removed.
:param str vhost_path: Augeas virtual host path
:returns: Augeas path to vhost relative to the containing file
:rtype: str
"""
return _split_aug_path(vhost_path)[1]
def _split_aug_path(vhost_path):
"""Splits an Augeas path into a file path and an internal path.
After removing "/files", this function splits vhost_path into the
file path and the remaining Augeas path.
:param str vhost_path: Augeas virtual host path
:returns: file path and internal Augeas path
:rtype: `tuple` of `str`
"""
# Strip off /files
file_path = vhost_path[6:]
internal_path = []
# Remove components from the end of file_path until it becomes valid
while not os.path.exists(file_path):
file_path, _, internal_path_part = file_path.rpartition("/")
internal_path.append(internal_path_part)
return file_path, "/".join(reversed(internal_path))
def parse_define_file(filepath, varname):
""" Parses Defines from a variable in configuration file
:param str filepath: Path of file to parse
:param str varname: Name of the variable
:returns: Dict of Define:Value pairs
:rtype: `dict`
"""
return_vars = {}
# Get list of words in the variable
a_opts = util.get_var_from_file(varname, filepath).split()
for i, v in enumerate(a_opts):
# Handle Define statements and make sure it has an argument
if v == "-D" and len(a_opts) >= i+2:
var_parts = a_opts[i+1].partition("=")
return_vars[var_parts[0]] = var_parts[2]
elif len(v) > 2 and v.startswith("-D"):
# Found var with no whitespace separator
var_parts = v[2:].partition("=")
return_vars[var_parts[0]] = var_parts[2]
return return_vars
def unique_id():
""" Returns an unique id to be used as a VirtualHost identifier"""
return binascii.hexlify(os.urandom(16)).decode("utf-8")

View File

@@ -1,207 +0,0 @@
"""Class of Augeas Configurators."""
import logging
from certbot import errors
from certbot.plugins import common
from certbot_apache import constants
logger = logging.getLogger(__name__)
class AugeasConfigurator(common.Installer):
"""Base Augeas Configurator class.
:ivar config: Configuration.
:type config: :class:`~certbot.interfaces.IConfig`
:ivar aug: Augeas object
:type aug: :class:`augeas.Augeas`
:ivar str save_notes: Human-readable configuration change notes
:ivar reverter: saves and reverts checkpoints
:type reverter: :class:`certbot.reverter.Reverter`
"""
def __init__(self, *args, **kwargs):
super(AugeasConfigurator, self).__init__(*args, **kwargs)
# Placeholder for augeas
self.aug = None
self.save_notes = ""
def init_augeas(self):
""" Initialize the actual Augeas instance """
import augeas
self.aug = augeas.Augeas(
# specify a directory to load our preferred lens from
loadpath=constants.AUGEAS_LENS_DIR,
# Do not save backup (we do it ourselves), do not load
# anything by default
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):
"""Verify Augeas can parse all of the lens files.
:param str lens: lens to check for errors
:raises .errors.PluginError: If there has been an error in parsing with
the specified lens.
"""
error_files = self.aug.match("/augeas//error")
for path in error_files:
# Check to see if it was an error resulting from the use of
# the httpd lens
lens_path = self.aug.get(path + "/lens")
# As aug.get may return null
if lens_path and lens in lens_path:
msg = (
"There has been an error in parsing the file {0} on line {1}: "
"{2}".format(
# Strip off /augeas/files and /error
path[13:len(path) - 6],
self.aug.get(path + "/line"),
self.aug.get(path + "/message")))
raise errors.PluginError(msg)
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
"""
if self.unsaved_files():
self.save_notes += "(autosave)"
self.save()
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")
# Existing Errors
ex_errs = self.aug.match("/augeas//error")
try:
# This is a noop save
self.aug.save()
except (RuntimeError, IOError):
self._log_save_errors(ex_errs)
# Erase Save Notes
self.save_notes = ""
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")
save_files = set()
if save_paths:
for path in save_paths:
save_files.add(self.aug.get(path)[6:])
return save_files
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.save_notes = ""
self.aug.save()
# Force reload if files were modified
# This is needed to recalculate augeas directive span
if save_files:
for sf in save_files:
self.aug.remove("/files/"+sf)
self.aug.load()
if title and not temporary:
self.finalize_checkpoint(title)
def _log_save_errors(self, ex_errs):
"""Log errors due to bad Augeas save.
:param list ex_errs: Existing errors before save
"""
# Check for the root of save problems
new_errs = self.aug.match("/augeas//error")
# logger.error("During Save - %s", mod_conf)
logger.error("Unable to save files: %s. Attempted Save Notes: %s",
", ".join(err[13:len(err) - 6] for err in new_errs
# Only new errors caused by recent save
if err not in ex_errs), self.save_notes)
# Wrapper functions for Reverter class
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
"""
super(AugeasConfigurator, self).recovery_routine()
# Need to reload configuration after these changes take effect
self.aug.load()
def revert_challenge_config(self):
"""Used to cleanup challenge configurations.
:raises .errors.PluginError: If unable to revert the challenge config.
"""
self.revert_temporary_config()
self.aug.load()
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
"""
super(AugeasConfigurator, self).rollback_checkpoints(rollback)
self.aug.load()

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