Merge branch 'master' into issue_2983

This commit is contained in:
Noah Swartz 2016-06-28 17:57:10 -07:00
commit 2884a7fcb7
42 changed files with 1202 additions and 414 deletions

3
.coveragerc Normal file
View file

@ -0,0 +1,3 @@
[report]
# show lines missing coverage in output
show_missing = True

10
.gitattributes vendored
View file

@ -1,7 +1,15 @@
* text=auto eol=lf
#Default, normalize CRLF into LF in non-binary files
# Files identified as binary by Git are not changed
* crlf=auto
# special files
*.sh crlf=input
*.py crlf=input
*.bat text eol=crlf
*.der binary
*.gz binary
*.jpeg binary
*.jpg binary
*.png binary

View file

@ -49,7 +49,7 @@ class MissingNonce(NonceError):
def __str__(self):
return ('Server {0} response did not include a replay '
'nonce, headers: {1}'.format(
'nonce, headers: {1} (This may be a service outage)'.format(
self.response.request.method, self.response.headers))

View file

@ -52,11 +52,14 @@ let sep_eq = del /[ \t]*=[ \t]*/ "="
let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/
let word = /[a-z][a-z0-9._-]*/i
let comment = Util.comment
let eol = Util.doseol
let empty = Util.empty_dos
let indent = Util.indent
let comment_val_re = /([^ \t\r\n](.|\\\\\r?\n)*[^ \\\t\r\n]|[^ \t\r\n])/
let comment = [ label "#comment" . del /[ \t]*#[ \t]*/ "# "
. store comment_val_re . eol ]
(* borrowed from shellvars.aug *)
let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ \t\r\n])|\\\\"|\\\\'|\\\\ /
let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'|\\\\ /

View file

@ -631,50 +631,93 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
# If nonstandard port, add service definition for matching
if port != "443":
port_service = "%s %s" % (port, "https")
else:
port_service = port
self.prepare_https_modules(temp)
# Check for Listen <port>
# Note: This could be made to also look for ip:443 combo
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"]
if port in listens:
# Listen already in place
if self._has_port_already(listens, port):
return
listen_dirs = set(listens)
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)
if port not in listen_dirs and port_service not in listen_dirs:
listen_dirs.add(port_service)
else:
# 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))
if "%s:%s" % (ip, port_service) not in listen_dirs and (
"%s:%s" % (ip, port_service) not in listen_dirs):
listen_dirs.add("%s:%s" % (ip, port_service))
self._add_listens(listen_dirs, listens, port)
def _add_listens(self, listens, listens_orig, port):
"""Helper method for prepare_server_https to figure out which new
listen statements need adding
:param set listens: Set of all needed Listen statements
:param list listens_orig: List of existing listen statements
:param string port: Port number we're adding
"""
# Add service definition for non-standard ports
if port != "443":
port_service = "%s %s" % (port, "https")
else:
port_service = port
new_listens = listens.difference(listens_orig)
if port in new_listens or port_service in new_listens:
# We have wildcard, skip the rest
self.parser.add_dir_to_ifmodssl(
parser.get_aug_path(self.parser.loc["listen"]),
"Listen", port_service.split(" "))
self.save_notes += "Added Listen %s directive to %s\n" % (
port_service, self.parser.loc["listen"])
else:
for listen in new_listens:
self.parser.add_dir_to_ifmodssl(
parser.get_aug_path(self.parser.loc["listen"]),
"Listen", listen.split(" "))
self.save_notes += ("Added Listen %s directive to "
"%s\n") % (listen,
self.parser.loc["listen"])
def _has_port_already(self, listens, port):
"""Helper method for prepare_server_https to find out if user
already has an active Listen statement for the port we need
:param list listens: List of listen variables
:param string port: Port in question
"""
if port in listens:
return True
# Check if Apache is already listening on a specific IP
for listen in listens:
if len(listen.split(":")) > 1:
# Ugly but takes care of protocol def, eg: 1.1.1.1:443 https
if listen.split(":")[-1].split(" ")[0] == port:
return True
def prepare_https_modules(self, temp):
"""Helper method for prepare_server_https, taking care of enabling

View file

@ -0,0 +1,428 @@
# ---------------------------------------------------------------
# Core ModSecurity Rule Set ver.2.2.6
# Copyright (C) 2006-2012 Trustwave All rights reserved.
#
# The OWASP ModSecurity Core Rule Set is distributed under
# Apache Software License (ASL) version 2
# Please see the enclosed LICENCE file for full details.
# ---------------------------------------------------------------
#
# -- [[ Recommended Base Configuration ]] -------------------------------------------------
#
# The configuration directives/settings in this file are used to control
# the OWASP ModSecurity CRS. These settings do **NOT** configure the main
# ModSecurity settings such as:
#
# - SecRuleEngine
# - SecRequestBodyAccess
# - SecAuditEngine
# - SecDebugLog
#
# You should use the modsecurity.conf-recommended file that comes with the
# ModSecurity source code archive.
#
# Ref: http://mod-security.svn.sourceforge.net/viewvc/mod-security/m2/trunk/modsecurity.conf-recommended
#
#
# -- [[ Rule Version ]] -------------------------------------------------------------------
#
# Rule version data is added to the "Producer" line of Section H of the Audit log:
#
# - Producer: ModSecurity for Apache/2.7.0-rc1 (http://www.modsecurity.org/); OWASP_CRS/2.2.4.
#
# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecComponentSignature
#
#SecComponentSignature "OWASP_CRS/2.2.6"
#
# -- [[ Modes of Operation: Self-Contained vs. Collaborative Detection ]] -----------------
#
# Each detection rule uses the "block" action which will inherit the SecDefaultAction
# specified below. Your settings here will determine which mode of operation you use.
#
# -- [[ Self-Contained Mode ]] --
# Rules inherit the "deny" disruptive action. The first rule that matches will block.
#
# -- [[ Collaborative Detection Mode ]] --
# This is a "delayed blocking" mode of operation where each matching rule will inherit
# the "pass" action and will only contribute to anomaly scores. Transactional blocking
# can be applied
#
# -- [[ Alert Logging Control ]] --
# You have three options -
#
# - To log to both the Apache error_log and ModSecurity audit_log file use: "log"
# - To log *only* to the ModSecurity audit_log file use: "nolog,auditlog"
# - To log *only* to the Apache error_log file use: "log,noauditlog"
#
# Ref: http://blog.spiderlabs.com/2010/11/advanced-topic-of-the-week-traditional-vs-anomaly-scoring-detection-modes.html
# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecDefaultAction
#
#SecDefaultAction "phase:1,deny,log"
#
# -- [[ Collaborative Detection Severity Levels ]] ----------------------------------------
#
# These are the default scoring points for each severity level. You may
# adjust these to you liking. These settings will be used in macro expansion
# in the rules to increment the anomaly scores when rules match.
#
# These are the default Severity ratings (with anomaly scores) of the individual rules -
#
# - 2: Critical - Anomaly Score of 5.
# Is the highest severity level possible without correlation. It is
# normally generated by the web attack rules (40 level files).
# - 3: Error - Anomaly Score of 4.
# Is generated mostly from outbound leakage rules (50 level files).
# - 4: Warning - Anomaly Score of 3.
# Is generated by malicious client rules (35 level files).
# - 5: Notice - Anomaly Score of 2.
# Is generated by the Protocol policy and anomaly files.
#
#SecAction \
"id:'900001', \
phase:1, \
t:none, \
setvar:tx.critical_anomaly_score=5, \
setvar:tx.error_anomaly_score=4, \
setvar:tx.warning_anomaly_score=3, \
setvar:tx.notice_anomaly_score=2, \
nolog, \
pass"
#
# -- [[ Collaborative Detection Scoring Threshold Levels ]] ------------------------------
#
# These variables are used in macro expansion in the 49 inbound blocking and 59
# outbound blocking files.
#
# **MUST HAVE** ModSecurity v2.5.12 or higher to use macro expansion in numeric
# operators. If you have an earlier version, edit the 49/59 files directly to
# set the appropriate anomaly score levels.
#
# You should set the score to the proper threshold you would prefer. If set to "5"
# it will work similarly to previous Mod CRS rules and will create an event in the error_log
# file if there are any rules that match. If you would like to lessen the number of events
# generated in the error_log file, you should increase the anomaly score threshold to
# something like "20". This would only generate an event in the error_log file if
# there are multiple lower severity rule matches or if any 1 higher severity item matches.
#
#SecAction \
"id:'900002', \
phase:1, \
t:none, \
setvar:tx.inbound_anomaly_score_level=5, \
nolog, \
pass"
#SecAction \
"id:'900003', \
phase:1, \
t:none, \
setvar:tx.outbound_anomaly_score_level=4, \
nolog, \
pass"
#
# -- [[ Collaborative Detection Blocking ]] -----------------------------------------------
#
# This is a collaborative detection mode where each rule will increment an overall
# anomaly score for the transaction. The scores are then evaluated in the following files:
#
# Inbound anomaly score - checked in the modsecurity_crs_49_inbound_blocking.conf file
# Outbound anomaly score - checked in the modsecurity_crs_59_outbound_blocking.conf file
#
# If you want to use anomaly scoring mode, then uncomment this line.
#
#SecAction \
"id:'900004', \
phase:1, \
t:none, \
setvar:tx.anomaly_score_blocking=on, \
nolog, \
pass"
#
# -- [[ GeoIP Database ]] -----------------------------------------------------------------
#
# There are some rulesets that need to inspect the GEO data of the REMOTE_ADDR data.
#
# You must first download the MaxMind GeoIP Lite City DB -
#
# http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz
#
# You then need to define the proper path for the SecGeoLookupDb directive
#
# Ref: http://blog.spiderlabs.com/2010/10/detecting-malice-with-modsecurity-geolocation-data.html
# Ref: http://blog.spiderlabs.com/2010/11/detecting-malice-with-modsecurity-ip-forensics.html
#
#SecGeoLookupDb /opt/modsecurity/lib/GeoLiteCity.dat
#
# -- [[ Regression Testing Mode ]] --------------------------------------------------------
#
# If you are going to run the regression testing mode, you should uncomment the
# following rule. It will enable DetectionOnly mode for the SecRuleEngine and
# will enable Response Header tagging so that the client testing script can see
# which rule IDs have matched.
#
# You must specify the your source IP address where you will be running the tests
# from.
#
#SecRule REMOTE_ADDR "@ipMatch 192.168.1.100" \
"id:'900005', \
phase:1, \
t:none, \
ctl:ruleEngine=DetectionOnly, \
setvar:tx.regression_testing=1, \
nolog, \
pass"
#
# -- [[ HTTP Policy Settings ]] ----------------------------------------------------------
#
# Set the following policy settings here and they will be propagated to the 23 rules
# file (modsecurity_common_23_request_limits.conf) by using macro expansion.
# If you run into false positives, you can adjust the settings here.
#
# Only the max number of args is uncommented by default as there are a high rate
# of false positives. Uncomment the items you wish to set.
#
#
# -- Maximum number of arguments in request limited
#SecAction \
"id:'900006', \
phase:1, \
t:none, \
setvar:tx.max_num_args=255, \
nolog, \
pass"
#
# -- Limit argument name length
#SecAction \
"id:'900007', \
phase:1, \
t:none, \
setvar:tx.arg_name_length=100, \
nolog, \
pass"
#
# -- Limit value name length
#SecAction \
"id:'900008', \
phase:1, \
t:none, \
setvar:tx.arg_length=400, \
nolog, \
pass"
#
# -- Limit arguments total length
#SecAction \
"id:'900009', \
phase:1, \
t:none, \
setvar:tx.total_arg_length=64000, \
nolog, \
pass"
#
# -- Individual file size is limited
#SecAction \
"id:'900010', \
phase:1, \
t:none, \
setvar:tx.max_file_size=1048576, \
nolog, \
pass"
#
# -- Combined file size is limited
#SecAction \
"id:'900011', \
phase:1, \
t:none, \
setvar:tx.combined_file_sizes=1048576, \
nolog, \
pass"
#
# Set the following policy settings here and they will be propagated to the 30 rules
# file (modsecurity_crs_30_http_policy.conf) by using macro expansion.
# If you run into false positves, you can adjust the settings here.
#
#SecAction \
"id:'900012', \
phase:1, \
t:none, \
setvar:'tx.allowed_methods=GET HEAD POST OPTIONS', \
setvar:'tx.allowed_request_content_type=application/x-www-form-urlencoded|multipart/form-data|text/xml|application/xml|application/x-amf', \
setvar:'tx.allowed_http_versions=HTTP/0.9 HTTP/1.0 HTTP/1.1', \
setvar:'tx.restricted_extensions=.asa/ .asax/ .ascx/ .axd/ .backup/ .bak/ .bat/ .cdx/ .cer/ .cfg/ .cmd/ .com/ .config/ .conf/ .cs/ .csproj/ .csr/ .dat/ .db/ .dbf/ .dll/ .dos/ .htr/ .htw/ .ida/ .idc/ .idq/ .inc/ .ini/ .key/ .licx/ .lnk/ .log/ .mdb/ .old/ .pass/ .pdb/ .pol/ .printer/ .pwd/ .resources/ .resx/ .sql/ .sys/ .vb/ .vbs/ .vbproj/ .vsdisco/ .webinfo/ .xsd/ .xsx/', \
setvar:'tx.restricted_headers=/Proxy-Connection/ /Lock-Token/ /Content-Range/ /Translate/ /via/ /if/', \
nolog, \
pass"
#
# -- [[ Content Security Policy (CSP) Settings ]] -----------------------------------------
#
# The purpose of these settings is to send CSP response headers to
# Mozilla FireFox users so that you can enforce how dynamic content
# is used. CSP usage helps to prevent XSS attacks against your users.
#
# Reference Link:
#
# https://developer.mozilla.org/en/Security/CSP
#
# Uncomment this SecAction line if you want use CSP enforcement.
# You need to set the appropriate directives and settings for your site/domain and
# and activate the CSP file in the experimental_rules directory.
#
# Ref: http://blog.spiderlabs.com/2011/04/modsecurity-advanced-topic-of-the-week-integrating-content-security-policy-csp.html
#
#SecAction \
"id:'900013', \
phase:1, \
t:none, \
setvar:tx.csp_report_only=1, \
setvar:tx.csp_report_uri=/csp_violation_report, \
setenv:'csp_policy=allow \'self\'; img-src *.yoursite.com; media-src *.yoursite.com; style-src *.yoursite.com; frame-ancestors *.yoursite.com; script-src *.yoursite.com; report-uri %{tx.csp_report_uri}', \
nolog, \
pass"
#
# -- [[ Brute Force Protection ]] ---------------------------------------------------------
#
# If you are using the Brute Force Protection rule set, then uncomment the following
# lines and set the following variables:
# - Protected URLs: resources to protect (e.g. login pages) - set to your login page
# - Burst Time Slice Interval: time interval window to monitor for bursts
# - Request Threshold: request # threshold to trigger a burst
# - Block Period: temporary block timeout
#
#SecAction \
"id:'900014', \
phase:1, \
t:none, \
setvar:'tx.brute_force_protected_urls=/login.jsp /partner_login.php', \
setvar:'tx.brute_force_burst_time_slice=60', \
setvar:'tx.brute_force_counter_threshold=10', \
setvar:'tx.brute_force_block_timeout=300', \
nolog, \
pass"
#
# -- [[ DoS Protection ]] ----------------------------------------------------------------
#
# If you are using the DoS Protection rule set, then uncomment the following
# lines and set the following variables:
# - Burst Time Slice Interval: time interval window to monitor for bursts
# - Request Threshold: request # threshold to trigger a burst
# - Block Period: temporary block timeout
#
#SecAction \
"id:'900015', \
phase:1, \
t:none, \
setvar:'tx.dos_burst_time_slice=60', \
setvar:'tx.dos_counter_threshold=100', \
setvar:'tx.dos_block_timeout=600', \
nolog, \
pass"
#
# -- [[ Check UTF enconding ]] -----------------------------------------------------------
#
# We only want to apply this check if UTF-8 encoding is actually used by the site, otherwise
# it will result in false positives.
#
# Uncomment this line if your site uses UTF8 encoding
#SecAction \
"id:'900016', \
phase:1, \
t:none, \
setvar:tx.crs_validate_utf8_encoding=1, \
nolog, \
pass"
#
# -- [[ Enable XML Body Parsing ]] -------------------------------------------------------
#
# The rules in this file will trigger the XML parser upon an XML request
#
# Initiate XML Processor in case of xml content-type
#
#SecRule REQUEST_HEADERS:Content-Type "text/xml" \
"id:'900017', \
phase:1, \
t:none,t:lowercase, \
nolog, \
pass, \
chain"
#SecRule REQBODY_PROCESSOR "!@streq XML" \
"ctl:requestBodyProcessor=XML"
#
# -- [[ Global and IP Collections ]] -----------------------------------------------------
#
# Create both Global and IP collections for rules to use
# There are some CRS rules that assume that these two collections
# have already been initiated.
#
#SecRule REQUEST_HEADERS:User-Agent "^(.*)$" \
"id:'900018', \
phase:1, \
t:none,t:sha1,t:hexEncode, \
setvar:tx.ua_hash=%{matched_var}, \
nolog, \
pass"
#SecRule REQUEST_HEADERS:x-forwarded-for "^\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b" \
"id:'900019', \
phase:1, \
t:none, \
capture, \
setvar:tx.real_ip=%{tx.1}, \
nolog, \
pass"
#SecRule &TX:REAL_IP "!@eq 0" \
"id:'900020', \
phase:1, \
t:none, \
initcol:global=global, \
initcol:ip=%{tx.real_ip}_%{tx.ua_hash}, \
nolog, \
pass"
#SecRule &TX:REAL_IP "@eq 0" \
"id:'900021', \
phase:1, \
t:none, \
initcol:global=global, \
initcol:ip=%{remote_addr}_%{tx.ua_hash}, \
nolog, \
pass"

View file

@ -497,13 +497,8 @@ class MultipleVhostsTest(util.ApacheTest):
# 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"])
# Should be 0 as one interface already listens to 443
self.assertEqual(mock_add_dir.call_count, 0)
# Reset return lists and inputs
mock_add_dir.reset_mock()
@ -519,6 +514,28 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(mock_add_dir.call_args_list[2][0][2],
["1.1.1.1:8080", "https"])
# mock_get.side_effect = ["1.2.3.4:80", "[::1]:80"]
# mock_find.return_value = ["test1", "test2", "test3"]
# self.config.parser.get_arg = mock_get
# self.config.prepare_server_https("8080", temp=True)
# self.assertEqual(self.listens, 0)
def test_prepare_server_https_needed_listen(self):
mock_find = mock.Mock()
mock_find.return_value = ["test1", "test2"]
mock_get = mock.Mock()
mock_get.side_effect = ["1.2.3.4:8080", "80"]
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
self.config.prepare_server_https("443")
self.assertEqual(mock_add_dir.call_count, 1)
def test_prepare_server_https_mixed_listen(self):
mock_find = mock.Mock()

View file

@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
VENV_BIN="$VENV_PATH/bin"
LE_AUTO_VERSION="0.8.0"
LE_AUTO_VERSION="0.8.1"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@ -172,7 +172,7 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
if apt-cache show virtualenv > /dev/null 2>&1; then
if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
virtualenv="virtualenv"
fi
@ -458,12 +458,39 @@ BootstrapSmartOS() {
pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv'
}
BootstrapMageiaCommon() {
if ! $SUDO urpmi --force \
python \
libpython-devel \
python-virtualenv
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1
fi
if ! $SUDO urpmi --force \
git \
gcc \
cdialog \
python-augeas \
libopenssl-devel \
libffi-devel \
rootcerts
then
echo "Could not install additional dependencies. Aborting bootstrap!"
exit 1
fi
}
# Install required OS packages:
Bootstrap() {
if [ -f /etc/debian_version ]; then
echo "Bootstrapping dependencies for Debian-based OSes..."
BootstrapDebCommon
elif [ -f /etc/mageia-release ] ; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
ExperimentalBootstrap "Mageia" BootstrapMageiaCommon
elif [ -f /etc/redhat-release ]; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
BootstrapRpmCommon
@ -476,7 +503,7 @@ Bootstrap() {
BootstrapArchCommon
else
echo "Please use pacman to install letsencrypt packages:"
echo "# pacman -S letsencrypt letsencrypt-apache"
echo "# pacman -S certbot certbot-apache"
echo
echo "If you would like to use the virtualenv way, please run the script again with the"
echo "--debug flag."
@ -500,6 +527,7 @@ Bootstrap() {
echo "You will need to bootstrap, configure virtualenv, and run pip install manually."
echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
echo "for more info."
exit 1
fi
}
@ -719,15 +747,15 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
acme==0.8.0 \
--hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \
--hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da
certbot==0.8.0 \
--hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \
--hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314
certbot-apache==0.8.0 \
--hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \
--hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268
acme==0.8.1 \
--hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \
--hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f
certbot==0.8.1 \
--hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \
--hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771
certbot-apache==0.8.1 \
--hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \
--hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed
UNLIKELY_EOF
# -------------------------------------------------------------------------

View file

@ -152,17 +152,17 @@ class NginxConfigurator(common.Plugin):
"install a cert.")
vhost = self.choose_vhost(domain)
cert_directives = [['ssl_certificate', fullchain_path],
['ssl_certificate_key', key_path]]
cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path],
['\n', 'ssl_certificate_key', ' ', key_path]]
# OCSP stapling was introduced in Nginx 1.3.7. If we have that version
# or greater, add config settings for it.
stapling_directives = []
if self.version >= (1, 3, 7):
stapling_directives = [
['ssl_trusted_certificate', chain_path],
['ssl_stapling', 'on'],
['ssl_stapling_verify', 'on']]
['\n', 'ssl_trusted_certificate', ' ', chain_path],
['\n', 'ssl_stapling', ' ', 'on'],
['\n', 'ssl_stapling_verify', ' ', 'on'], ['\n']]
if len(stapling_directives) != 0 and not chain_path:
raise errors.PluginError(
@ -225,7 +225,7 @@ class NginxConfigurator(common.Plugin):
if not matches:
# No matches. Create a new vhost with this name in nginx.conf.
filep = self.parser.loc["root"]
new_block = [['server'], [['server_name', target_name]]]
new_block = [['server'], [['\n', 'server_name', ' ', target_name]]]
self.parser.add_http_directives(filep, new_block)
vhost = obj.VirtualHost(filep, set([]), False, True,
set([target_name]), list(new_block[1]))
@ -337,10 +337,10 @@ class NginxConfigurator(common.Plugin):
"""
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)],
['ssl_certificate', snakeoil_cert],
['ssl_certificate_key', snakeoil_key],
['include', self.parser.loc["ssl_options"]]]
ssl_block = [['\n', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)],
['\n', 'ssl_certificate', ' ', snakeoil_cert],
['\n', 'ssl_certificate_key', ' ', snakeoil_key],
['\n', 'include', ' ', self.parser.loc["ssl_options"]]]
self.parser.add_server_directives(
vhost.filep, vhost.names, ssl_block, replace=False)
vhost.ssl = True
@ -689,11 +689,6 @@ def nginx_restart(nginx_ctl, nginx_conf="/etc/nginx.conf"):
def temp_install(options_ssl):
"""Temporary install for convenience."""
# WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY
# THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER
# AND TAKEN OUT BEFORE RELEASE, INSTEAD
# SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM.
# Check to make sure options-ssl.conf is installed
if not os.path.isfile(options_ssl):
shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl)

View file

@ -1,23 +1,30 @@
"""Very low-level nginx config parser based on pyparsing."""
# Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed)
import copy
import logging
import string
from pyparsing import (
Literal, White, Word, alphanums, CharsNotIn, Forward, Group,
Literal, White, Word, alphanums, CharsNotIn, Combine, Forward, Group,
Optional, OneOrMore, Regex, ZeroOrMore)
from pyparsing import stringEnd
from pyparsing import restOfLine
logger = logging.getLogger(__name__)
class RawNginxParser(object):
# pylint: disable=expression-not-assigned
"""A class that parses nginx configuration with pyparsing."""
# constants
space = Optional(White())
nonspace = Regex(r"\S+")
left_bracket = Literal("{").suppress()
right_bracket = Literal("}").suppress()
right_bracket = space.leaveWhitespace() + Literal("}").suppress()
semicolon = Literal(";").suppress()
space = White().suppress()
key = Word(alphanums + "_/+-.")
dollar_var = Combine(Literal('$') + nonspace)
condition = Regex(r"\(.+\)")
# Matches anything that is not a special character AND any chars in single
# or double quotes
value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+")
@ -26,20 +33,25 @@ class RawNginxParser(object):
modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~")
# rules
comment = Literal('#') + restOfLine()
assignment = (key + Optional(space + value, default=None) + semicolon)
location_statement = Optional(space + modifier) + Optional(space + location)
if_statement = Literal("if") + space + Regex(r"\(.+\)") + space
map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space
comment = space + Literal('#') + restOfLine()
assignment = space + key + Optional(space + value, default=None) + semicolon
location_statement = space + Optional(modifier) + Optional(space + location + space)
if_statement = space + Literal("if") + space + condition + space
map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space
block = Forward()
block << Group(
(Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) +
# key could for instance be "server" or "http", or "location" (in which case
# location_statement needs to have a non-empty location)
(Group(space + key + location_statement) ^ Group(if_statement) ^
Group(map_statement)).leaveWhitespace() +
left_bracket +
Group(ZeroOrMore(Group(comment | assignment) | block)) +
Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() +
right_bracket)
script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd
script = OneOrMore(Group(comment | assignment) ^ block) + space + stringEnd
script.parseWithTabs()
def __init__(self, source):
self.source = source
@ -52,42 +64,47 @@ class RawNginxParser(object):
"""Returns the parsed tree as a list."""
return self.parse().asList()
class RawNginxDumper(object):
# pylint: disable=too-few-public-methods
"""A class that dumps nginx configuration from the provided tree."""
def __init__(self, blocks, indentation=4):
def __init__(self, blocks):
self.blocks = blocks
self.indentation = indentation
def __iter__(self, blocks=None, current_indent=0, spacer=' '):
def __iter__(self, blocks=None):
"""Iterates the dumped nginx content."""
blocks = blocks or self.blocks
for key, values in blocks:
indentation = spacer * current_indent
for b0 in blocks:
if isinstance(b0, str):
yield b0
continue
b = copy.deepcopy(b0)
if spacey(b[0]):
yield b.pop(0) # indentation
if not b:
continue
key, values = b.pop(0), b.pop(0)
if isinstance(key, list):
if current_indent:
yield ''
yield indentation + spacer.join(key) + ' {'
yield "".join(key) + '{'
for parameter in values:
dumped = self.__iter__([parameter], current_indent + self.indentation)
for line in dumped:
for line in self.__iter__([parameter]): # negate "for b0 in blocks"
yield line
yield indentation + '}'
yield '}'
else:
if key == '#':
yield spacer * current_indent + key + values
else:
if values is None:
yield spacer * current_indent + key + ';'
else:
yield spacer * current_indent + key + spacer + values + ';'
if isinstance(key, str) and key.strip() == '#': # comment
yield key + values
else: # assignment
gap = ""
# Sometimes the parser has stuck some gap whitespace in here;
# if so rotate it into gap
if values and spacey(values):
gap = values
values = b.pop(0)
yield key + gap + values + ';'
def __str__(self):
"""Return the parsed block as a string."""
return '\n'.join(self) + '\n'
return ''.join(self)
# Shortcut functions to respect Python's serialization interface
@ -101,7 +118,7 @@ def loads(source):
:rtype: list
"""
return RawNginxParser(source).as_list()
return UnspacedList(RawNginxParser(source).as_list())
def load(_file):
@ -115,24 +132,143 @@ def load(_file):
return loads(_file.read())
def dumps(blocks, indentation=4):
def dumps(blocks):
"""Dump to a string.
:param list block: The parsed tree
:param UnspacedList block: The parsed tree
:param int indentation: The number of spaces to indent
:rtype: str
"""
return str(RawNginxDumper(blocks, indentation))
return str(RawNginxDumper(blocks.spaced))
def dump(blocks, _file, indentation=4):
def dump(blocks, _file):
"""Dump to a file.
:param list block: The parsed tree
:param UnspacedList block: The parsed tree
:param file _file: The file to dump to
:param int indentation: The number of spaces to indent
:rtype: NoneType
"""
return _file.write(dumps(blocks, indentation))
return _file.write(dumps(blocks))
spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == ''
class UnspacedList(list):
"""Wrap a list [of lists], making any whitespace entries magically invisible"""
def __init__(self, list_source):
# ensure our argument is not a generator, and duplicate any sublists
self.spaced = copy.deepcopy(list(list_source))
self.dirty = False
# Turn self into a version of the source list that has spaces removed
# and all sub-lists also UnspacedList()ed
list.__init__(self, list_source)
for i, entry in reversed(list(enumerate(self))):
if isinstance(entry, list):
sublist = UnspacedList(entry)
list.__setitem__(self, i, sublist)
self.spaced[i] = sublist.spaced
elif spacey(entry):
# don't delete comments
if "#" not in self[:i]:
list.__delitem__(self, i)
def _coerce(self, inbound):
"""
Coerce some inbound object to be appropriately usable in this object
:param inbound: string or None or list or UnspacedList
:returns: (coerced UnspacedList or string or None, spaced equivalent)
:rtype: tuple
"""
if not isinstance(inbound, list): # str or None
return (inbound, inbound)
else:
if not hasattr(inbound, "spaced"):
inbound = UnspacedList(inbound)
return (inbound, inbound.spaced)
def insert(self, i, x):
item, spaced_item = self._coerce(x)
self.spaced.insert(self._spaced_position(i), spaced_item)
list.insert(self, i, item)
self.dirty = True
def append(self, x):
item, spaced_item = self._coerce(x)
self.spaced.append(spaced_item)
list.append(self, item)
self.dirty = True
def extend(self, x):
item, spaced_item = self._coerce(x)
self.spaced.extend(spaced_item)
list.extend(self, item)
self.dirty = True
def __add__(self, other):
l = copy.deepcopy(self)
l.extend(other)
l.dirty = True
return l
def pop(self, _i=None):
raise NotImplementedError("UnspacedList.pop() not yet implemented")
def remove(self, _):
raise NotImplementedError("UnspacedList.remove() not yet implemented")
def reverse(self):
raise NotImplementedError("UnspacedList.reverse() not yet implemented")
def sort(self, _cmp=None, _key=None, _Rev=None):
raise NotImplementedError("UnspacedList.sort() not yet implemented")
def __setslice__(self, _i, _j, _newslice):
raise NotImplementedError("Slice operations on UnspacedLists not yet implemented")
def __setitem__(self, i, value):
if isinstance(i, slice):
raise NotImplementedError("Slice operations on UnspacedLists not yet implemented")
item, spaced_item = self._coerce(value)
self.spaced.__setitem__(self._spaced_position(i), spaced_item)
list.__setitem__(self, i, item)
self.dirty = True
def __delitem__(self, i):
self.spaced.__delitem__(self._spaced_position(i))
list.__delitem__(self, i)
self.dirty = True
def __deepcopy__(self, memo):
l = UnspacedList(self[:])
l.spaced = copy.deepcopy(self.spaced, memo=memo)
l.dirty = self.dirty
return l
def is_dirty(self):
"""Recurse through the parse tree to figure out if any sublists are dirty"""
if self.dirty:
return True
return any((isinstance(x, list) and x.is_dirty() for x in self))
def _spaced_position(self, idx):
"Convert from indexes in the unspaced list to positions in the spaced one"
pos = spaces = 0
# Normalize indexes like list[-1] etc, and save the result
if idx < 0:
idx = len(self) + idx
if not 0 <= idx < len(self):
raise IndexError("list index out of range")
idx0 = idx
# Count the number of spaces in the spaced list before idx in the unspaced one
while idx != -1:
if spacey(self.spaced[pos]):
spaces += 1
else:
idx -= 1
pos += 1
return idx0 + spaces

View file

@ -85,6 +85,9 @@ class Addr(common.Addr):
return parts
def __repr__(self):
return "Addr(" + self.__str__() + ")"
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.tup == other.tup and
@ -126,6 +129,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
"enabled: %s" % (self.filep, addr_str,
self.names, self.ssl, self.enabled))
def __repr__(self):
return "VirtualHost(" + self.__str__().replace("\n", ", ") + ")\n"
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.filep == other.filep and

View file

@ -1,4 +1,5 @@
"""NginxParser is a member object of the NginxConfigurator class."""
import copy
import glob
import logging
import os
@ -17,7 +18,7 @@ logger = logging.getLogger(__name__)
class NginxParser(object):
"""Class handles the fine details of parsing the Nginx Configuration.
:ivar str root: Normalized abosulte path to the server root
:ivar str root: Normalized absolute path to the server root
directory. Without trailing slash.
:ivar dict parsed: Mapping of file paths to parsed trees
@ -113,6 +114,7 @@ class NginxParser(object):
for filename in servers:
for server in servers[filename]:
# Parse the server block into a VirtualHost object
parsed_server = parse_server(server)
vhost = obj.VirtualHost(filename,
parsed_server['addrs'],
@ -132,7 +134,7 @@ class NginxParser(object):
:rtype: list
"""
result = list(block) # Copy the list to keep self.parsed idempotent
result = copy.deepcopy(block) # Copy the list to keep self.parsed idempotent
for directive in block:
if _is_include_directive(directive):
included_files = glob.glob(
@ -153,7 +155,9 @@ class NginxParser(object):
:rtype: list
"""
files = glob.glob(filepath)
files = glob.glob(filepath) # nginx on unix calls glob(3) for this
# XXX Windows nginx uses FindFirstFile, and
# should have a narrower call here
trees = []
for item in files:
if item in self.parsed and not override:
@ -201,21 +205,27 @@ class NginxParser(object):
raise errors.NoInstallationError(
"Could not find configuration root")
def filedump(self, ext='tmp'):
def filedump(self, ext='tmp', lazy=True):
"""Dumps parsed configurations into files.
:param str ext: The file extension to use for the dumped files. If
empty, this overrides the existing conf files.
:param bool lazy: Only write files that have been modified
"""
# Best-effort atomicity is enforced above us by reverter.py
for filename in self.parsed:
tree = self.parsed[filename]
if ext:
filename = filename + os.path.extsep + ext
try:
logger.debug('Dumping to %s:\n%s', filename, nginxparser.dumps(tree))
if lazy and not tree.is_dirty():
continue
out = nginxparser.dumps(tree)
logger.debug('Writing nginx conf tree to %s:\n%s', filename, out)
with open(filename, 'w') as _file:
nginxparser.dump(tree, _file)
_file.write(out)
except IOError:
logger.error("Could not open file for writing: %s", filename)
@ -463,6 +473,8 @@ def parse_server(server):
'names': set()}
for directive in server:
if not directive:
continue
if directive[0] == 'listen':
addr = obj.Addr.fromstring(directive[1])
parsed_server['addrs'].add(addr)
@ -503,6 +515,11 @@ def _add_directive(block, directive, replace):
See _add_directives for more documentation.
"""
directive = nginxparser.UnspacedList(directive)
if len(directive) == 0:
# whitespace
block.append(directive)
return
location = -1
# Find the index of a config line where the name of the directive matches
# the name of the directive we want to add.

View file

@ -83,7 +83,7 @@ class NginxConfiguratorTest(util.NginxTest):
filep = self.config.parser.abs_path('sites-enabled/example.com')
self.config.parser.add_server_directives(
filep, set(['.example.com', 'example.*']),
[['listen', '5001 ssl']],
[['listen', ' ', '5001 ssl']],
replace=False)
self.config.save()
@ -265,7 +265,8 @@ class NginxConfiguratorTest(util.NginxTest):
@mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform")
@mock.patch("certbot_nginx.configurator.NginxConfigurator.restart")
def test_perform(self, mock_restart, mock_perform):
@mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config")
def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_perform):
# Only tests functionality specific to configurator.perform
# Note: As more challenges are offered this will have to be expanded
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
@ -291,7 +292,11 @@ class NginxConfiguratorTest(util.NginxTest):
self.assertEqual(mock_perform.call_count, 1)
self.assertEqual(responses, expected)
self.assertEqual(mock_restart.call_count, 1)
self.config.cleanup([achall1, achall2])
self.assertEqual(0, self.config._chall_out) # pylint: disable=protected-access
self.assertEqual(mock_revert.call_count, 1)
self.assertEqual(mock_restart.call_count, 2)
@mock.patch("certbot_nginx.configurator.subprocess.Popen")
def test_get_version(self, mock_popen):

View file

@ -1,11 +1,13 @@
"""Test for certbot_nginx.nginxparser."""
import copy
import operator
import os
import unittest
from pyparsing import ParseException
from certbot_nginx.nginxparser import (
RawNginxParser, loads, load, dumps, dump)
RawNginxParser, loads, load, dumps, dump, UnspacedList)
from certbot_nginx.tests import util
@ -17,39 +19,39 @@ class TestRawNginxParser(unittest.TestCase):
def test_assignments(self):
parsed = RawNginxParser.assignment.parseString('root /test;').asList()
self.assertEqual(parsed, ['root', '/test'])
parsed = RawNginxParser.assignment.parseString('root /test;'
'foo bar;').asList()
self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar'])
self.assertEqual(parsed, ['root', ' ', '/test'])
parsed = RawNginxParser.assignment.parseString('root /test;foo bar;').asList()
self.assertEqual(parsed, ['root', ' ', '/test'], ['foo', ' ', 'bar'])
def test_blocks(self):
parsed = RawNginxParser.block.parseString('foo {}').asList()
self.assertEqual(parsed, [[['foo'], []]])
self.assertEqual(parsed, [[['foo', ' '], []]])
parsed = RawNginxParser.block.parseString('location /foo{}').asList()
self.assertEqual(parsed, [[['location', '/foo'], []]])
parsed = RawNginxParser.block.parseString('foo { bar foo; }').asList()
self.assertEqual(parsed, [[['foo'], [['bar', 'foo']]]])
self.assertEqual(parsed, [[['location', ' ', '/foo'], []]])
parsed = RawNginxParser.block.parseString('foo { bar foo ; }').asList()
self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo '], ' ']]])
def test_nested_blocks(self):
parsed = RawNginxParser.block.parseString('foo { bar {} }').asList()
block, content = FIRST(parsed)
self.assertEqual(FIRST(content), [['bar'], []])
self.assertEqual(FIRST(content), [[' ', 'bar', ' '], []])
self.assertEqual(FIRST(block), 'foo')
def test_dump_as_string(self):
dumped = dumps([
['user', 'www-data'],
[['server'], [
['listen', '80'],
['server_name', 'foo.com'],
['root', '/home/ubuntu/sites/foo/'],
[['location', '/status'], [
['check_status', None],
[['types'], [['image/jpeg', 'jpg']]],
dumped = dumps(UnspacedList([
['user', ' ', 'www-data'],
[['\n', 'server', ' '], [
['\n ', 'listen', ' ', '80'],
['\n ', 'server_name', ' ', 'foo.com'],
['\n ', 'root', ' ', '/home/ubuntu/sites/foo/'],
[['\n\n ', 'location', ' ', '/status', ' '], [
['\n ', 'check_status', ''],
[['\n\n ', 'types', ' '],
[['\n ', 'image/jpeg', ' ', 'jpg']]],
]]
]]])
]]]))
self.assertEqual(dumped,
self.assertEqual(dumped.split('\n'),
'user www-data;\n'
'server {\n'
' listen 80;\n'
@ -60,10 +62,7 @@ class TestRawNginxParser(unittest.TestCase):
' check_status;\n'
'\n'
' types {\n'
' image/jpeg jpg;\n'
' }\n'
' }\n'
'}\n')
' image/jpeg jpg;}}}'.split('\n'))
def test_parse_from_file(self):
with open(util.get_data_filename('foo.conf')) as handle:
@ -116,24 +115,29 @@ class TestRawNginxParser(unittest.TestCase):
def test_dump_as_file(self):
with open(util.get_data_filename('nginx.conf')) as handle:
parsed = util.filter_comments(load(handle))
parsed[-1][-1].append([['server'],
[['listen', '443 ssl'],
['server_name', 'localhost'],
['ssl_certificate', 'cert.pem'],
['ssl_certificate_key', 'cert.key'],
['ssl_session_cache', 'shared:SSL:1m'],
['ssl_session_timeout', '5m'],
['ssl_ciphers', 'HIGH:!aNULL:!MD5'],
[['location', '/'],
[['root', 'html'],
['index', 'index.html index.htm']]]]])
parsed = load(handle)
parsed[-1][-1].append(UnspacedList([['server'],
[['listen', ' ', '443 ssl'],
['server_name', ' ', 'localhost'],
['ssl_certificate', ' ', 'cert.pem'],
['ssl_certificate_key', ' ', 'cert.key'],
['ssl_session_cache', ' ', 'shared:SSL:1m'],
['ssl_session_timeout', ' ', '5m'],
['ssl_ciphers', ' ', 'HIGH:!aNULL:!MD5'],
[['location', ' ', '/'],
[['root', ' ', 'html'],
['index', ' ', 'index.html index.htm']]]]]))
with open(util.get_data_filename('nginx.new.conf'), 'w') as handle:
dump(parsed, handle)
with open(util.get_data_filename('nginx.new.conf')) as handle:
parsed_new = util.filter_comments(load(handle))
self.assertEquals(parsed, parsed_new)
parsed_new = load(handle)
try:
self.maxDiff = None
self.assertEquals(parsed[0], parsed_new[0])
self.assertEquals(parsed[1:], parsed_new[1:])
finally:
os.unlink(util.get_data_filename('nginx.new.conf'))
def test_comments(self):
with open(util.get_data_filename('minimalistic_comments.conf')) as handle:
@ -145,20 +149,23 @@ class TestRawNginxParser(unittest.TestCase):
with open(util.get_data_filename('minimalistic_comments.new.conf')) as handle:
parsed_new = load(handle)
self.assertEquals(parsed, parsed_new)
try:
self.assertEquals(parsed, parsed_new)
self.assertEqual(parsed_new, [
['#', " Use bar.conf when it's a full moon!"],
['include', 'foo.conf'],
['#', ' Kilroy was here'],
['check_status', None],
[['server'],
[['#', ''],
['#', " Don't forget to open up your firewall!"],
['#', ''],
['listen', '1234'],
['#', ' listen 80;']]],
])
self.assertEqual(parsed_new, [
['#', " Use bar.conf when it's a full moon!"],
['include', 'foo.conf'],
['#', ' Kilroy was here'],
['check_status'],
[['server'],
[['#', ''],
['#', " Don't forget to open up your firewall!"],
['#', ''],
['listen', '1234'],
['#', ' listen 80;']]],
])
finally:
os.unlink(util.get_data_filename('minimalistic_comments.new.conf'))
def test_issue_518(self):
parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }')
@ -168,5 +175,73 @@ class TestRawNginxParser(unittest.TestCase):
[['set', '$webp "true"']]]
])
class TestUnspacedList(unittest.TestCase):
"""Test the UnspacedList data structure"""
def setUp(self):
self.a = ["\n ", "things", " ", "quirk"]
self.b = ["y", " "]
self.l = self.a[:]
self.l2 = self.b[:]
self.ul = UnspacedList(self.l)
self.ul2 = UnspacedList(self.l2)
def test_construction(self):
self.assertEqual(self.ul, ["things", "quirk"])
self.assertEqual(self.ul2, ["y"])
def test_append(self):
ul3 = copy.deepcopy(self.ul)
ul3.append("wise")
self.assertEqual(ul3, ["things", "quirk", "wise"])
self.assertEqual(ul3.spaced, self.a + ["wise"])
def test_add(self):
ul3 = self.ul + self.ul2
self.assertEqual(ul3, ["things", "quirk", "y"])
self.assertEqual(ul3.spaced, self.a + self.b)
self.assertEqual(self.ul.spaced, self.a)
ul3 = self.ul + self.l2
self.assertEqual(ul3, ["things", "quirk", "y"])
self.assertEqual(ul3.spaced, self.a + self.b)
def test_extend(self):
ul3 = copy.deepcopy(self.ul)
ul3.extend(self.ul2)
self.assertEqual(ul3, ["things", "quirk", "y"])
self.assertEqual(ul3.spaced, self.a + self.b)
self.assertEqual(self.ul.spaced, self.a)
def test_set(self):
ul3 = copy.deepcopy(self.ul)
ul3[0] = "zither"
l = ["\n ", "zather", "zest"]
ul3[1] = UnspacedList(l)
self.assertEqual(ul3, ["zither", ["zather", "zest"]])
self.assertEqual(ul3.spaced, [self.a[0], "zither", " ", l])
def test_get(self):
self.assertRaises(IndexError, self.ul2.__getitem__, 2)
self.assertRaises(IndexError, self.ul2.__getitem__, -3)
def test_rawlists(self):
ul3 = copy.deepcopy(self.ul)
ul3.insert(0, "some")
ul3.append("why")
ul3.extend(["did", "whether"])
del ul3[2]
self.assertEqual(ul3, ["some", "things", "why", "did", "whether"])
def test_is_dirty(self):
self.assertEqual(False, self.ul2.is_dirty())
ul3 = UnspacedList([])
ul3.append(self.ul)
self.assertEqual(False, self.ul.is_dirty())
self.assertEqual(True, ul3.is_dirty())
ul4 = UnspacedList([[1], [2, 3, 4]])
self.assertEqual(False, ul4.is_dirty())
ul4[1][2] = 5
self.assertEqual(True, ul4.is_dirty())
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -66,7 +66,7 @@ class NginxParserTest(util.NginxTest):
def test_filedump(self):
nparser = parser.NginxParser(self.config_path, self.ssl_options)
nparser.filedump('test')
nparser.filedump('test', lazy=False)
# pylint: disable=protected-access
parsed = nparser._parse_files(nparser.abs_path(
'sites-enabled/example.com.test'))
@ -126,7 +126,7 @@ class NginxParserTest(util.NginxTest):
nparser.add_server_directives(nparser.abs_path('nginx.conf'),
set(['localhost',
r'~^(www\.)?(example|bar)\.']),
[['foo', 'bar'], ['ssl_certificate',
[['foo', 'bar'], ['\n ', 'ssl_certificate', ' ',
'/etc/ssl/cert.pem']],
replace=False)
ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem')

View file

@ -1,11 +0,0 @@
# Use bar.conf when it's a full moon!
include foo.conf;
# Kilroy was here
check_status;
server {
#
# Don't forget to open up your firewall!
#
listen 1234;
# listen 80;
}

View file

@ -1,83 +0,0 @@
user nobody;
worker_processes 1;
error_log logs/error.log;
error_log logs/error.log notice;
error_log logs/error.log info;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
include foo.conf;
http {
include mime.types;
include sites-enabled/*;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 0;
gzip on;
server {
listen 8080;
server_name localhost;
server_name ~^(www\.)?(example|bar)\.;
charset koi8-r;
access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location ~ \.php$ {
proxy_pass http://127.0.0.1;
}
location ~ \.php$ {
root html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 8000;
listen somename:8080;
include server.conf;
location / {
root html;
index index.html index.htm;
}
}
server {
listen 443 ssl;
server_name localhost;
ssl_certificate cert.pem;
ssl_certificate_key cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
root html;
index index.html index.htm;
}
}
}

View file

@ -80,7 +80,7 @@ class TlsSniPerformTest(util.NginxTest):
mock_setup_cert.assert_called_once_with(self.achalls[0])
self.assertEqual([response], responses)
self.assertEqual(mock_save.call_count, 2)
self.assertEqual(mock_save.call_count, 1)
# Make sure challenge config is included in main config
http = self.sni.configurator.parser.parsed[

View file

@ -1,4 +1,5 @@
"""Common utilities for certbot_nginx."""
import copy
import os
import pkg_resources
import unittest
@ -82,12 +83,15 @@ def filter_comments(tree):
def traverse(tree):
"""Generator dropping comment nodes"""
for key, values in tree:
for entry in tree:
key, values = entry
if isinstance(key, list):
yield [key, filter_comments(values)]
new = copy.deepcopy(entry)
new[1] = filter_comments(values)
yield new
else:
if key != '#':
yield [key, values]
yield entry
return list(traverse(tree))

View file

@ -46,8 +46,6 @@ class NginxTlsSni01(common.TLSSNI01):
if not self.achalls:
return []
self.configurator.save()
addresses = []
default_addr = "{0} default_server ssl".format(
self.configurator.config.tls_sni_01_port)
@ -93,10 +91,10 @@ class NginxTlsSni01(common.TLSSNI01):
# Add the 'include' statement for the challenges if it doesn't exist
# already in the main config
included = False
include_directive = ['include', self.challenge_conf]
include_directive = ['include', ' ', self.challenge_conf]
root = self.configurator.parser.loc["root"]
bucket_directive = ['server_names_hash_bucket_size', '128']
bucket_directive = ['server_names_hash_bucket_size', ' ', '128']
main = self.configurator.parser.parsed[root]
for key, body in main:
@ -118,6 +116,7 @@ class NginxTlsSni01(common.TLSSNI01):
config = [self._make_server_block(pair[0], pair[1])
for pair in itertools.izip(self.achalls, ll_addrs)]
config = nginxparser.UnspacedList(config)
self.configurator.reverter.register_file_creation(
True, self.challenge_conf)
@ -142,19 +141,19 @@ class NginxTlsSni01(common.TLSSNI01):
document_root = os.path.join(
self.configurator.config.work_dir, "tls_sni_01_page")
block = [['listen', str(addr)] for addr in addrs]
block = [['listen', ' ', str(addr)] for addr in addrs]
block.extend([['server_name',
block.extend([['server_name', ' ',
achall.response(achall.account_key).z_domain],
['include', self.configurator.parser.loc["ssl_options"]],
['include', ' ', self.configurator.parser.loc["ssl_options"]],
# access and error logs necessary for
# integration testing (non-root)
['access_log', os.path.join(
['access_log', ' ', os.path.join(
self.configurator.config.work_dir, 'access.log')],
['error_log', os.path.join(
['error_log', ' ', os.path.join(
self.configurator.config.work_dir, 'error.log')],
['ssl_certificate', self.get_cert_path(achall)],
['ssl_certificate_key', self.get_key_path(achall)],
[['location', '/'], [['root', document_root]]]])
['ssl_certificate', ' ', self.get_cert_path(achall)],
['ssl_certificate_key', ' ', self.get_key_path(achall)],
[['location', ' ', '/'], [['root', ' ', document_root]]]])
return [['server'], block]

View file

@ -773,7 +773,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
helpful.add(
"security", "--hsts", action="store_true",
help="Add the Strict-Transport-Security header to every HTTP response."
" Forcing browser to use always use SSL for the domain."
" Forcing browser to always use SSL for the domain."
" Defends against SSL Stripping.", dest="hsts", default=False)
helpful.add(
"security", "--no-hsts", action="store_false",

View file

@ -282,7 +282,7 @@ class Client(object):
"by your operating system package manager")
if self.config.dry_run:
logger.info("Dry run: Skipping creating new lineage for %s",
logger.debug("Dry run: Skipping creating new lineage for %s",
domains[0])
return None
else:

View file

@ -18,7 +18,7 @@ CLI_DEFAULTS = dict(
os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"),
"letsencrypt", "cli.ini"),
],
verbose_count=-(logging.WARNING / 10),
verbose_count=-(logging.INFO / 10),
server="https://acme-v01.api.letsencrypt.org/directory",
rsa_key_size=2048,
rollback_checkpoints=1,

View file

@ -180,6 +180,9 @@ class IAuthenticator(IPlugin):
def cleanup(achalls):
"""Revert changes and shutdown after challenges complete.
This method should be able to revert all changes made by
perform, even if perform exited abnormally.
:param list achalls: Non-empty (guaranteed) list of
:class:`~certbot.achallenges.AnnotatedChallenge`
instances, a subset of those previously passed to :func:`perform`.
@ -238,6 +241,14 @@ class IInstaller(IPlugin):
Represents any server that an X509 certificate can be placed.
It is assumed that :func:`save` is the only method that finalizes a
checkpoint. This is important to ensure that checkpoints are
restored in a consistent manner if requested by the user or in case
of an error.
Using :class:`certbot.reverter.Reverter` to implement checkpoints,
rollback, and recovery can dramatically simplify plugin development.
"""
def get_all_names():
@ -304,8 +315,11 @@ class IInstaller(IPlugin):
Both title and temporary are needed because a save may be
intended to be permanent, but the save is not ready to be a full
checkpoint. If an exception is raised, it is assumed a new
checkpoint was not created.
checkpoint.
It is assumed that at most one checkpoint is finalized by this
method. Additionally, if an exception is raised, it is assumed a
new checkpoint was not finalized.
: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

View file

@ -1,6 +1,7 @@
"""Certbot main entry point."""
from __future__ import print_function
import atexit
import dialog
import errno
import functools
import logging.handlers
@ -83,14 +84,17 @@ def _auth_from_domains(le_client, config, domains, lineage=None):
if action == "reinstall":
# The lineage already exists; allow the caller to try installing
# it without getting a new certificate at all.
logger.info("Keeping the existing certificate")
return lineage, "reinstall"
hooks.pre_hook(config)
try:
if action == "renew":
logger.info("Renewing an existing certificate")
renewal.renew_cert(config, domains, le_client, lineage)
elif action == "newcert":
# TREAT AS NEW REQUEST
logger.info("Obtaining a new certificate")
lineage = le_client.obtain_and_enroll_certificate(domains)
if lineage is False:
raise errors.Error("Certificate could not be obtained")
@ -526,7 +530,7 @@ def _csr_obtain_cert(config, le_client):
csr, typ = config.actual_csr
certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ)
if config.dry_run:
logger.info(
logger.debug(
"Dry run: skipping saving certificate to %s", config.cert_path)
else:
cert_path, _, cert_fullchain = le_client.save_certificate(
@ -624,11 +628,12 @@ def _cli_log_handler(config, level, fmt):
def setup_logging(config, cli_handler_factory, logfile):
"""Setup logging."""
fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s"
file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s"
cli_fmt = "%(message)s"
level = -config.verbose_count * 10
file_handler, log_file_path = setup_log_file_handler(
config, logfile=logfile, fmt=fmt)
cli_handler = cli_handler_factory(config, level, fmt)
config, logfile=logfile, fmt=file_fmt)
cli_handler = cli_handler_factory(config, level, cli_fmt)
# TODO: use fileConfig?
@ -674,7 +679,10 @@ def _handle_exception(exc_type, exc_value, trace, config):
# Here we're passing a client or ACME error out to the client at the shell
# Tell the user a bit about what happened, without overwhelming
# them with a full traceback
err = traceback.format_exception_only(exc_type, exc_value)[0]
if issubclass(exc_type, dialog.error):
err = exc_value.complete_message()
else:
err = traceback.format_exception_only(exc_type, exc_value)[0]
# Typical error from the ACME module:
# acme.messages.Error: urn:acme:error:malformed :: The request message was
# malformed :: Error creating new registration :: Validation of contact

View file

@ -108,7 +108,7 @@ def _restore_webroot_config(config, renewalparams):
if not cli.set_by_cli("webroot_map"):
config.namespace.webroot_map = renewalparams["webroot_map"]
elif "webroot_path" in renewalparams:
logger.info("Ancient renewal conf file without webroot-map, restoring webroot-path")
logger.debug("Ancient renewal conf file without webroot-map, restoring webroot-path")
wp = renewalparams["webroot_path"]
if isinstance(wp, str): # prior to 0.1.0, webroot_path was a string
wp = [wp]
@ -194,7 +194,7 @@ def _restore_required_config_elements(config, renewalparams):
def should_renew(config, lineage):
"Return true if any of the circumstances for automatic renewal apply."
if config.renew_by_default:
logger.info("Auto-renewal forced with --force-renewal...")
logger.debug("Auto-renewal forced with --force-renewal...")
return True
if lineage.should_autorenew(interactive=True):
logger.info("Cert is due for renewal, auto-renewing...")
@ -236,7 +236,7 @@ def renew_cert(config, domains, le_client, lineage):
_avoid_invalidating_lineage(config, lineage, original_server)
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
if config.dry_run:
logger.info("Dry run: skipping updating lineage at %s",
logger.debug("Dry run: skipping updating lineage at %s",
os.path.dirname(lineage.cert))
else:
prior_version = lineage.latest_common_version()

View file

@ -58,7 +58,7 @@ class Reporter(object):
"""
assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY
self.messages.put(self._msg_type(priority, msg, on_crash))
logger.info("Reporting to user: %s", msg)
logger.debug("Reporting to user: %s", msg)
def atexit_print_messages(self, pid=None):
"""Function to be registered with atexit to print messages.

View file

@ -24,6 +24,39 @@ logger = logging.getLogger(__name__)
class Reverter(object):
"""Reverter Class - save and revert configuration checkpoints.
This class can be used by the plugins, especially Installers, to
undo changes made to the user's system. Modifications to files and
commands to do undo actions taken by the plugin should be registered
with this class before the action is taken.
Once a change has been registered with this class, there are three
states the change can be in. First, the change can be a temporary
change. This should be used for changes that will soon be reverted,
such as config changes for the purpose of solving a challenge.
Changes are added to this state through calls to
:func:`~add_to_temp_checkpoint` and reverted when
:func:`~revert_temporary_config` or :func:`~recovery_routine` is
called.
The second state a change can be in is in progress. These changes
are not temporary, however, they also have not been finalized in a
checkpoint. A change must become in progress before it can be
finalized. Changes are added to this state through calls to
:func:`~add_to_checkpoint` and reverted when
:func:`~recovery_routine` is called.
The last state a change can be in is finalized in a checkpoint. A
change is put into this state by first becoming an in progress
change and then calling :func:`~finalize_checkpoint`. Changes
in this state can be reverted through calls to
:func:`~rollback_checkpoints`.
As a final note, creating new files and registering undo commands
are handled specially and use the methods
:func:`~register_file_creation` and :func:`~register_undo_command`
respectively. Both of these methods can be used to create either
temporary or in progress changes.
.. note:: Consider moving everything over to CSV format.
:param config: Configuration.

View file

@ -2,6 +2,7 @@
from __future__ import print_function
import argparse
import dialog
import functools
import itertools
import os
@ -341,11 +342,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
# FQDN
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'comma,gotwrong.tld'])
['-d', 'a' * 64])
# FQDN 2
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'illegal.character=.tld'])
['-d', (('a' * 50) + '.') * 10])
# Wildcard
self.assertRaises(errors.ConfigurationError,
self._call,
@ -922,6 +923,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
mock_sys.exit.assert_called_with(''.join(
traceback.format_exception_only(KeyboardInterrupt, interrupt)))
# Test dialog errors
exception = dialog.error(message="test message")
main._handle_exception(
dialog.DialogError, exc_value=exception, trace=None, config=None)
error_msg = mock_sys.exit.call_args_list[-1][0][0]
self.assertTrue("test message" in error_msg)
def test_read_file(self):
rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo'))
self.assertRaises(

View file

@ -248,9 +248,9 @@ class ChooseNamesTest(unittest.TestCase):
def test_get_valid_domains(self):
from certbot.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"]
"also.example.com", "under_score.example.com",
"justtld"]
all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "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), [])
@ -276,19 +276,18 @@ class ChooseNamesTest(unittest.TestCase):
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
# Valid domains
mock_util().input.return_value = (display_util.OK,
("example.com,"
"under_score.example.com,"
"justtld,"
"valid.example.com"))
self.assertEqual(_choose_names_manually(),
["example.com", "valid.example.com"])
["example.com", "under_score.example.com",
"justtld", "valid.example.com"])
# Three iterations
mock_util().input.return_value = (display_util.OK,
"notFQDN")
"uniçodé.com")
yn = mock.MagicMock()
yn.side_effect = [True, True, False]
mock_util().yesno = yn

View file

@ -423,14 +423,17 @@ def enforce_domain_sanity(domain):
# It wasn't an IP address, so that's good
pass
# 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 {0} is not a FQDN"
.format(domain))
# FQDN checks according to RFC 2181: domain name should be less than 255
# octets (inclusive). And each label is 1 - 63 octets (inclusive).
# https://tools.ietf.org/html/rfc2181#section-11
msg = "Requested domain {0} is not a FQDN because ".format(domain)
labels = domain.split('.')
for l in labels:
if not 0 < len(l) < 64:
raise errors.ConfigurationError(msg + "label {0} is too long.".format(l))
if len(domain) > 255:
raise errors.ConfigurationError(msg + "it is too long.")
return domain

View file

@ -147,7 +147,7 @@ security:
HTTPS for the newly authenticated vhost. (default:
None)
--hsts Add the Strict-Transport-Security header to every HTTP
response. Forcing browser to use always use SSL for
response. Forcing browser to always use SSL for
the domain. Defends against SSL Stripping. (default:
False)
--no-hsts Do not automatically add the Strict-Transport-Security
@ -200,6 +200,15 @@ renew:
and keys; the shell variable $RENEWED_DOMAINS will
contain a space-delimited list of renewed cert domains
(default: None)
--disable-hook-validation
Ordinarily the commands specified for --pre-hook
/--post-hook/--renew-hook will be checked for
validity, to see if the programs being run are in the
$PATH, so that mistakes can be caught early, even when
the hooks aren't being run just yet. The validation is
rather simplistic and fails if you use more advanced
shell constructs, so you can use this switch to
disable it. (default: True)
certonly:
Options for modifying how a cert is obtained

View file

@ -71,6 +71,9 @@ The following tools are there to help you:
experimental, non-production Apache2 install on them. ``tox -e
apacheconftest`` can be used to run those specific Apache conf tests.
- ``tox --skip-missing-interpreters`` runs tox while ignoring missing versions
of Python needed for running the tests.
- ``tox -e py27``, ``tox -e py26`` etc, run unit tests for specific Python
versions.
@ -313,7 +316,9 @@ Steps:
3. Run ``./pep8.travis.sh`` to do a cursory check of your code style.
Fix any errors.
4. Run ``tox -e lint`` to check for pylint errors. Fix any errors.
5. Run ``tox`` to run the entire test suite including coverage. Fix any errors.
5. Run ``tox --skip-missing-interpreters`` to run the entire test suite
including coverage. The ``--skip-missing-interpreters`` argument ignores
missing versions of Python needed for running the tests. Fix any errors.
6. If your code touches communication with an ACME server/Boulder, you
should run the integration tests, see `integration`_. See `Known Issues`_
for some common failures that have nothing to do with your code.

View file

@ -552,10 +552,3 @@ Beyond the methods discussed here, other methods may be possible, such as
installing Certbot directly with pip from PyPI or downloading a ZIP
archive from GitHub may be technically possible but are not presently
recommended or supported.
.. rubric:: Footnotes
.. [#venv] By using this virtualized Python environment (`virtualenv
<https://virtualenv.pypa.io>`_) we don't pollute the main
OS space with packages from PyPI!

View file

@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
VENV_BIN="$VENV_PATH/bin"
LE_AUTO_VERSION="0.8.0"
LE_AUTO_VERSION="0.8.1"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@ -172,7 +172,7 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
if apt-cache show virtualenv > /dev/null 2>&1; then
if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
virtualenv="virtualenv"
fi
@ -458,12 +458,39 @@ BootstrapSmartOS() {
pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv'
}
BootstrapMageiaCommon() {
if ! $SUDO urpmi --force \
python \
libpython-devel \
python-virtualenv
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1
fi
if ! $SUDO urpmi --force \
git \
gcc \
cdialog \
python-augeas \
libopenssl-devel \
libffi-devel \
rootcerts
then
echo "Could not install additional dependencies. Aborting bootstrap!"
exit 1
fi
}
# Install required OS packages:
Bootstrap() {
if [ -f /etc/debian_version ]; then
echo "Bootstrapping dependencies for Debian-based OSes..."
BootstrapDebCommon
elif [ -f /etc/mageia-release ] ; then
# Mageia has both /etc/mageia-release and /etc/redhat-release
ExperimentalBootstrap "Mageia" BootstrapMageiaCommon
elif [ -f /etc/redhat-release ]; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
BootstrapRpmCommon
@ -476,7 +503,7 @@ Bootstrap() {
BootstrapArchCommon
else
echo "Please use pacman to install letsencrypt packages:"
echo "# pacman -S letsencrypt letsencrypt-apache"
echo "# pacman -S certbot certbot-apache"
echo
echo "If you would like to use the virtualenv way, please run the script again with the"
echo "--debug flag."
@ -500,6 +527,7 @@ Bootstrap() {
echo "You will need to bootstrap, configure virtualenv, and run pip install manually."
echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
echo "for more info."
exit 1
fi
}
@ -719,15 +747,15 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
acme==0.8.0 \
--hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \
--hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da
certbot==0.8.0 \
--hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \
--hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314
certbot-apache==0.8.0 \
--hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \
--hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268
acme==0.8.1 \
--hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \
--hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f
certbot==0.8.1 \
--hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \
--hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771
certbot-apache==0.8.1 \
--hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \
--hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed
UNLIKELY_EOF
# -------------------------------------------------------------------------

View file

@ -1,11 +1,11 @@
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1
iQEcBAABAgAGBQJXUJvwAAoJEE0XyZXNl3XyvKsH/3qn7Xa/GQx3HvB6Io/Csn/E
v1nbUg5RPwvrTyyol8BJ6UrHiJw+gTbUgCAnBkZ7DYKaC8AQmQXVRcWXNALMMTzB
6LpBXjQQ2xrBYamGj70N7KnTM1QmxI96GUQouiHMJVugV4uihKJDjtR8/f2JWKok
ZSox6E4LqC45HzqLWiOqc13TrHbti32Mo8DyC63PBnSwMnypGLK6XcqM0L9Re62W
smoKu1VWKwWZYRYXIQr0dvK4JmVTrIsdASdZkhTC/vc8y4tGkdN0DcF2EHzci6OA
Tx0W+Ao+HM1ZcaaH3BJ1y3kYfT+mlt6o4OaK3UB/wtUzMmVih7l1UeiNkVL0oYk=
=t3L6
iQEcBAABAgAGBQJXYJmBAAoJEE0XyZXNl3XyyIMH/jtYFb7rl5XXN8hjlKuK5frq
z7/jdK7fvI+mtYJ4i2Cy3yMz8T4wscXGkhxNtipbATWlpevPfjYzm4ZGC25coFZx
fDX44w0hBBgel7EISXGR1ABXb2rj24TZxIYXwaeClylsK9n5CxcWBocn8tDlfr8t
7VQUJEL3l1IlrnKnvpoL4Eq11sxlIPtitDPJ5c98ZM1293ZbWzIqyZKoXLIUkKHg
pkaa80j/QMmFumxzXFenU91JusLdeoblvjjg+kzjGonjslAYIuH4wEEjz2VJuUYe
P2+2ZyW4eLA6rRZhZ3CMtV79HzTPTWiELCYbXezb+yXJJEqzCYtIXkmbNQ3jUEY=
=86lB
-----END PGP SIGNATURE-----

View file

@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed.
-n, --non-interactive, --noninteractive run without asking for user input
--no-self-upgrade do not download updates
--os-packages-only install OS dependencies and exit
-q, --quiet provide only update/error output
-v, --verbose provide more output
All arguments are accepted and forwarded to the Certbot client when run."
@ -52,15 +53,19 @@ for arg in "$@" ; do
HELP=1;;
--noninteractive|--non-interactive)
ASSUME_YES=1;;
--quiet)
QUIET=1;;
--verbose)
VERBOSE=1;;
-[!-]*)
while getopts ":hnv" short_arg $arg; do
while getopts ":hnvq" short_arg $arg; do
case "$short_arg" in
h)
HELP=1;;
n)
ASSUME_YES=1;;
q)
QUIET=1;;
v)
VERBOSE=1;;
esac
@ -598,28 +603,29 @@ ConfigArgParse==0.10.0 \
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
cryptography==1.2.3 \
--hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \
--hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \
--hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \
--hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \
--hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \
--hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \
--hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \
--hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \
--hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \
--hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \
--hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \
--hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \
--hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \
--hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \
--hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \
--hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \
--hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \
--hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \
--hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \
--hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \
--hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e
cryptography==1.3.4 \
--hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
--hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
--hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
--hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
--hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
--hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
--hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
--hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
--hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
--hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
--hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
--hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
--hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
--hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
--hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
--hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
--hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
--hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
--hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
--hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
--hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
--hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@ -679,9 +685,9 @@ pyasn1==0.1.9 \
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
pyOpenSSL==0.15.1 \
--hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \
--hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672
pyopenssl==16.0.0 \
--hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
--hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
pyRFC3339==1.0 \
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
@ -747,15 +753,15 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
acme==0.8.0 \
--hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \
--hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da
certbot==0.8.0 \
--hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \
--hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314
certbot-apache==0.8.0 \
--hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \
--hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268
acme==0.8.1 \
--hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \
--hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f
certbot==0.8.1 \
--hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \
--hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771
certbot-apache==0.8.1 \
--hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \
--hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed
UNLIKELY_EOF
# -------------------------------------------------------------------------
@ -926,8 +932,10 @@ UNLIKELY_EOF
fi
if [ -n "$SUDO" ]; then
# SUDO is su wrapper or sudo
echo "Requesting root privileges to run certbot..."
echo " $VENV_BIN/letsencrypt" "$@"
if [ "$QUIET" != 1 ]; then
echo "Requesting root privileges to run certbot..."
echo " $VENV_BIN/letsencrypt" "$@"
fi
fi
if [ -z "$SUDO_ENV" ] ; then
# SUDO is su wrapper / noop

View file

@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed.
-n, --non-interactive, --noninteractive run without asking for user input
--no-self-upgrade do not download updates
--os-packages-only install OS dependencies and exit
-q, --quiet provide only update/error output
-v, --verbose provide more output
All arguments are accepted and forwarded to the Certbot client when run."
@ -52,15 +53,19 @@ for arg in "$@" ; do
HELP=1;;
--noninteractive|--non-interactive)
ASSUME_YES=1;;
--quiet)
QUIET=1;;
--verbose)
VERBOSE=1;;
-[!-]*)
while getopts ":hnv" short_arg $arg; do
while getopts ":hnvq" short_arg $arg; do
case "$short_arg" in
h)
HELP=1;;
n)
ASSUME_YES=1;;
q)
QUIET=1;;
v)
VERBOSE=1;;
esac
@ -262,8 +267,10 @@ UNLIKELY_EOF
fi
if [ -n "$SUDO" ]; then
# SUDO is su wrapper or sudo
echo "Requesting root privileges to run certbot..."
echo " $VENV_BIN/letsencrypt" "$@"
if [ "$QUIET" != 1 ]; then
echo "Requesting root privileges to run certbot..."
echo " $VENV_BIN/letsencrypt" "$@"
fi
fi
if [ -z "$SUDO_ENV" ] ; then
# SUDO is su wrapper / noop

View file

@ -32,28 +32,29 @@ ConfigArgParse==0.10.0 \
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
cryptography==1.2.3 \
--hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \
--hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \
--hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \
--hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \
--hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \
--hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \
--hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \
--hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \
--hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \
--hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \
--hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \
--hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \
--hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \
--hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \
--hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \
--hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \
--hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \
--hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \
--hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \
--hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \
--hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e
cryptography==1.3.4 \
--hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
--hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
--hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
--hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
--hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
--hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
--hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
--hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
--hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
--hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
--hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
--hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
--hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
--hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
--hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
--hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
--hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
--hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
--hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
--hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
--hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
--hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@ -113,9 +114,9 @@ pyasn1==0.1.9 \
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
pyOpenSSL==0.15.1 \
--hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \
--hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672
pyopenssl==16.0.0 \
--hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
--hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
pyRFC3339==1.0 \
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
@ -181,12 +182,12 @@ letsencrypt==0.7.0 \
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
acme==0.8.0 \
--hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \
--hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da
certbot==0.8.0 \
--hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \
--hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314
certbot-apache==0.8.0 \
--hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \
--hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268
acme==0.8.1 \
--hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \
--hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f
certbot==0.8.1 \
--hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \
--hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771
certbot-apache==0.8.1 \
--hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \
--hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed

View file

@ -69,7 +69,6 @@ dev_extras = [
'astroid==1.3.5',
'coverage',
'nose',
'nosexcover',
'pep8',
'pylint==1.4.2', # upstream #248
'tox',

View file

@ -4,7 +4,7 @@
[tox]
skipsdist = true
envlist = py{26,27,33,34,35},py{26,27}-oldest,cover,lint
envlist = py{26,33,34,35},cover,lint
# nosetest -v => more verbose output, allows to detect busy waiting
# loops, especially on Travis