mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 16:22:18 -04:00
Merge remote-tracking branch 'origin/master' into py26
This commit is contained in:
commit
02053892d1
46 changed files with 968 additions and 232 deletions
|
|
@ -27,7 +27,7 @@ If ``letsencrypt`` is packaged for your OS, you can install it from there, and
|
|||
run it by typing ``letsencrypt``. Because not all operating systems have
|
||||
packages yet, we provide a temporary solution via the ``letsencrypt-auto``
|
||||
wrapper script, which obtains some dependencies from your OS and puts others
|
||||
in an python virtual environment::
|
||||
in a python virtual environment::
|
||||
|
||||
user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt
|
||||
user@webserver:~$ cd letsencrypt
|
||||
|
|
@ -128,7 +128,7 @@ launch. The client requires root access in order to write to
|
|||
bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and
|
||||
modify webserver configurations (if you use the ``apache`` or ``nginx``
|
||||
plugins). If none of these apply to you, it is theoretically possible to run
|
||||
without root privilegess, but for most users who want to avoid running an ACME
|
||||
without root privileges, but for most users who want to avoid running an ACME
|
||||
client as root, either `letsencrypt-nosudo
|
||||
<https://github.com/diafygi/letsencrypt-nosudo>`_ or `simp_le
|
||||
<https://github.com/kuba/simp_le>`_ are more appropriate choices.
|
||||
|
|
@ -163,5 +163,5 @@ Current Features
|
|||
* Free and Open Source Software, made with Python.
|
||||
|
||||
|
||||
.. _Freenode: https://freenode.net
|
||||
.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt
|
||||
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ from acme import messages
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Python does not validate certificates by default before version 2.7.9
|
||||
# Prior to Python 2.7.9 the stdlib SSL module did not allow a user to configure
|
||||
# many important security related options. On these platforms we use PyOpenSSL
|
||||
# for SSL, which does allow these options to be configured.
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
if sys.version_info < (2, 7, 9): # pragma: no cover
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
|
@ -338,7 +340,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
`PollError` with non-empty ``waiting`` is raised.
|
||||
|
||||
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
|
||||
the issued certificate (`.messages.CertificateResource.),
|
||||
the issued certificate (`.messages.CertificateResource`),
|
||||
and ``updated_authzrs`` is a `tuple` consisting of updated
|
||||
Authorization Resources (`.AuthorizationResource`) as
|
||||
present in the responses from server, and in the same order
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ class JSONDeSerializable(object):
|
|||
:rtype: str
|
||||
|
||||
"""
|
||||
return self.json_dumps(sort_keys=True, indent=4)
|
||||
return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
|
||||
|
||||
@classmethod
|
||||
def json_dump_default(cls, python_object):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"""Tests for acme.jose.interfaces."""
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class JSONDeSerializableTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
|
@ -92,9 +90,8 @@ class JSONDeSerializableTest(unittest.TestCase):
|
|||
self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
|
||||
|
||||
def test_json_dumps_pretty(self):
|
||||
filler = ' ' if six.PY2 else ''
|
||||
self.assertEqual(self.seq.json_dumps_pretty(),
|
||||
'[\n "foo1",{0}\n "foo2"\n]'.format(filler))
|
||||
'[\n "foo1",\n "foo2"\n]')
|
||||
|
||||
def test_json_dump_default(self):
|
||||
from acme.jose.interfaces import JSONDeSerializable
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ class Error(jose.JSONObjectWithFields, errors.Error):
|
|||
('urn:acme:error:' + name, description) for name, description in (
|
||||
('badCSR', 'The CSR is unacceptable (e.g., due to a short key)'),
|
||||
('badNonce', 'The client sent an unacceptable anti-replay nonce'),
|
||||
('connection', 'The server could not connect to the client for DV'),
|
||||
('connection', 'The server could not connect to the client to '
|
||||
'verify the domain'),
|
||||
('dnssec', 'The server could not validate a DNSSEC signed domain'),
|
||||
('malformed', 'The request message was malformed'),
|
||||
('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 DV'),
|
||||
('tls', 'The server experienced a TLS error during domain '
|
||||
'verification'),
|
||||
('unauthorized', 'The client lacks sufficient authorization'),
|
||||
('unknownHost', 'The server could not resolve a domain name'),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ acme = client.Client(DIRECTORY_URL, key)
|
|||
|
||||
regr = acme.register()
|
||||
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
|
||||
acme.update_registration(regr.update(
|
||||
body=regr.body.update(agreement=regr.terms_of_service)))
|
||||
acme.agree_to_tos(regr)
|
||||
logging.debug(regr)
|
||||
|
||||
authzr = acme.request_challenges(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.1.0.dev0'
|
||||
version = '0.2.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
# load_pem_private/public_key (>=0.6)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Tested with:
|
||||
# - Fedora 22, 23 (x64)
|
||||
# - Centos 7 (x64: onD igitalOcean droplet)
|
||||
# - Centos 7 (x64: on DigitalOcean droplet)
|
||||
|
||||
if type dnf 2>/dev/null
|
||||
then
|
||||
|
|
|
|||
|
|
@ -31,16 +31,21 @@ Firstly, please `install Git`_ and run the following commands:
|
|||
|
||||
.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
|
||||
|
||||
.. note:: On RedHat/CentOS 6 you will need to enable the EPEL_
|
||||
repository before install.
|
||||
|
||||
.. _EPEL: http://fedoraproject.org/wiki/EPEL
|
||||
|
||||
To install and run the client you just need to type:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
./letsencrypt-auto
|
||||
|
||||
.. note:: On RedHat/CentOS 6 you will need to enable the EPEL_
|
||||
repository before install.
|
||||
|
||||
.. _EPEL: http://fedoraproject.org/wiki/EPEL
|
||||
.. hint:: During the beta phase, Let's Encrypt enforces strict rate limits on
|
||||
the number of certificates issued for one domain. It is recommended to
|
||||
initially use the test server via `--test-cert` until you get the desired
|
||||
certificates.
|
||||
|
||||
Throughout the documentation, whenever you see references to
|
||||
``letsencrypt`` script/binary, you can substitute in
|
||||
|
|
@ -58,8 +63,8 @@ or for full help, type:
|
|||
|
||||
|
||||
``letsencrypt-auto`` is the recommended method of running the Let's Encrypt
|
||||
client beta releases on systems that don't have a packaged version. Debian
|
||||
experimental, Arch linux and FreeBSD now have native packages, so on those
|
||||
client beta releases on systems that don't have a packaged version. Debian,
|
||||
Arch linux and FreeBSD now have native packages, so on those
|
||||
systems you can just install ``letsencrypt`` (and perhaps
|
||||
``letsencrypt-apache``). If you'd like to run the latest copy from Git, or
|
||||
run your own locally modified copy of the client, follow the instructions in
|
||||
|
|
@ -173,10 +178,11 @@ Renewal
|
|||
In order to renew certificates simply call the ``letsencrypt`` (or
|
||||
letsencrypt-auto_) again, and use the same values when prompted. You
|
||||
can automate it slightly by passing necessary flags on the CLI (see
|
||||
`--help all`), or even further using the :ref:`config-file`. If you're
|
||||
sure that UI doesn't prompt for any details you can add the command to
|
||||
``crontab`` (make it less than every 90 days to avoid problems, say
|
||||
every month).
|
||||
`--help all`), or even further using the :ref:`config-file`. The
|
||||
``--renew-by-default`` flag may be helpful for automating renewal. If
|
||||
you're sure that UI doesn't prompt for any details you can add the
|
||||
command to ``crontab`` (make it less than every 90 days to avoid
|
||||
problems, say every month).
|
||||
|
||||
Please note that the CA will send notification emails to the address
|
||||
you provide if you do not renew certificates that are about to expire.
|
||||
|
|
@ -223,21 +229,23 @@ The following files are available:
|
|||
``cert.pem``
|
||||
Server certificate only.
|
||||
|
||||
This is what Apache needs for `SSLCertificateFile
|
||||
This is what Apache < 2.4.8 needs for `SSLCertificateFile
|
||||
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_.
|
||||
|
||||
``chain.pem``
|
||||
All certificates that need to be served by the browser **excluding**
|
||||
server certificate, i.e. root and intermediate certificates only.
|
||||
|
||||
This is what Apache needs for `SSLCertificateChainFile
|
||||
This is what Apache < 2.4.8 needs for `SSLCertificateChainFile
|
||||
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatechainfile>`_.
|
||||
|
||||
``fullchain.pem``
|
||||
All certificates, **including** server certificate. This is
|
||||
concatenation of ``chain.pem`` and ``cert.pem``.
|
||||
|
||||
This is what nginx needs for `ssl_certificate
|
||||
This is what Apache >= 2.4.8 needs for `SSLCertificateFile
|
||||
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_,
|
||||
and what nginx needs for `ssl_certificate
|
||||
<http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate>`_.
|
||||
|
||||
|
||||
|
|
@ -286,7 +294,7 @@ get support on our `forums <https://community.letsencrypt.org>`_.
|
|||
If you find a bug in the software, please do report it in our `issue
|
||||
tracker
|
||||
<https://github.com/letsencrypt/letsencrypt/issues>`_. Remember to
|
||||
give us us as much information as possible:
|
||||
give us as much information as possible:
|
||||
|
||||
- copy and paste exact command line used and the output (though mind
|
||||
that the latter might include some personally identifiable
|
||||
|
|
@ -349,20 +357,20 @@ Operating System Packages
|
|||
|
||||
sudo pacman -S letsencrypt letsencrypt-apache
|
||||
|
||||
**Debian Experimental**
|
||||
**Debian**
|
||||
|
||||
If you run Debian unstable, you can install experimental letsencrypt packages.
|
||||
Add the line ``deb http://ftp.us.debian.org/debian/ experimental main`` (or
|
||||
the equivalent for your country) to ``/etc/apt/sources.list``, then run
|
||||
If you run Debian Stretch or Debian Sid, you can install letsencrypt packages.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get -t experimental install letsencrypt python-letsencrypt-apache
|
||||
sudo apt-get install letsencrypt python-letsencrypt-apache
|
||||
|
||||
If you don't want to use the Apache plugin, you can ommit the
|
||||
``python-letsencrypt-apache`` package.
|
||||
|
||||
Packages for Debian Jessie are coming in the next few weeks.
|
||||
|
||||
**Other Operating Systems**
|
||||
|
||||
OS packaging is an ongoing effort. If you'd like to package
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ server = https://acme-staging.api.letsencrypt.org/directory
|
|||
# Uncomment and update to register with the specified e-mail address
|
||||
# email = foo@example.com
|
||||
|
||||
# Uncomment and update to generate certificates for the specified
|
||||
# domains.
|
||||
# domains = example.com, www.example.com
|
||||
|
||||
# Uncomment to use a text interface instead of ncurses
|
||||
# text = True
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
Let's Encrypt includes the very latest Augeas lenses in order to ship bug fixes
|
||||
to Apacche configuration handling bugs as quickly as possible
|
||||
to Apache configuration handling bugs as quickly as possible
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ let sep_osp = Sep.opt_space
|
|||
let sep_eq = del /[ \t]*=[ \t]*/ "="
|
||||
|
||||
let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/
|
||||
let word = /[a-zA-Z][a-zA-Z0-9._-]*/
|
||||
let word = /[a-z][a-z0-9._-]*/i
|
||||
|
||||
let comment = Util.comment
|
||||
let eol = Util.doseol
|
||||
|
|
@ -59,13 +59,18 @@ let empty = Util.empty_dos
|
|||
let indent = Util.indent
|
||||
|
||||
(* borrowed from shellvars.aug *)
|
||||
let char_arg_dir = /([^\\ '"\t\r\n]|[^\\ '"\t\r\n][^ '"\t\r\n]*[^\\ '"\t\r\n])|\\\\"|\\\\'/
|
||||
let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ '"\t\r\n])|\\\\"|\\\\'/
|
||||
let char_arg_sec = /[^ '"\t\r\n>]|\\\\"|\\\\'/
|
||||
let char_arg_wl = /([^\\ '"},\t\r\n]|[^ '"},\t\r\n]+[^\\ '"},\t\r\n])/
|
||||
|
||||
let cdot = /\\\\./
|
||||
let cl = /\\\\\n/
|
||||
let dquot =
|
||||
let no_dquot = /[^"\\\r\n]/
|
||||
in /"/ . (no_dquot|cdot|cl)* . /"/
|
||||
let dquot_msg =
|
||||
let no_dquot = /([^ \t"\\\r\n]|[^"\\\r\n]+[^ \t"\\\r\n])/
|
||||
in /"/ . (no_dquot|cdot|cl)*
|
||||
let squot =
|
||||
let no_squot = /[^'\\\r\n]/
|
||||
in /'/ . (no_squot|cdot|cl)* . /'/
|
||||
|
|
@ -76,12 +81,24 @@ let comp = /[<>=]?=/
|
|||
*****************************************************************)
|
||||
|
||||
let arg_dir = [ label "arg" . store (char_arg_dir+|dquot|squot) ]
|
||||
(* message argument starts with " but ends at EOL *)
|
||||
let arg_dir_msg = [ label "arg" . store dquot_msg ]
|
||||
let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ]
|
||||
let arg_wl = [ label "arg" . store (char_arg_wl+|dquot|squot) ]
|
||||
|
||||
(* comma-separated wordlist as permitted in the SSLRequire directive *)
|
||||
let arg_wordlist =
|
||||
let wl_start = Util.del_str "{" in
|
||||
let wl_end = Util.del_str "}" in
|
||||
let wl_sep = del /[ \t]*,[ \t]*/ ", "
|
||||
in [ label "wordlist" . wl_start . arg_wl . (wl_sep . arg_wl)* . wl_end ]
|
||||
|
||||
let argv (l:lens) = l . (sep_spc . l)*
|
||||
|
||||
let directive = [ indent . label "directive" . store word .
|
||||
(sep_spc . argv arg_dir)? . eol ]
|
||||
let directive =
|
||||
(* arg_dir_msg may be the last or only argument *)
|
||||
let dir_args = (argv (arg_dir|arg_wordlist) . (sep_spc . arg_dir_msg)?) | arg_dir_msg
|
||||
in [ indent . label "directive" . store word . (sep_spc . dir_args)? . eol ]
|
||||
|
||||
let section (body:lens) =
|
||||
(* opt_eol includes empty lines *)
|
||||
|
|
@ -91,7 +108,7 @@ let section (body:lens) =
|
|||
indent . dels "</" in
|
||||
let kword = key word in
|
||||
let dword = del word "a" in
|
||||
[ indent . dels "<" . square kword inner dword . del ">" ">" . eol ]
|
||||
[ indent . dels "<" . square kword inner dword . del />[ \t\n\r]*/ ">\n" ]
|
||||
|
||||
let rec content = section (content|directive)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
add("enmod", default=constants.CLI_DEFAULTS["enmod"],
|
||||
help="Path to the Apache 'a2enmod' binary.")
|
||||
add("dismod", default=constants.CLI_DEFAULTS["dismod"],
|
||||
help="Path to the Apache 'a2enmod' binary.")
|
||||
help="Path to the Apache 'a2dismod' binary.")
|
||||
add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"],
|
||||
help="SSL vhost configuration extension.")
|
||||
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
||||
|
|
@ -120,7 +120,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
self.version = version
|
||||
self.vhosts = None
|
||||
self._enhance_func = {"redirect": self._enable_redirect,
|
||||
"ensure-http-header": self._set_http_header}
|
||||
"ensure-http-header": self._set_http_header}
|
||||
|
||||
@property
|
||||
def mod_ssl_conf(self):
|
||||
|
|
@ -545,21 +545,43 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
|
||||
# Check for Listen <port>
|
||||
# Note: This could be made to also look for ip:443 combo
|
||||
if not self.parser.find_dir("Listen", port):
|
||||
logger.debug("No Listen %s directive found. Setting the "
|
||||
"Apache Server to Listen on port %s", port, port)
|
||||
|
||||
if port == "443":
|
||||
args = [port]
|
||||
listens = [self.parser.get_arg(x).split()[0] for x in self.parser.find_dir("Listen")]
|
||||
# In case no Listens are set (which really is a broken apache config)
|
||||
if not listens:
|
||||
listens = ["80"]
|
||||
for listen in listens:
|
||||
# For any listen statement, check if the machine also listens on Port 443.
|
||||
# If not, add such a listen statement.
|
||||
if len(listen.split(":")) == 1:
|
||||
# Its listening to all interfaces
|
||||
if port not in listens:
|
||||
if port == "443":
|
||||
args = [port]
|
||||
else:
|
||||
# Non-standard ports should specify https protocol
|
||||
args = [port, "https"]
|
||||
self.parser.add_dir_to_ifmodssl(
|
||||
parser.get_aug_path(
|
||||
self.parser.loc["listen"]), "Listen", args)
|
||||
self.save_notes += "Added Listen %s directive to %s\n" % (
|
||||
port, self.parser.loc["listen"])
|
||||
listens.append(port)
|
||||
else:
|
||||
# Non-standard ports should specify https protocol
|
||||
args = [port, "https"]
|
||||
|
||||
self.parser.add_dir_to_ifmodssl(
|
||||
parser.get_aug_path(
|
||||
self.parser.loc["listen"]), "Listen", args)
|
||||
self.save_notes += "Added Listen %s directive to %s\n" % (
|
||||
port, self.parser.loc["listen"])
|
||||
# The Listen statement specifies an ip
|
||||
_, ip = listen[::-1].split(":", 1)
|
||||
ip = ip[::-1]
|
||||
if "%s:%s" % (ip, port) not in listens:
|
||||
if port == "443":
|
||||
args = ["%s:%s" % (ip, port)]
|
||||
else:
|
||||
# Non-standard ports should specify https protocol
|
||||
args = ["%s:%s" % (ip, port), "https"]
|
||||
self.parser.add_dir_to_ifmodssl(
|
||||
parser.get_aug_path(
|
||||
self.parser.loc["listen"]), "Listen", args)
|
||||
self.save_notes += "Added Listen %s:%s directive to %s\n" % (
|
||||
ip, port, self.parser.loc["listen"])
|
||||
listens.append("%s:%s" % (ip, port))
|
||||
|
||||
def make_addrs_sni_ready(self, addrs):
|
||||
"""Checks to see if the server is ready for SNI challenges.
|
||||
|
|
|
|||
|
|
@ -391,6 +391,39 @@ class TwoVhost80Test(util.ApacheTest):
|
|||
|
||||
self.assertEqual(mock_add_dir.call_count, 2)
|
||||
|
||||
def test_prepare_server_https_named_listen(self):
|
||||
mock_find = mock.Mock()
|
||||
mock_find.return_value = ["test1", "test2", "test3"]
|
||||
mock_get = mock.Mock()
|
||||
mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"]
|
||||
mock_add_dir = mock.Mock()
|
||||
mock_enable = mock.Mock()
|
||||
|
||||
self.config.parser.find_dir = mock_find
|
||||
self.config.parser.get_arg = mock_get
|
||||
self.config.parser.add_dir_to_ifmodssl = mock_add_dir
|
||||
self.config.enable_mod = mock_enable
|
||||
|
||||
# Test Listen statements with specific ip listeed
|
||||
self.config.prepare_server_https("443")
|
||||
# Should only be 2 here, as the third interface already listens to the correct port
|
||||
self.assertEqual(mock_add_dir.call_count, 2)
|
||||
|
||||
# Check argument to new Listen statements
|
||||
self.assertEqual(mock_add_dir.call_args_list[0][0][2], ["1.2.3.4:443"])
|
||||
self.assertEqual(mock_add_dir.call_args_list[1][0][2], ["[::1]:443"])
|
||||
|
||||
# Reset return lists and inputs
|
||||
mock_add_dir.reset_mock()
|
||||
mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"]
|
||||
|
||||
# Test
|
||||
self.config.prepare_server_https("8080", temp=True)
|
||||
self.assertEqual(mock_add_dir.call_count, 3)
|
||||
self.assertEqual(mock_add_dir.call_args_list[0][0][2], ["1.2.3.4:8080", "https"])
|
||||
self.assertEqual(mock_add_dir.call_args_list[1][0][2], ["[::1]:8080", "https"])
|
||||
self.assertEqual(mock_add_dir.call_args_list[2][0][2], ["1.1.1.1:8080", "https"])
|
||||
|
||||
def test_make_vhost_ssl(self):
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
<VirtualHost 1.1.1.1>
|
||||
|
||||
ServerName invalid.net
|
||||
|
||||
</virtualHost>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.1.0.dev0'
|
||||
version = '0.2.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
|
|
|
|||
|
|
@ -147,9 +147,9 @@ then
|
|||
elif uname | grep -iq FreeBSD ; then
|
||||
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
|
||||
elif uname | grep -iq Darwin ; then
|
||||
ExperimentalBootstrap "Mac OS X" mac.sh
|
||||
ExperimentalBootstrap "Mac OS X" mac.sh # homebrew doesn't normally run as root
|
||||
elif grep -iq "Amazon Linux" /etc/issue ; then
|
||||
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh
|
||||
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh "$SUDO"
|
||||
else
|
||||
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
|
||||
echo
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.1.0.dev0'
|
||||
version = '0.2.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'letsencrypt=={0}'.format(version),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.1.0.dev0'
|
||||
version = '0.2.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Let's Encrypt client."""
|
||||
|
||||
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
|
||||
__version__ = '0.1.0.dev0'
|
||||
__version__ = '0.2.0.dev0'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
"""Let's Encrypt CLI."""
|
||||
from __future__ import print_function
|
||||
|
||||
# TODO: Sanity check all input. Be sure to avoid shell code etc...
|
||||
# pylint: disable=too-many-lines
|
||||
# (TODO: split this file into main.py and cli.py)
|
||||
|
|
@ -205,76 +207,131 @@ def _find_duplicative_certs(config, domains):
|
|||
if candidate_names == set(domains):
|
||||
identical_names_cert = candidate_lineage
|
||||
elif candidate_names.issubset(set(domains)):
|
||||
subset_names_cert = candidate_lineage
|
||||
# This logic finds and returns the largest subset-names cert
|
||||
# in the case where there are several available.
|
||||
if subset_names_cert is None:
|
||||
subset_names_cert = candidate_lineage
|
||||
elif len(candidate_names) > len(subset_names_cert.names()):
|
||||
subset_names_cert = candidate_lineage
|
||||
|
||||
return identical_names_cert, subset_names_cert
|
||||
|
||||
|
||||
def _treat_as_renewal(config, domains):
|
||||
"""Determine whether or not the call should be treated as a renewal.
|
||||
"""Determine whether there are duplicated names and how to handle them.
|
||||
|
||||
:returns: RenewableCert or None if renewal shouldn't occur.
|
||||
:rtype: :class:`.storage.RenewableCert`
|
||||
:returns: Two-element tuple containing desired new-certificate behavior as
|
||||
a string token ("reinstall", "renew", or "newcert"), plus either
|
||||
a RenewableCert instance or None if renewal shouldn't occur.
|
||||
|
||||
:raises .Error: If the user would like to rerun the client again.
|
||||
|
||||
"""
|
||||
renewal = False
|
||||
|
||||
# Considering the possibility that the requested certificate is
|
||||
# related to an existing certificate. (config.duplicate, which
|
||||
# is set with --duplicate, skips all of this logic and forces any
|
||||
# kind of certificate to be obtained with renewal = False.)
|
||||
if not config.duplicate:
|
||||
ident_names_cert, subset_names_cert = _find_duplicative_certs(
|
||||
config, domains)
|
||||
# I am not sure whether that correctly reads the systemwide
|
||||
# configuration file.
|
||||
question = None
|
||||
if ident_names_cert is not None:
|
||||
question = (
|
||||
"You have an existing certificate that contains exactly the "
|
||||
"same domains you requested (ref: {0}){br}{br}Do you want to "
|
||||
"renew and replace this certificate with a newly-issued one?"
|
||||
).format(ident_names_cert.configfile.filename, br=os.linesep)
|
||||
elif subset_names_cert is not None:
|
||||
question = (
|
||||
"You have an existing certificate that contains a portion of "
|
||||
"the domains you requested (ref: {0}){br}{br}It contains these "
|
||||
"names: {1}{br}{br}You requested these names for the new "
|
||||
"certificate: {2}.{br}{br}Do you want to replace this existing "
|
||||
"certificate with the new certificate?"
|
||||
).format(subset_names_cert.configfile.filename,
|
||||
", ".join(subset_names_cert.names()),
|
||||
", ".join(domains),
|
||||
br=os.linesep)
|
||||
if question is None:
|
||||
# We aren't in a duplicative-names situation at all, so we don't
|
||||
# have to tell or ask the user anything about this.
|
||||
pass
|
||||
elif config.renew_by_default or zope.component.getUtility(
|
||||
interfaces.IDisplay).yesno(question, "Replace", "Cancel"):
|
||||
renewal = True
|
||||
else:
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter_util.add_message(
|
||||
"To obtain a new certificate that {0} an existing certificate "
|
||||
"in its domain-name coverage, you must use the --duplicate "
|
||||
"option.{br}{br}For example:{br}{br}{1} --duplicate {2}".format(
|
||||
"duplicates" if ident_names_cert is not None else
|
||||
"overlaps with",
|
||||
sys.argv[0], " ".join(sys.argv[1:]),
|
||||
br=os.linesep
|
||||
),
|
||||
reporter_util.HIGH_PRIORITY)
|
||||
raise errors.Error(
|
||||
"User did not use proper CLI and would like "
|
||||
"to reinvoke the client.")
|
||||
if config.duplicate:
|
||||
return "newcert", None
|
||||
# TODO: Also address superset case
|
||||
ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains)
|
||||
# XXX ^ schoen is not sure whether that correctly reads the systemwide
|
||||
# configuration file.
|
||||
if ident_names_cert is None and subset_names_cert is None:
|
||||
return "newcert", None
|
||||
|
||||
if renewal:
|
||||
return ident_names_cert if ident_names_cert is not None else subset_names_cert
|
||||
if ident_names_cert is not None:
|
||||
return _handle_identical_cert_request(config, ident_names_cert)
|
||||
elif subset_names_cert is not None:
|
||||
return _handle_subset_cert_request(config, domains, subset_names_cert)
|
||||
|
||||
return None
|
||||
def _handle_identical_cert_request(config, cert):
|
||||
"""Figure out what to do if a cert has the same names as a perviously obtained one
|
||||
|
||||
:param storage.RenewableCert cert:
|
||||
|
||||
:returns: Tuple of (string, cert_or_None) as per _treat_as_renewal
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
if config.renew_by_default:
|
||||
logger.info("Auto-renewal forced with --renew-by-default...")
|
||||
return "renew", cert
|
||||
if cert.should_autorenew(interactive=True):
|
||||
logger.info("Cert is due for renewal, auto-renewing...")
|
||||
return "renew", cert
|
||||
if config.reinstall:
|
||||
# Set with --reinstall, force an identical certificate to be
|
||||
# reinstalled without further prompting.
|
||||
return "reinstall", cert
|
||||
|
||||
question = (
|
||||
"You have an existing certificate that contains exactly the same "
|
||||
"domains you requested and isn't close to expiry."
|
||||
"{br}(ref: {0}){br}{br}What would you like to do?"
|
||||
).format(cert.configfile.filename, br=os.linesep)
|
||||
|
||||
if config.verb == "run":
|
||||
keep_opt = "Attempt to reinstall this existing certificate"
|
||||
elif config.verb == "certonly":
|
||||
keep_opt = "Keep the existing certificate for now"
|
||||
choices = [keep_opt,
|
||||
"Renew & replace the cert (limit ~5 per 7 days)",
|
||||
"Cancel this operation and do nothing"]
|
||||
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
response = display.menu(question, choices, "OK", "Cancel")
|
||||
if response[0] == "cancel" or response[1] == 2:
|
||||
# TODO: Add notification related to command-line options for
|
||||
# skipping the menu for this case.
|
||||
raise errors.Error(
|
||||
"User chose to cancel the operation and may "
|
||||
"reinvoke the client.")
|
||||
elif response[1] == 0:
|
||||
return "reinstall", cert
|
||||
elif response[1] == 1:
|
||||
return "renew", cert
|
||||
else:
|
||||
assert False, "This is impossible"
|
||||
|
||||
def _handle_subset_cert_request(config, domains, cert):
|
||||
"""Figure out what to do if a previous cert had a subset of the names now requested
|
||||
|
||||
:param storage.RenewableCert cert:
|
||||
|
||||
:returns: Tuple of (string, cert_or_None) as per _treat_as_renewal
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
existing = ", ".join(cert.names())
|
||||
question = (
|
||||
"You have an existing certificate that contains a portion of "
|
||||
"the domains you requested (ref: {0}){br}{br}It contains these "
|
||||
"names: {1}{br}{br}You requested these names for the new "
|
||||
"certificate: {2}.{br}{br}Do you want to expand and replace this existing "
|
||||
"certificate with the new certificate?"
|
||||
).format(cert.configfile.filename,
|
||||
existing,
|
||||
", ".join(domains),
|
||||
br=os.linesep)
|
||||
if config.expand or config.renew_by_default or zope.component.getUtility(
|
||||
interfaces.IDisplay).yesno(question, "Expand", "Cancel"):
|
||||
return "renew", cert
|
||||
else:
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter_util.add_message(
|
||||
"To obtain a new certificate that contains these names without "
|
||||
"replacing your existing certificate for {0}, you must use the "
|
||||
"--duplicate option.{br}{br}"
|
||||
"For example:{br}{br}{1} --duplicate {2}".format(
|
||||
existing,
|
||||
sys.argv[0], " ".join(sys.argv[1:]),
|
||||
br=os.linesep
|
||||
),
|
||||
reporter_util.HIGH_PRIORITY)
|
||||
raise errors.Error(
|
||||
"User chose to cancel the operation and may "
|
||||
"reinvoke the client.")
|
||||
|
||||
|
||||
def _report_new_cert(cert_path, fullchain_path):
|
||||
|
|
@ -306,7 +363,7 @@ def _report_new_cert(cert_path, fullchain_path):
|
|||
def _suggest_donate():
|
||||
"Suggest a donation to support Let's Encrypt"
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
msg = ("If like Let's Encrypt, please consider supporting our work by:\n\n"
|
||||
msg = ("If you like Let's Encrypt, please consider supporting our work by:\n\n"
|
||||
"Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
|
||||
"Donating to EFF: https://eff.org/donate-le\n\n")
|
||||
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
|
||||
|
|
@ -314,10 +371,21 @@ def _suggest_donate():
|
|||
|
||||
def _auth_from_domains(le_client, config, domains):
|
||||
"""Authenticate and enroll certificate."""
|
||||
# Note: This can raise errors... caught above us though.
|
||||
lineage = _treat_as_renewal(config, domains)
|
||||
# Note: This can raise errors... caught above us though. This is now
|
||||
# a three-way case: reinstall (which results in a no-op here because
|
||||
# although there is a relevant lineage, we don't do anything to it
|
||||
# inside this function -- we don't obtain a new certificate), renew
|
||||
# (which results in treating the request as a renewal), or newcert
|
||||
# (which results in treating the request as a new certificate request).
|
||||
|
||||
if lineage is not None:
|
||||
action, lineage = _treat_as_renewal(config, domains)
|
||||
if action == "reinstall":
|
||||
# The lineage already exists; allow the caller to try installing
|
||||
# it without getting a new certificate at all.
|
||||
return lineage
|
||||
elif action == "renew":
|
||||
original_server = lineage.configuration["renewalparams"]["server"]
|
||||
_avoid_invalidating_lineage(config, lineage, original_server)
|
||||
# TODO: schoen wishes to reuse key - discussion
|
||||
# https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
|
||||
|
|
@ -331,7 +399,7 @@ def _auth_from_domains(le_client, config, domains):
|
|||
# TODO: Check return value of save_successor
|
||||
# TODO: Also update lineage renewal config with any relevant
|
||||
# configuration values from this attempt? <- Absolutely (jdkasten)
|
||||
else:
|
||||
elif action == "newcert":
|
||||
# TREAT AS NEW REQUEST
|
||||
lineage = le_client.obtain_and_enroll_certificate(domains)
|
||||
if not lineage:
|
||||
|
|
@ -341,6 +409,27 @@ def _auth_from_domains(le_client, config, domains):
|
|||
|
||||
return lineage
|
||||
|
||||
def _avoid_invalidating_lineage(config, lineage, original_server):
|
||||
"Do not renew a valid cert with one from a staging server!"
|
||||
def _is_staging(srv):
|
||||
return srv == constants.STAGING_URI or "staging" in srv
|
||||
|
||||
# Some lineages may have begun with --staging, but then had production certs
|
||||
# added to them
|
||||
latest_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
|
||||
open(lineage.cert).read())
|
||||
# all our test certs are from happy hacker fake CA, though maybe one day
|
||||
# we should test more methodically
|
||||
now_valid = not "fake" in repr(latest_cert.get_issuer()).lower()
|
||||
|
||||
if _is_staging(config.server):
|
||||
if not _is_staging(original_server) or now_valid:
|
||||
if not config.break_my_certs:
|
||||
names = ", ".join(lineage.names())
|
||||
raise errors.Error(
|
||||
"You've asked to renew/replace a seemingly valid certificate with "
|
||||
"a test certificate (domains: {0}). We will not do that "
|
||||
"unless you use the --break-my-certs flag!".format(names))
|
||||
|
||||
def set_configurator(previously, now):
|
||||
"""
|
||||
|
|
@ -485,7 +574,6 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
|
|||
|
||||
def obtain_cert(args, config, plugins):
|
||||
"""Authenticate & obtain cert, but do not install it."""
|
||||
|
||||
if args.domains and args.csr is not None:
|
||||
# TODO: --csr could have a priority, when --domains is
|
||||
# supplied, check if CSR matches given domains?
|
||||
|
|
@ -574,7 +662,7 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print
|
|||
logger.debug("Filtered plugins: %r", filtered)
|
||||
|
||||
if not args.init and not args.prepare:
|
||||
print str(filtered)
|
||||
print(str(filtered))
|
||||
return
|
||||
|
||||
filtered.init(config)
|
||||
|
|
@ -582,13 +670,13 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print
|
|||
logger.debug("Verified plugins: %r", verified)
|
||||
|
||||
if not args.prepare:
|
||||
print str(verified)
|
||||
print(str(verified))
|
||||
return
|
||||
|
||||
verified.prepare()
|
||||
available = verified.available()
|
||||
logger.debug("Prepared plugins: %s", available)
|
||||
print str(available)
|
||||
print(str(available))
|
||||
|
||||
|
||||
def read_file(filename, mode="rb"):
|
||||
|
|
@ -681,7 +769,7 @@ class HelpfulArgumentParser(object):
|
|||
self.help_arg = max(help1, help2)
|
||||
if self.help_arg is True:
|
||||
# just --help with no topic; avoid argparse altogether
|
||||
print usage
|
||||
print(usage)
|
||||
sys.exit(0)
|
||||
self.visible_topics = self.determine_help_topics(self.help_arg)
|
||||
self.groups = {} # elements are added by .add_group()
|
||||
|
|
@ -695,6 +783,15 @@ class HelpfulArgumentParser(object):
|
|||
"""
|
||||
parsed_args = self.parser.parse_args(self.args)
|
||||
parsed_args.func = self.VERBS[self.verb]
|
||||
parsed_args.verb = self.verb
|
||||
|
||||
# Do any post-parsing homework here
|
||||
|
||||
# argparse seemingly isn't flexible enough to give us this behaviour easily...
|
||||
if parsed_args.staging:
|
||||
if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
|
||||
raise errors.Error("--server value conflicts with --staging")
|
||||
parsed_args.server = constants.STAGING_URI
|
||||
|
||||
return parsed_args
|
||||
|
||||
|
|
@ -785,12 +882,12 @@ class HelpfulArgumentParser(object):
|
|||
|
||||
"""
|
||||
if self.visible_topics[topic]:
|
||||
#print "Adding visible group " + topic
|
||||
#print("Adding visible group " + topic)
|
||||
group = self.parser.add_argument_group(topic, **kwargs)
|
||||
self.groups[topic] = group
|
||||
return group
|
||||
else:
|
||||
#print "Invisible group " + topic
|
||||
#print("Invisible group " + topic)
|
||||
return self.silent_parser
|
||||
|
||||
def add_plugin_args(self, plugins):
|
||||
|
|
@ -802,7 +899,7 @@ class HelpfulArgumentParser(object):
|
|||
"""
|
||||
for name, plugin_ep in plugins.iteritems():
|
||||
parser_or_group = self.add_group(name, description=plugin_ep.description)
|
||||
#print parser_or_group
|
||||
#print(parser_or_group)
|
||||
plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)
|
||||
|
||||
def determine_help_topics(self, chosen_topic):
|
||||
|
|
@ -855,7 +952,7 @@ def prepare_and_parse_args(plugins, args):
|
|||
"email address. This is strongly discouraged, because in the "
|
||||
"event of key loss or account compromise you will irrevocably "
|
||||
"lose access to your account. You will also be unable to receive "
|
||||
"notice about impending expiration of revocation of your "
|
||||
"notice about impending expiration or revocation of your "
|
||||
"certificates. Updates to the Subscriber Agreement will still "
|
||||
"affect you, and will be effective 14 days after posting an "
|
||||
"update to the web site.")
|
||||
|
|
@ -869,13 +966,19 @@ def prepare_and_parse_args(plugins, args):
|
|||
help="Domain names to apply. For multiple domains you can use "
|
||||
"multiple -d flags or enter a comma separated list of domains "
|
||||
"as a parameter.")
|
||||
helpful.add(
|
||||
None, "--duplicate", dest="duplicate", action="store_true",
|
||||
help="Allow getting a certificate that duplicates an existing one")
|
||||
|
||||
helpful.add_group(
|
||||
"automation",
|
||||
description="Arguments for automating execution & other tweaks")
|
||||
helpful.add(
|
||||
"automation", "--keep-until-expiring", "--keep", "--reinstall",
|
||||
dest="reinstall", action="store_true",
|
||||
help="If the requested cert matches an existing cert, always keep the "
|
||||
"existing one until it is due for renewal (for the "
|
||||
"'run' subcommand this means reinstall the existing cert)")
|
||||
helpful.add(
|
||||
"automation", "--expand", action="store_true",
|
||||
help="If an existing cert covers some subset of the requested names, "
|
||||
"always expand and replace it with the additional names.")
|
||||
helpful.add(
|
||||
"automation", "--version", action="version",
|
||||
version="%(prog)s {0}".format(letsencrypt.__version__),
|
||||
|
|
@ -883,13 +986,18 @@ def prepare_and_parse_args(plugins, args):
|
|||
helpful.add(
|
||||
"automation", "--renew-by-default", action="store_true",
|
||||
help="Select renewal by default when domains are a superset of a "
|
||||
"previously attained cert")
|
||||
"previously attained cert (often --keep-until-expiring is "
|
||||
"more appropriate). Implies --expand.")
|
||||
helpful.add(
|
||||
"automation", "--agree-tos", dest="tos", action="store_true",
|
||||
help="Agree to the Let's Encrypt Subscriber Agreement")
|
||||
helpful.add(
|
||||
"automation", "--account", metavar="ACCOUNT_ID",
|
||||
help="Account ID to use")
|
||||
helpful.add(
|
||||
"automation", "--duplicate", dest="duplicate", action="store_true",
|
||||
help="Allow making a certificate lineage that duplicates an existing one "
|
||||
"(both can be renewed in parallel)")
|
||||
|
||||
helpful.add_group(
|
||||
"testing", description="The following flags are meant for "
|
||||
|
|
@ -910,7 +1018,10 @@ def prepare_and_parse_args(plugins, args):
|
|||
helpful.add(
|
||||
"testing", "--http-01-port", type=int, dest="http01_port",
|
||||
default=flag_default("http01_port"), help=config_help("http01_port"))
|
||||
|
||||
helpful.add(
|
||||
"testing", "--break-my-certs", action="store_true",
|
||||
help="Be willing to replace or renew valid certs with invalid "
|
||||
"(testing/staging) certs")
|
||||
helpful.add_group(
|
||||
"security", description="Security parameters & server settings")
|
||||
helpful.add(
|
||||
|
|
@ -1037,6 +1148,10 @@ def _paths_parser(helpful):
|
|||
help="Logs directory.")
|
||||
add("paths", "--server", default=flag_default("server"),
|
||||
help=config_help("server"))
|
||||
# overwrites server, handled in HelpfulArgumentParser.parse_args()
|
||||
add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
|
||||
help='Use the staging server to obtain test (invalid) certs; equivalent'
|
||||
' to --server ' + constants.STAGING_URI)
|
||||
|
||||
|
||||
def _plugins_parsing(helpful, plugins):
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"""Let's Encrypt user-supplied configuration."""
|
||||
import os
|
||||
import urlparse
|
||||
import re
|
||||
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
|
||||
|
||||
class NamespaceConfig(object):
|
||||
|
|
@ -123,31 +123,5 @@ def check_config_sanity(config):
|
|||
|
||||
# Domain checks
|
||||
if config.namespace.domains is not None:
|
||||
_check_config_domain_sanity(config.namespace.domains)
|
||||
|
||||
|
||||
def _check_config_domain_sanity(domains):
|
||||
"""Helper method for check_config_sanity which validates
|
||||
domain flag values and errors out if the requirements are not met.
|
||||
|
||||
:param domains: List of domains
|
||||
:type domains: `list` of `string`
|
||||
:raises ConfigurationError: for invalid domains and cases where Let's
|
||||
Encrypt currently will not issue certificates
|
||||
|
||||
"""
|
||||
# Check if there's a wildcard domain
|
||||
if any(d.startswith("*.") for d in domains):
|
||||
raise errors.ConfigurationError(
|
||||
"Wildcard domains are not supported")
|
||||
# Punycode
|
||||
if any("xn--" in d for d in domains):
|
||||
raise errors.ConfigurationError(
|
||||
"Punycode domains are not supported")
|
||||
# FQDN checks from
|
||||
# http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
|
||||
# Characters used, domain parts < 63 chars, tld > 1 < 64 chars
|
||||
# first and last char is not "-"
|
||||
fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,63}$")
|
||||
if any(True for d in domains if not fqdn.match(d)):
|
||||
raise errors.ConfigurationError("Requested domain is not a FQDN")
|
||||
for domain in config.namespace.domains:
|
||||
le_util.check_domain_sanity(domain)
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ CLI_DEFAULTS = dict(
|
|||
auth_chain_path="./chain.pem",
|
||||
strict_permissions=False,
|
||||
)
|
||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||
STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
|
||||
|
||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||
|
||||
RENEWER_DEFAULTS = dict(
|
||||
renewer_enabled="yes",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
|
||||
import zope.component
|
||||
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt.display import util as display_util
|
||||
|
|
@ -122,6 +123,7 @@ def pick_configurator(
|
|||
config, default, plugins, question,
|
||||
(interfaces.IAuthenticator, interfaces.IInstaller))
|
||||
|
||||
|
||||
def get_email(more=False, invalid=False):
|
||||
"""Prompt for valid email address.
|
||||
|
||||
|
|
@ -186,7 +188,8 @@ def choose_names(installer):
|
|||
logger.debug("No installer, picking names manually")
|
||||
return _choose_names_manually()
|
||||
|
||||
names = list(installer.get_all_names())
|
||||
domains = list(installer.get_all_names())
|
||||
names = get_valid_domains(domains)
|
||||
|
||||
if not names:
|
||||
manual = util(interfaces.IDisplay).yesno(
|
||||
|
|
@ -208,6 +211,24 @@ def choose_names(installer):
|
|||
return []
|
||||
|
||||
|
||||
def get_valid_domains(domains):
|
||||
"""Helper method for choose_names that implements basic checks
|
||||
on domain names
|
||||
|
||||
:param list domains: Domain names to validate
|
||||
:return: List of valid domains
|
||||
:rtype: list
|
||||
"""
|
||||
valid_domains = []
|
||||
for domain in domains:
|
||||
try:
|
||||
le_util.check_domain_sanity(domain)
|
||||
valid_domains.append(domain)
|
||||
except errors.ConfigurationError:
|
||||
continue
|
||||
return valid_domains
|
||||
|
||||
|
||||
def _filter_names(names):
|
||||
"""Determine which names the user would like to select from a list.
|
||||
|
||||
|
|
@ -232,7 +253,41 @@ def _choose_names_manually():
|
|||
"Please enter in your domain name(s) (comma and/or space separated) ")
|
||||
|
||||
if code == display_util.OK:
|
||||
return display_util.separate_list_input(input_)
|
||||
invalid_domains = dict()
|
||||
retry_message = ""
|
||||
try:
|
||||
domain_list = display_util.separate_list_input(input_)
|
||||
except UnicodeEncodeError:
|
||||
domain_list = []
|
||||
retry_message = (
|
||||
"Internationalized domain names are not presently "
|
||||
"supported.{0}{0}Would you like to re-enter the "
|
||||
"names?{0}").format(os.linesep)
|
||||
|
||||
for domain in domain_list:
|
||||
try:
|
||||
le_util.check_domain_sanity(domain)
|
||||
except errors.ConfigurationError as e:
|
||||
invalid_domains[domain] = e.message
|
||||
|
||||
if len(invalid_domains):
|
||||
retry_message = (
|
||||
"One or more of the entered domain names was not valid:"
|
||||
"{0}{0}").format(os.linesep)
|
||||
for domain in invalid_domains:
|
||||
retry_message = retry_message + "{1}: {2}{0}".format(
|
||||
os.linesep, domain, invalid_domains[domain])
|
||||
retry_message = retry_message + (
|
||||
"{0}Would you like to re-enter the names?{0}").format(
|
||||
os.linesep)
|
||||
|
||||
if retry_message:
|
||||
# We had error in input
|
||||
retry = util(interfaces.IDisplay).yesno(retry_message)
|
||||
if retry:
|
||||
return _choose_names_manually()
|
||||
else:
|
||||
return domain_list
|
||||
return []
|
||||
|
||||
|
||||
|
|
@ -245,7 +300,7 @@ def success_installation(domains):
|
|||
|
||||
"""
|
||||
util(interfaces.IDisplay).notification(
|
||||
"Congratulations! You have successfully enabled {0}!{1}{1}"
|
||||
"Congratulations! You have successfully enabled {0}{1}{1}"
|
||||
"You should test your configuration at:{1}{2}".format(
|
||||
_gen_https_names(domains),
|
||||
os.linesep,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import stat
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
import configargparse
|
||||
|
||||
from letsencrypt import errors
|
||||
|
||||
|
||||
|
|
@ -278,5 +280,41 @@ def add_deprecated_argument(add_argument, argument_name, nargs):
|
|||
sys.stderr.write(
|
||||
"Use of {0} is deprecated.\n".format(option_string))
|
||||
|
||||
configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add(ShowWarning)
|
||||
add_argument(argument_name, action=ShowWarning,
|
||||
help=argparse.SUPPRESS, nargs=nargs)
|
||||
|
||||
|
||||
def check_domain_sanity(domain):
|
||||
"""Method which validates domain value and errors out if
|
||||
the requirements are not met.
|
||||
|
||||
:param domain: Domain to check
|
||||
:type domains: `string`
|
||||
:raises ConfigurationError: for invalid domains and cases where Let's
|
||||
Encrypt currently will not issue certificates
|
||||
|
||||
"""
|
||||
# Check if there's a wildcard domain
|
||||
if domain.startswith("*."):
|
||||
raise errors.ConfigurationError(
|
||||
"Wildcard domains are not supported")
|
||||
# Punycode
|
||||
if "xn--" in domain:
|
||||
raise errors.ConfigurationError(
|
||||
"Punycode domains are not presently supported")
|
||||
|
||||
# Unicode
|
||||
try:
|
||||
domain.encode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
raise errors.ConfigurationError(
|
||||
"Internationalized domain names are not presently supported")
|
||||
|
||||
# FQDN checks from
|
||||
# http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
|
||||
# Characters used, domain parts < 63 chars, tld > 1 < 64 chars
|
||||
# first and last char is not "-"
|
||||
fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,63}$")
|
||||
if not fqdn.match(domain):
|
||||
raise errors.ConfigurationError("Requested domain is not a FQDN")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import argparse
|
||||
import collections
|
||||
import logging
|
||||
import random
|
||||
import socket
|
||||
import threading
|
||||
|
||||
|
|
@ -108,7 +107,7 @@ class ServerManager(object):
|
|||
in six.iteritems(self._instances))
|
||||
|
||||
|
||||
SUPPORTED_CHALLENGES = set([challenges.TLSSNI01, challenges.HTTP01])
|
||||
SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01]
|
||||
|
||||
|
||||
def supported_challenges_validator(data):
|
||||
|
|
@ -166,16 +165,16 @@ class Authenticator(common.Plugin):
|
|||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("supported-challenges", help="Supported challenges, "
|
||||
"order preferences are randomly chosen.",
|
||||
type=supported_challenges_validator, default=",".join(
|
||||
sorted(chall.typ for chall in SUPPORTED_CHALLENGES)))
|
||||
add("supported-challenges",
|
||||
help="Supported challenges. Preferred in the order they are listed.",
|
||||
type=supported_challenges_validator,
|
||||
default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES))
|
||||
|
||||
@property
|
||||
def supported_challenges(self):
|
||||
"""Challenges supported by this plugin."""
|
||||
return set(challenges.Challenge.TYPES[name] for name in
|
||||
self.conf("supported-challenges").split(","))
|
||||
return [challenges.Challenge.TYPES[name] for name in
|
||||
self.conf("supported-challenges").split(",")]
|
||||
|
||||
@property
|
||||
def _necessary_ports(self):
|
||||
|
|
@ -198,9 +197,7 @@ class Authenticator(common.Plugin):
|
|||
|
||||
def get_chall_pref(self, domain):
|
||||
# pylint: disable=unused-argument,missing-docstring
|
||||
chall_pref = list(self.supported_challenges)
|
||||
random.shuffle(chall_pref) # 50% for each challenge
|
||||
return chall_pref
|
||||
return self.supported_challenges
|
||||
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
if any(util.already_listening(port) for port in self._necessary_ports):
|
||||
|
|
|
|||
|
|
@ -98,17 +98,27 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
|
||||
def test_supported_challenges(self):
|
||||
self.assertEqual(self.auth.supported_challenges,
|
||||
set([challenges.TLSSNI01, challenges.HTTP01]))
|
||||
[challenges.TLSSNI01, challenges.HTTP01])
|
||||
|
||||
def test_supported_challenges_configured(self):
|
||||
self.config.standalone_supported_challenges = "tls-sni-01"
|
||||
self.assertEqual(self.auth.supported_challenges,
|
||||
[challenges.TLSSNI01])
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertEqual(set(self.auth.get_chall_pref(domain=None)),
|
||||
set([challenges.TLSSNI01, challenges.HTTP01]))
|
||||
self.assertEqual(self.auth.get_chall_pref(domain=None),
|
||||
[challenges.TLSSNI01, challenges.HTTP01])
|
||||
|
||||
def test_get_chall_pref_configured(self):
|
||||
self.config.standalone_supported_challenges = "tls-sni-01"
|
||||
self.assertEqual(self.auth.get_chall_pref(domain=None),
|
||||
[challenges.TLSSNI01])
|
||||
|
||||
@mock.patch("letsencrypt.plugins.standalone.util")
|
||||
def test_perform_alredy_listening(self, mock_util):
|
||||
def test_perform_already_listening(self, mock_util):
|
||||
for chall, port in ((challenges.TLSSNI01.typ, 1234),
|
||||
(challenges.HTTP01.typ, 4321)):
|
||||
mock_util.already_listening.return_value = True
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import errno
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
|
||||
import zope.interface
|
||||
|
||||
|
|
@ -59,24 +58,38 @@ to serve all files under specified web root ({0})."""
|
|||
|
||||
logger.debug("Creating root challenges validation dir at %s",
|
||||
self.full_roots[name])
|
||||
|
||||
# Change the permissions to be writable (GH #1389)
|
||||
# Umask is used instead of chmod to ensure the client can also
|
||||
# run as non-root (GH #1795)
|
||||
old_umask = os.umask(0o022)
|
||||
|
||||
try:
|
||||
os.makedirs(self.full_roots[name])
|
||||
# Set permissions as parent directory (GH #1389)
|
||||
# We don't use the parameters in makedirs because it
|
||||
# may not always work
|
||||
# This is coupled with the "umask" call above because
|
||||
# os.makedirs's "mode" parameter may not always work:
|
||||
# https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python
|
||||
stat_path = os.stat(path)
|
||||
filemode = stat.S_IMODE(stat_path.st_mode)
|
||||
os.chmod(self.full_roots[name], filemode)
|
||||
# Set owner and group, too
|
||||
os.chown(self.full_roots[name], stat_path.st_uid,
|
||||
stat_path.st_gid)
|
||||
os.makedirs(self.full_roots[name], 0o0755)
|
||||
|
||||
# Set owner as parent directory if possible
|
||||
try:
|
||||
stat_path = os.stat(path)
|
||||
os.chown(self.full_roots[name], stat_path.st_uid,
|
||||
stat_path.st_gid)
|
||||
except OSError as exception:
|
||||
if exception.errno == errno.EACCES:
|
||||
logger.debug("Insufficient permissions to change owner and uid - ignoring")
|
||||
else:
|
||||
raise errors.PluginError(
|
||||
"Couldn't create root for {0} http-01 "
|
||||
"challenge responses: {1}", name, exception)
|
||||
|
||||
except OSError as exception:
|
||||
if exception.errno != errno.EEXIST:
|
||||
raise errors.PluginError(
|
||||
"Couldn't create root for {0} http-01 "
|
||||
"challenge responses: {1}", name, exception)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
assert self.full_roots, "Webroot plugin appears to be missing webroot map"
|
||||
|
|
@ -87,26 +100,26 @@ to serve all files under specified web root ({0})."""
|
|||
path = self.full_roots[achall.domain]
|
||||
except IndexError:
|
||||
raise errors.PluginError("Missing --webroot-path for domain: {1}"
|
||||
.format(achall.domain))
|
||||
.format(achall.domain))
|
||||
if not os.path.exists(path):
|
||||
raise errors.PluginError("Mysteriously missing path {0} for domain: {1}"
|
||||
.format(path, achall.domain))
|
||||
.format(path, achall.domain))
|
||||
return os.path.join(path, achall.chall.encode("token"))
|
||||
|
||||
def _perform_single(self, achall):
|
||||
response, validation = achall.response_and_validation()
|
||||
|
||||
path = self._path_for_achall(achall)
|
||||
logger.debug("Attempting to save validation to %s", path)
|
||||
with open(path, "w") as validation_file:
|
||||
validation_file.write(validation.encode())
|
||||
|
||||
# Set permissions as parent directory (GH #1389)
|
||||
parent_path = self.full_roots[achall.domain]
|
||||
stat_parent_path = os.stat(parent_path)
|
||||
filemode = stat.S_IMODE(stat_parent_path.st_mode)
|
||||
# Remove execution bit (not needed for this file)
|
||||
os.chmod(path, filemode & ~stat.S_IEXEC)
|
||||
os.chown(path, stat_parent_path.st_uid, stat_parent_path.st_gid)
|
||||
# Change permissions to be world-readable, owner-writable (GH #1795)
|
||||
old_umask = os.umask(0o022)
|
||||
|
||||
try:
|
||||
with open(path, "w") as validation_file:
|
||||
validation_file.write(validation.encode())
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"""Tests for letsencrypt.plugins.webroot."""
|
||||
import errno
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
import unittest
|
||||
import stat
|
||||
|
||||
import mock
|
||||
|
||||
|
|
@ -35,7 +36,6 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.config = mock.MagicMock(webroot_path=self.path,
|
||||
webroot_map={"thing.com": self.path})
|
||||
self.auth = Authenticator(self.config, "webroot")
|
||||
self.auth.prepare()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.path)
|
||||
|
|
@ -48,7 +48,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
def test_add_parser_arguments(self):
|
||||
add = mock.MagicMock()
|
||||
self.auth.add_parser_arguments(add)
|
||||
self.assertEqual(0, add.call_count) # became 0 when we moved the args to cli.py!
|
||||
self.assertEqual(0, add.call_count) # args moved to cli.py!
|
||||
|
||||
def test_prepare_bad_root(self):
|
||||
self.config.webroot_path = os.path.join(self.path, "null")
|
||||
|
|
@ -70,17 +70,33 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||
os.chmod(self.path, 0o700)
|
||||
|
||||
@mock.patch("letsencrypt.plugins.webroot.os.chown")
|
||||
def test_failed_chown_eacces(self, mock_chown):
|
||||
mock_chown.side_effect = OSError(errno.EACCES, "msg")
|
||||
self.auth.prepare() # exception caught and logged
|
||||
|
||||
@mock.patch("letsencrypt.plugins.webroot.os.chown")
|
||||
def test_failed_chown_not_eacces(self, mock_chown):
|
||||
mock_chown.side_effect = OSError()
|
||||
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||
|
||||
def test_prepare_permissions(self):
|
||||
self.auth.prepare()
|
||||
|
||||
# Remove exec bit from permission check, so that it
|
||||
# matches the file
|
||||
self.auth.perform([self.achall])
|
||||
parent_permissions = (stat.S_IMODE(os.stat(self.path).st_mode) &
|
||||
~stat.S_IEXEC)
|
||||
path_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
|
||||
self.assertEqual(path_permissions, 0o644)
|
||||
|
||||
actual_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
|
||||
# Check permissions of the directories
|
||||
|
||||
for dirpath, dirnames, _ in os.walk(self.path):
|
||||
for directory in dirnames:
|
||||
full_path = os.path.join(dirpath, directory)
|
||||
dir_permissions = stat.S_IMODE(os.stat(full_path).st_mode)
|
||||
self.assertEqual(dir_permissions, 0o755)
|
||||
|
||||
self.assertEqual(parent_permissions, actual_permissions)
|
||||
parent_gid = os.stat(self.path).st_gid
|
||||
parent_uid = os.stat(self.path).st_uid
|
||||
|
||||
|
|
@ -88,6 +104,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid)
|
||||
|
||||
def test_perform_cleanup(self):
|
||||
self.auth.prepare()
|
||||
responses = self.auth.perform([self.achall])
|
||||
self.assertEqual(1, len(responses))
|
||||
self.assertTrue(os.path.exists(self.validation_path))
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ within lineages of successor certificates, according to configuration.
|
|||
.. todo:: Call new installer API to restart servers after deployment
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -169,7 +171,7 @@ def main(cli_args=sys.argv[1:]):
|
|||
constants.CONFIG_DIRS_MODE, uid)
|
||||
|
||||
for renewal_file in os.listdir(cli_config.renewal_configs_dir):
|
||||
print "Processing", renewal_file
|
||||
print("Processing " + renewal_file)
|
||||
try:
|
||||
# TODO: Before trying to initialize the RenewableCert object,
|
||||
# we could check here whether the combination of the config
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
"""Collects and displays information to the user."""
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -75,8 +77,8 @@ class Reporter(object):
|
|||
no_exception = sys.exc_info()[0] is None
|
||||
bold_on = sys.stdout.isatty()
|
||||
if bold_on:
|
||||
print le_util.ANSI_SGR_BOLD
|
||||
print 'IMPORTANT NOTES:'
|
||||
print(le_util.ANSI_SGR_BOLD)
|
||||
print('IMPORTANT NOTES:')
|
||||
first_wrapper = textwrap.TextWrapper(
|
||||
initial_indent=' - ', subsequent_indent=(' ' * 3))
|
||||
next_wrapper = textwrap.TextWrapper(
|
||||
|
|
@ -89,9 +91,9 @@ class Reporter(object):
|
|||
sys.stdout.write(le_util.ANSI_SGR_RESET)
|
||||
bold_on = False
|
||||
lines = msg.text.splitlines()
|
||||
print first_wrapper.fill(lines[0])
|
||||
print(first_wrapper.fill(lines[0]))
|
||||
if len(lines) > 1:
|
||||
print "\n".join(
|
||||
next_wrapper.fill(line) for line in lines[1:])
|
||||
print("\n".join(
|
||||
next_wrapper.fill(line) for line in lines[1:]))
|
||||
if bold_on:
|
||||
sys.stdout.write(le_util.ANSI_SGR_RESET)
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
return ("autodeploy" not in self.configuration or
|
||||
self.configuration.as_bool("autodeploy"))
|
||||
|
||||
def should_autodeploy(self):
|
||||
def should_autodeploy(self, interactive=False):
|
||||
"""Should this lineage now automatically deploy a newer version?
|
||||
|
||||
This is a policy question and does not only depend on whether
|
||||
|
|
@ -480,12 +480,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
exists, and whether the time interval for autodeployment has
|
||||
been reached.)
|
||||
|
||||
:param bool interactive: set to True to examine the question
|
||||
regardless of whether the renewal configuration allows
|
||||
automated deployment (for interactive use). Default False.
|
||||
|
||||
:returns: whether the lineage now ought to autodeploy an
|
||||
existing newer cert version
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if self.autodeployment_is_enabled():
|
||||
if interactive or self.autodeployment_is_enabled():
|
||||
if self.has_pending_deployment():
|
||||
interval = self.configuration.get("deploy_before_expiry",
|
||||
"5 days")
|
||||
|
|
@ -529,7 +533,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
return ("autorenew" not in self.configuration or
|
||||
self.configuration.as_bool("autorenew"))
|
||||
|
||||
def should_autorenew(self):
|
||||
def should_autorenew(self, interactive=False):
|
||||
"""Should we now try to autorenew the most recent cert version?
|
||||
|
||||
This is a policy question and does not only depend on whether
|
||||
|
|
@ -540,12 +544,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
Note that this examines the numerically most recent cert version,
|
||||
not the currently deployed version.
|
||||
|
||||
:param bool interactive: set to True to examine the question
|
||||
regardless of whether the renewal configuration allows
|
||||
automated renewal (for interactive use). Default False.
|
||||
|
||||
:returns: whether an attempt should now be made to autorenew the
|
||||
most current cert version in this lineage
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if self.autorenewal_is_enabled():
|
||||
if interactive or self.autorenewal_is_enabled():
|
||||
# Consider whether to attempt to autorenew this cert now
|
||||
|
||||
# Renewals on the basis of revocation
|
||||
|
|
@ -559,8 +567,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
"cert", self.latest_common_version()))
|
||||
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
if expiry < add_time_interval(now, interval):
|
||||
logger.debug("Should renew, certificate "
|
||||
"has been expired since %s.",
|
||||
logger.debug("Should renew, less than %s before certificate "
|
||||
"expiry %s.", interval,
|
||||
expiry.strftime("%Y-%m-%d %H:%M:%S %Z"))
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from acme import jose
|
|||
from letsencrypt import account
|
||||
from letsencrypt import cli
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
|
|
@ -343,6 +344,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
namespace = cli.prepare_and_parse_args(plugins, long_args)
|
||||
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
|
||||
|
||||
def test_parse_server(self):
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
short_args = ['--server', 'example.com']
|
||||
namespace = cli.prepare_and_parse_args(plugins, short_args)
|
||||
self.assertEqual(namespace.server, 'example.com')
|
||||
|
||||
short_args = ['--staging']
|
||||
namespace = cli.prepare_and_parse_args(plugins, short_args)
|
||||
self.assertEqual(namespace.server, constants.STAGING_URI)
|
||||
|
||||
short_args = ['--staging', '--server', 'example.com']
|
||||
self.assertRaises(errors.Error, cli.prepare_and_parse_args, plugins, short_args)
|
||||
|
||||
def test_parse_webroot(self):
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
webroot_args = ['--webroot', '-w', '/var/www/example',
|
||||
|
|
@ -389,7 +403,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
|
||||
def _certonly_new_request_common(self, mock_client):
|
||||
with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal:
|
||||
mock_renewal.return_value = None
|
||||
mock_renewal.return_value = ("newcert", None)
|
||||
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
|
||||
mock_init.return_value = mock_client
|
||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
|
||||
|
|
@ -399,13 +413,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
@mock.patch('letsencrypt.cli._treat_as_renewal')
|
||||
@mock.patch('letsencrypt.cli._init_le_client')
|
||||
def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility, _suggest):
|
||||
cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem'
|
||||
cert_path = 'letsencrypt/tests/testdata/cert.pem'
|
||||
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
|
||||
|
||||
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path)
|
||||
mock_cert = mock.MagicMock(body='body')
|
||||
mock_key = mock.MagicMock(pem='pem_key')
|
||||
mock_renewal.return_value = mock_lineage
|
||||
mock_renewal.return_value = ("renew", mock_lineage)
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_certificate.return_value = (mock_cert, 'chain',
|
||||
mock_key, 'csr')
|
||||
|
|
@ -537,6 +551,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self.assertEqual(path, os.path.abspath(path))
|
||||
self.assertEqual(contents, test_contents)
|
||||
|
||||
def test_agree_dev_preview_config(self):
|
||||
with MockedVerb('run') as mocked_run:
|
||||
self._call(['-c', test_util.vector_path('cli.ini')])
|
||||
self.assertTrue(mocked_run.called)
|
||||
|
||||
|
||||
class DetermineAccountTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.cli._determine_account."""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# coding=utf-8
|
||||
"""Test letsencrypt.display.ops."""
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -385,6 +386,55 @@ class ChooseNamesTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(self._call(self.mock_install), [])
|
||||
|
||||
def test_get_valid_domains(self):
|
||||
from letsencrypt.display.ops import get_valid_domains
|
||||
all_valid = ["example.com", "second.example.com",
|
||||
"also.example.com"]
|
||||
all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "notFQDN",
|
||||
"uniçodé.com"]
|
||||
two_valid = ["example.com", "xn--ls8h.tld", "also.example.com"]
|
||||
self.assertEqual(get_valid_domains(all_valid), all_valid)
|
||||
self.assertEqual(get_valid_domains(all_invalid), [])
|
||||
self.assertEqual(len(get_valid_domains(two_valid)), 2)
|
||||
|
||||
@mock.patch("letsencrypt.display.ops.util")
|
||||
def test_choose_manually(self, mock_util):
|
||||
from letsencrypt.display.ops import _choose_names_manually
|
||||
# No retry
|
||||
mock_util().yesno.return_value = False
|
||||
# IDN and no retry
|
||||
mock_util().input.return_value = (display_util.OK,
|
||||
"uniçodé.com")
|
||||
self.assertEqual(_choose_names_manually(), [])
|
||||
# IDN exception with previous mocks
|
||||
with mock.patch("letsencrypt.display.util") as mock_sl:
|
||||
uerror = UnicodeEncodeError('mock', u'',
|
||||
0, 1, 'mock')
|
||||
mock_sl.separate_list_input.side_effect = uerror
|
||||
self.assertEqual(_choose_names_manually(), [])
|
||||
# Punycode and no retry
|
||||
mock_util().input.return_value = (display_util.OK,
|
||||
"xn--ls8h.tld")
|
||||
self.assertEqual(_choose_names_manually(), [])
|
||||
# non-FQDN and no retry
|
||||
mock_util().input.return_value = (display_util.OK,
|
||||
"notFQDN")
|
||||
self.assertEqual(_choose_names_manually(), [])
|
||||
# Two valid domains
|
||||
mock_util().input.return_value = (display_util.OK,
|
||||
("example.com,"
|
||||
"valid.example.com"))
|
||||
self.assertEqual(_choose_names_manually(),
|
||||
["example.com", "valid.example.com"])
|
||||
# Three iterations
|
||||
mock_util().input.return_value = (display_util.OK,
|
||||
"notFQDN")
|
||||
yn = mock.MagicMock()
|
||||
yn.side_effect = [True, True, False]
|
||||
mock_util().yesno = yn
|
||||
_choose_names_manually()
|
||||
self.assertEqual(mock_util().yesno.call_count, 3)
|
||||
|
||||
|
||||
class SuccessInstallationTest(unittest.TestCase):
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
|
|
|||
1
letsencrypt/tests/testdata/cli.ini
vendored
Normal file
1
letsencrypt/tests/testdata/cli.ini
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
agree-dev-preview = True
|
||||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.1.0.dev0'
|
||||
version = '0.2.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'setuptools', # pkg_resources
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<VirtualHost *:443>
|
||||
ServerAdmin webmaster@localhost
|
||||
ServerAlias www.example.com
|
||||
ServerName example.com
|
||||
DocumentRoot /var/www/example.com/www/
|
||||
SSLEngine on
|
||||
|
||||
SSLProtocol all -SSLv2 -SSLv3
|
||||
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
|
||||
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
|
||||
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
<Directory /var/www/example.com/www>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride All
|
||||
Order allow,deny
|
||||
allow from all
|
||||
# This directive allows us to have apache2's default start page
|
||||
# in /apache2-default/, but still have / go to the right place
|
||||
</Directory>
|
||||
|
||||
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
|
||||
<Directory "/usr/lib/cgi-bin">
|
||||
AllowOverride None
|
||||
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
ErrorLog /var/log/apache2/error.log
|
||||
|
||||
# Possible values include: debug, info, notice, warn, error, crit,
|
||||
# alert, emerg.
|
||||
LogLevel warn
|
||||
|
||||
CustomLog /var/log/apache2/access.log combined
|
||||
ServerSignature On
|
||||
|
||||
Alias /apache_doc/ "/usr/share/doc/"
|
||||
<Directory "/usr/share/doc/">
|
||||
Options Indexes MultiViews FollowSymLinks
|
||||
AllowOverride None
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
Allow from 127.0.0.0/255.0.0.0 ::1/128
|
||||
</Directory>
|
||||
|
||||
</VirtualHost>
|
||||
28
tests/apache-conf-files/hackish-apache-test
Executable file
28
tests/apache-conf-files/hackish-apache-test
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
|
||||
# A hackish script to see if the client is behaving as expected
|
||||
# with each of the "passing" conf files.
|
||||
|
||||
# TODO presently this requires interaction and human judgement to
|
||||
# assess, but it should be automated
|
||||
export EA=/etc/apache2/
|
||||
TESTDIR="`dirname $0`"
|
||||
LEROOT="`realpath \"$TESTDIR/../../\"`"
|
||||
cd $TESTDIR/passing
|
||||
|
||||
function CleanupExit() {
|
||||
echo control c, exiting tests...
|
||||
if [ "$f" != "" ] ; then
|
||||
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap CleanupExit INT
|
||||
for f in *.conf ; do
|
||||
echo testing "$f"
|
||||
sudo cp "$f" "$EA"/sites-available/
|
||||
sudo ln -s "$EA/sites-available/$f" "$EA/sites-enabled/$f"
|
||||
sudo "$LEROOT"/venv/bin/letsencrypt --apache certonly -t
|
||||
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
|
||||
done
|
||||
37
tests/apache-conf-files/passing/1626-1531.conf
Normal file
37
tests/apache-conf-files/passing/1626-1531.conf
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<VirtualHost *:80>
|
||||
ServerAdmin denver@ossguy.com
|
||||
ServerName c-beta.ossguy.com
|
||||
|
||||
Alias /robots.txt /home/denver/www/c-beta.ossguy.com/static/robots.txt
|
||||
Alias /favicon.ico /home/denver/www/c-beta.ossguy.com/static/favicon.ico
|
||||
|
||||
AliasMatch /(.*\.css) /home/denver/www/c-beta.ossguy.com/static/$1
|
||||
AliasMatch /(.*\.js) /home/denver/www/c-beta.ossguy.com/static/$1
|
||||
AliasMatch /(.*\.png) /home/denver/www/c-beta.ossguy.com/static/$1
|
||||
AliasMatch /(.*\.gif) /home/denver/www/c-beta.ossguy.com/static/$1
|
||||
AliasMatch /(.*\.jpg) /home/denver/www/c-beta.ossguy.com/static/$1
|
||||
|
||||
WSGIScriptAlias / /home/denver/www/c-beta.ossguy.com/django.wsgi
|
||||
WSGIDaemonProcess c-beta-ossguy user=www-data group=www-data home=/var/www processes=5 threads=10 maximum-requests=1000 umask=0007 display-name=c-beta-ossguy
|
||||
WSGIProcessGroup c-beta-ossguy
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
|
||||
DocumentRoot /home/denver/www/c-beta.ossguy.com/static
|
||||
|
||||
<Directory /home/denver/www/c-beta.ossguy.com/static>
|
||||
Options -Indexes +FollowSymLinks -MultiViews
|
||||
Require all granted
|
||||
AllowOverride None
|
||||
</Directory>
|
||||
|
||||
<Directory /home/denver/www/c-beta.ossguy.com/static/source>
|
||||
Options +Indexes +FollowSymLinks -MultiViews
|
||||
Require all granted
|
||||
AllowOverride None
|
||||
</Directory>
|
||||
|
||||
# Custom log file locations
|
||||
LogLevel warn
|
||||
ErrorLog /tmp/error.log
|
||||
CustomLog /tmp/access.log combined
|
||||
</VirtualHost>
|
||||
|
|
@ -3,3 +3,5 @@ Modules required to parse these conf files:
|
|||
ssl
|
||||
rewrite
|
||||
macro
|
||||
wsgi
|
||||
deflate
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
#
|
||||
# Apache/PHP/Drupal settings:
|
||||
#
|
||||
|
||||
# Protect files and directories from prying eyes.
|
||||
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl|svn-base)$|^(code-style\.pl|Entries.*|Repository|Root|Tag|Template|all-wcprops|entries|format)$">
|
||||
Order allow,deny
|
||||
</FilesMatch>
|
||||
|
||||
# Don't show directory listings for URLs which map to a directory.
|
||||
Options -Indexes
|
||||
|
||||
# Follow symbolic links in this directory.
|
||||
Options +FollowSymLinks
|
||||
|
||||
# Make Drupal handle any 404 errors.
|
||||
ErrorDocument 404 /index.php
|
||||
|
||||
# Force simple error message for requests for non-existent favicon.ico.
|
||||
<Files favicon.ico>
|
||||
# There is no end quote below, for compatibility with Apache 1.3.
|
||||
ErrorDocument 404 "The requested file favicon.ico was not found.
|
||||
</Files>
|
||||
|
||||
# Set the default handler.
|
||||
DirectoryIndex index.php
|
||||
|
||||
# Override PHP settings. More in sites/default/settings.php
|
||||
# but the following cannot be changed at runtime.
|
||||
|
||||
# PHP 4, Apache 1.
|
||||
<IfModule mod_php4.c>
|
||||
php_value magic_quotes_gpc 0
|
||||
php_value register_globals 0
|
||||
php_value session.auto_start 0
|
||||
php_value mbstring.http_input pass
|
||||
php_value mbstring.http_output pass
|
||||
php_value mbstring.encoding_translation 0
|
||||
</IfModule>
|
||||
|
||||
# PHP 4, Apache 2.
|
||||
<IfModule sapi_apache2.c>
|
||||
php_value magic_quotes_gpc 0
|
||||
php_value register_globals 0
|
||||
php_value session.auto_start 0
|
||||
php_value mbstring.http_input pass
|
||||
php_value mbstring.http_output pass
|
||||
php_value mbstring.encoding_translation 0
|
||||
</IfModule>
|
||||
|
||||
# PHP 5, Apache 1 and 2.
|
||||
<IfModule mod_php5.c>
|
||||
php_value magic_quotes_gpc 0
|
||||
php_value register_globals 0
|
||||
php_value session.auto_start 0
|
||||
php_value mbstring.http_input pass
|
||||
php_value mbstring.http_output pass
|
||||
php_value mbstring.encoding_translation 0
|
||||
</IfModule>
|
||||
|
||||
# Requires mod_expires to be enabled.
|
||||
<IfModule mod_expires.c>
|
||||
# Enable expirations.
|
||||
ExpiresActive On
|
||||
|
||||
# Cache all files for 2 weeks after access (A).
|
||||
ExpiresDefault A1209600
|
||||
|
||||
<FilesMatch \.php$>
|
||||
# Do not allow PHP scripts to be cached unless they explicitly send cache
|
||||
# headers themselves. Otherwise all scripts would have to overwrite the
|
||||
# headers set by mod_expires if they want another caching behavior. This may
|
||||
# fail if an error occurs early in the bootstrap process, and it may cause
|
||||
# problems if a non-Drupal PHP file is installed in a subdirectory.
|
||||
ExpiresActive Off
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
# Various rewrite rules.
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine on
|
||||
|
||||
# If your site can be accessed both with and without the 'www.' prefix, you
|
||||
# can use one of the following settings to redirect users to your preferred
|
||||
# URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:
|
||||
#
|
||||
# To redirect all users to access the site WITH the 'www.' prefix,
|
||||
# (http://example.com/... will be redirected to http://www.example.com/...)
|
||||
# adapt and uncomment the following:
|
||||
# RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
|
||||
# RewriteRule ^(.*)$ http://www.example.com/$1 [L,R=301]
|
||||
#
|
||||
# To redirect all users to access the site WITHOUT the 'www.' prefix,
|
||||
# (http://www.example.com/... will be redirected to http://example.com/...)
|
||||
# uncomment and adapt the following:
|
||||
# RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
|
||||
# RewriteRule ^(.*)$ http://example.com/$1 [L,R=301]
|
||||
|
||||
# Modify the RewriteBase if you are using Drupal in a subdirectory or in a
|
||||
# VirtualDocumentRoot and the rewrite rules are not working properly.
|
||||
# For example if your site is at http://example.com/drupal uncomment and
|
||||
# modify the following line:
|
||||
# RewriteBase /drupal
|
||||
#
|
||||
# If your site is running in a VirtualDocumentRoot at http://example.com/,
|
||||
# uncomment the following line:
|
||||
# RewriteBase /
|
||||
|
||||
# Rewrite URLs of the form 'x' to the form 'index.php?q=x'.
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} !=/favicon.ico
|
||||
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
|
||||
</IfModule>
|
||||
|
||||
# $Id$
|
||||
36
tests/apache-conf-files/passing/example-1755.conf
Normal file
36
tests/apache-conf-files/passing/example-1755.conf
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<VirtualHost *:80>
|
||||
# The ServerName directive sets the request scheme, hostname and port that
|
||||
# the server uses to identify itself. This is used when creating
|
||||
# redirection URLs. In the context of virtual hosts, the ServerName
|
||||
# specifies what hostname must appear in the request's Host: header to
|
||||
# match this virtual host. For the default virtual host (this file) this
|
||||
# value is not decisive as it is used as a last resort host regardless.
|
||||
# However, you must set it for any further virtual host explicitly.
|
||||
ServerName www.example.com
|
||||
ServerAlias example.com
|
||||
SetOutputFilter DEFLATE
|
||||
# Do not attempt to compress the following extensions
|
||||
SetEnvIfNoCase Request_URI \
|
||||
\.(?:gif|jpe?g|png|swf|flv|zip|gz|tar|mp3|mp4|m4v)$ no-gzip dont-vary
|
||||
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/proof
|
||||
|
||||
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
|
||||
# error, crit, alert, emerg.
|
||||
# It is also possible to configure the loglevel for particular
|
||||
# modules, e.g.
|
||||
#LogLevel info ssl:warn
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
# For most configuration files from conf-available/, which are
|
||||
# enabled or disabled at a global level, it is possible to
|
||||
# include a line for only one particular virtual host. For example the
|
||||
# following line enables the CGI configuration for this host only
|
||||
# after it has been globally disabled with "a2disconf".
|
||||
#Include conf-available/serve-cgi-bin.conf
|
||||
</VirtualHost>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
52
tests/apache-conf-files/passing/missing-quote-1724.conf
Normal file
52
tests/apache-conf-files/passing/missing-quote-1724.conf
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<VirtualHost *:443>
|
||||
ServerAdmin webmaster@localhost
|
||||
ServerAlias www.example.com
|
||||
ServerName example.com
|
||||
DocumentRoot /var/www/example.com/www/
|
||||
SSLEngine on
|
||||
|
||||
SSLProtocol all -SSLv2 -SSLv3
|
||||
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
|
||||
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
|
||||
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
<Directory /var/www/example.com/www>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride All
|
||||
Order allow,deny
|
||||
allow from all
|
||||
# This directive allows us to have apache2's default start page
|
||||
# in /apache2-default/, but still have / go to the right place
|
||||
</Directory>
|
||||
|
||||
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
|
||||
<Directory "/usr/lib/cgi-bin">
|
||||
AllowOverride None
|
||||
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
ErrorLog /var/log/apache2/error.log
|
||||
|
||||
# Possible values include: debug, info, notice, warn, error, crit,
|
||||
# alert, emerg.
|
||||
LogLevel warn
|
||||
|
||||
CustomLog /var/log/apache2/access.log combined
|
||||
ServerSignature On
|
||||
|
||||
Alias /apache_doc/ "/usr/share/doc/"
|
||||
<Directory "/usr/share/doc/">
|
||||
Options Indexes MultiViews FollowSymLinks
|
||||
AllowOverride None
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
Allow from 127.0.0.0/255.0.0.0 ::1/128
|
||||
</Directory>
|
||||
|
||||
</VirtualHost>
|
||||
|
|
@ -0,0 +1 @@
|
|||
SSLRequire %{SSL_CLIENT_S_DN_CN} in {"foo@bar.com", "bar@foo.com"}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin info@somethingnewentertainment.com
|
||||
ServerName somethingnewentertainment.com
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
ErrorLog /var/log/apache2/error.log
|
||||
CustomLog /var/log/apache2/access.log combined
|
||||
|
||||
SSLEngine on
|
||||
SSLProtocol all -SSLv2 -SSLv3
|
||||
SSLHonorCipherOrder on
|
||||
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EEC DH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRS A RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4"
|
||||
|
||||
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
|
||||
|
||||
<FilesMatch "\.(cgi|shtml|phtml|php)$">
|
||||
SSLOptions +StdEnvVars
|
||||
</FilesMatch>
|
||||
<Directory /usr/lib/cgi-bin>
|
||||
SSLOptions +StdEnvVars
|
||||
</Directory>
|
||||
BrowserMatch "MSIE [2-6]" \
|
||||
nokeepalive ssl-unclean-shutdown \
|
||||
downgrade-1.0 force-response-1.0
|
||||
BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
|
||||
</VirtualHost> </IfModule>
|
||||
Loading…
Reference in a new issue