././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.384151 certbot-nginx-1.21.0/0000755000076500000240000000000000000000000013027 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/LICENSE.txt0000644000076500000240000002732000000000000014656 0ustar00bmwstaff Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Incorporating code from nginxparser Copyright 2014 Fatih Erikli Licensed MIT Text of Apache License ====================== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Text of MIT License =================== Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/MANIFEST.in0000644000076500000240000000026200000000000014565 0ustar00bmwstaffinclude LICENSE.txt include README.rst recursive-include tests * recursive-include certbot_nginx/_internal/tls_configs *.conf global-exclude __pycache__ global-exclude *.py[cod] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.384202 certbot-nginx-1.21.0/PKG-INFO0000644000076500000240000000212100000000000014120 0ustar00bmwstaffMetadata-Version: 2.1 Name: certbot-nginx Version: 1.21.0 Summary: Nginx plugin for Certbot Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Plugins Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities Requires-Python: >=3.6 License-File: LICENSE.txt UNKNOWN ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/README.rst0000644000076500000240000000003100000000000014510 0ustar00bmwstaffNginx plugin for Certbot ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888465.3771682 certbot-nginx-1.21.0/certbot_nginx/0000755000076500000240000000000000000000000015674 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/__init__.py0000644000076500000240000000003400000000000020002 0ustar00bmwstaff"""Certbot nginx plugin.""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888465.3789976 certbot-nginx-1.21.0/certbot_nginx/_internal/0000755000076500000240000000000000000000000017647 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/__init__.py0000644000076500000240000000006400000000000021760 0ustar00bmwstaff"""Certbot nginx plugin internal implementation.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/configurator.py0000644000076500000240000014565000000000000022736 0ustar00bmwstaff# pylint: disable=too-many-lines """Nginx Configuration""" from distutils.version import LooseVersion import logging import re import socket import subprocess import tempfile import time from typing import Dict from typing import List from typing import Optional from typing import Set from typing import Text from typing import Tuple import OpenSSL import pkg_resources from acme import challenges from acme import crypto_util as acme_crypto_util from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot import util from certbot.display import util as display_util from certbot.compat import os from certbot.plugins import common from certbot_nginx._internal import constants from certbot_nginx._internal import display_ops from certbot_nginx._internal import http_01 from certbot_nginx._internal import nginxparser from certbot_nginx._internal import obj # pylint: disable=unused-import from certbot_nginx._internal import parser NAME_RANK = 0 START_WILDCARD_RANK = 1 END_WILDCARD_RANK = 2 REGEX_RANK = 3 NO_SSL_MODIFIER = 4 logger = logging.getLogger(__name__) class NginxConfigurator(common.Installer, interfaces.Authenticator): """Nginx configurator. .. todo:: Add proper support for comments in the config. Currently, config files modified by the configurator will lose all their comments. :ivar config: Configuration. :type config: certbot.configuration.NamespaceConfig :ivar parser: Handles low level parsing :type parser: :class:`~certbot_nginx._internal.parser` :ivar str save_notes: Human-readable config change notes :ivar reverter: saves and reverts checkpoints :type reverter: :class:`certbot.reverter.Reverter` :ivar tup version: version of Nginx """ description = "Nginx Web Server plugin" DEFAULT_LISTEN_PORT = '80' # SSL directives that Certbot can add when installing a new certificate. SSL_DIRECTIVES = ['ssl_certificate', 'ssl_certificate_key', 'ssl_dhparam'] @classmethod def add_parser_arguments(cls, add): default_server_root = _determine_default_server_root() add("server-root", default=constants.CLI_DEFAULTS["server_root"], help="Nginx server root directory. (default: %s)" % default_server_root) add("ctl", default=constants.CLI_DEFAULTS["ctl"], help="Path to the " "'nginx' binary, used for 'configtest' and retrieving nginx " "version number.") add("sleep-seconds", default=constants.CLI_DEFAULTS["sleep_seconds"], type=int, help="Number of seconds to wait for nginx configuration changes " "to apply when reloading.") @property def nginx_conf(self): """Nginx config file path.""" return os.path.join(self.conf("server_root"), "nginx.conf") def __init__(self, *args, **kwargs): """Initialize an Nginx Configurator. :param tup version: version of Nginx as a tuple (1, 4, 7) (used mostly for unittesting) :param tup openssl_version: version of OpenSSL linked to Nginx as a tuple (1, 4, 7) (used mostly for unittesting) """ version = kwargs.pop("version", None) openssl_version = kwargs.pop("openssl_version", None) super().__init__(*args, **kwargs) # Files to save self.save_notes = "" # For creating new vhosts if no names match self.new_vhost: Optional[obj.VirtualHost] = None # List of vhosts configured per wildcard domain on this run. # used by deploy_cert() and enhance() self._wildcard_vhosts: Dict[str, List[obj.VirtualHost]] = {} self._wildcard_redirect_vhosts: Dict[str, List[obj.VirtualHost]] = {} # Add number of outstanding challenges self._chall_out = 0 # These will be set in the prepare function self.version = version self.openssl_version = openssl_version self._enhance_func = {"redirect": self._enable_redirect, "ensure-http-header": self._set_http_header, "staple-ocsp": self._enable_ocsp_stapling} self.reverter.recovery_routine() self.parser: parser.NginxParser @property def mod_ssl_conf_src(self): """Full absolute path to SSL configuration file source.""" # Why all this complexity? Well, we want to support Mozilla's intermediate # recommendations. But TLS1.3 is only supported by newer versions of Nginx. # And as for session tickets, our ideal is to turn them off across the board. # But! Turning them off at all is only supported with new enough versions of # Nginx. And older versions of OpenSSL have a bug that leads to browser errors # given certain configurations. While we'd prefer to have forward secrecy, we'd # rather fail open than error out. Unfortunately, Nginx can be compiled against # many versions of OpenSSL. So we have to check both for the two different features, # leading to four different combinations of options. # For a complete history, check out https://github.com/certbot/certbot/issues/7322 use_tls13 = self.version >= (1, 13, 0) session_tix_off = self.version >= (1, 5, 9) and self.openssl_version and\ LooseVersion(self.openssl_version) >= LooseVersion('1.0.2l') if use_tls13: if session_tix_off: config_filename = "options-ssl-nginx.conf" else: config_filename = "options-ssl-nginx-tls13-session-tix-on.conf" else: if session_tix_off: config_filename = "options-ssl-nginx-tls12-only.conf" else: config_filename = "options-ssl-nginx-old.conf" return pkg_resources.resource_filename( "certbot_nginx", os.path.join("_internal", "tls_configs", config_filename)) @property def mod_ssl_conf(self): """Full absolute path to SSL configuration file.""" return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) @property def updated_mod_ssl_conf_digest(self): """Full absolute path to digest of updated SSL configuration file.""" return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST) def install_ssl_options_conf(self, options_ssl, options_ssl_digest): """Copy Certbot's SSL options file into the system's config dir if required.""" return common.install_version_controlled_file(options_ssl, options_ssl_digest, self.mod_ssl_conf_src, constants.ALL_SSL_OPTIONS_HASHES) # This is called in determine_authenticator and determine_installer def prepare(self): """Prepare the authenticator/installer. :raises .errors.NoInstallationError: If Nginx ctl cannot be found :raises .errors.MisconfigurationError: If Nginx is misconfigured """ # Verify Nginx is installed if not util.exe_exists(self.conf('ctl')): raise errors.NoInstallationError( "Could not find a usable 'nginx' binary. Ensure nginx exists, " "the binary is executable, and your PATH is set correctly.") # Make sure configuration is valid self.config_test() self.parser = parser.NginxParser(self.conf('server-root')) # Set Version if self.version is None: self.version = self.get_version() if self.openssl_version is None: self.openssl_version = self._get_openssl_version() self.install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest) self.install_ssl_dhparams() # 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 {0}'.format(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): """Deploys certificate to specified virtual host. .. note:: Aborts if the vhost is missing ssl_certificate or ssl_certificate_key. .. note:: This doesn't save the config files! :raises errors.PluginError: When unable to deploy certificate due to a lack of directives or configuration """ if not fullchain_path: raise errors.PluginError( "The nginx plugin currently requires --fullchain-path to " "install a certificate.") vhosts = self.choose_vhosts(domain, create_if_no_match=True) for vhost in vhosts: self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path) display_util.notify("Successfully deployed certificate for {} to {}" .format(domain, vhost.filep)) def _deploy_cert(self, vhost, cert_path, key_path, chain_path, fullchain_path): # pylint: disable=unused-argument """ Helper function for deploy_cert() that handles the actual deployment this exists because we might want to do multiple deployments per domain originally passed for deploy_cert(). This is especially true with wildcard certificates """ cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path], ['\n ', 'ssl_certificate_key', ' ', key_path]] self.parser.update_or_add_server_directives(vhost, cert_directives) logger.info("Deploying Certificate to VirtualHost %s", vhost.filep) self.save_notes += ("Changed vhost at %s with addresses of %s\n" % (vhost.filep, ", ".join(str(addr) for addr in vhost.addrs))) self.save_notes += "\tssl_certificate %s\n" % fullchain_path self.save_notes += "\tssl_certificate_key %s\n" % key_path def _choose_vhosts_wildcard(self, domain, prefer_ssl, no_ssl_filter_port=None): """Prompts user to choose vhosts to install a wildcard certificate for""" if prefer_ssl: vhosts_cache = self._wildcard_vhosts preference_test = lambda x: x.ssl else: vhosts_cache = self._wildcard_redirect_vhosts preference_test = lambda x: not x.ssl # Caching! if domain in vhosts_cache: # Vhosts for a wildcard domain were already selected return vhosts_cache[domain] # Get all vhosts whether or not they are covered by the wildcard domain vhosts = self.parser.get_vhosts() # Go through the vhosts, making sure that we cover all the names # present, but preferring the SSL or non-SSL vhosts filtered_vhosts = {} for vhost in vhosts: # Ensure we're listening non-sslishly on no_ssl_filter_port if no_ssl_filter_port is not None: if not self._vhost_listening_on_port_no_ssl(vhost, no_ssl_filter_port): continue for name in vhost.names: if preference_test(vhost): # Prefer either SSL or non-SSL vhosts filtered_vhosts[name] = vhost elif name not in filtered_vhosts: # Add if not in list previously filtered_vhosts[name] = vhost # Only unique VHost objects dialog_input = set(filtered_vhosts.values()) # Ask the user which of names to enable, expect list of names back return_vhosts = display_ops.select_vhost_multiple(list(dialog_input)) for vhost in return_vhosts: if domain not in vhosts_cache: vhosts_cache[domain] = [] vhosts_cache[domain].append(vhost) return return_vhosts ####################### # Vhost parsing methods ####################### def _choose_vhost_single(self, target_name): matches = self._get_ranked_matches(target_name) vhosts = [x for x in [self._select_best_name_match(matches)] if x is not None] return vhosts def choose_vhosts(self, target_name, create_if_no_match=False): """Chooses a virtual host based on the given domain name. .. note:: This makes the vhost SSL-enabled if it isn't already. Follows Nginx's server block selection rules preferring blocks that are already SSL. .. todo:: This should maybe return list if no obvious answer is presented. :param str target_name: domain name :param bool create_if_no_match: If we should create a new vhost from default when there is no match found. If we can't choose a default, raise a MisconfigurationError. :returns: ssl vhosts associated with name :rtype: list of :class:`~certbot_nginx._internal.obj.VirtualHost` """ if util.is_wildcard_domain(target_name): # Ask user which VHosts to support. vhosts = self._choose_vhosts_wildcard(target_name, prefer_ssl=True) else: vhosts = self._choose_vhost_single(target_name) if not vhosts: if create_if_no_match: # result will not be [None] because it errors on failure vhosts = [self._vhost_from_duplicated_default(target_name, True, str(self.config.https_port))] else: # No matches. Raise a misconfiguration error. raise errors.MisconfigurationError( ("Cannot find a VirtualHost matching domain %s. " "In order for Certbot to correctly perform the challenge " "please add a corresponding server_name directive to your " "nginx configuration for every domain on your certificate: " "https://nginx.org/en/docs/http/server_names.html") % (target_name)) # Note: if we are enhancing with ocsp, vhost should already be ssl. for vhost in vhosts: if not vhost.ssl: self._make_server_ssl(vhost) return vhosts def ipv6_info(self, port): """Returns tuple of booleans (ipv6_active, ipv6only_present) ipv6_active is true if any server block listens ipv6 address in any port ipv6only_present is true if ipv6only=on option exists in any server block ipv6 listen directive for the specified port. :param str port: Port to check ipv6only=on directive for :returns: Tuple containing information if IPv6 is enabled in the global configuration, and existence of ipv6only directive for specified port :rtype: tuple of type (bool, bool) """ # port should be a string, but it's easy to mess up, so let's # make sure it is one port = str(port) vhosts = self.parser.get_vhosts() ipv6_active = False ipv6only_present = False for vh in vhosts: for addr in vh.addrs: if addr.ipv6: ipv6_active = True if addr.ipv6only and addr.get_port() == port: ipv6only_present = True return (ipv6_active, ipv6only_present) def _vhost_from_duplicated_default(self, domain: str, allow_port_mismatch: bool, port: str ) -> obj.VirtualHost: """if allow_port_mismatch is False, only server blocks with matching ports will be used as a default server block template. """ assert self.parser is not None # prepare should already have been called here if self.new_vhost is None: default_vhost = self._get_default_vhost(domain, allow_port_mismatch, port) self.new_vhost = self.parser.duplicate_vhost(default_vhost, remove_singleton_listen_params=True) self.new_vhost.names = set() self._add_server_name_to_vhost(self.new_vhost, domain) return self.new_vhost def _add_server_name_to_vhost(self, vhost, domain): vhost.names.add(domain) name_block = [['\n ', 'server_name']] for name in vhost.names: name_block[0].append(' ') name_block[0].append(name) self.parser.update_or_add_server_directives(vhost, name_block) def _get_default_vhost(self, domain, allow_port_mismatch, port): """Helper method for _vhost_from_duplicated_default; see argument documentation there""" vhost_list = self.parser.get_vhosts() # if one has default_server set, return that one all_default_vhosts = [] port_matching_vhosts = [] for vhost in vhost_list: for addr in vhost.addrs: if addr.default: all_default_vhosts.append(vhost) if self._port_matches(port, addr.get_port()): port_matching_vhosts.append(vhost) break if len(port_matching_vhosts) == 1: return port_matching_vhosts[0] elif len(all_default_vhosts) == 1 and allow_port_mismatch: return all_default_vhosts[0] # TODO: present a list of vhosts for user to choose from raise errors.MisconfigurationError("Could not automatically find a matching server" " block for %s. Set the `server_name` directive to use the Nginx installer." % domain) def _get_ranked_matches(self, target_name): """Returns a ranked list of vhosts that match target_name. The ranking gives preference to SSL vhosts. :param str target_name: The name to match :returns: list of dicts containing the vhost, the matching name, and the numerical rank :rtype: list """ vhost_list = self.parser.get_vhosts() return self._rank_matches_by_name_and_ssl(vhost_list, target_name) def _select_best_name_match(self, matches): """Returns the best name match of a ranked list of vhosts. :param list matches: list of dicts containing the vhost, the matching name, and the numerical rank :returns: the most matching vhost :rtype: :class:`~certbot_nginx._internal.obj.VirtualHost` """ if not matches: return None elif matches[0]['rank'] in [START_WILDCARD_RANK, END_WILDCARD_RANK, START_WILDCARD_RANK + NO_SSL_MODIFIER, END_WILDCARD_RANK + NO_SSL_MODIFIER]: # Wildcard match - need to find the longest one rank = matches[0]['rank'] wildcards = [x for x in matches if x['rank'] == rank] return max(wildcards, key=lambda x: len(x['name']))['vhost'] # Exact or regex match return matches[0]['vhost'] def _rank_matches_by_name(self, vhost_list, target_name): """Returns a ranked list of vhosts from vhost_list that match target_name. This method should always be followed by a call to _select_best_name_match. :param list vhost_list: list of vhosts to filter and rank :param str target_name: The name to match :returns: list of dicts containing the vhost, the matching name, and the numerical rank :rtype: list """ # Nginx chooses a matching server name for a request with precedence: # 1. exact name match # 2. longest wildcard name starting with * # 3. longest wildcard name ending with * # 4. first matching regex in order of appearance in the file matches = [] for vhost in vhost_list: name_type, name = parser.get_best_match(target_name, vhost.names) if name_type == 'exact': matches.append({'vhost': vhost, 'name': name, 'rank': NAME_RANK}) elif name_type == 'wildcard_start': matches.append({'vhost': vhost, 'name': name, 'rank': START_WILDCARD_RANK}) elif name_type == 'wildcard_end': matches.append({'vhost': vhost, 'name': name, 'rank': END_WILDCARD_RANK}) elif name_type == 'regex': matches.append({'vhost': vhost, 'name': name, 'rank': REGEX_RANK}) return sorted(matches, key=lambda x: x['rank']) def _rank_matches_by_name_and_ssl(self, vhost_list, target_name): """Returns a ranked list of vhosts from vhost_list that match target_name. The ranking gives preference to SSLishness before name match level. :param list vhost_list: list of vhosts to filter and rank :param str target_name: The name to match :returns: list of dicts containing the vhost, the matching name, and the numerical rank :rtype: list """ matches = self._rank_matches_by_name(vhost_list, target_name) for match in matches: if not match['vhost'].ssl: match['rank'] += NO_SSL_MODIFIER return sorted(matches, key=lambda x: x['rank']) def choose_redirect_vhosts(self, target_name: str, port: str) -> List[obj.VirtualHost]: """Chooses a single virtual host for redirect enhancement. Chooses the vhost most closely matching target_name that is listening to port without using ssl. .. todo:: This should maybe return list if no obvious answer is presented. .. todo:: The special name "$hostname" corresponds to the machine's hostname. Currently we just ignore this. :param str target_name: domain name :param str port: port number :returns: vhosts associated with name :rtype: list of :class:`~certbot_nginx._internal.obj.VirtualHost` """ if util.is_wildcard_domain(target_name): # Ask user which VHosts to enhance. vhosts = self._choose_vhosts_wildcard(target_name, prefer_ssl=False, no_ssl_filter_port=port) else: matches = self._get_redirect_ranked_matches(target_name, port) vhosts = [x for x in [self._select_best_name_match(matches)]if x is not None] return vhosts def choose_auth_vhosts(self, target_name: str) -> Tuple[List[obj.VirtualHost], List[obj.VirtualHost]]: """Returns a list of HTTP and HTTPS vhosts with a server_name matching target_name. If no HTTP vhost exists, one will be cloned from the default vhost. If that fails, no HTTP vhost will be returned. :param str target_name: non-wildcard domain name :returns: tuple of HTTP and HTTPS virtualhosts :rtype: tuple of :class:`~certbot_nginx._internal.obj.VirtualHost` """ vhosts = [m['vhost'] for m in self._get_ranked_matches(target_name) if m and 'vhost' in m] http_vhosts = [vh for vh in vhosts if self._vhost_listening(vh, str(self.config.http01_port), False)] https_vhosts = [vh for vh in vhosts if self._vhost_listening(vh, str(self.config.https_port), True)] # If no HTTP vhost matches, try create one from the default_server on http01_port. if not http_vhosts: try: http_vhosts = [self._vhost_from_duplicated_default(target_name, False, str(self.config.http01_port))] except errors.MisconfigurationError: http_vhosts = [] return http_vhosts, https_vhosts def _port_matches(self, test_port: str, matching_port: str) -> bool: # test_port is a number, matching is a number or "" or None if matching_port == "" or matching_port is None: # if no port is specified, Nginx defaults to listening on port 80. return test_port == self.DEFAULT_LISTEN_PORT return test_port == matching_port def _vhost_listening(self, vhost: obj.VirtualHost, port: str, ssl: bool) -> bool: """Tests whether a vhost has an address listening on a port with SSL enabled or disabled. :param `obj.VirtualHost` vhost: The vhost whose addresses will be tested :param port str: The port number as a string that the address should be bound to :param bool ssl: Whether SSL should be enabled or disabled on the address :returns: Whether the vhost has an address listening on the port and protocol. :rtype: bool """ assert self.parser is not None # prepare should already have been called here # if the 'ssl on' directive is present on the vhost, all its addresses have SSL enabled all_addrs_are_ssl = self.parser.has_ssl_on_directive(vhost) # if we want ssl vhosts: either 'ssl on' or 'addr.ssl' should be enabled # if we want plaintext vhosts: neither 'ssl on' nor 'addr.ssl' should be enabled _ssl_matches = lambda addr: addr.ssl or all_addrs_are_ssl if ssl else \ not addr.ssl and not all_addrs_are_ssl # if there are no listen directives at all, Nginx defaults to # listening on port 80. if not vhost.addrs: return port == self.DEFAULT_LISTEN_PORT and ssl == all_addrs_are_ssl return any(self._port_matches(port, addr.get_port()) and _ssl_matches(addr) for addr in vhost.addrs) def _vhost_listening_on_port_no_ssl(self, vhost: obj.VirtualHost, port: str) -> bool: return self._vhost_listening(vhost, port, False) def _get_redirect_ranked_matches(self, target_name, port): """Gets a ranked list of plaintextish port-listening vhosts matching target_name Filter all hosts for those listening on port without using ssl. Rank by how well these match target_name. :param str target_name: The name to match :param str port: port number as a string :returns: list of dicts containing the vhost, the matching name, and the numerical rank :rtype: list """ all_vhosts = self.parser.get_vhosts() def _vhost_matches(vhost, port): return self._vhost_listening_on_port_no_ssl(vhost, port) matching_vhosts = [vhost for vhost in all_vhosts if _vhost_matches(vhost, port)] return self._rank_matches_by_name(matching_vhosts, target_name) def get_all_names(self): """Returns all names found in the Nginx Configuration. :returns: All ServerNames, ServerAliases, and reverse DNS entries for virtual host addresses :rtype: set """ all_names: Set[str] = set() for vhost in self.parser.get_vhosts(): try: vhost.names.remove("$hostname") vhost.names.add(socket.gethostname()) except KeyError: pass all_names.update(vhost.names) for addr in vhost.addrs: host = addr.get_addr() if common.hostname_regex.match(host): # If it's a hostname, add it to the names. all_names.add(host) elif not common.private_ips_regex.match(host): # If it isn't a private IP, do a reverse DNS lookup try: if addr.ipv6: host = addr.get_ipv6_exploded() socket.inet_pton(socket.AF_INET6, host) else: socket.inet_pton(socket.AF_INET, host) all_names.add(socket.gethostbyaddr(host)[0]) except (socket.error, socket.herror, socket.timeout): continue return util.get_filtered_names(all_names) def _get_snakeoil_paths(self): """Generate invalid certs that let us create ssl directives for Nginx""" # TODO: generate only once tmp_dir = os.path.join(self.config.work_dir, "snakeoil") le_key = crypto_util.generate_key( key_size=1024, key_dir=tmp_dir, keyname="key.pem", strict_permissions=self.config.strict_permissions) key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, le_key.pem) cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()]) cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) cert_file, cert_path = util.unique_file( os.path.join(tmp_dir, "cert.pem"), mode="wb") with cert_file: cert_file.write(cert_pem) return cert_path, le_key.file def _make_server_ssl(self, vhost): """Make a server SSL. Make a server SSL by adding new listen and SSL directives. :param vhost: The vhost to add SSL to. :type vhost: :class:`~certbot_nginx._internal.obj.VirtualHost` """ https_port = self.config.https_port ipv6info = self.ipv6_info(https_port) ipv6_block = [''] ipv4_block = [''] # If the vhost was implicitly listening on the default Nginx port, # have it continue to do so. if not vhost.addrs: listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]] self.parser.add_server_directives(vhost, listen_block) if vhost.ipv6_enabled(): ipv6_block = ['\n ', 'listen', ' ', '[::]:{0}'.format(https_port), ' ', 'ssl'] if not ipv6info[1]: # ipv6only=on is absent in global config ipv6_block.append(' ') ipv6_block.append('ipv6only=on') if vhost.ipv4_enabled(): ipv4_block = ['\n ', 'listen', ' ', '{0}'.format(https_port), ' ', 'ssl'] snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() ssl_block = ([ ipv6_block, ipv4_block, ['\n ', 'ssl_certificate', ' ', snakeoil_cert], ['\n ', 'ssl_certificate_key', ' ', snakeoil_key], ['\n ', 'include', ' ', self.mod_ssl_conf], ['\n ', 'ssl_dhparam', ' ', self.ssl_dhparams], ]) self.parser.add_server_directives( vhost, ssl_block) ################################## # enhancement methods (Installer) ################################## def supported_enhancements(self): """Returns currently supported enhancements.""" return ['redirect', 'ensure-http-header', 'staple-ocsp'] def enhance(self, domain, enhancement, options=None): """Enhance configuration. :param str domain: domain to enhance :param str enhancement: enhancement type defined in :const:`~certbot.plugins.enhancements.ENHANCEMENTS` :param options: options for the enhancement See :const:`~certbot.plugins.enhancements.ENHANCEMENTS` documentation for appropriate parameter. """ try: return self._enhance_func[enhancement](domain, options) except (KeyError, ValueError): raise errors.PluginError( "Unsupported enhancement: {0}".format(enhancement)) def _has_certbot_redirect(self, vhost, domain): test_redirect_block = _test_block_from_block(_redirect_block_for_domain(domain)) return vhost.contains_list(test_redirect_block) def _set_http_header(self, domain, header_substring): """Enables header identified by header_substring on domain. If the vhost is listening plaintextishly, separates out the relevant directives into a new server block, and only add header directive to HTTPS block. :param str domain: the domain to enable header for. :param str header_substring: String to uniquely identify a header. e.g. Strict-Transport-Security, Upgrade-Insecure-Requests :returns: Success :raises .errors.PluginError: If no viable HTTPS host can be created or set with header header_substring. """ if not header_substring in constants.HEADER_ARGS: raise errors.NotSupportedError( f"{header_substring} is not supported by the nginx plugin.") vhosts = self.choose_vhosts(domain) if not vhosts: raise errors.PluginError( "Unable to find corresponding HTTPS host for enhancement.") for vhost in vhosts: if vhost.has_header(header_substring): raise errors.PluginEnhancementAlreadyPresent( "Existing %s header" % (header_substring)) # if there is no separate SSL block, break the block into two and # choose the SSL block. if vhost.ssl and any(not addr.ssl for addr in vhost.addrs): _, vhost = self._split_block(vhost) header_directives = [ ['\n ', 'add_header', ' ', header_substring, ' '] + constants.HEADER_ARGS[header_substring], ['\n']] self.parser.add_server_directives(vhost, header_directives) def _add_redirect_block(self, vhost, domain): """Add redirect directive to vhost """ redirect_block = _redirect_block_for_domain(domain) self.parser.add_server_directives( vhost, redirect_block, insert_at_top=True) def _split_block(self, vhost, only_directives=None): """Splits this "virtual host" (i.e. this nginx server block) into separate HTTP and HTTPS blocks. :param vhost: The server block to break up into two. :param list only_directives: If this exists, only duplicate these directives when splitting the block. :type vhost: :class:`~certbot_nginx._internal.obj.VirtualHost` :returns: tuple (http_vhost, https_vhost) :rtype: tuple of type :class:`~certbot_nginx._internal.obj.VirtualHost` """ http_vhost = self.parser.duplicate_vhost(vhost, only_directives=only_directives) def _ssl_match_func(directive): return 'ssl' in directive def _ssl_config_match_func(directive): return self.mod_ssl_conf in directive def _no_ssl_match_func(directive): return 'ssl' not in directive # remove all ssl addresses and related directives from the new block for directive in self.SSL_DIRECTIVES: self.parser.remove_server_directives(http_vhost, directive) self.parser.remove_server_directives(http_vhost, 'listen', match_func=_ssl_match_func) self.parser.remove_server_directives(http_vhost, 'include', match_func=_ssl_config_match_func) # remove all non-ssl addresses from the existing block self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func) return http_vhost, vhost def _enable_redirect(self, domain, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. If the vhost is listening plaintextishly, separate out the relevant directives into a new server block and add a rewrite directive. .. note:: This function saves the configuration :param str domain: domain to enable redirect for :param unused_options: Not currently used :type unused_options: Not Available """ port = self.DEFAULT_LISTEN_PORT # If there are blocks listening plaintextishly on self.DEFAULT_LISTEN_PORT, # choose the most name-matching one. vhosts = self.choose_redirect_vhosts(domain, port) if not vhosts: logger.info("No matching insecure server blocks listening on port %s found.", self.DEFAULT_LISTEN_PORT) return for vhost in vhosts: self._enable_redirect_single(domain, vhost) def _enable_redirect_single(self, domain, vhost): """Redirect all equivalent HTTP traffic to ssl_vhost. If the vhost is listening plaintextishly, separate out the relevant directives into a new server block and add a rewrite directive. .. note:: This function saves the configuration :param str domain: domain to enable redirect for :param `~obj.Vhost` vhost: vhost to enable redirect for """ if vhost.ssl: http_vhost, _ = self._split_block(vhost, ['listen', 'server_name']) # Add this at the bottom to get the right order of directives return_404_directive = [['\n ', 'return', ' ', '404']] self.parser.add_server_directives(http_vhost, return_404_directive) vhost = http_vhost if self._has_certbot_redirect(vhost, domain): logger.info("Traffic on port %s already redirecting to ssl in %s", self.DEFAULT_LISTEN_PORT, vhost.filep) else: # Redirect plaintextish host to https self._add_redirect_block(vhost, domain) logger.info("Redirecting all traffic on port %s to ssl in %s", self.DEFAULT_LISTEN_PORT, vhost.filep) def _enable_ocsp_stapling(self, domain, chain_path): """Include OCSP response in TLS handshake :param str domain: domain to enable OCSP response for :param chain_path: chain file path :type chain_path: `str` or `None` """ vhosts = self.choose_vhosts(domain) for vhost in vhosts: self._enable_ocsp_stapling_single(vhost, chain_path) def _enable_ocsp_stapling_single(self, vhost, chain_path): """Include OCSP response in TLS handshake :param str vhost: vhost to enable OCSP response for :param chain_path: chain file path :type chain_path: `str` or `None` """ if self.version < (1, 3, 7): raise errors.PluginError("Version 1.3.7 or greater of nginx " "is needed to enable OCSP stapling") if chain_path is None: raise errors.PluginError( "--chain-path is required to enable " "Online Certificate Status Protocol (OCSP) stapling " "on nginx >= 1.3.7.") stapling_directives = [ ['\n ', 'ssl_trusted_certificate', ' ', chain_path], ['\n ', 'ssl_stapling', ' ', 'on'], ['\n ', 'ssl_stapling_verify', ' ', 'on'], ['\n']] try: self.parser.add_server_directives(vhost, stapling_directives) except errors.MisconfigurationError as error: logger.debug(str(error)) raise errors.PluginError("An error occurred while enabling OCSP " "stapling for {0}.".format(vhost.names)) self.save_notes += ("OCSP Stapling was enabled " "on SSL Vhost: {0}.\n".format(vhost.filep)) self.save_notes += "\tssl_trusted_certificate {0}\n".format(chain_path) self.save_notes += "\tssl_stapling on\n" self.save_notes += "\tssl_stapling_verify on\n" ###################################### # Nginx server management (Installer) ###################################### def restart(self): """Restarts nginx server. :raises .errors.MisconfigurationError: If either the reload fails. """ nginx_restart(self.conf('ctl'), self.nginx_conf, self.conf('sleep-seconds')) def config_test(self): """Check the configuration of Nginx for errors. :raises .errors.MisconfigurationError: If config_test fails """ try: util.run_script([self.conf('ctl'), "-c", self.nginx_conf, "-t"]) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) def _nginx_version(self): """Return results of nginx -V :returns: version text :rtype: str :raises .PluginError: Unable to run Nginx version command """ try: proc = subprocess.run( [self.conf('ctl'), "-c", self.nginx_conf, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=False, env=util.env_no_snap_for_external_calls()) text = proc.stderr # nginx prints output to stderr except (OSError, ValueError) as error: logger.debug(str(error), exc_info=True) raise errors.PluginError( "Unable to run %s -V" % self.conf('ctl')) return text def get_version(self): """Return version of Nginx Server. Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)) :returns: version :rtype: tuple :raises .PluginError: Unable to find Nginx version or version is unsupported """ text = self._nginx_version() version_regex = re.compile(r"nginx version: ([^/]+)/([0-9\.]*)", re.IGNORECASE) version_matches = version_regex.findall(text) sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE) sni_matches = sni_regex.findall(text) ssl_regex = re.compile(r" --with-http_ssl_module") ssl_matches = ssl_regex.findall(text) if not version_matches: raise errors.PluginError("Unable to find Nginx version") if not ssl_matches: raise errors.PluginError( "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: raise errors.PluginError("Nginx build doesn't support SNI") product_name, product_version = version_matches[0] if product_name != 'nginx': logger.warning("NGINX derivative %s is not officially supported by" " certbot", product_name) nginx_version = tuple(int(i) for i in product_version.split(".")) # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string if nginx_version < (0, 8, 48): raise errors.NotSupportedError("Nginx version must be 0.8.48+") return nginx_version def _get_openssl_version(self): """Return version of OpenSSL linked to Nginx. Version is returned as string. If no version can be found, empty string is returned. :returns: openssl_version :rtype: str :raises .PluginError: Unable to run Nginx version command """ text = self._nginx_version() matches = re.findall(r"running with OpenSSL ([^ ]+) ", text) if not matches: matches = re.findall(r"built with OpenSSL ([^ ]+) ", text) if not matches: logger.warning("NGINX configured with OpenSSL alternatives is not officially" " supported by Certbot.") return "" return matches[0] def more_info(self): """Human-readable string to help understand the module""" return ( "Configures Nginx to authenticate and install HTTPS.{0}" "Server root: {root}{0}" "Version: {version}".format( os.linesep, root=self.parser.config_root, version=".".join(str(i) for i in self.version)) ) def auth_hint(self, failed_achalls): # pragma: no cover return ( "The Certificate Authority failed to verify the temporary nginx configuration changes " "made by Certbot. Ensure the listed domains point to this nginx server and that it is " "accessible from the internet." ) ################################################### # Wrapper functions for Reverter class (Installer) ################################################### def save(self, title=None, temporary=False): """Saves all changes to the configuration files. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a timestamped directory. :param bool temporary: Indicates whether the changes made will be quickly reversed in the future (ie. challenges) :raises .errors.PluginError: If there was an error in an attempt to save the configuration, or an error creating a checkpoint """ save_files = set(self.parser.parsed.keys()) self.add_to_checkpoint(save_files, self.save_notes, temporary) self.save_notes = "" # Change 'ext' to something else to not override existing conf files self.parser.filedump(ext='') if title and not temporary: self.finalize_checkpoint(title) def recovery_routine(self): """Revert all previously modified files. Reverts all modified files that have not been saved as a checkpoint :raises .errors.PluginError: If unable to recover the configuration """ super().recovery_routine() self.new_vhost = None self.parser.load() def revert_challenge_config(self): """Used to cleanup challenge configurations. :raises .errors.PluginError: If unable to revert the challenge config. """ self.revert_temporary_config() self.new_vhost = None self.parser.load() def rollback_checkpoints(self, rollback=1): """Rollback saved checkpoints. :param int rollback: Number of checkpoints to revert :raises .errors.PluginError: If there is a problem with the input or the function is unable to correctly revert the configuration """ super().rollback_checkpoints(rollback) self.new_vhost = None self.parser.load() ########################################################################### # Challenges Section for Authenticator ########################################################################### def get_chall_pref(self, unused_domain): """Return list of challenge preferences.""" return [challenges.HTTP01] # Entry point in main.py for performing challenges def perform(self, achalls): """Perform the configuration related challenge. This function currently assumes all challenges will be fulfilled. If this turns out not to be the case in the future. Cleanup and outstanding challenges will have to be designed better. """ self._chall_out += len(achalls) responses = [None] * len(achalls) http_doer = http_01.NginxHttp01(self) for i, achall in enumerate(achalls): # Currently also have chall_doer hold associated index of the # challenge. This helps to put all of the responses back together # when they are all complete. http_doer.add_chall(achall, i) http_response = http_doer.perform() # Must restart in order to activate the challenges. # Handled here because we may be able to load up other challenge types self.restart() # Go through all of the challenges and assign them to the proper place # in the responses return value. All responses must be in the same order # as the original challenges. for i, resp in enumerate(http_response): responses[http_doer.indices[i]] = resp return responses # called after challenges are performed def cleanup(self, achalls): """Revert all challenges.""" self._chall_out -= len(achalls) # If all of the challenges have been finished, clean up everything if self._chall_out <= 0: self.revert_challenge_config() self.restart() def _test_block_from_block(block): test_block = nginxparser.UnspacedList(block) parser.comment_directive(test_block, 0) return test_block[:-1] def _redirect_block_for_domain(domain): updated_domain = domain match_symbol = '=' if util.is_wildcard_domain(domain): match_symbol = '~' updated_domain = updated_domain.replace('.', r'\.') updated_domain = updated_domain.replace('*', '[^.]+') updated_domain = '^' + updated_domain + '$' redirect_block = [[ ['\n ', 'if', ' ', '($host', ' ', match_symbol, ' ', '%s)' % updated_domain, ' '], [['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'], '\n ']], ['\n']] return redirect_block def nginx_restart(nginx_ctl, nginx_conf, sleep_duration): """Restarts the Nginx Server. .. todo:: Nginx restart is fatal if the configuration references non-existent SSL cert/key files. Remove references to /etc/letsencrypt before restart. :param str nginx_ctl: Path to the Nginx binary. :param str nginx_conf: Path to the Nginx configuration file. :param int sleep_duration: How long to sleep after sending the reload signal. """ try: reload_output: Text = "" with tempfile.TemporaryFile() as out: proc = subprocess.run([nginx_ctl, "-c", nginx_conf, "-s", "reload"], env=util.env_no_snap_for_external_calls(), stdout=out, stderr=out, check=False) out.seek(0) reload_output = out.read().decode("utf-8") if proc.returncode != 0: logger.debug("nginx reload failed:\n%s", reload_output) # Maybe Nginx isn't running - try start it # 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: nginx_proc = subprocess.run([nginx_ctl, "-c", nginx_conf], stdout=out, stderr=out, env=util.env_no_snap_for_external_calls(), check=False) if nginx_proc.returncode != 0: out.seek(0) # Enter recovery routine... raise errors.MisconfigurationError( "nginx restart failed:\n%s" % out.read().decode("utf-8")) except (OSError, ValueError): raise errors.MisconfigurationError("nginx restart failed") # Nginx can take a significant duration of time to fully apply a new config, depending # on size and contents (https://github.com/certbot/certbot/issues/7422). Lacking a way # to reliably identify when this process is complete, we provide the user with control # over how long Certbot will sleep after reloading the configuration. if sleep_duration > 0: time.sleep(sleep_duration) def _determine_default_server_root(): if os.environ.get("CERTBOT_DOCS") == "1": default_server_root = "%s or %s" % (constants.LINUX_SERVER_ROOT, constants.FREEBSD_DARWIN_SERVER_ROOT) else: default_server_root = constants.CLI_DEFAULTS["server_root"] return default_server_root ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/constants.py0000644000076500000240000000562400000000000022244 0ustar00bmwstaff"""nginx plugin constants.""" import platform from typing import Any from typing import Dict FREEBSD_DARWIN_SERVER_ROOT = "/usr/local/etc/nginx" LINUX_SERVER_ROOT = "/etc/nginx" PKGSRC_SERVER_ROOT = "/usr/pkg/etc/nginx" if platform.system() in ('FreeBSD', 'Darwin'): server_root_tmp = FREEBSD_DARWIN_SERVER_ROOT elif platform.system() in ('NetBSD',): server_root_tmp = PKGSRC_SERVER_ROOT else: server_root_tmp = LINUX_SERVER_ROOT CLI_DEFAULTS: Dict[str, Any] = dict( server_root=server_root_tmp, ctl="nginx", sleep_seconds=1 ) """CLI defaults.""" MOD_SSL_CONF_DEST = "options-ssl-nginx.conf" """Name of the mod_ssl config file as saved in `certbot.configuration.NamespaceConfig.config_dir`.""" UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-nginx-conf-digest.txt" """Name of the hash of the updated or informed mod_ssl_conf as saved in `certbot.configuration.NamespaceConfig.config_dir`.""" ALL_SSL_OPTIONS_HASHES = [ '0f81093a1465e3d4eaa8b0c14e77b2a2e93568b0fc1351c2b87893a95f0de87c', '9a7b32c49001fed4cff8ad24353329472a50e86ade1ef9b2b9e43566a619612e', 'a6d9f1c7d6b36749b52ba061fff1421f9a0a3d2cfdafbd63c05d06f65b990937', '7f95624dd95cf5afc708b9f967ee83a24b8025dc7c8d9df2b556bbc64256b3ff', '394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d', '4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16', 'c052ffff0ad683f43bffe105f7c606b339536163490930e2632a335c8d191cc4', '02329eb19930af73c54b3632b3165d84571383b8c8c73361df940cb3894dd426', '63e2bddebb174a05c9d8a7cf2adf72f7af04349ba59a1a925fe447f73b2f1abf', '2901debc7ecbc10917edd9084c05464c9c5930b463677571eaf8c94bffd11ae2', '30baca73ed9a5b0e9a69ea40e30482241d8b1a7343aa79b49dc5d7db0bf53b6c', '02329eb19930af73c54b3632b3165d84571383b8c8c73361df940cb3894dd426', '108c4555058a087496a3893aea5d9e1cee0f20a3085d44a52dc1a66522299ac3', 'd5e021706ecdccc7090111b0ae9a29ef61523e927f020e410caf0a1fd7063981', 'ef11e3fb17213e74d3e1816cde0ec37b8b95b4167cf21e7b8ff1eaa9c6f918ee', 'af85f6193808a44789a1d293e6cffa249cad9a21135940800958b8e3c72dbc69', 'a2a612fd21b02abaa32d9d11ac63d987d6e3054dbfa356de5800eea0d7ce17f3', '2d9648302e3588a172c318e46bff88ade46fc7a16d6afc85322776a04800d473', ] """SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC""" def os_constant(key): # XXX TODO: In the future, this could return different constants # based on what OS we are running under. To see an # approach to how to handle different OSes, see the # apache version of this file. Currently, we do not # actually have any OS-specific constants on Nginx. """ Get a constant value for operating system :param key: name of cli constant :return: value of constant for active os """ return CLI_DEFAULTS[key] HSTS_ARGS = ['\"max-age=31536000\"', ' ', 'always'] HEADER_ARGS = {'Strict-Transport-Security': HSTS_ARGS} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/display_ops.py0000644000076500000240000000243500000000000022553 0ustar00bmwstaff"""Contains UI methods for Nginx operations.""" import logging from certbot.display import util as display_util logger = logging.getLogger(__name__) def select_vhost_multiple(vhosts): """Select multiple Vhosts to install the certificate for :param vhosts: Available Nginx VirtualHosts :type vhosts: :class:`list` of type `~obj.Vhost` :returns: List of VirtualHosts :rtype: :class:`list`of type `~obj.Vhost` """ if not vhosts: return [] tags_list = [vhost.display_repr()+"\n" for vhost in vhosts] # Remove the extra newline from the last entry if tags_list: tags_list[-1] = tags_list[-1][:-1] code, names = display_util.checklist( "Which server blocks would you like to modify?", tags=tags_list, force_interactive=True) if code == display_util.OK: return_vhosts = _reversemap_vhosts(names, vhosts) return return_vhosts return [] def _reversemap_vhosts(names, vhosts): """Helper function for select_vhost_multiple for mapping string representations back to actual vhost objects""" return_vhosts = [] for selection in names: for vhost in vhosts: if vhost.display_repr().strip() == selection.strip(): return_vhosts.append(vhost) return return_vhosts ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/http_01.py0000644000076500000240000001767000000000000021513 0ustar00bmwstaff"""A class that performs HTTP-01 challenges for Nginx""" import io import logging from typing import List from typing import Optional from acme import challenges from certbot import achallenges from certbot import errors from certbot.compat import os from certbot.plugins import common from certbot_nginx._internal import nginxparser from certbot_nginx._internal import obj logger = logging.getLogger(__name__) class NginxHttp01(common.ChallengePerformer): """HTTP-01 authenticator for Nginx :ivar configurator: NginxConfigurator object :type configurator: :class:`~nginx.configurator.NginxConfigurator` :ivar list achalls: Annotated class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge` challenges :param list indices: Meant to hold indices of challenges in a larger array. NginxHttp01 is capable of solving many challenges at once which causes an indexing issue within NginxConfigurator who must return all responses in order. Imagine NginxConfigurator maintaining state about where all of the challenges, possibly of different types, belong in the response array. This is an optional utility. """ def __init__(self, configurator): super().__init__(configurator) self.challenge_conf = os.path.join( configurator.config.config_dir, "le_http_01_cert_challenge.conf") def perform(self): """Perform a challenge on Nginx. :returns: list of :class:`certbot.acme.challenges.HTTP01Response` :rtype: list """ if not self.achalls: return [] responses = [x.response(x.account_key) for x in self.achalls] # Set up the configuration self._mod_config() # Save reversible changes self.configurator.save("HTTP Challenge", True) return responses 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 io.open(self.challenge_conf, "w", encoding="utf-8") as new_conf: nginxparser.dump(config, new_conf) def _default_listen_addresses(self): """Finds addresses for a challenge block to listen on. :returns: list of :class:`certbot_nginx._internal.obj.Addr` to apply :rtype: list """ addresses: List[obj.Addr] = [] default_addr = "%s" % self.configurator.config.http01_port ipv6_addr = "[::]:{0}".format( self.configurator.config.http01_port) port = self.configurator.config.http01_port ipv6, ipv6only = self.configurator.ipv6_info(port) if ipv6: # If IPv6 is active in Nginx configuration if not ipv6only: # If ipv6only=on is not already present in the config ipv6_addr = ipv6_addr + " ipv6only=on" addresses = [obj.Addr.fromstring(default_addr), obj.Addr.fromstring(ipv6_addr)] logger.debug(("Using default addresses %s and %s for authentication."), default_addr, ipv6_addr) else: addresses = [obj.Addr.fromstring(default_addr)] logger.debug("Using default address %s for authentication.", default_addr) return addresses def _get_validation_path(self, achall): return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, achall.chall.encode("token")) def _make_server_block(self, achall: achallenges.KeyAuthorizationAnnotatedChallenge) -> List: """Creates a server block for a challenge. :param achall: Annotated HTTP-01 challenge :type achall: :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge` :returns: server block for the challenge host :rtype: list """ addrs = self._default_listen_addresses() block = [['listen', ' ', addr.to_string(include_default=False)] for addr in addrs] # Ensure we 404 on any other request by setting a root document_root = os.path.join( self.configurator.config.work_dir, "http_01_nonexistent") block.extend([['server_name', ' ', achall.domain], ['root', ' ', document_root], self._location_directive_for_achall(achall) ]) # TODO: do we want to return something else if they otherwise access this block? return [['server'], block] def _location_directive_for_achall(self, achall): validation = achall.validation(achall.account_key) validation_path = self._get_validation_path(achall) location_directive = [['location', ' ', '=', ' ', validation_path], [['default_type', ' ', 'text/plain'], ['return', ' ', '200', ' ', validation]]] return location_directive def _make_or_mod_server_block(self, achall: achallenges.KeyAuthorizationAnnotatedChallenge ) -> Optional[List]: """Modifies server blocks to respond to a challenge. Returns a new HTTP server block to add to the configuration if an existing one can't be found. :param achall: Annotated HTTP-01 challenge :type achall: :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge` :returns: new server block to be added, if any :rtype: list """ http_vhosts, https_vhosts = self.configurator.choose_auth_vhosts(achall.domain) new_vhost: Optional[list] = None if not http_vhosts: # Couldn't find either a matching name+port server block # or a port+default_server block, so create a dummy block new_vhost = self._make_server_block(achall) # Modify any existing server blocks for vhost in set(http_vhosts + https_vhosts): location_directive = [self._location_directive_for_achall(achall)] self.configurator.parser.add_server_directives(vhost, location_directive) rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)', ' ', '$1', ' ', 'break']] self.configurator.parser.add_server_directives(vhost, rewrite_directive, insert_at_top=True) return new_vhost ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/nginxparser.py0000644000076500000240000002175500000000000022573 0ustar00bmwstaff"""Very low-level nginx config parser based on pyparsing.""" # Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed) import copy import logging from typing import Any from typing import IO from pyparsing import Combine from pyparsing import Forward from pyparsing import Group from pyparsing import Literal from pyparsing import Optional from pyparsing import QuotedString from pyparsing import Regex from pyparsing import restOfLine from pyparsing import stringEnd from pyparsing import White from pyparsing import ZeroOrMore logger = logging.getLogger(__name__) class RawNginxParser: # pylint: disable=pointless-statement """A class that parses nginx configuration with pyparsing.""" # constants space = Optional(White()).leaveWhitespace() required_space = White().leaveWhitespace() left_bracket = Literal("{").suppress() right_bracket = space + Literal("}").suppress() semicolon = Literal(";").suppress() 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. token = paren_quote_extend | tokenchars | quoted whitespace_token_group = space + token + ZeroOrMore(required_space + token) + space assignment = whitespace_token_group + semicolon comment = space + Literal('#') + restOfLine block = Forward() # order matters! see issue 518, and also http { # server { \n} contents = Group(comment) | Group(block) | Group(assignment) block_begin = Group(whitespace_token_group) block_innards = Group(ZeroOrMore(contents) + space).leaveWhitespace() block << block_begin + left_bracket + block_innards + right_bracket script = ZeroOrMore(contents) + space + stringEnd script.parseWithTabs().leaveWhitespace() def __init__(self, source): self.source = source def parse(self): """Returns the parsed tree.""" return self.script.parseString(self.source) def as_list(self): """Returns the parsed tree as a list.""" return self.parse().asList() class RawNginxDumper: """A class that dumps nginx configuration from the provided tree.""" def __init__(self, blocks): self.blocks = blocks def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks for b0 in blocks: if isinstance(b0, str): yield b0 continue item = copy.deepcopy(b0) if spacey(item[0]): yield item.pop(0) # indentation if not item: continue 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: # 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.""" return ''.join(self) 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) slicepos = self._spaced_position(i) if i < len(self) else len(self.spaced) self.spaced.insert(slicepos, spaced_item) if not spacey(item): list.insert(self, i, item) self.dirty = True def append(self, x): item, spaced_item = self._coerce(x) self.spaced.append(spaced_item) if not spacey(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) if not spacey(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): new_spaced = copy.deepcopy(self.spaced, memo=memo) l = UnspacedList(new_spaced) 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, UnspacedList) 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 # Shortcut functions to respect Python's serialization interface # (like pyyaml, picker or json) def loads(source): """Parses from a string. :param str source: The string to parse :returns: The parsed tree :rtype: list """ return UnspacedList(RawNginxParser(source).as_list()) def load(_file): """Parses from a file. :param file _file: The file to parse :returns: The parsed tree :rtype: list """ return loads(_file.read()) def dumps(blocks: UnspacedList) -> str: """Dump to a Unicode string. :param UnspacedList block: The parsed tree :rtype: six.text_type """ return str(RawNginxDumper(blocks.spaced)) def dump(blocks: UnspacedList, _file: IO[Any]) -> None: """Dump to a file. :param UnspacedList block: The parsed tree :param IO[Any] _file: The file stream to dump to. It must be opened with Unicode encoding. :rtype: None """ _file.write(dumps(blocks)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/obj.py0000644000076500000240000002206300000000000020776 0ustar00bmwstaff"""Module contains classes used by the Nginx Configurator.""" import re from certbot.plugins import common ADD_HEADER_DIRECTIVE = 'add_header' class Addr(common.Addr): r"""Represents an Nginx address, i.e. what comes after the 'listen' directive. According to the `documentation`_, this may be address[:port], port, or unix:path. The latter is ignored here. The default value if no directive is specified is \*:80 (superuser) or \*:8000 (otherwise). If no port is specified, the default is 80. If no address is specified, listen on all addresses. .. _documentation: https://nginx.org/en/docs/http/ngx_http_core_module.html#listen .. todo:: Old-style nginx configs define SSL vhosts in a separate block instead of using 'ssl' in the listen directive. :param str addr: addr part of vhost address, may be hostname, IPv4, IPv6, "", or "\*" :param str port: port number or "\*" or "" :param bool ssl: Whether the directive includes 'ssl' :param bool default: Whether the directive includes 'default_server' :param bool default: Whether this is an IPv6 address :param bool ipv6only: Whether the directive includes 'ipv6only=on' """ UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0') CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0] def __init__(self, host, port, ssl, default, ipv6, ipv6only): super().__init__((host, port)) self.ssl = ssl self.default = default self.ipv6 = ipv6 self.ipv6only = ipv6only self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES @classmethod def fromstring(cls, str_addr): """Initialize Addr from string.""" parts = str_addr.split(' ') ssl = False default = False ipv6 = False ipv6only = False host = '' port = '' # The first part must be the address addr = parts.pop(0) # Ignore UNIX-domain sockets if addr.startswith('unix:'): return None # IPv6 check ipv6_match = re.match(r'\[.*\]', addr) if ipv6_match: ipv6 = True # IPv6 handling host = ipv6_match.group() # The rest of the addr string will be the port, if any port = addr[ipv6_match.end()+1:] else: # IPv4 handling tup = addr.partition(':') if re.match(r'^\d+$', tup[0]): # This is a bare port, not a hostname. E.g. listen 80 host = '' port = tup[0] else: # This is a host-port tuple. E.g. listen 127.0.0.1:* host = tup[0] port = tup[2] # The rest of the parts are options; we only care about ssl and default while parts: nextpart = parts.pop() if nextpart == 'ssl': ssl = True elif nextpart == 'default_server': default = True elif nextpart == 'default': default = True elif nextpart == "ipv6only=on": ipv6only = True return cls(host, port, ssl, default, ipv6, ipv6only) def to_string(self, include_default=True): """Return string representation of Addr""" parts = '' if self.tup[0] and self.tup[1]: parts = "%s:%s" % self.tup elif self.tup[0]: parts = self.tup[0] else: parts = self.tup[1] if self.default and include_default: parts += ' default_server' if self.ssl: parts += ' ssl' return parts def __str__(self): return self.to_string() def __repr__(self): return "Addr(" + self.__str__() + ")" def __hash__(self): # pylint: disable=useless-super-delegation # Python 3 requires explicit overridden for __hash__ # See certbot-apache/certbot_apache/_internal/obj.py for more information return super().__hash__() def super_eq(self, other): """Check ip/port equality, with IPv6 support. """ # If both addresses got an unspecified address, then make sure the # host representation in each match when doing the comparison. if self.unspecified_address and other.unspecified_address: return common.Addr((self.CANONICAL_UNSPECIFIED_ADDRESS, self.tup[1]), self.ipv6) == \ common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS, other.tup[1]), other.ipv6) return super().__eq__(other) def __eq__(self, other): if isinstance(other, self.__class__): return (self.super_eq(other) and self.ssl == other.ssl and self.default == other.default) return False class VirtualHost: """Represents an Nginx Virtualhost. :ivar str filep: file path of VH :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) :ivar set names: Server names/aliases of vhost (:class:`list` of :class:`str`) :ivar list raw: The raw form of the parsed server block :ivar bool ssl: SSLEngine on in vhost :ivar bool enabled: Virtual host is enabled :ivar list path: The indices into the parsed file used to access the server block defining the vhost """ def __init__(self, filep, addrs, ssl, enabled, names, raw, path): """Initialize a VH.""" self.filep = filep self.addrs = addrs self.names = names self.ssl = ssl self.enabled = enabled self.raw = raw self.path = path def __str__(self): 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, list(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 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 self.path == other.path) return False def __hash__(self): return hash((self.filep, tuple(self.path), tuple(self.addrs), tuple(self.names), self.ssl, self.enabled)) def has_header(self, header_name): """Determine if this server block has a particular header set. :param str header_name: The name of the header to check for, e.g. 'Strict-Transport-Security' """ found = _find_directive(self.raw, ADD_HEADER_DIRECTIVE, header_name) return found is not None def contains_list(self, test): """Determine if raw server block contains test list at top level """ for i in range(0, len(self.raw) - len(test) + 1): if self.raw[i:i + len(test)] == test: return True return False def ipv6_enabled(self): """Return true if one or more of the listen directives in vhost supports IPv6""" for a in self.addrs: if a.ipv6: return True return False def ipv4_enabled(self): """Return true if one or more of the listen directives in vhost are IPv4 only""" if not self.addrs: return True for a in self.addrs: if not a.ipv6: return True return False def display_repr(self): """Return a representation of VHost to be used in dialog""" return ( "File: {filename}\n" "Addresses: {addrs}\n" "Names: {names}\n" "HTTPS: {https}\n".format( filename=self.filep, addrs=", ".join(str(addr) for addr in self.addrs), names=", ".join(self.names), https="Yes" if self.ssl else "No")) def _find_directive(directives, directive_name, match_content=None): """Find a directive of type directive_name in directives. If match_content is given, Searches for `match_content` in the directive arguments. """ if not directives or isinstance(directives, str): return None # If match_content is None, just match on directive type. Otherwise, match on # both directive type -and- the content! if directives[0] == directive_name and \ (match_content is None or match_content in directives): return directives matches = (_find_directive(line, directive_name, match_content) for line in directives) return next((m for m in matches if m is not None), None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/parser.py0000644000076500000240000007334500000000000021531 0ustar00bmwstaff"""NginxParser is a member object of the NginxConfigurator class.""" import copy import functools import glob import io import logging import re from typing import Dict from typing import List from typing import Optional from typing import Set from typing import Tuple from typing import Union import pyparsing from certbot import errors from certbot.compat import os from certbot_nginx._internal import nginxparser from certbot_nginx._internal import obj from certbot_nginx._internal.nginxparser import UnspacedList logger = logging.getLogger(__name__) class NginxParser: """Class handles the fine details of parsing the Nginx Configuration. :ivar str root: Normalized absolute path to the server root directory. Without trailing slash. :ivar dict parsed: Mapping of file paths to parsed trees """ def __init__(self, root): self.parsed: Dict[str, Union[List, nginxparser.UnspacedList]] = {} self.root = os.path.abspath(root) self.config_root = self._find_config_root() # Parse nginx.conf and included files. # TODO: Check sites-available/ as well. For now, the configurator does # not enable sites from there. self.load() def load(self): """Loads Nginx files into a parsed tree. """ self.parsed = {} self._parse_recursively(self.config_root) def _parse_recursively(self, filepath): """Parses nginx config files recursively by looking at 'include' directives inside 'http' and 'server' blocks. Note that this only reads Nginx files that potentially declare a virtual host. :param str filepath: The path to the files to parse, as a glob """ # pylint: disable=too-many-nested-blocks filepath = self.abs_path(filepath) trees = self._parse_files(filepath) for tree in trees: for entry in tree: if _is_include_directive(entry): # Parse the top-level included file self._parse_recursively(entry[1]) elif entry[0] == ['http'] or entry[0] == ['server']: # Look for includes in the top-level 'http'/'server' context for subentry in entry[1]: if _is_include_directive(subentry): self._parse_recursively(subentry[1]) elif entry[0] == ['http'] and subentry[0] == ['server']: # Look for includes in a 'server' context within # an 'http' context for server_entry in subentry[1]: if _is_include_directive(server_entry): self._parse_recursively(server_entry[1]) def abs_path(self, path): """Converts a relative path to an absolute path relative to the root. Does nothing for paths that are already absolute. :param str path: The path :returns: The absolute path :rtype: str """ if not os.path.isabs(path): return os.path.normpath(os.path.join(self.root, path)) return os.path.normpath(path) def _build_addr_to_ssl(self): """Builds a map from address to whether it listens on ssl in any server block """ servers = self._get_raw_servers() addr_to_ssl: Dict[Tuple[str, str], bool] = {} for server_list in servers.values(): for server, _ in server_list: # Parse the server block to save addr info parsed_server = _parse_server_raw(server) for addr in parsed_server['addrs']: addr_tuple = addr.normalized_tuple() if addr_tuple not in addr_to_ssl: addr_to_ssl[addr_tuple] = addr.ssl addr_to_ssl[addr_tuple] = addr.ssl or addr_to_ssl[addr_tuple] return addr_to_ssl def _get_raw_servers(self) -> Dict: # pylint: disable=cell-var-from-loop """Get a map of unparsed all server blocks """ servers: Dict[str, Union[List, nginxparser.UnspacedList]] = {} for filename, tree in self.parsed.items(): servers[filename] = [] srv = servers[filename] # workaround undefined loop var in lambdas # Find all the server blocks _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 for i, (server, path) in enumerate(servers[filename]): new_server = self._get_included_directives(server) servers[filename][i] = (new_server, path) return servers def get_vhosts(self): """Gets list of all 'virtual hosts' found in Nginx configuration. Technically this is a misnomer because Nginx does not have virtual hosts, it has 'server blocks'. :returns: List of :class:`~certbot_nginx._internal.obj.VirtualHost` objects found in configuration :rtype: list """ enabled = True # We only look at enabled vhosts for now servers = self._get_raw_servers() vhosts = [] for filename, server_list in servers.items(): for server, path in server_list: # Parse the server block into a VirtualHost object parsed_server = _parse_server_raw(server) vhost = obj.VirtualHost(filename, parsed_server['addrs'], parsed_server['ssl'], enabled, parsed_server['names'], server, path) vhosts.append(vhost) self._update_vhosts_addrs_ssl(vhosts) return vhosts def _update_vhosts_addrs_ssl(self, vhosts): """Update a list of raw parsed vhosts to include global address sslishness """ addr_to_ssl = self._build_addr_to_ssl() for vhost in vhosts: for addr in vhost.addrs: addr.ssl = addr_to_ssl[addr.normalized_tuple()] if addr.ssl: vhost.ssl = True def _get_included_directives(self, block): """Returns array with the "include" directives expanded out by concatenating the contents of the included file to the block. :param list block: :rtype: list """ 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( self.abs_path(directive[1])) for incl in included_files: try: result.extend(self.parsed[incl]) except KeyError: pass return result def _parse_files(self, filepath, override=False): """Parse files from a glob :param str filepath: Nginx config file path :param bool override: Whether to parse a file that has been parsed :returns: list of parsed tree structures :rtype: list """ 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: continue try: with io.open(item, "r", encoding="utf-8") as _file: parsed = nginxparser.load(_file) self.parsed[item] = parsed trees.append(parsed) except IOError: logger.warning("Could not open file: %s", item) except UnicodeDecodeError: logger.warning("Could not read file: %s due to invalid " "character. Only UTF-8 encoding is " "supported.", item) except pyparsing.ParseException as err: logger.warning("Could not parse file: %s due to %s", item, err) return trees def _find_config_root(self): """Return the Nginx Configuration Root file.""" location = ['nginx.conf'] for name in location: if os.path.isfile(os.path.join(self.root, name)): return os.path.join(self.root, name) raise errors.NoInstallationError( "Could not find Nginx root configuration file (nginx.conf)") 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, tree in self.parsed.items(): if ext: filename = filename + os.path.extsep + ext if not isinstance(tree, UnspacedList): raise ValueError(f"Error tree {tree} is not an UnspacedList") try: 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 io.open(filename, 'w', encoding='utf-8') as _file: _file.write(out) except IOError: logger.error("Could not open file for writing: %s", filename) def parse_server(self, server): """Parses a list of server directives, accounting for global address sslishness. :param list server: list of directives in a server block :rtype: dict """ addr_to_ssl = self._build_addr_to_ssl() parsed_server = _parse_server_raw(server) _apply_global_addr_ssl(addr_to_ssl, parsed_server) return parsed_server def has_ssl_on_directive(self, vhost): """Does vhost have ssl on for all ports? :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost in question :returns: True if 'ssl on' directive is included :rtype: bool """ server = vhost.raw for directive in server: if not directive: continue if _is_ssl_on_directive(directive): return True return False def add_server_directives(self, vhost, directives, insert_at_top=False): """Add directives to the server block identified by vhost. This method modifies vhost to be fully consistent with the new directives. ..note :: It's an error to try and add a nonrepeatable directive that already exists in the config block with a conflicting value. ..todo :: Doesn't match server blocks whose server_name directives are split across multiple conf files. :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost whose information we use to match on :param list directives: The directives to add :param bool insert_at_top: True if the directives need to be inserted at the top of the server block instead of the bottom """ self._modify_server_directives(vhost, functools.partial(_add_directives, directives, insert_at_top)) def update_or_add_server_directives(self, vhost, directives, insert_at_top=False): """Add or replace directives in the server block identified by vhost. This method modifies vhost to be fully consistent with the new directives. ..note :: When a directive with the same name already exists in the config block, the first instance will be replaced. Otherwise, the directive will be appended/prepended to the config block as in add_server_directives. ..todo :: Doesn't match server blocks whose server_name directives are split across multiple conf files. :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost whose information we use to match on :param list directives: The directives to add :param bool insert_at_top: True if the directives need to be inserted at the top of the server block instead of the bottom """ self._modify_server_directives(vhost, functools.partial(_update_or_add_directives, directives, insert_at_top)) def remove_server_directives(self, vhost, directive_name, match_func=None): """Remove all directives of type directive_name. :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost to remove directives from :param string directive_name: The directive type to remove :param callable match_func: Function of the directive that returns true for directives to be deleted. """ self._modify_server_directives(vhost, functools.partial(_remove_directives, directive_name, match_func)) def _update_vhost_based_on_new_directives(self, vhost, directives_list): new_server = self._get_included_directives(directives_list) parsed_server = self.parse_server(new_server) vhost.addrs = parsed_server['addrs'] vhost.ssl = parsed_server['ssl'] vhost.names = parsed_server['names'] vhost.raw = new_server def _modify_server_directives(self, vhost, block_func): filename = vhost.filep try: result = self.parsed[filename] for index in vhost.path: result = result[index] if not isinstance(result, list) or len(result) != 2: raise errors.MisconfigurationError("Not a server block.") result = result[1] block_func(result) self._update_vhost_based_on_new_directives(vhost, result) except errors.MisconfigurationError as err: raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err))) def duplicate_vhost(self, vhost_template: obj.VirtualHost, remove_singleton_listen_params: bool = False, only_directives: Optional[List] = None) -> obj.VirtualHost: """Duplicate the vhost in the configuration files. :param :class:`~certbot_nginx._internal.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._internal.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 = {'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 def _parse_ssl_options(ssl_options): if ssl_options is not None: try: with io.open(ssl_options, "r", encoding="utf-8") as _file: return nginxparser.load(_file) except IOError: logger.warning("Missing NGINX TLS options file: %s", ssl_options) except UnicodeDecodeError: logger.warning("Could not read file: %s due to invalid character. " "Only UTF-8 encoding is supported.", ssl_options) except pyparsing.ParseBaseException as err: logger.warning("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 the given condition. :param list entry: The list to iterate over :param function condition: Returns true iff func should be executed on item :param function func: The function to call for each matching item """ if path is None: path = [] if isinstance(entry, list): if condition(entry): func(entry, path) else: for index, item in enumerate(entry): _do_for_subarray(item, condition, func, path + [index]) def get_best_match(target_name, names): """Finds the best match for target_name out of names using the Nginx name-matching rules (exact > longest wildcard starting with * > longest wildcard ending with * > regex). :param str target_name: The name to match :param set names: The candidate server names :returns: Tuple of (type of match, the name that matched) :rtype: tuple """ exact = [] wildcard_start = [] wildcard_end = [] regex = [] for name in names: if _exact_match(target_name, name): exact.append(name) elif _wildcard_match(target_name, name, True): wildcard_start.append(name) elif _wildcard_match(target_name, name, False): wildcard_end.append(name) elif _regex_match(target_name, name): regex.append(name) if exact: # There can be more than one exact match; e.g. eff.org, .eff.org match = min(exact, key=len) return ('exact', match) if wildcard_start: # Return the longest wildcard match = max(wildcard_start, key=len) return ('wildcard_start', match) if wildcard_end: # Return the longest wildcard match = max(wildcard_end, key=len) return ('wildcard_end', match) if regex: # Just return the first one for now match = regex[0] return ('regex', match) return (None, None) def _exact_match(target_name, name): target_lower = target_name.lower() return name.lower() in (target_lower, '.' + target_lower) def _wildcard_match(target_name, name, start): # Degenerate case if name == '*': return True parts = target_name.split('.') match_parts = name.split('.') # If the domain ends in a wildcard, do the match procedure in reverse if not start: parts.reverse() match_parts.reverse() # The first part must be a wildcard or blank, e.g. '.eff.org' first = match_parts.pop(0) if first not in ('*', ''): return False target_name_lower = '.'.join(parts).lower() name_lower = '.'.join(match_parts).lower() # Ex: www.eff.org matches *.eff.org, eff.org does not match *.eff.org return target_name_lower.endswith('.' + name_lower) def _regex_match(target_name, name): # Must start with a tilde if len(name) < 2 or name[0] != '~': return False # After tilde is a perl-compatible regex try: regex = re.compile(name[1:]) return re.match(regex, target_name) except re.error: # pragma: no cover # perl-compatible regexes are sometimes not recognized by python return False def _is_include_directive(entry): """Checks if an nginx parsed entry is an 'include' directive. :param list entry: the parsed entry :returns: Whether it's an 'include' directive :rtype: bool """ return (isinstance(entry, list) and 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. :param list entry: the parsed entry :returns: Whether it's an 'ssl on' directive :rtype: bool """ return (isinstance(entry, list) and len(entry) == 2 and entry[0] == 'ssl' and entry[1] == 'on') def _add_directives(directives, insert_at_top, block): """Adds directives to a config block.""" for directive in directives: _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')) 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')) INCLUDE = 'include' REPEATABLE_DIRECTIVES = {'server_name', 'listen', INCLUDE, 'rewrite', 'add_header'} COMMENT = ' managed by Certbot' COMMENT_BLOCK = [' ', '#', COMMENT] def comment_directive(block, location): """Add a ``#managed by Certbot`` comment to the end of the line at location. :param list block: The block containing the directive to be commented :param int location: The location within ``block`` of the directive to be commented """ next_entry = block[location + 1] if location + 1 < len(block) else None if isinstance(next_entry, list) and next_entry: if len(next_entry) >= 2 and next_entry[-2] == "#" and COMMENT in next_entry[-1]: return if isinstance(next_entry, nginxparser.UnspacedList): next_entry = next_entry.spaced[0] else: next_entry = next_entry[0] block.insert(location + 1, COMMENT_BLOCK[:]) 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 _find_location(block, directive_name, match_func=None): """Finds the index of the first instance of directive_name in block. If no line exists, use None.""" return next((index for index, line in enumerate(block) \ if line and line[0] == directive_name and (match_func is None or match_func(line))), None) def _is_whitespace_or_comment(directive): """Is this directive either a whitespace or comment directive?""" return len(directive) == 0 or directive[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, 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(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])) _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])) def _update_directive(block, directive, location): block[location] = directive comment_directive(block, location) 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) def _is_certbot_comment(directive): return '#' in directive and COMMENT in directive def _remove_directives(directive_name, match_func, block): """Removes directives of name directive_name from a config block if match_func matches. """ while True: location = _find_location(block, directive_name, match_func=match_func) if location is None: return # if the directive was made by us, remove the comment following if location + 1 < len(block) and _is_certbot_comment(block[location + 1]): del block[location + 1] del block[location] def _apply_global_addr_ssl(addr_to_ssl, parsed_server): """Apply global sslishness information to the parsed server block """ for addr in parsed_server['addrs']: addr.ssl = addr_to_ssl[addr.normalized_tuple()] if addr.ssl: parsed_server['ssl'] = True def _parse_server_raw(server): """Parses a list of server directives. :param list server: list of directives in a server block :rtype: dict """ addrs: Set[obj.Addr] = set() ssl: bool = False names: Set[str] = set() apply_ssl_to_all_addrs = False for directive in server: if not directive: continue if directive[0] == 'listen': addr = obj.Addr.fromstring(" ".join(directive[1:])) if addr: addrs.add(addr) if addr.ssl: ssl = True elif directive[0] == 'server_name': names.update(x.strip('"\'') for x in directive[1:]) elif _is_ssl_on_directive(directive): ssl = True apply_ssl_to_all_addrs = True if apply_ssl_to_all_addrs: for addr in addrs: addr.ssl = True return { 'addrs': addrs, 'ssl': ssl, 'names': names } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/parser_obj.py0000644000076500000240000003511200000000000022351 0ustar00bmwstaff# type: ignore # This module is not used for now, so we just skip type check for the sake of simplicity. """ This file contains parsing routines and object classes to help derive meaning from raw lists of tokens from pyparsing. """ import abc import logging from typing import List from certbot import errors logger = logging.getLogger(__name__) COMMENT = " managed by Certbot" COMMENT_BLOCK = ["#", COMMENT] class Parsable: """ Abstract base class for "Parsable" objects whose underlying representation is a tree of lists. :param .Parsable parent: This object's parsed parent in the tree """ __metaclass__ = abc.ABCMeta def __init__(self, parent=None): self._data: List[object] = [] self._tabs = None self.parent = parent @classmethod def parsing_hooks(cls): """Returns object types that this class should be able to `parse` recusrively. The order of the objects indicates the order in which the parser should try to parse each subitem. :returns: A list of Parsable classes. :rtype list: """ return (Block, Sentence, Statements) @staticmethod @abc.abstractmethod def should_parse(lists): """ Returns whether the contents of `lists` can be parsed into this object. :returns: Whether `lists` can be parsed as this object. :rtype bool: """ raise NotImplementedError() @abc.abstractmethod def parse(self, raw_list, add_spaces=False): """ Loads information into this object from underlying raw_list structure. Each Parsable object might make different assumptions about the structure of raw_list. :param list raw_list: A list or sublist of tokens from pyparsing, containing whitespace as separate tokens. :param bool add_spaces: If set, the method can and should manipulate and insert spacing between non-whitespace tokens and lists to delimit them. :raises .errors.MisconfigurationError: when the assumptions about the structure of raw_list are not met. """ raise NotImplementedError() @abc.abstractmethod def iterate(self, expanded=False, match=None): """ Iterates across this object. If this object is a leaf object, only yields itself. If it contains references other parsing objects, and `expanded` is set, this function should first yield itself, then recursively iterate across all of them. :param bool expanded: Whether to recursively iterate on possible children. :param callable match: If provided, an object is only iterated if this callable returns True when called on that object. :returns: Iterator over desired objects. """ raise NotImplementedError() @abc.abstractmethod def get_tabs(self): """ Guess at the tabbing style of this parsed object, based on whitespace. If this object is a leaf, it deducts the tabbing based on its own contents. Other objects may guess by calling `get_tabs` recursively on child objects. :returns: Guess at tabbing for this object. Should only return whitespace strings that does not contain newlines. :rtype str: """ raise NotImplementedError() @abc.abstractmethod def set_tabs(self, tabs=" "): """This tries to set and alter the tabbing of the current object to a desired whitespace string. Primarily meant for objects that were constructed, so they can conform to surrounding whitespace. :param str tabs: A whitespace string (not containing newlines). """ raise NotImplementedError() def dump(self, include_spaces=False): """ Dumps back to pyparsing-like list tree. The opposite of `parse`. Note: if this object has not been modified, `dump` with `include_spaces=True` should always return the original input of `parse`. :param bool include_spaces: If set to False, magically hides whitespace tokens from dumped output. :returns: Pyparsing-like list tree. :rtype list: """ return [elem.dump(include_spaces) for elem in self._data] class Statements(Parsable): """ A group or list of "Statements". A Statement is either a Block or a Sentence. The underlying representation is simply a list of these Statement objects, with an extra `_trailing_whitespace` string to keep track of the whitespace that does not precede any more statements. """ def __init__(self, parent=None): super().__init__(parent) self._trailing_whitespace = None # ======== Begin overridden functions @staticmethod def should_parse(lists): return isinstance(lists, list) def set_tabs(self, tabs=" "): """ Sets the tabbing for this set of statements. Does this by calling `set_tabs` on each of the child statements. Then, if a parent is present, sets trailing whitespace to parent tabbing. This is so that the trailing } of any Block that contains Statements lines up with parent tabbing. """ for statement in self._data: statement.set_tabs(tabs) if self.parent is not None: self._trailing_whitespace = "\n" + self.parent.get_tabs() def parse(self, raw_list, add_spaces=False): """ Parses a list of statements. Expects all elements in `raw_list` to be parseable by `type(self).parsing_hooks`, with an optional whitespace string at the last index of `raw_list`. """ if not isinstance(raw_list, list): raise errors.MisconfigurationError("Statements parsing expects a list!") # If there's a trailing whitespace in the list of statements, keep track of it. if raw_list and isinstance(raw_list[-1], str) and raw_list[-1].isspace(): self._trailing_whitespace = raw_list[-1] raw_list = raw_list[:-1] self._data = [parse_raw(elem, self, add_spaces) for elem in raw_list] def get_tabs(self): """ Takes a guess at the tabbing of all contained Statements by retrieving the tabbing of the first Statement.""" if self._data: return self._data[0].get_tabs() return "" def dump(self, include_spaces=False): """ Dumps this object by first dumping each statement, then appending its trailing whitespace (if `include_spaces` is set) """ data = super().dump(include_spaces) if include_spaces and self._trailing_whitespace is not None: return data + [self._trailing_whitespace] return data def iterate(self, expanded=False, match=None): """ Combines each statement's iterator. """ for elem in self._data: for sub_elem in elem.iterate(expanded, match): yield sub_elem # ======== End overridden functions def _space_list(list_): """ Inserts whitespace between adjacent non-whitespace tokens. """ spaced_statement: List[str] = [] for i in reversed(range(len(list_))): spaced_statement.insert(0, list_[i]) if i > 0 and not list_[i].isspace() and not list_[i-1].isspace(): spaced_statement.insert(0, " ") return spaced_statement class Sentence(Parsable): """ A list of words. Non-whitespace words are typically separated with whitespace tokens. """ # ======== Begin overridden functions @staticmethod def should_parse(lists): """ Returns True if `lists` can be parseable as a `Sentence`-- that is, every element is a string type. :param list lists: The raw unparsed list to check. :returns: whether this lists is parseable by `Sentence`. """ return isinstance(lists, list) and len(lists) > 0 and \ all(isinstance(elem, str) for elem in lists) def parse(self, raw_list, add_spaces=False): """ Parses a list of string types into this object. If add_spaces is set, adds whitespace tokens between adjacent non-whitespace tokens.""" if add_spaces: raw_list = _space_list(raw_list) if not isinstance(raw_list, list) or \ any(not isinstance(elem, str) for elem in raw_list): raise errors.MisconfigurationError("Sentence parsing expects a list of string types.") self._data = raw_list def iterate(self, expanded=False, match=None): """ Simply yields itself. """ if match is None or match(self): yield self def set_tabs(self, tabs=" "): """ Sets the tabbing on this sentence. Inserts a newline and `tabs` at the beginning of `self._data`. """ if self._data[0].isspace(): return self._data.insert(0, "\n" + tabs) def dump(self, include_spaces=False): """ Dumps this sentence. If include_spaces is set, includes whitespace tokens.""" if not include_spaces: return self.words return self._data def get_tabs(self): """ Guesses at the tabbing of this sentence. If the first element is whitespace, returns the whitespace after the rightmost newline in the string. """ first = self._data[0] if not first.isspace(): return "" rindex = first.rfind("\n") return first[rindex+1:] # ======== End overridden functions @property def words(self): """ Iterates over words, but without spaces. Like Unspaced List. """ return [word.strip("\"\'") for word in self._data if not word.isspace()] def __getitem__(self, index): return self.words[index] def __contains__(self, word): return word in self.words class Block(Parsable): """ Any sort of block, denoted by a block name and curly braces, like so: The parsed block: block name { content 1; content 2; } might be represented with the list [names, contents], where names = ["block", " ", "name", " "] contents = [["\n ", "content", " ", "1"], ["\n ", "content", " ", "2"], "\n"] """ def __init__(self, parent=None): super().__init__(parent) self.names: Sentence = None self.contents: Block = None @staticmethod def should_parse(lists): """ Returns True if `lists` can be parseable as a `Block`-- that is, it's got a length of 2, the first element is a `Sentence` and the second can be a `Statements`. :param list lists: The raw unparsed list to check. :returns: whether this lists is parseable by `Block`. """ return isinstance(lists, list) and len(lists) == 2 and \ Sentence.should_parse(lists[0]) and isinstance(lists[1], list) def set_tabs(self, tabs=" "): """ Sets tabs by setting equivalent tabbing on names, then adding tabbing to contents.""" self.names.set_tabs(tabs) self.contents.set_tabs(tabs + " ") def iterate(self, expanded=False, match=None): """ Iterator over self, and if expanded is set, over its contents. """ if match is None or match(self): yield self if expanded: for elem in self.contents.iterate(expanded, match): yield elem def parse(self, raw_list, add_spaces=False): """ Parses a list that resembles a block. The assumptions that this routine makes are: 1. the first element of `raw_list` is a valid Sentence. 2. the second element of `raw_list` is a valid Statement. If add_spaces is set, we call it recursively on `names` and `contents`, and add an extra trailing space to `names` (to separate the block's opening bracket and the block name). """ if not Block.should_parse(raw_list): raise errors.MisconfigurationError("Block parsing expects a list of length 2. " "First element should be a list of string types (the block names), " "and second should be another list of statements (the block content).") self.names = Sentence(self) if add_spaces: raw_list[0].append(" ") self.names.parse(raw_list[0], add_spaces) self.contents = Statements(self) self.contents.parse(raw_list[1], add_spaces) self._data = [self.names, self.contents] def get_tabs(self): """ Guesses tabbing by retrieving tabbing guess of self.names. """ return self.names.get_tabs() def _is_comment(parsed_obj): """ Checks whether parsed_obj is a comment. :param .Parsable parsed_obj: :returns: whether parsed_obj represents a comment sentence. :rtype bool: """ if not isinstance(parsed_obj, Sentence): return False return parsed_obj.words[0] == "#" def _is_certbot_comment(parsed_obj): """ Checks whether parsed_obj is a "managed by Certbot" comment. :param .Parsable parsed_obj: :returns: whether parsed_obj is a "managed by Certbot" comment. :rtype bool: """ if not _is_comment(parsed_obj): return False if len(parsed_obj.words) != len(COMMENT_BLOCK): return False for i, word in enumerate(parsed_obj.words): if word != COMMENT_BLOCK[i]: return False return True def _certbot_comment(parent, preceding_spaces=4): """ A "Managed by Certbot" comment. :param int preceding_spaces: Number of spaces between the end of the previous statement and the comment. :returns: Sentence containing the comment. :rtype: .Sentence """ result = Sentence(parent) result.parse([" " * preceding_spaces] + COMMENT_BLOCK) return result def _choose_parser(parent, list_): """ Choose a parser from type(parent).parsing_hooks, depending on whichever hook returns True first. """ hooks = Parsable.parsing_hooks() if parent: hooks = type(parent).parsing_hooks() for type_ in hooks: if type_.should_parse(list_): return type_(parent) raise errors.MisconfigurationError( "None of the parsing hooks succeeded, so we don't know how to parse this set of lists.") def parse_raw(lists_, parent=None, add_spaces=False): """ Primary parsing factory function. :param list lists_: raw lists from pyparsing to parse. :param .Parent parent: The parent containing this object. :param bool add_spaces: Whether to pass add_spaces to the parser. :returns .Parsable: The parsed object. :raises errors.MisconfigurationError: If no parsing hook passes, and we can't determine which type to parse the raw lists into. """ parser = _choose_parser(parent, lists_) parser.parse(lists_, add_spaces) return parser ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888465.3794818 certbot-nginx-1.21.0/certbot_nginx/_internal/tls_configs/0000755000076500000240000000000000000000000022161 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/tls_configs/options-ssl-nginx-old.conf0000644000076500000240000000126000000000000027216 0ustar00bmwstaff# This file contains important security parameters. If you modify this file # manually, Certbot will be unable to automatically provide future security # updates. Instead, Certbot will print and log an error message with a path to # the up-to-date file that you will need to refer to when manually updating # this file. ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_protocols TLSv1.2; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/tls_configs/options-ssl-nginx-tls12-only.conf0000644000076500000240000000131100000000000030361 0ustar00bmwstaff# This file contains important security parameters. If you modify this file # manually, Certbot will be unable to automatically provide future security # updates. Instead, Certbot will print and log an error message with a path to # the up-to-date file that you will need to refer to when manually updating # this file. ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/tls_configs/options-ssl-nginx-tls13-session-tix-on.conf0000644000076500000240000000127000000000000032304 0ustar00bmwstaff# This file contains important security parameters. If you modify this file # manually, Certbot will be unable to automatically provide future security # updates. Instead, Certbot will print and log an error message with a path to # the up-to-date file that you will need to refer to when manually updating # this file. ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf0000644000076500000240000000132100000000000026440 0ustar00bmwstaff# This file contains important security parameters. If you modify this file # manually, Certbot will be unable to automatically provide future security # updates. Instead, Certbot will print and log an error message with a path to # the up-to-date file that you will need to refer to when manually updating # this file. ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888465.3779292 certbot-nginx-1.21.0/certbot_nginx.egg-info/0000755000076500000240000000000000000000000017366 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888465.0 certbot-nginx-1.21.0/certbot_nginx.egg-info/PKG-INFO0000644000076500000240000000212100000000000020457 0ustar00bmwstaffMetadata-Version: 2.1 Name: certbot-nginx Version: 1.21.0 Summary: Nginx plugin for Certbot Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Plugins Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities Requires-Python: >=3.6 License-File: LICENSE.txt UNKNOWN ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888465.0 certbot-nginx-1.21.0/certbot_nginx.egg-info/SOURCES.txt0000644000076500000240000000616400000000000021261 0ustar00bmwstaffLICENSE.txt MANIFEST.in README.rst setup.cfg setup.py certbot_nginx/__init__.py certbot_nginx.egg-info/PKG-INFO certbot_nginx.egg-info/SOURCES.txt certbot_nginx.egg-info/dependency_links.txt certbot_nginx.egg-info/entry_points.txt certbot_nginx.egg-info/requires.txt certbot_nginx.egg-info/top_level.txt certbot_nginx/_internal/__init__.py certbot_nginx/_internal/configurator.py certbot_nginx/_internal/constants.py certbot_nginx/_internal/display_ops.py certbot_nginx/_internal/http_01.py certbot_nginx/_internal/nginxparser.py certbot_nginx/_internal/obj.py certbot_nginx/_internal/parser.py certbot_nginx/_internal/parser_obj.py certbot_nginx/_internal/tls_configs/options-ssl-nginx-old.conf certbot_nginx/_internal/tls_configs/options-ssl-nginx-tls12-only.conf certbot_nginx/_internal/tls_configs/options-ssl-nginx-tls13-session-tix-on.conf certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf tests/boulder-integration.conf.sh tests/configurator_test.py tests/display_ops_test.py tests/http_01_test.py tests/nginxparser_test.py tests/obj_test.py tests/parser_obj_test.py tests/parser_test.py tests/test_util.py tests/testdata/etc_nginx/broken.conf tests/testdata/etc_nginx/comment_in_file.conf tests/testdata/etc_nginx/edge_cases.conf tests/testdata/etc_nginx/foo.conf tests/testdata/etc_nginx/invalid_unicode_comments.conf tests/testdata/etc_nginx/mime.types tests/testdata/etc_nginx/minimalistic_comments.conf tests/testdata/etc_nginx/multiline_quotes.conf tests/testdata/etc_nginx/nginx.conf tests/testdata/etc_nginx/server.conf tests/testdata/etc_nginx/valid_unicode_comments.conf tests/testdata/etc_nginx/sites-enabled/both.com tests/testdata/etc_nginx/sites-enabled/default tests/testdata/etc_nginx/sites-enabled/example.com tests/testdata/etc_nginx/sites-enabled/example.net tests/testdata/etc_nginx/sites-enabled/globalssl.com tests/testdata/etc_nginx/sites-enabled/headers.com tests/testdata/etc_nginx/sites-enabled/ipv6.com tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com tests/testdata/etc_nginx/sites-enabled/migration.com tests/testdata/etc_nginx/sites-enabled/sslon.com tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888465.0 certbot-nginx-1.21.0/certbot_nginx.egg-info/dependency_links.txt0000644000076500000240000000000100000000000023434 0ustar00bmwstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888465.0 certbot-nginx-1.21.0/certbot_nginx.egg-info/entry_points.txt0000644000076500000240000000012200000000000022657 0ustar00bmwstaff[certbot.plugins] nginx = certbot_nginx._internal.configurator:NginxConfigurator ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888465.0 certbot-nginx-1.21.0/certbot_nginx.egg-info/requires.txt0000644000076500000240000000012300000000000021762 0ustar00bmwstaffacme>=1.21.0 certbot>=1.21.0 PyOpenSSL>=17.3.0 pyparsing>=2.2.0 setuptools>=39.0.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888465.0 certbot-nginx-1.21.0/certbot_nginx.egg-info/top_level.txt0000644000076500000240000000001600000000000022115 0ustar00bmwstaffcertbot_nginx ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.384391 certbot-nginx-1.21.0/setup.cfg0000644000076500000240000000010300000000000014642 0ustar00bmwstaff[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888439.0 certbot-nginx-1.21.0/setup.py0000644000076500000240000000332700000000000014546 0ustar00bmwstafffrom setuptools import find_packages from setuptools import setup version = '1.21.0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin # version for simplicity. See # https://github.com/certbot/certbot/issues/8761 for more info. f'acme>={version}', f'certbot>={version}', 'PyOpenSSL>=17.3.0', 'pyparsing>=2.2.0', 'setuptools>=39.0.1', ] setup( name='certbot-nginx', version=version, description="Nginx plugin for Certbot", url='https://github.com/letsencrypt/letsencrypt', author="Certbot Project", author_email='certbot-dev@eff.org', license='Apache License 2.0', python_requires='>=3.6', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Plugins', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', 'Topic :: System :: Networking', 'Topic :: System :: Systems Administration', 'Topic :: Utilities', ], packages=find_packages(), include_package_data=True, install_requires=install_requires, entry_points={ 'certbot.plugins': [ 'nginx = certbot_nginx._internal.configurator:NginxConfigurator', ], }, ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.380541 certbot-nginx-1.21.0/tests/0000755000076500000240000000000000000000000014171 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/boulder-integration.conf.sh0000755000076500000240000000514100000000000021432 0ustar00bmwstaff#!/usr/bin/env bash # Based on # https://www.exratione.com/2014/03/running-nginx-as-a-non-root-user/ # https://github.com/exratione/non-root-nginx/blob/9a77f62e5d5cb9c9026fd62eece76b9514011019/nginx.conf # USAGE: ./boulder-integration.conf.sh /path/to/root cert.key cert.pem >> nginx.conf ROOT=$1 CERT_KEY_PATH=$2 CERT_PATH=$3 cat <= 1 and x[0] == 2, lambda x, y, pts=paths: pts.append(y)) self.assertEqual(paths, result) def test_get_vhosts_global_ssl(self): nparser = parser.NginxParser(self.config_path) vhosts = nparser.get_vhosts() vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'), [obj.Addr('4.8.2.6', '57', True, False, False, False)], True, True, {'globalssl.com'}, [], [0]) globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0] self.assertEqual(vhost, globalssl_com) def test_get_vhosts(self): nparser = parser.NginxParser(self.config_path) vhosts = nparser.get_vhosts() vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'), [obj.Addr('', '8080', False, False, False, False)], False, True, {'localhost', r'~^(www\.)?(example|bar)\.'}, [], [10, 1, 9]) vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'), [obj.Addr('somename', '8080', False, False, False, False), obj.Addr('', '8000', False, False, False, False)], False, True, {'somename', 'another.alias', 'alias'}, [], [10, 1, 12]) vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'), [obj.Addr('69.50.225.155', '9000', False, False, False, False), obj.Addr('127.0.0.1', '', False, False, False, False)], False, True, {'.example.com', 'example.*'}, [], [0]) vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'), [obj.Addr('myhost', '', False, True, False, False), obj.Addr('otherhost', '', False, True, False, False)], False, True, {'www.example.org'}, [], [0]) vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'), [obj.Addr('*', '80', True, True, False, False)], True, True, {'*.www.foo.com', '*.www.example.com'}, [], [2, 1, 0]) self.assertEqual(19, len(vhosts)) example_com = [x for x in vhosts if 'example.com' in x.filep][0] self.assertEqual(vhost3, example_com) default = [x for x in vhosts if 'default' in x.filep][0] self.assertEqual(vhost4, default) fooconf = [x for x in vhosts if 'foo.conf' in x.filep][0] self.assertEqual(vhost5, fooconf) localhost = [x for x in vhosts if 'localhost' in x.names][0] self.assertEqual(vhost1, localhost) somename = [x for x in vhosts if 'somename' in x.names][0] self.assertEqual(vhost2, somename) def test_has_ssl_on_directive(self): 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'], ['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']] self.assertFalse(nparser.has_ssl_on_directive(mock_vhost)) mock_vhost.raw = [['listen', '80'], ['ssl', 'on'], ['server_name', '*.www.foo.com', '*.www.example.com']] self.assertTrue(nparser.has_ssl_on_directive(mock_vhost)) def test_remove_server_directives(self): nparser = parser.NginxParser(self.config_path) mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'), None, None, None, {'localhost', r'~^(www\.)?(example|bar)\.'}, None, [10, 1, 9]) example_com = nparser.abs_path('sites-enabled/example.com') names = {'.example.com', 'example.*'} mock_vhost.filep = example_com mock_vhost.names = names mock_vhost.path = [0] nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert2.pem']]) nparser.remove_server_directives(mock_vhost, 'foo') nparser.remove_server_directives(mock_vhost, 'ssl_certificate') 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.*'], []]]]) def test_add_server_directives(self): nparser = parser.NginxParser(self.config_path) mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'), None, None, None, {'localhost', r'~^(www\.)?(example|bar)\.'}, None, [10, 1, 9]) nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['\n ', 'ssl_certificate', ' ', '/etc/ssl/cert.pem']]) ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem') dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')]) self.assertEqual(1, len(re.findall(ssl_re, dump))) example_com = nparser.abs_path('sites-enabled/example.com') names = {'.example.com', 'example.*'} mock_vhost.filep = example_com mock_vhost.names = names mock_vhost.path = [0] nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert2.pem']]) nparser.add_server_directives(mock_vhost, [['foo', 'bar']]) from certbot_nginx._internal.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.*'], ['foo', 'bar'], ['#', COMMENT], ['ssl_certificate', '/etc/ssl/cert2.pem'], ['#', COMMENT], [], [] ]]]) server_conf = nparser.abs_path('server.conf') names = {'alias', 'another.alias', 'somename'} mock_vhost.filep = server_conf mock_vhost.names = names mock_vhost.path = [] self.assertRaises(errors.MisconfigurationError, nparser.add_server_directives, mock_vhost, [['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert2.pem']]) 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, {'.example.com', 'example.*'}, None, [0]) nparser.add_server_directives(mock_vhost, [['\n ', '#', ' ', 'what a nice comment']]) nparser.add_server_directives(mock_vhost, [['\n ', 'include', ' ', nparser.abs_path('comment_in_file.conf')]]) from certbot_nginx._internal.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) target = {'.example.com', 'example.*'} filep = nparser.abs_path('sites-enabled/example.com') mock_vhost = obj.VirtualHost(filep, None, None, None, target, None, [0]) nparser.update_or_add_server_directives( mock_vhost, [['server_name', 'foobar.com']]) from certbot_nginx._internal.parser import COMMENT self.assertEqual( nparser.parsed[filep], [[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], ['server_name', 'foobar.com'], ['#', COMMENT], ['server_name', 'example.*'], [] ]]]) mock_vhost.names = {'foobar.com', 'example.*'} nparser.update_or_add_server_directives( mock_vhost, [['ssl_certificate', 'cert.pem']]) self.assertEqual( nparser.parsed[filep], [[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], ['server_name', 'foobar.com'], ['#', COMMENT], ['server_name', 'example.*'], [], ['ssl_certificate', 'cert.pem'], ['#', COMMENT], [], ]]]) def test_get_best_match(self): target_name = 'www.eff.org' names = [{'www.eff.org', 'irrelevant.long.name.eff.org', '*.org'}, {'eff.org', 'ww2.eff.org', 'test.www.eff.org'}, {'*.eff.org', '.www.eff.org'}, {'.eff.org', '*.org'}, {'www.eff.', 'www.eff.*', '*.www.eff.org'}, {'example.com', r'~^(www\.)?(eff.+)', '*.eff.*'}, {'*', r'~^(www\.)?(eff.+)'}, {'www.*', r'~^(www\.)?(eff.+)', '.test.eff.org'}, {'*.org', r'*.eff.org', 'www.eff.*'}, {'*.www.eff.org', 'www.*'}, {'*.org'}, set(), {'example.com'}, {'www.Eff.org'}, {'.efF.org'}] winners = [('exact', 'www.eff.org'), (None, None), ('exact', '.www.eff.org'), ('wildcard_start', '.eff.org'), ('wildcard_end', 'www.eff.*'), ('regex', r'~^(www\.)?(eff.+)'), ('wildcard_start', '*'), ('wildcard_end', 'www.*'), ('wildcard_start', '*.eff.org'), ('wildcard_end', 'www.*'), ('wildcard_start', '*.org'), (None, None), (None, None), ('exact', 'www.Eff.org'), ('wildcard_start', '.efF.org')] for i, winner in enumerate(winners): self.assertEqual(winner, parser.get_best_match(target_name, names[i])) def test_comment_directive(self): # pylint: disable=protected-access block = nginxparser.UnspacedList([ ["\n", "a", " ", "b", "\n"], ["c", " ", "d"], ["\n", "e", " ", "f"]]) from certbot_nginx._internal.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"]]) 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._internal.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'] ]) self.assertFalse(server['ssl']) server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443', 'ssl'] ]) self.assertTrue(server['ssl']) server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443'], ['ssl', 'off'] ]) self.assertFalse(server['ssl']) server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443'], ['ssl', 'on'] ]) 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) server = nparser.parse_server([ ['listen', '443'] ]) self.assertTrue(server['ssl']) def test_duplicate_vhost(self): nparser = parser.NginxParser(self.config_path) vhosts = nparser.get_vhosts() default = [x for x in vhosts if 'default' in x.filep][0] new_vhost = nparser.duplicate_vhost(default, remove_singleton_listen_params=True) nparser.filedump(ext='') # check properties of new vhost self.assertFalse(next(iter(new_vhost.addrs)).default) self.assertNotEqual(new_vhost.path, default.path) # check that things are written to file correctly new_nparser = parser.NginxParser(self.config_path) new_vhosts = new_nparser.get_vhosts() new_defaults = [x for x in new_vhosts if 'default' in x.filep] self.assertEqual(len(new_defaults), 2) new_vhost_parsed = new_defaults[1] self.assertFalse(next(iter(new_vhost_parsed.addrs)).default) self.assertEqual(next(iter(default.names)), next(iter(new_vhost_parsed.names))) self.assertEqual(len(default.raw), len(new_vhost_parsed.raw)) self.assertTrue(next(iter(default.addrs)).super_eq(next(iter(new_vhost_parsed.addrs)))) def test_duplicate_vhost_remove_ipv6only(self): nparser = parser.NginxParser(self.config_path) vhosts = nparser.get_vhosts() ipv6ssl = [x for x in vhosts if 'ipv6ssl' in x.filep][0] new_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=True) nparser.filedump(ext='') for addr in new_vhost.addrs: self.assertFalse(addr.ipv6only) identical_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=False) nparser.filedump(ext='') called = False for addr in identical_vhost.addrs: if addr.ipv6: self.assertTrue(addr.ipv6only) called = True self.assertTrue(called) def test_valid_unicode_characters(self): nparser = parser.NginxParser(self.config_path) path = nparser.abs_path('valid_unicode_comments.conf') parsed = nparser._parse_files(path) # pylint: disable=protected-access self.assertEqual(['server'], parsed[0][2][0]) self.assertEqual(['listen', '80'], parsed[0][2][1][3]) def test_valid_unicode_roundtrip(self): """This tests the parser's ability to load and save a config containing Unicode""" nparser = parser.NginxParser(self.config_path) nparser._parse_files( nparser.abs_path('valid_unicode_comments.conf') ) # pylint: disable=protected-access nparser.filedump(lazy=False) def test_invalid_unicode_characters(self): with self.assertLogs() as log: nparser = parser.NginxParser(self.config_path) path = nparser.abs_path('invalid_unicode_comments.conf') parsed = nparser._parse_files(path) # pylint: disable=protected-access self.assertEqual([], parsed) self.assertTrue(any( ('invalid character' in output) and ('UTF-8' in output) for output in log.output )) def test_valid_unicode_characters_in_ssl_options(self): nparser = parser.NginxParser(self.config_path) path = nparser.abs_path('valid_unicode_comments.conf') parsed = parser._parse_ssl_options(path) # pylint: disable=protected-access self.assertEqual(['server'], parsed[2][0]) self.assertEqual(['listen', '80'], parsed[2][1][3]) def test_invalid_unicode_characters_in_ssl_options(self): with self.assertLogs() as log: nparser = parser.NginxParser(self.config_path) path = nparser.abs_path('invalid_unicode_comments.conf') parsed = parser._parse_ssl_options(path) # pylint: disable=protected-access self.assertEqual([], parsed) self.assertTrue(any( ('invalid character' in output) and ('UTF-8' in output) for output in log.output )) if __name__ == "__main__": unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/test_util.py0000644000076500000240000001116400000000000016562 0ustar00bmwstaff"""Common utilities for certbot_nginx.""" import copy import shutil import tempfile import josepy as jose try: import mock except ImportError: # pragma: no cover from unittest import mock # type: ignore import pkg_resources from certbot import util from certbot.compat import os from certbot.plugins import common from certbot.tests import util as test_util from certbot_nginx._internal import configurator from certbot_nginx._internal import nginxparser class NginxTest(test_util.ConfigTestCase): def setUp(self): super().setUp() self.configuration = self.config self.config = None self.temp_dir, self.config_dir, self.work_dir = common.dir_setup( "etc_nginx", __name__) self.logs_dir = tempfile.mkdtemp('logs') self.config_path = os.path.join(self.temp_dir, "etc_nginx") self.rsa512jwk = jose.JWKRSA.load(test_util.load_vector( "rsa512_key.pem")) def tearDown(self): # Cleanup opened resources after a test. This is usually done through atexit handlers in # Certbot, but during tests, atexit will not run registered functions before tearDown is # called and instead will run them right before the entire test process exits. # It is a problem on Windows, that does not accept to clean resources before closing them. util._release_locks() # pylint: disable=protected-access shutil.rmtree(self.temp_dir) shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) shutil.rmtree(self.logs_dir) def get_nginx_configurator(self, config_path, config_dir, work_dir, logs_dir, version=(1, 6, 2), openssl_version="1.0.2g"): """Create an Nginx Configurator with the specified options.""" backups = os.path.join(work_dir, "backups") self.configuration.nginx_server_root = config_path self.configuration.nginx_sleep_seconds = 0.1234 self.configuration.le_vhost_ext = "-le-ssl.conf" self.configuration.config_dir = config_dir self.configuration.work_dir = work_dir self.configuration.logs_dir = logs_dir self.configuration.backup_dir = backups self.configuration.temp_checkpoint_dir = os.path.join(work_dir, "temp_checkpoints") self.configuration.in_progress_dir = os.path.join(backups, "IN_PROGRESS") self.configuration.server = "https://acme-server.org:443/new" self.configuration.http01_port = 80 self.configuration.https_port = 5001 with mock.patch("certbot_nginx._internal.configurator.NginxConfigurator." "config_test"): with mock.patch("certbot_nginx._internal.configurator.util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True config = configurator.NginxConfigurator( self.configuration, name="nginx", version=version, openssl_version=openssl_version) config.prepare() return config def get_data_filename(filename): """Gets the filename of a test data file.""" return pkg_resources.resource_filename( __name__, os.path.join( "testdata", "etc_nginx", filename)) def filter_comments(tree): """Filter comment nodes from parsed configurations.""" def traverse(tree): """Generator dropping comment nodes""" for entry in tree: # key, values = entry spaceless = [e for e in entry if not nginxparser.spacey(e)] if spaceless: key = spaceless[0] values = spaceless[1] if len(spaceless) > 1 else None else: key = values = "" if isinstance(key, list): new = copy.deepcopy(entry) new[1] = filter_comments(values) yield new else: if key != '#' and spaceless: yield spaceless return list(traverse(tree)) def contains_at_depth(haystack, needle, n): """Is the needle in haystack at depth n? Return true if the needle is present in one of the sub-iterables in haystack at depth n. Haystack must be an iterable. """ # Specifically use hasattr rather than isinstance(..., collections.Iterable) # because we want to include lists but reject strings. if not hasattr(haystack, '__iter__') or hasattr(haystack, 'strip'): return False if n == 0: return needle in haystack for item in haystack: if contains_at_depth(item, needle, n - 1): return True return False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.376223 certbot-nginx-1.21.0/tests/testdata/0000755000076500000240000000000000000000000016002 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888465.3816252 certbot-nginx-1.21.0/tests/testdata/etc_nginx/0000755000076500000240000000000000000000000017760 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/broken.conf0000644000076500000240000000017500000000000022112 0ustar00bmwstaff# A faulty configuration file pid logs/nginx.pid; events { worker_connections 1024; } include foo.conf; @@@ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/comment_in_file.conf0000644000076500000240000000003100000000000023750 0ustar00bmwstaff# a comment inside a file././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/edge_cases.conf0000644000076500000240000000110300000000000022704 0ustar00bmwstaff# This is not a valid nginx config file but it tests edge cases in valid nginx syntax server { server_name simple; } server { server_name with.if; location ~ ^/services/.+$ { 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 Cache-Control 'public, must-revalidate, proxy-revalidate' "test,;{}" foo; blah "hello;world"; try_files $uri @rewrites; } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/foo.conf0000644000076500000240000000103400000000000021410 0ustar00bmwstaff# a test nginx conf user www-data; http { server { listen *:80 default_server ssl; server_name *.www.foo.com *.www.example.com; root /home/ubuntu/sites/foo/; location /status { types { image/jpeg jpg; } } location ~ case_sensitive\.php$ { index index.php; root /var/root; } location ~* case_insensitive\.php$ {} location = exact_match\.php$ {} location ^~ ignore_regex\.php$ {} } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/invalid_unicode_comments.conf0000644000076500000240000000026500000000000025673 0ustar00bmwstaff# This configuration file is saved with EUC-KR (a.k.a. cp949) encoding, # including some Korean letters. server { # ȳϼ. 80 Ʈ û ٸ. listen 80; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/mime.types0000644000076500000240000000000000000000000021763 0ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/minimalistic_comments.conf0000644000076500000240000000030500000000000025214 0ustar00bmwstaff# 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; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/multiline_quotes.conf0000644000076500000240000000111100000000000024223 0ustar00bmwstaff# 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'; } } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/nginx.conf0000644000076500000240000000517400000000000021761 0ustar00bmwstaff# standard default nginx config 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; } empty { } 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; # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # proxy the PHP scripts to Nginx listening on 127.0.0.1:80 # location ~ \.php$ { proxy_pass http://127.0.0.1; } # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # location ~ \.php$ { root html; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; } # deny access to .htaccess files, if Nginx's document root # concurs with nginx's one # location ~ /\.ht { deny all; } } # another virtual host using mix of IP-, name-, and port-based configuration # server { listen 8000; listen somename:8080; include server.conf; location / { root html; index index.html index.htm; } } # HTTPS server # #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; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} #include conf.d/test.conf; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/server.conf0000644000076500000240000000005500000000000022135 0ustar00bmwstaffserver_name somename alias another.alias; ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.382597 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/0000755000076500000240000000000000000000000022477 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/both.com0000644000076500000240000000076500000000000024143 0ustar00bmwstaffserver { server_name ssl.both.com; } # a duplicate vhost server { server_name ssl.both.com; } # a duplicate by means of wildcard server { server_name *.both.com; } # combined HTTP and HTTPS server { server_name ssl.both.com; listen 80; listen 5001 ssl; ssl_certificate cert.pem; ssl_certificate_key cert.key; } # HTTPS, duplicate by means of wildcard server { server_name *.both.com; listen 5001 ssl; ssl_certificate cert.pem; ssl_certificate_key cert.key; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/default0000644000076500000240000000032300000000000024044 0ustar00bmwstaffserver { listen myhost default_server; listen otherhost default_server; server_name "www.example.org"; location / { root html; index index.html index.htm; } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/example.com0000644000076500000240000000020500000000000024627 0ustar00bmwstaffserver { listen 69.50.225.155:9000; listen 127.0.0.1; server_name .example.com; server_name example.*; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/example.net0000644000076500000240000000011100000000000024633 0ustar00bmwstaffserver { listen 80; listen [::]:80; server_name $hostname; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/globalssl.com0000644000076500000240000000022000000000000025153 0ustar00bmwstaffserver { server_name globalssl.com; listen 4.8.2.6:57; } server { server_name globalsslsetssl.com; listen 4.8.2.6:57 ssl; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/headers.com0000644000076500000240000000012700000000000024612 0ustar00bmwstaffserver { server_name headers.com; add_header X-Content-Type-Options nosniff; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/ipv6.com0000644000076500000240000000011000000000000024053 0ustar00bmwstaffserver { listen 80; listen [::]:80; server_name ipv6.com; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com0000644000076500000240000000023400000000000024604 0ustar00bmwstaffserver { listen 443 ssl; listen [::]:443 ssl ipv6only=on; listen 5001 ssl; listen [::]:5001 ssl ipv6only=on; server_name ipv6ssl.com; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/migration.com0000644000076500000240000000057700000000000025201 0ustar00bmwstaffserver { server_name migration.com; server_name summer.com; } server { listen 443 ssl; server_name migration.com; server_name geese.com; ssl_certificate cert.pem; ssl_certificate_key cert.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/sites-enabled/sslon.com0000644000076500000240000000015700000000000024340 0ustar00bmwstaffserver { server_name sslon.com; ssl on; ssl_certificate snakeoil.cert; ssl_certificate_key snakeoil.key; } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.376388 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/0000755000076500000240000000000000000000000023375 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888465.376429 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/0000755000076500000240000000000000000000000026244 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888465.3838382 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/0000755000076500000240000000000000000000000027367 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params0000644000076500000240000000161700000000000032302 0ustar00bmwstafffastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SCRIPT_FILENAME $request_filename; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; fastcgi_param HTTPS $https if_not_empty; # PHP only, required if PHP was built with --enable-force-cgi-redirect fastcgi_param REDIRECT_STATUS 200; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf0000644000076500000240000000432200000000000030671 0ustar00bmwstaff# This map is not a full koi8-r <> utf8 map: it does not contain # box-drawing and some other characters. Besides this map contains # several koi8-u and Byelorussian letters which are not in koi8-r. # If you need a full and standard map, use contrib/unicode2nginx/koi-utf # map instead. charset_map koi8-r utf-8 { 80 E282AC; # euro 95 E280A2; # bullet 9A C2A0; #   9E C2B7; # · A3 D191; # small yo A4 D194; # small Ukrainian ye A6 D196; # small Ukrainian i A7 D197; # small Ukrainian yi AD D291; # small Ukrainian soft g AE D19E; # small Byelorussian short u B0 C2B0; # ° B3 D081; # capital YO B4 D084; # capital Ukrainian YE B6 D086; # capital Ukrainian I B7 D087; # capital Ukrainian YI B9 E28496; # numero sign BD D290; # capital Ukrainian soft G BE D18E; # capital Byelorussian short U BF C2A9; # (C) C0 D18E; # small yu C1 D0B0; # small a C2 D0B1; # small b C3 D186; # small ts C4 D0B4; # small d C5 D0B5; # small ye C6 D184; # small f C7 D0B3; # small g C8 D185; # small kh C9 D0B8; # small i CA D0B9; # small j CB D0BA; # small k CC D0BB; # small l CD D0BC; # small m CE D0BD; # small n CF D0BE; # small o D0 D0BF; # small p D1 D18F; # small ya D2 D180; # small r D3 D181; # small s D4 D182; # small t D5 D183; # small u D6 D0B6; # small zh D7 D0B2; # small v D8 D18C; # small soft sign D9 D18B; # small y DA D0B7; # small z DB D188; # small sh DC D18D; # small e DD D189; # small shch DE D187; # small ch DF D18A; # small hard sign E0 D0AE; # capital YU E1 D090; # capital A E2 D091; # capital B E3 D0A6; # capital TS E4 D094; # capital D E5 D095; # capital YE E6 D0A4; # capital F E7 D093; # capital G E8 D0A5; # capital KH E9 D098; # capital I EA D099; # capital J EB D09A; # capital K EC D09B; # capital L ED D09C; # capital M EE D09D; # capital N EF D09E; # capital O F0 D09F; # capital P F1 D0AF; # capital YA F2 D0A0; # capital R F3 D0A1; # capital S F4 D0A2; # capital T F5 D0A3; # capital U F6 D096; # capital ZH F7 D092; # capital V F8 D0AC; # capital soft sign F9 D0AB; # capital Y FA D097; # capital Z FB D0A8; # capital SH FC D0AD; # capital E FD D0A9; # capital SHCH FE D0A7; # capital CH FF D0AA; # capital hard sign } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win0000644000076500000240000000341500000000000030672 0ustar00bmwstaffcharset_map koi8-r windows-1251 { 80 88; # euro 95 95; # bullet 9A A0; #   9E B7; # · A3 B8; # small yo A4 BA; # small Ukrainian ye A6 B3; # small Ukrainian i A7 BF; # small Ukrainian yi AD B4; # small Ukrainian soft g AE A2; # small Byelorussian short u B0 B0; # ° B3 A8; # capital YO B4 AA; # capital Ukrainian YE B6 B2; # capital Ukrainian I B7 AF; # capital Ukrainian YI B9 B9; # numero sign BD A5; # capital Ukrainian soft G BE A1; # capital Byelorussian short U BF A9; # (C) C0 FE; # small yu C1 E0; # small a C2 E1; # small b C3 F6; # small ts C4 E4; # small d C5 E5; # small ye C6 F4; # small f C7 E3; # small g C8 F5; # small kh C9 E8; # small i CA E9; # small j CB EA; # small k CC EB; # small l CD EC; # small m CE ED; # small n CF EE; # small o D0 EF; # small p D1 FF; # small ya D2 F0; # small r D3 F1; # small s D4 F2; # small t D5 F3; # small u D6 E6; # small zh D7 E2; # small v D8 FC; # small soft sign D9 FB; # small y DA E7; # small z DB F8; # small sh DC FD; # small e DD F9; # small shch DE F7; # small ch DF FA; # small hard sign E0 DE; # capital YU E1 C0; # capital A E2 C1; # capital B E3 D6; # capital TS E4 C4; # capital D E5 C5; # capital YE E6 D4; # capital F E7 C3; # capital G E8 D5; # capital KH E9 C8; # capital I EA C9; # capital J EB CA; # capital K EC CB; # capital L ED CC; # capital M EE CD; # capital N EF CE; # capital O F0 CF; # capital P F1 DF; # capital YA F2 D0; # capital R F3 D1; # capital S F4 D2; # capital T F5 D3; # capital U F6 C6; # capital ZH F7 C2; # capital V F8 DC; # capital soft sign F9 DB; # capital Y FA C7; # capital Z FB D8; # capital SH FC DD; # capital E FD D9; # capital SHCH FE D7; # capital CH FF DA; # capital hard sign } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types0000644000076500000240000000404500000000000031407 0ustar00bmwstafftypes { text/html html htm shtml; text/css css; text/xml xml rss; image/gif gif; image/jpeg jpeg jpg; application/x-javascript js; application/atom+xml atom; text/mathml mml; text/plain txt; text/vnd.sun.j2me.app-descriptor jad; text/vnd.wap.wml wml; text/x-component htc; image/png png; image/tiff tif tiff; image/vnd.wap.wbmp wbmp; image/x-icon ico; image/x-jng jng; image/x-ms-bmp bmp; image/svg+xml svg svgz; application/java-archive jar war ear; application/json json; application/mac-binhex40 hqx; application/msword doc; application/pdf pdf; application/postscript ps eps ai; application/rtf rtf; application/vnd.ms-excel xls; application/vnd.ms-powerpoint ppt; application/vnd.wap.wmlc wmlc; application/vnd.google-earth.kml+xml kml; application/vnd.google-earth.kmz kmz; application/x-7z-compressed 7z; application/x-cocoa cco; application/x-java-archive-diff jardiff; application/x-java-jnlp-file jnlp; application/x-makeself run; application/x-perl pl pm; application/x-pilot prc pdb; application/x-rar-compressed rar; application/x-redhat-package-manager rpm; application/x-sea sea; application/x-shockwave-flash swf; application/x-stuffit sit; application/x-tcl tcl tk; application/x-x509-ca-cert der pem crt; application/x-xpinstall xpi; application/xhtml+xml xhtml; application/zip zip; application/octet-stream bin exe dll; application/octet-stream deb; application/octet-stream dmg; application/octet-stream eot; application/octet-stream iso img; application/octet-stream msi msp msm; application/ogg ogx; audio/midi mid midi kar; audio/mpeg mpga mpega mp2 mp3 m4a; audio/ogg oga ogg spx; audio/x-realaudio ra; audio/webm weba; video/3gpp 3gpp 3gp; video/mp4 mp4; video/mpeg mpeg mpg mpe; video/ogg ogv; video/quicktime mov; video/webm webm; video/x-flv flv; video/x-mng mng; video/x-ms-asf asx asf; video/x-ms-wmv wmv; video/x-msvideo avi; } ././@PaxHeader0000000000000000000000000000021000000000000010206 xustar00114 path=certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 22 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.10000644000076500000240000000033600000000000032134 0ustar00bmwstaff[nx_extract] username = naxsi_web password = test port = 8081 rules_path = /etc/nginx/naxsi_core.rules [nx_intercept] port = 8080 [sql] dbtype = sqlite username = root password = hostname = 127.0.0.1 dbname = naxsi_sig ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules0000644000076500000240000000043700000000000031571 0ustar00bmwstaff# Sample rules file for default vhost. LearningMode; SecRulesEnabled; #SecRulesDisabled; DeniedUrl "/RequestDenied"; ## check rules CheckRule "$SQL >= 8" BLOCK; CheckRule "$RFI >= 8" BLOCK; CheckRule "$TRAVERSAL >= 4" BLOCK; CheckRule "$EVADE >= 4" BLOCK; CheckRule "$XSS >= 8" BLOCK; ././@PaxHeader0000000000000000000000000000020500000000000010212 xustar00111 path=certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules 22 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rule0000644000076500000240000001225000000000000032412 0ustar00bmwstaff################################## ## INTERNAL RULES IDS:1-10 ## ################################## #weird_request : 1 #big_body : 2 #no_content_type : 3 #MainRule "str:yesone" "msg:foobar test pattern" "mz:ARGS" "s:$SQL:42" id:1999; ################################## ## SQL Injections IDs:1000-1099 ## ################################## MainRule "rx:select|union|update|delete|insert|table|from|ascii|hex|unhex" "msg:sql keywords" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1000; MainRule "str:\"" "msg:double quote" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1001; MainRule "str:0x" "msg:0x, possible hex encoding" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:2" id:1002; ## Hardcore rules MainRule "str:/*" "msg:mysql comment (/*)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1003; MainRule "str:*/" "msg:mysql comment (*/)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1004; MainRule "str:|" "msg:mysql keyword (|)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1005; MainRule "rx:&&" "msg:mysql keyword (&&)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1006; ## end of hardcore rules MainRule "str:--" "msg:mysql comment (--)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1007; MainRule "str:;" "msg:; in stuff" "mz:BODY|URL|ARGS" "s:$SQL:4" id:1008; MainRule "str:=" "msg:equal in var, probable sql/xss" "mz:ARGS|BODY" "s:$SQL:2" id:1009; MainRule "str:(" "msg:parenthesis, probable sql/xss" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1010; MainRule "str:)" "msg:parenthesis, probable sql/xss" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1011; MainRule "str:'" "msg:simple quote" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1013; MainRule "str:\"" "msg:double quote" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1014; MainRule "str:," "msg:, in stuff" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1015; MainRule "str:#" "msg:mysql comment (#)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1016; ############################### ## OBVIOUS RFI IDs:1100-1199 ## ############################### MainRule "str:http://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1100; MainRule "str:https://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1101; MainRule "str:ftp://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1102; MainRule "str:php://" "msg:html comment tag" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$RFI:8" id:1103; ####################################### ## Directory traversal IDs:1200-1299 ## ####################################### MainRule "str:.." "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1200; MainRule "str:/etc/passwd" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1202; MainRule "str:c:\\" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1203; MainRule "str:cmd.exe" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1204; MainRule "str:\\" "msg:html comment tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:4" id:1205; #MainRule "str:/" "msg:slash in args" "mz:ARGS|BODY|$HEADERS_VAR:Cookie" "s:$TRAVERSAL:2" id:1206; ######################################## ## Cross Site Scripting IDs:1300-1399 ## ######################################## MainRule "str:<" "msg:html open tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1302; MainRule "str:>" "msg:html close tag" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1303; MainRule "str:'" "msg:simple quote" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1306; MainRule "str:\"" "msg:double quote" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1307; MainRule "str:(" "msg:parenthesis" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1308; MainRule "str:)" "msg:parenthesis" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1309; MainRule "str:[" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1310; MainRule "str:]" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1311; MainRule "str:~" "msg:html close comment tag" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$XSS:4" id:1312; MainRule "str:;" "msg:semi coma" "mz:ARGS|URL|BODY" "s:$XSS:8" id:1313; MainRule "str:`" "msg:grave accent !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1314; MainRule "rx:%[2|3]." "msg:double encoding !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie" "s:$XSS:8" id:1315; #################################### ## Evading tricks IDs: 1400-1500 ## #################################### 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 multipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402; ############################# ## File uploads: 1500-1600 ## ############################# MainRule "rx:.ph*|.asp*" "msg:asp/php file upload!" "mz:FILE_EXT" "s:$UPLOAD:8" id:1500; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf0000644000076500000240000000310100000000000031354 0ustar00bmwstaffuser www-data; worker_processes 4; pid /run/nginx.pid; events { worker_connections 768; # multi_accept on; } http { ## # Basic Settings ## sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # server_tokens off; # server_names_hash_bucket_size 64; # server_name_in_redirect off; include /etc/nginx/mime.types; default_type application/octet-stream; ## # Logging Settings ## access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; ## # Gzip Settings ## gzip on; gzip_disable "msie6"; # gzip_vary on; # gzip_proxied any; # gzip_comp_level 6; # gzip_buffers 16 8k; # gzip_http_version 1.1; # gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; ## # nginx-naxsi config ## # Uncomment it if you installed nginx-naxsi ## #include /etc/nginx/naxsi_core.rules; ## # nginx-passenger config ## # Uncomment it if you installed nginx-passenger ## #passenger_root /usr; #passenger_ruby /usr/bin/ruby; ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } #mail { # # See sample authentication script at: # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript # # # auth_http localhost/auth.php; # # pop3_capabilities "TOP" "USER"; # # imap_capabilities "IMAP4rev1" "UIDPLUS"; # # server { # listen localhost:110; # protocol pop3; # proxy on; # } # # server { # listen localhost:143; # protocol imap; # proxy on; # } #} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params0000644000076500000240000000026400000000000032040 0ustar00bmwstaffproxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params0000644000076500000240000000072100000000000031602 0ustar00bmwstaffscgi_param REQUEST_METHOD $request_method; scgi_param REQUEST_URI $request_uri; scgi_param QUERY_STRING $query_string; scgi_param CONTENT_TYPE $content_type; scgi_param DOCUMENT_URI $document_uri; scgi_param DOCUMENT_ROOT $document_root; scgi_param SCGI 1; scgi_param SERVER_PROTOCOL $server_protocol; scgi_param REMOTE_ADDR $remote_addr; scgi_param REMOTE_PORT $remote_port; scgi_param SERVER_PORT $server_port; scgi_param SERVER_NAME $server_name; ././@PaxHeader0000000000000000000000000000021300000000000010211 xustar00111 path=certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/ 28 mtime=1635888465.3839457 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available0000755000076500000240000000000000000000000032355 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000021400000000000010212 xustar00118 path=certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default 22 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available0000644000076500000240000000504100000000000032357 0ustar00bmwstaff# You may add here your # server { # ... # } # statements for each of your virtual hosts to this file ## # You should look at the following URL's in order to grasp a solid understanding # of Nginx configuration files in order to fully unleash the power of Nginx. # http://wiki.nginx.org/Pitfalls # http://wiki.nginx.org/QuickStart # http://wiki.nginx.org/Configuration # # Generally, you will want to move this file somewhere, and start with a clean # file but keep this around for reference. Or just disable in sites-enabled. # # Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. ## server { listen 80 default_server; listen [::]:80 default_server ipv6only=on; root /usr/share/nginx/html; index index.html index.htm; # Make site accessible from http://localhost/ server_name localhost; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; # Uncomment to enable naxsi on this location # include /etc/nginx/naxsi.rules } # Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests #location /RequestDenied { # proxy_pass http://127.0.0.1:8080; #} #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # #error_page 500 502 503 504 /50x.html; #location = /50x.html { # root /usr/share/nginx/html; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # fastcgi_split_path_info ^(.+\.php)(/.+)$; # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini # # # With php5-cgi alone: # fastcgi_pass 127.0.0.1:9000; # # With php5-fpm: # fastcgi_pass unix:/var/run/php5-fpm.sock; # fastcgi_index index.php; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # root html; # index index.html index.htm; # # location / { # try_files $uri $uri/ =404; # } #} # HTTPS server # #server { # listen 443; # server_name localhost; # # root html; # index index.html index.htm; # # ssl on; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # # ssl_session_timeout 5m; # # ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; # ssl_prefer_server_ciphers on; # # location / { # try_files $uri $uri/ =404; # } #} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888465.3840568 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/0000755000076500000240000000000000000000000032106 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000021200000000000010210 xustar00116 path=certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default 22 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/d0000644000076500000240000000504100000000000032254 0ustar00bmwstaff# You may add here your # server { # ... # } # statements for each of your virtual hosts to this file ## # You should look at the following URL's in order to grasp a solid understanding # of Nginx configuration files in order to fully unleash the power of Nginx. # http://wiki.nginx.org/Pitfalls # http://wiki.nginx.org/QuickStart # http://wiki.nginx.org/Configuration # # Generally, you will want to move this file somewhere, and start with a clean # file but keep this around for reference. Or just disable in sites-enabled. # # Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. ## server { listen 80 default_server; listen [::]:80 default_server ipv6only=on; root /usr/share/nginx/html; index index.html index.htm; # Make site accessible from http://localhost/ server_name localhost; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; # Uncomment to enable naxsi on this location # include /etc/nginx/naxsi.rules } # Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests #location /RequestDenied { # proxy_pass http://127.0.0.1:8080; #} #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # #error_page 500 502 503 504 /50x.html; #location = /50x.html { # root /usr/share/nginx/html; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # fastcgi_split_path_info ^(.+\.php)(/.+)$; # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini # # # With php5-cgi alone: # fastcgi_pass 127.0.0.1:9000; # # With php5-fpm: # fastcgi_pass unix:/var/run/php5-fpm.sock; # fastcgi_index index.php; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # root html; # index index.html index.htm; # # location / { # try_files $uri $uri/ =404; # } #} # HTTPS server # #server { # listen 443; # server_name localhost; # # root html; # index index.html index.htm; # # ssl on; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # # ssl_session_timeout 5m; # # ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; # ssl_prefer_server_ciphers on; # # location / { # try_files $uri $uri/ =404; # } #} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params0000644000076500000240000000102400000000000032010 0ustar00bmwstaffuwsgi_param QUERY_STRING $query_string; uwsgi_param REQUEST_METHOD $request_method; uwsgi_param CONTENT_TYPE $content_type; uwsgi_param CONTENT_LENGTH $content_length; uwsgi_param REQUEST_URI $request_uri; uwsgi_param PATH_INFO $document_uri; uwsgi_param DOCUMENT_ROOT $document_root; uwsgi_param SERVER_PROTOCOL $server_protocol; uwsgi_param UWSGI_SCHEME $scheme; uwsgi_param REMOTE_ADDR $remote_addr; uwsgi_param REMOTE_PORT $remote_port; uwsgi_param SERVER_PORT $server_port; uwsgi_param SERVER_NAME $server_name; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf0000644000076500000240000000600000000000000030677 0ustar00bmwstaff# This map is not a full windows-1251 <> utf8 map: it does not # contain Serbian and Macedonian letters. If you need a full map, # use contrib/unicode2nginx/win-utf map instead. charset_map windows-1251 utf-8 { 82 E2809A; # single low-9 quotation mark 84 E2809E; # double low-9 quotation mark 85 E280A6; # ellipsis 86 E280A0; # dagger 87 E280A1; # double dagger 88 E282AC; # euro 89 E280B0; # per mille 91 E28098; # left single quotation mark 92 E28099; # right single quotation mark 93 E2809C; # left double quotation mark 94 E2809D; # right double quotation mark 95 E280A2; # bullet 96 E28093; # en dash 97 E28094; # em dash 99 E284A2; # trade mark sign A0 C2A0; #   A1 D18E; # capital Byelorussian short U A2 D19E; # small Byelorussian short u A4 C2A4; # currency sign A5 D290; # capital Ukrainian soft G A6 C2A6; # borken bar A7 C2A7; # section sign A8 D081; # capital YO A9 C2A9; # (C) AA D084; # capital Ukrainian YE AB C2AB; # left-pointing double angle quotation mark AC C2AC; # not sign AD C2AD; # soft hyphen AE C2AE; # (R) AF D087; # capital Ukrainian YI B0 C2B0; # ° B1 C2B1; # plus-minus sign B2 D086; # capital Ukrainian I B3 D196; # small Ukrainian i B4 D291; # small Ukrainian soft g B5 C2B5; # micro sign B6 C2B6; # pilcrow sign B7 C2B7; # · B8 D191; # small yo B9 E28496; # numero sign BA D194; # small Ukrainian ye BB C2BB; # right-pointing double angle quotation mark BF D197; # small Ukrainian yi C0 D090; # capital A C1 D091; # capital B C2 D092; # capital V C3 D093; # capital G C4 D094; # capital D C5 D095; # capital YE C6 D096; # capital ZH C7 D097; # capital Z C8 D098; # capital I C9 D099; # capital J CA D09A; # capital K CB D09B; # capital L CC D09C; # capital M CD D09D; # capital N CE D09E; # capital O CF D09F; # capital P D0 D0A0; # capital R D1 D0A1; # capital S D2 D0A2; # capital T D3 D0A3; # capital U D4 D0A4; # capital F D5 D0A5; # capital KH D6 D0A6; # capital TS D7 D0A7; # capital CH D8 D0A8; # capital SH D9 D0A9; # capital SHCH DA D0AA; # capital hard sign DB D0AB; # capital Y DC D0AC; # capital soft sign DD D0AD; # capital E DE D0AE; # capital YU DF D0AF; # capital YA E0 D0B0; # small a E1 D0B1; # small b E2 D0B2; # small v E3 D0B3; # small g E4 D0B4; # small d E5 D0B5; # small ye E6 D0B6; # small zh E7 D0B7; # small z E8 D0B8; # small i E9 D0B9; # small j EA D0BA; # small k EB D0BB; # small l EC D0BC; # small m ED D0BD; # small n EE D0BE; # small o EF D0BF; # small p F0 D180; # small r F1 D181; # small s F2 D182; # small t F3 D183; # small u F4 D184; # small f F5 D185; # small kh F6 D186; # small ts F7 D187; # small ch F8 D188; # small sh F9 D189; # small shch FA D18A; # small hard sign FB D18B; # small y FC D18C; # small soft sign FD D18D; # small e FE D18E; # small yu FF D18F; # small ya } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 certbot-nginx-1.21.0/tests/testdata/etc_nginx/valid_unicode_comments.conf0000644000076500000240000000046400000000000025345 0ustar00bmwstaff# This configuration file is saved with valid UTF-8 encoding, # including some CJK alphabets. server { # 안녕하세요. 80번 포트에서 요청을 기다린다. # こんにちは。80番ポートからリクエストを待つ。 # 你好。等待端口80上的请求。 listen 80; }