diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/configurator.py python-certbot-nginx-0.14.2/certbot_nginx/configurator.py --- python-certbot-nginx-0.10.2/certbot_nginx/configurator.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/configurator.py 2017-05-25 21:12:46.000000000 +0000 @@ -5,9 +5,11 @@ import shutil import socket import subprocess +import tempfile import time import OpenSSL +import six import zope.interface from acme import challenges @@ -30,16 +32,16 @@ logger = logging.getLogger(__name__) REDIRECT_BLOCK = [[ - ['\n ', 'if', ' ', '($scheme != "https") '], - [['\n ', 'return', ' ', '301 https://$host$request_uri'], + ['\n ', 'if', ' ', '($scheme', ' ', '!=', ' ', '"https") '], + [['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'], '\n '] ], ['\n']] TEST_REDIRECT_BLOCK = [ [ - ['if', '($scheme != "https")'], + ['if', '($scheme', '!=', '"https")'], [ - ['return', '301 https://$host$request_uri'] + ['return', '301', 'https://$host$request_uri'] ] ], ['#', ' managed by Certbot'] @@ -151,15 +153,23 @@ # Make sure configuration is valid self.config_test() - # temp_install must be run before creating the NginxParser - temp_install(self.mod_ssl_conf) - self.parser = parser.NginxParser( - self.conf('server-root'), self.mod_ssl_conf) + + self.parser = parser.NginxParser(self.conf('server-root')) + + install_ssl_options_conf(self.mod_ssl_conf) # Set Version if self.version is None: self.version = self.get_version() + # Prevent two Nginx plugins from modifying a config at once + try: + util.lock_dir_until_exit(self.conf('server-root')) + except (OSError, errors.LockError): + logger.debug('Encountered error:', exc_info=True) + raise errors.PluginError( + 'Unable to lock %s', self.conf('server-root')) + # Entry point in main.py for installing cert def deploy_cert(self, domain, cert_path, key_path, chain_path=None, fullchain_path=None): @@ -262,7 +272,7 @@ """ if not matches: return None - elif matches[0]['rank'] in xrange(2, 6): + elif matches[0]['rank'] in six.moves.range(2, 6): # Wildcard match - need to find the longest one rank = matches[0]['rank'] wildcards = [x for x in matches if x['rank'] == rank] @@ -403,25 +413,7 @@ except (socket.error, socket.herror, socket.timeout): continue - return self._get_filtered_names(all_names) - - def _get_filtered_names(self, all_names): - """Removes names that aren't considered valid by Let's Encrypt. - - :param set all_names: all names found in the Nginx configuration - - :returns: all found names that are considered valid by LE - :rtype: set - - """ - filtered_names = set() - for name in all_names: - try: - filtered_names.add(util.enforce_le_validity(name)) - except errors.ConfigurationError as error: - logger.debug('Not suggesting name "%s"', name) - logger.debug(error) - return filtered_names + return util.get_filtered_names(all_names) def _get_snakeoil_paths(self): # TODO: generate only once @@ -460,14 +452,11 @@ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() - # the options file doesn't have a newline at the beginning, but there - # needs to be one when it's dropped into the file 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']] + - self.parser.loc["ssl_options"]) + ['\n ', 'include', ' ', self.mod_ssl_conf]]) self.parser.add_server_directives( vhost, ssl_block, replace=False) @@ -645,10 +634,11 @@ proc = subprocess.Popen( [self.conf('ctl'), "-c", self.nginx_conf, "-V"], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE, + universal_newlines=True) text = proc.communicate()[1] # nginx prints output to stderr except (OSError, ValueError) as error: - logging.debug(error, exc_info=True) + logger.debug(error, exc_info=True) raise errors.PluginError( "Unable to run %s -V" % self.conf('ctl')) @@ -684,7 +674,7 @@ "Configures Nginx to authenticate and install HTTPS.{0}" "Server root: {root}{0}" "Version: {version}".format( - os.linesep, root=self.parser.loc["root"], + os.linesep, root=self.parser.config_root, version=".".join(str(i) for i in self.version)) ) @@ -836,7 +826,7 @@ self.restart() -def nginx_restart(nginx_ctl, nginx_conf="/etc/nginx.conf"): +def nginx_restart(nginx_ctl, nginx_conf): """Restarts the Nginx Server. .. todo:: Nginx restart is fatal if the configuration references @@ -847,22 +837,22 @@ """ try: - proc = subprocess.Popen([nginx_ctl, "-c", nginx_conf, "-s", "reload"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = proc.communicate() + proc = subprocess.Popen([nginx_ctl, "-c", nginx_conf, "-s", "reload"]) + proc.communicate() if proc.returncode != 0: # Maybe Nginx isn't running - nginx_proc = subprocess.Popen([nginx_ctl, "-c", nginx_conf], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = nginx_proc.communicate() - - if nginx_proc.returncode != 0: - # Enter recovery routine... - raise errors.MisconfigurationError( - "nginx restart failed:\n%s\n%s" % (stdout, stderr)) + # Write to temporary files instead of piping because of communication issues on Arch + # https://github.com/certbot/certbot/issues/4324 + with tempfile.TemporaryFile() as out: + with tempfile.TemporaryFile() as err: + nginx_proc = subprocess.Popen([nginx_ctl, "-c", nginx_conf], + stdout=out, stderr=err) + nginx_proc.communicate() + if nginx_proc.returncode != 0: + # Enter recovery routine... + raise errors.MisconfigurationError( + "nginx restart failed:\n%s\n%s" % (out.read(), err.read())) except (OSError, ValueError): raise errors.MisconfigurationError("nginx restart failed") @@ -872,8 +862,8 @@ time.sleep(1) -def temp_install(options_ssl): - """Temporary install for convenience.""" +def install_ssl_options_conf(options_ssl): + """Copy Certbot's SSL options file into the system's config dir if required.""" # 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) diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/nginxparser.py python-certbot-nginx-0.14.2/certbot_nginx/nginxparser.py --- python-certbot-nginx-0.10.2/certbot_nginx/nginxparser.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/nginxparser.py 2017-05-25 21:12:46.000000000 +0000 @@ -2,11 +2,9 @@ # Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed) import copy import logging -import string from pyparsing import ( - Literal, White, Word, alphanums, CharsNotIn, Combine, Forward, Group, - Optional, OneOrMore, Regex, ZeroOrMore) + Literal, White, Forward, Group, Optional, OneOrMore, QuotedString, Regex, ZeroOrMore, Combine) from pyparsing import stringEnd from pyparsing import restOfLine @@ -14,73 +12,42 @@ class RawNginxParser(object): # pylint: disable=expression-not-assigned + # pylint: disable=pointless-statement """A class that parses nginx configuration with pyparsing.""" # constants - space = Optional(White()) - nonspace = Regex(r"\S+") + space = Optional(White()).leaveWhitespace() + required_space = White().leaveWhitespace() + left_bracket = Literal("{").suppress() - right_bracket = space.leaveWhitespace() + Literal("}").suppress() + right_bracket = space + Literal("}").suppress() semicolon = Literal(";").suppress() - key = Word(alphanums + "_/+-.") - dollar_var = Combine(Literal('$') + Regex(r"[^\{\};,\s]+")) - condition = Regex(r"\(.+\)") - # Matches anything that is not a special character, and ${SHELL_VARS}, AND - # any chars in single or double quotes - # All of these COULD be upgraded to something like - # https://stackoverflow.com/a/16130746 - dquoted = Regex(r'(\".*\")') - squoted = Regex(r"(\'.*\')") - nonspecial = Regex(r"[^\{\};,]") - varsub = Regex(r"(\$\{\w+\})") - # nonspecial nibbles one character at a time, but the other objects take - # precedence. We use ZeroOrMore to allow entries like "break ;" to be - # parsed as assignments - value = Combine(ZeroOrMore(dquoted | squoted | varsub | nonspecial)) - - location = CharsNotIn("{};," + string.whitespace) - # modifier for location uri [ = | ~ | ~* | ^~ ] - modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") - - # rules - comment = space + Literal('#') + restOfLine + dquoted = QuotedString('"', multiline=True, unquoteResults=False, escChar='\\') + squoted = QuotedString("'", multiline=True, unquoteResults=False, escChar='\\') + quoted = dquoted | squoted + head_tokenchars = Regex(r"[^{};\s'\"]") # if (last_space) + tail_tokenchars = Regex(r"(\$\{)|[^{;\s]") # else + tokenchars = Combine(head_tokenchars + ZeroOrMore(tail_tokenchars)) + paren_quote_extend = Combine(quoted + Literal(')') + ZeroOrMore(tail_tokenchars)) + # note: ')' allows extension, but then we fall into else, not last_space. - 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 - charset_map_statement = space + Literal("charset_map") + space + value + space + value - - map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space - # This is NOT an accurate way to parse nginx map entries; it's almost - # certianly too permissive and may be wrong in other ways, but it should - # preserve things correctly in mmmmost or all cases. - # - # - I can neither prove nor disprove that it is corect wrt all escaped - # semicolon situations - # Addresses https://github.com/fatiherikli/nginxparser/issues/19 - map_pattern = Regex(r'".*"') | Regex(r"'.*'") | nonspace - map_entry = space + map_pattern + space + value + space + semicolon - map_block = Group( - Group(map_statement).leaveWhitespace() + - left_bracket + - Group(ZeroOrMore(Group(comment | map_entry)) + space).leaveWhitespace() + - right_bracket) + token = paren_quote_extend | tokenchars | quoted - block = Forward() + whitespace_token_group = space + token + ZeroOrMore(required_space + token) + space + assignment = whitespace_token_group + semicolon - # key could for instance be "server" or "http", or "location" (in which case - # location_statement needs to have a non-empty location) + comment = space + Literal('#') + restOfLine - block_begin = (Group(space + key + location_statement) ^ - Group(if_statement) ^ - Group(charset_map_statement)).leaveWhitespace() + block = Forward() - block_innards = Group(ZeroOrMore(Group(comment | assignment) | block | map_block) - + space).leaveWhitespace() + # order matters! see issue 518, and also http { # server { \n} + contents = Group(comment) | Group(block) | Group(assignment) - block << Group(block_begin + left_bracket + block_innards + right_bracket) + block_begin = Group(whitespace_token_group) + block_innards = Group(ZeroOrMore(contents) + space).leaveWhitespace() + block << block_begin + left_bracket + block_innards + right_bracket - script = OneOrMore(Group(comment | assignment) ^ block ^ map_block) + space + stringEnd + script = OneOrMore(contents) + space + stringEnd script.parseWithTabs().leaveWhitespace() def __init__(self, source): @@ -107,30 +74,23 @@ if isinstance(b0, str): yield b0 continue - b = copy.deepcopy(b0) - if spacey(b[0]): - yield b.pop(0) # indentation - if not b: + item = copy.deepcopy(b0) + if spacey(item[0]): + yield item.pop(0) # indentation + if not item: continue - key, values = b.pop(0), b.pop(0) - if isinstance(key, list): - yield "".join(key) + '{' - for parameter in values: + if isinstance(item[0], list): # block + yield "".join(item.pop(0)) + '{' + for parameter in item.pop(0): for line in self.__iter__([parameter]): # negate "for b0 in blocks" yield line yield '}' - else: - 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 + ';' + else: # not a block - list of strings + semicolon = ";" + if isinstance(item[0], str) and item[0].strip() == '#': # comment + semicolon = "" + yield "".join(item) + semicolon def __str__(self): """Return the parsed block as a string.""" @@ -143,7 +103,7 @@ def loads(source): """Parses from a string. - :param str souce: The string to parse + :param str source: The string to parse :returns: The parsed tree :rtype: list diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/obj.py python-certbot-nginx-0.14.2/certbot_nginx/obj.py --- python-certbot-nginx-0.10.2/certbot_nginx/obj.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/obj.py 2017-05-25 21:12:46.000000000 +0000 @@ -1,6 +1,8 @@ """Module contains classes used by the Nginx Configurator.""" import re +import six + from certbot.plugins import common REDIRECT_DIRECTIVES = ['return', 'rewrite'] @@ -97,6 +99,11 @@ def __repr__(self): return "Addr(" + self.__str__() + ")" + def __hash__(self): + # Python 3 requires explicit overridden for __hash__ + # See certbot-apache/certbot_apache/obj.py for more information + return super(Addr, self).__hash__() + def super_eq(self, other): """Check ip/port equality, with IPv6 support. """ @@ -147,13 +154,15 @@ self.path = path def __str__(self): - addr_str = ", ".join(str(addr) for addr in self.addrs) + addr_str = ", ".join(str(addr) for addr in sorted(self.addrs, key=str)) + # names might be a set, and it has different representations in Python + # 2 and 3. Force it to be a list here for consistent outputs return ("file: %s\n" "addrs: %s\n" "names: %s\n" "ssl: %s\n" "enabled: %s" % (self.filep, addr_str, - self.names, self.ssl, self.enabled)) + list(self.names), self.ssl, self.enabled)) def __repr__(self): return "VirtualHost(" + self.__str__().replace("\n", ", ") + ")\n" @@ -161,7 +170,7 @@ def __eq__(self, other): if isinstance(other, self.__class__): return (self.filep == other.filep and - list(self.addrs) == list(other.addrs) and + sorted(self.addrs, key=str) == sorted(other.addrs, key=str) and self.names == other.names and self.ssl == other.ssl and self.enabled == other.enabled and @@ -181,7 +190,7 @@ def contains_list(self, test): """Determine if raw server block contains test list at top level """ - for i in xrange(0, len(self.raw) - len(test)): + for i in six.moves.range(0, len(self.raw) - len(test)): if self.raw[i:i + len(test)] == test: return True return False diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/options-ssl-nginx.conf python-certbot-nginx-0.14.2/certbot_nginx/options-ssl-nginx.conf --- python-certbot-nginx-0.10.2/certbot_nginx/options-ssl-nginx.conf 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/options-ssl-nginx.conf 2017-05-25 21:12:46.000000000 +0000 @@ -4,4 +4,4 @@ ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; -ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"; +ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"; diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/parser.py python-certbot-nginx-0.14.2/certbot_nginx/parser.py --- python-certbot-nginx-0.10.2/certbot_nginx/parser.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/parser.py 2017-05-25 21:12:46.000000000 +0000 @@ -24,10 +24,10 @@ """ - def __init__(self, root, ssl_options): + def __init__(self, root): self.parsed = {} self.root = os.path.abspath(root) - self.loc = self._set_locations(ssl_options) + self.config_root = self._find_config_root() # Parse nginx.conf and included files. # TODO: Check sites-available/ as well. For now, the configurator does @@ -39,7 +39,7 @@ """ self.parsed = {} - self._parse_recursively(self.loc["root"]) + self._parse_recursively(self.config_root) def _parse_recursively(self, filepath): """Parses nginx config files recursively by looking at 'include' @@ -110,7 +110,7 @@ srv = servers[filename] # workaround undefined loop var in lambdas # Find all the server blocks - _do_for_subarray(tree, lambda x: x[0] == ['server'], + _do_for_subarray(tree, lambda x: len(x) >= 2 and x[0] == ['server'], lambda x, y: srv.append((x[1], y))) # Find 'include' statements in server blocks and append their trees @@ -205,44 +205,12 @@ trees.append(parsed) except IOError: logger.warning("Could not open file: %s", item) - except pyparsing.ParseException: - logger.debug("Could not parse file: %s", item) + except pyparsing.ParseException as err: + logger.debug("Could not parse file: %s due to %s", item, err) return trees - def _parse_ssl_options(self, ssl_options): - if ssl_options is not None: - try: - with open(ssl_options) as _file: - return nginxparser.load(_file).spaced - except IOError: - logger.warn("Missing NGINX TLS options file: %s", ssl_options) - except pyparsing.ParseBaseException: - logger.debug("Could not parse file: %s", ssl_options) - return [] - - def _set_locations(self, ssl_options): - """Set default location for directives. - - Locations are given as file_paths - .. todo:: Make sure that files are included - - """ - root = self._find_config_root() - default = root - - nginx_temp = os.path.join(self.root, "nginx_ports.conf") - if os.path.isfile(nginx_temp): - listen = nginx_temp - name = nginx_temp - else: - listen = default - name = default - - return {"root": root, "default": default, "listen": listen, - "name": name, "ssl_options": self._parse_ssl_options(ssl_options)} - def _find_config_root(self): - """Find the Nginx Configuration Root file.""" + """Return the Nginx Configuration Root file.""" location = ['nginx.conf'] for name in location: @@ -298,9 +266,9 @@ """ server = vhost.raw for directive in server: - if not directive or len(directive) < 2: + if not directive: continue - elif directive[0] == 'ssl' and directive[1] == 'on': + elif _is_ssl_on_directive(directive): return True return False @@ -342,8 +310,18 @@ vhost.names = parsed_server['names'] vhost.raw = new_server except errors.MisconfigurationError as err: - raise errors.MisconfigurationError("Problem in %s: %s" % (filename, err.message)) + raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err))) +def _parse_ssl_options(ssl_options): + if ssl_options is not None: + try: + with open(ssl_options) as _file: + return nginxparser.load(_file) + except IOError: + logger.warn("Missing NGINX TLS options file: %s", ssl_options) + except pyparsing.ParseBaseException as err: + logger.debug("Could not parse file: %s due to %s", ssl_options, err) + return [] def _do_for_subarray(entry, condition, func, path=None): """Executes a function for a subarray of a nested array if it matches @@ -468,17 +446,17 @@ len(entry) == 2 and entry[0] == 'include' and isinstance(entry[1], str)) +def _is_ssl_on_directive(entry): + """Checks if an nginx parsed entry is an 'ssl on' directive. -def _get_servernames(names): - """Turns a server_name string into a list of server names - - :param str names: server names - :rtype: list + :param list entry: the parsed entry + :returns: Whether it's an 'ssl on' directive + :rtype: bool """ - whitespace_re = re.compile(r'\s+') - names = re.sub(whitespace_re, ' ', names) - return names.split(' ') + return (isinstance(entry, list) and + len(entry) == 2 and entry[0] == 'ssl' and + entry[1] == 'on') def _add_directives(block, directives, replace): """Adds or replaces directives in a config block. @@ -501,11 +479,11 @@ block.append(nginxparser.UnspacedList('\n')) -REPEATABLE_DIRECTIVES = set(['server_name', 'listen', 'include']) +INCLUDE = 'include' +REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE]) COMMENT = ' managed by Certbot' COMMENT_BLOCK = [' ', '#', COMMENT] - def _comment_directive(block, location): """Add a comment to the end of the line at location.""" next_entry = block[location + 1] if location + 1 < len(block) else None @@ -521,6 +499,28 @@ if next_entry is not None and "\n" not in next_entry: block.insert(location + 2, '\n') +def _comment_out_directive(block, location, include_location): + """Comment out the line at location, with a note of explanation.""" + comment_message = ' duplicated in {0}'.format(include_location) + # add the end comment + # create a dumpable object out of block[location] (so it includes the ;) + directive = block[location] + new_dir_block = nginxparser.UnspacedList([]) # just a wrapper + new_dir_block.append(directive) + dumped = nginxparser.dumps(new_dir_block) + commented = dumped + ' #' + comment_message # add the comment directly to the one-line string + new_dir = nginxparser.loads(commented) # reload into UnspacedList + + # add the beginning comment + insert_location = 0 + if new_dir[0].spaced[0] != new_dir[0][0]: # if there's whitespace at the beginning + insert_location = 1 + new_dir[0].spaced.insert(insert_location, "# ") # comment out the line + new_dir[0].spaced.append(";") # directly add in the ;, because now dumping won't work properly + dumped = nginxparser.dumps(new_dir) + new_dir = nginxparser.loads(dumped) # reload into an UnspacedList + + block[location] = new_dir[0] # set the now-single-line-comment directive back in place def _add_directive(block, directive, replace): """Adds or replaces a single directive in a config block. @@ -529,15 +529,23 @@ """ directive = nginxparser.UnspacedList(directive) - if len(directive) == 0 or directive[0] == '#': + def is_whitespace_or_comment(directive): + """Is this directive either a whitespace or comment directive?""" + return len(directive) == 0 or directive[0] == '#' + if is_whitespace_or_comment(directive): # whitespace or comment block.append(directive) return - # Find the index of a config line where the name of the directive matches - # the name of the directive we want to add. If no line exists, use None. - location = next((index for index, line in enumerate(block) - if line and line[0] == directive[0]), None) + def find_location(direc): + """ Find the index of a config line where the name of the directive matches + the name of the directive we want to add. If no line exists, use None. + """ + return next((index for index, line in enumerate(block) \ + if line and line[0] == direc[0]), None) + + location = find_location(directive) + if replace: if location is None: raise errors.MisconfigurationError( @@ -549,16 +557,39 @@ # Append directive. Fail if the name is not a repeatable directive name, # and there is already a copy of that directive with a different value # in the config file. + + # handle flat include files + directive_name = directive[0] - directive_value = directive[1] - if location is None or (isinstance(directive_name, str) and - directive_name in REPEATABLE_DIRECTIVES): + def can_append(loc, dir_name): + """ Can we append this directive to the block? """ + return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES) + + err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".' + + # Give a better error message about the specific directive than Nginx's "fail to restart" + if directive_name == INCLUDE: + # in theory, we might want to do this recursively, but in practice, that's really not + # necessary because we know what file we're talking about (and if we don't recurse, we + # just give a worse error message) + included_directives = _parse_ssl_options(directive[1]) + + for included_directive in included_directives: + included_dir_loc = find_location(included_directive) + included_dir_name = included_directive[0] + if not is_whitespace_or_comment(included_directive) \ + and not can_append(included_dir_loc, included_dir_name): + if block[included_dir_loc] != included_directive: + raise errors.MisconfigurationError(err_fmt.format(included_directive, + block[included_dir_loc])) + else: + _comment_out_directive(block, included_dir_loc, directive[1]) + + if can_append(location, directive_name): block.append(directive) _comment_directive(block, len(block) - 1) - elif block[location][1] != directive_value: - raise errors.MisconfigurationError( - 'tried to insert directive "{0}" but found ' - 'conflicting "{1}".'.format(directive, block[location])) + elif block[location] != directive: + raise errors.MisconfigurationError(err_fmt.format(directive, block[location])) def _apply_global_addr_ssl(addr_to_ssl, parsed_server): """Apply global sslishness information to the parsed server block @@ -585,14 +616,14 @@ if not directive: continue if directive[0] == 'listen': - addr = obj.Addr.fromstring(directive[1]) - parsed_server['addrs'].add(addr) - if addr.ssl: - parsed_server['ssl'] = True + addr = obj.Addr.fromstring(" ".join(directive[1:])) + if addr: + parsed_server['addrs'].add(addr) + if addr.ssl: + parsed_server['ssl'] = True elif directive[0] == 'server_name': - parsed_server['names'].update( - _get_servernames(directive[1])) - elif directive[0] == 'ssl' and directive[1] == 'on': + parsed_server['names'].update(directive[1:]) + elif _is_ssl_on_directive(directive): parsed_server['ssl'] = True apply_ssl_to_all_addrs = True diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/configurator_test.py python-certbot-nginx-0.14.2/certbot_nginx/tests/configurator_test.py --- python-certbot-nginx-0.10.2/certbot_nginx/tests/configurator_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/configurator_test.py 2017-05-25 21:12:46.000000000 +0000 @@ -12,6 +12,7 @@ from certbot import achallenges from certbot import errors +from certbot.tests import util as certbot_test_util from certbot_nginx import obj from certbot_nginx import parser @@ -27,12 +28,13 @@ super(NginxConfiguratorTest, self).setUp() self.config = util.get_nginx_configurator( - self.config_path, self.config_dir, self.work_dir) + self.config_path, self.config_dir, self.work_dir, self.logs_dir) def tearDown(self): shutil.rmtree(self.temp_dir) shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) + shutil.rmtree(self.logs_dir) @mock.patch("certbot_nginx.configurator.util.exe_exists") def test_prepare_no_install(self, mock_exe_exists): @@ -43,8 +45,6 @@ def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) self.assertEqual(8, len(self.config.parser.parsed)) - # ensure we successfully parsed a file for ssl_options - self.assertTrue(self.config.parser.loc["ssl_options"]) @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") @@ -64,6 +64,23 @@ self.config.prepare() self.assertEqual((1, 6, 2), self.config.version) + def test_prepare_locked(self): + server_root = self.config.conf("server-root") + self.config.config_test = mock.Mock() + os.remove(os.path.join(server_root, ".certbot.lock")) + certbot_test_util.lock_and_call(self._test_prepare_locked, server_root) + + @mock.patch("certbot_nginx.configurator.util.exe_exists") + def _test_prepare_locked(self, unused_exe_exists): + try: + self.config.prepare() + except errors.PluginError as err: + err_msg = str(err) + self.assertTrue("lock" in err_msg) + self.assertTrue(self.config.conf("server-root") in err_msg) + else: # pragma: no cover + self.fail("Exception wasn't raised!") + @mock.patch("certbot_nginx.configurator.socket.gethostbyaddr") def test_get_all_names(self, mock_gethostbyaddr): mock_gethostbyaddr.return_value = ('155.225.50.69.nephoscale.net', [], []) @@ -93,7 +110,7 @@ None, [0]) self.config.parser.add_server_directives( mock_vhost, - [['listen', ' ', '5001 ssl']], + [['listen', ' ', '5001', ' ', 'ssl']], replace=False) self.config.save() @@ -104,7 +121,7 @@ ['listen', '127.0.0.1'], ['server_name', '.example.com'], ['server_name', 'example.*'], - ['listen', '5001 ssl'], + ['listen', '5001', 'ssl'], ['#', parser.COMMENT]]]], parsed[0]) @@ -204,13 +221,13 @@ ['server_name', '.example.com'], ['server_name', 'example.*'], - ['listen', '5001 ssl'], + ['listen', '5001', 'ssl'], ['ssl_certificate', 'example/fullchain.pem'], - ['ssl_certificate_key', 'example/key.pem']] + - util.filter_comments(self.config.parser.loc["ssl_options"]) + ['ssl_certificate_key', 'example/key.pem'], + ['include', self.config.mod_ssl_conf]] ]], parsed_example_conf) - self.assertEqual([['server_name', 'somename alias another.alias']], + self.assertEqual([['server_name', 'somename', 'alias', 'another.alias']], parsed_server_conf) self.assertTrue(util.contains_at_depth( parsed_nginx_conf, @@ -221,11 +238,11 @@ ['include', 'server.conf'], [['location', '/'], [['root', 'html'], - ['index', 'index.html index.htm']]], - ['listen', '5001 ssl'], + ['index', 'index.html', 'index.htm']]], + ['listen', '5001', 'ssl'], ['ssl_certificate', '/etc/nginx/fullchain.pem'], - ['ssl_certificate_key', '/etc/nginx/key.pem']] + - util.filter_comments(self.config.parser.loc["ssl_options"]) + ['ssl_certificate_key', '/etc/nginx/key.pem'], + ['include', self.config.mod_ssl_conf]] ], 2)) @@ -246,10 +263,10 @@ ['server_name', 'summer.com'], ['listen', '80'], - ['listen', '5001 ssl'], + ['listen', '5001', 'ssl'], ['ssl_certificate', 'summer/fullchain.pem'], - ['ssl_certificate_key', 'summer/key.pem']] + - util.filter_comments(self.config.parser.loc["ssl_options"]) + ['ssl_certificate_key', 'summer/key.pem'], + ['include', self.config.mod_ssl_conf]] ], parsed_migration_conf[0]) @@ -261,13 +278,13 @@ # Note: As more challenges are offered this will have to be expanded achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), + chall=challenges.TLSSNI01(token=b"kNdwjwOeX0I_A8DXt9Msmg"), uri="https://ca.org/chall0_uri", status=messages.Status("pending"), ), domain="localhost", account_key=self.rsa512jwk) achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.TLSSNI01(token="m8TdO1qik4JVFtgPPurJmg"), + chall=challenges.TLSSNI01(token=b"m8TdO1qik4JVFtgPPurJmg"), uri="https://ca.org/chall1_uri", status=messages.Status("pending"), ), domain="example.com", account_key=self.rsa512jwk) @@ -407,8 +424,8 @@ # Test that we successfully add a redirect when there is # a listen directive expected = [ - ['if', '($scheme != "https") '], - [['return', '301 https://$host$request_uri']] + ['if', '($scheme', '!=', '"https") '], + [['return', '301', 'https://$host$request_uri']] ] example_conf = self.config.parser.abs_path('sites-enabled/example.com') diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/nginxparser_test.py python-certbot-nginx-0.14.2/certbot_nginx/tests/nginxparser_test.py --- python-certbot-nginx-0.10.2/certbot_nginx/tests/nginxparser_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/nginxparser_test.py 2017-05-25 21:12:46.000000000 +0000 @@ -25,15 +25,15 @@ 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'], []]]) + self.assertEqual(parsed, [['location', ' ', '/foo'], []]) parsed = RawNginxParser.block.parseString('foo { bar foo ; }').asList() - self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo '], ' ']]]) + self.assertEqual(parsed, [['foo', ' '], [[' ', 'bar', ' ', 'foo', ' '], ' ']]) def test_nested_blocks(self): parsed = RawNginxParser.block.parseString('foo { bar {} }').asList() - block, content = FIRST(parsed) + block, content = parsed self.assertEqual(FIRST(content), [[' ', 'bar', ' '], []]) self.assertEqual(FIRST(block), 'foo') @@ -72,8 +72,8 @@ [['user', 'www-data'], [['http'], [[['server'], [ - ['listen', '*:80 default_server ssl'], - ['server_name', '*.www.foo.com *.www.example.com'], + ['listen', '*:80', 'default_server', 'ssl'], + ['server_name', '*.www.foo.com', '*.www.example.com'], ['root', '/home/ubuntu/sites/foo/'], [['location', '/status'], [ [['types'], [['image/jpeg', 'jpg']]], @@ -97,17 +97,35 @@ [['server'], [['server_name', 'with.if'], [['location', '~', '^/services/.+$'], - [[['if', '($request_filename ~* \\.(ttf|woff)$)'], - [['add_header', 'Access-Control-Allow-Origin "*"']]]]]]], + [[['if', '($request_filename', '~*', '\\.(ttf|woff)$)'], + [['add_header', 'Access-Control-Allow-Origin', '"*"']]]]]]], [['server'], [['server_name', 'with.complicated.headers'], [['location', '~*', '\\.(?:gif|jpe?g|png)$'], - [['add_header', 'Pragma public'], + [['add_header', 'Pragma', 'public'], ['add_header', - 'Cache-Control \'public, must-revalidate, proxy-revalidate\'' - ' "test,;{}" foo'], + 'Cache-Control', '\'public, must-revalidate, proxy-revalidate\'', + '"test,;{}"', 'foo'], ['blah', '"hello;world"'], - ['try_files', '$uri @rewrites']]]]]]) + ['try_files', '$uri', '@rewrites']]]]]]) + + def test_parse_from_file3(self): + with open(util.get_data_filename('multiline_quotes.conf')) as handle: + parsed = util.filter_comments(load(handle)) + self.assertEqual( + parsed, + [[['http'], + [[['server'], + [['listen', '*:443'], + [['location', '/'], + [['body_filter_by_lua', + '\'ngx.ctx.buffered = (ngx.ctx.buffered or "")' + ' .. string.sub(ngx.arg[1], 1, 1000)\n' + ' ' + 'if ngx.arg[2] then\n' + ' ' + 'ngx.var.resp_body = ngx.ctx.buffered\n' + ' end\'']]]]]]]]) def test_abort_on_parse_failure(self): with open(util.get_data_filename('broken.conf')) as handle: @@ -117,7 +135,7 @@ with open(util.get_data_filename('nginx.conf')) as handle: parsed = load(handle) parsed[-1][-1].append(UnspacedList([['server'], - [['listen', ' ', '443 ssl'], + [['listen', ' ', '443', ' ', 'ssl'], ['server_name', ' ', 'localhost'], ['ssl_certificate', ' ', 'cert.pem'], ['ssl_certificate_key', ' ', 'cert.key'], @@ -126,9 +144,9 @@ ['ssl_ciphers', ' ', 'HIGH:!aNULL:!MD5'], [['location', ' ', '/'], [['root', ' ', 'html'], - ['index', ' ', 'index.html index.htm']]]]])) + ['index', ' ', 'index.html', ' ', 'index.htm']]]]])) - with tempfile.TemporaryFile() as f: + with tempfile.TemporaryFile(mode='w+t') as f: dump(parsed, f) f.seek(0) parsed_new = load(f) @@ -138,7 +156,7 @@ with open(util.get_data_filename('minimalistic_comments.conf')) as handle: parsed = load(handle) - with tempfile.TemporaryFile() as f: + with tempfile.TemporaryFile(mode='w+t') as f: dump(parsed, f) f.seek(0) parsed_new = load(f) @@ -161,10 +179,177 @@ parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }') self.assertEqual(parsed, [ - [['if', '($http_accept ~* "webp")'], - [['set', '$webp "true"']]] + [['if', '($http_accept', '~*', '"webp")'], + [['set', '$webp', '"true"']]] ]) + def test_comment_in_block(self): + parsed = loads("""http { + # server{ + }""") + + self.assertEqual(parsed, [ + [['http'], + [['#', ' server{']]] + ]) + + def test_access_log(self): + # see issue #3798 + parsed = loads('access_log syslog:server=unix:/dev/log,facility=auth,' + 'tag=nginx_post,severity=info custom;') + + self.assertEqual(parsed, [ + ['access_log', + 'syslog:server=unix:/dev/log,facility=auth,tag=nginx_post,severity=info', + 'custom'] + ]) + + def test_add_header(self): + # see issue #3798 + parsed = loads('add_header Cache-Control no-cache,no-store,must-revalidate,max-age=0;') + + self.assertEqual(parsed, [ + ['add_header', 'Cache-Control', 'no-cache,no-store,must-revalidate,max-age=0'] + ]) + + def test_map_then_assignment_in_block(self): + # see issue #3798 + test_str = """http { + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + "~Opera Mini" 1; + *.example.com 1; + } + one; + }""" + parsed = loads(test_str) + self.assertEqual(parsed, [ + [['http'], [ + [['map', '$http_upgrade', '$connection_upgrade'], [ + ['default', 'upgrade'], + ["''", 'close'], + ['"~Opera Mini"', '1'], + ['*.example.com', '1'] + ]], + ['one'] + ]] + ]) + + def test_variable_name(self): + parsed = loads('try_files /typo3temp/tx_ncstaticfilecache/' + '$host${request_uri}index.html @nocache;') + + self.assertEqual(parsed, [ + ['try_files', + '/typo3temp/tx_ncstaticfilecache/$host${request_uri}index.html', + '@nocache'] + ]) + + def test_weird_blocks(self): + test = r""" + if ($http_user_agent ~ MSIE) { + rewrite ^(.*)$ /msie/$1 break; + } + + if ($http_cookie ~* "id=([^;]+)(?:;|$)") { + set $id $1; + } + + if ($request_method = POST) { + return 405; + } + + if ($request_method) { + return 403; + } + + if ($args ~ post=140){ + rewrite ^ http://example.com/; + } + + location ~ ^/users/(.+\.(?:gif|jpe?g|png))$ { + alias /data/w3/images/$1; + } + """ + parsed = loads(test) + self.assertEqual(parsed, [[['if', '($http_user_agent', '~', 'MSIE)'], + [['rewrite', '^(.*)$', '/msie/$1', 'break']]], + [['if', '($http_cookie', '~*', '"id=([^;]+)(?:;|$)")'], [['set', '$id', '$1']]], + [['if', '($request_method', '=', 'POST)'], [['return', '405']]], + [['if', '($request_method)'], + [['return', '403']]], [['if', '($args', '~', 'post=140)'], + [['rewrite', '^', 'http://example.com/']]], + [['location', '~', '^/users/(.+\\.(?:gif|jpe?g|png))$'], + [['alias', '/data/w3/images/$1']]]] + ) + + def test_edge_cases(self): + # quotes + parsed = loads(r'"hello\""; # blah "heh heh"') + self.assertEqual(parsed, [['"hello\\""'], ['#', ' blah "heh heh"']]) + + # empty var as block + parsed = loads(r"${}") + self.assertEqual(parsed, [[['$'], []]]) + + # if with comment + parsed = loads("""if ($http_cookie ~* "id=([^;]+)(?:;|$)") { # blah ) + }""") + self.assertEqual(parsed, [[['if', '($http_cookie', '~*', '"id=([^;]+)(?:;|$)")'], + [['#', ' blah )']]]]) + + # end paren + test = """ + one"test"; + ("two"); + "test")red; + "test")"blue"; + "test")"three; + (one"test")one; + one"; + one"test; + one"test"one; + """ + parsed = loads(test) + self.assertEqual(parsed, [ + ['one"test"'], + ['("two")'], + ['"test")red'], + ['"test")"blue"'], + ['"test")"three'], + ['(one"test")one'], + ['one"'], + ['one"test'], + ['one"test"one'] + ]) + self.assertRaises(ParseException, loads, r'"test"one;') # fails + self.assertRaises(ParseException, loads, r'"test;') # fails + + # newlines + test = """ + server_name foo.example.com bar.example.com \ + baz.example.com qux.example.com; + server_name foo.example.com bar.example.com + baz.example.com qux.example.com; + """ + parsed = loads(test) + self.assertEqual(parsed, [ + ['server_name', 'foo.example.com', 'bar.example.com', + 'baz.example.com', 'qux.example.com'], + ['server_name', 'foo.example.com', 'bar.example.com', + 'baz.example.com', 'qux.example.com'] + ]) + + # variable weirdness + parsed = loads("directive $var;") + self.assertEqual(parsed, [['directive', '$var']]) + self.assertRaises(ParseException, loads, "server {server_name test.com};") + self.assertRaises(ParseException, loads, "directive ${var};") + self.assertEqual(loads("blag${dfgdfg};"), [['blag${dfgdfg}']]) + self.assertRaises(ParseException, loads, "blag${dfgdf{g};") + + class TestUnspacedList(unittest.TestCase): """Test the UnspacedList data structure""" def setUp(self): @@ -219,18 +404,18 @@ ['\n ', 'listen', ' ', '127.0.0.1'], ['\n ', 'server_name', ' ', '.example.com'], ['\n ', 'server_name', ' ', 'example.*'], '\n', - ['listen', ' ', '5001 ssl']]) + ['listen', ' ', '5001', ' ', 'ssl']]) x.insert(5, "FROGZ") self.assertEqual(x, [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], ['server_name', '.example.com'], ['server_name', 'example.*'], - ['listen', '5001 ssl'], 'FROGZ']) + ['listen', '5001', 'ssl'], 'FROGZ']) self.assertEqual(x.spaced, [['\n ', 'listen', ' ', '69.50.225.155:9000'], ['\n ', 'listen', ' ', '127.0.0.1'], ['\n ', 'server_name', ' ', '.example.com'], ['\n ', 'server_name', ' ', 'example.*'], '\n', - ['listen', ' ', '5001 ssl'], + ['listen', ' ', '5001', ' ', 'ssl'], 'FROGZ']) def test_rawlists(self): diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/obj_test.py python-certbot-nginx-0.14.2/certbot_nginx/tests/obj_test.py --- python-certbot-nginx-0.10.2/certbot_nginx/tests/obj_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/obj_test.py 2017-05-25 21:12:46.000000000 +0000 @@ -108,8 +108,8 @@ from certbot_nginx.obj import Addr raw1 = [ ['listen', '69.50.225.155:9000'], - [['if', '($scheme != "https") '], - [['return', '301 https://$host$request_uri']] + [['if', '($scheme', '!=', '"https") '], + [['return', '301', 'https://$host$request_uri']] ], ['#', ' managed by Certbot'] ] @@ -119,8 +119,8 @@ set(['localhost']), raw1, []) raw2 = [ ['listen', '69.50.225.155:9000'], - [['if', '($scheme != "https") '], - [['return', '301 https://$host$request_uri']] + [['if', '($scheme', '!=', '"https") '], + [['return', '301', 'https://$host$request_uri']] ] ] self.vhost2 = VirtualHost( @@ -129,7 +129,7 @@ set(['localhost']), raw2, []) raw3 = [ ['listen', '69.50.225.155:9000'], - ['rewrite', '^(.*)$ $scheme://www.domain.com$1 permanent;'] + ['rewrite', '^(.*)$', '$scheme://www.domain.com$1', 'permanent'] ] self.vhost3 = VirtualHost( "filep", @@ -158,7 +158,7 @@ def test_str(self): stringified = '\n'.join(['file: filep', 'addrs: localhost', - "names: set(['localhost'])", 'ssl: False', + "names: ['localhost']", 'ssl: False', 'enabled: False']) self.assertEqual(stringified, str(self.vhost1)) @@ -181,7 +181,9 @@ ['#', ' managed by Certbot'], ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'], ['#', ' managed by Certbot'], - [['if', '($scheme != "https")'], [['return', '301 https://$host$request_uri']]], + [['if', '($scheme', '!=', '"https")'], + [['return', '301', 'https://$host$request_uri']] + ], ['#', ' managed by Certbot'], []] vhost_haystack = VirtualHost( "filp", @@ -195,7 +197,9 @@ ['#', ' managed by Certbot'], ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'], ['#', ' managed by Certbot'], - [['if', '($scheme != "https")'], [['return', '302 https://$host$request_uri']]], + [['if', '($scheme', '!=', '"https")'], + [['return', '302', 'https://$host$request_uri']] + ], ['#', ' managed by Certbot'], []] vhost_bad_haystack = VirtualHost( "filp", diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/parser_test.py python-certbot-nginx-0.14.2/certbot_nginx/tests/parser_test.py --- python-certbot-nginx-0.10.2/certbot_nginx/tests/parser_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/parser_test.py 2017-05-25 21:12:46.000000000 +0000 @@ -13,7 +13,7 @@ from certbot_nginx.tests import util -class NginxParserTest(util.NginxTest): +class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods """Nginx Parser Test.""" def setUp(self): @@ -27,22 +27,22 @@ def test_root_normalized(self): path = os.path.join(self.temp_dir, "etc_nginx/////" "ubuntu_nginx/../../etc_nginx") - nparser = parser.NginxParser(path, None) + nparser = parser.NginxParser(path) self.assertEqual(nparser.root, self.config_path) def test_root_absolute(self): - nparser = parser.NginxParser(os.path.relpath(self.config_path), None) + nparser = parser.NginxParser(os.path.relpath(self.config_path)) self.assertEqual(nparser.root, self.config_path) def test_root_no_trailing_slash(self): - nparser = parser.NginxParser(self.config_path + os.path.sep, None) + nparser = parser.NginxParser(self.config_path + os.path.sep) self.assertEqual(nparser.root, self.config_path) def test_load(self): """Test recursive conf file parsing. """ - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) nparser.load() self.assertEqual(set([nparser.abs_path(x) for x in ['foo.conf', 'nginx.conf', 'server.conf', @@ -52,7 +52,7 @@ 'sites-enabled/sslon.com', 'sites-enabled/globalssl.com']]), set(nparser.parsed.keys())) - self.assertEqual([['server_name', 'somename alias another.alias']], + self.assertEqual([['server_name', 'somename', 'alias', 'another.alias']], nparser.parsed[nparser.abs_path('server.conf')]) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], @@ -62,13 +62,13 @@ 'sites-enabled/example.com')]) def test_abs_path(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) self.assertEqual('/etc/nginx/*', nparser.abs_path('/etc/nginx/*')) self.assertEqual(os.path.join(self.config_path, 'foo/bar/'), nparser.abs_path('foo/bar/')) def test_filedump(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) nparser.filedump('test', lazy=False) # pylint: disable=protected-access parsed = nparser._parse_files(nparser.abs_path( @@ -106,7 +106,7 @@ self.assertEqual(paths, result) def test_get_vhosts_global_ssl(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) vhosts = nparser.get_vhosts() vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'), @@ -117,7 +117,7 @@ self.assertEqual(vhost, globalssl_com) def test_get_vhosts(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) vhosts = nparser.get_vhosts() vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'), @@ -125,13 +125,13 @@ False, True, set(['localhost', r'~^(www\.)?(example|bar)\.']), - [], [9, 1, 9]) + [], [10, 1, 9]) vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'), [obj.Addr('somename', '8080', False, False), obj.Addr('', '8000', False, False)], False, True, set(['somename', 'another.alias', 'alias']), - [], [9, 1, 12]) + [], [10, 1, 12]) vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'), [obj.Addr('69.50.225.155', '9000', False, False), @@ -161,32 +161,32 @@ self.assertEqual(vhost2, somename) def test_has_ssl_on_directive(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) mock_vhost = obj.VirtualHost(None, None, None, None, None, [['listen', 'myhost default_server'], ['server_name', 'www.example.org'], [['location', '/'], [['root', 'html'], ['index', 'index.html index.htm']]] ], None) self.assertFalse(nparser.has_ssl_on_directive(mock_vhost)) - mock_vhost.raw = [['listen', '*:80 default_server ssl'], - ['server_name', '*.www.foo.com *.www.example.com'], + mock_vhost.raw = [['listen', '*:80', 'default_server', 'ssl'], + ['server_name', '*.www.foo.com', '*.www.example.com'], ['root', '/home/ubuntu/sites/foo/']] self.assertFalse(nparser.has_ssl_on_directive(mock_vhost)) mock_vhost.raw = [['listen', '80 ssl'], - ['server_name', '*.www.foo.com *.www.example.com']] + ['server_name', '*.www.foo.com', '*.www.example.com']] self.assertFalse(nparser.has_ssl_on_directive(mock_vhost)) mock_vhost.raw = [['listen', '80'], ['ssl', 'on'], - ['server_name', '*.www.foo.com *.www.example.com']] + ['server_name', '*.www.foo.com', '*.www.example.com']] self.assertTrue(nparser.has_ssl_on_directive(mock_vhost)) def test_add_server_directives(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'), None, None, None, set(['localhost', r'~^(www\.)?(example|bar)\.']), - None, [9, 1, 9]) + None, [10, 1, 9]) nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['\n ', 'ssl_certificate', ' ', '/etc/ssl/cert.pem']], @@ -230,8 +230,35 @@ ['ssl_certificate', '/etc/ssl/cert2.pem']], replace=False) + def test_comment_is_repeatable(self): + nparser = parser.NginxParser(self.config_path) + example_com = nparser.abs_path('sites-enabled/example.com') + mock_vhost = obj.VirtualHost(example_com, + None, None, None, + set(['.example.com', 'example.*']), + None, [0]) + nparser.add_server_directives(mock_vhost, + [['\n ', '#', ' ', 'what a nice comment']], + replace=False) + nparser.add_server_directives(mock_vhost, + [['\n ', 'include', ' ', + nparser.abs_path('comment_in_file.conf')]], + replace=False) + from certbot_nginx.parser import COMMENT + self.assertEqual(nparser.parsed[example_com], + [[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + ['#', ' ', 'what a nice comment'], + [], + ['include', nparser.abs_path('comment_in_file.conf')], + ['#', COMMENT], + []]]] +) + def test_replace_server_directives(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) target = set(['.example.com', 'example.*']) filep = nparser.abs_path('sites-enabled/example.com') mock_vhost = obj.VirtualHost(filep, None, None, None, target, None, [0]) @@ -302,6 +329,33 @@ COMMENT_BLOCK, ["\n", "e", " ", "f"]]) + def test_comment_out_directive(self): + server_block = nginxparser.loads(""" + server { + listen 80; + root /var/www/html; + index star.html; + + server_name *.functorkitten.xyz; + ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + + ssl_prefer_server_ciphers on; + }""") + block = server_block[0][1] + from certbot_nginx.parser import _comment_out_directive + _comment_out_directive(block, 4, "blah1") + _comment_out_directive(block, 5, "blah2") + _comment_out_directive(block, 6, "blah3") + self.assertEqual(block.spaced, [ + ['\n ', 'listen', ' ', '80'], + ['\n ', 'root', ' ', '/var/www/html'], + ['\n ', 'index', ' ', 'star.html'], + ['\n\n ', 'server_name', ' ', '*.functorkitten.xyz'], + ['\n ', '#', ' ssl_session_timeout 1440m; # duplicated in blah1'], + [' ', '#', ' ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # duplicated in blah2'], + ['\n\n ', '#', ' ssl_prefer_server_ciphers on; # duplicated in blah3'], + '\n ']) + def test_parse_server_raw_ssl(self): server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443'] @@ -309,7 +363,7 @@ self.assertFalse(server['ssl']) server = parser._parse_server_raw([ #pylint: disable=protected-access - ['listen', '443 ssl'] + ['listen', '443', 'ssl'] ]) self.assertTrue(server['ssl']) @@ -323,29 +377,19 @@ ]) self.assertTrue(server['ssl']) + def test_parse_server_raw_unix(self): + server = parser._parse_server_raw([ #pylint: disable=protected-access + ['listen', 'unix:/var/run/nginx.sock'] + ]) + self.assertEqual(len(server['addrs']), 0) + def test_parse_server_global_ssl_applied(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) + nparser = parser.NginxParser(self.config_path) server = nparser.parse_server([ ['listen', '443'] ]) self.assertTrue(server['ssl']) - def test_ssl_options_should_be_parsed_ssl_directives(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) - self.assertEqual(nginxparser.UnspacedList(nparser.loc["ssl_options"]), - [['ssl_session_cache', 'shared:le_nginx_SSL:1m'], - ['ssl_session_timeout', '1440m'], - ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], - ['ssl_prefer_server_ciphers', 'on'], - ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-'+ - 'AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256'+ - '-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384'+ - ' ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384'+ - ' ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-'+ - 'AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM'+ - '-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-'+ - 'AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] - ]) if __name__ == "__main__": unittest.main() # pragma: no cover diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/comment_in_file.conf python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/comment_in_file.conf --- python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/comment_in_file.conf 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/comment_in_file.conf 2017-05-25 21:12:46.000000000 +0000 @@ -0,0 +1 @@ +# a comment inside a file \ No newline at end of file diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/multiline_quotes.conf python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/multiline_quotes.conf --- python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/multiline_quotes.conf 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/multiline_quotes.conf 2017-05-25 21:12:46.000000000 +0000 @@ -0,0 +1,16 @@ +# Test nginx configuration file with multiline quoted strings. +# Good example of usage for multilined quoted values is when +# using Openresty's Lua directives and you wish to keep the +# inline Lua code readable. +http { + server { + listen *:443; # because there should be no other port open. + + location / { + body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) + if ngx.arg[2] then + ngx.var.resp_body = ngx.ctx.buffered + end'; + } + } +} diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/nginx.conf python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/nginx.conf --- python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/nginx.conf 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/nginx.conf 2017-05-25 21:12:46.000000000 +0000 @@ -14,6 +14,9 @@ worker_connections 1024; } +empty { +} + include foo.conf; http { diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules --- python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules 2017-05-25 21:12:46.000000000 +0000 @@ -67,7 +67,7 @@ #################################### MainRule "str:&#" "msg: utf7/8 encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1400; MainRule "str:%U" "msg: M$ encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1401; -MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither mulipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402; +MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither multipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402; ############################# ## File uploads: 1500-1600 ## diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf --- python-certbot-nginx-0.10.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf 2017-05-25 21:12:46.000000000 +0000 @@ -36,7 +36,7 @@ AA D084; # capital Ukrainian YE AB C2AB; # left-pointing double angle quotation mark AC C2AC; # not sign - AD C2AD; # soft hypen + AD C2AD; # soft hyphen AE C2AE; # (R) AF D087; # capital Ukrainian YI diff -Nru python-certbot-nginx-0.10.2/certbot_nginx/tests/tls_sni_01_test.py python-certbot-nginx-0.14.2/certbot_nginx/tests/tls_sni_01_test.py --- python-certbot-nginx-0.10.2/certbot_nginx/tests/tls_sni_01_test.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx/tests/tls_sni_01_test.py 2017-05-25 21:12:46.000000000 +0000 @@ -3,6 +3,7 @@ import shutil import mock +import six from acme import challenges @@ -23,25 +24,25 @@ achalls = [ achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"), + challenges.TLSSNI01(token=b"kNdwjwOeX0I_A8DXt9Msmg"), "pending"), domain="www.example.com", account_key=account_key), achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( challenges.TLSSNI01( - token="\xba\xa9\xda?=1.5.5 setuptools>=1.0 zope.interface -mock [docs] Sphinx>=1.0 diff -Nru python-certbot-nginx-0.10.2/certbot_nginx.egg-info/SOURCES.txt python-certbot-nginx-0.14.2/certbot_nginx.egg-info/SOURCES.txt --- python-certbot-nginx-0.10.2/certbot_nginx.egg-info/SOURCES.txt 2017-01-26 02:59:21.000000000 +0000 +++ python-certbot-nginx-0.14.2/certbot_nginx.egg-info/SOURCES.txt 2017-05-25 21:13:09.000000000 +0000 @@ -1,6 +1,7 @@ LICENSE.txt MANIFEST.in README.rst +setup.cfg setup.py certbot_nginx/__init__.py certbot_nginx/configurator.py @@ -24,10 +25,12 @@ certbot_nginx/tests/tls_sni_01_test.py certbot_nginx/tests/util.py certbot_nginx/tests/testdata/etc_nginx/broken.conf +certbot_nginx/tests/testdata/etc_nginx/comment_in_file.conf certbot_nginx/tests/testdata/etc_nginx/edge_cases.conf certbot_nginx/tests/testdata/etc_nginx/foo.conf certbot_nginx/tests/testdata/etc_nginx/mime.types certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.conf +certbot_nginx/tests/testdata/etc_nginx/multiline_quotes.conf certbot_nginx/tests/testdata/etc_nginx/nginx.conf certbot_nginx/tests/testdata/etc_nginx/server.conf certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default diff -Nru python-certbot-nginx-0.10.2/debian/changelog python-certbot-nginx-0.14.2/debian/changelog --- python-certbot-nginx-0.10.2/debian/changelog 2017-01-26 06:19:50.000000000 +0000 +++ python-certbot-nginx-0.14.2/debian/changelog 2017-08-08 18:37:14.000000000 +0000 @@ -1,3 +1,29 @@ +python-certbot-nginx (0.14.2-0ubuntu0.17.04.1) zesty; urgency=medium + + * Backport to Zesty (LP: #1640978). + + -- Robie Basak Tue, 08 Aug 2017 19:26:51 +0100 + +python-certbot-nginx (0.14.2-1) experimental; urgency=medium + + * Team upload. + * New upstream release. + * Add dep8 smoke test. + + -- Robie Basak Fri, 26 May 2017 14:25:07 +0100 + +python-certbot-nginx (0.12.0-1) experimental; urgency=medium + + * New upstream release. + + -- Harlan Lieberman-Berg Tue, 21 Mar 2017 23:21:31 -0400 + +python-certbot-nginx (0.11.1-1) unstable; urgency=medium + + * New upstream release. + + -- Harlan Lieberman-Berg Sun, 19 Feb 2017 14:15:14 -0500 + python-certbot-nginx (0.10.2-1) unstable; urgency=medium * New upstream release. diff -Nru python-certbot-nginx-0.10.2/debian/control python-certbot-nginx-0.14.2/debian/control --- python-certbot-nginx-0.10.2/debian/control 2017-01-26 06:19:50.000000000 +0000 +++ python-certbot-nginx-0.14.2/debian/control 2017-08-08 18:37:14.000000000 +0000 @@ -1,14 +1,15 @@ Source: python-certbot-nginx Section: python Priority: extra -Maintainer: Debian Let's Encrypt +Maintainer: Ubuntu Developers +XSBC-Original-Maintainer: Debian Let's Encrypt Uploaders: Harlan Lieberman-Berg , Francois Marier Build-Depends: debhelper (>= 9~), dh-python, python-all (>= 2.6.6.3~), python-configargparse (>= 0.10.0), - python-certbot (>= 0.10.2~), + python-certbot (>= 0.14.2~), python-mock, python-openssl (>= 0.13), python-parsedatetime (>= 1.3), @@ -31,7 +32,7 @@ Package: python-certbot-nginx Architecture: all Depends: nginx, - certbot (>= 0.10.2~), + certbot (>= 0.14.2~), ${misc:Depends}, ${python:Depends} Suggests: python-certbot-nginx-doc diff -Nru python-certbot-nginx-0.10.2/debian/control.in python-certbot-nginx-0.14.2/debian/control.in --- python-certbot-nginx-0.10.2/debian/control.in 2016-10-23 23:29:40.000000000 +0000 +++ python-certbot-nginx-0.14.2/debian/control.in 2017-08-08 18:26:51.000000000 +0000 @@ -1,7 +1,8 @@ Source: python-certbot-nginx Section: python Priority: extra -Maintainer: Debian Let's Encrypt +Maintainer: Ubuntu Developers +XSBC-Original-Maintainer: Debian Let's Encrypt Uploaders: Harlan Lieberman-Berg , Francois Marier Build-Depends: debhelper (>= 9~), diff -Nru python-certbot-nginx-0.10.2/debian/tests/control python-certbot-nginx-0.14.2/debian/tests/control --- python-certbot-nginx-0.10.2/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ python-certbot-nginx-0.14.2/debian/tests/control 2017-07-07 10:46:10.000000000 +0000 @@ -0,0 +1,8 @@ +# rbasak: while it's not exactly pretty, here's a pretty decent +# one line test that certbot and its plugins and dependencies are +# installed correctly +# certbot plugins --prepare | grep -q apache && certbot --help +# nginx | grep -q nginx-ctl + +Test-Command: certbot --help nginx | grep -q nginx-ctl +Depends: python-certbot-nginx diff -Nru python-certbot-nginx-0.10.2/PKG-INFO python-certbot-nginx-0.14.2/PKG-INFO --- python-certbot-nginx-0.10.2/PKG-INFO 2017-01-26 02:59:21.000000000 +0000 +++ python-certbot-nginx-0.14.2/PKG-INFO 2017-05-25 21:13:09.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: certbot-nginx -Version: 0.10.2 +Version: 0.14.2 Summary: Nginx plugin for Certbot Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -17,6 +17,11 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup diff -Nru python-certbot-nginx-0.10.2/setup.cfg python-certbot-nginx-0.14.2/setup.cfg --- python-certbot-nginx-0.10.2/setup.cfg 2017-01-26 02:59:21.000000000 +0000 +++ python-certbot-nginx-0.14.2/setup.cfg 2017-05-25 21:13:09.000000000 +0000 @@ -1,3 +1,6 @@ +[bdist_wheel] +universal = 1 + [egg_info] tag_build = tag_date = 0 diff -Nru python-certbot-nginx-0.10.2/setup.py python-certbot-nginx-0.14.2/setup.py --- python-certbot-nginx-0.10.2/setup.py 2017-01-26 02:58:36.000000000 +0000 +++ python-certbot-nginx-0.14.2/setup.py 2017-05-25 21:12:46.000000000 +0000 @@ -4,12 +4,13 @@ from setuptools import find_packages -version = '0.10.2' +version = '0.14.2' # Please update tox.ini when modifying dependency version requirements install_requires = [ 'acme=={0}'.format(version), 'certbot=={0}'.format(version), + 'mock', 'PyOpenSSL', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? # For pkg_resources. >=1.0 so pip resolves it to a version cryptography @@ -18,11 +19,6 @@ 'zope.interface', ] -if sys.version_info < (2, 7): - install_requires.append('mock<1.1.0') -else: - install_requires.append('mock') - docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', @@ -46,6 +42,11 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup',