Exemple #1
0
def _add_directive(block, directive, insert_at_top):
    if not isinstance(directive, nginxparser.UnspacedList):
        directive = nginxparser.UnspacedList(directive)
    if _is_whitespace_or_comment(directive):
        # whitespace or comment
        block.append(directive)
        return

    location = _find_location(block, directive[0])

    # Append or prepend 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]

    def can_append(loc, dir_name):
        """ Can we append this directive to the block? """
        return loc is None or (isinstance(dir_name, six.string_types)
                               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(block, included_directive[0])
            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):
        if insert_at_top:
            # Add a newline so the comment doesn't comment
            # out existing directives
            block.insert(0, nginxparser.UnspacedList('\n'))
            block.insert(0, directive)
            comment_directive(block, 0)
        else:
            block.append(directive)
            comment_directive(block, len(block) - 1)
    elif block[location] != directive:
        raise errors.MisconfigurationError(
            err_fmt.format(directive, block[location]))
Exemple #2
0
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
Exemple #3
0
    def duplicate_vhost(self,
                        vhost_template,
                        remove_singleton_listen_params=False,
                        only_directives=None):
        """Duplicate the vhost in the configuration files.

        :param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost
            whose information we copy
        :param bool remove_singleton_listen_params: If we should remove parameters
            from listen directives in the block that can only be used once per address
        :param list only_directives: If it exists, only duplicate the named directives. Only
            looks at first level of depth; does not expand includes.

        :returns: A vhost object for the newly created vhost
        :rtype: :class:`~certbot_nginx.obj.VirtualHost`
        """
        # TODO: https://github.com/certbot/certbot/issues/5185
        # put it in the same file as the template, at the same level
        new_vhost = copy.deepcopy(vhost_template)

        enclosing_block = self.parsed[vhost_template.filep]
        for index in vhost_template.path[:-1]:
            enclosing_block = enclosing_block[index]
        raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]])

        if only_directives is not None:
            new_directives = nginxparser.UnspacedList([])
            for directive in raw_in_parsed[1]:
                if directive and directive[0] in only_directives:
                    new_directives.append(directive)
            raw_in_parsed[1] = new_directives

            self._update_vhost_based_on_new_directives(new_vhost,
                                                       new_directives)

        enclosing_block.append(raw_in_parsed)
        new_vhost.path[-1] = len(enclosing_block) - 1
        if remove_singleton_listen_params:
            for addr in new_vhost.addrs:
                addr.default = False
                addr.ipv6only = False
            for directive in enclosing_block[new_vhost.path[-1]][1]:
                if directive and directive[0] == 'listen':
                    # Exclude one-time use parameters which will cause an error if repeated.
                    # https://nginx.org/en/docs/http/ngx_http_core_module.html#listen
                    exclude = set(('default_server', 'default', 'setfib',
                                   'fastopen', 'backlog', 'rcvbuf', 'sndbuf',
                                   'accept_filter', 'deferred', 'bind',
                                   'ipv6only', 'reuseport', 'so_keepalive'))

                    for param in exclude:
                        # See: github.com/certbot/certbot/pull/6223#pullrequestreview-143019225
                        keys = [x.split('=')[0] for x in directive]
                        if param in keys:
                            del directive[keys.index(param)]
        return new_vhost
Exemple #4
0
    def _mod_config(self, ll_addrs):
        """Modifies Nginx config to include challenge server blocks.

        :param list ll_addrs: list of lists of
            :class:`certbot_nginx.obj.Addr` to apply

        :raises .MisconfigurationError:
            Unable to find a suitable HTTP block in which to include
            authenticator hosts.

        """
        # Add the 'include' statement for the challenges if it doesn't exist
        # already in the main config
        included = False
        include_directive = ['\n', 'include', ' ', self.challenge_conf]
        root = self.configurator.parser.config_root

        bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128']

        main = self.configurator.parser.parsed[root]
        for line in main:
            if line[0] == ['http']:
                body = line[1]
                found_bucket = False
                posn = 0
                for inner_line in body:
                    if inner_line[0] == bucket_directive[1]:
                        if int(inner_line[1]) < int(bucket_directive[3]):
                            body[posn] = bucket_directive
                        found_bucket = True
                    posn += 1
                if not found_bucket:
                    body.insert(0, bucket_directive)
                if include_directive not in body:
                    body.insert(0, include_directive)
                included = True
                break
        if not included:
            raise errors.MisconfigurationError(
                'Certbot could not find an HTTP block to include '
                'TLS-SNI-01 challenges in %s.' % root)
        config = [
            self._make_server_block(pair[0], pair[1])
            for pair in six.moves.zip(self.achalls, ll_addrs)
        ]
        config = nginxparser.UnspacedList(config)

        self.configurator.reverter.register_file_creation(
            True, self.challenge_conf)

        with open(self.challenge_conf, "w") as new_conf:
            nginxparser.dump(config, new_conf)

        logger.debug("Generated server block:\n%s", str(config))
Exemple #5
0
 def test_comment_directive(self):
     # pylint: disable=protected-access
     block = nginxparser.UnspacedList([["\n", "a", " ", "b", "\n"],
                                       ["c", " ", "d"],
                                       ["\n", "e", " ", "f"]])
     from certbot_nginx.parser import _comment_directive, COMMENT_BLOCK
     _comment_directive(block, 1)
     _comment_directive(block, 0)
     self.assertEqual(
         block.spaced,
         [["\n", "a", " ", "b", "\n"], COMMENT_BLOCK, "\n", ["c", " ", "d"],
          COMMENT_BLOCK, ["\n", "e", " ", "f"]])
Exemple #6
0
    def duplicate_vhost(self,
                        vhost_template,
                        remove_singleton_listen_params=False,
                        only_directives=None):
        """Duplicate the vhost in the configuration files.

        :param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost
            whose information we copy
        :param bool remove_singleton_listen_params: If we should remove parameters
            from listen directives in the block that can only be used once per address
        :param list only_directives: If it exists, only duplicate the named directives. Only
            looks at first level of depth; does not expand includes.

        :returns: A vhost object for the newly created vhost
        :rtype: :class:`~certbot_nginx.obj.VirtualHost`
        """
        # TODO: https://github.com/certbot/certbot/issues/5185
        # put it in the same file as the template, at the same level
        new_vhost = copy.deepcopy(vhost_template)

        enclosing_block = self.parsed[vhost_template.filep]
        for index in vhost_template.path[:-1]:
            enclosing_block = enclosing_block[index]
        raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]])

        if only_directives is not None:
            new_directives = nginxparser.UnspacedList([])
            for directive in raw_in_parsed[1]:
                if len(directive) > 0 and directive[0] in only_directives:
                    new_directives.append(directive)
            raw_in_parsed[1] = new_directives

            self._update_vhost_based_on_new_directives(new_vhost,
                                                       new_directives)

        enclosing_block.append(raw_in_parsed)
        new_vhost.path[-1] = len(enclosing_block) - 1
        if remove_singleton_listen_params:
            for addr in new_vhost.addrs:
                addr.default = False
                addr.ipv6only = False
            for directive in enclosing_block[new_vhost.path[-1]][1]:
                if len(directive) > 0 and directive[0] == 'listen':
                    if 'default_server' in directive:
                        del directive[directive.index('default_server')]
                    if 'default' in directive:
                        del directive[directive.index('default')]
                    if 'ipv6only=on' in directive:
                        del directive[directive.index('ipv6only=on')]
        return new_vhost
Exemple #7
0
    def _mod_config(self, ll_addrs):
        """Modifies Nginx config to include challenge server blocks.

        :param list ll_addrs: list of lists of
            :class:`certbot_nginx.obj.Addr` to apply

        :raises .MisconfigurationError:
            Unable to find a suitable HTTP block in which to include
            authenticator hosts.

        """
        # Add the 'include' statement for the challenges if it doesn't exist
        # already in the main config
        included = False
        include_directive = ['\n', 'include', ' ', self.challenge_conf]
        root = self.configurator.parser.loc["root"]

        bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128']

        main = self.configurator.parser.parsed[root]
        for key, body in main:
            if key == ['http']:
                found_bucket = False
                for k, _ in body:
                    if k == bucket_directive[1]:
                        found_bucket = True
                if not found_bucket:
                    body.insert(0, bucket_directive)
                if include_directive not in body:
                    body.insert(0, include_directive)
                included = True
                break
        if not included:
            raise errors.MisconfigurationError(
                'LetsEncrypt could not find an HTTP block to include '
                'TLS-SNI-01 challenges in %s.' % root)

        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)

        with open(self.challenge_conf, "w") as new_conf:
            nginxparser.dump(config, new_conf)
Exemple #8
0
    def _mod_config(self):
        """Modifies Nginx config to include server_names_hash_bucket_size directive
           and server challenge blocks.

        :raises .MisconfigurationError:
            Unable to find a suitable HTTP block in which to include
            authenticator hosts.
        """
        included = False
        include_directive = ['\n', 'include', ' ', self.challenge_conf]
        root = self.configurator.parser.config_root

        bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128']

        main = self.configurator.parser.parsed[root]
        for line in main:
            if line[0] == ['http']:
                body = line[1]
                found_bucket = False
                posn = 0
                for inner_line in body:
                    if inner_line[0] == bucket_directive[1]:
                        if int(inner_line[1]) < int(bucket_directive[3]):
                            body[posn] = bucket_directive
                        found_bucket = True
                    posn += 1
                if not found_bucket:
                    body.insert(0, bucket_directive)
                if include_directive not in body:
                    body.insert(0, include_directive)
                included = True
                break
        if not included:
            raise errors.MisconfigurationError(
                'Certbot could not find a block to include '
                'challenges in %s.' % root)
        config = [
            self._make_or_mod_server_block(achall) for achall in self.achalls
        ]
        config = [x for x in config if x is not None]
        config = nginxparser.UnspacedList(config)
        logger.debug("Generated server block:\n%s", str(config))

        self.configurator.reverter.register_file_creation(
            True, self.challenge_conf)

        with open(self.challenge_conf, "w") as new_conf:
            nginxparser.dump(config, new_conf)
Exemple #9
0
 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"']
                      ])
Exemple #10
0
def _update_or_add_directive(block, directive, insert_at_top):
    if not isinstance(directive, nginxparser.UnspacedList):
        directive = nginxparser.UnspacedList(directive)
    if _is_whitespace_or_comment(directive):
        # whitespace or comment
        block.append(directive)
        return

    location = _find_location(block, directive[0])

    # we can update directive
    if location is not None:
        _update_directive(block, directive, location)
        return

    _add_directive(block, directive, insert_at_top)
Exemple #11
0
def _add_directives(block, directives, replace):
    """Adds or replaces directives in a config block.

    When replace=False, it's an error to try and add a directive that already
    exists in the config block with a conflicting value.

    When replace=True, a directive with the same name MUST already exist in the
    config block, and the first instance will be replaced.

    ..todo :: Find directives that are in included files.

    :param list block: The block to replace in
    :param list directives: The new directives.

    """
    for directive in directives:
        _add_directive(block, directive, replace)
    if block and '\n' not in block[-1]:  # could be "   \n  " or ["\n"] !
        block.append(nginxparser.UnspacedList('\n'))
Exemple #12
0
def _add_directive(block, directive, replace):
    """Adds or replaces a single directive in a config block.

    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.
    for index, line in enumerate(block):
        if len(line) > 0 and line[0] == directive[0]:
            location = index
            break
    if replace:
        if location == -1:
            raise errors.MisconfigurationError(
                'expected directive for %s in the Nginx '
                'config but did not find it.' % directive[0])
        block[location] = directive
    else:
        # Append directive. Fail if the name is not a repeatable directive name,
        # and there is already a copy of that directive with a different value
        # in the config file.
        directive_name = directive[0]
        directive_value = directive[1]
        if location != -1 and directive_name.__str__(
        ) not in repeatable_directives:
            if block[location][1] == directive_value:
                # There's a conflict, but the existing value matches the one we
                # want to insert, so it's fine.
                pass
            else:
                raise errors.MisconfigurationError(
                    'tried to insert directive "%s" but found conflicting "%s".'
                    % (directive, block[location]))
        else:
            block.append(directive)
Exemple #13
0
def _add_directives(directives, replace, block):
    """Adds or replaces directives in a config block.

    When replace=False, it's an error to try and add a nonrepeatable directive that already
    exists in the config block with a conflicting value.

    When replace=True and a directive with the same name already exists in the
    config block, the first instance will be replaced. Otherwise, the directive
    will be added to the config block.

    ..todo :: Find directives that are in included files.

    :param list directives: The new directives.
    :param bool replace: Described above.
    :param list block: The block to replace in

    """
    for directive in directives:
        _add_directive(block, directive, replace)
    if block and '\n' not in block[-1]:  # could be "   \n  " or ["\n"] !
        block.append(nginxparser.UnspacedList('\n'))
Exemple #14
0
 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-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"']
                      ])
Exemple #15
0
def _add_directive(block, directive, replace):
    """Adds or replaces a single directive in a config block.

    See _add_directives for more documentation.

    """
    directive = nginxparser.UnspacedList(directive)
    if len(directive) == 0 or directive[0] == '#':
        # 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)
    if replace:
        if location is None:
            raise errors.MisconfigurationError(
                'expected directive for {0} in the Nginx '
                'config but did not find it.'.format(directive[0]))
        block[location] = directive
        _comment_directive(block, location)
    else:
        # Append directive. Fail if the name is not a repeatable directive name,
        # and there is already a copy of that directive with a different value
        # in the config file.
        directive_name = directive[0]
        directive_value = directive[1]
        if location is None or (isinstance(directive_name, str)
                                and directive_name in REPEATABLE_DIRECTIVES):
            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]))
Exemple #16
0
def _test_block_from_block(block):
    test_block = nginxparser.UnspacedList(block)
    parser.comment_directive(test_block, 0)
    return test_block[:-1]
Exemple #17
0
def _add_directive(block, directive, replace):
    """Adds or replaces a single directive in a config block.

    See _add_directives for more documentation.

    """
    directive = nginxparser.UnspacedList(directive)
    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

    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 not None:
            block[location] = directive
            _comment_directive(block, location)
            return
    # Append directive. Fail if the name is not a repeatable directive name,
    # and there is already a copy of that directive with a different value
    # in the config file.

    # handle flat include files

    directive_name = directive[0]
    def can_append(loc, dir_name):
        """ Can we append this directive to the block? """
        return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES)

    err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".'

    # 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] != directive:
        raise errors.MisconfigurationError(err_fmt.format(directive, block[location]))
Exemple #18
0
def _update_or_add_directives(directives, insert_at_top, block):
    """Adds or replaces directives in a config block."""
    for directive in directives:
        _update_or_add_directive(block, directive, insert_at_top)
    if block and '\n' not in block[-1]:  # could be "   \n  " or ["\n"] !
        block.append(nginxparser.UnspacedList('\n'))