From 0137055c240f4bd4fd6475d7df93eb2ac2288b04 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Sun, 5 Nov 2017 21:59:55 +0100 Subject: [PATCH 01/67] First shot at updates at documentation, plenty of questions left at issue #4736 --- certbot/main.py | 262 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 237 insertions(+), 25 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 9e2850891..77d474d5f 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -43,7 +43,14 @@ logger = logging.getLogger(__name__) def _suggest_donation_if_appropriate(config): - """Potentially suggest a donation to support Certbot.""" + """Potentially suggest a donation to support Certbot. + + :param interfaces.IConfig config: Configuration object + + :returns: `None` + :rtype: None + + """ assert config.verb != "renew" if config.staging: # --dry-run implies --staging @@ -55,6 +62,14 @@ def _suggest_donation_if_appropriate(config): reporter_util.add_message(msg, reporter_util.LOW_PRIORITY) def _report_successful_dry_run(config): + """Reports on successful dry run + + :param interfaces.IConfig config: Configuration object + + :returns: `None` + :rtype: None + + """ reporter_util = zope.component.getUtility(interfaces.IReporter) assert config.verb != "renew" reporter_util.add_message("The dry run was successful.", @@ -68,8 +83,16 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N then performs that action. Includes calls to hooks, various reports, checks, and requests for user input. + :param interfaces.IConfig config: Configuration object + :param list domains: domains to get a certificate. This argument is optional, if not supplied it will default to `None` + :param str certname: Name of new cert. This argument is optional, if not supplied it will default to `None` + :param storage.RenewableCert lineage: + :returns: the issued certificate or `None` if doing a dry run - :rtype: `storage.RenewableCert` or `None` + :rtype: storage.RenewableCert or None + + :raises errors.Error: if certificate could not be obtained + """ hooks.pre_hook(config) try: @@ -96,6 +119,8 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N def _handle_subset_cert_request(config, domains, cert): """Figure out what to do if a previous cert had a subset of the names now requested + :param interfaces.IConfig config: Configuration object + :param list domains: Domain names. :param storage.RenewableCert cert: :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname @@ -137,6 +162,7 @@ def _handle_subset_cert_request(config, domains, cert): def _handle_identical_cert_request(config, lineage): """Figure out what to do if a lineage has the same names as a previously obtained one + :param interfaces.IConfig config: Configuration object :param storage.RenewableCert lineage: :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname @@ -186,11 +212,14 @@ def _find_lineage_for_domains(config, domains): the client run if the user chooses to cancel the operation when prompted). + :param interfaces.IConfig config: Configuration object + :param list domains: Domain names. + :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either - a RenewableCert instance or None if renewal shouldn't occur. + a RenewableCert instance or `None` if renewal shouldn't occur. - :raises .Error: If the user would like to rerun the client again. + :raises errors.Error: If the user would like to rerun the client again. """ # Considering the possibility that the requested certificate is @@ -214,6 +243,10 @@ def _find_lineage_for_domains(config, domains): def _find_cert(config, domains, certname): """Finds an existing certificate object given domains and/or a certificate name. + :param interfaces.IConfig config: Configuration object + :param list domains: Domain names. + :param str certname: Name of cert + :returns: Two-element tuple of a boolean that indicates if this function should be followed by a call to fetch a certificate from the server, and either a RenewableCert instance or None. @@ -226,11 +259,15 @@ def _find_cert(config, domains, certname): def _find_lineage_for_domains_and_certname(config, domains, certname): """Find appropriate lineage based on given domains and/or certname. + :param interfaces.IConfig config: Configuration object + :param list domains: Domain names. + :param str certname: Name of cert + :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either - a RenewableCert instance or None if renewal shouldn't occur. + a RenewableCert instance or None if renewal should not occur. - :raises .Error: If the user would like to rerun the client again. + :raises errors.Error: If the user would like to rerun the client again. """ if not certname: @@ -255,6 +292,17 @@ def _find_lineage_for_domains_and_certname(config, domains, certname): def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): """Ask user to confirm update cert certname to contain new_domains. + + :param interfaces.IConfig config: Configuration object + :param list new_domains: Domain names. + :param str certname: Name of cert + :param list old_domains: Domain names. + + :returns: None + :rtype: None + + :raises errors.ConfigurationError: if cert name and domains mismatch + """ if config.renew_with_new_domains: return @@ -272,6 +320,15 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): def _find_domains_or_certname(config, installer): """Retrieve domains and certname from config or user input. + + :param interfaces.IConfig config: Configuration object + :param: TODO installer? + + :returns: Two-part tuple of domains and certname + :rtype: tuple + + :raises errors.Error: Usage message, if parameters are not used correctly + """ domains = None certname = config.certname @@ -303,6 +360,9 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None): :param str fullchain_path: path to full chain :param str key_path: path to private key, if available + :returns: 'None' + :rtype: None + """ if config.dry_run: _report_successful_dry_run(config) @@ -337,14 +397,13 @@ def _determine_account(config): if ``config.account`` is ``None``, it will be updated based on the user input. Same for ``config.email``. - :param argparse.Namespace config: CLI arguments - :param certbot.interface.IConfig config: Configuration object - :param .AccountStorage account_storage: Account storage. + :param interfaces.IConfig config: Configuration object :returns: Account and optionally ACME client API (biproduct of new registration). - :rtype: `tuple` of `certbot.account.Account` and - `acme.client.Client` + :rtype: tuple of certbot.account.Account and acme.client.Client + + :raises errors.Error: If unable to register an account with ACME server """ account_storage = account.AccountFileStorage(config) @@ -394,7 +453,7 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b :param `configuration.NamespaceConfig` config: parsed command line arguments - :raises `error.Errors`: If anything goes wrong, including bad user input, if an overlapping + :raises errors.Error: If anything goes wrong, including bad user input, if an overlapping archive dir is found for the specified lineage, etc ... """ display = zope.component.getUtility(interfaces.IDisplay) @@ -474,6 +533,15 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b def _init_le_client(config, authenticator, installer): + """Initialize Let's Encrypt Client + + :param interfaces.IConfig config: Configuration object + :param: TODO authenticator + :param: TODO installer + + :returns: client: Client object + + """ if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(config) @@ -487,7 +555,15 @@ def _init_le_client(config, authenticator, installer): def unregister(config, unused_plugins): - """Deactivate account on server""" + """Deactivate account on server + + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` + :rtype: None + + """ account_storage = account.AccountFileStorage(config) accounts = account_storage.find_all() reporter_util = zope.component.getUtility(interfaces.IReporter) @@ -516,8 +592,15 @@ def unregister(config, unused_plugins): def register(config, unused_plugins): - """Create or modify accounts on the server.""" + """Create or modify accounts on the server. + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` or a string indicating and error + :rtype: None or str + + """ # Portion of _determine_account logic to see whether accounts already # exist or not. account_storage = account.AccountFileStorage(config) @@ -566,7 +649,15 @@ def _install_cert(config, le_client, domains, lineage=None): le_client.enhance_config(domains, path_provider.chain_path) def install(config, plugins): - """Install a previously obtained cert in a server.""" + """Install a previously obtained cert in a server. + + :param interfaces.IConfig config: Configuration object + :param plugins: list of plugins + + :returns: `None` + :rtype: None + + """ # XXX: Update for renewer/RenewableCert # FIXME: be consistent about whether errors are raised or returned from # this function ... @@ -582,7 +673,15 @@ def install(config, plugins): def plugins_cmd(config, plugins): - """List server software plugins.""" + """List server software plugins. + + :param interfaces.IConfig config: Configuration object + :param plugins: list of plugins + + :returns: `None` + :rtype: None + + """ logger.debug("Expected interfaces: %s", config.ifaces) ifaces = [] if config.ifaces is None else config.ifaces @@ -610,7 +709,15 @@ def plugins_cmd(config, plugins): def rollback(config, plugins): - """Rollback server configuration changes made during install.""" + """Rollback server configuration changes made during install. + + :param interfaces.IConfig config: Configuration object + :param plugins: list of plugins + + :returns: `None` + :rtype: None + + """ client.rollback(config.installer, config.checkpoints, config, plugins) @@ -619,6 +726,12 @@ def config_changes(config, unused_plugins): View checkpoints and associated configuration changes. + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` + :rtype: None + """ client.view_config_changes(config, num=config.num) @@ -627,6 +740,13 @@ def update_symlinks(config, unused_plugins): Use the information in the config file to make symlinks point to the correct archive directory. + + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` + :rtype: None + """ cert_manager.update_live_symlinks(config) @@ -635,6 +755,13 @@ def rename(config, unused_plugins): Use the information in the config file to rename an existing lineage. + + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` + :rtype: None + """ cert_manager.rename_lineage(config) @@ -643,16 +770,37 @@ def delete(config, unused_plugins): Use the information in the config file to delete an existing lineage. + + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` + :rtype: None + """ cert_manager.delete(config) def certificates(config, unused_plugins): """Display information about certs configured with Certbot + + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` + :rtype: None """ cert_manager.certificates(config) def revoke(config, unused_plugins): # TODO: coop with renewal config - """Revoke a previously obtained certificate.""" + """Revoke a previously obtained certificate. + + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` returns string indicating error in case of error + :rtype: None or str + + """ # For user-agent construction config.installer = config.authenticator = "None" if config.key_path is not None: # revocation by cert key @@ -678,7 +826,15 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals - """Obtain a certificate and install.""" + """Obtain a certificate and install. + + :param interfaces.IConfig config: Configuration object + :param plugins: list of plugins + + :returns: `None` + :rtype: None + + """ # TODO: Make run as close to auth + install as possible # Possible difficulties: config.csr was hacked into auth try: @@ -718,6 +874,13 @@ def _csr_get_and_save_cert(config, le_client): This works differently in the CSR case (for now) because we don't have the privkey, and therefore can't construct the files for a lineage. So we just save the cert & chain to disk :/ + + :param interfaces.IConfig config: Configuration object + :param client.Client client: Client object + + :returns: `cert_path` and `fullchain_path` as absolute paths to the actual files + :rtype: tuple of str + """ csr, _ = config.actual_csr certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr) @@ -730,7 +893,19 @@ def _csr_get_and_save_cert(config, le_client): return cert_path, fullchain_path def renew_cert(config, plugins, lineage): - """Renew & save an existing cert. Do not install it.""" + """Renew & save an existing cert. Do not install it. + + :param interfaces.IConfig config: Configuration object + :param plugins: TODO + :param lineage: TODO + + :returns: `None` + :rtype: None + + :raises errors.PluginSelectionError: MissingCommandlineFlag in case supplied parameters do not pass + + + """ try: # installers are used in auth mode to determine domain names installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly") @@ -757,8 +932,17 @@ def renew_cert(config, plugins, lineage): def certonly(config, plugins): """Authenticate & obtain cert, but do not install it. - This implements the 'certonly' subcommand.""" + This implements the 'certonly' subcommand. + :param interfaces.IConfig config: Configuration object + :param: TODO plugins + + :returns: `None` + :rtype: None + + :raises errors.Error: If specified plugin could not be used + + """ # SETUP: Select plugins and construct a client instance try: # installers are used in auth mode to determine domain names @@ -792,7 +976,15 @@ def certonly(config, plugins): _suggest_donation_if_appropriate(config) def renew(config, unused_plugins): - """Renew previously-obtained certificates.""" + """Renew previously-obtained certificates. + + :param interfaces.IConfig config: Configuration object + :param unused_plugins: list of plugins (deprecated) + + :returns: `None` + :rtype: None + + """ try: renewal.handle_renewal_request(config) finally: @@ -800,7 +992,14 @@ def renew(config, unused_plugins): def make_or_verify_needed_dirs(config): - """Create or verify existence of config, work, and hook directories.""" + """Create or verify existence of config, work, and hook directories. + + :param interfaces.IConfig config: Configuration object + + :returns: `None` + :rtype: None + + """ util.set_up_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), config.strict_permissions) util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, @@ -816,7 +1015,14 @@ def make_or_verify_needed_dirs(config): def set_displayer(config): - """Set the displayer""" + """Set the displayer + + :param interfaces.IConfig config: Configuration object + + :returns: `None` + :rtype: None + + """ if config.quiet: config.noninteractive_mode = True displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w")) @@ -829,7 +1035,13 @@ def set_displayer(config): def main(cli_args=sys.argv[1:]): - """Command line argument parsing and main script execution.""" + """Command line argument parsing and main script execution. + + :returns: TODO + + :raises errors.Error: General operating system errors triggered by issues related to wrong permissions + + """ log.pre_arg_parse_setup() plugins = plugins_disco.PluginsRegistry.find_all() From 4e73d7ce00ce76fa0ef5b5c653f7943e436b6a7a Mon Sep 17 00:00:00 2001 From: jonasbn Date: Tue, 7 Nov 2017 21:24:30 +0100 Subject: [PATCH 02/67] Specified the list parameters after reading up on lists as parameters Ref: https://stackoverflow.com/questions/3961007/passing-an-array-list-into-python --- certbot/main.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 77d474d5f..273cc0263 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -558,7 +558,7 @@ def unregister(config, unused_plugins): """Deactivate account on server :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` :rtype: None @@ -595,7 +595,7 @@ def register(config, unused_plugins): """Create or modify accounts on the server. :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` or a string indicating and error :rtype: None or str @@ -652,7 +652,7 @@ def install(config, plugins): """Install a previously obtained cert in a server. :param interfaces.IConfig config: Configuration object - :param plugins: list of plugins + :param list plugins: list of plugins :returns: `None` :rtype: None @@ -676,7 +676,7 @@ def plugins_cmd(config, plugins): """List server software plugins. :param interfaces.IConfig config: Configuration object - :param plugins: list of plugins + :param list plugins: list of plugins :returns: `None` :rtype: None @@ -712,7 +712,7 @@ def rollback(config, plugins): """Rollback server configuration changes made during install. :param interfaces.IConfig config: Configuration object - :param plugins: list of plugins + :param list plugins: list of plugins :returns: `None` :rtype: None @@ -727,7 +727,7 @@ def config_changes(config, unused_plugins): View checkpoints and associated configuration changes. :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` :rtype: None @@ -742,7 +742,7 @@ def update_symlinks(config, unused_plugins): the correct archive directory. :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` :rtype: None @@ -757,7 +757,7 @@ def rename(config, unused_plugins): lineage. :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` :rtype: None @@ -772,7 +772,7 @@ def delete(config, unused_plugins): lineage. :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` :rtype: None @@ -784,7 +784,7 @@ def certificates(config, unused_plugins): """Display information about certs configured with Certbot :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` :rtype: None @@ -795,7 +795,7 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config """Revoke a previously obtained certificate. :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` returns string indicating error in case of error :rtype: None or str @@ -829,7 +829,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals """Obtain a certificate and install. :param interfaces.IConfig config: Configuration object - :param plugins: list of plugins + :param list plugins: list of plugins :returns: `None` :rtype: None @@ -896,7 +896,7 @@ def renew_cert(config, plugins, lineage): """Renew & save an existing cert. Do not install it. :param interfaces.IConfig config: Configuration object - :param plugins: TODO + :param list plugins: TODO :param lineage: TODO :returns: `None` @@ -935,7 +935,7 @@ def certonly(config, plugins): This implements the 'certonly' subcommand. :param interfaces.IConfig config: Configuration object - :param: TODO plugins + :param list plugins: List of plugins :returns: `None` :rtype: None @@ -979,7 +979,7 @@ def renew(config, unused_plugins): """Renew previously-obtained certificates. :param interfaces.IConfig config: Configuration object - :param unused_plugins: list of plugins (deprecated) + :param list unused_plugins: list of plugins (deprecated) :returns: `None` :rtype: None From 89485f7463123f2a687876e5950a6f729c66e37f Mon Sep 17 00:00:00 2001 From: jonasbn Date: Tue, 7 Nov 2017 21:40:35 +0100 Subject: [PATCH 03/67] I think I figured out the authentication handler object --- certbot/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 273cc0263..6c9e377ef 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -322,7 +322,7 @@ def _find_domains_or_certname(config, installer): """Retrieve domains and certname from config or user input. :param interfaces.IConfig config: Configuration object - :param: TODO installer? + :param installer: Installer object :returns: Two-part tuple of domains and certname :rtype: tuple @@ -536,8 +536,8 @@ def _init_le_client(config, authenticator, installer): """Initialize Let's Encrypt Client :param interfaces.IConfig config: Configuration object - :param: TODO authenticator - :param: TODO installer + :param AuthHandler authenticator: Acme authentication handler + :param installer: Installer object :returns: client: Client object @@ -896,7 +896,7 @@ def renew_cert(config, plugins, lineage): """Renew & save an existing cert. Do not install it. :param interfaces.IConfig config: Configuration object - :param list plugins: TODO + :param list plugins: List of plugins :param lineage: TODO :returns: `None` From 0aa9322280f9ad7a780c470607c4da1c1ca1bb1d Mon Sep 17 00:00:00 2001 From: jonasbn Date: Tue, 7 Nov 2017 21:47:59 +0100 Subject: [PATCH 04/67] Added a shot at what might be the proper type, I need to get a better understanding of certbot's datatypes --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 6c9e377ef..f3096da6c 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -897,7 +897,7 @@ def renew_cert(config, plugins, lineage): :param interfaces.IConfig config: Configuration object :param list plugins: List of plugins - :param lineage: TODO + :param RenewableCert lineage: a certificate lineage object :returns: `None` :rtype: None From 1173acfaf0a377917442d7becd16dd863a1f27aa Mon Sep 17 00:00:00 2001 From: jonasbn Date: Tue, 7 Nov 2017 22:18:11 +0100 Subject: [PATCH 05/67] Making friends with the linter lint: commands succeeded congratulations :) --- certbot/main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index f3096da6c..5f2803da7 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -84,8 +84,11 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N checks, and requests for user input. :param interfaces.IConfig config: Configuration object - :param list domains: domains to get a certificate. This argument is optional, if not supplied it will default to `None` - :param str certname: Name of new cert. This argument is optional, if not supplied it will default to `None` + + :param list domains: domains to get a certificate. Defaults to `None` + + :param str certname: Name of new cert. Defaults to `None` + :param storage.RenewableCert lineage: :returns: the issued certificate or `None` if doing a dry run @@ -902,8 +905,7 @@ def renew_cert(config, plugins, lineage): :returns: `None` :rtype: None - :raises errors.PluginSelectionError: MissingCommandlineFlag in case supplied parameters do not pass - + :raises errors.PluginSelectionError: MissingCommandlineFlag if supplied parameters do not pass """ try: @@ -1039,7 +1041,7 @@ def main(cli_args=sys.argv[1:]): :returns: TODO - :raises errors.Error: General operating system errors triggered by issues related to wrong permissions + :raises errors.Error: OS errors triggered by wrong permissions """ log.pre_arg_parse_setup() From eb26e0aacf4ac2454fe8eb5dcb8a529b466737e1 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Sun, 12 Nov 2017 00:32:24 +0100 Subject: [PATCH 06/67] Updated parameter types for a lot of parametersm some aspects are still a bug unclear, hopefully a review can shed some light on this details --- certbot/main.py | 235 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 167 insertions(+), 68 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 5f2803da7..089149858 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -45,7 +45,8 @@ logger = logging.getLogger(__name__) def _suggest_donation_if_appropriate(config): """Potentially suggest a donation to support Certbot. - :param interfaces.IConfig config: Configuration object + :param config: Configuration object + :type config: interfaces.IConfig :returns: `None` :rtype: None @@ -64,7 +65,8 @@ def _suggest_donation_if_appropriate(config): def _report_successful_dry_run(config): """Reports on successful dry run - :param interfaces.IConfig config: Configuration object + :param config: Configuration object + :type config: interfaces.IConfig :returns: `None` :rtype: None @@ -83,13 +85,17 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N then performs that action. Includes calls to hooks, various reports, checks, and requests for user input. - :param interfaces.IConfig config: Configuration object + :param config: Configuration object + :type config: interfaces.IConfig - :param list domains: domains to get a certificate. Defaults to `None` + :param domains: domains to get a certificate. Defaults to `None` + :type domains: `list` of `str` - :param str certname: Name of new cert. Defaults to `None` + :param certname: Name of new cert. Defaults to `None` + :type certname: str - :param storage.RenewableCert lineage: + :param lineage: + :type lineage: storage.RenewableCert :returns: the issued certificate or `None` if doing a dry run :rtype: storage.RenewableCert or None @@ -122,9 +128,14 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N def _handle_subset_cert_request(config, domains, cert): """Figure out what to do if a previous cert had a subset of the names now requested - :param interfaces.IConfig config: Configuration object - :param list domains: Domain names. - :param storage.RenewableCert cert: + :param config: Configuration object + :type config: interfaces.IConfig + + :param domains: domains + :type domains: `list` of `str` + + :param cert: + :type cert: storage.RenewableCert :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" @@ -165,8 +176,11 @@ def _handle_subset_cert_request(config, domains, cert): def _handle_identical_cert_request(config, lineage): """Figure out what to do if a lineage has the same names as a previously obtained one - :param interfaces.IConfig config: Configuration object - :param storage.RenewableCert lineage: + :param config: Configuration object + :type config: interfaces.IConfig + + :param lineage: + :type lineage: storage.RenewableCert :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" @@ -215,8 +229,11 @@ def _find_lineage_for_domains(config, domains): the client run if the user chooses to cancel the operation when prompted). - :param interfaces.IConfig config: Configuration object - :param list domains: Domain names. + :param config: Configuration object + :type config: interfaces.IConfig + + :param domains: domains + :type domains: `list` of `str` :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either @@ -246,9 +263,14 @@ def _find_lineage_for_domains(config, domains): def _find_cert(config, domains, certname): """Finds an existing certificate object given domains and/or a certificate name. - :param interfaces.IConfig config: Configuration object - :param list domains: Domain names. - :param str certname: Name of cert + :param config: Configuration object + :type config: interfaces.IConfig + + :param domains: domains + :type domains: `list` of `str` + + :param certname: Name of cert + :type certname: str :returns: Two-element tuple of a boolean that indicates if this function should be followed by a call to fetch a certificate from the server, and either a @@ -262,9 +284,14 @@ def _find_cert(config, domains, certname): def _find_lineage_for_domains_and_certname(config, domains, certname): """Find appropriate lineage based on given domains and/or certname. - :param interfaces.IConfig config: Configuration object - :param list domains: Domain names. - :param str certname: Name of cert + :param config: Configuration object + :type config: interfaces.IConfig + + :param domains: domains + :type domains: `list` of `str` + + :param certname: Name of cert + :type certname: str :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either @@ -296,10 +323,17 @@ def _find_lineage_for_domains_and_certname(config, domains, certname): def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): """Ask user to confirm update cert certname to contain new_domains. - :param interfaces.IConfig config: Configuration object - :param list new_domains: Domain names. - :param str certname: Name of cert - :param list old_domains: Domain names. + :param config: Configuration object + :type config: interfaces.IConfig + + :param new_domains: domains + :type new_domains: `list` of `str` + + :param certname: Name of cert + :type certname: str + + :param old_domains: domains + :type old_domains: `list` of `str` :returns: None :rtype: None @@ -324,8 +358,12 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): def _find_domains_or_certname(config, installer): """Retrieve domains and certname from config or user input. - :param interfaces.IConfig config: Configuration object + :param config: Configuration object + :type config: interfaces.IConfig + :param installer: Installer object + :type installer: interfaces.IInstaller + :returns: Two-part tuple of domains and certname :rtype: tuple @@ -359,11 +397,14 @@ def _find_domains_or_certname(config, installer): def _report_new_cert(config, cert_path, fullchain_path, key_path=None): """Reports the creation of a new certificate to the user. - :param str cert_path: path to cert - :param str fullchain_path: path to full chain - :param str key_path: path to private key, if available + :param cert_path: path to cert + :type cert_path: str + :param fullchain_path: path to full chain + :type fullchain_path: str + :param key_path: path to private key, if available + :type key_path: str - :returns: 'None' + :returns: `None` :rtype: None """ @@ -400,7 +441,8 @@ def _determine_account(config): if ``config.account`` is ``None``, it will be updated based on the user input. Same for ``config.email``. - :param interfaces.IConfig config: Configuration object + :param config: Configuration object + :type config: interfaces.IConfig :returns: Account and optionally ACME client API (biproduct of new registration). @@ -538,9 +580,13 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b def _init_le_client(config, authenticator, installer): """Initialize Let's Encrypt Client - :param interfaces.IConfig config: Configuration object - :param AuthHandler authenticator: Acme authentication handler + :param config: Configuration object + :type config: interfaces.IConfig + + :param authenticator: Acme authentication handler + :type authenticator: interfaces.IAuthenticator :param installer: Installer object + :type installer: interfaces.IInstaller :returns: client: Client object @@ -560,8 +606,11 @@ def _init_le_client(config, authenticator, installer): def unregister(config, unused_plugins): """Deactivate account on server - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` :rtype: None @@ -597,8 +646,11 @@ def unregister(config, unused_plugins): def register(config, unused_plugins): """Create or modify accounts on the server. - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` or a string indicating and error :rtype: None or str @@ -654,8 +706,11 @@ def _install_cert(config, le_client, domains, lineage=None): def install(config, plugins): """Install a previously obtained cert in a server. - :param interfaces.IConfig config: Configuration object - :param list plugins: list of plugins + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: list of plugins + :type plugins: `list` of `str` :returns: `None` :rtype: None @@ -678,8 +733,11 @@ def install(config, plugins): def plugins_cmd(config, plugins): """List server software plugins. - :param interfaces.IConfig config: Configuration object - :param list plugins: list of plugins + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: list of plugins + :type plugins: `list` of `str` :returns: `None` :rtype: None @@ -714,8 +772,11 @@ def plugins_cmd(config, plugins): def rollback(config, plugins): """Rollback server configuration changes made during install. - :param interfaces.IConfig config: Configuration object - :param list plugins: list of plugins + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: list of plugins + :type plugins: `list` of `str` :returns: `None` :rtype: None @@ -729,8 +790,11 @@ def config_changes(config, unused_plugins): View checkpoints and associated configuration changes. - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` :rtype: None @@ -744,8 +808,11 @@ def update_symlinks(config, unused_plugins): Use the information in the config file to make symlinks point to the correct archive directory. - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` :rtype: None @@ -759,8 +826,11 @@ def rename(config, unused_plugins): Use the information in the config file to rename an existing lineage. - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` :rtype: None @@ -774,8 +844,11 @@ def delete(config, unused_plugins): Use the information in the config file to delete an existing lineage. - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` :rtype: None @@ -786,8 +859,11 @@ def delete(config, unused_plugins): def certificates(config, unused_plugins): """Display information about certs configured with Certbot - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` :rtype: None @@ -797,10 +873,13 @@ def certificates(config, unused_plugins): def revoke(config, unused_plugins): # TODO: coop with renewal config """Revoke a previously obtained certificate. - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig - :returns: `None` returns string indicating error in case of error + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` or string indicating error in case of error :rtype: None or str """ @@ -831,8 +910,11 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals """Obtain a certificate and install. - :param interfaces.IConfig config: Configuration object - :param list plugins: list of plugins + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: list of plugins + :type plugins: `list` of `str` :returns: `None` :rtype: None @@ -878,8 +960,11 @@ def _csr_get_and_save_cert(config, le_client): have the privkey, and therefore can't construct the files for a lineage. So we just save the cert & chain to disk :/ - :param interfaces.IConfig config: Configuration object - :param client.Client client: Client object + :param config: Configuration object + :type config: interfaces.IConfig + + :param client: Client object + :type client: client.Client :returns: `cert_path` and `fullchain_path` as absolute paths to the actual files :rtype: tuple of str @@ -898,9 +983,14 @@ def _csr_get_and_save_cert(config, le_client): def renew_cert(config, plugins, lineage): """Renew & save an existing cert. Do not install it. - :param interfaces.IConfig config: Configuration object - :param list plugins: List of plugins - :param RenewableCert lineage: a certificate lineage object + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: list of plugins + :type plugins: `list` of `str` + + :param lineage: certificate lineage object + :type lineage: storage.RenewableCert :returns: `None` :rtype: None @@ -936,8 +1026,11 @@ def certonly(config, plugins): This implements the 'certonly' subcommand. - :param interfaces.IConfig config: Configuration object - :param list plugins: List of plugins + :param config: Configuration object + :type config: interfaces.IConfig + + :param plugins: list of plugins + :type plugins: `list` of `str` :returns: `None` :rtype: None @@ -980,8 +1073,11 @@ def certonly(config, plugins): def renew(config, unused_plugins): """Renew previously-obtained certificates. - :param interfaces.IConfig config: Configuration object - :param list unused_plugins: list of plugins (deprecated) + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: list of plugins (deprecated) + :type unused_plugins: `list` of `str` :returns: `None` :rtype: None @@ -996,7 +1092,8 @@ def renew(config, unused_plugins): def make_or_verify_needed_dirs(config): """Create or verify existence of config, work, and hook directories. - :param interfaces.IConfig config: Configuration object + :param config: Configuration object + :type config: interfaces.IConfig :returns: `None` :rtype: None @@ -1019,7 +1116,8 @@ def make_or_verify_needed_dirs(config): def set_displayer(config): """Set the displayer - :param interfaces.IConfig config: Configuration object + :param config: Configuration object + :type config: interfaces.IConfig :returns: `None` :rtype: None @@ -1039,9 +1137,10 @@ def set_displayer(config): def main(cli_args=sys.argv[1:]): """Command line argument parsing and main script execution. - :returns: TODO + :returns: result of requested command :raises errors.Error: OS errors triggered by wrong permissions + :raises errors.Error: error if plugin command is not supported """ log.pre_arg_parse_setup() From 4d60f32865ceedc7a39f0f74cfe53445f7610fb4 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Sun, 12 Nov 2017 13:03:09 +0100 Subject: [PATCH 07/67] Minor corrections to return types for improved formatting --- certbot/main.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 089149858..d30f434c0 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -139,7 +139,7 @@ def _handle_subset_cert_request(config, domains, cert): :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" - :rtype: tuple + :rtype: `tuple` of `str` """ existing = ", ".join(cert.names()) @@ -184,7 +184,7 @@ def _handle_identical_cert_request(config, lineage): :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" - :rtype: tuple + :rtype: `tuple` of `str` """ if not lineage.ensure_deployed(): @@ -238,6 +238,7 @@ def _find_lineage_for_domains(config, domains): :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either a RenewableCert instance or `None` if renewal shouldn't occur. + :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None` :raises errors.Error: If the user would like to rerun the client again. @@ -275,6 +276,8 @@ def _find_cert(config, domains, certname): :returns: Two-element tuple of a boolean that indicates if this function should be followed by a call to fetch a certificate from the server, and either a RenewableCert instance or None. + :rtype: `tuple` of `bool` and :class:`storage.RenewableCert` or `None` + """ action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname) if action == "reinstall": @@ -297,6 +300,8 @@ def _find_lineage_for_domains_and_certname(config, domains, certname): a string token ("reinstall", "renew", or "newcert"), plus either a RenewableCert instance or None if renewal should not occur. + :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None` + :raises errors.Error: If the user would like to rerun the client again. """ @@ -366,7 +371,7 @@ def _find_domains_or_certname(config, installer): :returns: Two-part tuple of domains and certname - :rtype: tuple + :rtype: `tuple` of list of `str` and `str` :raises errors.Error: Usage message, if parameters are not used correctly @@ -446,7 +451,7 @@ def _determine_account(config): :returns: Account and optionally ACME client API (biproduct of new registration). - :rtype: tuple of certbot.account.Account and acme.client.Client + :rtype: tuple of :class:`certbot.account.Account` and :class:`acme.client.Client` :raises errors.Error: If unable to register an account with ACME server @@ -498,6 +503,9 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b :param `configuration.NamespaceConfig` config: parsed command line arguments + :returns: `None` + :rtype: None + :raises errors.Error: If anything goes wrong, including bad user input, if an overlapping archive dir is found for the specified lineage, etc ... """ @@ -589,6 +597,7 @@ def _init_le_client(config, authenticator, installer): :type installer: interfaces.IInstaller :returns: client: Client object + :rtype: client.Client """ if authenticator is not None: @@ -867,6 +876,7 @@ def certificates(config, unused_plugins): :returns: `None` :rtype: None + """ cert_manager.certificates(config) @@ -967,7 +977,7 @@ def _csr_get_and_save_cert(config, le_client): :type client: client.Client :returns: `cert_path` and `fullchain_path` as absolute paths to the actual files - :rtype: tuple of str + :rtype: `tuple` of `str` """ csr, _ = config.actual_csr From 0b843bb8515822bd7f6564180630699d01d3abdb Mon Sep 17 00:00:00 2001 From: jonasbn Date: Wed, 15 Nov 2017 07:23:34 +0100 Subject: [PATCH 08/67] Added some missing documentation --- certbot/main.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/certbot/main.py b/certbot/main.py index d30f434c0..aeb147e86 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -705,6 +705,24 @@ def register(config, unused_plugins): add_msg("Your e-mail address was updated to {0}.".format(config.email)) def _install_cert(config, le_client, domains, lineage=None): + """Install a cert + + :param config: Configuration object + :type config: interfaces.IConfig + + :param le_client: Client object + :type le_client: client.Client + + :param plugins: list of domains + :type plugins: `list` of `str` + + :param lineage: certificate lineage object + :type lineage: storage.RenewableCert + + :returns: `None` + :rtype: None + + """ path_provider = lineage if lineage else config assert path_provider.cert_path is not None From 02126c0961817ba9b87b0864a75c92800fff8e08 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Wed, 15 Nov 2017 07:24:54 +0100 Subject: [PATCH 09/67] Minor improvement to newly added documentation section --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index aeb147e86..7359b88d5 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -716,7 +716,7 @@ def _install_cert(config, le_client, domains, lineage=None): :param plugins: list of domains :type plugins: `list` of `str` - :param lineage: certificate lineage object + :param lineage: certificate lineage object. Defaults to `None` :type lineage: storage.RenewableCert :returns: `None` From e795a79547d65f0965aa4091123214800d40949d Mon Sep 17 00:00:00 2001 From: jonasbn Date: Wed, 15 Nov 2017 07:38:09 +0100 Subject: [PATCH 10/67] Lots of minor small cosmetic changes and addressing the feedback on uniformity (in the file) from @SwartzCr --- certbot/main.py | 71 ++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 7359b88d5..82063d0db 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -88,13 +88,13 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N :param config: Configuration object :type config: interfaces.IConfig - :param domains: domains to get a certificate. Defaults to `None` + :param domains: List of domain names to get a certificate. Defaults to `None` :type domains: `list` of `str` - :param certname: Name of new cert. Defaults to `None` + :param certname: Name of new certificate. Defaults to `None` :type certname: str - :param lineage: + :param lineage: Certificate lineage object. Defaults to `None` :type lineage: storage.RenewableCert :returns: the issued certificate or `None` if doing a dry run @@ -131,10 +131,10 @@ def _handle_subset_cert_request(config, domains, cert): :param config: Configuration object :type config: interfaces.IConfig - :param domains: domains + :param domains: List of domain names :type domains: `list` of `str` - :param cert: + :param cert: Certificate object :type cert: storage.RenewableCert :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname @@ -179,7 +179,7 @@ def _handle_identical_cert_request(config, lineage): :param config: Configuration object :type config: interfaces.IConfig - :param lineage: + :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname @@ -232,7 +232,7 @@ def _find_lineage_for_domains(config, domains): :param config: Configuration object :type config: interfaces.IConfig - :param domains: domains + :param domains: List of domain names :type domains: `list` of `str` :returns: Two-element tuple containing desired new-certificate behavior as @@ -267,10 +267,10 @@ def _find_cert(config, domains, certname): :param config: Configuration object :type config: interfaces.IConfig - :param domains: domains + :param domains: List of domain names :type domains: `list` of `str` - :param certname: Name of cert + :param certname: Name of certificate :type certname: str :returns: Two-element tuple of a boolean that indicates if this function should be @@ -290,10 +290,10 @@ def _find_lineage_for_domains_and_certname(config, domains, certname): :param config: Configuration object :type config: interfaces.IConfig - :param domains: domains + :param domains: List of domain names :type domains: `list` of `str` - :param certname: Name of cert + :param certname: Name of certificate :type certname: str :returns: Two-element tuple containing desired new-certificate behavior as @@ -331,13 +331,13 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): :param config: Configuration object :type config: interfaces.IConfig - :param new_domains: domains + :param new_domains: List of new domain names :type new_domains: `list` of `str` - :param certname: Name of cert + :param certname: Name of certificate :type certname: str - :param old_domains: domains + :param old_domains: List of old domain names :type old_domains: `list` of `str` :returns: None @@ -402,10 +402,12 @@ def _find_domains_or_certname(config, installer): def _report_new_cert(config, cert_path, fullchain_path, key_path=None): """Reports the creation of a new certificate to the user. - :param cert_path: path to cert + :param cert_path: path to certificate :type cert_path: str + :param fullchain_path: path to full chain :type fullchain_path: str + :param key_path: path to private key, if available :type key_path: str @@ -501,7 +503,8 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b deleting happens automatically, unless if both `--cert-name` and `--cert-path` were specified with conflicting values. - :param `configuration.NamespaceConfig` config: parsed command line arguments + :param config: parsed command line arguments + :type config: interfaces.IConfig :returns: `None` :rtype: None @@ -618,7 +621,7 @@ def unregister(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` @@ -658,7 +661,7 @@ def register(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` or a string indicating and error @@ -713,10 +716,10 @@ def _install_cert(config, le_client, domains, lineage=None): :param le_client: Client object :type le_client: client.Client - :param plugins: list of domains + :param plugins: List of domains :type plugins: `list` of `str` - :param lineage: certificate lineage object. Defaults to `None` + :param lineage: Certificate lineage object. Defaults to `None` :type lineage: storage.RenewableCert :returns: `None` @@ -736,7 +739,7 @@ def install(config, plugins): :param config: Configuration object :type config: interfaces.IConfig - :param plugins: list of plugins + :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` @@ -763,7 +766,7 @@ def plugins_cmd(config, plugins): :param config: Configuration object :type config: interfaces.IConfig - :param plugins: list of plugins + :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` @@ -802,7 +805,7 @@ def rollback(config, plugins): :param config: Configuration object :type config: interfaces.IConfig - :param plugins: list of plugins + :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` @@ -820,7 +823,7 @@ def config_changes(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` @@ -838,7 +841,7 @@ def update_symlinks(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` @@ -856,7 +859,7 @@ def rename(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` @@ -874,7 +877,7 @@ def delete(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` @@ -889,7 +892,7 @@ def certificates(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` @@ -904,7 +907,7 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` or string indicating error in case of error @@ -941,7 +944,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals :param config: Configuration object :type config: interfaces.IConfig - :param plugins: list of plugins + :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` @@ -1014,10 +1017,10 @@ def renew_cert(config, plugins, lineage): :param config: Configuration object :type config: interfaces.IConfig - :param plugins: list of plugins + :param plugins: List of plugins :type plugins: `list` of `str` - :param lineage: certificate lineage object + :param lineage: Certificate lineage object :type lineage: storage.RenewableCert :returns: `None` @@ -1057,7 +1060,7 @@ def certonly(config, plugins): :param config: Configuration object :type config: interfaces.IConfig - :param plugins: list of plugins + :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` @@ -1104,7 +1107,7 @@ def renew(config, unused_plugins): :param config: Configuration object :type config: interfaces.IConfig - :param unused_plugins: list of plugins (deprecated) + :param unused_plugins: List of plugins (deprecated) :type unused_plugins: `list` of `str` :returns: `None` From cdd89998e3bb20d9337bbc3ef8d2d7fad9a43454 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 27 Nov 2017 14:49:19 -0800 Subject: [PATCH 11/67] Add nginx to these weird instructions (#5243) These are probably made obsolete by the instruction generator, and they don't include Ubuntu... --- docs/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index 1f6a45e07..a914586ff 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -156,7 +156,7 @@ If you run Debian Stretch or Debian Sid, you can install certbot packages. sudo apt-get install certbot python-certbot-apache If you don't want to use the Apache plugin, you can omit the -``python-certbot-apache`` package. +``python-certbot-apache`` package. Or you can install ``python-certbot-nginx`` instead. Packages exist for Debian Jessie via backports. First you'll have to follow the instructions at http://backports.debian.org/Instructions/ to enable the Jessie backports From f5ed771d4f884c2b2ca7df8cf973e633ae04be2b Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 27 Nov 2017 14:50:06 -0800 Subject: [PATCH 12/67] change some instances of help to flag (#5248) --- certbot/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 3a12f86c7..622462278 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -828,11 +828,11 @@ class HelpfulArgumentParser(object): return dict([(t, t == chosen_topic) for t in self.help_topics]) def _add_all_groups(helpful): - helpful.add_group("automation", description="Arguments for automating execution & other tweaks") + helpful.add_group("automation", description="Flags for automating execution & other tweaks") helpful.add_group("security", description="Security parameters & server settings") helpful.add_group("testing", description="The following flags are meant for testing and integration purposes only.") - helpful.add_group("paths", description="Arguments changing execution paths & servers") + helpful.add_group("paths", description="Flags for changing execution paths & servers") helpful.add_group("manage", description="Various subcommands and flags are available for managing your certificates:", verbs=["certificates", "delete", "renew", "revoke", "update_symlinks"]) From 8fd1d0d19e1be1ecbf3896ca66dbfa0c6732e6d9 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 28 Nov 2017 18:22:01 -0800 Subject: [PATCH 13/67] Small Travis cleanups (#5273) * Test with no hosts. * Simplify build matrix. * Remove after_failure. --- .travis.yml | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48b9b43cb..55f18338d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,6 @@ before_install: before_script: - 'if [ $TRAVIS_OS_NAME = osx ] ; then ulimit -n 1024 ; fi' -# using separate envs with different TOXENVs creates 4x1 Travis build -# matrix, which allows us to clearly distinguish which component under -# test has failed matrix: include: - python: "2.7" @@ -22,23 +19,14 @@ matrix: - python: "2.7" env: TOXENV=py27-oldest BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - python: "2.6" env: TOXENV=py26 BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - python: "2.7" env: TOXENV=py27_install BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - sudo: required env: TOXENV=apache_compat @@ -81,30 +69,18 @@ matrix: - python: "3.3" env: TOXENV=py33 BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - python: "3.4" env: TOXENV=py34 BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - python: "3.5" env: TOXENV=py35 BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - python: "3.6" env: TOXENV=py36 BOULDER_INTEGRATION=1 sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql services: docker - python: "2.7" env: TOXENV=nginxroundtrip @@ -130,17 +106,6 @@ branches: sudo: false addons: - # Custom /etc/hosts required for simple verification of http-01 - # and tls-sni-01, and for certbot_test_nginx - hosts: - - le.wtf - - le1.wtf - - le2.wtf - - le3.wtf - - nginx.wtf - - boulder - - boulder-mysql - - boulder-rabbitmq apt: sources: - augeas From d246ba78c76e686e252a3b190710f7563d87c025 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 29 Nov 2017 13:09:25 -0800 Subject: [PATCH 14/67] Use pip3 if pip isn't available (#5277) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 55f18338d..d331dc980 100644 --- a/.travis.yml +++ b/.travis.yml @@ -125,7 +125,7 @@ addons: - libapache2-mod-wsgi - libapache2-mod-macro -install: "travis_retry pip install tox coveralls" +install: "travis_retry $(command -v pip || command -v pip3) install tox coveralls" script: - travis_retry tox - '[ -z "${BOULDER_INTEGRATION+x}" ] || (travis_retry tests/boulder-fetch.sh && tests/tox-boulder-integration.sh)' From 20bca1942033c2c0164fd7b98be9243b323c7067 Mon Sep 17 00:00:00 2001 From: Eccenux Date: Thu, 30 Nov 2017 20:24:49 +0100 Subject: [PATCH 15/67] Show a diff when re-creating certificate instead of full list of domains #5274 --- certbot/main.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 9e2850891..11f7ddab7 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -253,18 +253,39 @@ def _find_lineage_for_domains_and_certname(config, domains, certname): "Use -d to specify domains, or run certbot --certificates to see " "possible certificate names.".format(certname)) +def _get_added_removed(after, before): + """Get lists of items removed from `before` + and a lists of items added to `after` + """ + added = list(set(after) - set(before)) + removed = list(set(before) - set(after)) + added.sort() + removed.sort() + return added, removed + +def _format_list(character, list): + """Format list with given character + """ + formatted = "{br}{ch} " + "{br}{ch} ".join(list) + return formatted.format( + ch=character, + br=os.linesep + ) + def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): """Ask user to confirm update cert certname to contain new_domains. """ if config.renew_with_new_domains: return - msg = ("You are updating certificate {0} to include domains: {1}{br}{br}" - "It previously included domains: {2}{br}{br}" + added, removed = _get_added_removed(new_domains, old_domains) + + msg = ("You are updating certificate {0} to include new domain(s): {1}{br}{br}" + "You are also removing previously included domain(s): {2}{br}{br}" "Did you intend to make this change?".format( certname, - ", ".join(new_domains), - ", ".join(old_domains), + _format_list("+", added), + _format_list("-", removed), br=os.linesep)) obj = zope.component.getUtility(interfaces.IDisplay) if not obj.yesno(msg, "Update cert", "Cancel", default=True): From 48173ed1cb075b73ac1b913cd3f3f872ba700c03 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 1 Dec 2017 10:59:55 -0800 Subject: [PATCH 16/67] Switch from nose to pytest (#5282) * Use pipstrap to install a good version of pip * Use pytest in cb-auto tests * Remove nose usage in auto_test.py * remove nose dev dep * use pytest in test_tests * Use pytest in tox * Update dev dependency pinnings * remove nose multiprocess lines * Use pytest for coverage * Use older py and pytest for old python versions * Add test for Error.__str__ * pin pytest in oldest test * Fix tests for DNS-DO plugin on py26 * Work around bug for Python 3.3 * Clarify dockerfile comments --- .coveragerc | 3 +-- acme/acme/crypto_util_test.py | 3 --- acme/acme/messages_test.py | 6 ++++++ acme/acme/standalone_test.py | 7 ------- acme/setup.py | 3 ++- .../tests/augeas_configurator_test.py | 1 - .../certbot_apache/tests/configurator_test.py | 2 -- .../dns_digitalocean_test.py | 5 ++--- .../certbot_nginx/tests/configurator_test.py | 1 - certbot/tests/cli_test.py | 4 ---- certbot/tests/ocsp_test.py | 1 - certbot/tests/storage_test.py | 1 - letsencrypt-auto-source/Dockerfile.centos6 | 10 ++++++--- letsencrypt-auto-source/Dockerfile.precise | 11 ++++++---- letsencrypt-auto-source/Dockerfile.trusty | 10 ++++++--- letsencrypt-auto-source/Dockerfile.wheezy | 12 ++++++----- letsencrypt-auto-source/tests/auto_test.py | 15 ++++++------- setup.cfg | 6 ------ setup.py | 4 +++- tests/letstest/scripts/test_tests.sh | 4 ++-- tools/install_and_test.sh | 6 +++++- tools/pip_constraints.txt | 7 ++++++- tools/release.sh | 6 +++--- tox.cover.sh | 21 +++++++------------ tox.ini | 4 +--- 25 files changed, 74 insertions(+), 79 deletions(-) diff --git a/.coveragerc b/.coveragerc index 087900105..1a87ab2da 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,2 @@ [report] -# show lines missing coverage in output -show_missing = True +omit = */setup.py diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 4046aa197..da433c5a2 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -18,7 +18,6 @@ from acme import test_util class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" - _multiprocess_can_split_ = True def setUp(self): self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') @@ -69,7 +68,6 @@ class SSLSocketAndProbeSNITest(unittest.TestCase): class PyOpenSSLCertOrReqSANTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" - _multiprocess_can_split_ = True @classmethod def _call(cls, loader, name): @@ -140,7 +138,6 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase): class RandomSnTest(unittest.TestCase): """Test for random certificate serial numbers.""" - _multiprocess_can_split_ = True def setUp(self): self.cert_count = 5 diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index b4ce19a08..631f0ce4d 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -71,6 +71,12 @@ class ErrorTest(unittest.TestCase): self.assertTrue(is_acme_error(Error.with_code('badCSR'))) self.assertRaises(ValueError, Error.with_code, 'not an ACME error code') + def test_str(self): + self.assertEqual( + str(self.error), + u"{0.typ} :: {0.description} :: {0.detail} :: {0.title}" + .format(self.error)) + class ConstantTest(unittest.TestCase): """Tests for acme.messages._Constant.""" diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 16669680c..48e13d0b6 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -23,7 +23,6 @@ from acme import test_util class TLSServerTest(unittest.TestCase): """Tests for acme.standalone.TLSServer.""" - _multiprocess_can_split_ = True def test_bind(self): # pylint: disable=no-self-use from acme.standalone import TLSServer @@ -42,7 +41,6 @@ class TLSServerTest(unittest.TestCase): class TLSSNI01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01Server.""" - _multiprocess_can_split_ = True def setUp(self): self.certs = {b'localhost': ( @@ -70,7 +68,6 @@ class TLSSNI01ServerTest(unittest.TestCase): class HTTP01ServerTest(unittest.TestCase): """Tests for acme.standalone.HTTP01Server.""" - _multiprocess_can_split_ = True def setUp(self): self.account_key = jose.JWK.load( @@ -124,7 +121,6 @@ class HTTP01ServerTest(unittest.TestCase): class BaseDualNetworkedServersTest(unittest.TestCase): """Test for acme.standalone.BaseDualNetworkedServers.""" - _multiprocess_can_split_ = True class SingleProtocolServer(socketserver.TCPServer): """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.""" @@ -174,7 +170,6 @@ class BaseDualNetworkedServersTest(unittest.TestCase): class TLSSNI01DualNetworkedServersTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01DualNetworkedServers.""" - _multiprocess_can_split_ = True def setUp(self): self.certs = {b'localhost': ( @@ -202,7 +197,6 @@ class TLSSNI01DualNetworkedServersTest(unittest.TestCase): class HTTP01DualNetworkedServersTest(unittest.TestCase): """Tests for acme.standalone.HTTP01DualNetworkedServers.""" - _multiprocess_can_split_ = True def setUp(self): self.account_key = jose.JWK.load( @@ -254,7 +248,6 @@ class HTTP01DualNetworkedServersTest(unittest.TestCase): class TestSimpleTLSSNI01Server(unittest.TestCase): """Tests for acme.standalone.simple_tls_sni_01_server.""" - _multiprocess_can_split_ = True def setUp(self): # mirror ../examples/standalone diff --git a/acme/setup.py b/acme/setup.py index c86a72b9d..c28e0c152 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -31,7 +31,8 @@ if sys.version_info < (2, 7): ]) dev_extras = [ - 'nose', + 'pytest', + 'pytest-xdist', 'tox', ] diff --git a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py index 742afce2d..c121ecdf3 100644 --- a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py +++ b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py @@ -13,7 +13,6 @@ from certbot_apache.tests import util class AugeasConfiguratorTest(util.ApacheTest): """Test for Augeas Configurator base class.""" - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=arguments-differ super(AugeasConfiguratorTest, self).setUp() diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 7c6b071da..90561d6ad 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -30,7 +30,6 @@ from certbot_apache.tests import util class MultipleVhostsTest(util.ApacheTest): """Test two standard well-configured HTTP vhosts.""" - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=arguments-differ super(MultipleVhostsTest, self).setUp() @@ -1369,7 +1368,6 @@ class MultipleVhostsTest(util.ApacheTest): class AugeasVhostsTest(util.ApacheTest): """Test vhosts with illegal names dependent on augeas version.""" # pylint: disable=protected-access - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=arguments-differ td = "debian_apache_2_4/augeas_vhosts" diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py index 11c8c57d5..0fdacf4ad 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py @@ -5,7 +5,6 @@ import unittest import digitalocean import mock -import six from certbot import errors from certbot.plugins import dns_test_common @@ -134,8 +133,8 @@ class DigitalOceanClientTest(unittest.TestCase): correct_record_mock.destroy.assert_called() - six.assertCountEqual(self, first_record_mock.destroy.call_args_list, []) - six.assertCountEqual(self, last_record_mock.destroy.call_args_list, []) + self.assertFalse(first_record_mock.destroy.call_args_list) + self.assertFalse(last_record_mock.destroy.call_args_list) def test_del_txt_record_error_finding_domain(self): self.manager.get_all_domains.side_effect = API_ERROR diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index aa94abecb..996bd238b 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -24,7 +24,6 @@ from certbot_nginx.tests import util class NginxConfiguratorTest(util.NginxTest): """Test a semi complex vhost configuration.""" - _multiprocess_can_split_ = True def setUp(self): super(NginxConfiguratorTest, self).setUp() diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index ba5e2775f..2fce412e2 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -26,7 +26,6 @@ PLUGINS = disco.PluginsRegistry.find_all() class TestReadFile(TempDirTestCase): '''Test cli.read_file''' - _multiprocess_can_split_ = True def test_read_file(self): rel_test_path = os.path.relpath(os.path.join(self.tempdir, 'foo')) @@ -46,7 +45,6 @@ class TestReadFile(TempDirTestCase): class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods '''Test the cli args entrypoint''' - _multiprocess_can_split_ = True def setUp(self): reload_module(cli) @@ -418,7 +416,6 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" - _multiprocess_can_split_ = True def setUp(self): # pylint: disable=protected-access @@ -439,7 +436,6 @@ class DefaultTest(unittest.TestCase): class SetByCliTest(unittest.TestCase): """Tests for certbot.set_by_cli and related functions.""" - _multiprocess_can_split_ = True def setUp(self): reload_module(cli) diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index 91dd6f8d6..2d54274f0 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -13,7 +13,6 @@ ocsp: Use -help for summary. class OCSPTest(unittest.TestCase): - _multiprocess_can_split_ = True def setUp(self): from certbot import ocsp diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index df6391758..6c8f775e2 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -43,7 +43,6 @@ class BaseRenewableCertTest(test_util.ConfigTestCase): your test. Check :class:`.cli_test.DuplicateCertTest` for an example. """ - _multiprocess_can_split_ = True def setUp(self): from certbot import storage diff --git a/letsencrypt-auto-source/Dockerfile.centos6 b/letsencrypt-auto-source/Dockerfile.centos6 index e1280109b..8c1a4b353 100644 --- a/letsencrypt-auto-source/Dockerfile.centos6 +++ b/letsencrypt-auto-source/Dockerfile.centos6 @@ -5,9 +5,13 @@ FROM centos:6 RUN yum install -y epel-release -# Install pip, sudo and nose: +# Install pip and sudo: RUN yum install -y python-pip sudo -RUN pip install nose +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 # Add an unprivileged user: RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups wheel --uid 1000 lea @@ -29,4 +33,4 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/Dockerfile.precise b/letsencrypt-auto-source/Dockerfile.precise index 5ee32c7cc..71d572315 100644 --- a/letsencrypt-auto-source/Dockerfile.precise +++ b/letsencrypt-auto-source/Dockerfile.precise @@ -6,13 +6,16 @@ FROM ubuntu:precise # Add an unprivileged user: RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea -# Install pip, sudo, openssl, and nose: +# Install pip, sudo, and openssl: RUN apt-get update && \ apt-get -q -y install python-pip sudo openssl && \ apt-get clean -ENV PIP_INDEX_URL https://pypi.python.org/simple -RUN pip install nose +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 # Let that user sudo: RUN sed -i.bkp -e \ @@ -30,4 +33,4 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/Dockerfile.trusty b/letsencrypt-auto-source/Dockerfile.trusty index 23e8f26de..e0aacd118 100644 --- a/letsencrypt-auto-source/Dockerfile.trusty +++ b/letsencrypt-auto-source/Dockerfile.trusty @@ -11,11 +11,15 @@ RUN sed -i.bkp -e \ 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \ /etc/sudoers -# Install pip and nose: +# Install pip: RUN apt-get update && \ apt-get -q -y install python-pip && \ apt-get clean -RUN pip install nose +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 RUN mkdir -p /home/lea/certbot @@ -29,4 +33,4 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/Dockerfile.wheezy b/letsencrypt-auto-source/Dockerfile.wheezy index acdb791a4..56948d22a 100644 --- a/letsencrypt-auto-source/Dockerfile.wheezy +++ b/letsencrypt-auto-source/Dockerfile.wheezy @@ -6,13 +6,15 @@ FROM debian:wheezy # Add an unprivileged user: RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea -# Install pip, sudo, openssl, and nose: +# Install pip, sudo, and openssl: RUN apt-get update && \ apt-get -q -y install python-pip sudo openssl && \ apt-get clean - -ENV PIP_INDEX_URL https://pypi.python.org/simple -RUN pip install nose +# Use pipstrap to update to a stable and tested version of pip +COPY ./pieces/pipstrap.py /opt +RUN /opt/pipstrap.py +# Pin pytest version for increased stability +RUN pip install pytest==3.2.5 # Let that user sudo: RUN sed -i.bkp -e \ @@ -30,4 +32,4 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 5c63325ee..2fa03105d 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -17,10 +17,10 @@ from tempfile import mkdtemp from threading import Thread from unittest import TestCase -from nose.tools import eq_, nottest, ok_ +from pytest import mark -@nottest +@mark.skip def tests_dir(): """Return a path to the "tests" directory.""" return dirname(abspath(__file__)) @@ -279,8 +279,8 @@ class AutoTests(TestCase): # installed, and pip hashes verify: install_le_auto(build_le_auto(version='50.0.0'), le_auto_path) out, err = run_letsencrypt_auto() - ok_(re.match(r'letsencrypt \d+\.\d+\.\d+', - err.strip().splitlines()[-1])) + self.assertTrue(re.match(r'letsencrypt \d+\.\d+\.\d+', + err.strip().splitlines()[-1])) # Make a few assertions to test the validity of the next tests: self.assertTrue('Upgrading certbot-auto ' in out) self.assertTrue('Creating virtual environment...' in out) @@ -327,7 +327,7 @@ class AutoTests(TestCase): try: out, err = run_le_auto(le_auto_path, venv_dir, base_url) except CalledProcessError as exc: - eq_(exc.returncode, 1) + self.assertEqual(exc.returncode, 1) self.assertTrue("Couldn't verify signature of downloaded " "certbot-auto." in exc.output) else: @@ -348,10 +348,11 @@ class AutoTests(TestCase): try: out, err = run_le_auto(le_auto_path, venv_dir, base_url) except CalledProcessError as exc: - eq_(exc.returncode, 1) + self.assertEqual(exc.returncode, 1) self.assertTrue("THESE PACKAGES DO NOT MATCH THE HASHES " "FROM THE REQUIREMENTS FILE" in exc.output) - ok_(not exists(venv_dir), + self.assertFalse( + exists(venv_dir), msg="The virtualenv was left around, even though " "installation didn't succeed. We shouldn't do " "this, as it foils our detection of whether we " diff --git a/setup.cfg b/setup.cfg index 3b4dbaf87..a21bab793 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,9 +3,3 @@ universal = 1 [easy_install] zip_ok = false - -[nosetests] -nocapture=1 -cover-package=certbot,acme,certbot_apache,certbot_nginx -cover-erase=1 -cover-tests=1 diff --git a/setup.py b/setup.py index 6ffc9a134..ee108c514 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,9 @@ dev_extras = [ 'astroid==1.3.5', 'coverage', 'ipdb', - 'nose', + 'pytest', + 'pytest-cov', + 'pytest-xdist', 'pylint==1.4.2', # upstream #248 'tox', 'twine', diff --git a/tests/letstest/scripts/test_tests.sh b/tests/letstest/scripts/test_tests.sh index 6d67bdf2e..4bed2dd3a 100755 --- a/tests/letstest/scripts/test_tests.sh +++ b/tests/letstest/scripts/test_tests.sh @@ -10,9 +10,9 @@ LE_AUTO_SUDO="" VENV_PATH=$VENV_NAME letsencrypt/certbot-auto --debug --no-boots # change to an empty directory to ensure CWD doesn't affect tests cd $(mktemp -d) -pip install nose +pip install pytest==3.2.5 for module in $MODULES ; do echo testing $module - nosetests -v $module + pytest -v --pyargs $module done diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh index 0de2ea3f8..0edb41c53 100755 --- a/tools/install_and_test.sh +++ b/tools/install_and_test.sh @@ -16,6 +16,10 @@ for requirement in "$@" ; do pkg=$(echo $requirement | cut -f1 -d\[) # remove any extras such as [dev] if [ $pkg = "." ]; then pkg="certbot" + else + # Work around a bug in pytest/importlib for the deprecated Python 3.3. + # See https://travis-ci.org/certbot/certbot/jobs/308774157#L1333. + pkg=$(echo "$pkg" | tr - _) fi - nosetests -v $pkg --processes=-1 --process-timeout=100 + pytest --numprocesses auto --quiet --pyargs $pkg done diff --git a/tools/pip_constraints.txt b/tools/pip_constraints.txt index 8970d417c..f2fd3d836 100644 --- a/tools/pip_constraints.txt +++ b/tools/pip_constraints.txt @@ -4,6 +4,7 @@ # invocation, some constraints may be ignored due to pip's lack of dependency # resolution. alabaster==0.7.10 +apipkg==1.4 astroid==1.3.5 Babel==2.5.1 backports.shutil-get-terminal-size==1.0.0 @@ -15,6 +16,7 @@ decorator==4.1.2 dns-lexicon[dnsmadeeasy]==2.1.11 dnspython==1.15.0 docutils==0.14 +execnet==1.5.0 future==0.16.0 futures==3.1.1 google-api-python-client==1.6.4 @@ -27,7 +29,6 @@ Jinja2==2.9.6 jmespath==0.9.3 logilab-common==1.4.1 MarkupSafe==1.0 -nose==1.3.7 oauth2client==4.1.2 pathlib2==2.3.0 pexpect==4.2.1 @@ -42,6 +43,10 @@ pyasn1==0.3.7 pyasn1-modules==0.1.5 Pygments==2.2.0 pylint==1.4.2 +pytest==3.2.5 +pytest-cov==2.5.1 +pytest-forked==0.2 +pytest-xdist==1.20.1 python-dateutil==2.6.1 python-digitalocean==1.12 PyYAML==3.12 diff --git a/tools/release.sh b/tools/release.sh index 2a8e00aa1..a8de208b5 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -55,7 +55,7 @@ SUBPKGS="$SUBPKGS_IN_AUTO $SUBPKGS_NOT_IN_AUTO" subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs -# - it causes problems when running nosetests - the latter tries to +# - it causes problems when running pytest - the latter tries to # run everything that matches test*, while there are no unittests # there @@ -166,10 +166,10 @@ fi mkdir kgs kgs="kgs/$version" pip freeze | tee $kgs -pip install nose +pip install pytest for module in $subpkgs_modules ; do echo testing $module - nosetests $module + pytest --pyargs $module done cd ~- diff --git a/tox.cover.sh b/tox.cover.sh index fc0c9f476..3f0a5f72e 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -16,7 +16,7 @@ fi cover () { if [ "$1" = "certbot" ]; then - min=98 + min=97 elif [ "$1" = "acme" ]; then min=100 elif [ "$1" = "certbot_apache" ]; then @@ -24,23 +24,23 @@ cover () { elif [ "$1" = "certbot_dns_cloudflare" ]; then min=98 elif [ "$1" = "certbot_dns_cloudxns" ]; then - min=99 + min=98 elif [ "$1" = "certbot_dns_digitalocean" ]; then min=98 elif [ "$1" = "certbot_dns_dnsimple" ]; then min=98 elif [ "$1" = "certbot_dns_dnsmadeeasy" ]; then - min=99 + min=98 elif [ "$1" = "certbot_dns_google" ]; then min=99 elif [ "$1" = "certbot_dns_luadns" ]; then min=98 elif [ "$1" = "certbot_dns_nsone" ]; then - min=99 + min=98 elif [ "$1" = "certbot_dns_rfc2136" ]; then min=99 elif [ "$1" = "certbot_dns_route53" ]; then - min=99 + min=91 elif [ "$1" = "certbot_nginx" ]; then min=97 elif [ "$1" = "letshelp_certbot" ]; then @@ -50,17 +50,10 @@ cover () { exit 1 fi - # "-c /dev/null" makes sure setup.cfg is not loaded (multiple - # --with-cover add up, --cover-erase must not be set for coveralls - # to get all the data); --with-cover scopes coverage to only - # specific package, positional argument scopes tests only to - # specific package directory; --cover-tests makes sure every tests - # is run (c.f. #403) - nosetests -c /dev/null --with-cover --cover-tests --cover-package \ - "$1" --cover-min-percentage="$min" "$1" + pytest --cov "$1" --cov-report term-missing \ + --cov-fail-under "$min" --numprocesses auto --pyargs "$1" } -rm -f .coverage # --cover-erase is off, make sure stats are correct for pkg in $pkgs do cover $pkg diff --git a/tox.ini b/tox.ini index dee14b8b3..c6dc61155 100644 --- a/tox.ini +++ b/tox.ini @@ -6,9 +6,6 @@ skipsdist = true envlist = modification,py{26,33,34,35,36},cover,lint -# nosetest -v => more verbose output, allows to detect busy waiting -# loops, especially on Travis - [base] # pip installs the requested packages in editable mode pip_install = {toxinidir}/tools/pip_install_editable.sh @@ -96,6 +93,7 @@ deps = pyasn1==0.1.9 pyparsing==1.5.6 pyrfc3339==1.0 + pytest==3.2.5 python-augeas==0.4.1 pytz==2012c requests[security]==2.6.0 From b9b329ecf7f3a285758a2f0e69c5f35e4c1e0259 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 1 Dec 2017 13:20:27 -0800 Subject: [PATCH 17/67] pin pkging tools that have dropped support (#5281) --- tox.ini | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tox.ini b/tox.ini index c6dc61155..bb421daa5 100644 --- a/tox.ini +++ b/tox.ini @@ -59,6 +59,9 @@ source_paths = commands = {[base]install_and_test} {[base]py26_packages} python tests/lock_test.py +deps = + setuptools==36.8.0 + wheel==0.29.0 [testenv] commands = @@ -68,6 +71,12 @@ setenv = PYTHONPATH = {toxinidir} PYTHONHASHSEED = 0 +[testenv:py33] +commands = + {[testenv]commands} +deps = + wheel==0.29.0 + [testenv:py27-oldest] commands = {[testenv]commands} From 8ce6ee5f3e976c7cc795ba3049b3f77367d3b557 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 1 Dec 2017 16:10:16 -0800 Subject: [PATCH 18/67] Remove all but one BOULDER_INTEGRATION, and macOS (#5270) These tests are retained in the test-everything branch, which has a Travis cron job to run nightly. Removing these speeds up the Certbot Travis builds dramatically for two reasons: - The Boulder integration tests are slow (10-12 minutes), and it's exceedingly rare for them to fail on one Python environment but not another. - The macOS tests take a very long time to run, because they need to wait for build slots on the limited number of macOS instances, which are often in high demand. --- .travis.yml | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index d331dc980..359801622 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,15 +13,15 @@ before_script: matrix: include: - python: "2.7" - env: TOXENV=cover + env: TOXENV=cover FYI="this also tests py27" - python: "2.7" env: TOXENV=lint - python: "2.7" - env: TOXENV=py27-oldest BOULDER_INTEGRATION=1 + env: TOXENV=py27-oldest sudo: required services: docker - python: "2.6" - env: TOXENV=py26 BOULDER_INTEGRATION=1 + env: TOXENV=py26 sudo: required services: docker - python: "2.7" @@ -67,29 +67,23 @@ matrix: env: TOXENV=apacheconftest sudo: required - python: "3.3" - env: TOXENV=py33 BOULDER_INTEGRATION=1 + env: TOXENV=py33 sudo: required services: docker - python: "3.4" - env: TOXENV=py34 BOULDER_INTEGRATION=1 + env: TOXENV=py34 sudo: required services: docker - python: "3.5" - env: TOXENV=py35 BOULDER_INTEGRATION=1 + env: TOXENV=py35 sudo: required services: docker - python: "3.6" - env: TOXENV=py36 BOULDER_INTEGRATION=1 + env: TOXENV=py36 sudo: required services: docker - python: "2.7" env: TOXENV=nginxroundtrip - - language: generic - env: TOXENV=py27 - os: osx - - language: generic - env: TOXENV=py36 - os: osx # Only build pushes to the master branch, PRs, and branches beginning with From 394dafd38c91818582ec55372f887108762f32eb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 1 Dec 2017 17:00:24 -0800 Subject: [PATCH 19/67] Revert requiring dnsmadeeasy extras for lexicon (#5291) Fixes failures at https://travis-ci.org/certbot/certbot/jobs/310248574#L1558. Additional context can be found at #5230 and https://github.com/AnalogJ/lexicon/commit/604584521a487dd0b8814320b3f1af52b82ad205#diff-2eeaed663bd0d25b7e608891384b7298. --- certbot-dns-dnsmadeeasy/setup.py | 4 +--- tools/pip_constraints.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index 1ddec208b..88e02304e 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -10,9 +10,7 @@ version = '0.20.0.dev0' install_requires = [ 'acme=={0}'.format(version), 'certbot=={0}'.format(version), - # new versions of lexicon require that we install dnsmadeeasy extras and - # 2.1.11 is the first version that defines them. - 'dns-lexicon[dnsmadeeasy]>=2.1.11', + 'dns-lexicon', 'mock', # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: diff --git a/tools/pip_constraints.txt b/tools/pip_constraints.txt index f2fd3d836..cacec37d6 100644 --- a/tools/pip_constraints.txt +++ b/tools/pip_constraints.txt @@ -13,7 +13,7 @@ botocore==1.7.41 cloudflare==1.8.1 coverage==4.4.2 decorator==4.1.2 -dns-lexicon[dnsmadeeasy]==2.1.11 +dns-lexicon==2.1.14 dnspython==1.15.0 docutils==0.14 execnet==1.5.0 From 7319cc975a1bb41102a948399cd1a09b4c90b17b Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 1 Dec 2017 23:40:09 -0800 Subject: [PATCH 20/67] Quiet pip install output. (#5288) pip install generates a lot of lines of output that make it harder to see what tox is running in general. This adds the -q flag to pip install. At the same time, add `set -x` in install_and_test.sh and pip_install.sh so they echo the commands they are running. This makes it a little clearer what's going on in tests. I didn't put `set -x` at the top or in the shebang, because moving it lower lets us avoid echoing some of the messy if/then setup statements in these scripts, which focussed attention on the pip install command. --- tools/install_and_test.sh | 3 ++- tools/pip_install.sh | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh index 0edb41c53..d57f0974e 100755 --- a/tools/install_and_test.sh +++ b/tools/install_and_test.sh @@ -6,11 +6,12 @@ # constraints. if [ "$CERTBOT_NO_PIN" = 1 ]; then - pip_install="pip install -e" + pip_install="pip install -q -e" else pip_install="$(dirname $0)/pip_install_editable.sh" fi +set -x for requirement in "$@" ; do $pip_install $requirement pkg=$(echo $requirement | cut -f1 -d\[) # remove any extras such as [dev] diff --git a/tools/pip_install.sh b/tools/pip_install.sh index 194501c7d..fafd58e54 100755 --- a/tools/pip_install.sh +++ b/tools/pip_install.sh @@ -11,5 +11,7 @@ trap "rm -f $certbot_auto_constraints" EXIT sed -n -e 's/^\([^[:space:]]*==[^[:space:]]*\).*$/\1/p' $requirements > $certbot_auto_constraints dev_constraints="$(dirname $my_path)/pip_constraints.txt" +set -x + # install the requested packages using the pinned requirements as constraints -pip install --constraint $certbot_auto_constraints --constraint $dev_constraints "$@" +pip install -q --constraint $certbot_auto_constraints --constraint $dev_constraints "$@" From abdde886fa3d6361336a2fb1d7e091b53be4b6f2 Mon Sep 17 00:00:00 2001 From: Eccenux Date: Sat, 2 Dec 2017 12:25:58 +0100 Subject: [PATCH 21/67] code style --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 11f7ddab7..cee381cbd 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -254,7 +254,7 @@ def _find_lineage_for_domains_and_certname(config, domains, certname): "possible certificate names.".format(certname)) def _get_added_removed(after, before): - """Get lists of items removed from `before` + """Get lists of items removed from `before` and a lists of items added to `after` """ added = list(set(after) - set(before)) From 840c94371111ead7f20be925688ad2fb6d931551 Mon Sep 17 00:00:00 2001 From: Eccenux Date: Sat, 2 Dec 2017 12:28:53 +0100 Subject: [PATCH 22/67] W:266,28: Redefining built-in 'list' (redefined-builtin) --- certbot/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index cee381cbd..f61c70b05 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -263,10 +263,10 @@ def _get_added_removed(after, before): removed.sort() return added, removed -def _format_list(character, list): +def _format_list(character, strings): """Format list with given character """ - formatted = "{br}{ch} " + "{br}{ch} ".join(list) + formatted = "{br}{ch} " + "{br}{ch} ".join(strings) return formatted.format( ch=character, br=os.linesep From 73ba9af4422b61ad2b45ed9d3f1e8d19b5f9167a Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 4 Dec 2017 11:20:53 -0800 Subject: [PATCH 23/67] Don't echo Boulder logs on failure. (#5290) The extensive logs made it hard to spot the actual failure. --- tests/boulder-integration.sh | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 7afba19df..1e0b7754b 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -20,14 +20,6 @@ cleanup_and_exit() { echo Kill server subprocess, left running by abnormal exit kill $SERVER_STILL_RUNNING fi - # Dump boulder logs in case they contain useful debugging information. - : "------------------ ------------------ ------------------" - : "------------------ begin boulder logs ------------------" - : "------------------ ------------------ ------------------" - docker logs boulder_boulder_1 - : "------------------ ------------------ ------------------" - : "------------------ end boulder logs ------------------" - : "------------------ ------------------ ------------------" if [ -f "$HOOK_DIRS_TEST" ]; then rm -f "$HOOK_DIRS_TEST" fi From dc78fd731ee2b5b88e140f99242b4267f2fd0f27 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 4 Dec 2017 21:49:18 +0200 Subject: [PATCH 24/67] Distribution specific override functionality based on class inheritance (#5202) Class inheritance based approach to distro specific overrides. How it works: The certbot-apache plugin entrypoint has been changed to entrypoint.ENTRYPOINT which is a variable containing appropriate override class for system, if available. Override classes register themselves using decorator override.register() which takes a list of distribution fingerprints (ID & LIKE variables in /etc/os-release, or platform.linux_distribution() as a fallback). These end up as keys in dict override.OVERRIDE_CLASSES and values for the keys are references to the class that called the decorator, hence allowing self-registration of override classes when they are imported. The only file importing these override classes is entrypoint.py, so adding new override classes would need only one import in addition to the actual override class file. Generic changes: Parser initialization has been moved to separate class method, allowing easy override where needed. Cleaned up configurator.py a bit, and moved some helper functions to newly created apache_util.py Split Debian specific code from configurator.py to debian_override.py Changed define_cmd to apache_cmd because the parameters are for every distribution supporting this behavior, and we're able to use the value to build the additional configuration dump commands. Moved add_parser_mod() from configurator to parser add_mod() Added two new configuration dump parsing methods to update_runtime_variables() in parser: update_includes() and update_modules(). Changed init_modules() in parser to accommodate the changes above. (ie. don't throw existing self.modules out). Moved OS based constants to their respective override classes. Refactored configurator class discovery in tests to help easier test case creation using distribution based override configurator class. tests.util.get_apache_configurator() now takes keyword argument os_info which is string of the desired mock OS fingerprint response that's used for picking the right override class. This PR includes two major generic additions that should vastly improve our parsing accuracy and quality: Includes are parsed from config dump from httpd binary. This is mandatory for some distributions (Like OpenSUSE) to get visibility over the whole configuration tree because of Include statements passed on in command line, and not via root httpd.conf file. Modules are parsed from config dump from httpd binary. This lets us jump into correct IfModule directives if for some reason we have missed the module availability (because of one being included on command line or such). Distribution specific changes Because of the generic changes, there are two distributions (or distribution families) that do not provide such functionality, so it had to be overridden in their respective override files. These distributions are: CentOS, because it deliberately limits httpd binary stdout using SELinux as a feature. We are doing opportunistic config dumps here however, in case SELinux enforcing is off. Gentoo, because it does not provide a way to invoke httpd with command line parsed from its specific configuration file. Gentoo relies heavily on Define statements that are passed over from APACHE2_OPTS variable /etc/conf.d/apache2 file and most of the configuration in root Apache configuration are dependent on these values. Debian Moved the Debian specific parts from configurator.py to Debian specific override. CentOS Parsing of /etc/sysconfig/httpd file for additional Define statements. This could hold other parameters too, but parsing everything off it would require a full Apache lexer. For CLI parameters, I think Defines are the most common ones. This is done in addition of opportunistic parsing of httpd binary config dump. Added CentOS default Apache configuration tree for realistic test cases. Gentoo Parsing Defines from /etc/conf.d/apache2 variable APACHE2_OPTS, which holds additional Define statements to enable certain functionalities, enabling parts of the configuration in the Apache2 DOM. This is done instead of trying to parse httpd binary configuration dumps. Added default Apache configuration from Gentoo to testdata, including /etc/conf.d/apache2 file for realistic test cases. * Distribution specific override functionality based on class inheritance * Need to patch get_systemd_os_like to as travis has proper os-release * Added pydoc * Move parser initialization to a method and fix Python 3 __new__ errors * Parser changes to parse HTTPD config * Try to get modules and includes from httpd process for better visibility over the configuration * Had to disable duplicate-code because of test setup (PyCQA/pylint/issues/214) * CentOS tests and linter fixes * Gentoo override, tests and linter fixes * Mock the process call in all the tests that require it * Fix CentOS test mock * Restore reseting modules list functionality for cleanup * Move OS fingerprinting and constant mocks to parent class * Fixes requested in review * New entrypoint structure and started moving OS constants to override classes * OS constants move continued, test and linter fixes * Removed dead code * Apache compatibility test changest to reflect OS constant restructure * Test fix * Requested changes * Moved Debian specific tests to own test file * Removed decorator based override class registration in favor of entrypoint dict * Fix for update_includes for some versions of Augeas * Take fedora fix into account in tests * Review fixes --- .pylintrc | 2 +- certbot-apache/certbot_apache/apache_util.py | 96 +++++ certbot-apache/certbot_apache/configurator.py | 282 ++++--------- certbot-apache/certbot_apache/constants.py | 181 -------- certbot-apache/certbot_apache/entrypoint.py | 47 +++ .../certbot_apache/override_arch.py | 31 ++ .../certbot_apache/override_centos.py | 59 +++ .../certbot_apache/override_darwin.py | 31 ++ .../certbot_apache/override_debian.py | 144 +++++++ .../certbot_apache/override_gentoo.py | 58 +++ .../certbot_apache/override_suse.py | 31 ++ certbot-apache/certbot_apache/parser.py | 123 ++++-- .../certbot_apache/tests/centos_test.py | 123 ++++++ .../tests/complex_parsing_test.py | 2 +- .../certbot_apache/tests/configurator_test.py | 329 +++++---------- .../certbot_apache/tests/constants_test.py | 44 -- .../certbot_apache/tests/debian_test.py | 209 ++++++++++ .../certbot_apache/tests/entrypoint_test.py | 41 ++ .../certbot_apache/tests/gentoo_test.py | 86 ++++ .../certbot_apache/tests/parser_test.py | 125 +++++- .../centos7_apache/apache/httpd/conf.d/README | 9 + .../apache/httpd/conf.d/autoindex.conf | 94 +++++ .../httpd/conf.d/centos.example.com.conf | 7 + .../apache/httpd/conf.d/ssl.conf | 211 ++++++++++ .../apache/httpd/conf.d/userdir.conf | 36 ++ .../apache/httpd/conf.d/welcome.conf | 22 + .../apache/httpd/conf.modules.d/00-base.conf | 77 ++++ .../apache/httpd/conf.modules.d/00-dav.conf | 3 + .../apache/httpd/conf.modules.d/00-lua.conf | 1 + .../apache/httpd/conf.modules.d/00-mpm.conf | 19 + .../apache/httpd/conf.modules.d/00-proxy.conf | 16 + .../apache/httpd/conf.modules.d/00-ssl.conf | 1 + .../httpd/conf.modules.d/00-systemd.conf | 2 + .../apache/httpd/conf.modules.d/01-cgi.conf | 14 + .../apache/httpd/conf/httpd.conf | 353 ++++++++++++++++ .../centos7_apache/apache/httpd/conf/magic | 385 ++++++++++++++++++ .../testdata/centos7_apache/apache/sites | 1 + .../centos7_apache/apache/sysconfig/httpd | 25 ++ .../gentoo_apache/apache/apache2/httpd.conf | 157 +++++++ .../gentoo_apache/apache/apache2/magic | 385 ++++++++++++++++++ .../modules.d/.keep_www-servers_apache-2 | 0 .../modules.d/00_default_settings.conf | 131 ++++++ .../apache2/modules.d/00_error_documents.conf | 57 +++ .../apache2/modules.d/00_languages.conf | 133 ++++++ .../apache2/modules.d/00_mod_autoindex.conf | 85 ++++ .../apache/apache2/modules.d/00_mod_info.conf | 10 + .../apache2/modules.d/00_mod_log_config.conf | 35 ++ .../apache/apache2/modules.d/00_mod_mime.conf | 46 +++ .../apache2/modules.d/00_mod_status.conf | 15 + .../apache2/modules.d/00_mod_userdir.conf | 32 ++ .../apache/apache2/modules.d/00_mpm.conf | 99 +++++ .../apache2/modules.d/10_mod_mem_cache.conf | 10 + .../apache/apache2/modules.d/40_mod_ssl.conf | 67 +++ .../apache2/modules.d/41_mod_http2.conf | 9 + .../apache/apache2/modules.d/45_mod_dav.conf | 19 + .../apache/apache2/modules.d/46_mod_ldap.conf | 18 + .../vhosts.d/.keep_www-servers_apache-2 | 0 .../vhosts.d/00_default_ssl_vhost.conf | 191 +++++++++ .../apache2/vhosts.d/00_default_vhost.conf | 45 ++ .../apache2/vhosts.d/default_vhost.include | 71 ++++ .../apache2/vhosts.d/gentoo.example.com.conf | 7 + .../gentoo_apache/apache/conf.d/apache2 | 74 ++++ .../tests/testdata/gentoo_apache/apache/sites | 3 + .../certbot_apache/tests/tls_sni_01_test.py | 16 +- certbot-apache/certbot_apache/tests/util.py | 70 +++- certbot-apache/setup.py | 2 +- .../configurators/apache/common.py | 10 +- certbot/util.py | 14 +- 68 files changed, 4383 insertions(+), 748 deletions(-) create mode 100644 certbot-apache/certbot_apache/apache_util.py create mode 100644 certbot-apache/certbot_apache/entrypoint.py create mode 100644 certbot-apache/certbot_apache/override_arch.py create mode 100644 certbot-apache/certbot_apache/override_centos.py create mode 100644 certbot-apache/certbot_apache/override_darwin.py create mode 100644 certbot-apache/certbot_apache/override_debian.py create mode 100644 certbot-apache/certbot_apache/override_gentoo.py create mode 100644 certbot-apache/certbot_apache/override_suse.py create mode 100644 certbot-apache/certbot_apache/tests/centos_test.py delete mode 100644 certbot-apache/certbot_apache/tests/constants_test.py create mode 100644 certbot-apache/certbot_apache/tests/debian_test.py create mode 100644 certbot-apache/certbot_apache/tests/entrypoint_test.py create mode 100644 certbot-apache/certbot_apache/tests/gentoo_test.py create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/README create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/magic create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sites create mode 100644 certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/httpd.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/magic create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/.keep_www-servers_apache-2 create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/.keep_www-servers_apache-2 create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/conf.d/apache2 create mode 100644 certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/sites diff --git a/.pylintrc b/.pylintrc index d510fddfd..36d8c286f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -41,7 +41,7 @@ load-plugins=linter_plugin # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import +disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code # abstract-class-not-used cannot be disabled locally (at least in # pylint 1.4.1), same for abstract-class-little-used diff --git a/certbot-apache/certbot_apache/apache_util.py b/certbot-apache/certbot_apache/apache_util.py new file mode 100644 index 000000000..b4a24f137 --- /dev/null +++ b/certbot-apache/certbot_apache/apache_util.py @@ -0,0 +1,96 @@ +""" Utility functions for certbot-apache plugin """ +import os + +from certbot import util + +def get_mod_deps(mod_name): + """Get known module dependencies. + + .. note:: This does not need to be accurate in order for the client to + run. This simply keeps things clean if the user decides to revert + changes. + .. warning:: If all deps are not included, it may cause incorrect parsing + behavior, due to enable_mod's shortcut for updating the parser's + currently defined modules (`.ApacheParser.add_mod`) + This would only present a major problem in extremely atypical + configs that use ifmod for the missing deps. + + """ + deps = { + "ssl": ["setenvif", "mime"] + } + return deps.get(mod_name, []) + + +def get_file_path(vhost_path): + """Get file path from augeas_vhost_path. + + Takes in Augeas path and returns the file name + + :param str vhost_path: Augeas virtual host path + + :returns: filename of vhost + :rtype: str + + """ + if not vhost_path or not vhost_path.startswith("/files/"): + return None + + return _split_aug_path(vhost_path)[0] + + +def get_internal_aug_path(vhost_path): + """Get the Augeas path for a vhost with the file path removed. + + :param str vhost_path: Augeas virtual host path + + :returns: Augeas path to vhost relative to the containing file + :rtype: str + + """ + return _split_aug_path(vhost_path)[1] + + +def _split_aug_path(vhost_path): + """Splits an Augeas path into a file path and an internal path. + + After removing "/files", this function splits vhost_path into the + file path and the remaining Augeas path. + + :param str vhost_path: Augeas virtual host path + + :returns: file path and internal Augeas path + :rtype: `tuple` of `str` + + """ + # Strip off /files + file_path = vhost_path[6:] + internal_path = [] + + # Remove components from the end of file_path until it becomes valid + while not os.path.exists(file_path): + file_path, _, internal_path_part = file_path.rpartition("/") + internal_path.append(internal_path_part) + + return file_path, "/".join(reversed(internal_path)) + + +def parse_define_file(filepath, varname): + """ Parses Defines from a variable in configuration file + + :param str filepath: Path of file to parse + :param str varname: Name of the variable + + :returns: Dict of Define:Value pairs + :rtype: `dict` + + """ + return_vars = {} + # Get list of words in the variable + a_opts = util.get_var_from_file(varname, filepath).split() + for i, v in enumerate(a_opts): + # Handle Define statements and make sure it has an argument + if v == "-D" and len(a_opts) >= i+2: + var_parts = a_opts[i+1].partition("=") + return_vars[var_parts[0]] = var_parts[2] + return return_vars diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 8b6c578d6..5a33346ea 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -3,6 +3,7 @@ import fnmatch import logging import os +import pkg_resources import re import socket import time @@ -19,6 +20,7 @@ from certbot import util from certbot.plugins import common from certbot.plugins.util import path_surgery +from certbot_apache import apache_util from certbot_apache import augeas_configurator from certbot_apache import constants from certbot_apache import display_ops @@ -85,27 +87,50 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): description = "Apache Web Server plugin - Beta" + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/sites-available", + vhost_files="*", + logs_root="/var/log/apache2", + version_cmd=['apache2ctl', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) + + def constant(self, key): + """Get constant for OS_DEFAULTS""" + return self.OS_DEFAULTS.get(key) + @classmethod def add_parser_arguments(cls, add): - add("enmod", default=constants.os_constant("enmod"), + add("enmod", default=cls.OS_DEFAULTS["enmod"], help="Path to the Apache 'a2enmod' binary.") - add("dismod", default=constants.os_constant("dismod"), + add("dismod", default=cls.OS_DEFAULTS["dismod"], help="Path to the Apache 'a2dismod' binary.") - add("le-vhost-ext", default=constants.os_constant("le_vhost_ext"), + add("le-vhost-ext", default=cls.OS_DEFAULTS["le_vhost_ext"], help="SSL vhost configuration extension.") - add("server-root", default=constants.os_constant("server_root"), + add("server-root", default=cls.OS_DEFAULTS["server_root"], help="Apache server root directory.") add("vhost-root", default=None, help="Apache server VirtualHost configuration root") - add("logs-root", default=constants.os_constant("logs_root"), + add("logs-root", default=cls.OS_DEFAULTS["logs_root"], help="Apache server logs directory") add("challenge-location", - default=constants.os_constant("challenge_location"), + default=cls.OS_DEFAULTS["challenge_location"], help="Directory path for challenge configuration.") - add("handle-modules", default=constants.os_constant("handle_mods"), + add("handle-modules", default=cls.OS_DEFAULTS["handle_mods"], help="Let installer handle enabling required modules for you." + "(Only Ubuntu/Debian currently)") - add("handle-sites", default=constants.os_constant("handle_sites"), + add("handle-sites", default=cls.OS_DEFAULTS["handle_sites"], help="Let installer handle enabling sites for you." + "(Only Ubuntu/Debian currently)") util.add_deprecated_argument(add, argument_name="ctl", nargs=1) @@ -166,7 +191,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): raise errors.NoInstallationError("Problem in Augeas installation") # Verify Apache is installed - restart_cmd = constants.os_constant("restart_cmd")[0] + restart_cmd = self.constant("restart_cmd")[0] if not util.exe_exists(restart_cmd): if not path_surgery(restart_cmd): raise errors.NoInstallationError( @@ -192,20 +217,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Parse vhost-root if defined on cli if not self.conf("vhost-root"): - self.vhostroot = constants.os_constant("vhost_root") + self.vhostroot = self.constant("vhost_root") else: self.vhostroot = os.path.abspath(self.conf("vhost-root")) - self.parser = parser.ApacheParser( - self.aug, self.conf("server-root"), self.conf("vhost-root"), - self.version, configurator=self) + self.parser = self.get_parser() + # Check for errors in parsing files with Augeas self.check_parsing_errors("httpd.aug") # Get all of the available vhosts self.vhosts = self.get_virtual_hosts() - install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest) + self.install_ssl_options_conf(self.mod_ssl_conf, + self.updated_mod_ssl_conf_digest) # Prevent two Apache plugins from modifying a config at once try: @@ -230,6 +255,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.aug.remove("/test/path") return matches + def get_parser(self): + """Initializes the ApacheParser""" + return parser.ApacheParser( + self.aug, self.conf("server-root"), self.conf("vhost-root"), + self.version, configurator=self) + def deploy_cert(self, domain, cert_path, key_path, chain_path=None, fullchain_path=None): """Deploys certificate to specified virtual host. @@ -585,7 +616,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if addr.get_port() == "443": is_ssl = True - filename = get_file_path(self.aug.get("/augeas/files%s/path" % get_file_path(path))) + filename = apache_util.get_file_path( + self.aug.get("/augeas/files%s/path" % apache_util.get_file_path(path))) if filename is None: return None @@ -624,7 +656,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): new_vhost = self._create_vhost(path) if not new_vhost: continue - internal_path = get_internal_aug_path(new_vhost.path) + internal_path = apache_util.get_internal_aug_path(new_vhost.path) realpath = os.path.realpath(new_vhost.filep) if realpath not in file_paths: file_paths[realpath] = new_vhost.filep @@ -640,7 +672,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): for v in vhs: if v.filep == file_paths[realpath]: internal_paths[realpath].remove( - get_internal_aug_path(v.path)) + apache_util.get_internal_aug_path(v.path)) else: new_vhs.append(v) vhs = new_vhs @@ -828,7 +860,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): Duplicates vhost and adds default ssl options New vhost will reside as (nonssl_vhost.path) + - ``certbot_apache.constants.os_constant("le_vhost_ext")`` + ``self.constant("le_vhost_ext")`` .. note:: This function saves the configuration @@ -1702,17 +1734,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return redirects - def enable_site(self, vhost): """Enables an available site, Apache reload required. .. note:: Does not make sure that the site correctly works or that all modules are enabled appropriately. - - .. todo:: This function should number subdomains before the domain - vhost - - .. todo:: Make sure link is not broken... + .. note:: The distribution specific override replaces functionality + of this method where available. :param vhost: vhost to enable :type vhost: :class:`~certbot_apache.obj.VirtualHost` @@ -1724,39 +1752,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if vhost.enabled: return - # Handle non-debian systems - if not self.conf("handle-sites"): - if not self.parser.parsed_in_original(vhost.filep): - # Add direct include to root conf - self.parser.add_include(self.parser.loc["default"], vhost.filep) - vhost.enabled = True - return + if not self.parser.parsed_in_original(vhost.filep): + # Add direct include to root conf + logger.info("Enabling site %s by adding Include to root configuration", + vhost.filep) + self.save_notes += "Enabled site %s\n" % vhost.filep + self.parser.add_include(self.parser.loc["default"], vhost.filep) + vhost.enabled = True + return - enabled_path = ("%s/sites-enabled/%s" % - (self.parser.root, os.path.basename(vhost.filep))) - self.reverter.register_file_creation(False, enabled_path) - try: - os.symlink(vhost.filep, enabled_path) - except OSError as err: - if os.path.islink(enabled_path) and os.path.realpath( - enabled_path) == vhost.filep: - # Already in shape - vhost.enabled = True - return - else: - logger.warning( - "Could not symlink %s to %s, got error: %s", enabled_path, - vhost.filep, err.strerror) - errstring = ("Encountered error while trying to enable a " + - "newly created VirtualHost located at {0} by " + - "linking to it from {1}") - raise errors.NotSupportedError(errstring.format(vhost.filep, - enabled_path)) - vhost.enabled = True - logger.info("Enabling available site: %s", vhost.filep) - self.save_notes += "Enabled site %s\n" % vhost.filep - - def enable_mod(self, mod_name, temp=False): + def enable_mod(self, mod_name, temp=False): # pylint: disable=unused-argument """Enables module in Apache. Both enables and reloads Apache so module is active. @@ -1764,64 +1769,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str mod_name: Name of the module to enable. (e.g. 'ssl') :param bool temp: Whether or not this is a temporary action. - :raises .errors.NotSupportedError: If the filesystem layout is not - supported. - :raises .errors.MisconfigurationError: If a2enmod or a2dismod cannot be - run. + .. note:: The distribution specific override replaces functionality + of this method where available. + + :raises .errors.MisconfigurationError: We cannot enable modules in + generic fashion. """ - # Support Debian specific setup - avail_path = os.path.join(self.parser.root, "mods-available") - enabled_path = os.path.join(self.parser.root, "mods-enabled") - if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path): - raise errors.NotSupportedError( - "Unsupported directory layout. You may try to enable mod %s " - "and try again." % mod_name) - - deps = _get_mod_deps(mod_name) - - # Enable all dependencies - for dep in deps: - if (dep + "_module") not in self.parser.modules: - self._enable_mod_debian(dep, temp) - self._add_parser_mod(dep) - - note = "Enabled dependency of %s module - %s" % (mod_name, dep) - if not temp: - self.save_notes += note + os.linesep - logger.debug(note) - - # Enable actual module - self._enable_mod_debian(mod_name, temp) - self._add_parser_mod(mod_name) - - if not temp: - self.save_notes += "Enabled %s module in Apache\n" % mod_name - logger.info("Enabled Apache %s module", mod_name) - - # Modules can enable additional config files. Variables may be defined - # within these new configuration sections. - # Reload is not necessary as DUMP_RUN_CFG uses latest config. - self.parser.update_runtime_variables() - - def _add_parser_mod(self, mod_name): - """Shortcut for updating parser modules.""" - self.parser.modules.add(mod_name + "_module") - self.parser.modules.add("mod_" + mod_name + ".c") - - def _enable_mod_debian(self, mod_name, temp): - """Assumes mods-available, mods-enabled layout.""" - # Generate reversal command. - # Try to be safe here... check that we can probably reverse before - # applying enmod command - if not util.exe_exists(self.conf("dismod")): - raise errors.MisconfigurationError( - "Unable to find a2dismod, please make sure a2enmod and " - "a2dismod are configured correctly for certbot.") - - self.reverter.register_undo_command( - temp, [self.conf("dismod"), mod_name]) - util.run_script([self.conf("enmod"), mod_name]) + mod_message = ("Apache needs to have module \"{0}\" active for the " + + "requested installation options. Unfortunately Certbot is unable " + + "to install or enable it for you. Please install the module, and " + + "run Certbot again.") + raise errors.MisconfigurationError(mod_message.format(mod_name)) def restart(self): """Runs a config test and reloads the Apache server. @@ -1840,7 +1799,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - util.run_script(constants.os_constant("restart_cmd")) + util.run_script(self.constant("restart_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1851,7 +1810,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - util.run_script(constants.os_constant("conftest_cmd")) + util.run_script(self.constant("conftest_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1867,11 +1826,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - stdout, _ = util.run_script(constants.os_constant("version_cmd")) + stdout, _ = util.run_script(self.constant("version_cmd")) except errors.SubprocessError: raise errors.PluginError( "Unable to run %s -v" % - constants.os_constant("version_cmd")) + self.constant("version_cmd")) regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) matches = regex.findall(stdout) @@ -1943,86 +1902,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not self._chall_out: self.revert_challenge_config() self.restart() - self.parser.init_modules() + self.parser.reset_modules() + + 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.""" + + # XXX if we ever try to enforce a local privilege boundary (eg, running + # certbot for unprivileged users via setuid), this function will need + # to be modified. + return common.install_version_controlled_file(options_ssl, options_ssl_digest, + self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES) -def _get_mod_deps(mod_name): - """Get known module dependencies. - - .. note:: This does not need to be accurate in order for the client to - run. This simply keeps things clean if the user decides to revert - changes. - .. warning:: If all deps are not included, it may cause incorrect parsing - behavior, due to enable_mod's shortcut for updating the parser's - currently defined modules (`.ApacheConfigurator._add_parser_mod`) - This would only present a major problem in extremely atypical - configs that use ifmod for the missing deps. - - """ - deps = { - "ssl": ["setenvif", "mime"] - } - return deps.get(mod_name, []) - - -def get_file_path(vhost_path): - """Get file path from augeas_vhost_path. - - Takes in Augeas path and returns the file name - - :param str vhost_path: Augeas virtual host path - - :returns: filename of vhost - :rtype: str - - """ - if not vhost_path or not vhost_path.startswith("/files/"): - return None - - return _split_aug_path(vhost_path)[0] - - -def get_internal_aug_path(vhost_path): - """Get the Augeas path for a vhost with the file path removed. - - :param str vhost_path: Augeas virtual host path - - :returns: Augeas path to vhost relative to the containing file - :rtype: str - - """ - return _split_aug_path(vhost_path)[1] - - -def _split_aug_path(vhost_path): - """Splits an Augeas path into a file path and an internal path. - - After removing "/files", this function splits vhost_path into the - file path and the remaining Augeas path. - - :param str vhost_path: Augeas virtual host path - - :returns: file path and internal Augeas path - :rtype: `tuple` of `str` - - """ - # Strip off /files - file_path = vhost_path[6:] - internal_path = [] - - # Remove components from the end of file_path until it becomes valid - while not os.path.exists(file_path): - file_path, _, internal_path_part = file_path.rpartition("/") - internal_path.append(internal_path_part) - - return file_path, "/".join(reversed(internal_path)) - - -def install_ssl_options_conf(options_ssl, options_ssl_digest): - """Copy Certbot's SSL options file into the system's config dir if required.""" - - # XXX if we ever try to enforce a local privilege boundary (eg, running - # certbot for unprivileged users via setuid), this function will need - # to be modified. - return common.install_version_controlled_file(options_ssl, options_ssl_digest, - constants.os_constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index 0c5f7263a..a13ca04a6 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -1,151 +1,6 @@ """Apache plugin constants.""" import pkg_resources -from certbot import util -CLI_DEFAULTS_DEFAULT = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/sites-available", - vhost_files="*", - logs_root="/var/log/apache2", - version_cmd=['apache2ctl', '-v'], - define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_DEBIAN = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/sites-available", - vhost_files="*", - logs_root="/var/log/apache2", - version_cmd=['apache2ctl', '-v'], - define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod="a2enmod", - dismod="a2dismod", - le_vhost_ext="-le-ssl.conf", - handle_mods=True, - handle_sites=True, - challenge_location="/etc/apache2", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_CENTOS = dict( - server_root="/etc/httpd", - vhost_root="/etc/httpd/conf.d", - vhost_files="*.conf", - logs_root="/var/log/httpd", - version_cmd=['apachectl', '-v'], - define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apachectl', 'graceful'], - conftest_cmd=['apachectl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/httpd/conf.d", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "centos-options-ssl-apache.conf") -) -CLI_DEFAULTS_GENTOO = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/vhosts.d", - vhost_files="*.conf", - logs_root="/var/log/apache2", - version_cmd=['/usr/sbin/apache2', '-v'], - define_cmd=['apache2ctl', 'virtualhosts'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2/vhosts.d", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_DARWIN = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/other", - vhost_files="*.conf", - logs_root="/var/log/apache2", - version_cmd=['/usr/sbin/httpd', '-v'], - define_cmd=['/usr/sbin/httpd', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apachectl', 'graceful'], - conftest_cmd=['apachectl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2/other", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_SUSE = dict( - server_root="/etc/apache2", - vhost_root="/etc/apache2/vhosts.d", - vhost_files="*.conf", - logs_root="/var/log/apache2", - version_cmd=['apache2ctl', '-v'], - define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apache2ctl', 'graceful'], - conftest_cmd=['apache2ctl', 'configtest'], - enmod="a2enmod", - dismod="a2dismod", - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/apache2/vhosts.d", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS_ARCH = dict( - server_root="/etc/httpd", - vhost_root="/etc/httpd/conf", - vhost_files="*.conf", - logs_root="/var/log/httpd", - version_cmd=['apachectl', '-v'], - define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'], - restart_cmd=['apachectl', 'graceful'], - conftest_cmd=['apachectl', 'configtest'], - enmod=None, - dismod=None, - le_vhost_ext="-le-ssl.conf", - handle_mods=False, - handle_sites=False, - challenge_location="/etc/httpd/conf", - MOD_SSL_CONF_SRC=pkg_resources.resource_filename( - "certbot_apache", "options-ssl-apache.conf") -) -CLI_DEFAULTS = { - "default": CLI_DEFAULTS_DEFAULT, - "debian": CLI_DEFAULTS_DEBIAN, - "ubuntu": CLI_DEFAULTS_DEBIAN, - "centos": CLI_DEFAULTS_CENTOS, - "centos linux": CLI_DEFAULTS_CENTOS, - "fedora": CLI_DEFAULTS_CENTOS, - "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, - "rhel": CLI_DEFAULTS_CENTOS, - "amazon": CLI_DEFAULTS_CENTOS, - "gentoo": CLI_DEFAULTS_GENTOO, - "gentoo base system": CLI_DEFAULTS_GENTOO, - "darwin": CLI_DEFAULTS_DARWIN, - "opensuse": CLI_DEFAULTS_SUSE, - "suse": CLI_DEFAULTS_SUSE, - "arch": CLI_DEFAULTS_ARCH, -} -"""CLI defaults.""" MOD_SSL_CONF_DEST = "options-ssl-apache.conf" """Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" @@ -191,39 +46,3 @@ UIR_ARGS = ["always", "set", "Content-Security-Policy", HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS, "Upgrade-Insecure-Requests": UIR_ARGS} - - -def os_constant(key): - """ - Get a constant value for operating system - - :param key: name of cli constant - :return: value of constant for active os - """ - - os_info = util.get_os_info() - try: - constants = CLI_DEFAULTS[os_info[0].lower()] - except KeyError: - constants = os_like_constants() - if not constants: - constants = CLI_DEFAULTS["default"] - return constants[key] - - -def os_like_constants(): - """ - Try to get constants for distribution with - similar layout and configuration, indicated by - /etc/os-release variable "LIKE" - - :returns: Constants dictionary - :rtype: `dict` - """ - - os_like = util.get_systemd_os_like() - if os_like: - for os_name in os_like: - if os_name in CLI_DEFAULTS.keys(): - return CLI_DEFAULTS[os_name] - return {} diff --git a/certbot-apache/certbot_apache/entrypoint.py b/certbot-apache/certbot_apache/entrypoint.py new file mode 100644 index 000000000..4267398d5 --- /dev/null +++ b/certbot-apache/certbot_apache/entrypoint.py @@ -0,0 +1,47 @@ +""" Entry point for Apache Plugin """ +from certbot import util + +from certbot_apache import configurator +from certbot_apache import override_arch +from certbot_apache import override_darwin +from certbot_apache import override_debian +from certbot_apache import override_centos +from certbot_apache import override_gentoo +from certbot_apache import override_suse + +OVERRIDE_CLASSES = { + "arch": override_arch.ArchConfigurator, + "darwin": override_darwin.DarwinConfigurator, + "debian": override_debian.DebianConfigurator, + "ubuntu": override_debian.DebianConfigurator, + "centos": override_centos.CentOSConfigurator, + "centos linux": override_centos.CentOSConfigurator, + "fedora": override_centos.CentOSConfigurator, + "red hat enterprise linux server": override_centos.CentOSConfigurator, + "rhel": override_centos.CentOSConfigurator, + "amazon": override_centos.CentOSConfigurator, + "gentoo": override_gentoo.GentooConfigurator, + "gentoo base system": override_gentoo.GentooConfigurator, + "opensuse": override_suse.OpenSUSEConfigurator, + "suse": override_suse.OpenSUSEConfigurator, +} + +def get_configurator(): + """ Get correct configurator class based on the OS fingerprint """ + os_info = util.get_os_info() + override_class = None + try: + override_class = OVERRIDE_CLASSES[os_info[0].lower()] + except KeyError: + # OS not found in the list + os_like = util.get_systemd_os_like() + if os_like: + for os_name in os_like: + if os_name in OVERRIDE_CLASSES.keys(): + override_class = OVERRIDE_CLASSES[os_name] + if not override_class: + # No override class found, return the generic configurator + override_class = configurator.ApacheConfigurator + return override_class + +ENTRYPOINT = get_configurator() diff --git a/certbot-apache/certbot_apache/override_arch.py b/certbot-apache/certbot_apache/override_arch.py new file mode 100644 index 000000000..ea5155a3c --- /dev/null +++ b/certbot-apache/certbot_apache/override_arch.py @@ -0,0 +1,31 @@ +""" Distribution specific override class for Arch Linux """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import configurator + +@zope.interface.provider(interfaces.IPluginFactory) +class ArchConfigurator(configurator.ApacheConfigurator): + """Arch Linux specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/httpd", + vhost_root="/etc/httpd/conf", + vhost_files="*.conf", + logs_root="/var/log/httpd", + version_cmd=['apachectl', '-v'], + apache_cmd="apachectl", + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/httpd/conf", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) diff --git a/certbot-apache/certbot_apache/override_centos.py b/certbot-apache/certbot_apache/override_centos.py new file mode 100644 index 000000000..db6cd6fba --- /dev/null +++ b/certbot-apache/certbot_apache/override_centos.py @@ -0,0 +1,59 @@ +""" Distribution specific override class for CentOS family (RHEL, Fedora) """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import apache_util +from certbot_apache import configurator +from certbot_apache import parser + +@zope.interface.provider(interfaces.IPluginFactory) +class CentOSConfigurator(configurator.ApacheConfigurator): + """CentOS specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/httpd", + vhost_root="/etc/httpd/conf.d", + vhost_files="*.conf", + logs_root="/var/log/httpd", + version_cmd=['apachectl', '-v'], + apache_cmd="apachectl", + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/httpd/conf.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "centos-options-ssl-apache.conf") + ) + + def get_parser(self): + """Initializes the ApacheParser""" + return CentOSParser( + self.aug, self.conf("server-root"), self.conf("vhost-root"), + self.version, configurator=self) + + +class CentOSParser(parser.ApacheParser): + """CentOS specific ApacheParser override class""" + def __init__(self, *args, **kwargs): + # CentOS specific configuration file for Apache + self.sysconfig_filep = "/etc/sysconfig/httpd" + super(CentOSParser, self).__init__(*args, **kwargs) + + def update_runtime_variables(self, *args, **kwargs): + """ Override for update_runtime_variables for custom parsing """ + # Opportunistic, works if SELinux not enforced + super(CentOSParser, self).update_runtime_variables(*args, **kwargs) + self.parse_sysconfig_var() + + def parse_sysconfig_var(self): + """ Parses Apache CLI options from CentOS configuration file """ + defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS") + for k in defines.keys(): + self.variables[k] = defines[k] diff --git a/certbot-apache/certbot_apache/override_darwin.py b/certbot-apache/certbot_apache/override_darwin.py new file mode 100644 index 000000000..53741d504 --- /dev/null +++ b/certbot-apache/certbot_apache/override_darwin.py @@ -0,0 +1,31 @@ +""" Distribution specific override class for macOS """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import configurator + +@zope.interface.provider(interfaces.IPluginFactory) +class DarwinConfigurator(configurator.ApacheConfigurator): + """macOS specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/other", + vhost_files="*.conf", + logs_root="/var/log/apache2", + version_cmd=['/usr/sbin/httpd', '-v'], + apache_cmd="/usr/sbin/httpd", + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2/other", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) diff --git a/certbot-apache/certbot_apache/override_debian.py b/certbot-apache/certbot_apache/override_debian.py new file mode 100644 index 000000000..6e2e34ba9 --- /dev/null +++ b/certbot-apache/certbot_apache/override_debian.py @@ -0,0 +1,144 @@ +""" Distribution specific override class for Debian family (Ubuntu/Debian) """ +import logging +import os +import pkg_resources + +import zope.interface + +from certbot import errors +from certbot import interfaces +from certbot import util + +from certbot_apache import apache_util +from certbot_apache import configurator + +logger = logging.getLogger(__name__) + +@zope.interface.provider(interfaces.IPluginFactory) +class DebianConfigurator(configurator.ApacheConfigurator): + """Debian specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/sites-available", + vhost_files="*", + logs_root="/var/log/apache2", + version_cmd=['apache2ctl', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod="a2enmod", + dismod="a2dismod", + le_vhost_ext="-le-ssl.conf", + handle_mods=True, + handle_sites=True, + challenge_location="/etc/apache2", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) + + def enable_site(self, vhost): + """Enables an available site, Apache reload required. + + .. note:: Does not make sure that the site correctly works or that all + modules are enabled appropriately. + + :param vhost: vhost to enable + :type vhost: :class:`~certbot_apache.obj.VirtualHost` + + :raises .errors.NotSupportedError: If filesystem layout is not + supported. + + """ + if vhost.enabled: + return + + enabled_path = ("%s/sites-enabled/%s" % + (self.parser.root, + os.path.basename(vhost.filep))) + if not os.path.isdir(os.path.dirname(enabled_path)): + # For some reason, sites-enabled / sites-available do not exist + # Call the parent method + return super(DebianConfigurator, self).enable_site(vhost) + self.reverter.register_file_creation(False, enabled_path) + try: + os.symlink(vhost.filep, enabled_path) + except OSError as err: + if os.path.islink(enabled_path) and os.path.realpath( + enabled_path) == vhost.filep: + # Already in shape + vhost.enabled = True + return + else: + logger.warning( + "Could not symlink %s to %s, got error: %s", enabled_path, + vhost.filep, err.strerror) + errstring = ("Encountered error while trying to enable a " + + "newly created VirtualHost located at {0} by " + + "linking to it from {1}") + raise errors.NotSupportedError(errstring.format(vhost.filep, + enabled_path)) + vhost.enabled = True + logger.info("Enabling available site: %s", vhost.filep) + self.save_notes += "Enabled site %s\n" % vhost.filep + + def enable_mod(self, mod_name, temp=False): + # pylint: disable=unused-argument + """Enables module in Apache. + + Both enables and reloads Apache so module is active. + + :param str mod_name: Name of the module to enable. (e.g. 'ssl') + :param bool temp: Whether or not this is a temporary action. + + :raises .errors.NotSupportedError: If the filesystem layout is not + supported. + :raises .errors.MisconfigurationError: If a2enmod or a2dismod cannot be + run. + + """ + avail_path = os.path.join(self.parser.root, "mods-available") + enabled_path = os.path.join(self.parser.root, "mods-enabled") + if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path): + raise errors.NotSupportedError( + "Unsupported directory layout. You may try to enable mod %s " + "and try again." % mod_name) + + deps = apache_util.get_mod_deps(mod_name) + + # Enable all dependencies + for dep in deps: + if (dep + "_module") not in self.parser.modules: + self._enable_mod_debian(dep, temp) + self.parser.add_mod(dep) + note = "Enabled dependency of %s module - %s" % (mod_name, dep) + if not temp: + self.save_notes += note + os.linesep + logger.debug(note) + + # Enable actual module + self._enable_mod_debian(mod_name, temp) + self.parser.add_mod(mod_name) + + if not temp: + self.save_notes += "Enabled %s module in Apache\n" % mod_name + logger.info("Enabled Apache %s module", mod_name) + + # Modules can enable additional config files. Variables may be defined + # within these new configuration sections. + # Reload is not necessary as DUMP_RUN_CFG uses latest config. + self.parser.update_runtime_variables() + + def _enable_mod_debian(self, mod_name, temp): + """Assumes mods-available, mods-enabled layout.""" + # Generate reversal command. + # Try to be safe here... check that we can probably reverse before + # applying enmod command + if not util.exe_exists(self.conf("dismod")): + raise errors.MisconfigurationError( + "Unable to find a2dismod, please make sure a2enmod and " + "a2dismod are configured correctly for certbot.") + + self.reverter.register_undo_command( + temp, [self.conf("dismod"), mod_name]) + util.run_script([self.conf("enmod"), mod_name]) diff --git a/certbot-apache/certbot_apache/override_gentoo.py b/certbot-apache/certbot_apache/override_gentoo.py new file mode 100644 index 000000000..d4d4e96b9 --- /dev/null +++ b/certbot-apache/certbot_apache/override_gentoo.py @@ -0,0 +1,58 @@ +""" Distribution specific override class for Gentoo Linux """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import apache_util +from certbot_apache import configurator +from certbot_apache import parser + +@zope.interface.provider(interfaces.IPluginFactory) +class GentooConfigurator(configurator.ApacheConfigurator): + """Gentoo specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/vhosts.d", + vhost_files="*.conf", + logs_root="/var/log/apache2", + version_cmd=['/usr/sbin/apache2', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2/vhosts.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) + + def get_parser(self): + """Initializes the ApacheParser""" + return GentooParser( + self.aug, self.conf("server-root"), self.conf("vhost-root"), + self.version, configurator=self) + + +class GentooParser(parser.ApacheParser): + """Gentoo specific ApacheParser override class""" + def __init__(self, *args, **kwargs): + # Gentoo specific configuration file for Apache2 + self.apacheconfig_filep = "/etc/conf.d/apache2" + super(GentooParser, self).__init__(*args, **kwargs) + + def update_runtime_variables(self): + """ Override for update_runtime_variables for custom parsing """ + self.parse_sysconfig_var() + + def parse_sysconfig_var(self): + """ Parses Apache CLI options from Gentoo configuration file """ + defines = apache_util.parse_define_file(self.apacheconfig_filep, + "APACHE2_OPTS") + for k in defines.keys(): + self.variables[k] = defines[k] diff --git a/certbot-apache/certbot_apache/override_suse.py b/certbot-apache/certbot_apache/override_suse.py new file mode 100644 index 000000000..a67054b5b --- /dev/null +++ b/certbot-apache/certbot_apache/override_suse.py @@ -0,0 +1,31 @@ +""" Distribution specific override class for OpenSUSE """ +import pkg_resources + +import zope.interface + +from certbot import interfaces + +from certbot_apache import configurator + +@zope.interface.provider(interfaces.IPluginFactory) +class OpenSUSEConfigurator(configurator.ApacheConfigurator): + """OpenSUSE specific ApacheConfigurator override class""" + + OS_DEFAULTS = dict( + server_root="/etc/apache2", + vhost_root="/etc/apache2/vhosts.d", + vhost_files="*.conf", + logs_root="/var/log/apache2", + version_cmd=['apache2ctl', '-v'], + apache_cmd="apache2ctl", + restart_cmd=['apache2ctl', 'graceful'], + conftest_cmd=['apache2ctl', 'configtest'], + enmod="a2enmod", + dismod="a2dismod", + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/apache2/vhosts.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") + ) diff --git a/certbot-apache/certbot_apache/parser.py b/certbot-apache/certbot_apache/parser.py index b15608d61..7715d2c35 100644 --- a/certbot-apache/certbot_apache/parser.py +++ b/certbot-apache/certbot_apache/parser.py @@ -11,8 +11,6 @@ import six from certbot import errors -from certbot_apache import constants - logger = logging.getLogger(__name__) @@ -40,14 +38,9 @@ class ApacheParser(object): # issues with aug.load() after adding new files / defines to parse tree self.configurator = configurator - # This uses the binary, so it can be done first. - # https://httpd.apache.org/docs/2.4/mod/core.html#define - # https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine - # This only handles invocation parameters and Define directives! + self.modules = set() self.parser_paths = {} self.variables = {} - if version >= (2, 4): - self.update_runtime_variables() self.aug = aug # Find configuration root and make sure augeas can parse it. @@ -55,24 +48,26 @@ class ApacheParser(object): self.loc = {"root": self._find_config_root()} self.parse_file(self.loc["root"]) + if version >= (2, 4): + # Look up variables from httpd and add to DOM if not already parsed + self.update_runtime_variables() + # This problem has been fixed in Augeas 1.0 self.standardize_excl() - # Temporarily set modules to be empty, so that find_dirs can work - # https://httpd.apache.org/docs/2.4/mod/core.html#ifmodule - # This needs to come before locations are set. - self.modules = set() - self.init_modules() + # Parse LoadModule directives from configuration files + self.parse_modules() # Set up rest of locations self.loc.update(self._set_locations()) + # list of the active include paths, before modifications self.existing_paths = copy.deepcopy(self.parser_paths) # Must also attempt to parse additional virtual host root if vhostroot: self.parse_file(os.path.abspath(vhostroot) + "/" + - constants.os_constant("vhost_files")) + self.configurator.constant("vhost_files")) # check to see if there were unparsed define statements if version < (2, 4): @@ -103,50 +98,61 @@ class ApacheParser(object): # Create a new path self.existing_paths[new_dir] = [new_file] - def init_modules(self): + def add_mod(self, mod_name): + """Shortcut for updating parser modules.""" + if mod_name + "_module" not in self.modules: + self.modules.add(mod_name + "_module") + if "mod_" + mod_name + ".c" not in self.modules: + self.modules.add("mod_" + mod_name + ".c") + + def reset_modules(self): + """Reset the loaded modules list. This is called from cleanup to clear + temporarily loaded modules.""" + self.modules = set() + self.update_modules() + self.parse_modules() + + def parse_modules(self): """Iterates on the configuration until no new modules are loaded. ..todo:: This should be attempted to be done with a binary to avoid the iteration issue. Else... parse and enable mods at same time. """ - # Since modules are being initiated... clear existing set. - self.modules = set() + mods = set() matches = self.find_dir("LoadModule") - iterator = iter(matches) # Make sure prev_size != cur_size for do: while: iteration prev_size = -1 - while len(self.modules) != prev_size: - prev_size = len(self.modules) + while len(mods) != prev_size: + prev_size = len(mods) for match_name, match_filename in six.moves.zip( iterator, iterator): mod_name = self.get_arg(match_name) mod_filename = self.get_arg(match_filename) if mod_name and mod_filename: - self.modules.add(mod_name) - self.modules.add(os.path.basename(mod_filename)[:-2] + "c") + mods.add(mod_name) + mods.add(os.path.basename(mod_filename)[:-2] + "c") else: logger.debug("Could not read LoadModule directive from " + "Augeas path: {0}".format(match_name[6:])) + self.modules.update(mods) def update_runtime_variables(self): - """" + """Update Includes, Defines and Includes from httpd config dump data""" + self.update_defines() + self.update_includes() + self.update_modules() - .. note:: Compile time variables (apache2ctl -V) are not used within - the dynamic configuration files. These should not be parsed or - interpreted. - - .. todo:: Create separate compile time variables... - simply for arg_get() - - """ - stdout = self._get_runtime_cfg() + def update_defines(self): + """Get Defines from httpd process""" variables = dict() - matches = re.compile(r"Define: ([^ \n]*)").findall(stdout) + define_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D", + "DUMP_RUN_CFG"] + matches = self.parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)") try: matches.remove("DUMP_RUN_CFG") except ValueError: @@ -163,15 +169,54 @@ class ApacheParser(object): self.variables = variables - def _get_runtime_cfg(self): # pylint: disable=no-self-use - """Get runtime configuration info. + def update_includes(self): + """Get includes from httpd process, and add them to DOM if needed""" - :returns: stdout from DUMP_RUN_CFG + # Find_dir iterates over configuration for Include and IncludeOptional + # directives to make sure we see the full include tree present in the + # configuration files + _ = self.find_dir("Include") + + inc_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D", + "DUMP_INCLUDES"] + matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)") + if matches: + for i in matches: + if not self.parsed_in_current(i): + self.parse_file(i) + + def update_modules(self): + """Get loaded modules from httpd process, and add them to DOM""" + + mod_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D", + "DUMP_MODULES"] + matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module") + for mod in matches: + self.add_mod(mod.strip()) + + def parse_from_subprocess(self, command, regexp): + """Get values from stdout of subprocess command + + :param list command: Command to run + :param str regexp: Regexp for parsing + + :returns: list parsed from command output + :rtype: list + + """ + stdout = self._get_runtime_cfg(command) + return re.compile(regexp).findall(stdout) + + def _get_runtime_cfg(self, command): # pylint: disable=no-self-use + """Get runtime configuration info. + :param command: Command to run + + :returns: stdout from command """ try: proc = subprocess.Popen( - constants.os_constant("define_cmd"), + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) @@ -180,10 +225,10 @@ class ApacheParser(object): except (OSError, ValueError): logger.error( "Error running command %s for runtime parameters!%s", - constants.os_constant("define_cmd"), os.linesep) + command, os.linesep) raise errors.MisconfigurationError( "Error accessing loaded Apache parameters: %s", - constants.os_constant("define_cmd")) + command) # Small errors that do not impede if proc.returncode != 0: logger.warning("Error in checking parameter list: %s", stderr) diff --git a/certbot-apache/certbot_apache/tests/centos_test.py b/certbot-apache/certbot_apache/tests/centos_test.py new file mode 100644 index 000000000..7ca47a4d5 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/centos_test.py @@ -0,0 +1,123 @@ +"""Test for certbot_apache.configurator for Centos overrides""" +import os +import unittest + +import mock + +from certbot_apache import obj +from certbot_apache import override_centos +from certbot_apache.tests import util + +def get_vh_truth(temp_dir, config_name): + """Return the ground truth for the specified directory.""" + prefix = os.path.join( + temp_dir, config_name, "httpd/conf.d") + + aug_pre = "/files" + prefix + vh_truth = [ + obj.VirtualHost( + os.path.join(prefix, "centos.example.com.conf"), + os.path.join(aug_pre, "centos.example.com.conf/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), + False, True, "centos.example.com"), + obj.VirtualHost( + os.path.join(prefix, "ssl.conf"), + os.path.join(aug_pre, "ssl.conf/VirtualHost"), + set([obj.Addr.fromstring("_default_:443")]), + True, True, None) + ] + return vh_truth + +class MultipleVhostsTestCentOS(util.ApacheTest): + """Multiple vhost tests for CentOS / RHEL family of distros""" + + _multiprocess_can_split_ = True + + def setUp(self): # pylint: disable=arguments-differ + test_dir = "centos7_apache/apache" + config_root = "centos7_apache/apache/httpd" + vhost_root = "centos7_apache/apache/httpd/conf.d" + super(MultipleVhostsTestCentOS, self).setUp(test_dir=test_dir, + config_root=config_root, + vhost_root=vhost_root) + + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir, + os_info="centos") + self.vh_truth = get_vh_truth( + self.temp_dir, "centos7_apache/apache") + + def test_get_parser(self): + self.assertTrue(isinstance(self.config.parser, + override_centos.CentOSParser)) + + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_opportunistic_httpd_runtime_parsing(self, mock_get): + define_val = ( + 'Define: TEST1\n' + 'Define: TEST2\n' + 'Define: DUMP_RUN_CFG\n' + ) + mod_val = ( + 'Loaded Modules:\n' + ' mock_module (static)\n' + ' another_module (static)\n' + ) + def mock_get_cfg(command): + """Mock httpd process stdout""" + if command == ['apachectl', '-t', '-D', 'DUMP_RUN_CFG']: + return define_val + elif command == ['apachectl', '-t', '-D', 'DUMP_MODULES']: + return mod_val + return "" + mock_get.side_effect = mock_get_cfg + self.config.parser.modules = set() + self.config.parser.variables = {} + + with mock.patch("certbot.util.get_os_info") as mock_osi: + # Make sure we have the have the CentOS httpd constants + mock_osi.return_value = ("centos", "7") + self.config.parser.update_runtime_variables() + + self.assertEquals(mock_get.call_count, 3) + self.assertEquals(len(self.config.parser.modules), 4) + self.assertEquals(len(self.config.parser.variables), 2) + self.assertTrue("TEST2" in self.config.parser.variables.keys()) + self.assertTrue("mod_another.c" in self.config.parser.modules) + + def test_get_virtual_hosts(self): + """Make sure all vhosts are being properly found.""" + vhs = self.config.get_virtual_hosts() + self.assertEqual(len(vhs), 2) + found = 0 + + for vhost in vhs: + for centos_truth in self.vh_truth: + if vhost == centos_truth: + found += 1 + break + else: + raise Exception("Missed: %s" % vhost) # pragma: no cover + self.assertEqual(found, 2) + + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_get_sysconfig_vars(self, mock_cfg): + """Make sure we read the sysconfig OPTIONS variable correctly""" + # Return nothing for the process calls + mock_cfg.return_value = "" + self.config.parser.sysconfig_filep = os.path.realpath( + os.path.join(self.config.parser.root, "../sysconfig/httpd")) + self.config.parser.variables = {} + + with mock.patch("certbot.util.get_os_info") as mock_osi: + # Make sure we have the have the CentOS httpd constants + mock_osi.return_value = ("centos", "7") + self.config.parser.update_runtime_variables() + + self.assertTrue("mock_define" in self.config.parser.variables.keys()) + self.assertTrue("mock_define_too" in self.config.parser.variables.keys()) + self.assertTrue("mock_value" in self.config.parser.variables.keys()) + self.assertEqual("TRUE", self.config.parser.variables["mock_value"]) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/complex_parsing_test.py b/certbot-apache/certbot_apache/tests/complex_parsing_test.py index 079d7e95f..a296fb0eb 100644 --- a/certbot-apache/certbot_apache/tests/complex_parsing_test.py +++ b/certbot-apache/certbot_apache/tests/complex_parsing_test.py @@ -18,7 +18,7 @@ class ComplexParserTest(util.ParserTest): self.setup_variables() # This needs to happen after due to setup_variables not being run # until after - self.parser.init_modules() # pylint: disable=protected-access + self.parser.parse_modules() # pylint: disable=protected-access def tearDown(self): shutil.rmtree(self.temp_dir) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 90561d6ad..4f85e1e3f 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -3,12 +3,12 @@ import os import shutil import socket +import tempfile import unittest import mock # six is used in mock.patch() import six # pylint: disable=unused-import -import tempfile from acme import challenges @@ -19,7 +19,7 @@ from certbot import errors from certbot.tests import acme_util from certbot.tests import util as certbot_util -from certbot_apache import configurator +from certbot_apache import apache_util from certbot_apache import constants from certbot_apache import parser from certbot_apache import obj @@ -34,39 +34,24 @@ class MultipleVhostsTest(util.ApacheTest): def setUp(self): # pylint: disable=arguments-differ super(MultipleVhostsTest, self).setUp() - from certbot_apache.constants import os_constant - orig_os_constant = os_constant - def mock_os_constant(key, vhost_path=self.vhost_path): - """Mock default vhost path""" - if key == "vhost_root": - return vhost_path - else: - return orig_os_constant(key) - - with mock.patch("certbot_apache.constants.os_constant") as mock_c: - mock_c.side_effect = mock_os_constant - self.config = util.get_apache_configurator( - self.config_path, None, self.config_dir, self.work_dir) - self.config = self.mock_deploy_cert(self.config) + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir) + self.config = self.mock_deploy_cert(self.config) self.vh_truth = util.get_vh_truth( self.temp_dir, "debian_apache_2_4/multiple_vhosts") def mock_deploy_cert(self, config): """A test for a mock deploy cert""" - self.config.real_deploy_cert = self.config.deploy_cert + config.real_deploy_cert = self.config.deploy_cert def mocked_deploy_cert(*args, **kwargs): """a helper to mock a deployed cert""" - with mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod"): + g_mod = "certbot_apache.configurator.ApacheConfigurator.enable_mod" + with mock.patch(g_mod): config.real_deploy_cert(*args, **kwargs) self.config.deploy_cert = mocked_deploy_cert return self.config - def tearDown(self): - shutil.rmtree(self.temp_dir) - shutil.rmtree(self.config_dir) - shutil.rmtree(self.work_dir) - @mock.patch("certbot_apache.configurator.ApacheConfigurator.init_augeas") @mock.patch("certbot_apache.configurator.path_surgery") def test_prepare_no_install(self, mock_surgery, _init_augeas): @@ -130,6 +115,10 @@ class MultipleVhostsTest(util.ApacheTest): # Weak test.. ApacheConfigurator.add_parser_arguments(mock.MagicMock()) + def test_constant(self): + self.assertEqual(self.config.constant("server_root"), "/etc/apache2") + self.assertEqual(self.config.constant("nonexistent"), None) + @certbot_util.patch_get_utility() def test_get_all_names(self, mock_getutility): mock_utility = mock_getutility() @@ -163,13 +152,12 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue("certbot.demo" in names) def test_get_bad_path(self): - from certbot_apache.configurator import get_file_path - self.assertEqual(get_file_path(None), None) - self.assertEqual(get_file_path("nonexistent"), None) + self.assertEqual(apache_util.get_file_path(None), None) + self.assertEqual(apache_util.get_file_path("nonexistent"), None) self.assertEqual(self.config._create_vhost("nonexistent"), None) # pylint: disable=protected-access def test_get_aug_internal_path(self): - from certbot_apache.configurator import get_internal_aug_path + from certbot_apache.apache_util import get_internal_aug_path internal_paths = [ "Virtualhost", "IfModule/VirtualHost", "VirtualHost", "VirtualHost", "Macro/VirtualHost", "IfModule/VirtualHost", "VirtualHost", @@ -319,190 +307,23 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.assertEqual(len(self.config._non_default_vhosts()), 8) - @mock.patch("certbot.util.run_script") - @mock.patch("certbot.util.exe_exists") - @mock.patch("certbot_apache.parser.subprocess.Popen") - def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script): - mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "") - mock_popen().returncode = 0 - mock_exe_exists.return_value = True - - self.config.enable_mod("ssl") - self.assertTrue("ssl_module" in self.config.parser.modules) - self.assertTrue("mod_ssl.c" in self.config.parser.modules) - - self.assertTrue(mock_run_script.called) - - def test_enable_mod_unsupported_dirs(self): - shutil.rmtree(os.path.join(self.config.parser.root, "mods-enabled")) - self.assertRaises( - errors.NotSupportedError, self.config.enable_mod, "ssl") - - @mock.patch("certbot.util.exe_exists") - def test_enable_mod_no_disable(self, mock_exe_exists): - mock_exe_exists.return_value = False - self.assertRaises( - errors.MisconfigurationError, self.config.enable_mod, "ssl") - - def test_enable_site_already_enabled(self): - self.assertTrue(self.vh_truth[1].enabled) - self.config.enable_site(self.vh_truth[1]) - - def test_enable_site_failure(self): - self.config.parser.root = "/tmp/nonexistent" - self.assertRaises( - errors.NotSupportedError, - self.config.enable_site, - obj.VirtualHost("asdf", "afsaf", set(), False, False)) - - def test_enable_site_nondebian(self): - mock_c = "certbot_apache.configurator.ApacheConfigurator.conf" - def conf_side_effect(arg): - """ Mock function for ApacheConfigurator.conf """ - confvars = {"handle-sites": False} - if arg in confvars: - return confvars[arg] - inc_path = "/path/to/wherever" - vhost = self.vh_truth[0] - with mock.patch(mock_c) as mock_conf: - mock_conf.side_effect = conf_side_effect - vhost.enabled = False - vhost.filep = inc_path - self.assertFalse(self.config.parser.find_dir("Include", inc_path)) - self.assertFalse( - os.path.dirname(inc_path) in self.config.parser.existing_paths) - self.config.enable_site(vhost) - self.assertTrue(self.config.parser.find_dir("Include", inc_path)) - self.assertTrue( - os.path.dirname(inc_path) in self.config.parser.existing_paths) - self.assertTrue( - os.path.basename(inc_path) in self.config.parser.existing_paths[ - os.path.dirname(inc_path)]) - def test_deploy_cert_enable_new_vhost(self): # Create ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) self.config.parser.modules.add("ssl_module") self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") + self.assertFalse(ssl_vhost.enabled) self.config.deploy_cert( "encryption-example.demo", "example/cert.pem", "example/key.pem", "example/cert_chain.pem", "example/fullchain.pem") self.assertTrue(ssl_vhost.enabled) - # Make sure that we don't error out if symlink already exists - ssl_vhost.enabled = False - self.assertFalse(ssl_vhost.enabled) - self.config.deploy_cert( - "encryption-example.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.assertTrue(ssl_vhost.enabled) - - def test_deploy_cert_newssl(self): - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, - self.work_dir, version=(2, 4, 16)) - - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") - - # Get the default 443 vhost - self.config.assoc["random.demo"] = self.vh_truth[1] - self.config = self.mock_deploy_cert(self.config) - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - - # Verify ssl_module was enabled. - self.assertTrue(self.vh_truth[1].enabled) - self.assertTrue("ssl_module" in self.config.parser.modules) - - loc_cert = self.config.parser.find_dir( - "sslcertificatefile", "example/fullchain.pem", - self.vh_truth[1].path) - loc_key = self.config.parser.find_dir( - "sslcertificateKeyfile", "example/key.pem", self.vh_truth[1].path) - - # Verify one directive was found in the correct file - self.assertEqual(len(loc_cert), 1) - self.assertEqual( - configurator.get_file_path(loc_cert[0]), - self.vh_truth[1].filep) - - self.assertEqual(len(loc_key), 1) - self.assertEqual( - configurator.get_file_path(loc_key[0]), - self.vh_truth[1].filep) - - def test_deploy_cert_newssl_no_fullchain(self): - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, - self.work_dir, version=(2, 4, 16)) - self.config = self.mock_deploy_cert(self.config) - - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") - - # Get the default 443 vhost - self.config.assoc["random.demo"] = self.vh_truth[1] - self.assertRaises(errors.PluginError, - lambda: self.config.deploy_cert( - "random.demo", "example/cert.pem", - "example/key.pem")) - - def test_deploy_cert_old_apache_no_chain(self): - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, - self.work_dir, version=(2, 4, 7)) - self.config = self.mock_deploy_cert(self.config) - - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") - - # Get the default 443 vhost - self.config.assoc["random.demo"] = self.vh_truth[1] - self.assertRaises(errors.PluginError, - lambda: self.config.deploy_cert( - "random.demo", "example/cert.pem", - "example/key.pem")) - - def test_deploy_cert_not_parsed_path(self): - # Make sure that we add include to root config for vhosts when - # handle-sites is false - self.config.parser.modules.add("ssl_module") - self.config.parser.modules.add("mod_ssl.c") - tmp_path = os.path.realpath(tempfile.mkdtemp("vhostroot")) - os.chmod(tmp_path, 0o755) - mock_p = "certbot_apache.configurator.ApacheConfigurator._get_ssl_vhost_path" - mock_a = "certbot_apache.parser.ApacheParser.add_include" - mock_c = "certbot_apache.configurator.ApacheConfigurator.conf" - orig_conf = self.config.conf - def conf_side_effect(arg): - """ Mock function for ApacheConfigurator.conf """ - confvars = {"handle-sites": False} - if arg in confvars: - return confvars[arg] - else: - return orig_conf("arg") - - with mock.patch(mock_c) as mock_conf: - mock_conf.side_effect = conf_side_effect - with mock.patch(mock_p) as mock_path: - mock_path.return_value = os.path.join(tmp_path, "whatever.conf") - with mock.patch(mock_a) as mock_add: - self.config.deploy_cert( - "encryption-example.demo", - "example/cert.pem", "example/key.pem", - "example/cert_chain.pem") - # Test that we actually called add_include - self.assertTrue(mock_add.called) - shutil.rmtree(tmp_path) - def test_deploy_cert(self): self.config.parser.modules.add("ssl_module") self.config.parser.modules.add("mod_ssl.c") - + self.config.parser.modules.add("socache_shmcb_module") # Patch _add_dummy_ssl_directives to make sure we write them correctly # pylint: disable=protected-access orig_add_dummy = self.config._add_dummy_ssl_directives @@ -531,7 +352,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue( "insert_key_file_path" in find_args(vhostpath, "SSLCertificateKeyFile")) - # pylint: disable=protected-access self.config._add_dummy_ssl_directives = mock_add_dummy_ssl @@ -557,17 +377,17 @@ class MultipleVhostsTest(util.ApacheTest): # Verify one directive was found in the correct file self.assertEqual(len(loc_cert), 1) self.assertEqual( - configurator.get_file_path(loc_cert[0]), + apache_util.get_file_path(loc_cert[0]), self.vh_truth[1].filep) self.assertEqual(len(loc_key), 1) self.assertEqual( - configurator.get_file_path(loc_key[0]), + apache_util.get_file_path(loc_key[0]), self.vh_truth[1].filep) self.assertEqual(len(loc_chain), 1) self.assertEqual( - configurator.get_file_path(loc_chain[0]), + apache_util.get_file_path(loc_chain[0]), self.vh_truth[1].filep) # One more time for chain directive setting @@ -877,7 +697,9 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(mock_restart.call_count, 1) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") - def test_cleanup(self, mock_restart): + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_cleanup(self, mock_cfg, mock_restart): + mock_cfg.return_value = "" _, achall1, achall2 = self.get_achalls() self.config._chall_out.add(achall1) # pylint: disable=protected-access @@ -890,7 +712,9 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue(mock_restart.called) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") - def test_cleanup_no_errors(self, mock_restart): + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_cleanup_no_errors(self, mock_cfg, mock_restart): + mock_cfg.return_value = "" _, achall1, achall2 = self.get_achalls() self.config._chall_out.add(achall1) # pylint: disable=protected-access @@ -951,10 +775,9 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue(isinstance(self.config.get_chall_pref(""), list)) def test_install_ssl_options_conf(self): - from certbot_apache.configurator import install_ssl_options_conf path = os.path.join(self.work_dir, "test_it") other_path = os.path.join(self.work_dir, "other_test_it") - install_ssl_options_conf(path, other_path) + self.config.install_ssl_options_conf(path, other_path) self.assertTrue(os.path.isfile(path)) self.assertTrue(os.path.isfile(other_path)) @@ -994,20 +817,17 @@ class MultipleVhostsTest(util.ApacheTest): errors.PluginError, self.config.enhance, "certbot.demo", "unknown_enhancement") - @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") - def test_ocsp_stapling(self, mock_exe, mock_run_script): + def test_ocsp_stapling(self, mock_exe): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") self.config.get_version = mock.Mock(return_value=(2, 4, 7)) mock_exe.return_value = True # This will create an ssl vhost for certbot.demo self.config.enhance("certbot.demo", "staple-ocsp") - self.assertTrue("socache_shmcb_module" in self.config.parser.modules) - self.assertTrue(mock_run_script.called) - # Get the ssl vhost for certbot.demo ssl_vhost = self.config.assoc["certbot.demo"] @@ -1077,14 +897,13 @@ class MultipleVhostsTest(util.ApacheTest): def test_http_header_hsts(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("headers_module") mock_exe.return_value = True # This will create an ssl vhost for certbot.demo self.config.enhance("certbot.demo", "ensure-http-header", "Strict-Transport-Security") - self.assertTrue("headers_module" in self.config.parser.modules) - # Get the ssl vhost for certbot.demo ssl_vhost = self.config.assoc["certbot.demo"] @@ -1115,6 +934,8 @@ class MultipleVhostsTest(util.ApacheTest): def test_http_header_uir(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("headers_module") + mock_exe.return_value = True # This will create an ssl vhost for certbot.demo @@ -1151,6 +972,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") def test_redirect_well_formed_http(self, mock_exe, _): + self.config.parser.modules.add("rewrite_module") self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True self.config.get_version = mock.Mock(return_value=(2, 2)) @@ -1173,8 +995,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path[:-3])) self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path[:-3])) - self.assertTrue("rewrite_module" in self.config.parser.modules) - def test_rewrite_rule_exists(self): # Skip the enable mod self.config.parser.modules.add("rewrite_module") @@ -1196,6 +1016,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") def test_redirect_with_existing_rewrite(self, mock_exe, _): + self.config.parser.modules.add("rewrite_module") self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True self.config.get_version = mock.Mock(return_value=(2, 2, 0)) @@ -1228,6 +1049,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot.util.run_script") @mock.patch("certbot.util.exe_exists") def test_redirect_with_old_https_redirection(self, mock_exe, _): + self.config.parser.modules.add("rewrite_module") self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True self.config.get_version = mock.Mock(return_value=(2, 2, 0)) @@ -1365,6 +1187,57 @@ class MultipleVhostsTest(util.ApacheTest): self.config.aug.match.side_effect = RuntimeError self.assertFalse(self.config._check_aug_version()) + def test_enable_site_nondebian(self): + inc_path = "/path/to/wherever" + vhost = self.vh_truth[0] + vhost.enabled = False + vhost.filep = inc_path + self.assertFalse(self.config.parser.find_dir("Include", inc_path)) + self.assertFalse( + os.path.dirname(inc_path) in self.config.parser.existing_paths) + self.config.enable_site(vhost) + self.assertTrue(self.config.parser.find_dir("Include", inc_path)) + self.assertTrue( + os.path.dirname(inc_path) in self.config.parser.existing_paths) + self.assertTrue( + os.path.basename(inc_path) in self.config.parser.existing_paths[ + os.path.dirname(inc_path)]) + + def test_deploy_cert_not_parsed_path(self): + # Make sure that we add include to root config for vhosts when + # handle-sites is false + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") + tmp_path = os.path.realpath(tempfile.mkdtemp("vhostroot")) + os.chmod(tmp_path, 0o755) + mock_p = "certbot_apache.configurator.ApacheConfigurator._get_ssl_vhost_path" + mock_a = "certbot_apache.parser.ApacheParser.add_include" + + with mock.patch(mock_p) as mock_path: + mock_path.return_value = os.path.join(tmp_path, "whatever.conf") + with mock.patch(mock_a) as mock_add: + self.config.deploy_cert( + "encryption-example.demo", + "example/cert.pem", "example/key.pem", + "example/cert_chain.pem") + # Test that we actually called add_include + self.assertTrue(mock_add.called) + shutil.rmtree(tmp_path) + + @mock.patch("certbot_apache.parser.ApacheParser.parsed_in_original") + def test_choose_vhost_and_servername_addition_parsed(self, mock_parsed): + ret_vh = self.vh_truth[8] + ret_vh.enabled = True + self.config.enable_site(ret_vh) + # Make sure that we return early + self.assertFalse(mock_parsed.called) + + def test_enable_mod_unsupported(self): + self.assertRaises(errors.MisconfigurationError, + self.config.enable_mod, + "whatever") + class AugeasVhostsTest(util.ApacheTest): """Test vhosts with illegal names dependent on augeas version.""" # pylint: disable=protected-access @@ -1378,12 +1251,8 @@ class AugeasVhostsTest(util.ApacheTest): vhost_root=vr) self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir) - - def tearDown(self): - shutil.rmtree(self.temp_dir) - shutil.rmtree(self.config_dir) - shutil.rmtree(self.work_dir) + self.config_path, self.vhost_path, self.config_dir, + self.work_dir) def test_choosevhost_with_illegal_name(self): self.config.aug = mock.MagicMock() @@ -1461,15 +1330,11 @@ class MultiVhostsTest(util.ApacheTest): vhost_root=vr) self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir) + self.config_path, self.vhost_path, + self.config_dir, self.work_dir, conf_vhost_path=self.vhost_path) self.vh_truth = util.get_vh_truth( self.temp_dir, "debian_apache_2_4/multi_vhosts") - def tearDown(self): - shutil.rmtree(self.temp_dir) - shutil.rmtree(self.config_dir) - shutil.rmtree(self.work_dir) - def test_make_vhost_ssl(self): ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1]) @@ -1569,11 +1434,11 @@ class InstallSslOptionsConfTest(util.ApacheTest): self.config_path, self.vhost_path, self.config_dir, self.work_dir) def _call(self): - from certbot_apache.configurator import install_ssl_options_conf - install_ssl_options_conf(self.config.mod_ssl_conf, self.config.updated_mod_ssl_conf_digest) + self.config.install_ssl_options_conf(self.config.mod_ssl_conf, + self.config.updated_mod_ssl_conf_digest) def _current_ssl_options_hash(self): - return crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")) + return crypto_util.sha256sum(self.config.constant("MOD_SSL_CONF_SRC")) def _assert_current_file(self): self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) @@ -1608,7 +1473,8 @@ class InstallSslOptionsConfTest(util.ApacheTest): self._call() self.assertFalse(mock_logger.warning.called) self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) - self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self.assertEqual(crypto_util.sha256sum( + self.config.constant("MOD_SSL_CONF_SRC")), self._current_ssl_options_hash()) self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf), self._current_ssl_options_hash()) @@ -1623,7 +1489,8 @@ class InstallSslOptionsConfTest(util.ApacheTest): self.assertEqual(mock_logger.warning.call_args[0][0], "%s has been manually modified; updated file " "saved to %s. We recommend updating %s for security purposes.") - self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self.assertEqual(crypto_util.sha256sum( + self.config.constant("MOD_SSL_CONF_SRC")), self._current_ssl_options_hash()) # only print warning once with mock.patch("certbot.plugins.common.logger") as mock_logger: diff --git a/certbot-apache/certbot_apache/tests/constants_test.py b/certbot-apache/certbot_apache/tests/constants_test.py deleted file mode 100644 index 5ab324101..000000000 --- a/certbot-apache/certbot_apache/tests/constants_test.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Test for certbot_apache.configurator.""" - -import mock -import unittest - -from certbot_apache import constants - - -class ConstantsTest(unittest.TestCase): - - @mock.patch("certbot.util.get_os_info") - def test_get_debian_value(self, os_info): - os_info.return_value = ('Debian', '', '') - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/apache2/sites-available") - - @mock.patch("certbot.util.get_os_info") - def test_get_centos_value(self, os_info): - os_info.return_value = ('CentOS Linux', '', '') - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/httpd/conf.d") - - @mock.patch("certbot.util.get_systemd_os_like") - @mock.patch("certbot.util.get_os_info") - def test_get_default_values(self, os_info, os_like): - os_info.return_value = ('Nonexistent Linux', '', '') - os_like.return_value = {} - self.assertFalse(constants.os_constant("handle_mods")) - self.assertEqual(constants.os_constant("server_root"), "/etc/apache2") - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/apache2/sites-available") - - @mock.patch("certbot.util.get_systemd_os_like") - @mock.patch("certbot.util.get_os_info") - def test_get_darwin_like_values(self, os_info, os_like): - os_info.return_value = ('Nonexistent Linux', '', '') - os_like.return_value = ["something", "nonexistent", "darwin"] - self.assertFalse(constants.os_constant("enmod")) - self.assertEqual(constants.os_constant("vhost_root"), - "/etc/apache2/other") - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/debian_test.py b/certbot-apache/certbot_apache/tests/debian_test.py new file mode 100644 index 000000000..a648101e9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/debian_test.py @@ -0,0 +1,209 @@ +"""Test for certbot_apache.configurator for Debian overrides""" +import os +import shutil +import unittest + +import mock + +from certbot import errors + +from certbot_apache import apache_util +from certbot_apache import obj +from certbot_apache.tests import util + + +class MultipleVhostsTestDebian(util.ApacheTest): + """Multiple vhost tests for Debian family of distros""" + + _multiprocess_can_split_ = True + + def setUp(self): # pylint: disable=arguments-differ + super(MultipleVhostsTestDebian, self).setUp() + self.config = util.get_apache_configurator( + self.config_path, None, self.config_dir, self.work_dir, + os_info="debian") + self.config = self.mock_deploy_cert(self.config) + self.vh_truth = util.get_vh_truth(self.temp_dir, + "debian_apache_2_4/multiple_vhosts") + + def mock_deploy_cert(self, config): + """A test for a mock deploy cert""" + config.real_deploy_cert = self.config.deploy_cert + + def mocked_deploy_cert(*args, **kwargs): + """a helper to mock a deployed cert""" + g_mod = "certbot_apache.configurator.ApacheConfigurator.enable_mod" + d_mod = "certbot_apache.override_debian.DebianConfigurator.enable_mod" + with mock.patch(g_mod): + with mock.patch(d_mod): + config.real_deploy_cert(*args, **kwargs) + self.config.deploy_cert = mocked_deploy_cert + return self.config + + def test_enable_mod_unsupported_dirs(self): + shutil.rmtree(os.path.join(self.config.parser.root, "mods-enabled")) + self.assertRaises( + errors.NotSupportedError, self.config.enable_mod, "ssl") + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + @mock.patch("certbot_apache.parser.subprocess.Popen") + def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script): + mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "") + mock_popen().returncode = 0 + mock_exe_exists.return_value = True + + self.config.enable_mod("ssl") + self.assertTrue("ssl_module" in self.config.parser.modules) + self.assertTrue("mod_ssl.c" in self.config.parser.modules) + + self.assertTrue(mock_run_script.called) + + def test_deploy_cert_enable_new_vhost(self): + # Create + ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + self.assertFalse(ssl_vhost.enabled) + self.config.deploy_cert( + "encryption-example.demo", "example/cert.pem", "example/key.pem", + "example/cert_chain.pem", "example/fullchain.pem") + self.assertTrue(ssl_vhost.enabled) + # Make sure that we don't error out if symlink already exists + ssl_vhost.enabled = False + self.assertFalse(ssl_vhost.enabled) + self.config.deploy_cert( + "encryption-example.demo", "example/cert.pem", "example/key.pem", + "example/cert_chain.pem", "example/fullchain.pem") + self.assertTrue(ssl_vhost.enabled) + + def test_enable_site_failure(self): + self.config.parser.root = "/tmp/nonexistent" + with mock.patch("os.path.isdir") as mock_dir: + mock_dir.return_value = True + with mock.patch("os.path.islink") as mock_link: + mock_link.return_value = False + self.assertRaises( + errors.NotSupportedError, + self.config.enable_site, + obj.VirtualHost("asdf", "afsaf", set(), False, False)) + + def test_deploy_cert_newssl(self): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, + self.work_dir, version=(2, 4, 16)) + self.config = self.mock_deploy_cert(self.config) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + + # Get the default 443 vhost + self.config.assoc["random.demo"] = self.vh_truth[1] + self.config.deploy_cert( + "random.demo", "example/cert.pem", "example/key.pem", + "example/cert_chain.pem", "example/fullchain.pem") + self.config.save() + + # Verify ssl_module was enabled. + self.assertTrue(self.vh_truth[1].enabled) + self.assertTrue("ssl_module" in self.config.parser.modules) + + loc_cert = self.config.parser.find_dir( + "sslcertificatefile", "example/fullchain.pem", + self.vh_truth[1].path) + loc_key = self.config.parser.find_dir( + "sslcertificateKeyfile", "example/key.pem", self.vh_truth[1].path) + + # Verify one directive was found in the correct file + self.assertEqual(len(loc_cert), 1) + self.assertEqual( + apache_util.get_file_path(loc_cert[0]), + self.vh_truth[1].filep) + + self.assertEqual(len(loc_key), 1) + self.assertEqual( + apache_util.get_file_path(loc_key[0]), + self.vh_truth[1].filep) + + def test_deploy_cert_newssl_no_fullchain(self): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, + self.work_dir, version=(2, 4, 16)) + self.config = self.mock_deploy_cert(self.config) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + + # Get the default 443 vhost + self.config.assoc["random.demo"] = self.vh_truth[1] + self.assertRaises(errors.PluginError, + lambda: self.config.deploy_cert( + "random.demo", "example/cert.pem", + "example/key.pem")) + + def test_deploy_cert_old_apache_no_chain(self): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, + self.work_dir, version=(2, 4, 7)) + self.config = self.mock_deploy_cert(self.config) + self.config.parser.modules.add("ssl_module") + self.config.parser.modules.add("mod_ssl.c") + + # Get the default 443 vhost + self.config.assoc["random.demo"] = self.vh_truth[1] + self.assertRaises(errors.PluginError, + lambda: self.config.deploy_cert( + "random.demo", "example/cert.pem", + "example/key.pem")) + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + def test_ocsp_stapling_enable_mod(self, mock_exe, _): + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + self.config.get_version = mock.Mock(return_value=(2, 4, 7)) + mock_exe.return_value = True + self.config.enhance("certbot.demo", "staple-ocsp") + self.assertTrue("socache_shmcb_module" in self.config.parser.modules) + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + def test_ensure_http_header_enable_mod(self, mock_exe, _): + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + mock_exe.return_value = True + + # This will create an ssl vhost for certbot.demo + self.config.enhance("certbot.demo", "ensure-http-header", + "Strict-Transport-Security") + self.assertTrue("headers_module" in self.config.parser.modules) + + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") + def test_redirect_enable_mod(self, mock_exe, _): + self.config.parser.update_runtime_variables = mock.Mock() + mock_exe.return_value = True + self.config.get_version = mock.Mock(return_value=(2, 2)) + # This will create an ssl vhost for certbot.demo + self.config.enhance("certbot.demo", "redirect") + self.assertTrue("rewrite_module" in self.config.parser.modules) + + def test_enable_site_already_enabled(self): + self.assertTrue(self.vh_truth[1].enabled) + self.config.enable_site(self.vh_truth[1]) + + def test_enable_site_call_parent(self): + with mock.patch( + "certbot_apache.configurator.ApacheConfigurator.enable_site") as e_s: + self.config.parser.root = "/tmp/nonexistent" + vh = self.vh_truth[0] + vh.enabled = False + self.config.enable_site(vh) + self.assertTrue(e_s.called) + + @mock.patch("certbot.util.exe_exists") + def test_enable_mod_no_disable(self, mock_exe_exists): + mock_exe_exists.return_value = False + self.assertRaises( + errors.MisconfigurationError, self.config.enable_mod, "ssl") + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/entrypoint_test.py b/certbot-apache/certbot_apache/tests/entrypoint_test.py new file mode 100644 index 000000000..c04611465 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/entrypoint_test.py @@ -0,0 +1,41 @@ +"""Test for certbot_apache.entrypoint for override class resolution""" +import unittest + +import mock + +from certbot_apache import configurator +from certbot_apache import entrypoint + +class EntryPointTest(unittest.TestCase): + """Entrypoint tests""" + + _multiprocess_can_split_ = True + + def test_get_configurator(self): + + with mock.patch("certbot.util.get_os_info") as mock_info: + for distro in entrypoint.OVERRIDE_CLASSES.keys(): + mock_info.return_value = (distro, "whatever") + self.assertEqual(entrypoint.get_configurator(), + entrypoint.OVERRIDE_CLASSES[distro]) + + def test_nonexistent_like(self): + with mock.patch("certbot.util.get_os_info") as mock_info: + mock_info.return_value = ("nonexistent", "irrelevant") + with mock.patch("certbot.util.get_systemd_os_like") as mock_like: + for like in entrypoint.OVERRIDE_CLASSES.keys(): + mock_like.return_value = [like] + self.assertEqual(entrypoint.get_configurator(), + entrypoint.OVERRIDE_CLASSES[like]) + + def test_nonexistent_generic(self): + with mock.patch("certbot.util.get_os_info") as mock_info: + mock_info.return_value = ("nonexistent", "irrelevant") + with mock.patch("certbot.util.get_systemd_os_like") as mock_like: + mock_like.return_value = ["unknonwn"] + self.assertEqual(entrypoint.get_configurator(), + configurator.ApacheConfigurator) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/gentoo_test.py b/certbot-apache/certbot_apache/tests/gentoo_test.py new file mode 100644 index 000000000..0f2b96818 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/gentoo_test.py @@ -0,0 +1,86 @@ +"""Test for certbot_apache.configurator for Gentoo overrides""" +import os +import unittest + +from certbot_apache import override_gentoo +from certbot_apache import obj +from certbot_apache.tests import util + +def get_vh_truth(temp_dir, config_name): + """Return the ground truth for the specified directory.""" + prefix = os.path.join( + temp_dir, config_name, "apache2/vhosts.d") + + aug_pre = "/files" + prefix + vh_truth = [ + obj.VirtualHost( + os.path.join(prefix, "gentoo.example.com.conf"), + os.path.join(aug_pre, "gentoo.example.com.conf/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), + False, True, "gentoo.example.com"), + obj.VirtualHost( + os.path.join(prefix, "00_default_vhost.conf"), + os.path.join(aug_pre, "00_default_vhost.conf/IfDefine/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), + False, True, "localhost"), + obj.VirtualHost( + os.path.join(prefix, "00_default_ssl_vhost.conf"), + os.path.join(aug_pre, + "00_default_ssl_vhost.conf" + + "/IfDefine/IfDefine/IfModule/VirtualHost"), + set([obj.Addr.fromstring("_default_:443")]), + True, True, "localhost") + ] + return vh_truth + +class MultipleVhostsTestGentoo(util.ApacheTest): + """Multiple vhost tests for non-debian distro""" + + _multiprocess_can_split_ = True + + def setUp(self): # pylint: disable=arguments-differ + test_dir = "gentoo_apache/apache" + config_root = "gentoo_apache/apache/apache2" + vhost_root = "gentoo_apache/apache/apache2/vhosts.d" + super(MultipleVhostsTestGentoo, self).setUp(test_dir=test_dir, + config_root=config_root, + vhost_root=vhost_root) + + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir, + os_info="gentoo") + self.vh_truth = get_vh_truth( + self.temp_dir, "gentoo_apache/apache") + + def test_get_parser(self): + self.assertTrue(isinstance(self.config.parser, + override_gentoo.GentooParser)) + + def test_get_virtual_hosts(self): + """Make sure all vhosts are being properly found.""" + vhs = self.config.get_virtual_hosts() + self.assertEqual(len(vhs), 3) + found = 0 + + for vhost in vhs: + for gentoo_truth in self.vh_truth: + if vhost == gentoo_truth: + found += 1 + break + else: + raise Exception("Missed: %s" % vhost) # pragma: no cover + self.assertEqual(found, 3) + + def test_get_sysconfig_vars(self): + """Make sure we read the Gentoo APACHE2_OPTS variable correctly""" + defines = ['DEFAULT_VHOST', 'INFO', + 'SSL', 'SSL_DEFAULT_VHOST', 'LANGUAGE'] + self.config.parser.apacheconfig_filep = os.path.realpath( + os.path.join(self.config.parser.root, "../conf.d/apache2")) + self.config.parser.variables = {} + self.config.parser.update_runtime_variables() + for define in defines: + self.assertTrue(define in self.config.parser.variables.keys()) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/parser_test.py b/certbot-apache/certbot_apache/tests/parser_test.py index 38c5b29c7..a9eb129c2 100644 --- a/certbot-apache/certbot_apache/tests/parser_test.py +++ b/certbot-apache/certbot_apache/tests/parser_test.py @@ -120,17 +120,18 @@ class BasicParserTest(util.ParserTest): @mock.patch("certbot_apache.parser.ApacheParser.find_dir") @mock.patch("certbot_apache.parser.ApacheParser.get_arg") - def test_init_modules_bad_syntax(self, mock_arg, mock_find): + def test_parse_modules_bad_syntax(self, mock_arg, mock_find): mock_find.return_value = ["1", "2", "3", "4", "5", "6", "7", "8"] mock_arg.return_value = None with mock.patch("certbot_apache.parser.logger") as mock_logger: - self.parser.init_modules() + self.parser.parse_modules() # Make sure that we got None return value and logged the file self.assertTrue(mock_logger.debug.called) + @mock.patch("certbot_apache.parser.ApacheParser.find_dir") @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") - def test_update_runtime_variables(self, mock_cfg): - mock_cfg.return_value = ( + def test_update_runtime_variables(self, mock_cfg, _): + define_val = ( 'ServerRoot: "/etc/apache2"\n' 'Main DocumentRoot: "/var/www"\n' 'Main ErrorLog: "/var/log/apache2/error.log"\n' @@ -147,11 +148,113 @@ class BasicParserTest(util.ParserTest): 'User: name="www-data" id=33 not_used\n' 'Group: name="www-data" id=33 not_used\n' ) + inc_val = ( + 'Included configuration files:\n' + ' (*) /etc/apache2/apache2.conf\n' + ' (146) /etc/apache2/mods-enabled/access_compat.load\n' + ' (146) /etc/apache2/mods-enabled/alias.load\n' + ' (146) /etc/apache2/mods-enabled/auth_basic.load\n' + ' (146) /etc/apache2/mods-enabled/authn_core.load\n' + ' (146) /etc/apache2/mods-enabled/authn_file.load\n' + ' (146) /etc/apache2/mods-enabled/authz_core.load\n' + ' (146) /etc/apache2/mods-enabled/authz_host.load\n' + ' (146) /etc/apache2/mods-enabled/authz_user.load\n' + ' (146) /etc/apache2/mods-enabled/autoindex.load\n' + ' (146) /etc/apache2/mods-enabled/deflate.load\n' + ' (146) /etc/apache2/mods-enabled/dir.load\n' + ' (146) /etc/apache2/mods-enabled/env.load\n' + ' (146) /etc/apache2/mods-enabled/filter.load\n' + ' (146) /etc/apache2/mods-enabled/mime.load\n' + ' (146) /etc/apache2/mods-enabled/mpm_event.load\n' + ' (146) /etc/apache2/mods-enabled/negotiation.load\n' + ' (146) /etc/apache2/mods-enabled/reqtimeout.load\n' + ' (146) /etc/apache2/mods-enabled/setenvif.load\n' + ' (146) /etc/apache2/mods-enabled/socache_shmcb.load\n' + ' (146) /etc/apache2/mods-enabled/ssl.load\n' + ' (146) /etc/apache2/mods-enabled/status.load\n' + ' (147) /etc/apache2/mods-enabled/alias.conf\n' + ' (147) /etc/apache2/mods-enabled/autoindex.conf\n' + ' (147) /etc/apache2/mods-enabled/deflate.conf\n' + ) + mod_val = ( + 'Loaded Modules:\n' + ' core_module (static)\n' + ' so_module (static)\n' + ' watchdog_module (static)\n' + ' http_module (static)\n' + ' log_config_module (static)\n' + ' logio_module (static)\n' + ' version_module (static)\n' + ' unixd_module (static)\n' + ' access_compat_module (shared)\n' + ' alias_module (shared)\n' + ' auth_basic_module (shared)\n' + ' authn_core_module (shared)\n' + ' authn_file_module (shared)\n' + ' authz_core_module (shared)\n' + ' authz_host_module (shared)\n' + ' authz_user_module (shared)\n' + ' autoindex_module (shared)\n' + ' deflate_module (shared)\n' + ' dir_module (shared)\n' + ' env_module (shared)\n' + ' filter_module (shared)\n' + ' mime_module (shared)\n' + ' mpm_event_module (shared)\n' + ' negotiation_module (shared)\n' + ' reqtimeout_module (shared)\n' + ' setenvif_module (shared)\n' + ' socache_shmcb_module (shared)\n' + ' ssl_module (shared)\n' + ' status_module (shared)\n' + ) + + def mock_get_vars(cmd): + """Mock command output""" + if cmd[-1] == "DUMP_RUN_CFG": + return define_val + elif cmd[-1] == "DUMP_INCLUDES": + return inc_val + elif cmd[-1] == "DUMP_MODULES": + return mod_val + + mock_cfg.side_effect = mock_get_vars + expected_vars = {"TEST": "", "U_MICH": "", "TLS": "443", "example_path": "Documents/path"} - self.parser.update_runtime_variables() - self.assertEqual(self.parser.variables, expected_vars) + self.parser.modules = set() + with mock.patch( + "certbot_apache.parser.ApacheParser.parse_file") as mock_parse: + self.parser.update_runtime_variables() + self.assertEqual(self.parser.variables, expected_vars) + self.assertEqual(len(self.parser.modules), 58) + # None of the includes in inc_val should be in parsed paths. + # Make sure we tried to include them all. + self.assertEqual(mock_parse.call_count, 25) + + @mock.patch("certbot_apache.parser.ApacheParser.find_dir") + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_update_runtime_variables_alt_values(self, mock_cfg, _): + inc_val = ( + 'Included configuration files:\n' + ' (*) {0}\n' + ' (146) /etc/apache2/mods-enabled/access_compat.load\n' + ' (146) {1}/mods-enabled/alias.load\n' + ).format(self.parser.loc["root"], + os.path.dirname(self.parser.loc["root"])) + + mock_cfg.return_value = inc_val + self.parser.modules = set() + + with mock.patch( + "certbot_apache.parser.ApacheParser.parse_file") as mock_parse: + self.parser.update_runtime_variables() + # No matching modules should have been found + self.assertEqual(len(self.parser.modules), 0) + # Only one of the three includes do not exist in already parsed + # path derived from root configuration Include statements + self.assertEqual(mock_parse.call_count, 1) @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") def test_update_runtime_vars_bad_output(self, mock_cfg): @@ -162,7 +265,7 @@ class BasicParserTest(util.ParserTest): self.assertRaises( errors.PluginError, self.parser.update_runtime_variables) - @mock.patch("certbot_apache.constants.os_constant") + @mock.patch("certbot_apache.configurator.ApacheConfigurator.constant") @mock.patch("certbot_apache.parser.subprocess.Popen") def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_const): mock_popen.side_effect = OSError @@ -198,7 +301,7 @@ class ParserInitTest(util.ApacheTest): self.assertRaises( errors.PluginError, ApacheParser, self.aug, os.path.relpath(self.config_path), - "/dummy/vhostpath", version=(2, 2, 22)) + "/dummy/vhostpath", version=(2, 2, 22), configurator=self.config) def test_root_normalized(self): from certbot_apache.parser import ApacheParser @@ -210,7 +313,7 @@ class ParserInitTest(util.ApacheTest): "debian_apache_2_4/////multiple_vhosts/../multiple_vhosts/apache2") parser = ApacheParser(self.aug, path, - "/dummy/vhostpath") + "/dummy/vhostpath", configurator=self.config) self.assertEqual(parser.root, self.config_path) @@ -220,7 +323,7 @@ class ParserInitTest(util.ApacheTest): "update_runtime_variables"): parser = ApacheParser( self.aug, os.path.relpath(self.config_path), - "/dummy/vhostpath") + "/dummy/vhostpath", configurator=self.config) self.assertEqual(parser.root, self.config_path) @@ -230,7 +333,7 @@ class ParserInitTest(util.ApacheTest): "update_runtime_variables"): parser = ApacheParser( self.aug, self.config_path + os.path.sep, - "/dummy/vhostpath") + "/dummy/vhostpath", configurator=self.config) self.assertEqual(parser.root, self.config_path) diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/README b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/README new file mode 100644 index 000000000..f5e96615a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/README @@ -0,0 +1,9 @@ + +This directory holds configuration files for the Apache HTTP Server; +any files in this directory which have the ".conf" extension will be +processed as httpd configuration files. The directory is used in +addition to the directory /etc/httpd/conf.modules.d/, which contains +configuration files necessary to load modules. + +Files are processed in alphabetical order. + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf new file mode 100644 index 000000000..a85cf5dca --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf @@ -0,0 +1,94 @@ +# +# Directives controlling the display of server-generated directory listings. +# +# Required modules: mod_authz_core, mod_authz_host, +# mod_autoindex, mod_alias +# +# To see the listing of a directory, the Options directive for the +# directory must include "Indexes", and the directory must not contain +# a file matching those listed in the DirectoryIndex directive. +# + +# +# IndexOptions: Controls the appearance of server-generated directory +# listings. +# +IndexOptions FancyIndexing HTMLTable VersionSort + +# We include the /icons/ alias for FancyIndexed directory listings. If +# you do not use FancyIndexing, you may comment this out. +# +Alias /icons/ "/usr/share/httpd/icons/" + + + Options Indexes MultiViews FollowSymlinks + AllowOverride None + Require all granted + + +# +# AddIcon* directives tell the server which icon to show for different +# files or filename extensions. These are only displayed for +# FancyIndexed directories. +# +AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip + +AddIconByType (TXT,/icons/text.gif) text/* +AddIconByType (IMG,/icons/image2.gif) image/* +AddIconByType (SND,/icons/sound2.gif) audio/* +AddIconByType (VID,/icons/movie.gif) video/* + +AddIcon /icons/binary.gif .bin .exe +AddIcon /icons/binhex.gif .hqx +AddIcon /icons/tar.gif .tar +AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv +AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip +AddIcon /icons/a.gif .ps .ai .eps +AddIcon /icons/layout.gif .html .shtml .htm .pdf +AddIcon /icons/text.gif .txt +AddIcon /icons/c.gif .c +AddIcon /icons/p.gif .pl .py +AddIcon /icons/f.gif .for +AddIcon /icons/dvi.gif .dvi +AddIcon /icons/uuencoded.gif .uu +AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl +AddIcon /icons/tex.gif .tex +AddIcon /icons/bomb.gif /core +AddIcon /icons/bomb.gif */core.* + +AddIcon /icons/back.gif .. +AddIcon /icons/hand.right.gif README +AddIcon /icons/folder.gif ^^DIRECTORY^^ +AddIcon /icons/blank.gif ^^BLANKICON^^ + +# +# DefaultIcon is which icon to show for files which do not have an icon +# explicitly set. +# +DefaultIcon /icons/unknown.gif + +# +# AddDescription allows you to place a short description after a file in +# server-generated indexes. These are only displayed for FancyIndexed +# directories. +# Format: AddDescription "description" filename +# +#AddDescription "GZIP compressed document" .gz +#AddDescription "tar archive" .tar +#AddDescription "GZIP compressed tar archive" .tgz + +# +# ReadmeName is the name of the README file the server will look for by +# default, and append to directory listings. +# +# HeaderName is the name of a file which should be prepended to +# directory indexes. +ReadmeName README.html +HeaderName HEADER.html + +# +# IndexIgnore is a set of filenames which directory indexing should ignore +# and not include in the listing. Shell-style wildcarding is permitted. +# +IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf new file mode 100644 index 000000000..de7ac2777 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf @@ -0,0 +1,7 @@ + + ServerName centos.example.com + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf new file mode 100644 index 000000000..6e2502e9a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf @@ -0,0 +1,211 @@ +# +# When we also provide SSL we have to listen to the +# the HTTPS port in addition. +# +Listen 443 https + +## +## SSL Global Context +## +## All SSL configuration in this context applies both to +## the main server and all SSL-enabled virtual hosts. +## + +# Pass Phrase Dialog: +# Configure the pass phrase gathering process. +# The filtering dialog program (`builtin' is a internal +# terminal dialog) has to provide the pass phrase on stdout. +SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog + +# Inter-Process Session Cache: +# Configure the SSL Session Cache: First the mechanism +# to use and second the expiring timeout (in seconds). +SSLSessionCache shmcb:/run/httpd/sslcache(512000) +SSLSessionCacheTimeout 300 + +# Pseudo Random Number Generator (PRNG): +# Configure one or more sources to seed the PRNG of the +# SSL library. The seed data should be of good random quality. +# WARNING! On some platforms /dev/random blocks if not enough entropy +# is available. This means you then cannot use the /dev/random device +# because it would lead to very long connection times (as long as +# it requires to make more entropy available). But usually those +# platforms additionally provide a /dev/urandom device which doesn't +# block. So, if available, use this one instead. Read the mod_ssl User +# Manual for more details. +SSLRandomSeed startup file:/dev/urandom 256 +SSLRandomSeed connect builtin +#SSLRandomSeed startup file:/dev/random 512 +#SSLRandomSeed connect file:/dev/random 512 +#SSLRandomSeed connect file:/dev/urandom 512 + +# +# Use "SSLCryptoDevice" to enable any supported hardware +# accelerators. Use "openssl engine -v" to list supported +# engine names. NOTE: If you enable an accelerator and the +# server does not start, consult the error logs and ensure +# your accelerator is functioning properly. +# +SSLCryptoDevice builtin +#SSLCryptoDevice ubsec + +## +## SSL Virtual Host Context +## + + + +# General setup for the virtual host, inherited from global configuration +#DocumentRoot "/var/www/html" +#ServerName www.example.com:443 + +# Use separate log files for the SSL virtual host; note that LogLevel +# is not inherited from httpd.conf. +ErrorLog logs/ssl_error_log +TransferLog logs/ssl_access_log +LogLevel warn + +# SSL Engine Switch: +# Enable/Disable SSL for this virtual host. +SSLEngine on + +# SSL Protocol support: +# List the enable protocol levels with which clients will be able to +# connect. Disable SSLv2 access by default: +SSLProtocol all -SSLv2 + +# SSL Cipher Suite: +# List the ciphers that the client is permitted to negotiate. +# See the mod_ssl documentation for a complete list. +SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!SEED:!IDEA + +# Speed-optimized SSL Cipher configuration: +# If speed is your main concern (on busy HTTPS servers e.g.), +# you might want to force clients to specific, performance +# optimized ciphers. In this case, prepend those ciphers +# to the SSLCipherSuite list, and enable SSLHonorCipherOrder. +# Caveat: by giving precedence to RC4-SHA and AES128-SHA +# (as in the example below), most connections will no longer +# have perfect forward secrecy - if the server's key is +# compromised, captures of past or future traffic must be +# considered compromised, too. +#SSLCipherSuite RC4-SHA:AES128-SHA:HIGH:MEDIUM:!aNULL:!MD5 +#SSLHonorCipherOrder on + +# Server Certificate: +# Point SSLCertificateFile at a PEM encoded certificate. If +# the certificate is encrypted, then you will be prompted for a +# pass phrase. Note that a kill -HUP will prompt again. A new +# certificate can be generated using the genkey(1) command. + +# Server Private Key: +# If the key is not combined with the certificate, use this +# directive to point at the key file. Keep in mind that if +# you've both a RSA and a DSA private key you can configure +# both in parallel (to also allow the use of DSA ciphers, etc.) + +# Server Certificate Chain: +# Point SSLCertificateChainFile at a file containing the +# concatenation of PEM encoded CA certificates which form the +# certificate chain for the server certificate. Alternatively +# the referenced file can be the same as SSLCertificateFile +# when the CA certificates are directly appended to the server +# certificate for convinience. +#SSLCertificateChainFile /etc/pki/tls/certs/server-chain.crt + +# Certificate Authority (CA): +# Set the CA certificate verification path where to find CA +# certificates for client authentication or alternatively one +# huge file containing all of them (file must be PEM encoded) +#SSLCACertificateFile /etc/pki/tls/certs/ca-bundle.crt + +# Client Authentication (Type): +# Client certificate verification type and depth. Types are +# none, optional, require and optional_no_ca. Depth is a +# number which specifies how deeply to verify the certificate +# issuer chain before deciding the certificate is not valid. +#SSLVerifyClient require +#SSLVerifyDepth 10 + +# Access Control: +# With SSLRequire you can do per-directory access control based +# on arbitrary complex boolean expressions containing server +# variable checks and other lookup directives. The syntax is a +# mixture between C and Perl. See the mod_ssl documentation +# for more details. +# +#SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ +# and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ +# and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ +# and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ +# and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ +# or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ +# + +# SSL Engine Options: +# Set various options for the SSL engine. +# o FakeBasicAuth: +# Translate the client X.509 into a Basic Authorisation. This means that +# the standard Auth/DBMAuth methods can be used for access control. The +# user name is the `one line' version of the client's X.509 certificate. +# Note that no password is obtained from the user. Every entry in the user +# file needs this password: `xxj31ZMTZzkVA'. +# o ExportCertData: +# This exports two additional environment variables: SSL_CLIENT_CERT and +# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the +# server (always existing) and the client (only existing when client +# authentication is used). This can be used to import the certificates +# into CGI scripts. +# o StdEnvVars: +# This exports the standard SSL/TLS related `SSL_*' environment variables. +# Per default this exportation is switched off for performance reasons, +# because the extraction step is an expensive operation and is usually +# useless for serving static content. So one usually enables the +# exportation for CGI and SSI requests only. +# o StrictRequire: +# This denies access when "SSLRequireSSL" or "SSLRequire" applied even +# under a "Satisfy any" situation, i.e. when it applies access is denied +# and no other module can change it. +# o OptRenegotiate: +# This enables optimized SSL connection renegotiation handling when SSL +# directives are used in per-directory context. +#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + +# SSL Protocol Adjustments: +# The safe and default but still SSL/TLS standard compliant shutdown +# approach is that mod_ssl sends the close notify alert but doesn't wait for +# the close notify alert from client. When you need a different shutdown +# approach you can use one of the following variables: +# o ssl-unclean-shutdown: +# This forces an unclean shutdown when the connection is closed, i.e. no +# SSL close notify alert is send or allowed to received. This violates +# the SSL/TLS standard but is needed for some brain-dead browsers. Use +# this when you receive I/O errors because of the standard approach where +# mod_ssl sends the close notify alert. +# o ssl-accurate-shutdown: +# This forces an accurate shutdown when the connection is closed, i.e. a +# SSL close notify alert is send and mod_ssl waits for the close notify +# alert of the client. This is 100% SSL/TLS standard compliant, but in +# practice often causes hanging connections with brain-dead browsers. Use +# this only for browsers where you know that their SSL implementation +# works correctly. +# Notice: Most problems of broken clients are also related to the HTTP +# keep-alive facility, so you usually additionally want to disable +# keep-alive for those clients, too. Use variable "nokeepalive" for this. +# Similarly, one has to force some clients to use HTTP/1.0 to workaround +# their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and +# "force-response-1.0" for this. +BrowserMatch "MSIE [2-5]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0 + +# Per-Server Logging: +# The home of a custom SSL log file. Use this when you want a +# compact non-error SSL logfile on a virtual host basis. +CustomLog logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf new file mode 100644 index 000000000..b5d7a49ef --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf @@ -0,0 +1,36 @@ +# +# UserDir: The name of the directory that is appended onto a user's home +# directory if a ~user request is received. +# +# The path to the end user account 'public_html' directory must be +# accessible to the webserver userid. This usually means that ~userid +# must have permissions of 711, ~userid/public_html must have permissions +# of 755, and documents contained therein must be world-readable. +# Otherwise, the client will only receive a "403 Forbidden" message. +# + + # + # UserDir is disabled by default since it can confirm the presence + # of a username on the system (depending on home directory + # permissions). + # + UserDir disabled + + # + # To enable requests to /~user/ to serve the user's public_html + # directory, remove the "UserDir disabled" line above, and uncomment + # the following line instead: + # + #UserDir public_html + + +# +# Control access to UserDir directories. The following is an example +# for a site where these directories are restricted to read-only. +# + + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + Require method GET POST OPTIONS + + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf new file mode 100644 index 000000000..c1b6c11d9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf @@ -0,0 +1,22 @@ +# +# This configuration file enables the default "Welcome" page if there +# is no default index page present for the root URL. To disable the +# Welcome page, comment out all the lines below. +# +# NOTE: if this file is removed, it will be restored on upgrades. +# + + Options -Indexes + ErrorDocument 403 /.noindex.html + + + + AllowOverride None + Require all granted + + +Alias /.noindex.html /usr/share/httpd/noindex/index.html +Alias /noindex/css/bootstrap.min.css /usr/share/httpd/noindex/css/bootstrap.min.css +Alias /noindex/css/open-sans.css /usr/share/httpd/noindex/css/open-sans.css +Alias /images/apache_pb.gif /usr/share/httpd/noindex/images/apache_pb.gif +Alias /images/poweredby.png /usr/share/httpd/noindex/images/poweredby.png diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf new file mode 100644 index 000000000..31d979f20 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf @@ -0,0 +1,77 @@ +# +# This file loads most of the modules included with the Apache HTTP +# Server itself. +# + +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule actions_module modules/mod_actions.so +LoadModule alias_module modules/mod_alias.so +LoadModule allowmethods_module modules/mod_allowmethods.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule auth_digest_module modules/mod_auth_digest.so +LoadModule authn_anon_module modules/mod_authn_anon.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authn_dbd_module modules/mod_authn_dbd.so +LoadModule authn_dbm_module modules/mod_authn_dbm.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authn_socache_module modules/mod_authn_socache.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule authz_dbd_module modules/mod_authz_dbd.so +LoadModule authz_dbm_module modules/mod_authz_dbm.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_owner_module modules/mod_authz_owner.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule autoindex_module modules/mod_autoindex.so +LoadModule cache_module modules/mod_cache.so +LoadModule cache_disk_module modules/mod_cache_disk.so +LoadModule data_module modules/mod_data.so +LoadModule dbd_module modules/mod_dbd.so +LoadModule deflate_module modules/mod_deflate.so +LoadModule dir_module modules/mod_dir.so +LoadModule dumpio_module modules/mod_dumpio.so +LoadModule echo_module modules/mod_echo.so +LoadModule env_module modules/mod_env.so +LoadModule expires_module modules/mod_expires.so +LoadModule ext_filter_module modules/mod_ext_filter.so +LoadModule filter_module modules/mod_filter.so +LoadModule headers_module modules/mod_headers.so +LoadModule include_module modules/mod_include.so +LoadModule info_module modules/mod_info.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule logio_module modules/mod_logio.so +LoadModule mime_magic_module modules/mod_mime_magic.so +LoadModule mime_module modules/mod_mime.so +LoadModule negotiation_module modules/mod_negotiation.so +LoadModule remoteip_module modules/mod_remoteip.so +LoadModule reqtimeout_module modules/mod_reqtimeout.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule setenvif_module modules/mod_setenvif.so +LoadModule slotmem_plain_module modules/mod_slotmem_plain.so +LoadModule slotmem_shm_module modules/mod_slotmem_shm.so +LoadModule socache_dbm_module modules/mod_socache_dbm.so +LoadModule socache_memcache_module modules/mod_socache_memcache.so +LoadModule socache_shmcb_module modules/mod_socache_shmcb.so +LoadModule status_module modules/mod_status.so +LoadModule substitute_module modules/mod_substitute.so +LoadModule suexec_module modules/mod_suexec.so +LoadModule unique_id_module modules/mod_unique_id.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule userdir_module modules/mod_userdir.so +LoadModule version_module modules/mod_version.so +LoadModule vhost_alias_module modules/mod_vhost_alias.so + +#LoadModule buffer_module modules/mod_buffer.so +#LoadModule watchdog_module modules/mod_watchdog.so +#LoadModule heartbeat_module modules/mod_heartbeat.so +#LoadModule heartmonitor_module modules/mod_heartmonitor.so +#LoadModule usertrack_module modules/mod_usertrack.so +#LoadModule dialup_module modules/mod_dialup.so +#LoadModule charset_lite_module modules/mod_charset_lite.so +#LoadModule log_debug_module modules/mod_log_debug.so +#LoadModule ratelimit_module modules/mod_ratelimit.so +#LoadModule reflector_module modules/mod_reflector.so +#LoadModule request_module modules/mod_request.so +#LoadModule sed_module modules/mod_sed.so +#LoadModule speling_module modules/mod_speling.so + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf new file mode 100644 index 000000000..e6af8decd --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf @@ -0,0 +1,3 @@ +LoadModule dav_module modules/mod_dav.so +LoadModule dav_fs_module modules/mod_dav_fs.so +LoadModule dav_lock_module modules/mod_dav_lock.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf new file mode 100644 index 000000000..9e0d0db6e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf @@ -0,0 +1 @@ +LoadModule lua_module modules/mod_lua.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf new file mode 100644 index 000000000..7bfd1d413 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf @@ -0,0 +1,19 @@ +# Select the MPM module which should be used by uncommenting exactly +# one of the following LoadModule lines: + +# prefork MPM: Implements a non-threaded, pre-forking web server +# See: http://httpd.apache.org/docs/2.4/mod/prefork.html +LoadModule mpm_prefork_module modules/mod_mpm_prefork.so + +# worker MPM: Multi-Processing Module implementing a hybrid +# multi-threaded multi-process web server +# See: http://httpd.apache.org/docs/2.4/mod/worker.html +# +#LoadModule mpm_worker_module modules/mod_mpm_worker.so + +# event MPM: A variant of the worker MPM with the goal of consuming +# threads only for connections with active processing +# See: http://httpd.apache.org/docs/2.4/mod/event.html +# +#LoadModule mpm_event_module modules/mod_mpm_event.so + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf new file mode 100644 index 000000000..cc0bca077 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf @@ -0,0 +1,16 @@ +# This file configures all the proxy modules: +LoadModule proxy_module modules/mod_proxy.so +LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so +LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so +LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so +LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so +LoadModule proxy_ajp_module modules/mod_proxy_ajp.so +LoadModule proxy_balancer_module modules/mod_proxy_balancer.so +LoadModule proxy_connect_module modules/mod_proxy_connect.so +LoadModule proxy_express_module modules/mod_proxy_express.so +LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so +LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so +LoadModule proxy_ftp_module modules/mod_proxy_ftp.so +LoadModule proxy_http_module modules/mod_proxy_http.so +LoadModule proxy_scgi_module modules/mod_proxy_scgi.so +LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf new file mode 100644 index 000000000..53235cd76 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf @@ -0,0 +1 @@ +LoadModule ssl_module modules/mod_ssl.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf new file mode 100644 index 000000000..b208c972d --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf @@ -0,0 +1,2 @@ +# This file configures systemd module: +LoadModule systemd_module modules/mod_systemd.so diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf new file mode 100644 index 000000000..5b8b9362e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf @@ -0,0 +1,14 @@ +# This configuration file loads a CGI module appropriate to the MPM +# which has been configured in 00-mpm.conf. mod_cgid should be used +# with a threaded MPM; mod_cgi with the prefork MPM. + + + LoadModule cgid_module modules/mod_cgid.so + + + LoadModule cgid_module modules/mod_cgid.so + + + LoadModule cgi_module modules/mod_cgi.so + + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf new file mode 100644 index 000000000..a7af0dc1e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf @@ -0,0 +1,353 @@ +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See for detailed information. +# In particular, see +# +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so 'log/access_log' +# with ServerRoot set to '/www' will be interpreted by the +# server as '/www/log/access_log', where as '/log/access_log' will be +# interpreted as '/log/access_log'. + +# +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to specify a local disk on the +# Mutex directive, if file-based mutexes are used. If you wish to share the +# same ServerRoot for multiple httpd daemons, you will need to change at +# least PidFile. +# +ServerRoot "/etc/httpd" + +# +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 + +# +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +Include conf.modules.d/*.conf + +# +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +# +User apache +Group apache + +# 'Main' server configuration +# +# The directives in this section set up the values used by the 'main' +# server, which responds to any requests that aren't handled by a +# definition. These values also provide defaults for +# any containers you may define later in the file. +# +# All of these directives may appear inside containers, +# in which case these default settings will be overridden for the +# virtual host being defined. +# + +# +# ServerAdmin: Your address, where problems with the server should be +# e-mailed. This address appears on some server-generated pages, such +# as error documents. e.g. admin@your-domain.com +# +ServerAdmin root@localhost + +# +# ServerName gives the name and port that the server uses to identify itself. +# This can often be determined automatically, but we recommend you specify +# it explicitly to prevent problems during startup. +# +# If your host doesn't have a registered DNS name, enter its IP address here. +# +#ServerName www.example.com:80 + +# +# Deny access to the entirety of your server's filesystem. You must +# explicitly permit access to web content directories in other +# blocks below. +# + + AllowOverride none + Require all denied + + +# +# Note that from this point forward you must specifically allow +# particular features to be enabled - so if something's not working as +# you might expect, make sure that you have specifically enabled it +# below. +# + +# +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +DocumentRoot "/var/www/html" + +# +# Relax access to content within /var/www. +# + + AllowOverride None + # Allow open access: + Require all granted + + +# Further relax access to the default document root: + + # + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + # + Options Indexes FollowSymLinks + + # + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # Options FileInfo AuthConfig Limit + # + AllowOverride None + + # + # Controls who can get stuff from this server. + # + Require all granted + + +# +# DirectoryIndex: sets the file that Apache will serve if a directory +# is requested. +# + + DirectoryIndex index.html + + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + +# +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +ErrorLog "logs/error_log" + +# +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +# +LogLevel warn + + + # + # The following directives define some format nicknames for use with + # a CustomLog directive (see below). + # + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + + # You need to enable mod_logio.c to use %I and %O + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + + # + # The location and format of the access logfile (Common Logfile Format). + # If you do not define any access logfiles within a + # container, they will be logged here. Contrariwise, if you *do* + # define per- access logfiles, transactions will be + # logged therein and *not* in this file. + # + #CustomLog "logs/access_log" common + + # + # If you prefer a logfile with access, agent, and referer information + # (Combined Logfile Format) you can use the following directive. + # + CustomLog "logs/access_log" combined + + + + # + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a section to allow access to + # the filesystem path. + + # + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + # + ScriptAlias /cgi-bin/ "/var/www/cgi-bin/" + + + +# +# "/var/www/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. +# + + AllowOverride None + Options None + Require all granted + + + + # + # TypesConfig points to the file containing the list of mappings from + # filename extension to MIME-type. + # + TypesConfig /etc/mime.types + + # + # AddType allows you to add to or override the MIME configuration + # file specified in TypesConfig for specific file types. + # + #AddType application/x-gzip .tgz + # + # AddEncoding allows you to have certain browsers uncompress + # information on the fly. Note: Not all browsers support this. + # + #AddEncoding x-compress .Z + #AddEncoding x-gzip .gz .tgz + # + # If the AddEncoding directives above are commented-out, then you + # probably should define those extensions to indicate media types: + # + AddType application/x-compress .Z + AddType application/x-gzip .gz .tgz + + # + # AddHandler allows you to map certain file extensions to "handlers": + # actions unrelated to filetype. These can be either built into the server + # or added with the Action directive (see below) + # + # To use CGI scripts outside of ScriptAliased directories: + # (You will also need to add "ExecCGI" to the "Options" directive.) + # + #AddHandler cgi-script .cgi + + # For type maps (negotiated resources): + #AddHandler type-map var + + # + # Filters allow you to process content before it is sent to the client. + # + # To parse .shtml files for server-side includes (SSI): + # (You will also need to add "Includes" to the "Options" directive.) + # + AddType text/html .shtml + AddOutputFilter INCLUDES .shtml + + +# +# Specify a default charset for all content served; this enables +# interpretation of all content as UTF-8 by default. To use the +# default browser choice (ISO-8859-1), or to allow the META tags +# in HTML content to override this choice, comment out this +# directive: +# +AddDefaultCharset UTF-8 + + + # + # The mod_mime_magic module allows the server to use various hints from the + # contents of the file itself to determine its type. The MIMEMagicFile + # directive tells the module where the hint definitions are located. + # + MIMEMagicFile conf/magic + + +# +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html +# + +# +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall may be used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +# Defaults if commented: EnableMMAP On, EnableSendfile Off +# +#EnableMMAP off +EnableSendfile on + +# Supplemental configuration +# +# Load config files in the "/etc/httpd/conf.d" directory, if any. +IncludeOptional conf.d/*.conf diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/magic b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/magic new file mode 100644 index 000000000..7c56119e9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/httpd/conf/magic @@ -0,0 +1,385 @@ +# Magic data for mod_mime_magic Apache module (originally for file(1) command) +# The module is described in /manual/mod/mod_mime_magic.html +# +# The format is 4-5 columns: +# Column #1: byte number to begin checking from, ">" indicates continuation +# Column #2: type of data to match +# Column #3: contents of data to match +# Column #4: MIME type of result +# Column #5: MIME encoding of result (optional) + +#------------------------------------------------------------------------------ +# Localstuff: file(1) magic for locally observed files +# Add any locally observed files here. + +#------------------------------------------------------------------------------ +# end local stuff +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# Java + +0 short 0xcafe +>2 short 0xbabe application/java + +#------------------------------------------------------------------------------ +# audio: file(1) magic for sound formats +# +# from Jan Nicolai Langfeldt , +# + +# Sun/NeXT audio data +0 string .snd +>12 belong 1 audio/basic +>12 belong 2 audio/basic +>12 belong 3 audio/basic +>12 belong 4 audio/basic +>12 belong 5 audio/basic +>12 belong 6 audio/basic +>12 belong 7 audio/basic + +>12 belong 23 audio/x-adpcm + +# DEC systems (e.g. DECstation 5000) use a variant of the Sun/NeXT format +# that uses little-endian encoding and has a different magic number +# (0x0064732E in little-endian encoding). +0 lelong 0x0064732E +>12 lelong 1 audio/x-dec-basic +>12 lelong 2 audio/x-dec-basic +>12 lelong 3 audio/x-dec-basic +>12 lelong 4 audio/x-dec-basic +>12 lelong 5 audio/x-dec-basic +>12 lelong 6 audio/x-dec-basic +>12 lelong 7 audio/x-dec-basic +# compressed (G.721 ADPCM) +>12 lelong 23 audio/x-dec-adpcm + +# Bytes 0-3 of AIFF, AIFF-C, & 8SVX audio files are "FORM" +# AIFF audio data +8 string AIFF audio/x-aiff +# AIFF-C audio data +8 string AIFC audio/x-aiff +# IFF/8SVX audio data +8 string 8SVX audio/x-aiff + +# Creative Labs AUDIO stuff +# Standard MIDI data +0 string MThd audio/unknown +#>9 byte >0 (format %d) +#>11 byte >1 using %d channels +# Creative Music (CMF) data +0 string CTMF audio/unknown +# SoundBlaster instrument data +0 string SBI audio/unknown +# Creative Labs voice data +0 string Creative\ Voice\ File audio/unknown +## is this next line right? it came this way... +#>19 byte 0x1A +#>23 byte >0 - version %d +#>22 byte >0 \b.%d + +# [GRR 950115: is this also Creative Labs? Guessing that first line +# should be string instead of unknown-endian long...] +#0 long 0x4e54524b MultiTrack sound data +#0 string NTRK MultiTrack sound data +#>4 long x - version %ld + +# Microsoft WAVE format (*.wav) +# [GRR 950115: probably all of the shorts and longs should be leshort/lelong] +# Microsoft RIFF +0 string RIFF audio/unknown +# - WAVE format +>8 string WAVE audio/x-wav +# MPEG audio. +0 beshort&0xfff0 0xfff0 audio/mpeg +# C64 SID Music files, from Linus Walleij +0 string PSID audio/prs.sid + +#------------------------------------------------------------------------------ +# c-lang: file(1) magic for C programs or various scripts +# + +# XPM icons (Greg Roelofs, newt@uchicago.edu) +# ideally should go into "images", but entries below would tag XPM as C source +0 string /*\ XPM image/x-xbm 7bit + +# this first will upset you if you're a PL/1 shop... (are there any left?) +# in which case rm it; ascmagic will catch real C programs +# C or REXX program text +0 string /* text/plain +# C++ program text +0 string // text/plain + +#------------------------------------------------------------------------------ +# compress: file(1) magic for pure-compression formats (no archives) +# +# compress, gzip, pack, compact, huf, squeeze, crunch, freeze, yabba, whap, etc. +# +# Formats for various forms of compressed data +# Formats for "compress" proper have been moved into "compress.c", +# because it tries to uncompress it to figure out what's inside. + +# standard unix compress +0 string \037\235 application/octet-stream x-compress + +# gzip (GNU zip, not to be confused with [Info-ZIP/PKWARE] zip archiver) +0 string \037\213 application/octet-stream x-gzip + +# According to gzip.h, this is the correct byte order for packed data. +0 string \037\036 application/octet-stream +# +# This magic number is byte-order-independent. +# +0 short 017437 application/octet-stream + +# XXX - why *two* entries for "compacted data", one of which is +# byte-order independent, and one of which is byte-order dependent? +# +# compacted data +0 short 0x1fff application/octet-stream +0 string \377\037 application/octet-stream +# huf output +0 short 0145405 application/octet-stream + +# Squeeze and Crunch... +# These numbers were gleaned from the Unix versions of the programs to +# handle these formats. Note that I can only uncrunch, not crunch, and +# I didn't have a crunched file handy, so the crunch number is untested. +# Keith Waclena +#0 leshort 0x76FF squeezed data (CP/M, DOS) +#0 leshort 0x76FE crunched data (CP/M, DOS) + +# Freeze +#0 string \037\237 Frozen file 2.1 +#0 string \037\236 Frozen file 1.0 (or gzip 0.5) + +# lzh? +#0 string \037\240 LZH compressed data + +#------------------------------------------------------------------------------ +# frame: file(1) magic for FrameMaker files +# +# This stuff came on a FrameMaker demo tape, most of which is +# copyright, but this file is "published" as witness the following: +# +0 string \ +# and Anna Shergold +# +0 string \ +0 string \14 byte 12 (OS/2 1.x format) +#>14 byte 64 (OS/2 2.x format) +#>14 byte 40 (Windows 3.x format) +#0 string IC icon +#0 string PI pointer +#0 string CI color icon +#0 string CP color pointer +#0 string BA bitmap array + +0 string \x89PNG image/png +0 string FWS application/x-shockwave-flash +0 string CWS application/x-shockwave-flash + +#------------------------------------------------------------------------------ +# lisp: file(1) magic for lisp programs +# +# various lisp types, from Daniel Quinlan (quinlan@yggdrasil.com) +0 string ;; text/plain 8bit +# Emacs 18 - this is always correct, but not very magical. +0 string \012( application/x-elc +# Emacs 19 +0 string ;ELC\023\000\000\000 application/x-elc + +#------------------------------------------------------------------------------ +# mail.news: file(1) magic for mail and news +# +# There are tests to ascmagic.c to cope with mail and news. +0 string Relay-Version: message/rfc822 7bit +0 string #!\ rnews message/rfc822 7bit +0 string N#!\ rnews message/rfc822 7bit +0 string Forward\ to message/rfc822 7bit +0 string Pipe\ to message/rfc822 7bit +0 string Return-Path: message/rfc822 7bit +0 string Path: message/news 8bit +0 string Xref: message/news 8bit +0 string From: message/rfc822 7bit +0 string Article message/news 8bit +#------------------------------------------------------------------------------ +# msword: file(1) magic for MS Word files +# +# Contributor claims: +# Reversed-engineered MS Word magic numbers +# + +0 string \376\067\0\043 application/msword +0 string \333\245-\0\0\0 application/msword + +# disable this one because it applies also to other +# Office/OLE documents for which msword is not correct. See PR#2608. +#0 string \320\317\021\340\241\261 application/msword + + + +#------------------------------------------------------------------------------ +# printer: file(1) magic for printer-formatted files +# + +# PostScript +0 string %! application/postscript +0 string \004%! application/postscript + +# Acrobat +# (due to clamen@cs.cmu.edu) +0 string %PDF- application/pdf + +#------------------------------------------------------------------------------ +# sc: file(1) magic for "sc" spreadsheet +# +38 string Spreadsheet application/x-sc + +#------------------------------------------------------------------------------ +# tex: file(1) magic for TeX files +# +# XXX - needs byte-endian stuff (big-endian and little-endian DVI?) +# +# From + +# Although we may know the offset of certain text fields in TeX DVI +# and font files, we can't use them reliably because they are not +# zero terminated. [but we do anyway, christos] +0 string \367\002 application/x-dvi +#0 string \367\203 TeX generic font data +#0 string \367\131 TeX packed font data +#0 string \367\312 TeX virtual font data +#0 string This\ is\ TeX, TeX transcript text +#0 string This\ is\ METAFONT, METAFONT transcript text + +# There is no way to detect TeX Font Metric (*.tfm) files without +# breaking them apart and reading the data. The following patterns +# match most *.tfm files generated by METAFONT or afm2tfm. +#2 string \000\021 TeX font metric data +#2 string \000\022 TeX font metric data +#>34 string >\0 (%s) + +# Texinfo and GNU Info, from Daniel Quinlan (quinlan@yggdrasil.com) +#0 string \\input\ texinfo Texinfo source text +#0 string This\ is\ Info\ file GNU Info text + +# correct TeX magic for Linux (and maybe more) +# from Peter Tobias (tobias@server.et-inf.fho-emden.de) +# +0 leshort 0x02f7 application/x-dvi + +# RTF - Rich Text Format +0 string {\\rtf application/rtf + +#------------------------------------------------------------------------------ +# animation: file(1) magic for animation/movie formats +# +# animation formats, originally from vax@ccwf.cc.utexas.edu (VaX#n8) +# MPEG file +0 string \000\000\001\263 video/mpeg +# +# The contributor claims: +# I couldn't find a real magic number for these, however, this +# -appears- to work. Note that it might catch other files, too, +# so BE CAREFUL! +# +# Note that title and author appear in the two 20-byte chunks +# at decimal offsets 2 and 22, respectively, but they are XOR'ed with +# 255 (hex FF)! DL format SUCKS BIG ROCKS. +# +# DL file version 1 , medium format (160x100, 4 images/screen) +0 byte 1 video/unknown +0 byte 2 video/unknown +# Quicktime video, from Linus Walleij +# from Apple quicktime file format documentation. +4 string moov video/quicktime +4 string mdat video/quicktime + diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sites b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sites new file mode 100644 index 000000000..6af1f63fa --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sites @@ -0,0 +1 @@ +conf.d/centos.example.com.conf, centos.example.com diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd new file mode 100644 index 000000000..0bf6b176c --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd @@ -0,0 +1,25 @@ +# +# This file can be used to set additional environment variables for +# the httpd process, or pass additional options to the httpd +# executable. +# +# Note: With previous versions of httpd, the MPM could be changed by +# editing an "HTTPD" variable here. With the current version, that +# variable is now ignored. The MPM is a loadable module, and the +# choice of MPM can be changed by editing the configuration file +# /etc/httpd/conf.modules.d/00-mpm.conf. +# + +# +# To pass additional options (for instance, -D definitions) to the +# httpd binary at startup, set OPTIONS here. +# +OPTIONS="-D mock_define -D mock_define_too -D mock_value=TRUE" + +# +# This setting ensures the httpd process is started in the "C" locale +# by default. (Some modules will not behave correctly if +# case-sensitive string comparisons are performed in a different +# locale.) +# +LANG=C diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/httpd.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/httpd.conf new file mode 100644 index 000000000..e5693ffff --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/httpd.conf @@ -0,0 +1,157 @@ +# This is a modification of the default Apache 2.4 configuration file +# for Gentoo Linux. +# +# Support: +# http://www.gentoo.org/main/en/lists.xml [mailing lists] +# http://forums.gentoo.org/ [web forums] +# irc://irc.freenode.net#gentoo-apache [irc chat] +# +# Bug Reports: +# http://bugs.gentoo.org [gentoo related bugs] +# http://httpd.apache.org/bug_report.html [apache httpd related bugs] +# +# +# This is the main Apache HTTP server configuration file. It contains the +# configuration directives that give the server its instructions. +# See for detailed information. +# In particular, see +# +# for a discussion of each configuration directive. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# +# Configuration and logfile names: If the filenames you specify for many +# of the server's control files begin with "/" (or "drive:/" for Win32), the +# server will use that explicit path. If the filenames do *not* begin +# with "/", the value of ServerRoot is prepended -- so "var/log/apache2/foo_log" +# with ServerRoot set to "/usr" will be interpreted by the +# server as "/usr/var/log/apache2/foo.log". + +# ServerRoot: The top of the directory tree under which the server's +# configuration, error, and log files are kept. +# +# Do not add a slash at the end of the directory path. If you point +# ServerRoot at a non-local disk, be sure to point the LockFile directive +# at a local disk. If you wish to share the same ServerRoot for multiple +# httpd daemons, you will need to change at least LockFile and PidFile. +# Comment: The LockFile directive has been replaced by the Mutex directive +ServerRoot "/usr/lib64/apache2" + +# Dynamic Shared Object (DSO) Support +# +# To be able to use the functionality of a module which was built as a DSO you +# have to place corresponding `LoadModule' lines at this location so the +# directives contained in it are actually available _before_ they are used. +# Statically compiled modules (those listed by `httpd -l') do not need +# to be loaded here. +# +# Example: +# LoadModule foo_module modules/mod_foo.so +# +# GENTOO: Automatically defined based on APACHE2_MODULES USE_EXPAND variable. +# Do not change manually, it will be overwritten on upgrade. +# +# The following modules are considered as the default configuration. +# If you wish to disable one of them, you may have to alter other +# configuration directives. +# +# Change these at your own risk! + +LoadModule actions_module modules/mod_actions.so +LoadModule alias_module modules/mod_alias.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule authn_anon_module modules/mod_authn_anon.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authn_dbm_module modules/mod_authn_dbm.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule authz_dbm_module modules/mod_authz_dbm.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_owner_module modules/mod_authz_owner.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule autoindex_module modules/mod_autoindex.so + +LoadModule cache_module modules/mod_cache.so + +LoadModule cgi_module modules/mod_cgi.so +LoadModule cgid_module modules/mod_cgid.so + +LoadModule dav_module modules/mod_dav.so + + +LoadModule dav_fs_module modules/mod_dav_fs.so + + +LoadModule dav_lock_module modules/mod_dav_lock.so + +LoadModule deflate_module modules/mod_deflate.so +LoadModule dir_module modules/mod_dir.so +LoadModule env_module modules/mod_env.so +LoadModule expires_module modules/mod_expires.so +LoadModule ext_filter_module modules/mod_ext_filter.so + +LoadModule file_cache_module modules/mod_file_cache.so + +LoadModule filter_module modules/mod_filter.so +LoadModule headers_module modules/mod_headers.so +LoadModule include_module modules/mod_include.so + +LoadModule info_module modules/mod_info.so + +LoadModule log_config_module modules/mod_log_config.so +LoadModule logio_module modules/mod_logio.so +LoadModule mime_module modules/mod_mime.so +LoadModule mime_magic_module modules/mod_mime_magic.so +LoadModule negotiation_module modules/mod_negotiation.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule setenvif_module modules/mod_setenvif.so + +LoadModule socache_shmcb_module modules/mod_socache_shmcb.so + +LoadModule speling_module modules/mod_speling.so + +LoadModule ssl_module modules/mod_ssl.so + + +LoadModule status_module modules/mod_status.so + +LoadModule unique_id_module modules/mod_unique_id.so +LoadModule unixd_module modules/mod_unixd.so + +LoadModule userdir_module modules/mod_userdir.so + +LoadModule usertrack_module modules/mod_usertrack.so +LoadModule vhost_alias_module modules/mod_vhost_alias.so + +# If you wish httpd to run as a different user or group, you must run +# httpd as root initially and it will switch. +# +# User/Group: The name (or #number) of the user/group to run httpd as. +# It is usually good practice to create a dedicated user and group for +# running httpd, as with most system services. +User apache +Group apache + +# Supplemental configuration +# +# Most of the configuration files in the /etc/apache2/modules.d/ directory can +# be turned on using APACHE2_OPTS in /etc/conf.d/apache2 to add extra features +# or to modify the default configuration of the server. +# +# To know which flag to add to APACHE2_OPTS, look at the first line of the +# the file, which will usually be an where OPTION is the +# flag to use. + +Include modules.d/*.conf + +# Virtual-host support +# +# Gentoo has made using virtual-hosts easy. In /etc/apache2/vhosts.d/ we +# include a default vhost (enabled by adding -D DEFAULT_VHOST to +# APACHE2_OPTS in /etc/conf.d/apache2). +Include vhosts.d/*.conf + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/magic b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/magic new file mode 100644 index 000000000..7c56119e9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/magic @@ -0,0 +1,385 @@ +# Magic data for mod_mime_magic Apache module (originally for file(1) command) +# The module is described in /manual/mod/mod_mime_magic.html +# +# The format is 4-5 columns: +# Column #1: byte number to begin checking from, ">" indicates continuation +# Column #2: type of data to match +# Column #3: contents of data to match +# Column #4: MIME type of result +# Column #5: MIME encoding of result (optional) + +#------------------------------------------------------------------------------ +# Localstuff: file(1) magic for locally observed files +# Add any locally observed files here. + +#------------------------------------------------------------------------------ +# end local stuff +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# Java + +0 short 0xcafe +>2 short 0xbabe application/java + +#------------------------------------------------------------------------------ +# audio: file(1) magic for sound formats +# +# from Jan Nicolai Langfeldt , +# + +# Sun/NeXT audio data +0 string .snd +>12 belong 1 audio/basic +>12 belong 2 audio/basic +>12 belong 3 audio/basic +>12 belong 4 audio/basic +>12 belong 5 audio/basic +>12 belong 6 audio/basic +>12 belong 7 audio/basic + +>12 belong 23 audio/x-adpcm + +# DEC systems (e.g. DECstation 5000) use a variant of the Sun/NeXT format +# that uses little-endian encoding and has a different magic number +# (0x0064732E in little-endian encoding). +0 lelong 0x0064732E +>12 lelong 1 audio/x-dec-basic +>12 lelong 2 audio/x-dec-basic +>12 lelong 3 audio/x-dec-basic +>12 lelong 4 audio/x-dec-basic +>12 lelong 5 audio/x-dec-basic +>12 lelong 6 audio/x-dec-basic +>12 lelong 7 audio/x-dec-basic +# compressed (G.721 ADPCM) +>12 lelong 23 audio/x-dec-adpcm + +# Bytes 0-3 of AIFF, AIFF-C, & 8SVX audio files are "FORM" +# AIFF audio data +8 string AIFF audio/x-aiff +# AIFF-C audio data +8 string AIFC audio/x-aiff +# IFF/8SVX audio data +8 string 8SVX audio/x-aiff + +# Creative Labs AUDIO stuff +# Standard MIDI data +0 string MThd audio/unknown +#>9 byte >0 (format %d) +#>11 byte >1 using %d channels +# Creative Music (CMF) data +0 string CTMF audio/unknown +# SoundBlaster instrument data +0 string SBI audio/unknown +# Creative Labs voice data +0 string Creative\ Voice\ File audio/unknown +## is this next line right? it came this way... +#>19 byte 0x1A +#>23 byte >0 - version %d +#>22 byte >0 \b.%d + +# [GRR 950115: is this also Creative Labs? Guessing that first line +# should be string instead of unknown-endian long...] +#0 long 0x4e54524b MultiTrack sound data +#0 string NTRK MultiTrack sound data +#>4 long x - version %ld + +# Microsoft WAVE format (*.wav) +# [GRR 950115: probably all of the shorts and longs should be leshort/lelong] +# Microsoft RIFF +0 string RIFF audio/unknown +# - WAVE format +>8 string WAVE audio/x-wav +# MPEG audio. +0 beshort&0xfff0 0xfff0 audio/mpeg +# C64 SID Music files, from Linus Walleij +0 string PSID audio/prs.sid + +#------------------------------------------------------------------------------ +# c-lang: file(1) magic for C programs or various scripts +# + +# XPM icons (Greg Roelofs, newt@uchicago.edu) +# ideally should go into "images", but entries below would tag XPM as C source +0 string /*\ XPM image/x-xbm 7bit + +# this first will upset you if you're a PL/1 shop... (are there any left?) +# in which case rm it; ascmagic will catch real C programs +# C or REXX program text +0 string /* text/plain +# C++ program text +0 string // text/plain + +#------------------------------------------------------------------------------ +# compress: file(1) magic for pure-compression formats (no archives) +# +# compress, gzip, pack, compact, huf, squeeze, crunch, freeze, yabba, whap, etc. +# +# Formats for various forms of compressed data +# Formats for "compress" proper have been moved into "compress.c", +# because it tries to uncompress it to figure out what's inside. + +# standard unix compress +0 string \037\235 application/octet-stream x-compress + +# gzip (GNU zip, not to be confused with [Info-ZIP/PKWARE] zip archiver) +0 string \037\213 application/octet-stream x-gzip + +# According to gzip.h, this is the correct byte order for packed data. +0 string \037\036 application/octet-stream +# +# This magic number is byte-order-independent. +# +0 short 017437 application/octet-stream + +# XXX - why *two* entries for "compacted data", one of which is +# byte-order independent, and one of which is byte-order dependent? +# +# compacted data +0 short 0x1fff application/octet-stream +0 string \377\037 application/octet-stream +# huf output +0 short 0145405 application/octet-stream + +# Squeeze and Crunch... +# These numbers were gleaned from the Unix versions of the programs to +# handle these formats. Note that I can only uncrunch, not crunch, and +# I didn't have a crunched file handy, so the crunch number is untested. +# Keith Waclena +#0 leshort 0x76FF squeezed data (CP/M, DOS) +#0 leshort 0x76FE crunched data (CP/M, DOS) + +# Freeze +#0 string \037\237 Frozen file 2.1 +#0 string \037\236 Frozen file 1.0 (or gzip 0.5) + +# lzh? +#0 string \037\240 LZH compressed data + +#------------------------------------------------------------------------------ +# frame: file(1) magic for FrameMaker files +# +# This stuff came on a FrameMaker demo tape, most of which is +# copyright, but this file is "published" as witness the following: +# +0 string \ +# and Anna Shergold +# +0 string \ +0 string \14 byte 12 (OS/2 1.x format) +#>14 byte 64 (OS/2 2.x format) +#>14 byte 40 (Windows 3.x format) +#0 string IC icon +#0 string PI pointer +#0 string CI color icon +#0 string CP color pointer +#0 string BA bitmap array + +0 string \x89PNG image/png +0 string FWS application/x-shockwave-flash +0 string CWS application/x-shockwave-flash + +#------------------------------------------------------------------------------ +# lisp: file(1) magic for lisp programs +# +# various lisp types, from Daniel Quinlan (quinlan@yggdrasil.com) +0 string ;; text/plain 8bit +# Emacs 18 - this is always correct, but not very magical. +0 string \012( application/x-elc +# Emacs 19 +0 string ;ELC\023\000\000\000 application/x-elc + +#------------------------------------------------------------------------------ +# mail.news: file(1) magic for mail and news +# +# There are tests to ascmagic.c to cope with mail and news. +0 string Relay-Version: message/rfc822 7bit +0 string #!\ rnews message/rfc822 7bit +0 string N#!\ rnews message/rfc822 7bit +0 string Forward\ to message/rfc822 7bit +0 string Pipe\ to message/rfc822 7bit +0 string Return-Path: message/rfc822 7bit +0 string Path: message/news 8bit +0 string Xref: message/news 8bit +0 string From: message/rfc822 7bit +0 string Article message/news 8bit +#------------------------------------------------------------------------------ +# msword: file(1) magic for MS Word files +# +# Contributor claims: +# Reversed-engineered MS Word magic numbers +# + +0 string \376\067\0\043 application/msword +0 string \333\245-\0\0\0 application/msword + +# disable this one because it applies also to other +# Office/OLE documents for which msword is not correct. See PR#2608. +#0 string \320\317\021\340\241\261 application/msword + + + +#------------------------------------------------------------------------------ +# printer: file(1) magic for printer-formatted files +# + +# PostScript +0 string %! application/postscript +0 string \004%! application/postscript + +# Acrobat +# (due to clamen@cs.cmu.edu) +0 string %PDF- application/pdf + +#------------------------------------------------------------------------------ +# sc: file(1) magic for "sc" spreadsheet +# +38 string Spreadsheet application/x-sc + +#------------------------------------------------------------------------------ +# tex: file(1) magic for TeX files +# +# XXX - needs byte-endian stuff (big-endian and little-endian DVI?) +# +# From + +# Although we may know the offset of certain text fields in TeX DVI +# and font files, we can't use them reliably because they are not +# zero terminated. [but we do anyway, christos] +0 string \367\002 application/x-dvi +#0 string \367\203 TeX generic font data +#0 string \367\131 TeX packed font data +#0 string \367\312 TeX virtual font data +#0 string This\ is\ TeX, TeX transcript text +#0 string This\ is\ METAFONT, METAFONT transcript text + +# There is no way to detect TeX Font Metric (*.tfm) files without +# breaking them apart and reading the data. The following patterns +# match most *.tfm files generated by METAFONT or afm2tfm. +#2 string \000\021 TeX font metric data +#2 string \000\022 TeX font metric data +#>34 string >\0 (%s) + +# Texinfo and GNU Info, from Daniel Quinlan (quinlan@yggdrasil.com) +#0 string \\input\ texinfo Texinfo source text +#0 string This\ is\ Info\ file GNU Info text + +# correct TeX magic for Linux (and maybe more) +# from Peter Tobias (tobias@server.et-inf.fho-emden.de) +# +0 leshort 0x02f7 application/x-dvi + +# RTF - Rich Text Format +0 string {\\rtf application/rtf + +#------------------------------------------------------------------------------ +# animation: file(1) magic for animation/movie formats +# +# animation formats, originally from vax@ccwf.cc.utexas.edu (VaX#n8) +# MPEG file +0 string \000\000\001\263 video/mpeg +# +# The contributor claims: +# I couldn't find a real magic number for these, however, this +# -appears- to work. Note that it might catch other files, too, +# so BE CAREFUL! +# +# Note that title and author appear in the two 20-byte chunks +# at decimal offsets 2 and 22, respectively, but they are XOR'ed with +# 255 (hex FF)! DL format SUCKS BIG ROCKS. +# +# DL file version 1 , medium format (160x100, 4 images/screen) +0 byte 1 video/unknown +0 byte 2 video/unknown +# Quicktime video, from Linus Walleij +# from Apple quicktime file format documentation. +4 string moov video/quicktime +4 string mdat video/quicktime + diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/.keep_www-servers_apache-2 b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/.keep_www-servers_apache-2 new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf new file mode 100644 index 000000000..38635aa9d --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf @@ -0,0 +1,131 @@ +# This configuration file reflects default settings for Apache HTTP Server. +# You may change these, but chances are that you may not need to. + +# Timeout: The number of seconds before receives and sends time out. +Timeout 300 + +# KeepAlive: Whether or not to allow persistent connections (more than +# one request per connection). Set to "Off" to deactivate. +KeepAlive On + +# MaxKeepAliveRequests: The maximum number of requests to allow +# during a persistent connection. Set to 0 to allow an unlimited amount. +# We recommend you leave this number high, for maximum performance. +MaxKeepAliveRequests 100 + +# KeepAliveTimeout: Number of seconds to wait for the next request from the +# same client on the same connection. +KeepAliveTimeout 15 + +# UseCanonicalName: Determines how Apache constructs self-referencing +# URLs and the SERVER_NAME and SERVER_PORT variables. +# When set "Off", Apache will use the Hostname and Port supplied +# by the client. When set "On", Apache will use the value of the +# ServerName directive. +UseCanonicalName Off + +# AccessFileName: The name of the file to look for in each directory +# for additional configuration directives. See also the AllowOverride +# directive. +AccessFileName .htaccess + +# ServerTokens +# This directive configures what you return as the Server HTTP response +# Header. The default is 'Full' which sends information about the OS-Type +# and compiled in modules. +# Set to one of: Full | OS | Minor | Minimal | Major | Prod +# where Full conveys the most information, and Prod the least. +ServerTokens Prod + +# TraceEnable +# This directive overrides the behavior of TRACE for both the core server and +# mod_proxy. The default TraceEnable on permits TRACE requests per RFC 2616, +# which disallows any request body to accompany the request. TraceEnable off +# causes the core server and mod_proxy to return a 405 (Method not allowed) +# error to the client. +# For security reasons this is turned off by default. (bug #240680) +TraceEnable off + +# Optionally add a line containing the server version and virtual host +# name to server-generated pages (internal error documents, FTP directory +# listings, mod_status and mod_info output etc., but not CGI generated +# documents or custom error documents). +# Set to "EMail" to also include a mailto: link to the ServerAdmin. +# Set to one of: On | Off | EMail +ServerSignature On + +# HostnameLookups: Log the names of clients or just their IP addresses +# e.g., www.apache.org (on) or 204.62.129.132 (off). +# The default is off because it'd be overall better for the net if people +# had to knowingly turn this feature on, since enabling it means that +# each client request will result in AT LEAST one lookup request to the +# nameserver. +HostnameLookups Off + +# EnableMMAP and EnableSendfile: On systems that support it, +# memory-mapping or the sendfile syscall is used to deliver +# files. This usually improves server performance, but must +# be turned off when serving from networked-mounted +# filesystems or if support for these functions is otherwise +# broken on your system. +EnableMMAP On +EnableSendfile Off + +# FileETag: Configures the file attributes that are used to create +# the ETag (entity tag) response header field when the document is +# based on a static file. (The ETag value is used in cache management +# to save network bandwidth.) +FileETag MTime Size + +# ContentDigest: This directive enables the generation of Content-MD5 +# headers as defined in RFC1864 respectively RFC2616. +# The Content-MD5 header provides an end-to-end message integrity +# check (MIC) of the entity-body. A proxy or client may check this +# header for detecting accidental modification of the entity-body +# in transit. +# Note that this can cause performance problems on your server since +# the message digest is computed on every request (the values are +# not cached). +# Content-MD5 is only sent for documents served by the core, and not +# by any module. For example, SSI documents, output from CGI scripts, +# and byte range responses do not have this header. +ContentDigest Off + +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +ErrorLog /var/log/apache2/error_log + +# LogLevel: Control the number of messages logged to the error_log. +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +LogLevel warn + +# We configure the "default" to be a very restrictive set of features. + + Options FollowSymLinks + AllowOverride None + Require all denied + + +# DirectoryIndex: sets the file that Apache will serve if a directory +# is requested. +# +# The index.html.var file (a type-map) is used to deliver content- +# negotiated documents. The MultiViews Options can be used for the +# same purpose, but it is much slower. +# +# Do not change this entry unless you know what you are doing. + + DirectoryIndex index.html index.html.var + + +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. + + Require all denied + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf new file mode 100644 index 000000000..61479fa53 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf @@ -0,0 +1,57 @@ +# The configuration below implements multi-language error documents through +# content-negotiation. + +# Customizable error responses come in three flavors: +# 1) plain text 2) local redirects 3) external redirects +# Some examples: +#ErrorDocument 500 "The server made a boo boo." +#ErrorDocument 404 /missing.html +#ErrorDocument 404 "/cgi-bin/missing_handler.pl" +#ErrorDocument 402 http://www.example.com/subscription_info.html + +# Required modules: mod_alias, mod_include, mod_negotiation +# We use Alias to redirect any /error/HTTP_.html.var response to +# our collection of by-error message multi-language collections. We use +# includes to substitute the appropriate text. +# You can modify the messages' appearance without changing any of the +# default HTTP_.html.var files by adding the line: +# Alias /error/include/ "/your/include/path/" +# which allows you to create your own set of files by starting with the +# /var/www/localhost/error/include/ files and copying them to /your/include/path/, +# even on a per-VirtualHost basis. The default include files will display +# your Apache version number and your ServerAdmin email address regardless +# of the setting of ServerSignature. + + +Alias /error/ "/usr/share/apache2/error/" + + + AllowOverride None + Options IncludesNoExec + AddOutputFilter Includes html + AddHandler type-map var + Require all granted + LanguagePriority en cs de es fr it ja ko nl pl pt-br ro sv tr + ForceLanguagePriority Prefer Fallback + + +ErrorDocument 400 /error/HTTP_BAD_REQUEST.html.var +ErrorDocument 401 /error/HTTP_UNAUTHORIZED.html.var +ErrorDocument 403 /error/HTTP_FORBIDDEN.html.var +ErrorDocument 404 /error/HTTP_NOT_FOUND.html.var +ErrorDocument 405 /error/HTTP_METHOD_NOT_ALLOWED.html.var +ErrorDocument 408 /error/HTTP_REQUEST_TIME_OUT.html.var +ErrorDocument 410 /error/HTTP_GONE.html.var +ErrorDocument 411 /error/HTTP_LENGTH_REQUIRED.html.var +ErrorDocument 412 /error/HTTP_PRECONDITION_FAILED.html.var +ErrorDocument 413 /error/HTTP_REQUEST_ENTITY_TOO_LARGE.html.var +ErrorDocument 414 /error/HTTP_REQUEST_URI_TOO_LARGE.html.var +ErrorDocument 415 /error/HTTP_UNSUPPORTED_MEDIA_TYPE.html.var +ErrorDocument 500 /error/HTTP_INTERNAL_SERVER_ERROR.html.var +ErrorDocument 501 /error/HTTP_NOT_IMPLEMENTED.html.var +ErrorDocument 502 /error/HTTP_BAD_GATEWAY.html.var +ErrorDocument 503 /error/HTTP_SERVICE_UNAVAILABLE.html.var +ErrorDocument 506 /error/HTTP_VARIANT_ALSO_VARIES.html.var + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf new file mode 100644 index 000000000..c429bf94c --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf @@ -0,0 +1,133 @@ +# Settings for hosting different languages. + +# DefaultLanguage and AddLanguage allows you to specify the language of +# a document. You can then use content negotiation to give a browser a +# file in a language the user can understand. +# +# Specify a default language. This means that all data +# going out without a specific language tag (see below) will +# be marked with this one. You probably do NOT want to set +# this unless you are sure it is correct for all cases. +# +# It is generally better to not mark a page as +# being a certain language than marking it with the wrong +# language! +# +# DefaultLanguage nl +# +# Note 1: The suffix does not have to be the same as the language +# keyword --- those with documents in Polish (whose net-standard +# language code is pl) may wish to use "AddLanguage pl .po" to +# avoid the ambiguity with the common suffix for perl scripts. +# +# Note 2: The example entries below illustrate that in some cases +# the two character 'Language' abbreviation is not identical to +# the two character 'Country' code for its country, +# E.g. 'Danmark/dk' versus 'Danish/da'. +# +# Note 3: In the case of 'ltz' we violate the RFC by using a three char +# specifier. There is 'work in progress' to fix this and get +# the reference data for rfc1766 cleaned up. +# +# Catalan (ca) - Croatian (hr) - Czech (cs) - Danish (da) - Dutch (nl) +# English (en) - Esperanto (eo) - Estonian (et) - French (fr) - German (de) +# Greek-Modern (el) - Hebrew (he) - Italian (it) - Japanese (ja) +# Korean (ko) - Luxembourgeois* (ltz) - Norwegian Nynorsk (nn) +# Norwegian (no) - Polish (pl) - Portugese (pt) +# Brazilian Portuguese (pt-BR) - Russian (ru) - Swedish (sv) +# Simplified Chinese (zh-CN) - Spanish (es) - Traditional Chinese (zh-TW) +AddLanguage ca .ca +AddLanguage cs .cz .cs +AddLanguage da .dk +AddLanguage de .de +AddLanguage el .el +AddLanguage en .en +AddLanguage eo .eo +AddLanguage es .es +AddLanguage et .et +AddLanguage fr .fr +AddLanguage he .he +AddLanguage hr .hr +AddLanguage it .it +AddLanguage ja .ja +AddLanguage ko .ko +AddLanguage ltz .ltz +AddLanguage nl .nl +AddLanguage nn .nn +AddLanguage no .no +AddLanguage pl .po +AddLanguage pt .pt +AddLanguage pt-BR .pt-br +AddLanguage ru .ru +AddLanguage sv .sv +AddLanguage zh-CN .zh-cn +AddLanguage zh-TW .zh-tw + +# LanguagePriority allows you to give precedence to some languages +# in case of a tie during content negotiation. +# +# Just list the languages in decreasing order of preference. We have +# more or less alphabetized them here. You probably want to change this. +LanguagePriority en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv zh-CN zh-TW + +# ForceLanguagePriority allows you to serve a result page rather than +# MULTIPLE CHOICES (Prefer) [in case of a tie] or NOT ACCEPTABLE (Fallback) +# [in case no accepted languages matched the available variants] +ForceLanguagePriority Prefer Fallback + +# Commonly used filename extensions to character sets. You probably +# want to avoid clashes with the language extensions, unless you +# are good at carefully testing your setup after each change. +# See http://www.iana.org/assignments/character-sets for the +# official list of charset names and their respective RFCs. +AddCharset us-ascii.ascii .us-ascii +AddCharset ISO-8859-1 .iso8859-1 .latin1 +AddCharset ISO-8859-2 .iso8859-2 .latin2 .cen +AddCharset ISO-8859-3 .iso8859-3 .latin3 +AddCharset ISO-8859-4 .iso8859-4 .latin4 +AddCharset ISO-8859-5 .iso8859-5 .cyr .iso-ru +AddCharset ISO-8859-6 .iso8859-6 .arb .arabic +AddCharset ISO-8859-7 .iso8859-7 .grk .greek +AddCharset ISO-8859-8 .iso8859-8 .heb .hebrew +AddCharset ISO-8859-9 .iso8859-9 .latin5 .trk +AddCharset ISO-8859-10 .iso8859-10 .latin6 +AddCharset ISO-8859-13 .iso8859-13 +AddCharset ISO-8859-14 .iso8859-14 .latin8 +AddCharset ISO-8859-15 .iso8859-15 .latin9 +AddCharset ISO-8859-16 .iso8859-16 .latin10 +AddCharset ISO-2022-JP .iso2022-jp .jis +AddCharset ISO-2022-KR .iso2022-kr .kis +AddCharset ISO-2022-CN .iso2022-cn .cis +AddCharset Big5.Big5 .big5 .b5 +AddCharset cn-Big5 .cn-big5 +# For russian, more than one charset is used (depends on client, mostly): +AddCharset WINDOWS-1251 .cp-1251 .win-1251 +AddCharset CP866 .cp866 +AddCharset KOI8 .koi8 +AddCharset KOI8-E .koi8-e +AddCharset KOI8-r .koi8-r .koi8-ru +AddCharset KOI8-U .koi8-u +AddCharset KOI8-ru .koi8-uk .ua +AddCharset ISO-10646-UCS-2 .ucs2 +AddCharset ISO-10646-UCS-4 .ucs4 +AddCharset UTF-7 .utf7 +AddCharset UTF-8 .utf8 +AddCharset UTF-16 .utf16 +AddCharset UTF-16BE .utf16be +AddCharset UTF-16LE .utf16le +AddCharset UTF-32 .utf32 +AddCharset UTF-32BE .utf32be +AddCharset UTF-32LE .utf32le +AddCharset euc-cn .euc-cn +AddCharset euc-gb .euc-gb +AddCharset euc-jp .euc-jp +AddCharset euc-kr .euc-kr +# Not sure how euc-tw got in - IANA doesn't list it??? +AddCharset EUC-TW .euc-tw +AddCharset gb2312 .gb2312 .gb +AddCharset iso-10646-ucs-2 .ucs-2 .iso-10646-ucs-2 +AddCharset iso-10646-ucs-4 .ucs-4 .iso-10646-ucs-4 +AddCharset shift_jis .shift_jis .sjis + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf new file mode 100644 index 000000000..10bf48317 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf @@ -0,0 +1,85 @@ + + + + +# We include the /icons/ alias for FancyIndexed directory listings. If +# you do not use FancyIndexing, you may comment this out. +Alias /icons/ "/usr/share/apache2/icons/" + + + Options Indexes MultiViews + AllowOverride None + Require all granted + + + +# Directives controlling the display of server-generated directory listings. +# +# To see the listing of a directory, the Options directive for the +# directory must include "Indexes", and the directory must not contain +# a file matching those listed in the DirectoryIndex directive. + +# IndexOptions: Controls the appearance of server-generated directory +# listings. +IndexOptions FancyIndexing VersionSort + +# AddIcon* directives tell the server which icon to show for different +# files or filename extensions. These are only displayed for +# FancyIndexed directories. +AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip + +AddIconByType (TXT,/icons/text.gif) text/* +AddIconByType (IMG,/icons/image2.gif) image/* +AddIconByType (SND,/icons/sound2.gif) audio/* +AddIconByType (VID,/icons/movie.gif) video/* + +AddIcon /icons/binary.gif .bin .exe +AddIcon /icons/binhex.gif .hqx +AddIcon /icons/tar.gif .tar +AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv +AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip +AddIcon /icons/a.gif .ps .ai .eps +AddIcon /icons/layout.gif .html .shtml .htm .pdf +AddIcon /icons/text.gif .txt +AddIcon /icons/c.gif .c +AddIcon /icons/p.gif .pl .py +AddIcon /icons/f.gif .for +AddIcon /icons/dvi.gif .dvi +AddIcon /icons/uuencoded.gif .uu +AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl +AddIcon /icons/tex.gif .tex +AddIcon /icons/bomb.gif core + +AddIcon /icons/back.gif .. +AddIcon /icons/hand.right.gif README +AddIcon /icons/folder.gif ^^DIRECTORY^^ +AddIcon /icons/blank.gif ^^BLANKICON^^ + +# DefaultIcon is which icon to show for files which do not have an icon +# explicitly set. +DefaultIcon /icons/unknown.gif + +# AddDescription allows you to place a short description after a file in +# server-generated indexes. These are only displayed for FancyIndexed +# directories. +# Format: AddDescription "description" filename + +#AddDescription "GZIP compressed document" .gz +#AddDescription "tar archive" .tar +#AddDescription "GZIP compressed tar archive" .tgz + +# ReadmeName is the name of the README file the server will look for by +# default, and append to directory listings. + +# HeaderName is the name of a file which should be prepended to +# directory indexes. +ReadmeName README.html +HeaderName HEADER.html + +# IndexIgnore is a set of filenames which directory indexing should ignore +# and not include in the listing. Shell-style wildcarding is permitted. +IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf new file mode 100644 index 000000000..2cd32c477 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf @@ -0,0 +1,10 @@ + +# Allow remote server configuration reports, with the URL of +# http://servername/server-info + + SetHandler server-info + Require local + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf new file mode 100644 index 000000000..ce0238eee --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf @@ -0,0 +1,35 @@ + +# The following directives define some format nicknames for use with +# a CustomLog directive (see below). +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined +LogFormat "%h %l %u %t \"%r\" %>s %b" common + +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-Agent}i" agent +LogFormat "%v %h %l %u %t \"%r\" %>s %b %T" script +LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" VLOG=%{VLOG}e" vhost + + +# You need to enable mod_logio.c to use %I and %O +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio +LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" vhostio + + +# The location and format of the access logfile (Common Logfile Format). +# If you do not define any access logfiles within a +# container, they will be logged here. Contrariwise, if you *do* +# define per- access logfiles, transactions will be +# logged therein and *not* in this file. +CustomLog /var/log/apache2/access_log common + +# If you would like to have agent and referer logfiles, +# uncomment the following directives. +#CustomLog /var/log/apache2/referer_log referer +#CustomLog /var/log/apache2/agent_logs agent + +# If you prefer a logfile with access, agent, and referer information +# (Combined Logfile Format) you can use the following directive. +#CustomLog /var/log/apache2/access_log combined + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf new file mode 100644 index 000000000..fb8a9a5d5 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf @@ -0,0 +1,46 @@ + +# TypesConfig points to the file containing the list of mappings from +# filename extension to MIME-type. +TypesConfig /etc/mime.types + +# AddType allows you to add to or override the MIME configuration +# file specified in TypesConfig for specific file types. +#AddType application/x-gzip .tgz + +# AddEncoding allows you to have certain browsers uncompress +# information on the fly. Note: Not all browsers support this. +#AddEncoding x-compress .Z +#AddEncoding x-gzip .gz .tgz + +# If the AddEncoding directives above are commented-out, then you +# probably should define those extensions to indicate media types: +AddType application/x-compress .Z +AddType application/x-gzip .gz .tgz + +# AddHandler allows you to map certain file extensions to "handlers": +# actions unrelated to filetype. These can be either built into the server +# or added with the Action directive (see below) + +# To use CGI scripts outside of ScriptAliased directories: +# (You will also need to add "ExecCGI" to the "Options" directive.) +#AddHandler cgi-script .cgi + +# For type maps (negotiated resources): +#AddHandler type-map var + +# Filters allow you to process content before it is sent to the client. +# +# To parse .shtml files for server-side includes (SSI): +# (You will also need to add "Includes" to the "Options" directive.) +#AddType text/html .shtml +#AddOutputFilter INCLUDES .shtml + + + +# The mod_mime_magic module allows the server to use various hints from the +# contents of the file itself to determine its type. The MIMEMagicFile +# directive tells the module where the hint definitions are located. +MIMEMagicFile /etc/apache2/magic + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf new file mode 100644 index 000000000..ed8b3c7cb --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf @@ -0,0 +1,15 @@ + +# Allow server status reports generated by mod_status, +# with the URL of http://servername/server-status + + SetHandler server-status + Require local + + +# ExtendedStatus controls whether Apache will generate "full" status +# information (ExtendedStatus On) or just basic information (ExtendedStatus +# Off) when the "server-status" handler is called. +ExtendedStatus On + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf new file mode 100644 index 000000000..0087126c4 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf @@ -0,0 +1,32 @@ +# Settings for user home directories + +# UserDir: The name of the directory that is appended onto a user's home +# directory if a ~user request is received. Note that you must also set +# the default access control for these directories, as in the example below. +UserDir public_html + +# Control access to UserDir directories. The following is an example +# for a site where these directories are restricted to read-only. + + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + + Require all granted + + + Require all denied + + + +# Suexec isn't really required to run cgi-scripts, but it's a really good +# idea if you have multiple users serving websites... + + + Options ExecCGI + SetHandler cgi-script + + + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf new file mode 100644 index 000000000..bcb9b6b47 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf @@ -0,0 +1,99 @@ +# Server-Pool Management (MPM specific) + +# PidFile: The file in which the server should record its process +# identification number when it starts. +# +# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING +PidFile /run/apache2.pid + +# The accept serialization lock file MUST BE STORED ON A LOCAL DISK. +# Mutex file:/run/apache_mpm_mutex + +# Only one of the below sections will be relevant on your +# installed httpd. Use "/usr/sbin/apache2 -l" to find out the +# active mpm. + +# common MPM configuration +# These configuration directives apply to all MPMs +# +# StartServers: Number of child server processes created at startup +# MaxRequestWorkers: Maximum number of child processes to serve requests +# MaxConnectionsPerChild: Limit on the number of connections that an individual +# child server will handle during its life + + +# prefork MPM +# This is the default MPM if USE=-threads +# +# MinSpareServers: Minimum number of idle child server processes +# MaxSpareServers: Maximum number of idle child server processes + + StartServers 5 + MinSpareServers 5 + MaxSpareServers 10 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# worker MPM +# This is the default MPM if USE=threads +# +# MinSpareThreads: Minimum number of idle threads available to handle request spikes +# MaxSpareThreads: Maximum number of idle threads +# ThreadsPerChild: Number of threads created by each child process + + StartServers 2 + MinSpareThreads 25 + MaxSpareThreads 75 + ThreadsPerChild 25 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# event MPM +# +# MinSpareThreads: Minimum number of idle threads available to handle request spikes +# MaxSpareThreads: Maximum number of idle threads +# ThreadsPerChild: Number of threads created by each child process + + StartServers 2 + MinSpareThreads 25 + MaxSpareThreads 75 + ThreadsPerChild 25 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# peruser MPM +# +# MinSpareProcessors: Minimum number of idle child server processes +# MinProcessors: Minimum number of processors per virtual host +# MaxProcessors: Maximum number of processors per virtual host +# ExpireTimeout: Maximum idle time before a child is killed, 0 to disable +# Multiplexer: Specify a Multiplexer child configuration. +# Processor: Specify a user and group for a specific child process + + MinSpareProcessors 2 + MinProcessors 2 + MaxProcessors 10 + MaxRequestWorkers 150 + MaxConnectionsPerChild 1000 + ExpireTimeout 1800 + + Multiplexer nobody nobody + Processor apache apache + + +# itk MPM +# +# MinSpareServers: Minimum number of idle child server processes +# MaxSpareServers: Maximum number of idle child server processes + + StartServers 5 + MinSpareServers 5 + MaxSpareServers 10 + MaxRequestWorkers 150 + MaxConnectionsPerChild 10000 + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf new file mode 100644 index 000000000..520d9fd82 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf @@ -0,0 +1,10 @@ + +# 128MB cache for objects < 2MB +CacheEnable mem / +MCacheSize 131072 +MCacheMaxObjectCount 1000 +MCacheMinObjectSize 1 +MCacheMaxObjectSize 2097152 + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf new file mode 100644 index 000000000..f51de4641 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf @@ -0,0 +1,67 @@ +# Note: The following must must be present to support +# starting without SSL on platforms with no /dev/random equivalent +# but a statically compiled-in mod_ssl. + +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin + + + +# This is the Apache server configuration file providing SSL support. +# It contains the configuration directives to instruct the server how to +# serve pages over an https connection. For detailing information about these +# directives see + +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. + +## Pseudo Random Number Generator (PRNG): +# Configure one or more sources to seed the PRNG of the SSL library. +# The seed data should be of good random quality. +# WARNING! On some platforms /dev/random blocks if not enough entropy +# is available. This means you then cannot use the /dev/random device +# because it would lead to very long connection times (as long as +# it requires to make more entropy available). But usually those +# platforms additionally provide a /dev/urandom device which doesn't +# block. So, if available, use this one instead. Read the mod_ssl User +# Manual for more details. +#SSLRandomSeed startup file:/dev/random 512 +#SSLRandomSeed startup file:/dev/urandom 512 +#SSLRandomSeed connect file:/dev/random 512 +#SSLRandomSeed connect file:/dev/urandom 512 + +## SSL Global Context: +# All SSL configuration in this context applies both to the main server and +# all SSL-enabled virtual hosts. + +# Some MIME-types for downloading Certificates and CRLs + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + + +## Pass Phrase Dialog: +# Configure the pass phrase gathering process. The filtering dialog program +# (`builtin' is a internal terminal dialog) has to provide the pass phrase on +# stdout. +SSLPassPhraseDialog builtin + +## Inter-Process Session Cache: +# Configure the SSL Session Cache: First the mechanism to use and second the +# expiring timeout (in seconds). +#SSLSessionCache dbm:/run/ssl_scache +SSLSessionCache shmcb:/run/ssl_scache(512000) +SSLSessionCacheTimeout 300 + +## Semaphore: +# Configure the path to the mutual exclusion semaphore the SSL engine uses +# internally for inter-process synchronization. +Mutex file:/run/apache_ssl_mutex ssl-cache + +## SSL Compression: +# Known to be vulnerable thus disabled by default (bug #507324). +SSLCompression off + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf new file mode 100644 index 000000000..e4c9454e0 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf @@ -0,0 +1,9 @@ + + + # enable debugging for this module + #LogLevel http2:info + + #Enable HTTP/2 support + Protocols h2 h2c http/1.1 + + diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf new file mode 100644 index 000000000..36f6b9cca --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf @@ -0,0 +1,19 @@ + +DavLockDB "/var/lib/dav/lockdb" + +# The following directives disable redirects on non-GET requests for +# a directory that does not include the trailing slash. This fixes a +# problem with several clients that do not appropriately handle +# redirects for folders with DAV methods. + +BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully +BrowserMatch "MS FrontPage" redirect-carefully +BrowserMatch "^WebDrive" redirect-carefully +BrowserMatch "^WebDAVFS/1.[012345678]" redirect-carefully +BrowserMatch "^gnome-vfs/1.0" redirect-carefully +BrowserMatch "^XML Spy" redirect-carefully +BrowserMatch "^Dreamweaver-WebDAV-SCM1" redirect-carefully + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf new file mode 100644 index 000000000..883061fee --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf @@ -0,0 +1,18 @@ +# Examples below are taken from the online documentation +# Refer to: +# http://localhost/manual/mod/mod_ldap.html +# http://localhost/manual/mod/mod_auth_ldap.html + +LDAPSharedCacheSize 200000 +LDAPCacheEntries 1024 +LDAPCacheTTL 600 +LDAPOpCacheEntries 1024 +LDAPOpCacheTTL 600 + + + SetHandler ldap-status + Require local + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/.keep_www-servers_apache-2 b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/.keep_www-servers_apache-2 new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf new file mode 100644 index 000000000..bb395473c --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf @@ -0,0 +1,191 @@ + + + +# see bug #178966 why this is in here + +# When we also provide SSL we have to listen to the HTTPS port +# Note: Configurations that use IPv6 but not IPv4-mapped addresses need two +# Listen directives: "Listen [::]:443" and "Listen 0.0.0.0:443" +Listen 443 + + + ServerName localhost + Include /etc/apache2/vhosts.d/default_vhost.include + ErrorLog /var/log/apache2/ssl_error_log + + + TransferLog /var/log/apache2/ssl_access_log + + + ## SSL Engine Switch: + # Enable/Disable SSL for this virtual host. + SSLEngine on + + ## SSLProtocol: + # Don't use SSLv2 anymore as it's considered to be broken security-wise. + # Also disable SSLv3 as most modern browsers are capable of TLS. + SSLProtocol ALL -SSLv2 -SSLv3 + + ## SSL Cipher Suite: + # List the ciphers that the client is permitted to negotiate. + # See the mod_ssl documentation for a complete list. + # This list of ciphers is recommended by mozilla and was stripped off + # its RC4 ciphers. (bug #506924) + SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:HIGH:!RC4:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK + + ## SSLHonorCipherOrder: + # Prefer the server's cipher preference order as the client may have a + # weak default order. + SSLHonorCipherOrder On + + ## Server Certificate: + # Point SSLCertificateFile at a PEM encoded certificate. If the certificate + # is encrypted, then you will be prompted for a pass phrase. Note that a + # kill -HUP will prompt again. Keep in mind that if you have both an RSA + # and a DSA certificate you can configure both in parallel (to also allow + # the use of DSA ciphers, etc.) + SSLCertificateFile /etc/ssl/apache2/server.crt + + ## Server Private Key: + # If the key is not combined with the certificate, use this directive to + # point at the key file. Keep in mind that if you've both a RSA and a DSA + # private key you can configure both in parallel (to also allow the use of + # DSA ciphers, etc.) + SSLCertificateKeyFile /etc/ssl/apache2/server.key + + ## Server Certificate Chain: + # Point SSLCertificateChainFile at a file containing the concatenation of + # PEM encoded CA certificates which form the certificate chain for the + # server certificate. Alternatively the referenced file can be the same as + # SSLCertificateFile when the CA certificates are directly appended to the + # server certificate for convinience. + #SSLCertificateChainFile /etc/ssl/apache2/ca.crt + + ## Certificate Authority (CA): + # Set the CA certificate verification path where to find CA certificates + # for client authentication or alternatively one huge file containing all + # of them (file must be PEM encoded). + # Note: Inside SSLCACertificatePath you need hash symlinks to point to the + # certificate files. Use the provided Makefile to update the hash symlinks + # after changes. + #SSLCACertificatePath /etc/ssl/apache2/ssl.crt + #SSLCACertificateFile /etc/ssl/apache2/ca-bundle.crt + + ## Certificate Revocation Lists (CRL): + # Set the CA revocation path where to find CA CRLs for client authentication + # or alternatively one huge file containing all of them (file must be PEM + # encoded). + # Note: Inside SSLCARevocationPath you need hash symlinks to point to the + # certificate files. Use the provided Makefile to update the hash symlinks + # after changes. + #SSLCARevocationPath /etc/ssl/apache2/ssl.crl + #SSLCARevocationFile /etc/ssl/apache2/ca-bundle.crl + + ## Client Authentication (Type): + # Client certificate verification type and depth. Types are none, optional, + # require and optional_no_ca. Depth is a number which specifies how deeply + # to verify the certificate issuer chain before deciding the certificate is + # not valid. + #SSLVerifyClient require + #SSLVerifyDepth 10 + + ## Access Control: + # With SSLRequire you can do per-directory access control based on arbitrary + # complex boolean expressions containing server variable checks and other + # lookup directives. The syntax is a mixture between C and Perl. See the + # mod_ssl documentation for more details. + # + # #SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ + # and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ + # and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ + # and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ + # and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ + # or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ + # + + ## SSL Engine Options: + # Set various options for the SSL engine. + + ## FakeBasicAuth: + # Translate the client X.509 into a Basic Authorisation. This means that the + # standard Auth/DBMAuth methods can be used for access control. The user + # name is the `one line' version of the client's X.509 certificate. + # Note that no password is obtained from the user. Every entry in the user + # file needs this password: `xxj31ZMTZzkVA'. + + ## ExportCertData: + # This exports two additional environment variables: SSL_CLIENT_CERT and + # SSL_SERVER_CERT. These contain the PEM-encoded certificates of the server + # (always existing) and the client (only existing when client + # authentication is used). This can be used to import the certificates into + # CGI scripts. + + ## StdEnvVars: + # This exports the standard SSL/TLS related `SSL_*' environment variables. + # Per default this exportation is switched off for performance reasons, + # because the extraction step is an expensive operation and is usually + # useless for serving static content. So one usually enables the exportation + # for CGI and SSI requests only. + + ## StrictRequire: + # This denies access when "SSLRequireSSL" or "SSLRequire" applied even under + # a "Satisfy any" situation, i.e. when it applies access is denied and no + # other module can change it. + + ## OptRenegotiate: + # This enables optimized SSL connection renegotiation handling when SSL + # directives are used in per-directory context. + #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + + SSLOptions +StdEnvVars + + + ## SSL Protocol Adjustments: + # The safe and default but still SSL/TLS standard compliant shutdown + # approach is that mod_ssl sends the close notify alert but doesn't wait + # for the close notify alert from client. When you need a different + # shutdown approach you can use one of the following variables: + + ## ssl-unclean-shutdown: + # This forces an unclean shutdown when the connection is closed, i.e. no + # SSL close notify alert is send or allowed to received. This violates the + # SSL/TLS standard but is needed for some brain-dead browsers. Use this when + # you receive I/O errors because of the standard approach where mod_ssl + # sends the close notify alert. + + ## ssl-accurate-shutdown: + # This forces an accurate shutdown when the connection is closed, i.e. a + # SSL close notify alert is send and mod_ssl waits for the close notify + # alert of the client. This is 100% SSL/TLS standard compliant, but in + # practice often causes hanging connections with brain-dead browsers. Use + # this only for browsers where you know that their SSL implementation works + # correctly. + # Notice: Most problems of broken clients are also related to the HTTP + # keep-alive facility, so you usually additionally want to disable + # keep-alive for those clients, too. Use variable "nokeepalive" for this. + # Similarly, one has to force some clients to use HTTP/1.0 to workaround + # their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and + # "force-response-1.0" for this. + + BrowserMatch ".*MSIE.*" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + + + ## Per-Server Logging: + # The home of a custom SSL log file. Use this when you want a compact + # non-error SSL logfile on a virtual host basis. + + CustomLog /var/log/apache2/ssl_request_log \ + "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf new file mode 100644 index 000000000..b9766b5f1 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf @@ -0,0 +1,45 @@ +# Virtual Hosts +# +# If you want to maintain multiple domains/hostnames on your +# machine you can setup VirtualHost containers for them. Most configurations +# use only name-based virtual hosts so the server doesn't need to worry about +# IP addresses. This is indicated by the asterisks in the directives below. +# +# Please see the documentation at +# +# for further details before you try to setup virtual hosts. +# +# You may use the command line option '-S' to verify your virtual host +# configuration. + + +# see bug #178966 why this is in here + +# Listen: Allows you to bind Apache to specific IP addresses and/or +# ports, instead of the default. See also the +# directive. +# +# Change this to Listen on specific IP addresses as shown below to +# prevent Apache from glomming onto all bound IP addresses. +# +#Listen 12.34.56.78:80 +Listen 80 + +# When virtual hosts are enabled, the main host defined in the default +# httpd.conf configuration will go away. We redefine it here so that it is +# still available. +# +# If you disable this vhost by removing -D DEFAULT_VHOST from +# /etc/conf.d/apache2, the first defined virtual host elsewhere will be +# the default. + + ServerName localhost + Include /etc/apache2/vhosts.d/default_vhost.include + + + ServerEnvironment apache apache + + + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include new file mode 100644 index 000000000..af6ece85b --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include @@ -0,0 +1,71 @@ +# ServerAdmin: Your address, where problems with the server should be +# e-mailed. This address appears on some server-generated pages, such +# as error documents. e.g. admin@your-domain.com +ServerAdmin root@localhost + +# DocumentRoot: The directory out of which you will serve your +# documents. By default, all requests are taken from this directory, but +# symbolic links and aliases may be used to point to other locations. +# +# If you change this to something that isn't under /var/www then suexec +# will no longer work. +DocumentRoot "/var/www/localhost/htdocs" + +# This should be changed to whatever you set DocumentRoot to. + + # Possible values for the Options directive are "None", "All", + # or any combination of: + # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews + # + # Note that "MultiViews" must be named *explicitly* --- "Options All" + # doesn't give it to you. + # + # The Options directive is both complicated and important. Please see + # http://httpd.apache.org/docs/2.4/mod/core.html#options + # for more information. + Options Indexes FollowSymLinks + + # AllowOverride controls what directives may be placed in .htaccess files. + # It can be "All", "None", or any combination of the keywords: + # Options FileInfo AuthConfig Limit + AllowOverride All + + # Controls who can get stuff from this server. + Require all granted + + + + # Redirect: Allows you to tell clients about documents that used to + # exist in your server's namespace, but do not anymore. The client + # will make a new request for the document at its new location. + # Example: + # Redirect permanent /foo http://www.example.com/bar + + # Alias: Maps web paths into filesystem paths and is used to + # access content that does not live under the DocumentRoot. + # Example: + # Alias /webpath /full/filesystem/path + # + # If you include a trailing / on /webpath then the server will + # require it to be present in the URL. You will also likely + # need to provide a section to allow access to + # the filesystem path. + + # ScriptAlias: This controls which directories contain server scripts. + # ScriptAliases are essentially the same as Aliases, except that + # documents in the target directory are treated as applications and + # run by the server when requested rather than as documents sent to the + # client. The same rules about trailing "/" apply to ScriptAlias + # directives as to Alias. + ScriptAlias /cgi-bin/ "/var/www/localhost/cgi-bin/" + + +# "/var/www/localhost/cgi-bin" should be changed to whatever your ScriptAliased +# CGI directory exists, if you have that configured. + + AllowOverride None + Options None + Require all granted + + +# vim: ts=4 filetype=apache diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf new file mode 100644 index 000000000..41de4d236 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf @@ -0,0 +1,7 @@ + + ServerName gentoo.example.com + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/conf.d/apache2 b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/conf.d/apache2 new file mode 100644 index 000000000..b7ecb4f2a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/conf.d/apache2 @@ -0,0 +1,74 @@ +# /etc/conf.d/apache2: config file for /etc/init.d/apache2 + +# When you install a module it is easy to activate or deactivate the modules +# and other features of apache using the APACHE2_OPTS line. Every module should +# install a configuration in /etc/apache2/modules.d. In that file will have an +# directive where NNN is the option to enable that module. +# +# Here are the options available in the default configuration: +# +# AUTH_DIGEST Enables mod_auth_digest +# AUTHNZ_LDAP Enables authentication through mod_ldap (available if USE=ldap) +# CACHE Enables mod_cache +# DAV Enables mod_dav +# ERRORDOCS Enables default error documents for many languages. +# INFO Enables mod_info, a useful module for debugging +# LANGUAGE Enables content-negotiation based on language and charset. +# LDAP Enables mod_ldap (available if USE=ldap) +# MANUAL Enables /manual/ to be the apache manual (available if USE=docs) +# MEM_CACHE Enables default configuration mod_mem_cache +# PROXY Enables mod_proxy +# SSL Enables SSL (available if USE=ssl) +# STATUS Enabled mod_status, a useful module for statistics +# SUEXEC Enables running CGI scripts (in USERDIR) through suexec. +# USERDIR Enables /~username mapping to /home/username/public_html +# +# +# The following two options provide the default virtual host for the HTTP and +# HTTPS protocol. YOU NEED TO ENABLE AT LEAST ONE OF THEM, otherwise apache +# will not listen for incomming connections on the approriate port. +# +# DEFAULT_VHOST Enables name-based virtual hosts, with the default +# virtual host being in /var/www/localhost/htdocs +# SSL_DEFAULT_VHOST Enables default vhost for SSL (you should enable this +# when you enable SSL) +# +APACHE2_OPTS="-D DEFAULT_VHOST -D INFO -D SSL -D SSL_DEFAULT_VHOST -D LANGUAGE" + +# Extended options for advanced uses of Apache ONLY +# You don't need to edit these unless you are doing crazy Apache stuff +# As not having them set correctly, or feeding in an incorrect configuration +# via them will result in Apache failing to start +# YOU HAVE BEEN WARNED. + +# PID file +#PIDFILE=/var/run/apache2.pid + +# timeout for startup/shutdown checks +#TIMEOUT=10 + +# ServerRoot setting +#SERVERROOT=/usr/lib64/apache2 + +# Configuration file location +# - If this does NOT start with a '/', then it is treated relative to +# $SERVERROOT by Apache +#CONFIGFILE=/etc/apache2/httpd.conf + +# Location to log startup errors to +# They are normally dumped to your terminal. +#STARTUPERRORLOG="/var/log/apache2/startuperror.log" + +# A command that outputs a formatted text version of the HTML at the URL +# of the command line. Designed for lynx, however other programs may work. +#LYNX="lynx -dump" + +# The URL to your server's mod_status status page. +# Required for status and fullstatus +#STATUSURL="http://localhost/server-status" + +# Method to use when reloading the server +# Valid options are 'restart' and 'graceful' +# See http://httpd.apache.org/docs/2.2/stopping.html for information on +# what they do and how they differ. +#RELOAD_TYPE="graceful" diff --git a/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/sites b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/sites new file mode 100644 index 000000000..7f0b3a8b3 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/gentoo_apache/apache/sites @@ -0,0 +1,3 @@ +vhosts.d/gentoo.example.com.conf, gentoo.example.com +vhosts.d/00_default_vhost.conf, localhost +vhosts.d/00_default_ssl_vhost.conf, localhost diff --git a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py index 62464d5d0..6c37c2ecc 100644 --- a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py +++ b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py @@ -23,7 +23,8 @@ class TlsSniPerformTest(util.ApacheTest): super(TlsSniPerformTest, self).setUp() config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir) + self.config_path, self.vhost_path, self.config_dir, + self.work_dir) config.config.tls_sni_01_port = 443 from certbot_apache import tls_sni_01 @@ -41,8 +42,8 @@ class TlsSniPerformTest(util.ApacheTest): @mock.patch("certbot.util.exe_exists") @mock.patch("certbot.util.run_script") def test_perform1(self, _, mock_exists): - mock_register = mock.Mock() - self.sni.configurator.reverter.register_undo_command = mock_register + self.sni.configurator.parser.modules.add("socache_shmcb_module") + self.sni.configurator.parser.modules.add("ssl_module") mock_exists.return_value = True self.sni.configurator.parser.update_runtime_variables = mock.Mock() @@ -55,10 +56,6 @@ class TlsSniPerformTest(util.ApacheTest): self.sni._setup_challenge_cert = mock_setup_cert responses = self.sni.perform() - - # Make sure that register_undo_command was called into temp directory. - self.assertEqual(True, mock_register.call_args[0][0]) - mock_setup_cert.assert_called_once_with(achall) # Check to make sure challenge config path is included in apache config @@ -71,7 +68,7 @@ class TlsSniPerformTest(util.ApacheTest): def test_perform2(self): # Avoid load module self.sni.configurator.parser.modules.add("ssl_module") - + self.sni.configurator.parser.modules.add("socache_shmcb_module") acme_responses = [] for achall in self.achalls: self.sni.add_chall(achall) @@ -81,7 +78,8 @@ class TlsSniPerformTest(util.ApacheTest): # pylint: disable=protected-access self.sni._setup_challenge_cert = mock_setup_cert - with mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod"): + with mock.patch( + "certbot_apache.override_debian.DebianConfigurator.enable_mod"): sni_responses = self.sni.perform() self.assertEqual(mock_setup_cert.call_count, 2) diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 34d2476f7..2405110c5 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -1,5 +1,6 @@ """Common utilities for certbot_apache.""" import os +import shutil import sys import unittest @@ -16,7 +17,7 @@ from certbot.plugins import common from certbot.tests import util as test_util from certbot_apache import configurator -from certbot_apache import constants +from certbot_apache import entrypoint from certbot_apache import obj @@ -38,6 +39,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods self.rsa512jwk = jose.JWKRSA.load(test_util.load_vector( "rsa512_key.pem")) + self.config = get_apache_configurator(self.config_path, vhost_root, + self.config_dir, self.work_dir) + # Make sure all vhosts in sites-enabled are symlinks (Python packaging # does not preserve symlinks) sites_enabled = os.path.join(self.config_path, "sites-enabled") @@ -55,8 +59,13 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods os.path.pardir, "sites-available", vhost_basename) os.symlink(target, vhost) + def tearDown(self): + shutil.rmtree(self.temp_dir) + shutil.rmtree(self.config_dir) + shutil.rmtree(self.work_dir) -class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods + +class ParserTest(ApacheTest): def setUp(self, test_dir="debian_apache_2_4/multiple_vhosts", config_root="debian_apache_2_4/multiple_vhosts/apache2", @@ -72,12 +81,16 @@ class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods with mock.patch("certbot_apache.parser.ApacheParser." "update_runtime_variables"): self.parser = ApacheParser( - self.aug, self.config_path, self.vhost_path) + self.aug, self.config_path, self.vhost_path, + configurator=self.config) -def get_apache_configurator( +def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-locals config_path, vhost_path, - config_dir, work_dir, version=(2, 4, 7), conf=None): + config_dir, work_dir, version=(2, 4, 7), + conf=None, + os_info="generic", + conf_vhost_path=None): """Create an Apache Configurator with the specified options. :param conf: Function that returns binary paths. self.conf in Configurator @@ -86,8 +99,8 @@ def get_apache_configurator( backups = os.path.join(work_dir, "backups") mock_le_config = mock.MagicMock( apache_server_root=config_path, - apache_vhost_root=vhost_path, - apache_le_vhost_ext=constants.os_constant("le_vhost_ext"), + apache_vhost_root=conf_vhost_path, + apache_le_vhost_ext="-le-ssl.conf", apache_challenge_location=config_path, backup_dir=backups, config_dir=config_dir, @@ -95,22 +108,37 @@ def get_apache_configurator( in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) - with mock.patch("certbot_apache.configurator.util.run_script"): - with mock.patch("certbot_apache.configurator.util." - "exe_exists") as mock_exe_exists: - mock_exe_exists.return_value = True - with mock.patch("certbot_apache.parser.ApacheParser." - "update_runtime_variables"): - config = configurator.ApacheConfigurator( - config=mock_le_config, - name="apache", - version=version) - # This allows testing scripts to set it a bit more quickly - if conf is not None: - config.conf = conf # pragma: no cover + orig_os_constant = configurator.ApacheConfigurator(mock_le_config, + name="apache", + version=version).constant - config.prepare() + def mock_os_constant(key, vhost_path=vhost_path): + """Mock default vhost path""" + if key == "vhost_root": + return vhost_path + else: + return orig_os_constant(key) + with mock.patch("certbot_apache.configurator.ApacheConfigurator.constant") as mock_cons: + mock_cons.side_effect = mock_os_constant + with mock.patch("certbot_apache.configurator.util.run_script"): + with mock.patch("certbot_apache.configurator.util." + "exe_exists") as mock_exe_exists: + mock_exe_exists.return_value = True + with mock.patch("certbot_apache.parser.ApacheParser." + "update_runtime_variables"): + try: + config_class = entrypoint.OVERRIDE_CLASSES[os_info] + except KeyError: + config_class = configurator.ApacheConfigurator + config = config_class(config=mock_le_config, name="apache", + version=version) + # This allows testing scripts to set it a bit more + # quickly + if conf is not None: + config.conf = conf # pragma: no cover + + config.prepare() return config diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index b276f49f8..8dc283f2d 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -63,7 +63,7 @@ setup( }, entry_points={ 'certbot.plugins': [ - 'apache = certbot_apache.configurator:ApacheConfigurator', + 'apache = certbot_apache.entrypoint:ENTRYPOINT', ], }, test_suite='certbot_apache', diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 2e9e68daf..1d2cfdeca 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -9,8 +9,7 @@ import zope.interface from certbot import configuration from certbot import errors as le_errors from certbot import util as certbot_util -from certbot_apache import configurator -from certbot_apache import constants +from certbot_apache import entrypoint from certbot_compatibility_test import errors from certbot_compatibility_test import interfaces from certbot_compatibility_test import util @@ -56,13 +55,14 @@ class Proxy(configurators_common.Proxy): def _prepare_configurator(self): """Prepares the Apache plugin for testing""" - for k in constants.CLI_DEFAULTS_DEBIAN.keys(): - setattr(self.le_config, "apache_" + k, constants.os_constant(k)) + for k in entrypoint.ENTRYPOINT.OS_DEFAULTS.keys(): + setattr(self.le_config, "apache_" + k, + entrypoint.ENTRYPOINT.OS_DEFAULTS[k]) # An alias self.le_config.apache_handle_modules = self.le_config.apache_handle_mods - self._configurator = configurator.ApacheConfigurator( + self._configurator = entrypoint.ENTRYPOINT( config=configuration.NamespaceConfig(self.le_config), name="apache") self._configurator.prepare() diff --git a/certbot/util.py b/certbot/util.py index 30de0c157..b7e60a225 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -342,9 +342,9 @@ def get_os_info_ua(filepath="/etc/os-release"): """ if os.path.isfile(filepath): - os_ua = _get_systemd_os_release_var("PRETTY_NAME", filepath=filepath) + os_ua = get_var_from_file("PRETTY_NAME", filepath=filepath) if not os_ua: - os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + os_ua = get_var_from_file("NAME", filepath=filepath) if os_ua: return os_ua @@ -361,8 +361,8 @@ def get_systemd_os_info(filepath="/etc/os-release"): :rtype: `tuple` of `str` """ - os_name = _get_systemd_os_release_var("ID", filepath=filepath) - os_version = _get_systemd_os_release_var("VERSION_ID", filepath=filepath) + os_name = get_var_from_file("ID", filepath=filepath) + os_version = get_var_from_file("VERSION_ID", filepath=filepath) return (os_name, os_version) @@ -377,10 +377,10 @@ def get_systemd_os_like(filepath="/etc/os-release"): :rtype: `list` of `str` """ - return _get_systemd_os_release_var("ID_LIKE", filepath).split(" ") + return get_var_from_file("ID_LIKE", filepath).split(" ") -def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): +def get_var_from_file(varname, filepath="/etc/os-release"): """ Get single value from systemd /etc/os-release @@ -405,7 +405,7 @@ def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): def _normalize_string(orig): """ - Helper function for _get_systemd_os_release_var() to remove quotes + Helper function for get_var_from_file() to remove quotes and whitespaces """ return orig.replace('"', '').replace("'", "").strip() From bb70962bb8f7f4bedb5e31c47fad63f30a9eb952 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 4 Dec 2017 14:44:22 -0800 Subject: [PATCH 25/67] Stop using new mock functionality in tests (#5295) * Remove assert_called_once from dns-route53 * Remove assert_called_once from main_test.py * Remove assert_called() usage in dns-digitalocean * Remove assert_called() usage in dns-route53 * Downgrade mock version in certbot-auto --- .../certbot_dns_digitalocean/dns_digitalocean_test.py | 2 +- .../certbot_dns_route53/dns_route53_test.py | 7 ++++--- certbot/tests/main_test.py | 10 +++++----- letsencrypt-auto-source/letsencrypt-auto | 7 ++++--- .../pieces/dependency-requirements.txt | 7 ++++--- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py index 0fdacf4ad..3b8edce64 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py @@ -131,7 +131,7 @@ class DigitalOceanClientTest(unittest.TestCase): self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - correct_record_mock.destroy.assert_called() + self.assertTrue(correct_record_mock.destroy.called) self.assertFalse(first_record_mock.destroy.call_args_list) self.assertFalse(last_record_mock.destroy.call_args_list) diff --git a/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py index ff07b6ccd..d5f1b2816 100644 --- a/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py +++ b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py @@ -31,7 +31,7 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest self.auth._change_txt_record.assert_called_once_with("UPSERT", '_acme-challenge.' + DOMAIN, mock.ANY) - self.auth._wait_for_change.assert_called_once() + self.assertEqual(self.auth._wait_for_change.call_count, 1) def test_perform_no_credentials_error(self): self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) @@ -183,7 +183,8 @@ class ClientTest(unittest.TestCase): self.client._change_txt_record("FOO", DOMAIN, "foo") - self.client.r53.change_resource_record_sets.assert_called_once() + call_count = self.client.r53.change_resource_record_sets.call_count + self.assertEqual(call_count, 1) def test_wait_for_change(self): self.client.r53.get_change = mock.MagicMock( @@ -192,7 +193,7 @@ class ClientTest(unittest.TestCase): self.client._wait_for_change(1) - self.client.r53.get_change.assert_called() + self.assertTrue(self.client.r53.get_change.called) if __name__ == "__main__": diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 45e5db1df..1f690df26 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -356,7 +356,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_cert_path_for_cert_name.return_value = "/some/reasonable/path" mock_overlapping_archive_dirs.return_value = False self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -375,7 +375,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_cert_path_to_lineage.return_value = "example.com" mock_overlapping_archive_dirs.return_value = False self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -396,7 +396,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_full_archive_dir.return_value = "" mock_match_and_check_overlaps.return_value = "" self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -415,7 +415,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): mock_cert_path_to_lineage.return_value = config.certname mock_overlapping_archive_dirs.return_value = False self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.cert_manager.match_and_check_overlaps') @@ -442,7 +442,7 @@ class DeleteIfAppropriateTest(unittest.TestCase): util_mock = mock_get_utility() util_mock.menu.return_value = (display_util.OK, 0) self._call(config) - mock_delete.assert_called_once() + self.assertEqual(mock_delete.call_count, 1) # pylint: disable=too-many-arguments @mock.patch('certbot.cert_manager.match_and_check_overlaps') diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 215b684cf..21e47feb8 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -1062,9 +1062,10 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 # Contains the requirements for the letsencrypt package. # diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index 4b3da685c..dec7ae7d0 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -184,6 +184,7 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 From 4db7195e7740fc76c1bf27d8d875ef6bdd70eb9a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 4 Dec 2017 17:09:01 -0800 Subject: [PATCH 26/67] Fix coveralls (#5298) --- tox.cover.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tox.cover.sh b/tox.cover.sh index 3f0a5f72e..2b5a3cf19 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -16,7 +16,7 @@ fi cover () { if [ "$1" = "certbot" ]; then - min=97 + min=98 elif [ "$1" = "acme" ]; then min=100 elif [ "$1" = "certbot_apache" ]; then @@ -24,23 +24,23 @@ cover () { elif [ "$1" = "certbot_dns_cloudflare" ]; then min=98 elif [ "$1" = "certbot_dns_cloudxns" ]; then - min=98 + min=99 elif [ "$1" = "certbot_dns_digitalocean" ]; then min=98 elif [ "$1" = "certbot_dns_dnsimple" ]; then min=98 elif [ "$1" = "certbot_dns_dnsmadeeasy" ]; then - min=98 + min=99 elif [ "$1" = "certbot_dns_google" ]; then min=99 elif [ "$1" = "certbot_dns_luadns" ]; then min=98 elif [ "$1" = "certbot_dns_nsone" ]; then - min=98 + min=99 elif [ "$1" = "certbot_dns_rfc2136" ]; then min=99 elif [ "$1" = "certbot_dns_route53" ]; then - min=91 + min=92 elif [ "$1" = "certbot_nginx" ]; then min=97 elif [ "$1" = "letshelp_certbot" ]; then @@ -50,10 +50,12 @@ cover () { exit 1 fi - pytest --cov "$1" --cov-report term-missing \ - --cov-fail-under "$min" --numprocesses auto --pyargs "$1" + pkg_dir=$(echo "$1" | tr _ -) + pytest --cov "$pkg_dir" --cov-append --cov-report= --numprocesses auto --pyargs "$1" + coverage report --fail-under="$min" --include="$pkg_dir/*" --show-missing } +rm -f .coverage # --cov-append is on, make sure stats are correct for pkg in $pkgs do cover $pkg From 8c4f016b2d4524387ce2ddddf0284118eae455b7 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 22 Nov 2017 13:00:29 -0800 Subject: [PATCH 27/67] In ACMEv2, challenges have "url" instead of "uri". To handle this smoothly, Challenge's uri field becomes private (_uri), and is joined by _url. Serialization and deserialization will preserve whichever one was set. The uri name is taken over by an @property that returns whichever of the two is set. I chose not to enforce that they shouldn't both be present because it would just add unnecessary code and brittleness with no stability benefit. * Make url a virtual field. * Add @property annotation. --- acme/acme/client_test.py | 2 +- acme/acme/messages.py | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 4bd762865..3aac9c874 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -181,7 +181,7 @@ class ClientTest(unittest.TestCase): # TODO: split here and separate test self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, - self.challr.body.update(uri='foo'), chall_response) + self.challr.body.update(_uri='foo'), chall_response) def test_answer_challenge_missing_next(self): self.assertRaises( diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 4b4fa5003..4dee96c58 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -325,13 +325,26 @@ class ChallengeBody(ResourceBody): """ __slots__ = ('chall',) - uri = jose.Field('uri') + # ACMEv1 has a "uri" field in challenges. ACMEv2 has a "url" field. This + # challenge object supports either one. In Client.answer_challenge, + # whichever one is set will be used. + _uri = jose.Field('uri', omitempty=True, default=None) + _url = jose.Field('url', omitempty=True, default=None) status = jose.Field('status', decoder=Status.from_json, omitempty=True, default=STATUS_PENDING) validated = fields.RFC3339Field('validated', omitempty=True) error = jose.Field('error', decoder=Error.from_json, omitempty=True, default=None) + def __init__(self, **kwargs): + new_kwargs = {} + for k, v in kwargs.items(): + if k in ('uri', 'url',): + k = '_' + k + new_kwargs[k] = v + # pylint: disable=star-args + super(ChallengeBody, self).__init__(**new_kwargs) + def to_partial_json(self): jobj = super(ChallengeBody, self).to_partial_json() jobj.update(self.chall.to_partial_json()) @@ -343,6 +356,11 @@ class ChallengeBody(ResourceBody): jobj_fields['chall'] = challenges.Challenge.from_json(jobj) return jobj_fields + @property + def uri(self): + """The URL of this challenge.""" + return self._url or self._uri + def __getattr__(self, name): return getattr(self.chall, name) @@ -358,10 +376,10 @@ class ChallengeResource(Resource): authzr_uri = jose.Field('authzr_uri') @property - def uri(self): # pylint: disable=missing-docstring,no-self-argument - # bug? 'method already defined line None' - # pylint: disable=function-redefined - return self.body.uri # pylint: disable=no-member + def uri(self): + """The URL of the challenge body.""" + # pylint: disable=function-redefined,no-member + return self.body.uri class Authorization(ResourceBody): From 62c1112d10927026501ff7c1d6a830d5e4fa9fee Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 4 Dec 2017 20:50:26 -0800 Subject: [PATCH 28/67] Keep the same behavior with the uri attribute --- acme/acme/client_test.py | 2 +- acme/acme/messages.py | 25 +++++++++++++++++-------- acme/acme/messages_test.py | 3 +++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 3aac9c874..4bd762865 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -181,7 +181,7 @@ class ClientTest(unittest.TestCase): # TODO: split here and separate test self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, - self.challr.body.update(_uri='foo'), chall_response) + self.challr.body.update(uri='foo'), chall_response) def test_answer_challenge_missing_next(self): self.assertRaises( diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 4dee96c58..2ac29941e 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -326,8 +326,9 @@ class ChallengeBody(ResourceBody): """ __slots__ = ('chall',) # ACMEv1 has a "uri" field in challenges. ACMEv2 has a "url" field. This - # challenge object supports either one. In Client.answer_challenge, - # whichever one is set will be used. + # challenge object supports either one, but should be accessed through the + # name "uri". In Client.answer_challenge, whichever one is set will be + # used. _uri = jose.Field('uri', omitempty=True, default=None) _url = jose.Field('url', omitempty=True, default=None) status = jose.Field('status', decoder=Status.from_json, @@ -337,13 +338,12 @@ class ChallengeBody(ResourceBody): omitempty=True, default=None) def __init__(self, **kwargs): - new_kwargs = {} - for k, v in kwargs.items(): - if k in ('uri', 'url',): - k = '_' + k - new_kwargs[k] = v + kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items()) # pylint: disable=star-args - super(ChallengeBody, self).__init__(**new_kwargs) + super(ChallengeBody, self).__init__(**kwargs) + + def encode(self, name): + return super(ChallengeBody, self).encode(self._internal_name(name)) def to_partial_json(self): jobj = super(ChallengeBody, self).to_partial_json() @@ -364,6 +364,15 @@ class ChallengeBody(ResourceBody): def __getattr__(self, name): return getattr(self.chall, name) + def __iter__(self): + # When iterating over fields, use the external name 'uri' instead of + # the internal '_uri'. + for name in super(ChallengeBody, self).__iter__(): + yield name[1:] if name == '_uri' else name + + def _internal_name(self, name): + return '_' + name if name == 'uri' else name + class ChallengeResource(Resource): """Challenge Resource. diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 631f0ce4d..c9e5c2cf1 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -283,6 +283,9 @@ class ChallengeBodyTest(unittest.TestCase): 'detail': 'Unable to communicate with DNS server', } + def test_encode(self): + self.assertEqual(self.challb.encode('uri'), self.challb.uri) + def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) From c9949411cdc5a058d8114a430d98b45c80384650 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 5 Dec 2017 20:04:08 -0800 Subject: [PATCH 29/67] Nginx reversion (#5299) The reason for this PR is many bug fixes in the nginx plugin for changes we haven't released yet are included in #5220 which may not make our next release. If it doesn't, we will (mostly) revert the nginx plugin back to its previous state to avoid releasing these bugs and will revert this PR after the release. * Revert "Nginx IPv6 support (#5178)" This reverts commit 68e37b03c821560e7f316d260f8da97ef3e2087c. * Revert "Fix bug that stopped nginx from finding new server block for redirect (#5198)" This reverts commit e2ab940ac03ffe4f2cf7c478a1597c0b52f14bc4. * Revert "Nginx creates a vhost block if no matching block is found (#5153)" This reverts commit 95a7d4585619d612ff28ac24dac4faefaee59e72. --- certbot-nginx/certbot_nginx/configurator.py | 122 ++----------- certbot-nginx/certbot_nginx/nginxparser.py | 20 +-- certbot-nginx/certbot_nginx/obj.py | 55 ++---- certbot-nginx/certbot_nginx/parser.py | 33 +--- .../certbot_nginx/tests/configurator_test.py | 163 +----------------- .../certbot_nginx/tests/parser_test.py | 56 ++---- .../testdata/etc_nginx/sites-enabled/default | 1 - .../testdata/etc_nginx/sites-enabled/ipv6.com | 5 - .../etc_nginx/sites-enabled/ipv6ssl.com | 5 - .../certbot_nginx/tests/tls_sni_01_test.py | 10 +- certbot-nginx/certbot_nginx/tls_sni_01.py | 32 ++-- certbot/plugins/common.py | 2 +- 12 files changed, 68 insertions(+), 436 deletions(-) delete mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com delete mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 98990664f..fe27dbc4b 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -117,9 +117,6 @@ class NginxConfigurator(common.Installer): # Files to save self.save_notes = "" - # For creating new vhosts if no names match - self.new_vhost = None - # Add number of outstanding challenges self._chall_out = 0 @@ -194,11 +191,9 @@ class NginxConfigurator(common.Installer): "The nginx plugin currently requires --fullchain-path to " "install a cert.") - vhost = self.choose_vhost(domain, raise_if_no_match=False) - if vhost is None: - vhost = self._vhost_from_duplicated_default(domain) - cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path], - ['\n ', 'ssl_certificate_key', ' ', key_path]] + vhost = self.choose_vhost(domain) + cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path], + ['\n', 'ssl_certificate_key', ' ', key_path]] self.parser.add_server_directives(vhost, cert_directives, replace=True) @@ -214,7 +209,7 @@ class NginxConfigurator(common.Installer): ####################### # Vhost parsing methods ####################### - def choose_vhost(self, target_name, raise_if_no_match=True): + def choose_vhost(self, target_name): """Chooses a virtual host based on the given domain name. .. note:: This makes the vhost SSL-enabled if it isn't already. Follows @@ -228,8 +223,6 @@ class NginxConfigurator(common.Installer): hostname. Currently we just ignore this. :param str target_name: domain name - :param bool raise_if_no_match: True iff not finding a match is an error; - otherwise, return None :returns: ssl vhost associated with name :rtype: :class:`~certbot_nginx.obj.VirtualHost` @@ -240,16 +233,13 @@ class NginxConfigurator(common.Installer): matches = self._get_ranked_matches(target_name) vhost = self._select_best_name_match(matches) if not vhost: - if raise_if_no_match: - # 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: " - "https://nginx.org/en/docs/http/server_names.html") % (target_name)) - else: - return None + # 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: " + "https://nginx.org/en/docs/http/server_names.html") % (target_name)) else: # Note: if we are enhancing with ocsp, vhost should already be ssl. if not vhost.ssl: @@ -257,65 +247,6 @@ class NginxConfigurator(common.Installer): return vhost - - 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) - """ - 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): - if self.new_vhost is None: - default_vhost = self._get_default_vhost() - self.new_vhost = self.parser.create_new_vhost_from_default(default_vhost) - if not self.new_vhost.ssl: - self._make_server_ssl(self.new_vhost) - self.new_vhost.names = set() - - self.new_vhost.names.add(domain) - name_block = [['\n ', 'server_name']] - for name in self.new_vhost.names: - name_block[0].append(' ') - name_block[0].append(name) - self.parser.add_server_directives(self.new_vhost, name_block, replace=True) - return self.new_vhost - - def _get_default_vhost(self): - vhost_list = self.parser.get_vhosts() - # if one has default_server set, return that one - default_vhosts = [] - for vhost in vhost_list: - for addr in vhost.addrs: - if addr.default: - default_vhosts.append(vhost) - break - - if len(default_vhosts) == 1: - return 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. Set the `server_name` directive to use the Nginx installer.") - def _get_ranked_matches(self, target_name): """Returns a ranked list of vhosts that match target_name. The ranking gives preference to SSL vhosts. @@ -474,12 +405,9 @@ class NginxConfigurator(common.Installer): all_names.add(host) elif not common.private_ips_regex.match(host): # If it isn't a private IP, do a reverse DNS lookup + # TODO: IPv6 support try: - if addr.ipv6: - host = addr.get_ipv6_exploded() - socket.inet_pton(socket.AF_INET6, host) - else: - socket.inet_pton(socket.AF_INET, host) + socket.inet_aton(host) all_names.add(socket.gethostbyaddr(host)[0]) except (socket.error, socket.herror, socket.timeout): continue @@ -515,38 +443,16 @@ class NginxConfigurator(common.Installer): :type vhost: :class:`~certbot_nginx.obj.VirtualHost` """ - ipv6info = self.ipv6_info(self.config.tls_sni_01_port) - ipv6_block = [''] - ipv4_block = [''] - # If the vhost was implicitly listening on the default Nginx port, # have it continue to do so. if len(vhost.addrs) == 0: listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]] self.parser.add_server_directives(vhost, listen_block, replace=False) - if vhost.ipv6_enabled(): - ipv6_block = ['\n ', - 'listen', - ' ', - '[::]:{0} ssl'.format(self.config.tls_sni_01_port)] - 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} ssl'.format(self.config.tls_sni_01_port)] - - snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() ssl_block = ([ - ipv6_block, - ipv4_block, + ['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], ['\n ', 'ssl_certificate', ' ', snakeoil_cert], ['\n ', 'ssl_certificate_key', ' ', snakeoil_key], ['\n ', 'include', ' ', self.mod_ssl_conf], diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 14481e298..20aeeb554 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -7,7 +7,6 @@ from pyparsing import ( Literal, White, Forward, Group, Optional, OneOrMore, QuotedString, Regex, ZeroOrMore, Combine) from pyparsing import stringEnd from pyparsing import restOfLine -import six logger = logging.getLogger(__name__) @@ -72,7 +71,7 @@ class RawNginxDumper(object): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks for b0 in blocks: - if isinstance(b0, six.string_types): + if isinstance(b0, str): yield b0 continue item = copy.deepcopy(b0) @@ -89,7 +88,7 @@ class RawNginxDumper(object): yield '}' else: # not a block - list of strings semicolon = ";" - if isinstance(item[0], six.string_types) and item[0].strip() == '#': # comment + if isinstance(item[0], str) and item[0].strip() == '#': # comment semicolon = "" yield "".join(item) + semicolon @@ -146,7 +145,7 @@ def dump(blocks, _file): return _file.write(dumps(blocks)) -spacey = lambda x: (isinstance(x, six.string_types) and x.isspace()) or x == '' +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""" @@ -190,15 +189,13 @@ class UnspacedList(list): 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) + 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) + list.append(self, item) self.dirty = True def extend(self, x): @@ -229,8 +226,7 @@ class UnspacedList(list): 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) + list.__setitem__(self, i, item) self.dirty = True def __delitem__(self, i): @@ -239,8 +235,8 @@ class UnspacedList(list): self.dirty = True def __deepcopy__(self, memo): - new_spaced = copy.deepcopy(self.spaced, memo=memo) - l = UnspacedList(new_spaced) + l = UnspacedList(self[:]) + l.spaced = copy.deepcopy(self.spaced, memo=memo) l.dirty = self.dirty return l diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 5816c5571..849cefe1f 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -34,13 +34,10 @@ class Addr(common.Addr): UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0') CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0] - def __init__(self, host, port, ssl, default, ipv6, ipv6only): - # pylint: disable=too-many-arguments + def __init__(self, host, port, ssl, default): super(Addr, self).__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 @@ -49,8 +46,6 @@ class Addr(common.Addr): parts = str_addr.split(' ') ssl = False default = False - ipv6 = False - ipv6only = False host = '' port = '' @@ -61,25 +56,15 @@ class Addr(common.Addr): 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:] + 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: - # 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] + # 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 len(parts) > 0: @@ -88,10 +73,8 @@ class Addr(common.Addr): ssl = True elif nextpart == 'default_server': default = True - elif nextpart == "ipv6only=on": - ipv6only = True - return cls(host, port, ssl, default, ipv6, ipv6only) + return cls(host, port, ssl, default) def to_string(self, include_default=True): """Return string representation of Addr""" @@ -131,6 +114,8 @@ class Addr(common.Addr): self.tup[1]), self.ipv6) == \ common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS, other.tup[1]), other.ipv6) + # Nginx plugin currently doesn't support IPv6 but this will + # future-proof it return super(Addr, self).__eq__(other) def __eq__(self, other): @@ -210,24 +195,10 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods 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 - - def ipv4_enabled(self): - """Return true if one or more of the listen directives in vhost are IPv4 - only""" - for a in self.addrs: - if not a.ipv6: - return True - def _find_directive(directives, directive_name): """Find a directive of type directive_name in directives """ - if not directives or isinstance(directives, six.string_types) or len(directives) == 0: + if not directives or isinstance(directives, str) or len(directives) == 0: return None if directives[0] == directive_name: diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 3eb6264aa..158cb9929 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -6,8 +6,6 @@ import os import pyparsing import re -import six - from certbot import errors from certbot_nginx import obj @@ -314,32 +312,6 @@ class NginxParser(object): except errors.MisconfigurationError as err: raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err))) - def create_new_vhost_from_default(self, vhost_template): - """Duplicate the default vhost in the configuration files. - - :param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost - whose information we copy - - :returns: A vhost object for the newly created vhost - :rtype: :class:`~certbot_nginx.obj.VirtualHost` - """ - # TODO: https://github.com/certbot/certbot/issues/5185 - # put it in the same file as the template, at the same level - enclosing_block = self.parsed[vhost_template.filep] - for index in vhost_template.path[:-1]: - enclosing_block = enclosing_block[index] - new_location = vhost_template.path[-1] + 1 - raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]]) - enclosing_block.insert(new_location, raw_in_parsed) - new_vhost = copy.deepcopy(vhost_template) - new_vhost.path[-1] = new_location - for addr in new_vhost.addrs: - addr.default = False - for directive in enclosing_block[new_vhost.path[-1]][1]: - if len(directive) > 0 and directive[0] == 'listen' and 'default_server' in directive: - del directive[directive.index('default_server')] - return new_vhost - def _parse_ssl_options(ssl_options): if ssl_options is not None: try: @@ -472,7 +444,7 @@ def _is_include_directive(entry): """ return (isinstance(entry, list) and len(entry) == 2 and entry[0] == 'include' and - isinstance(entry[1], six.string_types)) + isinstance(entry[1], str)) def _is_ssl_on_directive(entry): """Checks if an nginx parsed entry is an 'ssl on' directive. @@ -589,8 +561,7 @@ def _add_directive(block, directive, replace): directive_name = directive[0] def can_append(loc, dir_name): """ Can we append this directive to the block? """ - return loc is None or (isinstance(dir_name, six.string_types) - and dir_name in REPEATABLE_DIRECTIVES) + 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}".' diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 996bd238b..f4fe16924 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -45,7 +45,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) - self.assertEqual(10, len(self.config.parser.parsed)) + self.assertEqual(8, len(self.config.parser.parsed)) @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") @@ -89,7 +89,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(names, set( ["155.225.50.69.nephoscale.net", "www.example.org", "another.alias", "migration.com", "summer.com", "geese.com", "sslon.com", - "globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com"])) + "globalssl.com", "globalsslsetssl.com"])) def test_supported_enhancements(self): self.assertEqual(['redirect', 'staple-ocsp'], @@ -131,7 +131,6 @@ class NginxConfiguratorTest(util.NginxTest): server_conf = set(['somename', 'another.alias', 'alias']) example_conf = set(['.example.com', 'example.*']) foo_conf = set(['*.www.foo.com', '*.www.example.com']) - ipv6_conf = set(['ipv6.com']) results = {'localhost': localhost_conf, 'alias': server_conf, @@ -140,8 +139,7 @@ class NginxConfiguratorTest(util.NginxTest): 'www.example.com': example_conf, 'test.www.example.com': foo_conf, 'abc.www.foo.com': foo_conf, - 'www.bar.co.uk': localhost_conf, - 'ipv6.com': ipv6_conf} + 'www.bar.co.uk': localhost_conf} conf_path = {'localhost': "etc_nginx/nginx.conf", 'alias': "etc_nginx/nginx.conf", @@ -150,8 +148,7 @@ class NginxConfiguratorTest(util.NginxTest): 'www.example.com': "etc_nginx/sites-enabled/example.com", 'test.www.example.com': "etc_nginx/foo.conf", 'abc.www.foo.com': "etc_nginx/foo.conf", - 'www.bar.co.uk': "etc_nginx/nginx.conf", - 'ipv6.com': "etc_nginx/sites-enabled/ipv6.com"} + 'www.bar.co.uk': "etc_nginx/nginx.conf"} bad_results = ['www.foo.com', 'example', 't.www.bar.co', '69.255.225.155'] @@ -162,24 +159,11 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(results[name], vhost.names) self.assertEqual(conf_path[name], path) - # IPv6 specific checks - if name == "ipv6.com": - self.assertTrue(vhost.ipv6_enabled()) - # Make sure that we have SSL enabled also for IPv6 addr - self.assertTrue( - any([True for x in vhost.addrs if x.ssl and x.ipv6])) for name in bad_results: self.assertRaises(errors.MisconfigurationError, self.config.choose_vhost, name) - def test_ipv6only(self): - # ipv6_info: (ipv6_active, ipv6only_present) - self.assertEquals((True, False), self.config.ipv6_info("80")) - # Port 443 has ipv6only=on because of ipv6ssl.com vhost - self.assertEquals((True, True), self.config.ipv6_info("443")) - - def test_more_info(self): self.assertTrue('nginx.conf' in self.config.more_info()) @@ -574,145 +558,6 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue(util.contains_at_depth( generated_conf, ['ssl_stapling_verify', 'on'], 2)) - def test_deploy_no_match_default_set(self): - default_conf = self.config.parser.abs_path('sites-enabled/default') - foo_conf = self.config.parser.abs_path('foo.conf') - del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server - self.config.version = (1, 3, 1) - - self.config.deploy_cert( - "www.nomatch.com", - "example/cert.pem", - "example/key.pem", - "example/chain.pem", - "example/fullchain.pem") - self.config.save() - - self.config.parser.load() - - parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf]) - - self.assertEqual([[['server'], - [['listen', 'myhost', 'default_server'], - ['listen', 'otherhost', 'default_server'], - ['server_name', 'www.example.org'], - [['location', '/'], - [['root', 'html'], - ['index', 'index.html', 'index.htm']]]]], - [['server'], - [['listen', 'myhost'], - ['listen', 'otherhost'], - ['server_name', 'www.nomatch.com'], - [['location', '/'], - [['root', 'html'], - ['index', 'index.html', 'index.htm']]], - ['listen', '5001', 'ssl'], - ['ssl_certificate', 'example/fullchain.pem'], - ['ssl_certificate_key', 'example/key.pem'], - ['include', self.config.mod_ssl_conf], - ['ssl_dhparam', self.config.ssl_dhparams]]]], - parsed_default_conf) - - self.config.deploy_cert( - "nomatch.com", - "example/cert.pem", - "example/key.pem", - "example/chain.pem", - "example/fullchain.pem") - self.config.save() - - self.config.parser.load() - - parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf]) - - self.assertTrue(util.contains_at_depth(parsed_default_conf, "nomatch.com", 3)) - - def test_deploy_no_match_default_set_multi_level_path(self): - default_conf = self.config.parser.abs_path('sites-enabled/default') - foo_conf = self.config.parser.abs_path('foo.conf') - del self.config.parser.parsed[default_conf][0][1][0] - del self.config.parser.parsed[default_conf][0][1][0] - self.config.version = (1, 3, 1) - - self.config.deploy_cert( - "www.nomatch.com", - "example/cert.pem", - "example/key.pem", - "example/chain.pem", - "example/fullchain.pem") - self.config.save() - - self.config.parser.load() - - parsed_foo_conf = util.filter_comments(self.config.parser.parsed[foo_conf]) - - self.assertEqual([['server'], - [['listen', '*:80', 'ssl'], - ['server_name', 'www.nomatch.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$'], []], - ['ssl_certificate', 'example/fullchain.pem'], - ['ssl_certificate_key', 'example/key.pem']]], - parsed_foo_conf[1][1][1]) - - def test_deploy_no_match_no_default_set(self): - default_conf = self.config.parser.abs_path('sites-enabled/default') - foo_conf = self.config.parser.abs_path('foo.conf') - del self.config.parser.parsed[default_conf][0][1][0] - del self.config.parser.parsed[default_conf][0][1][0] - del self.config.parser.parsed[foo_conf][2][1][0][1][0] - self.config.version = (1, 3, 1) - - self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert, - "www.nomatch.com", "example/cert.pem", "example/key.pem", - "example/chain.pem", "example/fullchain.pem") - - def test_deploy_no_match_fail_multiple_defaults(self): - self.config.version = (1, 3, 1) - self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert, - "www.nomatch.com", "example/cert.pem", "example/key.pem", - "example/chain.pem", "example/fullchain.pem") - - def test_deploy_no_match_add_redirect(self): - default_conf = self.config.parser.abs_path('sites-enabled/default') - foo_conf = self.config.parser.abs_path('foo.conf') - del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server - self.config.version = (1, 3, 1) - - self.config.deploy_cert( - "www.nomatch.com", - "example/cert.pem", - "example/key.pem", - "example/chain.pem", - "example/fullchain.pem") - - self.config.deploy_cert( - "nomatch.com", - "example/cert.pem", - "example/key.pem", - "example/chain.pem", - "example/fullchain.pem") - - self.config.enhance("www.nomatch.com", "redirect") - - self.config.save() - - self.config.parser.load() - - expected = [ - ['if', '($scheme', '!=', '"https")'], - [['return', '301', 'https://$host$request_uri']] - ] - - generated_conf = self.config.parser.parsed[default_conf] - self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - - class InstallSslOptionsConfTest(util.NginxTest): """Test that the options-ssl-nginx.conf file is installed and updated properly.""" diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index ca5de7ff6..e655bc3e3 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -50,9 +50,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods 'sites-enabled/example.com', 'sites-enabled/migration.com', 'sites-enabled/sslon.com', - 'sites-enabled/globalssl.com', - 'sites-enabled/ipv6.com', - 'sites-enabled/ipv6ssl.com']]), + 'sites-enabled/globalssl.com']]), set(nparser.parsed.keys())) self.assertEqual([['server_name', 'somename', 'alias', 'another.alias']], nparser.parsed[nparser.abs_path('server.conf')]) @@ -76,7 +74,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test')))) - self.assertEqual(7, len( + self.assertEqual(5, len( glob.glob(nparser.abs_path('sites-enabled/*.test')))) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], @@ -112,8 +110,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods 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)], + [obj.Addr('4.8.2.6', '57', True, False)], True, True, set(['globalssl.com']), [], [0]) globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0] @@ -124,42 +121,34 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods vhosts = nparser.get_vhosts() vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'), - [obj.Addr('', '8080', False, False, - False, False)], + [obj.Addr('', '8080', False, False)], False, True, set(['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)], + [obj.Addr('somename', '8080', False, False), + obj.Addr('', '8000', False, False)], False, True, set(['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, False), + obj.Addr('127.0.0.1', '', False, False)], False, True, set(['.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)], + [obj.Addr('myhost', '', False, True)], False, True, set(['www.example.org']), [], [0]) vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'), - [obj.Addr('*', '80', True, True, - False, False)], + [obj.Addr('*', '80', True, True)], True, True, set(['*.www.foo.com', '*.www.example.com']), [], [2, 1, 0]) - self.assertEqual(12, len(vhosts)) + self.assertEqual(10, 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] @@ -406,29 +395,6 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods ]) self.assertTrue(server['ssl']) - def test_create_new_vhost_from_default(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.create_new_vhost_from_default(default) - 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)))) - if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default index 4f67fa7d1..26f37020c 100644 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default @@ -1,6 +1,5 @@ server { listen myhost default_server; - listen otherhost default_server; server_name www.example.org; location / { diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com deleted file mode 100644 index 7a7744b92..000000000 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com +++ /dev/null @@ -1,5 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name ipv6.com; -} diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com deleted file mode 100644 index d8f7eff12..000000000 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com +++ /dev/null @@ -1,5 +0,0 @@ -server { - listen 443 ssl; - listen [::]:443 ssl ipv6only=on; - server_name ipv6ssl.com; -} diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 32a5ed7d2..85db584b3 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -66,7 +66,7 @@ class TlsSniPerformTest(util.NginxTest): self.sni.add_chall(self.achalls[1]) mock_choose.return_value = None result = self.sni.perform() - self.assertFalse(result is None) + self.assertTrue(result is None) def test_perform0(self): responses = self.sni.perform() @@ -125,10 +125,10 @@ class TlsSniPerformTest(util.NginxTest): self.sni.add_chall(self.achalls[0]) self.sni.add_chall(self.achalls[2]) - v_addr1 = [obj.Addr("69.50.225.155", "9000", True, False, False, False), - obj.Addr("127.0.0.1", "", False, False, False, False)] - v_addr2 = [obj.Addr("myhost", "", False, True, False, False)] - v_addr2_print = [obj.Addr("myhost", "", False, False, False, False)] + v_addr1 = [obj.Addr("69.50.225.155", "9000", True, False), + obj.Addr("127.0.0.1", "", False, False)] + v_addr2 = [obj.Addr("myhost", "", False, True)] + v_addr2_print = [obj.Addr("myhost", "", False, False)] ll_addr = [v_addr1, v_addr2] self.sni._mod_config(ll_addr) # pylint: disable=protected-access diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index 7f597ac4a..d6faa12be 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -51,32 +51,19 @@ class NginxTlsSni01(common.TLSSNI01): default_addr = "{0} ssl".format( self.configurator.config.tls_sni_01_port) - ipv6, ipv6only = self.configurator.ipv6_info( - self.configurator.config.tls_sni_01_port) - for achall in self.achalls: - vhost = self.configurator.choose_vhost(achall.domain, raise_if_no_match=False) + vhost = self.configurator.choose_vhost(achall.domain) + if vhost is None: + logger.error( + "No nginx vhost exists with server_name matching: %s. " + "Please specify server_names in the Nginx config.", + achall.domain) + return None - if vhost is not None and vhost.addrs: + if vhost.addrs: addresses.append(list(vhost.addrs)) else: - if ipv6: - # If IPv6 is active in Nginx configuration - ipv6_addr = "[::]:{0} ssl".format( - self.configurator.config.tls_sni_01_port) - if not ipv6only: - # If ipv6only=on is not already present in the config - ipv6_addr = ipv6_addr + " ipv6only=on" - addresses.append([obj.Addr.fromstring(default_addr), - obj.Addr.fromstring(ipv6_addr)]) - logger.info(("Using default addresses %s and %s for " + - "TLSSNI01 authentication."), - default_addr, - ipv6_addr) - else: - addresses.append([obj.Addr.fromstring(default_addr)]) - logger.info("Using default address %s for TLSSNI01 authentication.", - default_addr) + addresses.append([obj.Addr.fromstring(default_addr)]) # Create challenge certs responses = [self._setup_challenge_cert(x) for x in self.achalls] @@ -130,6 +117,7 @@ class NginxTlsSni01(common.TLSSNI01): raise errors.MisconfigurationError( 'Certbot could not find an HTTP block to include ' 'TLS-SNI-01 challenges in %s.' % root) + config = [self._make_server_block(pair[0], pair[1]) for pair in six.moves.zip(self.achalls, ll_addrs)] config = nginxparser.UnspacedList(config) diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index 420d15679..f605eb751 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -251,7 +251,7 @@ class Addr(object): """Normalized representation of addr/port tuple """ if self.ipv6: - return (self.get_ipv6_exploded(), self.tup[1]) + return (self._normalize_ipv6(self.tup[0]), self.tup[1]) return self.tup def __eq__(self, other): From f1554324da4c68bfe8ba035647e2664edeb561aa Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 6 Dec 2017 14:46:55 -0800 Subject: [PATCH 30/67] Release 0.20.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 33 ++++++++++--------- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-dnsmadeeasy/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-luadns/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-rfc2136/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 6 ++-- letsencrypt-auto | 33 ++++++++++--------- letsencrypt-auto-source/certbot-auto.asc | 14 ++++---- letsencrypt-auto-source/letsencrypt-auto | 26 +++++++-------- letsencrypt-auto-source/letsencrypt-auto.sig | 4 +-- .../pieces/certbot-requirements.txt | 24 +++++++------- 22 files changed, 86 insertions(+), 84 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index c28e0c152..c5a85c96b 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 8dc283f2d..838d2fd04 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 25f2ce889..444bee1b9 100755 --- a/certbot-auto +++ b/certbot-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.19.0" +LE_AUTO_VERSION="0.20.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -1062,9 +1062,10 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 # Contains the requirements for the letsencrypt package. # @@ -1077,18 +1078,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.19.0 \ - --hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \ - --hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53 -acme==0.19.0 \ - --hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \ - --hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822 -certbot-apache==0.19.0 \ - --hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \ - --hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619 -certbot-nginx==0.19.0 \ - --hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \ - --hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1 +certbot==0.20.0 \ + --hash=sha256:c6b6bd288700898d1eb31a65b605e3a5fc10f1e3213ce468207d76a2decb9d35 \ + --hash=sha256:cabf505b64fb400c4239dcdbaeb882079477eb6a8442268596a8791b9e34de88 +acme==0.20.0 \ + --hash=sha256:8b0cee192c0d76d6f4045bdb14b3cfd29d9720e0dad2046794a2a555f1eaccb7 \ + --hash=sha256:45121aed6c8cc2f31896ac1083068dfdeb613f3edeff9576dc0d10632ea5a3d5 +certbot-apache==0.20.0 \ + --hash=sha256:f7e4dbc154d2e9d1461118b6dd3dbd16f6892da468f060eeaa162aff673347e2 \ + --hash=sha256:0ba499706451ffbccb172bcf93d6ef4c6cc8599157077a4fa6dfbe5a83c7921f +certbot-nginx==0.20.0 \ + --hash=sha256:b6e372e8740b20dd9bd63837646157ac97b3c9a65affd3954571b8e872ae9ecf \ + --hash=sha256:6379fdf20d9a7651fe30bb8d4b828cbea178cc263d7af5a380fc4508d793b9ae UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 166c383b3..d8965f2e4 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 6392e483c..448df1ab8 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 304acf110..5ad92f961 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 489321435..dbb4e9c68 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 67d68ee16..e24a9116c 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index 88e02304e..0c0bbdeb9 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index b40899e80..49c4f8ad9 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 1b72168e8..5c5f10e90 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index e9dc2b31d..6b626ad5e 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 79b523aed..aab3bd0ee 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 2a14e8ab1..8223226a5 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' install_requires = [ 'acme=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index f3919413d..94beef24b 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0.dev0' +version = '0.20.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 231a0f5f5..0f7b8f5fd 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.20.0.dev0' +__version__ = '0.20.0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 6b43fd0a2..abaa95b9b 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -107,7 +107,7 @@ optional arguments: case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/0.19.0 (certbot; + "". (default: CertbotACMEClient/0.20.0 (certbot; Ubuntu 16.04.3 LTS) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/2.7.12). The flags encoded in the user agent are: --duplicate, --force- @@ -121,7 +121,7 @@ optional arguments: (Example: Foo-Wrapper/1.0) (default: None) automation: - Arguments for automating execution & other tweaks + Flags for automating execution & other tweaks --keep-until-expiring, --keep, --reinstall If the requested certificate matches an existing @@ -228,7 +228,7 @@ testing: False) paths: - Arguments changing execution paths & servers + Flags for changing execution paths & servers --cert-path CERT_PATH Path to where certificate is saved (with auth --csr), diff --git a/letsencrypt-auto b/letsencrypt-auto index 25f2ce889..444bee1b9 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.19.0" +LE_AUTO_VERSION="0.20.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -1062,9 +1062,10 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==2.0.0 \ - --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ - --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 # Contains the requirements for the letsencrypt package. # @@ -1077,18 +1078,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.19.0 \ - --hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \ - --hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53 -acme==0.19.0 \ - --hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \ - --hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822 -certbot-apache==0.19.0 \ - --hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \ - --hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619 -certbot-nginx==0.19.0 \ - --hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \ - --hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1 +certbot==0.20.0 \ + --hash=sha256:c6b6bd288700898d1eb31a65b605e3a5fc10f1e3213ce468207d76a2decb9d35 \ + --hash=sha256:cabf505b64fb400c4239dcdbaeb882079477eb6a8442268596a8791b9e34de88 +acme==0.20.0 \ + --hash=sha256:8b0cee192c0d76d6f4045bdb14b3cfd29d9720e0dad2046794a2a555f1eaccb7 \ + --hash=sha256:45121aed6c8cc2f31896ac1083068dfdeb613f3edeff9576dc0d10632ea5a3d5 +certbot-apache==0.20.0 \ + --hash=sha256:f7e4dbc154d2e9d1461118b6dd3dbd16f6892da468f060eeaa162aff673347e2 \ + --hash=sha256:0ba499706451ffbccb172bcf93d6ef4c6cc8599157077a4fa6dfbe5a83c7921f +certbot-nginx==0.20.0 \ + --hash=sha256:b6e372e8740b20dd9bd63837646157ac97b3c9a65affd3954571b8e872ae9ecf \ + --hash=sha256:6379fdf20d9a7651fe30bb8d4b828cbea178cc263d7af5a380fc4508d793b9ae UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 834358464..eeab78cd6 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v2 -iQEcBAABCAAGBQJZ1TJAAAoJEE0XyZXNl3XyWjAIAKxR5v0qbSyOEwM1LrSoLqud -V3KkyEUlMq7IPHxoPKXbqUrIi4eZuhpJz+84LtVJe4ZQ6HYP9lPogX+PtmWW7dyO -YerxA2rUVGB9rFZofZYwTuJyvO5Nc0aDyp1FHHPg/5khWWhhhxKpWqqG3zT01+Vf -W8Lvvn7vr7sjTvxBdqHQ3z3hlUY62P2IKui9C5un5ozlSQpDrWh3Thi9r6CxbASL -/r1PQ6EfnNdPAizVrJWe5iUd0Nzj7VMkFwZ02A3OlOUvrHGVb1H6oj0S1lZ8LEpj -awOTys8PVBQ3vW2qbAL3Zk7Lr+CGfVfmoWC9TQEKiSN1woYFrFD39S527vB1onc= -=Meks +iQEcBAABCAAGBQJaKHMlAAoJEE0XyZXNl3Xy6OEH/iPg6D6+zco4NHMwxYIcTWVt +XE4u3CjuLcEVsvEnJYNSA48NHyi9rIqMHd+IneLU+lCG2D7eBsisNNyVPIgHktTf +p9i0WoZB+axe1glv9FJSZvjvr2d/ic4/wYHBF1c+szb9p8Z7o5Lhqa9/gtLJ/SZX +OGU0wok4hPIB6emq5zvmi/+r1AiOECXE26lZ0STp6wDkvz+ahTJSk6UaPCDY+Az4 +X2VmnRSks/gk7Q8cloFnyiPXyFMQHdGIBRrIXsSix90QqmNUF7iYb8sbHksU23EI +/LmIwSJlDm6KNOO2nllBB/uIg2ki7g0z7R4uf7XF4im+P95PAL/tQQ45lVj8DXE= +=Is56 -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 21e47feb8..444bee1b9 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.20.0.dev0" +LE_AUTO_VERSION="0.20.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -1078,18 +1078,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.19.0 \ - --hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \ - --hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53 -acme==0.19.0 \ - --hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \ - --hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822 -certbot-apache==0.19.0 \ - --hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \ - --hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619 -certbot-nginx==0.19.0 \ - --hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \ - --hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1 +certbot==0.20.0 \ + --hash=sha256:c6b6bd288700898d1eb31a65b605e3a5fc10f1e3213ce468207d76a2decb9d35 \ + --hash=sha256:cabf505b64fb400c4239dcdbaeb882079477eb6a8442268596a8791b9e34de88 +acme==0.20.0 \ + --hash=sha256:8b0cee192c0d76d6f4045bdb14b3cfd29d9720e0dad2046794a2a555f1eaccb7 \ + --hash=sha256:45121aed6c8cc2f31896ac1083068dfdeb613f3edeff9576dc0d10632ea5a3d5 +certbot-apache==0.20.0 \ + --hash=sha256:f7e4dbc154d2e9d1461118b6dd3dbd16f6892da468f060eeaa162aff673347e2 \ + --hash=sha256:0ba499706451ffbccb172bcf93d6ef4c6cc8599157077a4fa6dfbe5a83c7921f +certbot-nginx==0.20.0 \ + --hash=sha256:b6e372e8740b20dd9bd63837646157ac97b3c9a65affd3954571b8e872ae9ecf \ + --hash=sha256:6379fdf20d9a7651fe30bb8d4b828cbea178cc263d7af5a380fc4508d793b9ae UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 708bbbee6..e276aae53 100644 --- a/letsencrypt-auto-source/letsencrypt-auto.sig +++ b/letsencrypt-auto-source/letsencrypt-auto.sig @@ -1,2 +1,2 @@ -è¾לHêÉ­mì³ÊÄ+ˆ²Ä~™¦ES«ëM„4ø»ò¡Ù K“íY”jLãŸÁèÚê7øñöZ½åÕ³ÿ°dŸdÝïI.:†ÓdZMOü|’±K¢Öí°¾âm|göÊ(–$bšljÇÐ…’/ñAâ^Ãéÿ©¶`ra®^ª0˜Ôß÷xÜÐå’²ƒwæÈá9”¦ckâNÃù¬Å‘.[ ?ë” -hð¡/Ì8!÷ü\§º’Å!»ÎöØÿ¯U5ñ£9bÉR£Ÿlb±-•«1‰Âà‰±ü(›p>¹ -û¢%Îu2ÁgnêÍ \ No newline at end of file +HtÃÚPdM-b_ 8Gݵ¥œx\¨cf<9n™$-ä€^5¶¤¡ÌÙð—6¯ò¢¹zéOy¯3üäðo-äÃN~“ֹ麛À²Ñn%… ww''}q;å̰: + M§4­Ìàí\¬¬@¿)€°-¶ã:ǺzD•Y›Ááþ‘=ð›ìŸ­*†à'žà Date: Wed, 6 Dec 2017 14:52:16 -0800 Subject: [PATCH 31/67] Bump version to 0.21.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-dnsmadeeasy/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-luadns/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-rfc2136/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index c5a85c96b..d04b84739 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 838d2fd04..3270f2c79 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index d8965f2e4..1faf30643 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 448df1ab8..428271045 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 5ad92f961..4a103193f 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index dbb4e9c68..23098d4b6 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index e24a9116c..4ed5a06ca 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index 0c0bbdeb9..8a0b88aab 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 49c4f8ad9..b00bd1ac3 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 5c5f10e90..b8f50254e 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 6b626ad5e..2a388e487 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index aab3bd0ee..78007afb5 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 8223226a5..7d1eb0bc9 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' install_requires = [ 'acme=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 94beef24b..2ad7aaf08 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.20.0' +version = '0.21.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 0f7b8f5fd..cbea701ee 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.20.0' +__version__ = '0.21.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 444bee1b9..8d2e8a6b6 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.20.0" +LE_AUTO_VERSION="0.21.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 716f25743ca7df91b0d55ee08058f2271983d9d4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 6 Dec 2017 16:33:55 -0800 Subject: [PATCH 32/67] Update changelog for 0.20.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aaef4af1..92d059b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 0.20.0 - 2017-12-06 + +### Added + +* Certbot's ACME library now recognizes URL fields in challenge objects in + preparation for Let's Encrypt's new ACME endpoint. The value is still + accessible in our ACME library through the name "uri". + +### Changed + +* The Apache plugin now parses some distro specific Apache configuration files + on non-Debian systems allowing it to get a clearer picture on the running + Apache configuration. +* Certbot better reports network failures by removing information about + connection retries from the error output. +* An unnecessary question when using Certbot's webroot plugin interactively has + been removed. + +### Fixed + +* Certbot's NGINX plugin no longer sometimes incorrectly reports that it was + unable to deploy a HTTP->HTTPS redirect when requesting Certbot to enable a + redirect for multiple domains. +* An issue running the test shipped with Certbot and some our DNS plugins with + older versions of mock have been resolved. +* On some systems, users reported strangely interleaved output depending on + when stdout and stderr were flushed. This problem was resolved by having + Certbot regularly flush these streams. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/44?closed=1 + ## 0.19.0 - 2017-10-04 ### Added From abed73a8e4877e5166b017d5fe29bb9d9a497cb0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 6 Dec 2017 17:45:20 -0800 Subject: [PATCH 33/67] Revert "Nginx reversion (#5299)" (#5305) This reverts commit c9949411cdc5a058d8114a430d98b45c80384650. --- certbot-nginx/certbot_nginx/configurator.py | 122 +++++++++++-- certbot-nginx/certbot_nginx/nginxparser.py | 20 ++- certbot-nginx/certbot_nginx/obj.py | 55 ++++-- certbot-nginx/certbot_nginx/parser.py | 33 +++- .../certbot_nginx/tests/configurator_test.py | 163 +++++++++++++++++- .../certbot_nginx/tests/parser_test.py | 56 ++++-- .../testdata/etc_nginx/sites-enabled/default | 1 + .../testdata/etc_nginx/sites-enabled/ipv6.com | 5 + .../etc_nginx/sites-enabled/ipv6ssl.com | 5 + .../certbot_nginx/tests/tls_sni_01_test.py | 10 +- certbot-nginx/certbot_nginx/tls_sni_01.py | 34 ++-- certbot/plugins/common.py | 2 +- 12 files changed, 437 insertions(+), 69 deletions(-) create mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com create mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index fe27dbc4b..98990664f 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -117,6 +117,9 @@ class NginxConfigurator(common.Installer): # Files to save self.save_notes = "" + # For creating new vhosts if no names match + self.new_vhost = None + # Add number of outstanding challenges self._chall_out = 0 @@ -191,9 +194,11 @@ class NginxConfigurator(common.Installer): "The nginx plugin currently requires --fullchain-path to " "install a cert.") - vhost = self.choose_vhost(domain) - cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path], - ['\n', 'ssl_certificate_key', ' ', key_path]] + vhost = self.choose_vhost(domain, raise_if_no_match=False) + if vhost is None: + vhost = self._vhost_from_duplicated_default(domain) + cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path], + ['\n ', 'ssl_certificate_key', ' ', key_path]] self.parser.add_server_directives(vhost, cert_directives, replace=True) @@ -209,7 +214,7 @@ class NginxConfigurator(common.Installer): ####################### # Vhost parsing methods ####################### - def choose_vhost(self, target_name): + def choose_vhost(self, target_name, raise_if_no_match=True): """Chooses a virtual host based on the given domain name. .. note:: This makes the vhost SSL-enabled if it isn't already. Follows @@ -223,6 +228,8 @@ class NginxConfigurator(common.Installer): hostname. Currently we just ignore this. :param str target_name: domain name + :param bool raise_if_no_match: True iff not finding a match is an error; + otherwise, return None :returns: ssl vhost associated with name :rtype: :class:`~certbot_nginx.obj.VirtualHost` @@ -233,13 +240,16 @@ class NginxConfigurator(common.Installer): matches = self._get_ranked_matches(target_name) vhost = self._select_best_name_match(matches) if not vhost: - # 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: " - "https://nginx.org/en/docs/http/server_names.html") % (target_name)) + if raise_if_no_match: + # 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: " + "https://nginx.org/en/docs/http/server_names.html") % (target_name)) + else: + return None else: # Note: if we are enhancing with ocsp, vhost should already be ssl. if not vhost.ssl: @@ -247,6 +257,65 @@ class NginxConfigurator(common.Installer): return vhost + + 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) + """ + 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): + if self.new_vhost is None: + default_vhost = self._get_default_vhost() + self.new_vhost = self.parser.create_new_vhost_from_default(default_vhost) + if not self.new_vhost.ssl: + self._make_server_ssl(self.new_vhost) + self.new_vhost.names = set() + + self.new_vhost.names.add(domain) + name_block = [['\n ', 'server_name']] + for name in self.new_vhost.names: + name_block[0].append(' ') + name_block[0].append(name) + self.parser.add_server_directives(self.new_vhost, name_block, replace=True) + return self.new_vhost + + def _get_default_vhost(self): + vhost_list = self.parser.get_vhosts() + # if one has default_server set, return that one + default_vhosts = [] + for vhost in vhost_list: + for addr in vhost.addrs: + if addr.default: + default_vhosts.append(vhost) + break + + if len(default_vhosts) == 1: + return 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. Set the `server_name` directive to use the Nginx installer.") + def _get_ranked_matches(self, target_name): """Returns a ranked list of vhosts that match target_name. The ranking gives preference to SSL vhosts. @@ -405,9 +474,12 @@ class NginxConfigurator(common.Installer): all_names.add(host) elif not common.private_ips_regex.match(host): # If it isn't a private IP, do a reverse DNS lookup - # TODO: IPv6 support try: - socket.inet_aton(host) + 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 @@ -443,16 +515,38 @@ class NginxConfigurator(common.Installer): :type vhost: :class:`~certbot_nginx.obj.VirtualHost` """ + ipv6info = self.ipv6_info(self.config.tls_sni_01_port) + ipv6_block = [''] + ipv4_block = [''] + # If the vhost was implicitly listening on the default Nginx port, # have it continue to do so. if len(vhost.addrs) == 0: listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]] self.parser.add_server_directives(vhost, listen_block, replace=False) + if vhost.ipv6_enabled(): + ipv6_block = ['\n ', + 'listen', + ' ', + '[::]:{0} ssl'.format(self.config.tls_sni_01_port)] + 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} ssl'.format(self.config.tls_sni_01_port)] + + snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() ssl_block = ([ - ['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], + ipv6_block, + ipv4_block, ['\n ', 'ssl_certificate', ' ', snakeoil_cert], ['\n ', 'ssl_certificate_key', ' ', snakeoil_key], ['\n ', 'include', ' ', self.mod_ssl_conf], diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 20aeeb554..14481e298 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -7,6 +7,7 @@ from pyparsing import ( Literal, White, Forward, Group, Optional, OneOrMore, QuotedString, Regex, ZeroOrMore, Combine) from pyparsing import stringEnd from pyparsing import restOfLine +import six logger = logging.getLogger(__name__) @@ -71,7 +72,7 @@ class RawNginxDumper(object): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks for b0 in blocks: - if isinstance(b0, str): + if isinstance(b0, six.string_types): yield b0 continue item = copy.deepcopy(b0) @@ -88,7 +89,7 @@ class RawNginxDumper(object): yield '}' else: # not a block - list of strings semicolon = ";" - if isinstance(item[0], str) and item[0].strip() == '#': # comment + if isinstance(item[0], six.string_types) and item[0].strip() == '#': # comment semicolon = "" yield "".join(item) + semicolon @@ -145,7 +146,7 @@ def dump(blocks, _file): return _file.write(dumps(blocks)) -spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == '' +spacey = lambda x: (isinstance(x, six.string_types) and x.isspace()) or x == '' class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" @@ -189,13 +190,15 @@ class UnspacedList(list): 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) - list.insert(self, i, 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) - list.append(self, item) + if not spacey(item): + list.append(self, item) self.dirty = True def extend(self, x): @@ -226,7 +229,8 @@ class UnspacedList(list): raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") item, spaced_item = self._coerce(value) self.spaced.__setitem__(self._spaced_position(i), spaced_item) - list.__setitem__(self, i, item) + if not spacey(item): + list.__setitem__(self, i, item) self.dirty = True def __delitem__(self, i): @@ -235,8 +239,8 @@ class UnspacedList(list): self.dirty = True def __deepcopy__(self, memo): - l = UnspacedList(self[:]) - l.spaced = copy.deepcopy(self.spaced, memo=memo) + new_spaced = copy.deepcopy(self.spaced, memo=memo) + l = UnspacedList(new_spaced) l.dirty = self.dirty return l diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 849cefe1f..5816c5571 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -34,10 +34,13 @@ class Addr(common.Addr): UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0') CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0] - def __init__(self, host, port, ssl, default): + def __init__(self, host, port, ssl, default, ipv6, ipv6only): + # pylint: disable=too-many-arguments super(Addr, self).__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 @@ -46,6 +49,8 @@ class Addr(common.Addr): parts = str_addr.split(' ') ssl = False default = False + ipv6 = False + ipv6only = False host = '' port = '' @@ -56,15 +61,25 @@ class Addr(common.Addr): if addr.startswith('unix:'): return None - 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] + # 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: - # This is a host-port tuple. E.g. listen 127.0.0.1:* - host = tup[0] - port = tup[2] + # 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 len(parts) > 0: @@ -73,8 +88,10 @@ class Addr(common.Addr): ssl = True elif nextpart == 'default_server': default = True + elif nextpart == "ipv6only=on": + ipv6only = True - return cls(host, port, ssl, default) + return cls(host, port, ssl, default, ipv6, ipv6only) def to_string(self, include_default=True): """Return string representation of Addr""" @@ -114,8 +131,6 @@ class Addr(common.Addr): self.tup[1]), self.ipv6) == \ common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS, other.tup[1]), other.ipv6) - # Nginx plugin currently doesn't support IPv6 but this will - # future-proof it return super(Addr, self).__eq__(other) def __eq__(self, other): @@ -195,10 +210,24 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods 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 + + def ipv4_enabled(self): + """Return true if one or more of the listen directives in vhost are IPv4 + only""" + for a in self.addrs: + if not a.ipv6: + return True + def _find_directive(directives, directive_name): """Find a directive of type directive_name in directives """ - if not directives or isinstance(directives, str) or len(directives) == 0: + if not directives or isinstance(directives, six.string_types) or len(directives) == 0: return None if directives[0] == directive_name: diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 158cb9929..3eb6264aa 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -6,6 +6,8 @@ import os import pyparsing import re +import six + from certbot import errors from certbot_nginx import obj @@ -312,6 +314,32 @@ class NginxParser(object): except errors.MisconfigurationError as err: raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err))) + def create_new_vhost_from_default(self, vhost_template): + """Duplicate the default vhost in the configuration files. + + :param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost + whose information we copy + + :returns: A vhost object for the newly created vhost + :rtype: :class:`~certbot_nginx.obj.VirtualHost` + """ + # TODO: https://github.com/certbot/certbot/issues/5185 + # put it in the same file as the template, at the same level + enclosing_block = self.parsed[vhost_template.filep] + for index in vhost_template.path[:-1]: + enclosing_block = enclosing_block[index] + new_location = vhost_template.path[-1] + 1 + raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]]) + enclosing_block.insert(new_location, raw_in_parsed) + new_vhost = copy.deepcopy(vhost_template) + new_vhost.path[-1] = new_location + for addr in new_vhost.addrs: + addr.default = False + for directive in enclosing_block[new_vhost.path[-1]][1]: + if len(directive) > 0 and directive[0] == 'listen' and 'default_server' in directive: + del directive[directive.index('default_server')] + return new_vhost + def _parse_ssl_options(ssl_options): if ssl_options is not None: try: @@ -444,7 +472,7 @@ def _is_include_directive(entry): """ return (isinstance(entry, list) and len(entry) == 2 and entry[0] == 'include' and - isinstance(entry[1], str)) + isinstance(entry[1], six.string_types)) def _is_ssl_on_directive(entry): """Checks if an nginx parsed entry is an 'ssl on' directive. @@ -561,7 +589,8 @@ def _add_directive(block, directive, replace): 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) + return loc is None or (isinstance(dir_name, six.string_types) + and dir_name in REPEATABLE_DIRECTIVES) err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".' diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index f4fe16924..996bd238b 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -45,7 +45,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) - self.assertEqual(8, len(self.config.parser.parsed)) + self.assertEqual(10, len(self.config.parser.parsed)) @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") @@ -89,7 +89,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(names, set( ["155.225.50.69.nephoscale.net", "www.example.org", "another.alias", "migration.com", "summer.com", "geese.com", "sslon.com", - "globalssl.com", "globalsslsetssl.com"])) + "globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com"])) def test_supported_enhancements(self): self.assertEqual(['redirect', 'staple-ocsp'], @@ -131,6 +131,7 @@ class NginxConfiguratorTest(util.NginxTest): server_conf = set(['somename', 'another.alias', 'alias']) example_conf = set(['.example.com', 'example.*']) foo_conf = set(['*.www.foo.com', '*.www.example.com']) + ipv6_conf = set(['ipv6.com']) results = {'localhost': localhost_conf, 'alias': server_conf, @@ -139,7 +140,8 @@ class NginxConfiguratorTest(util.NginxTest): 'www.example.com': example_conf, 'test.www.example.com': foo_conf, 'abc.www.foo.com': foo_conf, - 'www.bar.co.uk': localhost_conf} + 'www.bar.co.uk': localhost_conf, + 'ipv6.com': ipv6_conf} conf_path = {'localhost': "etc_nginx/nginx.conf", 'alias': "etc_nginx/nginx.conf", @@ -148,7 +150,8 @@ class NginxConfiguratorTest(util.NginxTest): 'www.example.com': "etc_nginx/sites-enabled/example.com", 'test.www.example.com': "etc_nginx/foo.conf", 'abc.www.foo.com': "etc_nginx/foo.conf", - 'www.bar.co.uk': "etc_nginx/nginx.conf"} + 'www.bar.co.uk': "etc_nginx/nginx.conf", + 'ipv6.com': "etc_nginx/sites-enabled/ipv6.com"} bad_results = ['www.foo.com', 'example', 't.www.bar.co', '69.255.225.155'] @@ -159,11 +162,24 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(results[name], vhost.names) self.assertEqual(conf_path[name], path) + # IPv6 specific checks + if name == "ipv6.com": + self.assertTrue(vhost.ipv6_enabled()) + # Make sure that we have SSL enabled also for IPv6 addr + self.assertTrue( + any([True for x in vhost.addrs if x.ssl and x.ipv6])) for name in bad_results: self.assertRaises(errors.MisconfigurationError, self.config.choose_vhost, name) + def test_ipv6only(self): + # ipv6_info: (ipv6_active, ipv6only_present) + self.assertEquals((True, False), self.config.ipv6_info("80")) + # Port 443 has ipv6only=on because of ipv6ssl.com vhost + self.assertEquals((True, True), self.config.ipv6_info("443")) + + def test_more_info(self): self.assertTrue('nginx.conf' in self.config.more_info()) @@ -558,6 +574,145 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue(util.contains_at_depth( generated_conf, ['ssl_stapling_verify', 'on'], 2)) + def test_deploy_no_match_default_set(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server + self.config.version = (1, 3, 1) + + self.config.deploy_cert( + "www.nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.save() + + self.config.parser.load() + + parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf]) + + self.assertEqual([[['server'], + [['listen', 'myhost', 'default_server'], + ['listen', 'otherhost', 'default_server'], + ['server_name', 'www.example.org'], + [['location', '/'], + [['root', 'html'], + ['index', 'index.html', 'index.htm']]]]], + [['server'], + [['listen', 'myhost'], + ['listen', 'otherhost'], + ['server_name', 'www.nomatch.com'], + [['location', '/'], + [['root', 'html'], + ['index', 'index.html', 'index.htm']]], + ['listen', '5001', 'ssl'], + ['ssl_certificate', 'example/fullchain.pem'], + ['ssl_certificate_key', 'example/key.pem'], + ['include', self.config.mod_ssl_conf], + ['ssl_dhparam', self.config.ssl_dhparams]]]], + parsed_default_conf) + + self.config.deploy_cert( + "nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.save() + + self.config.parser.load() + + parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf]) + + self.assertTrue(util.contains_at_depth(parsed_default_conf, "nomatch.com", 3)) + + def test_deploy_no_match_default_set_multi_level_path(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[default_conf][0][1][0] + del self.config.parser.parsed[default_conf][0][1][0] + self.config.version = (1, 3, 1) + + self.config.deploy_cert( + "www.nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.save() + + self.config.parser.load() + + parsed_foo_conf = util.filter_comments(self.config.parser.parsed[foo_conf]) + + self.assertEqual([['server'], + [['listen', '*:80', 'ssl'], + ['server_name', 'www.nomatch.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$'], []], + ['ssl_certificate', 'example/fullchain.pem'], + ['ssl_certificate_key', 'example/key.pem']]], + parsed_foo_conf[1][1][1]) + + def test_deploy_no_match_no_default_set(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[default_conf][0][1][0] + del self.config.parser.parsed[default_conf][0][1][0] + del self.config.parser.parsed[foo_conf][2][1][0][1][0] + self.config.version = (1, 3, 1) + + self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert, + "www.nomatch.com", "example/cert.pem", "example/key.pem", + "example/chain.pem", "example/fullchain.pem") + + def test_deploy_no_match_fail_multiple_defaults(self): + self.config.version = (1, 3, 1) + self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert, + "www.nomatch.com", "example/cert.pem", "example/key.pem", + "example/chain.pem", "example/fullchain.pem") + + def test_deploy_no_match_add_redirect(self): + default_conf = self.config.parser.abs_path('sites-enabled/default') + foo_conf = self.config.parser.abs_path('foo.conf') + del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server + self.config.version = (1, 3, 1) + + self.config.deploy_cert( + "www.nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + + self.config.deploy_cert( + "nomatch.com", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + + self.config.enhance("www.nomatch.com", "redirect") + + self.config.save() + + self.config.parser.load() + + expected = [ + ['if', '($scheme', '!=', '"https")'], + [['return', '301', 'https://$host$request_uri']] + ] + + generated_conf = self.config.parser.parsed[default_conf] + self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + + class InstallSslOptionsConfTest(util.NginxTest): """Test that the options-ssl-nginx.conf file is installed and updated properly.""" diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index e655bc3e3..ca5de7ff6 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -50,7 +50,9 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods 'sites-enabled/example.com', 'sites-enabled/migration.com', 'sites-enabled/sslon.com', - 'sites-enabled/globalssl.com']]), + 'sites-enabled/globalssl.com', + 'sites-enabled/ipv6.com', + 'sites-enabled/ipv6ssl.com']]), set(nparser.parsed.keys())) self.assertEqual([['server_name', 'somename', 'alias', 'another.alias']], nparser.parsed[nparser.abs_path('server.conf')]) @@ -74,7 +76,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test')))) - self.assertEqual(5, len( + self.assertEqual(7, len( glob.glob(nparser.abs_path('sites-enabled/*.test')))) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], @@ -110,7 +112,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods vhosts = nparser.get_vhosts() vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'), - [obj.Addr('4.8.2.6', '57', True, False)], + [obj.Addr('4.8.2.6', '57', True, False, + False, False)], True, True, set(['globalssl.com']), [], [0]) globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0] @@ -121,34 +124,42 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods vhosts = nparser.get_vhosts() vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'), - [obj.Addr('', '8080', False, False)], + [obj.Addr('', '8080', False, False, + False, False)], False, True, set(['localhost', r'~^(www\.)?(example|bar)\.']), [], [10, 1, 9]) vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'), - [obj.Addr('somename', '8080', False, False), - obj.Addr('', '8000', False, False)], + [obj.Addr('somename', '8080', False, False, + False, False), + obj.Addr('', '8000', False, False, + False, False)], False, True, set(['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), - obj.Addr('127.0.0.1', '', False, False)], + False, False, False, False), + obj.Addr('127.0.0.1', '', False, False, + False, False)], False, True, set(['.example.com', 'example.*']), [], [0]) vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'), - [obj.Addr('myhost', '', False, True)], + [obj.Addr('myhost', '', False, True, + False, False), + obj.Addr('otherhost', '', False, True, + False, False)], False, True, set(['www.example.org']), [], [0]) vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'), - [obj.Addr('*', '80', True, True)], + [obj.Addr('*', '80', True, True, + False, False)], True, True, set(['*.www.foo.com', '*.www.example.com']), [], [2, 1, 0]) - self.assertEqual(10, len(vhosts)) + self.assertEqual(12, 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] @@ -395,6 +406,29 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods ]) self.assertTrue(server['ssl']) + def test_create_new_vhost_from_default(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.create_new_vhost_from_default(default) + 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)))) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default index 26f37020c..4f67fa7d1 100644 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/default @@ -1,5 +1,6 @@ server { listen myhost default_server; + listen otherhost default_server; server_name www.example.org; location / { diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com new file mode 100644 index 000000000..7a7744b92 --- /dev/null +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com @@ -0,0 +1,5 @@ +server { + listen 80; + listen [::]:80; + server_name ipv6.com; +} diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com new file mode 100644 index 000000000..d8f7eff12 --- /dev/null +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com @@ -0,0 +1,5 @@ +server { + listen 443 ssl; + listen [::]:443 ssl ipv6only=on; + server_name ipv6ssl.com; +} diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 85db584b3..32a5ed7d2 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -66,7 +66,7 @@ class TlsSniPerformTest(util.NginxTest): self.sni.add_chall(self.achalls[1]) mock_choose.return_value = None result = self.sni.perform() - self.assertTrue(result is None) + self.assertFalse(result is None) def test_perform0(self): responses = self.sni.perform() @@ -125,10 +125,10 @@ class TlsSniPerformTest(util.NginxTest): self.sni.add_chall(self.achalls[0]) self.sni.add_chall(self.achalls[2]) - v_addr1 = [obj.Addr("69.50.225.155", "9000", True, False), - obj.Addr("127.0.0.1", "", False, False)] - v_addr2 = [obj.Addr("myhost", "", False, True)] - v_addr2_print = [obj.Addr("myhost", "", False, False)] + v_addr1 = [obj.Addr("69.50.225.155", "9000", True, False, False, False), + obj.Addr("127.0.0.1", "", False, False, False, False)] + v_addr2 = [obj.Addr("myhost", "", False, True, False, False)] + v_addr2_print = [obj.Addr("myhost", "", False, False, False, False)] ll_addr = [v_addr1, v_addr2] self.sni._mod_config(ll_addr) # pylint: disable=protected-access diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index d6faa12be..7f597ac4a 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -51,19 +51,32 @@ class NginxTlsSni01(common.TLSSNI01): default_addr = "{0} ssl".format( self.configurator.config.tls_sni_01_port) - for achall in self.achalls: - vhost = self.configurator.choose_vhost(achall.domain) - if vhost is None: - logger.error( - "No nginx vhost exists with server_name matching: %s. " - "Please specify server_names in the Nginx config.", - achall.domain) - return None + ipv6, ipv6only = self.configurator.ipv6_info( + self.configurator.config.tls_sni_01_port) - if vhost.addrs: + for achall in self.achalls: + vhost = self.configurator.choose_vhost(achall.domain, raise_if_no_match=False) + + if vhost is not None and vhost.addrs: addresses.append(list(vhost.addrs)) else: - addresses.append([obj.Addr.fromstring(default_addr)]) + if ipv6: + # If IPv6 is active in Nginx configuration + ipv6_addr = "[::]:{0} ssl".format( + self.configurator.config.tls_sni_01_port) + if not ipv6only: + # If ipv6only=on is not already present in the config + ipv6_addr = ipv6_addr + " ipv6only=on" + addresses.append([obj.Addr.fromstring(default_addr), + obj.Addr.fromstring(ipv6_addr)]) + logger.info(("Using default addresses %s and %s for " + + "TLSSNI01 authentication."), + default_addr, + ipv6_addr) + else: + addresses.append([obj.Addr.fromstring(default_addr)]) + logger.info("Using default address %s for TLSSNI01 authentication.", + default_addr) # Create challenge certs responses = [self._setup_challenge_cert(x) for x in self.achalls] @@ -117,7 +130,6 @@ class NginxTlsSni01(common.TLSSNI01): raise errors.MisconfigurationError( 'Certbot could not find an HTTP block to include ' 'TLS-SNI-01 challenges in %s.' % root) - config = [self._make_server_block(pair[0], pair[1]) for pair in six.moves.zip(self.achalls, ll_addrs)] config = nginxparser.UnspacedList(config) diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index f605eb751..420d15679 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -251,7 +251,7 @@ class Addr(object): """Normalized representation of addr/port tuple """ if self.ipv6: - return (self._normalize_ipv6(self.tup[0]), self.tup[1]) + return (self.get_ipv6_exploded(), self.tup[1]) return self.tup def __eq__(self, other): From 8b5d6879cc3dcdf41ed097cf08ef2ac9dc8a1e36 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Thu, 7 Dec 2017 09:48:54 -0800 Subject: [PATCH 34/67] Create a new server block when making server block ssl (#5220) * create_new_vhost_from_default --> duplicate_vhost * add source_path property * set source path for duplicated vhost * change around logic of where making ssl happens * don't add listen 80 to newly created ssl block * cache vhosts list * remove source path * add redirect block if we created a new server block * Remove listen directives when making server block ssl * Reset vhost cache on parser load * flip connected pointer direction for finding newly made server block to match previous redirect search constraints * also test for new redirect block styles * fix contains_list and test redirect blocks * update lint, parser, and obj tests * reset new vhost (fixing previous bug) and move removing default from addrs under if statement * reuse and update newly created ssl server block when appropriate, and update unit tests * append newly created server blocks to file instead of inserting directly after, so we don't have to update other vhosts' paths * add coverage for NO_IF_REDIRECT_COMMENT_BLOCK * add coverage for parser load calls * replace some double quotes with single quotes * replace backslash continuations with parentheses * update docstrings * switch to only creating a new block on redirect enhancement, including removing the get_vhosts cache * update configurator tests * update obj test * switch delete_default default for duplicate_vhost --- certbot-nginx/certbot_nginx/configurator.py | 150 +++++++++--------- certbot-nginx/certbot_nginx/obj.py | 4 +- certbot-nginx/certbot_nginx/parser.py | 116 ++++++++++---- .../certbot_nginx/tests/configurator_test.py | 81 ++++++++-- certbot-nginx/certbot_nginx/tests/obj_test.py | 8 +- .../certbot_nginx/tests/parser_test.py | 10 +- certbot-nginx/certbot_nginx/tls_sni_01.py | 2 +- 7 files changed, 243 insertions(+), 128 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 98990664f..e9d4e36d4 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -23,43 +23,24 @@ from certbot import util from certbot.plugins import common from certbot_nginx import constants -from certbot_nginx import tls_sni_01 +from certbot_nginx import nginxparser from certbot_nginx import parser +from certbot_nginx import tls_sni_01 logger = logging.getLogger(__name__) -REDIRECT_BLOCK = [[ - ['\n ', 'if', ' ', '($scheme', ' ', '!=', ' ', '"https")'], - [['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'], - '\n '] -], ['\n']] - -TEST_REDIRECT_BLOCK = [ - [ - ['if', '($scheme', '!=', '"https")'], - [ - ['return', '301', 'https://$host$request_uri'] - ] - ], - ['#', ' managed by Certbot'] +REDIRECT_BLOCK = [ + ['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'], + ['\n'] ] REDIRECT_COMMENT_BLOCK = [ ['\n ', '#', ' Redirect non-https traffic to https'], - ['\n ', '#', ' if ($scheme != "https") {'], - ['\n ', '#', " return 301 https://$host$request_uri;"], - ['\n ', '#', " } # managed by Certbot"], + ['\n ', '#', ' return 301 https://$host$request_uri;'], ['\n'] ] -TEST_REDIRECT_COMMENT_BLOCK = [ - ['#', ' Redirect non-https traffic to https'], - ['#', ' if ($scheme != "https") {'], - ['#', " return 301 https://$host$request_uri;"], - ['#', " } # managed by Certbot"], -] - @zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller) @zope.interface.provider(interfaces.IPluginFactory) class NginxConfigurator(common.Installer): @@ -194,9 +175,7 @@ class NginxConfigurator(common.Installer): "The nginx plugin currently requires --fullchain-path to " "install a cert.") - vhost = self.choose_vhost(domain, raise_if_no_match=False) - if vhost is None: - vhost = self._vhost_from_duplicated_default(domain) + vhost = self.choose_vhost(domain, create_if_no_match=True) cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path], ['\n ', 'ssl_certificate_key', ' ', key_path]] @@ -214,7 +193,7 @@ class NginxConfigurator(common.Installer): ####################### # Vhost parsing methods ####################### - def choose_vhost(self, target_name, raise_if_no_match=True): + def choose_vhost(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 @@ -228,8 +207,8 @@ class NginxConfigurator(common.Installer): hostname. Currently we just ignore this. :param str target_name: domain name - :param bool raise_if_no_match: True iff not finding a match is an error; - otherwise, return None + :param bool create_if_no_match: If we should create a new vhost from default + when there is no match found :returns: ssl vhost associated with name :rtype: :class:`~certbot_nginx.obj.VirtualHost` @@ -240,7 +219,9 @@ class NginxConfigurator(common.Installer): matches = self._get_ranked_matches(target_name) vhost = self._select_best_name_match(matches) if not vhost: - if raise_if_no_match: + if create_if_no_match: + vhost = self._vhost_from_duplicated_default(target_name) + else: # No matches. Raise a misconfiguration error. raise errors.MisconfigurationError( ("Cannot find a VirtualHost matching domain %s. " @@ -248,16 +229,12 @@ class NginxConfigurator(common.Installer): "please add a corresponding server_name directive to your " "nginx configuration: " "https://nginx.org/en/docs/http/server_names.html") % (target_name)) - else: - return None - else: - # Note: if we are enhancing with ocsp, vhost should already be ssl. - if not vhost.ssl: - self._make_server_ssl(vhost) + # Note: if we are enhancing with ocsp, vhost should already be ssl. + if not vhost.ssl: + self._make_server_ssl(vhost) return vhost - 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 @@ -285,18 +262,19 @@ class NginxConfigurator(common.Installer): def _vhost_from_duplicated_default(self, domain): if self.new_vhost is None: default_vhost = self._get_default_vhost() - self.new_vhost = self.parser.create_new_vhost_from_default(default_vhost) - if not self.new_vhost.ssl: - self._make_server_ssl(self.new_vhost) + self.new_vhost = self.parser.duplicate_vhost(default_vhost, delete_default=True) self.new_vhost.names = set() - self.new_vhost.names.add(domain) + 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 self.new_vhost.names: + for name in vhost.names: name_block[0].append(' ') name_block[0].append(name) - self.parser.add_server_directives(self.new_vhost, name_block, replace=True) - return self.new_vhost + self.parser.add_server_directives(vhost, name_block, replace=True) def _get_default_vhost(self): vhost_list = self.parser.get_vhosts() @@ -505,11 +483,7 @@ class NginxConfigurator(common.Installer): def _make_server_ssl(self, vhost): """Make a server SSL. - Make a server SSL based on server_name and filename by adding a - ``listen IConfig.tls_sni_01_port ssl`` directive to the server block. - - .. todo:: Maybe this should create a new block instead of modifying - the existing one? + Make a server SSL by adding new listen and SSL directives. :param vhost: The vhost to add SSL to. :type vhost: :class:`~certbot_nginx.obj.VirtualHost` @@ -529,7 +503,9 @@ class NginxConfigurator(common.Installer): ipv6_block = ['\n ', 'listen', ' ', - '[::]:{0} ssl'.format(self.config.tls_sni_01_port)] + '[::]:{0}'.format(self.config.tls_sni_01_port), + ' ', + 'ssl'] if not ipv6info[1]: # ipv6only=on is absent in global config ipv6_block.append(' ') @@ -539,8 +515,9 @@ class NginxConfigurator(common.Installer): ipv4_block = ['\n ', 'listen', ' ', - '{0} ssl'.format(self.config.tls_sni_01_port)] - + '{0}'.format(self.config.tls_sni_01_port), + ' ', + 'ssl'] snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() @@ -584,10 +561,12 @@ class NginxConfigurator(common.Installer): raise def _has_certbot_redirect(self, vhost): - return vhost.contains_list(TEST_REDIRECT_BLOCK) + test_redirect_block = _test_block_from_block(REDIRECT_BLOCK) + return vhost.contains_list(test_redirect_block) def _has_certbot_redirect_comment(self, vhost): - return vhost.contains_list(TEST_REDIRECT_COMMENT_BLOCK) + test_redirect_comment_block = _test_block_from_block(REDIRECT_COMMENT_BLOCK) + return vhost.contains_list(test_redirect_comment_block) def _add_redirect_block(self, vhost, active=True): """Add redirect directive to vhost @@ -603,7 +582,8 @@ class NginxConfigurator(common.Installer): def _enable_redirect(self, domain, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. - Add rewrite directive to non https traffic + 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 @@ -616,26 +596,46 @@ class NginxConfigurator(common.Installer): vhost = None # If there are blocks listening plaintextishly on self.DEFAULT_LISTEN_PORT, # choose the most name-matching one. + vhost = self.choose_redirect_vhost(domain, port) if vhost is None: logger.info("No matching insecure server blocks listening on port %s found.", self.DEFAULT_LISTEN_PORT) + return + + if vhost.ssl: + new_vhost = self.parser.duplicate_vhost(vhost, + only_directives=['listen', 'server_name']) + + def _ssl_match_func(directive): + return 'ssl' in directive + + def _no_ssl_match_func(directive): + return 'ssl' not in directive + + # remove all ssl addresses from the new block + self.parser.remove_server_directives(new_vhost, 'listen', match_func=_ssl_match_func) + + # remove all non-ssl addresses from the existing block + self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func) + + vhost = new_vhost + + if self._has_certbot_redirect(vhost): + logger.info("Traffic on port %s already redirecting to ssl in %s", + self.DEFAULT_LISTEN_PORT, vhost.filep) + elif vhost.has_redirect(): + if not self._has_certbot_redirect_comment(vhost): + self._add_redirect_block(vhost, active=False) + logger.info("The appropriate server block is already redirecting " + "traffic. To enable redirect anyway, uncomment the " + "redirect lines in %s.", vhost.filep) else: - if self._has_certbot_redirect(vhost): - logger.info("Traffic on port %s already redirecting to ssl in %s", - self.DEFAULT_LISTEN_PORT, vhost.filep) - elif vhost.has_redirect(): - if not self._has_certbot_redirect_comment(vhost): - self._add_redirect_block(vhost, active=False) - logger.info("The appropriate server block is already redirecting " - "traffic. To enable redirect anyway, uncomment the " - "redirect lines in %s.", vhost.filep) - else: - # Redirect plaintextish host to https - self._add_redirect_block(vhost, active=True) - logger.info("Redirecting all traffic on port %s to ssl in %s", - self.DEFAULT_LISTEN_PORT, vhost.filep) + # Redirect plaintextish host to https + self._add_redirect_block(vhost, active=True) + 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 @@ -809,6 +809,7 @@ class NginxConfigurator(common.Installer): """ super(NginxConfigurator, self).recovery_routine() + self.new_vhost = None self.parser.load() def revert_challenge_config(self): @@ -818,6 +819,7 @@ class NginxConfigurator(common.Installer): """ self.revert_temporary_config() + self.new_vhost = None self.parser.load() def rollback_checkpoints(self, rollback=1): @@ -830,6 +832,7 @@ class NginxConfigurator(common.Installer): """ super(NginxConfigurator, self).rollback_checkpoints(rollback) + self.new_vhost = None self.parser.load() ########################################################################### @@ -882,6 +885,11 @@ class NginxConfigurator(common.Installer): self.restart() +def _test_block_from_block(block): + test_block = nginxparser.UnspacedList(block) + parser.comment_directive(test_block, 0) + return test_block[:-1] + def nginx_restart(nginx_ctl, nginx_conf): """Restarts the Nginx Server. diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 5816c5571..f5ac5c2e3 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -205,7 +205,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods def contains_list(self, test): """Determine if raw server block contains test list at top level """ - for i in six.moves.range(0, len(self.raw) - len(test)): + for i in six.moves.range(0, len(self.raw) - len(test) + 1): if self.raw[i:i + len(test)] == test: return True return False @@ -220,6 +220,8 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods def ipv4_enabled(self): """Return true if one or more of the listen directives in vhost are IPv4 only""" + if self.addrs is None or len(self.addrs) == 0: + return True for a in self.addrs: if not a.ipv6: return True diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 3eb6264aa..9f13bc59f 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -1,5 +1,6 @@ """NginxParser is a member object of the NginxConfigurator class.""" import copy +import functools import glob import logging import os @@ -294,6 +295,30 @@ class NginxParser(object): :param bool replace: Whether to only replace existing directives """ + self._modify_server_directives(vhost, + functools.partial(_add_directives, directives, replace)) + + def remove_server_directives(self, vhost, directive_name, match_func=None): + """Remove all directives of type directive_name. + + :param :class:`~certbot_nginx.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] @@ -302,42 +327,52 @@ class NginxParser(object): if not isinstance(result, list) or len(result) != 2: raise errors.MisconfigurationError("Not a server block.") result = result[1] - _add_directives(result, directives, replace) + block_func(result) - # update vhost based on new directives - new_server = self._get_included_directives(result) - 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 + 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 create_new_vhost_from_default(self, vhost_template): - """Duplicate the default vhost in the configuration files. + def duplicate_vhost(self, vhost_template, delete_default=False, only_directives=None): + """Duplicate the vhost in the configuration files. :param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost whose information we copy + :param bool delete_default: If we should remove default_server + from listen directives in the block. + :param list only_directives: If it exists, only duplicate the named directives. Only + looks at first level of depth; does not expand includes. :returns: A vhost object for the newly created vhost :rtype: :class:`~certbot_nginx.obj.VirtualHost` """ # TODO: https://github.com/certbot/certbot/issues/5185 # put it in the same file as the template, at the same level + new_vhost = copy.deepcopy(vhost_template) + enclosing_block = self.parsed[vhost_template.filep] for index in vhost_template.path[:-1]: enclosing_block = enclosing_block[index] - new_location = vhost_template.path[-1] + 1 raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]]) - enclosing_block.insert(new_location, raw_in_parsed) - new_vhost = copy.deepcopy(vhost_template) - new_vhost.path[-1] = new_location - for addr in new_vhost.addrs: - addr.default = False - for directive in enclosing_block[new_vhost.path[-1]][1]: - if len(directive) > 0 and directive[0] == 'listen' and 'default_server' in directive: - del directive[directive.index('default_server')] + + if only_directives is not None: + new_directives = nginxparser.UnspacedList([]) + for directive in raw_in_parsed[1]: + if len(directive) > 0 and directive[0] in only_directives: + new_directives.append(directive) + raw_in_parsed[1] = new_directives + + self._update_vhost_based_on_new_directives(new_vhost, new_directives) + + enclosing_block.append(raw_in_parsed) + new_vhost.path[-1] = len(enclosing_block) - 1 + if delete_default: + for addr in new_vhost.addrs: + addr.default = False + for directive in enclosing_block[new_vhost.path[-1]][1]: + if (len(directive) > 0 and directive[0] == 'listen' + and 'default_server' in directive): + del directive[directive.index('default_server')] return new_vhost def _parse_ssl_options(ssl_options): @@ -486,7 +521,7 @@ def _is_ssl_on_directive(entry): len(entry) == 2 and entry[0] == 'ssl' and entry[1] == 'on') -def _add_directives(block, directives, replace): +def _add_directives(directives, replace, block): """Adds or replaces directives in a config block. When replace=False, it's an error to try and add a directive that already @@ -498,8 +533,9 @@ def _add_directives(block, directives, replace): ..todo :: Find directives that are in included files. - :param list block: The block to replace in :param list directives: The new directives. + :param bool replace: Described above. + :param list block: The block to replace in """ for directive in directives: @@ -513,8 +549,12 @@ REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE]) COMMENT = ' managed by Certbot' COMMENT_BLOCK = [' ', '#', COMMENT] -def _comment_directive(block, location): - """Add a comment to the end of the line at location.""" +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]: @@ -551,6 +591,12 @@ def _comment_out_directive(block, location, include_location): 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 _add_directive(block, directive, replace): """Adds or replaces a single directive in a config block. @@ -566,19 +612,12 @@ def _add_directive(block, directive, replace): block.append(directive) return - def find_location(direc): - """ Find the index of a config line where the name of the directive matches - the name of the directive we want to add. If no line exists, use None. - """ - return next((index for index, line in enumerate(block) \ - if line and line[0] == direc[0]), None) - - location = find_location(directive) + location = _find_location(block, directive[0]) if replace: if location is not None: block[location] = directive - _comment_directive(block, location) + comment_directive(block, location) return # Append directive. Fail if the name is not a repeatable directive name, # and there is already a copy of that directive with a different value @@ -602,7 +641,7 @@ def _add_directive(block, directive, replace): included_directives = _parse_ssl_options(directive[1]) for included_directive in included_directives: - included_dir_loc = find_location(included_directive) + 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): @@ -614,10 +653,19 @@ def _add_directive(block, directive, replace): if can_append(location, directive_name): block.append(directive) - _comment_directive(block, len(block) - 1) + comment_directive(block, len(block) - 1) elif block[location] != directive: raise errors.MisconfigurationError(err_fmt.format(directive, block[location])) +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 + del block[location] + def _apply_global_addr_ssl(addr_to_ssl, parsed_server): """Apply global sslishness information to the parsed server block """ diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 996bd238b..e708b159a 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -443,10 +443,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_redirect_enhance(self): # Test that we successfully add a redirect when there is # a listen directive - expected = [ - ['if', '($scheme', '!=', '"https")'], - [['return', '301', 'https://$host$request_uri']] - ] + expected = ['return', '301', 'https://$host$request_uri'] example_conf = self.config.parser.abs_path('sites-enabled/example.com') self.config.enhance("www.example.com", "redirect") @@ -462,6 +459,35 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[migration_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + def test_split_for_redirect(self): + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + self.config.deploy_cert( + "example.org", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.enhance("www.example.com", "redirect") + generated_conf = self.config.parser.parsed[example_conf] + self.assertEqual( + [[['server'], [ + ['server_name', '.example.com'], + ['server_name', 'example.*'], [], + ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'], + ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'], + ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'], + ['include', self.config.mod_ssl_conf], ['#', ' managed by Certbot'], + ['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'], + [], []]], + [['server'], [ + ['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + ['return', '301', 'https://$host$request_uri'], ['#', ' managed by Certbot'], + [], []]]], + generated_conf) + @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') def test_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list): @@ -494,9 +520,38 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] expected = [ ['#', ' Redirect non-https traffic to https'], - ['#', ' if ($scheme != "https") {'], - ['#', ' return 301 https://$host$request_uri;'], - ['#', ' } # managed by Certbot'] + ['#', ' return 301 https://$host$request_uri;'], + ] + for line in expected: + self.assertTrue(util.contains_at_depth(generated_conf, line, 2)) + + @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') + @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') + def test_non_certbot_redirect_exists_has_ssl_copy(self, mock_has_redirect, mock_contains_list): + # Test that we add a redirect as a comment if there is already a + # redirect-class statement in the block that isn't managed by certbot + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + + self.config.deploy_cert( + "example.org", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + + # Has a non-Certbot redirect, and has no existing comment + mock_contains_list.return_value = False + mock_has_redirect.return_value = True + with mock.patch("certbot_nginx.configurator.logger") as mock_logger: + self.config.enhance("www.example.com", "redirect") + self.assertEqual(mock_logger.info.call_args[0][0], + "The appropriate server block is already redirecting " + "traffic. To enable redirect anyway, uncomment the " + "redirect lines in %s.") + generated_conf = self.config.parser.parsed[example_conf] + expected = [ + ['#', ' Redirect non-https traffic to https'], + ['#', ' return 301 https://$host$request_uri;'], ] for line in expected: self.assertTrue(util.contains_at_depth(generated_conf, line, 2)) @@ -704,14 +759,18 @@ class NginxConfiguratorTest(util.NginxTest): self.config.parser.load() - expected = [ - ['if', '($scheme', '!=', '"https")'], - [['return', '301', 'https://$host$request_uri']] - ] + expected = ['return', '301', 'https://$host$request_uri'] generated_conf = self.config.parser.parsed[default_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + @mock.patch('certbot.reverter.logger') + @mock.patch('certbot_nginx.parser.NginxParser.load') + def test_parser_reload_after_config_changes(self, mock_parser_load, unused_mock_logger): + self.config.recovery_routine() + self.config.revert_challenge_config() + self.config.rollback_checkpoints() + self.assertTrue(mock_parser_load.call_count == 3) class InstallSslOptionsConfTest(util.NginxTest): """Test that the options-ssl-nginx.conf file is installed and updated properly.""" diff --git a/certbot-nginx/certbot_nginx/tests/obj_test.py b/certbot-nginx/certbot_nginx/tests/obj_test.py index ba136bb78..92cb0e086 100644 --- a/certbot-nginx/certbot_nginx/tests/obj_test.py +++ b/certbot-nginx/certbot_nginx/tests/obj_test.py @@ -171,8 +171,8 @@ class VirtualHostTest(unittest.TestCase): def test_contains_list(self): from certbot_nginx.obj import VirtualHost from certbot_nginx.obj import Addr - from certbot_nginx.configurator import TEST_REDIRECT_BLOCK - test_needle = TEST_REDIRECT_BLOCK + from certbot_nginx.configurator import REDIRECT_BLOCK, _test_block_from_block + test_needle = _test_block_from_block(REDIRECT_BLOCK) test_haystack = [['listen', '80'], ['root', '/var/www/html'], ['index', 'index.html index.htm index.nginx-debian.html'], ['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'], @@ -181,9 +181,7 @@ class VirtualHostTest(unittest.TestCase): ['#', ' managed by Certbot'], ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'], ['#', ' managed by Certbot'], - [['if', '($scheme', '!=', '"https")'], - [['return', '301', 'https://$host$request_uri']] - ], + ['return', '301', 'https://$host$request_uri'], ['#', ' managed by Certbot'], []] vhost_haystack = VirtualHost( "filp", diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index ca5de7ff6..e21acb8ea 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -334,9 +334,9 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods ["\n", "a", " ", "b", "\n"], ["c", " ", "d"], ["\n", "e", " ", "f"]]) - from certbot_nginx.parser import _comment_directive, COMMENT_BLOCK - _comment_directive(block, 1) - _comment_directive(block, 0) + from certbot_nginx.parser import comment_directive, COMMENT_BLOCK + comment_directive(block, 1) + comment_directive(block, 0) self.assertEqual(block.spaced, [ ["\n", "a", " ", "b", "\n"], COMMENT_BLOCK, @@ -406,12 +406,12 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods ]) self.assertTrue(server['ssl']) - def test_create_new_vhost_from_default(self): + 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.create_new_vhost_from_default(default) + new_vhost = nparser.duplicate_vhost(default, delete_default=True) nparser.filedump(ext='') # check properties of new vhost diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index 7f597ac4a..eca198bfe 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -55,7 +55,7 @@ class NginxTlsSni01(common.TLSSNI01): self.configurator.config.tls_sni_01_port) for achall in self.achalls: - vhost = self.configurator.choose_vhost(achall.domain, raise_if_no_match=False) + vhost = self.configurator.choose_vhost(achall.domain, create_if_no_match=True) if vhost is not None and vhost.addrs: addresses.append(list(vhost.addrs)) From e696766ed102a6999a53bba8cb2881348d870487 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 7 Dec 2017 13:48:44 -0800 Subject: [PATCH 35/67] Expand on changes to the Apache plugin --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92d059b53..4acfc0401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). * The Apache plugin now parses some distro specific Apache configuration files on non-Debian systems allowing it to get a clearer picture on the running - Apache configuration. + configuration. Internally, these changes were structured so that external + contributors can easily write patches to make the plugin work in new Apache + configurations. * Certbot better reports network failures by removing information about connection retries from the error output. * An unnecessary question when using Certbot's webroot plugin interactively has @@ -25,6 +27,8 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). * Certbot's NGINX plugin no longer sometimes incorrectly reports that it was unable to deploy a HTTP->HTTPS redirect when requesting Certbot to enable a redirect for multiple domains. +* Problems where the Apache plugin was failing to find directives and + duplicating existing directives on openSUSE have been resolved. * An issue running the test shipped with Certbot and some our DNS plugins with older versions of mock have been resolved. * On some systems, users reported strangely interleaved output depending on From 5d0888809f6337e36616bf753e24405d6d957491 Mon Sep 17 00:00:00 2001 From: Michael Coleman Date: Fri, 8 Dec 2017 12:53:47 +1300 Subject: [PATCH 36/67] Remove slash from document root path in Webroot example (#5293) It seems the document root path to the `--webroot-path`, `-w` option can't have a trailing slash. Here is an example of a user who followed this example and had their certificate signing request error out. https://superuser.com/questions/1273984/why-does-certbot-letsencrypt-recieve-a-403-forbidden --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 4fd0b5ec8..ab4670052 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -106,7 +106,7 @@ specified ``--webroot-path``. So, for instance, :: - certbot certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net + certbot certonly --webroot -w /var/www/example -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and From 00464283824045e026552b8871677b3bfdbebac8 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 8 Dec 2017 12:45:04 -0800 Subject: [PATCH 37/67] print warnings for 3.3 users (#5283) fix errors --- acme/acme/__init__.py | 9 +++++++++ certbot/main.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/acme/acme/__init__.py b/acme/acme/__init__.py index e8a0b16a8..618dda200 100644 --- a/acme/acme/__init__.py +++ b/acme/acme/__init__.py @@ -10,3 +10,12 @@ supported version: `draft-ietf-acme-01`_. https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01 """ +import sys +import warnings + +if sys.version_info[:2] == (3, 3): + warnings.warn( + "Python 3.3 support will be dropped in the next release of " + "acme. Please upgrade your Python version.", + PendingDeprecationWarning, + ) #pragma: no cover diff --git a/certbot/main.py b/certbot/main.py index 2d4881d1d..72af7fbba 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1215,6 +1215,9 @@ def main(cli_args=sys.argv[1:]): # Let plugins_cmd be run as un-privileged user. if config.func != plugins_cmd: raise + if sys.version_info[:2] == (3, 3): + logger.warning("Python 3.3 support will be dropped in the next release " + "of Certbot - please upgrade your Python version.") set_displayer(config) From 8bc785ed4656db9542571b2093815265acaad201 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Dec 2017 16:35:59 -0800 Subject: [PATCH 38/67] Make Travis builds faster in master (#5314) * Remove extra le-auto tests from master * Remove dockerfile-dev test from master * Remove intermediate Python 3.x tests from master * Reorder travis jobs for speed --- .travis.yml | 66 +++++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index 359801622..3d41bfa4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,19 +13,32 @@ before_script: matrix: include: - python: "2.7" - env: TOXENV=cover FYI="this also tests py27" - - python: "2.7" - env: TOXENV=lint - - python: "2.7" - env: TOXENV=py27-oldest + env: TOXENV=py27_install BOULDER_INTEGRATION=1 sudo: required services: docker + - python: "2.7" + env: TOXENV=cover FYI="this also tests py27" + - sudo: required + env: TOXENV=nginx_compat + services: docker + before_install: + addons: + - python: "2.7" + env: TOXENV=lint - python: "2.6" env: TOXENV=py26 sudo: required services: docker - python: "2.7" - env: TOXENV=py27_install BOULDER_INTEGRATION=1 + env: TOXENV=py27-oldest + sudo: required + services: docker + - python: "3.3" + env: TOXENV=py33 + sudo: required + services: docker + - python: "3.6" + env: TOXENV=py36 sudo: required services: docker - sudo: required @@ -33,55 +46,14 @@ matrix: services: docker before_install: addons: - - sudo: required - env: TOXENV=nginx_compat - services: docker - before_install: - addons: - - sudo: required - env: TOXENV=le_auto_precise - services: docker - before_install: - addons: - sudo: required env: TOXENV=le_auto_trusty services: docker before_install: addons: - - sudo: required - env: TOXENV=le_auto_wheezy - services: docker - before_install: - addons: - - sudo: required - env: TOXENV=le_auto_centos6 - services: docker - before_install: - addons: - - sudo: required - env: TOXENV=docker_dev - services: docker - before_install: - addons: - python: "2.7" env: TOXENV=apacheconftest sudo: required - - python: "3.3" - env: TOXENV=py33 - sudo: required - services: docker - - python: "3.4" - env: TOXENV=py34 - sudo: required - services: docker - - python: "3.5" - env: TOXENV=py35 - sudo: required - services: docker - - python: "3.6" - env: TOXENV=py36 - sudo: required - services: docker - python: "2.7" env: TOXENV=nginxroundtrip From 2abc94661a16f1a4c5bc18e9c48870f2db458eac Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Mon, 11 Dec 2017 20:25:09 +0100 Subject: [PATCH 39/67] Use josepy instead of acme.jose. (#5203) --- acme/acme/challenges.py | 2 +- acme/acme/challenges_test.py | 2 +- acme/acme/client.py | 4 +- acme/acme/client_test.py | 2 +- acme/acme/crypto_util_test.py | 2 +- acme/acme/errors.py | 2 +- acme/acme/fields.py | 3 +- acme/acme/fields_test.py | 3 +- acme/acme/jose/__init__.py | 82 --- acme/acme/jose/b64.py | 61 --- acme/acme/jose/b64_test.py | 77 --- acme/acme/jose/errors.py | 35 -- acme/acme/jose/errors_test.py | 17 - acme/acme/jose/interfaces.py | 216 -------- acme/acme/jose/interfaces_test.py | 114 ---- acme/acme/jose/json_util.py | 485 ------------------ acme/acme/jose/json_util_test.py | 381 -------------- acme/acme/jose/jwa.py | 180 ------- acme/acme/jose/jwa_test.py | 104 ---- acme/acme/jose/jwk.py | 281 ---------- acme/acme/jose/jwk_test.py | 191 ------- acme/acme/jose/jws.py | 433 ---------------- acme/acme/jose/jws_test.py | 239 --------- acme/acme/jose/util.py | 226 -------- acme/acme/jose/util_test.py | 199 ------- acme/acme/jws.py | 6 +- acme/acme/jws_test.py | 3 +- acme/acme/messages.py | 9 +- acme/acme/messages_test.py | 2 +- acme/acme/standalone_test.py | 2 +- acme/acme/test_util.py | 3 +- acme/docs/api/jose.rst | 9 +- acme/docs/api/jose/base64.rst | 5 - acme/docs/api/jose/errors.rst | 5 - acme/docs/api/jose/interfaces.rst | 5 - acme/docs/api/jose/json_util.rst | 5 - acme/docs/api/jose/jwa.rst | 5 - acme/docs/api/jose/jwk.rst | 5 - acme/docs/api/jose/jws.rst | 5 - acme/docs/api/jose/util.rst | 5 - acme/docs/conf.py | 1 + acme/examples/example_client.py | 2 +- acme/setup.py | 7 +- certbot-apache/certbot_apache/tests/util.py | 3 +- .../certbot_compatibility_test/util.py | 3 +- certbot-nginx/certbot_nginx/tests/util.py | 3 +- certbot/account.py | 2 +- certbot/achallenges.py | 3 +- certbot/client.py | 2 +- certbot/crypto_util.py | 4 +- certbot/main.py | 2 +- certbot/plugins/common.py | 2 +- certbot/plugins/common_test.py | 2 +- certbot/plugins/dns_test_common.py | 2 +- certbot/plugins/dns_test_common_lexicon.py | 2 +- certbot/plugins/standalone_test.py | 2 +- certbot/plugins/webroot_test.py | 2 +- certbot/tests/account_test.py | 2 +- certbot/tests/acme_util.py | 2 +- certbot/tests/client_test.py | 2 +- certbot/tests/display/ops_test.py | 2 +- certbot/tests/main_test.py | 3 +- certbot/tests/util.py | 3 +- tools/deactivate.py | 2 +- 64 files changed, 53 insertions(+), 3422 deletions(-) delete mode 100644 acme/acme/jose/__init__.py delete mode 100644 acme/acme/jose/b64.py delete mode 100644 acme/acme/jose/b64_test.py delete mode 100644 acme/acme/jose/errors.py delete mode 100644 acme/acme/jose/errors_test.py delete mode 100644 acme/acme/jose/interfaces.py delete mode 100644 acme/acme/jose/interfaces_test.py delete mode 100644 acme/acme/jose/json_util.py delete mode 100644 acme/acme/jose/json_util_test.py delete mode 100644 acme/acme/jose/jwa.py delete mode 100644 acme/acme/jose/jwa_test.py delete mode 100644 acme/acme/jose/jwk.py delete mode 100644 acme/acme/jose/jwk_test.py delete mode 100644 acme/acme/jose/jws.py delete mode 100644 acme/acme/jose/jws_test.py delete mode 100644 acme/acme/jose/util.py delete mode 100644 acme/acme/jose/util_test.py delete mode 100644 acme/docs/api/jose/base64.rst delete mode 100644 acme/docs/api/jose/errors.rst delete mode 100644 acme/docs/api/jose/interfaces.rst delete mode 100644 acme/docs/api/jose/json_util.rst delete mode 100644 acme/docs/api/jose/jwa.rst delete mode 100644 acme/docs/api/jose/jwk.rst delete mode 100644 acme/docs/api/jose/jws.rst delete mode 100644 acme/docs/api/jose/util.rst diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 14641af10..96997297b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -6,13 +6,13 @@ import logging import socket from cryptography.hazmat.primitives import hashes # type: ignore +import josepy as jose import OpenSSL import requests from acme import errors from acme import crypto_util from acme import fields -from acme import jose logger = logging.getLogger(__name__) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 49e790102..834d569aa 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -1,6 +1,7 @@ """Tests for acme.challenges.""" import unittest +import josepy as jose import mock import OpenSSL import requests @@ -8,7 +9,6 @@ import requests from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error from acme import errors -from acme import jose from acme import test_util CERT = test_util.load_comparable_cert('cert.pem') diff --git a/acme/acme/client.py b/acme/acme/client.py index 2e07d34d7..dc5efbe86 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -10,13 +10,13 @@ import time import six from six.moves import http_client # pylint: disable=import-error +import josepy as jose import OpenSSL import re import requests import sys from acme import errors -from acme import jose from acme import jws from acme import messages @@ -408,7 +408,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes :param str uri: URI of certificate :returns: tuple of the form - (response, :class:`acme.jose.ComparableX509`) + (response, :class:`josepy.util.ComparableX509`) :rtype: tuple """ diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 4bd762865..84620fc99 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -5,12 +5,12 @@ import unittest from six.moves import http_client # pylint: disable=import-error +import josepy as jose import mock import requests from acme import challenges from acme import errors -from acme import jose from acme import jws as acme_jws from acme import messages from acme import messages_test diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index da433c5a2..1d7f83ccf 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -8,10 +8,10 @@ import unittest import six from six.moves import socketserver #type: ignore # pylint: disable=import-error +import josepy as jose import OpenSSL from acme import errors -from acme import jose from acme import test_util diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 9d991fd75..de5f9d1f4 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -1,5 +1,5 @@ """ACME errors.""" -from acme.jose import errors as jose_errors +from josepy import errors as jose_errors class Error(Exception): diff --git a/acme/acme/fields.py b/acme/acme/fields.py index 12d09acf4..d7ec78403 100644 --- a/acme/acme/fields.py +++ b/acme/acme/fields.py @@ -1,10 +1,9 @@ """ACME JSON fields.""" import logging +import josepy as jose import pyrfc3339 -from acme import jose - logger = logging.getLogger(__name__) diff --git a/acme/acme/fields_test.py b/acme/acme/fields_test.py index de852b6fa..69dde8b89 100644 --- a/acme/acme/fields_test.py +++ b/acme/acme/fields_test.py @@ -2,10 +2,9 @@ import datetime import unittest +import josepy as jose import pytz -from acme import jose - class FixedTest(unittest.TestCase): """Tests for acme.fields.Fixed.""" diff --git a/acme/acme/jose/__init__.py b/acme/acme/jose/__init__.py deleted file mode 100644 index 9116bc433..000000000 --- a/acme/acme/jose/__init__.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Javascript Object Signing and Encryption (jose). - -This package is a Python implementation of the standards developed by -IETF `Javascript Object Signing and Encryption (Active WG)`_, in -particular the following RFCs: - - - `JSON Web Algorithms (JWA)`_ - - `JSON Web Key (JWK)`_ - - `JSON Web Signature (JWS)`_ - - -.. _`Javascript Object Signing and Encryption (Active WG)`: - https://tools.ietf.org/wg/jose/ - -.. _`JSON Web Algorithms (JWA)`: - https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-algorithms/ - -.. _`JSON Web Key (JWK)`: - https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-key/ - -.. _`JSON Web Signature (JWS)`: - https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-signature/ - -""" -from acme.jose.b64 import ( - b64decode, - b64encode, -) - -from acme.jose.errors import ( - DeserializationError, - SerializationError, - Error, - UnrecognizedTypeError, -) - -from acme.jose.interfaces import JSONDeSerializable - -from acme.jose.json_util import ( - Field, - JSONObjectWithFields, - TypedJSONObjectWithFields, - decode_b64jose, - decode_cert, - decode_csr, - decode_hex16, - encode_b64jose, - encode_cert, - encode_csr, - encode_hex16, -) - -from acme.jose.jwa import ( - HS256, - HS384, - HS512, - JWASignature, - PS256, - PS384, - PS512, - RS256, - RS384, - RS512, -) - -from acme.jose.jwk import ( - JWK, - JWKRSA, -) - -from acme.jose.jws import ( - Header, - JWS, - Signature, -) - -from acme.jose.util import ( - ComparableX509, - ComparableKey, - ComparableRSAKey, - ImmutableMap, -) diff --git a/acme/acme/jose/b64.py b/acme/acme/jose/b64.py deleted file mode 100644 index cf79aa820..000000000 --- a/acme/acme/jose/b64.py +++ /dev/null @@ -1,61 +0,0 @@ -"""JOSE Base64. - -`JOSE Base64`_ is defined as: - - - URL-safe Base64 - - padding stripped - - -.. _`JOSE Base64`: - https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-37#appendix-C - -.. Do NOT try to call this module "base64", as it will "shadow" the - standard library. - -""" -import base64 - -import six - - -def b64encode(data): - """JOSE Base64 encode. - - :param data: Data to be encoded. - :type data: `bytes` - - :returns: JOSE Base64 string. - :rtype: bytes - - :raises TypeError: if `data` is of incorrect type - - """ - if not isinstance(data, six.binary_type): - raise TypeError('argument should be {0}'.format(six.binary_type)) - return base64.urlsafe_b64encode(data).rstrip(b'=') - - -def b64decode(data): - """JOSE Base64 decode. - - :param data: Base64 string to be decoded. If it's unicode, then - only ASCII characters are allowed. - :type data: `bytes` or `unicode` - - :returns: Decoded data. - :rtype: bytes - - :raises TypeError: if input is of incorrect type - :raises ValueError: if input is unicode with non-ASCII characters - - """ - if isinstance(data, six.string_types): - try: - data = data.encode('ascii') - except UnicodeEncodeError: - raise ValueError( - 'unicode argument should contain only ASCII characters') - elif not isinstance(data, six.binary_type): - raise TypeError('argument should be a str or unicode') - - return base64.urlsafe_b64decode(data + b'=' * (4 - (len(data) % 4))) diff --git a/acme/acme/jose/b64_test.py b/acme/acme/jose/b64_test.py deleted file mode 100644 index cbabe2251..000000000 --- a/acme/acme/jose/b64_test.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Tests for acme.jose.b64.""" -import unittest - -import six - - -# https://en.wikipedia.org/wiki/Base64#Examples -B64_PADDING_EXAMPLES = { - b'any carnal pleasure.': (b'YW55IGNhcm5hbCBwbGVhc3VyZS4', b'='), - b'any carnal pleasure': (b'YW55IGNhcm5hbCBwbGVhc3VyZQ', b'=='), - b'any carnal pleasur': (b'YW55IGNhcm5hbCBwbGVhc3Vy', b''), - b'any carnal pleasu': (b'YW55IGNhcm5hbCBwbGVhc3U', b'='), - b'any carnal pleas': (b'YW55IGNhcm5hbCBwbGVhcw', b'=='), -} - - -B64_URL_UNSAFE_EXAMPLES = { - six.int2byte(251) + six.int2byte(239): b'--8', - six.int2byte(255) * 2: b'__8', -} - - -class B64EncodeTest(unittest.TestCase): - """Tests for acme.jose.b64.b64encode.""" - - @classmethod - def _call(cls, data): - from acme.jose.b64 import b64encode - return b64encode(data) - - def test_empty(self): - self.assertEqual(self._call(b''), b'') - - def test_unsafe_url(self): - for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES): - self.assertEqual(self._call(text), b64) - - def test_different_paddings(self): - for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES): - self.assertEqual(self._call(text), b64) - - def test_unicode_fails_with_type_error(self): - self.assertRaises(TypeError, self._call, u'some unicode') - - -class B64DecodeTest(unittest.TestCase): - """Tests for acme.jose.b64.b64decode.""" - - @classmethod - def _call(cls, data): - from acme.jose.b64 import b64decode - return b64decode(data) - - def test_unsafe_url(self): - for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES): - self.assertEqual(self._call(b64), text) - - def test_input_without_padding(self): - for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES): - self.assertEqual(self._call(b64), text) - - def test_input_with_padding(self): - for text, (b64, pad) in six.iteritems(B64_PADDING_EXAMPLES): - self.assertEqual(self._call(b64 + pad), text) - - def test_unicode_with_ascii(self): - self.assertEqual(self._call(u'YQ'), b'a') - - def test_non_ascii_unicode_fails(self): - self.assertRaises(ValueError, self._call, u'\u0105') - - def test_type_error_no_unicode_or_bytes(self): - self.assertRaises(TypeError, self._call, object()) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/errors.py b/acme/acme/jose/errors.py deleted file mode 100644 index 74c9443e1..000000000 --- a/acme/acme/jose/errors.py +++ /dev/null @@ -1,35 +0,0 @@ -"""JOSE errors.""" - - -class Error(Exception): - """Generic JOSE Error.""" - - -class DeserializationError(Error): - """JSON deserialization error.""" - - def __str__(self): - return "Deserialization error: {0}".format( - super(DeserializationError, self).__str__()) - - -class SerializationError(Error): - """JSON serialization error.""" - - -class UnrecognizedTypeError(DeserializationError): - """Unrecognized type error. - - :ivar str typ: The unrecognized type of the JSON object. - :ivar jobj: Full JSON object. - - """ - - def __init__(self, typ, jobj): - self.typ = typ - self.jobj = jobj - super(UnrecognizedTypeError, self).__init__(str(self)) - - def __str__(self): - return '{0} was not recognized, full message: {1}'.format( - self.typ, self.jobj) diff --git a/acme/acme/jose/errors_test.py b/acme/acme/jose/errors_test.py deleted file mode 100644 index 919980920..000000000 --- a/acme/acme/jose/errors_test.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Tests for acme.jose.errors.""" -import unittest - - -class UnrecognizedTypeErrorTest(unittest.TestCase): - def setUp(self): - from acme.jose.errors import UnrecognizedTypeError - self.error = UnrecognizedTypeError('foo', {'type': 'foo'}) - - def test_str(self): - self.assertEqual( - "foo was not recognized, full message: {'type': 'foo'}", - str(self.error)) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/interfaces.py b/acme/acme/jose/interfaces.py deleted file mode 100644 index f841848b3..000000000 --- a/acme/acme/jose/interfaces.py +++ /dev/null @@ -1,216 +0,0 @@ -"""JOSE interfaces.""" -import abc -import collections -import json - -import six - -from acme.jose import errors -from acme.jose import util - -# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class -# pylint: disable=too-few-public-methods - - -@six.add_metaclass(abc.ABCMeta) -class JSONDeSerializable(object): - # pylint: disable=too-few-public-methods - """Interface for (de)serializable JSON objects. - - Please recall, that standard Python library implements - :class:`json.JSONEncoder` and :class:`json.JSONDecoder` that perform - translations based on respective :ref:`conversion tables - ` that look pretty much like the one below (for - complete tables see relevant Python documentation): - - .. _conversion-table: - - ====== ====== - JSON Python - ====== ====== - object dict - ... ... - ====== ====== - - While the above **conversion table** is about translation of JSON - documents to/from the basic Python types only, - :class:`JSONDeSerializable` introduces the following two concepts: - - serialization - Turning an arbitrary Python object into Python object that can - be encoded into a JSON document. **Full serialization** produces - a Python object composed of only basic types as required by the - :ref:`conversion table `. **Partial - serialization** (accomplished by :meth:`to_partial_json`) - produces a Python object that might also be built from other - :class:`JSONDeSerializable` objects. - - deserialization - Turning a decoded Python object (necessarily one of the basic - types as required by the :ref:`conversion table - `) into an arbitrary Python object. - - Serialization produces **serialized object** ("partially serialized - object" or "fully serialized object" for partial and full - serialization respectively) and deserialization produces - **deserialized object**, both usually denoted in the source code as - ``jobj``. - - Wording in the official Python documentation might be confusing - after reading the above, but in the light of those definitions, one - can view :meth:`json.JSONDecoder.decode` as decoder and - deserializer of basic types, :meth:`json.JSONEncoder.default` as - serializer of basic types, :meth:`json.JSONEncoder.encode` as - serializer and encoder of basic types. - - One could extend :mod:`json` to support arbitrary object - (de)serialization either by: - - - overriding :meth:`json.JSONDecoder.decode` and - :meth:`json.JSONEncoder.default` in subclasses - - - or passing ``object_hook`` argument (or ``object_hook_pairs``) - to :func:`json.load`/:func:`json.loads` or ``default`` argument - for :func:`json.dump`/:func:`json.dumps`. - - Interestingly, ``default`` is required to perform only partial - serialization, as :func:`json.dumps` applies ``default`` - recursively. This is the idea behind making :meth:`to_partial_json` - produce only partial serialization, while providing custom - :meth:`json_dumps` that dumps with ``default`` set to - :meth:`json_dump_default`. - - To make further documentation a bit more concrete, please, consider - the following imaginatory implementation example:: - - class Foo(JSONDeSerializable): - def to_partial_json(self): - return 'foo' - - @classmethod - def from_json(cls, jobj): - return Foo() - - class Bar(JSONDeSerializable): - def to_partial_json(self): - return [Foo(), Foo()] - - @classmethod - def from_json(cls, jobj): - return Bar() - - """ - - @abc.abstractmethod - def to_partial_json(self): # pragma: no cover - """Partially serialize. - - Following the example, **partial serialization** means the following:: - - assert isinstance(Bar().to_partial_json()[0], Foo) - assert isinstance(Bar().to_partial_json()[1], Foo) - - # in particular... - assert Bar().to_partial_json() != ['foo', 'foo'] - - :raises acme.jose.errors.SerializationError: - in case of any serialization error. - :returns: Partially serializable object. - - """ - raise NotImplementedError() - - def to_json(self): - """Fully serialize. - - Again, following the example from before, **full serialization** - means the following:: - - assert Bar().to_json() == ['foo', 'foo'] - - :raises acme.jose.errors.SerializationError: - in case of any serialization error. - :returns: Fully serialized object. - - """ - def _serialize(obj): - if isinstance(obj, JSONDeSerializable): - return _serialize(obj.to_partial_json()) - if isinstance(obj, six.string_types): # strings are Sequence - return obj - elif isinstance(obj, list): - return [_serialize(subobj) for subobj in obj] - elif isinstance(obj, collections.Sequence): - # default to tuple, otherwise Mapping could get - # unhashable list - return tuple(_serialize(subobj) for subobj in obj) - elif isinstance(obj, collections.Mapping): - return dict((_serialize(key), _serialize(value)) - for key, value in six.iteritems(obj)) - else: - return obj - - return _serialize(self) - - @util.abstractclassmethod - def from_json(cls, jobj): # pylint: disable=unused-argument - """Deserialize a decoded JSON document. - - :param jobj: Python object, composed of only other basic data - types, as decoded from JSON document. Not necessarily - :class:`dict` (as decoded from "JSON object" document). - - :raises acme.jose.errors.DeserializationError: - if decoding was unsuccessful, e.g. in case of unparseable - X509 certificate, or wrong padding in JOSE base64 encoded - string, etc. - - """ - # TypeError: Can't instantiate abstract class with - # abstract methods from_json, to_partial_json - return cls() # pylint: disable=abstract-class-instantiated - - @classmethod - def json_loads(cls, json_string): - """Deserialize from JSON document string.""" - try: - loads = json.loads(json_string) - except ValueError as error: - raise errors.DeserializationError(error) - return cls.from_json(loads) - - def json_dumps(self, **kwargs): - """Dump to JSON string using proper serializer. - - :returns: JSON document string. - :rtype: str - - """ - return json.dumps(self, default=self.json_dump_default, **kwargs) - - def json_dumps_pretty(self): - """Dump the object to pretty JSON document string. - - :rtype: str - - """ - return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': ')) - - @classmethod - def json_dump_default(cls, python_object): - """Serialize Python object. - - This function is meant to be passed as ``default`` to - :func:`json.dump` or :func:`json.dumps`. They call - ``default(python_object)`` only for non-basic Python types, so - this function necessarily raises :class:`TypeError` if - ``python_object`` is not an instance of - :class:`IJSONSerializable`. - - Please read the class docstring for more information. - - """ - if isinstance(python_object, JSONDeSerializable): - return python_object.to_partial_json() - else: # this branch is necessary, cannot just "return" - raise TypeError(repr(python_object) + ' is not JSON serializable') diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py deleted file mode 100644 index cf98ff371..000000000 --- a/acme/acme/jose/interfaces_test.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Tests for acme.jose.interfaces.""" -import unittest - - -class JSONDeSerializableTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.jose.interfaces import JSONDeSerializable - - # pylint: disable=missing-docstring,invalid-name - - class Basic(JSONDeSerializable): - def __init__(self, v): - self.v = v - - def to_partial_json(self): - return self.v - - @classmethod - def from_json(cls, jobj): - return cls(jobj) - - class Sequence(JSONDeSerializable): - def __init__(self, x, y): - self.x = x - self.y = y - - def to_partial_json(self): - return [self.x, self.y] - - @classmethod - def from_json(cls, jobj): - return cls( - Basic.from_json(jobj[0]), Basic.from_json(jobj[1])) - - class Mapping(JSONDeSerializable): - def __init__(self, x, y): - self.x = x - self.y = y - - def to_partial_json(self): - return {self.x: self.y} - - @classmethod - def from_json(cls, jobj): - pass # pragma: no cover - - self.basic1 = Basic('foo1') - self.basic2 = Basic('foo2') - self.seq = Sequence(self.basic1, self.basic2) - self.mapping = Mapping(self.basic1, self.basic2) - self.nested = Basic([[self.basic1]]) - self.tuple = Basic(('foo',)) - - # pylint: disable=invalid-name - self.Basic = Basic - self.Sequence = Sequence - self.Mapping = Mapping - - def test_to_json_sequence(self): - self.assertEqual(self.seq.to_json(), ['foo1', 'foo2']) - - def test_to_json_mapping(self): - self.assertEqual(self.mapping.to_json(), {'foo1': 'foo2'}) - - def test_to_json_other(self): - mock_value = object() - self.assertTrue(self.Basic(mock_value).to_json() is mock_value) - - def test_to_json_nested(self): - self.assertEqual(self.nested.to_json(), [['foo1']]) - - def test_to_json(self): - self.assertEqual(self.tuple.to_json(), (('foo', ))) - - def test_from_json_not_implemented(self): - from acme.jose.interfaces import JSONDeSerializable - self.assertRaises(TypeError, JSONDeSerializable.from_json, 'xxx') - - def test_json_loads(self): - seq = self.Sequence.json_loads('["foo1", "foo2"]') - self.assertTrue(isinstance(seq, self.Sequence)) - self.assertTrue(isinstance(seq.x, self.Basic)) - self.assertTrue(isinstance(seq.y, self.Basic)) - self.assertEqual(seq.x.v, 'foo1') - self.assertEqual(seq.y.v, 'foo2') - - def test_json_dumps(self): - self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps()) - - def test_json_dumps_pretty(self): - self.assertEqual(self.seq.json_dumps_pretty(), - '[\n "foo1",\n "foo2"\n]') - - def test_json_dump_default(self): - from acme.jose.interfaces import JSONDeSerializable - - self.assertEqual( - 'foo1', JSONDeSerializable.json_dump_default(self.basic1)) - - jobj = JSONDeSerializable.json_dump_default(self.seq) - self.assertEqual(len(jobj), 2) - self.assertTrue(jobj[0] is self.basic1) - self.assertTrue(jobj[1] is self.basic2) - - def test_json_dump_default_type_error(self): - from acme.jose.interfaces import JSONDeSerializable - self.assertRaises( - TypeError, JSONDeSerializable.json_dump_default, object()) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/json_util.py b/acme/acme/jose/json_util.py deleted file mode 100644 index 4baadda5e..000000000 --- a/acme/acme/jose/json_util.py +++ /dev/null @@ -1,485 +0,0 @@ -"""JSON (de)serialization framework. - -The framework presented here is somewhat based on `Go's "json" package`_ -(especially the ``omitempty`` functionality). - -.. _`Go's "json" package`: http://golang.org/pkg/encoding/json/ - -""" -import abc -import binascii -import logging - -import OpenSSL -import six - -from acme.jose import b64 -from acme.jose import errors -from acme.jose import interfaces -from acme.jose import util - - -logger = logging.getLogger(__name__) - - -class Field(object): - """JSON object field. - - :class:`Field` is meant to be used together with - :class:`JSONObjectWithFields`. - - ``encoder`` (``decoder``) is a callable that accepts a single - parameter, i.e. a value to be encoded (decoded), and returns the - serialized (deserialized) value. In case of errors it should raise - :class:`~acme.jose.errors.SerializationError` - (:class:`~acme.jose.errors.DeserializationError`). - - Note, that ``decoder`` should perform partial serialization only. - - :ivar str json_name: Name of the field when encoded to JSON. - :ivar default: Default value (used when not present in JSON object). - :ivar bool omitempty: If ``True`` and the field value is empty, then - it will not be included in the serialized JSON object, and - ``default`` will be used for deserialization. Otherwise, if ``False``, - field is considered as required, value will always be included in the - serialized JSON objected, and it must also be present when - deserializing. - - """ - __slots__ = ('json_name', 'default', 'omitempty', 'fdec', 'fenc') - - def __init__(self, json_name, default=None, omitempty=False, - decoder=None, encoder=None): - # pylint: disable=too-many-arguments - self.json_name = json_name - self.default = default - self.omitempty = omitempty - - self.fdec = self.default_decoder if decoder is None else decoder - self.fenc = self.default_encoder if encoder is None else encoder - - @classmethod - def _empty(cls, value): - """Is the provided value considered "empty" for this field? - - This is useful for subclasses that might want to override the - definition of being empty, e.g. for some more exotic data types. - - """ - return not isinstance(value, bool) and not value - - def omit(self, value): - """Omit the value in output?""" - return self._empty(value) and self.omitempty - - def _update_params(self, **kwargs): - current = dict(json_name=self.json_name, default=self.default, - omitempty=self.omitempty, - decoder=self.fdec, encoder=self.fenc) - current.update(kwargs) - return type(self)(**current) # pylint: disable=star-args - - def decoder(self, fdec): - """Descriptor to change the decoder on JSON object field.""" - return self._update_params(decoder=fdec) - - def encoder(self, fenc): - """Descriptor to change the encoder on JSON object field.""" - return self._update_params(encoder=fenc) - - def decode(self, value): - """Decode a value, optionally with context JSON object.""" - return self.fdec(value) - - def encode(self, value): - """Encode a value, optionally with context JSON object.""" - return self.fenc(value) - - @classmethod - def default_decoder(cls, value): - """Default decoder. - - Recursively deserialize into immutable types ( - :class:`acme.jose.util.frozendict` instead of - :func:`dict`, :func:`tuple` instead of :func:`list`). - - """ - # bases cases for different types returned by json.loads - if isinstance(value, list): - return tuple(cls.default_decoder(subvalue) for subvalue in value) - elif isinstance(value, dict): - return util.frozendict( - dict((cls.default_decoder(key), cls.default_decoder(value)) - for key, value in six.iteritems(value))) - else: # integer or string - return value - - @classmethod - def default_encoder(cls, value): - """Default (passthrough) encoder.""" - # field.to_partial_json() is no good as encoder has to do partial - # serialization only - return value - - -class JSONObjectWithFieldsMeta(abc.ABCMeta): - """Metaclass for :class:`JSONObjectWithFields` and its subclasses. - - It makes sure that, for any class ``cls`` with ``__metaclass__`` - set to ``JSONObjectWithFieldsMeta``: - - 1. All fields (attributes of type :class:`Field`) in the class - definition are moved to the ``cls._fields`` dictionary, where - keys are field attribute names and values are fields themselves. - - 2. ``cls.__slots__`` is extended by all field attribute names - (i.e. not :attr:`Field.json_name`). Original ``cls.__slots__`` - are stored in ``cls._orig_slots``. - - In a consequence, for a field attribute name ``some_field``, - ``cls.some_field`` will be a slot descriptor and not an instance - of :class:`Field`. For example:: - - some_field = Field('someField', default=()) - - class Foo(object): - __metaclass__ = JSONObjectWithFieldsMeta - __slots__ = ('baz',) - some_field = some_field - - assert Foo.__slots__ == ('some_field', 'baz') - assert Foo._orig_slots == () - assert Foo.some_field is not Field - - assert Foo._fields.keys() == ['some_field'] - assert Foo._fields['some_field'] is some_field - - As an implementation note, this metaclass inherits from - :class:`abc.ABCMeta` (and not the usual :class:`type`) to mitigate - the metaclass conflict (:class:`ImmutableMap` and - :class:`JSONDeSerializable`, parents of :class:`JSONObjectWithFields`, - use :class:`abc.ABCMeta` as its metaclass). - - """ - - def __new__(mcs, name, bases, dikt): - fields = {} - - for base in bases: - fields.update(getattr(base, '_fields', {})) - # Do not reorder, this class might override fields from base classes! - for key, value in tuple(six.iteritems(dikt)): - # not six.iterkeys() (in-place edit!) - if isinstance(value, Field): - fields[key] = dikt.pop(key) - - dikt['_orig_slots'] = dikt.get('__slots__', ()) - dikt['__slots__'] = tuple( - list(dikt['_orig_slots']) + list(six.iterkeys(fields))) - dikt['_fields'] = fields - - return abc.ABCMeta.__new__(mcs, name, bases, dikt) - - -@six.add_metaclass(JSONObjectWithFieldsMeta) -class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): - # pylint: disable=too-few-public-methods - """JSON object with fields. - - Example:: - - class Foo(JSONObjectWithFields): - bar = Field('Bar') - empty = Field('Empty', omitempty=True) - - @bar.encoder - def bar(value): - return value + 'bar' - - @bar.decoder - def bar(value): - if not value.endswith('bar'): - raise errors.DeserializationError('No bar suffix!') - return value[:-3] - - assert Foo(bar='baz').to_partial_json() == {'Bar': 'bazbar'} - assert Foo.from_json({'Bar': 'bazbar'}) == Foo(bar='baz') - assert (Foo.from_json({'Bar': 'bazbar', 'Empty': '!'}) - == Foo(bar='baz', empty='!')) - assert Foo(bar='baz').bar == 'baz' - - """ - - @classmethod - def _defaults(cls): - """Get default fields values.""" - return dict([(slot, field.default) for slot, field - in six.iteritems(cls._fields)]) - - def __init__(self, **kwargs): - # pylint: disable=star-args - super(JSONObjectWithFields, self).__init__( - **(dict(self._defaults(), **kwargs))) - - def encode(self, name): - """Encode a single field. - - :param str name: Name of the field to be encoded. - - :raises errors.SerializationError: if field cannot be serialized - :raises errors.Error: if field could not be found - - """ - try: - field = self._fields[name] - except KeyError: - raise errors.Error("Field not found: {0}".format(name)) - - return field.encode(getattr(self, name)) - - def fields_to_partial_json(self): - """Serialize fields to JSON.""" - jobj = {} - omitted = set() - for slot, field in six.iteritems(self._fields): - value = getattr(self, slot) - - if field.omit(value): - omitted.add((slot, value)) - else: - try: - jobj[field.json_name] = field.encode(value) - except errors.SerializationError as error: - raise errors.SerializationError( - 'Could not encode {0} ({1}): {2}'.format( - slot, value, error)) - return jobj - - def to_partial_json(self): - return self.fields_to_partial_json() - - @classmethod - def _check_required(cls, jobj): - missing = set() - for _, field in six.iteritems(cls._fields): - if not field.omitempty and field.json_name not in jobj: - missing.add(field.json_name) - - if missing: - raise errors.DeserializationError( - 'The following fields are required: {0}'.format( - ','.join(missing))) - - @classmethod - def fields_from_json(cls, jobj): - """Deserialize fields from JSON.""" - cls._check_required(jobj) - fields = {} - for slot, field in six.iteritems(cls._fields): - if field.json_name not in jobj and field.omitempty: - fields[slot] = field.default - else: - value = jobj[field.json_name] - try: - fields[slot] = field.decode(value) - except errors.DeserializationError as error: - raise errors.DeserializationError( - 'Could not decode {0!r} ({1!r}): {2}'.format( - slot, value, error)) - return fields - - @classmethod - def from_json(cls, jobj): - return cls(**cls.fields_from_json(jobj)) - - -def encode_b64jose(data): - """Encode JOSE Base-64 field. - - :param bytes data: - :rtype: `unicode` - - """ - # b64encode produces ASCII characters only - return b64.b64encode(data).decode('ascii') - - -def decode_b64jose(data, size=None, minimum=False): - """Decode JOSE Base-64 field. - - :param unicode data: - :param int size: Required length (after decoding). - :param bool minimum: If ``True``, then `size` will be treated as - minimum required length, as opposed to exact equality. - - :rtype: bytes - - """ - error_cls = TypeError if six.PY2 else binascii.Error - try: - decoded = b64.b64decode(data.encode()) - except error_cls as error: - raise errors.DeserializationError(error) - - if size is not None and ((not minimum and len(decoded) != size) or - (minimum and len(decoded) < size)): - raise errors.DeserializationError( - "Expected at least or exactly {0} bytes".format(size)) - - return decoded - - -def encode_hex16(value): - """Hexlify. - - :param bytes value: - :rtype: unicode - - """ - return binascii.hexlify(value).decode() - - -def decode_hex16(value, size=None, minimum=False): - """Decode hexlified field. - - :param unicode value: - :param int size: Required length (after decoding). - :param bool minimum: If ``True``, then `size` will be treated as - minimum required length, as opposed to exact equality. - - :rtype: bytes - - """ - value = value.encode() - if size is not None and ((not minimum and len(value) != size * 2) or - (minimum and len(value) < size * 2)): - raise errors.DeserializationError() - error_cls = TypeError if six.PY2 else binascii.Error - try: - return binascii.unhexlify(value) - except error_cls as error: - raise errors.DeserializationError(error) - - -def encode_cert(cert): - """Encode certificate as JOSE Base-64 DER. - - :type cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` - :rtype: unicode - - """ - return encode_b64jose(OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) - - -def decode_cert(b64der): - """Decode JOSE Base-64 DER-encoded certificate. - - :param unicode b64der: - :rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` - - """ - try: - return util.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der))) - except OpenSSL.crypto.Error as error: - raise errors.DeserializationError(error) - - -def encode_csr(csr): - """Encode CSR as JOSE Base-64 DER. - - :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - :rtype: unicode - - """ - return encode_b64jose(OpenSSL.crypto.dump_certificate_request( - OpenSSL.crypto.FILETYPE_ASN1, csr.wrapped)) - - -def decode_csr(b64der): - """Decode JOSE Base-64 DER-encoded CSR. - - :param unicode b64der: - :rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - - """ - try: - return util.ComparableX509(OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der))) - except OpenSSL.crypto.Error as error: - raise errors.DeserializationError(error) - - -class TypedJSONObjectWithFields(JSONObjectWithFields): - """JSON object with type.""" - - typ = NotImplemented - """Type of the object. Subclasses must override.""" - - type_field_name = "type" - """Field name used to distinguish different object types. - - Subclasses will probably have to override this. - - """ - - TYPES = NotImplemented - """Types registered for JSON deserialization""" - - @classmethod - def register(cls, type_cls, typ=None): - """Register class for JSON deserialization.""" - typ = type_cls.typ if typ is None else typ - cls.TYPES[typ] = type_cls - return type_cls - - @classmethod - def get_type_cls(cls, jobj): - """Get the registered class for ``jobj``.""" - if cls in six.itervalues(cls.TYPES): - if cls.type_field_name not in jobj: - raise errors.DeserializationError( - "Missing type field ({0})".format(cls.type_field_name)) - # cls is already registered type_cls, force to use it - # so that, e.g Revocation.from_json(jobj) fails if - # jobj["type"] != "revocation". - return cls - - if not isinstance(jobj, dict): - raise errors.DeserializationError( - "{0} is not a dictionary object".format(jobj)) - try: - typ = jobj[cls.type_field_name] - except KeyError: - raise errors.DeserializationError("missing type field") - - try: - return cls.TYPES[typ] - except KeyError: - raise errors.UnrecognizedTypeError(typ, jobj) - - def to_partial_json(self): - """Get JSON serializable object. - - :returns: Serializable JSON object representing ACME typed object. - :meth:`validate` will almost certainly not work, due to reasons - explained in :class:`acme.interfaces.IJSONSerializable`. - :rtype: dict - - """ - jobj = self.fields_to_partial_json() - jobj[self.type_field_name] = self.typ - return jobj - - @classmethod - def from_json(cls, jobj): - """Deserialize ACME object from valid JSON object. - - :raises acme.errors.UnrecognizedTypeError: if type - of the ACME object has not been registered. - - """ - # make sure subclasses don't cause infinite recursive from_json calls - type_cls = cls.get_type_cls(jobj) - return type_cls(**type_cls.fields_from_json(jobj)) diff --git a/acme/acme/jose/json_util_test.py b/acme/acme/jose/json_util_test.py deleted file mode 100644 index 25e36211e..000000000 --- a/acme/acme/jose/json_util_test.py +++ /dev/null @@ -1,381 +0,0 @@ -"""Tests for acme.jose.json_util.""" -import itertools -import unittest - -import mock -import six - -from acme import test_util - -from acme.jose import errors -from acme.jose import interfaces -from acme.jose import util - - -CERT = test_util.load_comparable_cert('cert.pem') -CSR = test_util.load_comparable_csr('csr.pem') - - -class FieldTest(unittest.TestCase): - """Tests for acme.jose.json_util.Field.""" - - def test_no_omit_boolean(self): - from acme.jose.json_util import Field - for default, omitempty, value in itertools.product( - [True, False], [True, False], [True, False]): - self.assertFalse( - Field("foo", default=default, omitempty=omitempty).omit(value)) - - def test_descriptors(self): - mock_value = mock.MagicMock() - - # pylint: disable=missing-docstring - - def decoder(unused_value): - return 'd' - - def encoder(unused_value): - return 'e' - - from acme.jose.json_util import Field - field = Field('foo') - - field = field.encoder(encoder) - self.assertEqual('e', field.encode(mock_value)) - - field = field.decoder(decoder) - self.assertEqual('e', field.encode(mock_value)) - self.assertEqual('d', field.decode(mock_value)) - - def test_default_encoder_is_partial(self): - class MockField(interfaces.JSONDeSerializable): - # pylint: disable=missing-docstring - def to_partial_json(self): - return 'foo' # pragma: no cover - - @classmethod - def from_json(cls, jobj): - pass # pragma: no cover - mock_field = MockField() - - from acme.jose.json_util import Field - self.assertTrue(Field.default_encoder(mock_field) is mock_field) - # in particular... - self.assertNotEqual('foo', Field.default_encoder(mock_field)) - - def test_default_encoder_passthrough(self): - mock_value = mock.MagicMock() - from acme.jose.json_util import Field - self.assertTrue(Field.default_encoder(mock_value) is mock_value) - - def test_default_decoder_list_to_tuple(self): - from acme.jose.json_util import Field - self.assertEqual((1, 2, 3), Field.default_decoder([1, 2, 3])) - - def test_default_decoder_dict_to_frozendict(self): - from acme.jose.json_util import Field - obj = Field.default_decoder({'x': 2}) - self.assertTrue(isinstance(obj, util.frozendict)) - self.assertEqual(obj, util.frozendict(x=2)) - - def test_default_decoder_passthrough(self): - mock_value = mock.MagicMock() - from acme.jose.json_util import Field - self.assertTrue(Field.default_decoder(mock_value) is mock_value) - - -class JSONObjectWithFieldsMetaTest(unittest.TestCase): - """Tests for acme.jose.json_util.JSONObjectWithFieldsMeta.""" - - def setUp(self): - from acme.jose.json_util import Field - from acme.jose.json_util import JSONObjectWithFieldsMeta - self.field = Field('Baz') - self.field2 = Field('Baz2') - # pylint: disable=invalid-name,missing-docstring,too-few-public-methods - # pylint: disable=blacklisted-name - - @six.add_metaclass(JSONObjectWithFieldsMeta) - class A(object): - __slots__ = ('bar',) - baz = self.field - - class B(A): - pass - - class C(A): - baz = self.field2 - - self.a_cls = A - self.b_cls = B - self.c_cls = C - - def test_fields(self): - # pylint: disable=protected-access,no-member - self.assertEqual({'baz': self.field}, self.a_cls._fields) - self.assertEqual({'baz': self.field}, self.b_cls._fields) - - def test_fields_inheritance(self): - # pylint: disable=protected-access,no-member - self.assertEqual({'baz': self.field2}, self.c_cls._fields) - - def test_slots(self): - self.assertEqual(('bar', 'baz'), self.a_cls.__slots__) - self.assertEqual(('baz',), self.b_cls.__slots__) - - def test_orig_slots(self): - # pylint: disable=protected-access,no-member - self.assertEqual(('bar',), self.a_cls._orig_slots) - self.assertEqual((), self.b_cls._orig_slots) - - -class JSONObjectWithFieldsTest(unittest.TestCase): - """Tests for acme.jose.json_util.JSONObjectWithFields.""" - # pylint: disable=protected-access - - def setUp(self): - from acme.jose.json_util import JSONObjectWithFields - from acme.jose.json_util import Field - - class MockJSONObjectWithFields(JSONObjectWithFields): - # pylint: disable=invalid-name,missing-docstring,no-self-argument - # pylint: disable=too-few-public-methods - x = Field('x', omitempty=True, - encoder=(lambda x: x * 2), - decoder=(lambda x: x / 2)) - y = Field('y') - z = Field('Z') # on purpose uppercase - - @y.encoder - def y(value): - if value == 500: - raise errors.SerializationError() - return value - - @y.decoder - def y(value): - if value == 500: - raise errors.DeserializationError() - return value - - # pylint: disable=invalid-name - self.MockJSONObjectWithFields = MockJSONObjectWithFields - self.mock = MockJSONObjectWithFields(x=None, y=2, z=3) - - def test_init_defaults(self): - self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3)) - - def test_encode(self): - self.assertEqual(10, self.MockJSONObjectWithFields( - x=5, y=0, z=0).encode("x")) - - def test_encode_wrong_field(self): - self.assertRaises(errors.Error, self.mock.encode, 'foo') - - def test_encode_serialization_error_passthrough(self): - self.assertRaises( - errors.SerializationError, - self.MockJSONObjectWithFields(y=500, z=None).encode, "y") - - def test_fields_to_partial_json_omits_empty(self): - self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3}) - - def test_fields_from_json_fills_default_for_empty(self): - self.assertEqual( - {'x': None, 'y': 2, 'z': 3}, - self.MockJSONObjectWithFields.fields_from_json({'y': 2, 'Z': 3})) - - def test_fields_from_json_fails_on_missing(self): - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'y': 0}) - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'Z': 0}) - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'y': 0}) - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'Z': 0}) - - def test_fields_to_partial_json_encoder(self): - self.assertEqual( - self.MockJSONObjectWithFields(x=1, y=2, z=3).to_partial_json(), - {'x': 2, 'y': 2, 'Z': 3}) - - def test_fields_from_json_decoder(self): - self.assertEqual( - {'x': 2, 'y': 2, 'z': 3}, - self.MockJSONObjectWithFields.fields_from_json( - {'x': 4, 'y': 2, 'Z': 3})) - - def test_fields_to_partial_json_error_passthrough(self): - self.assertRaises( - errors.SerializationError, self.MockJSONObjectWithFields( - x=1, y=500, z=3).to_partial_json) - - def test_fields_from_json_error_passthrough(self): - self.assertRaises( - errors.DeserializationError, - self.MockJSONObjectWithFields.from_json, - {'x': 4, 'y': 500, 'Z': 3}) - - -class DeEncodersTest(unittest.TestCase): - def setUp(self): - self.b64_cert = ( - u'MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhM' - u'CVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKz' - u'ApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxF' - u'DASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIx' - u'ODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRI' - u'wEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTW' - u'ljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwD' - u'QYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1' - u'AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwE' - u'AATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMnd' - u'fk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o' - ) - self.b64_csr = ( - u'MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2F' - u'uMRIwEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECw' - u'wWVW5pdmVyc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb' - u'20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD_N_HW9As' - u'dRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3' - u'C5QIDAQABoCkwJwYJKoZIhvcNAQkOMRowGDAWBgNVHREEDzANggtleGFtcG' - u'xlLmNvbTANBgkqhkiG9w0BAQsFAANBAHJH_O6BtC9aGzEVCMGOZ7z9iIRHW' - u'Szr9x_bOzn7hLwsbXPAgO1QxEwL-X-4g20Gn9XBE1N9W6HCIEut2d8wACg' - ) - - def test_encode_b64jose(self): - from acme.jose.json_util import encode_b64jose - encoded = encode_b64jose(b'x') - self.assertTrue(isinstance(encoded, six.string_types)) - self.assertEqual(u'eA', encoded) - - def test_decode_b64jose(self): - from acme.jose.json_util import decode_b64jose - decoded = decode_b64jose(u'eA') - self.assertTrue(isinstance(decoded, six.binary_type)) - self.assertEqual(b'x', decoded) - - def test_decode_b64jose_padding_error(self): - from acme.jose.json_util import decode_b64jose - self.assertRaises(errors.DeserializationError, decode_b64jose, u'x') - - def test_decode_b64jose_size(self): - from acme.jose.json_util import decode_b64jose - self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3)) - self.assertRaises( - errors.DeserializationError, decode_b64jose, u'Zm9v', size=2) - self.assertRaises( - errors.DeserializationError, decode_b64jose, u'Zm9v', size=4) - - def test_decode_b64jose_minimum_size(self): - from acme.jose.json_util import decode_b64jose - self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3, minimum=True)) - self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=2, minimum=True)) - self.assertRaises(errors.DeserializationError, decode_b64jose, - u'Zm9v', size=4, minimum=True) - - def test_encode_hex16(self): - from acme.jose.json_util import encode_hex16 - encoded = encode_hex16(b'foo') - self.assertEqual(u'666f6f', encoded) - self.assertTrue(isinstance(encoded, six.string_types)) - - def test_decode_hex16(self): - from acme.jose.json_util import decode_hex16 - decoded = decode_hex16(u'666f6f') - self.assertEqual(b'foo', decoded) - self.assertTrue(isinstance(decoded, six.binary_type)) - - def test_decode_hex16_minimum_size(self): - from acme.jose.json_util import decode_hex16 - self.assertEqual(b'foo', decode_hex16(u'666f6f', size=3, minimum=True)) - self.assertEqual(b'foo', decode_hex16(u'666f6f', size=2, minimum=True)) - self.assertRaises(errors.DeserializationError, decode_hex16, - u'666f6f', size=4, minimum=True) - - def test_decode_hex16_odd_length(self): - from acme.jose.json_util import decode_hex16 - self.assertRaises(errors.DeserializationError, decode_hex16, u'x') - - def test_encode_cert(self): - from acme.jose.json_util import encode_cert - self.assertEqual(self.b64_cert, encode_cert(CERT)) - - def test_decode_cert(self): - from acme.jose.json_util import decode_cert - cert = decode_cert(self.b64_cert) - self.assertTrue(isinstance(cert, util.ComparableX509)) - self.assertEqual(cert, CERT) - self.assertRaises(errors.DeserializationError, decode_cert, u'') - - def test_encode_csr(self): - from acme.jose.json_util import encode_csr - self.assertEqual(self.b64_csr, encode_csr(CSR)) - - def test_decode_csr(self): - from acme.jose.json_util import decode_csr - csr = decode_csr(self.b64_csr) - self.assertTrue(isinstance(csr, util.ComparableX509)) - self.assertEqual(csr, CSR) - self.assertRaises(errors.DeserializationError, decode_csr, u'') - - -class TypedJSONObjectWithFieldsTest(unittest.TestCase): - - def setUp(self): - from acme.jose.json_util import TypedJSONObjectWithFields - - # pylint: disable=missing-docstring,abstract-method - # pylint: disable=too-few-public-methods - - class MockParentTypedJSONObjectWithFields(TypedJSONObjectWithFields): - TYPES = {} - type_field_name = 'type' - - @MockParentTypedJSONObjectWithFields.register - class MockTypedJSONObjectWithFields( - MockParentTypedJSONObjectWithFields): - typ = 'test' - __slots__ = ('foo',) - - @classmethod - def fields_from_json(cls, jobj): - return {'foo': jobj['foo']} - - def fields_to_partial_json(self): - return {'foo': self.foo} - - self.parent_cls = MockParentTypedJSONObjectWithFields - self.msg = MockTypedJSONObjectWithFields(foo='bar') - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), { - 'type': 'test', - 'foo': 'bar', - }) - - def test_from_json_non_dict_fails(self): - for value in [[], (), 5, "asd"]: # all possible input types - self.assertRaises( - errors.DeserializationError, self.parent_cls.from_json, value) - - def test_from_json_dict_no_type_fails(self): - self.assertRaises( - errors.DeserializationError, self.parent_cls.from_json, {}) - - def test_from_json_unknown_type_fails(self): - self.assertRaises(errors.UnrecognizedTypeError, - self.parent_cls.from_json, {'type': 'bar'}) - - def test_from_json_returns_obj(self): - self.assertEqual({'foo': 'bar'}, self.parent_cls.from_json( - {'type': 'test', 'foo': 'bar'})) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/jwa.py b/acme/acme/jose/jwa.py deleted file mode 100644 index 9b682ecab..000000000 --- a/acme/acme/jose/jwa.py +++ /dev/null @@ -1,180 +0,0 @@ -"""JSON Web Algorithm. - -https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 - -""" -import abc -import collections -import logging - -import cryptography.exceptions -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes # type: ignore -from cryptography.hazmat.primitives import hmac # type: ignore -from cryptography.hazmat.primitives.asymmetric import padding # type: ignore - -from acme.jose import errors -from acme.jose import interfaces -from acme.jose import jwk - - -logger = logging.getLogger(__name__) - - -class JWA(interfaces.JSONDeSerializable): # pylint: disable=abstract-method - # pylint: disable=too-few-public-methods - # for some reason disable=abstract-method has to be on the line - # above... - """JSON Web Algorithm.""" - - -class JWASignature(JWA, collections.Hashable): # type: ignore - """JSON Web Signature Algorithm.""" - SIGNATURES = {} # type: dict - - def __init__(self, name): - self.name = name - - def __eq__(self, other): - if not isinstance(other, JWASignature): - return NotImplemented - return self.name == other.name - - def __hash__(self): - return hash((self.__class__, self.name)) - - def __ne__(self, other): - return not self == other - - @classmethod - def register(cls, signature_cls): - """Register class for JSON deserialization.""" - cls.SIGNATURES[signature_cls.name] = signature_cls - return signature_cls - - def to_partial_json(self): - return self.name - - @classmethod - def from_json(cls, jobj): - return cls.SIGNATURES[jobj] - - @abc.abstractmethod - def sign(self, key, msg): # pragma: no cover - """Sign the ``msg`` using ``key``.""" - raise NotImplementedError() - - @abc.abstractmethod - def verify(self, key, msg, sig): # pragma: no cover - """Verify the ``msg` and ``sig`` using ``key``.""" - raise NotImplementedError() - - def __repr__(self): - return self.name - - -class _JWAHS(JWASignature): - - kty = jwk.JWKOct - - def __init__(self, name, hash_): - super(_JWAHS, self).__init__(name) - self.hash = hash_() - - def sign(self, key, msg): - signer = hmac.HMAC(key, self.hash, backend=default_backend()) - signer.update(msg) - return signer.finalize() - - def verify(self, key, msg, sig): - verifier = hmac.HMAC(key, self.hash, backend=default_backend()) - verifier.update(msg) - try: - verifier.verify(sig) - except cryptography.exceptions.InvalidSignature as error: - logger.debug(error, exc_info=True) - return False - else: - return True - - -class _JWARSA(object): - - kty = jwk.JWKRSA - padding = NotImplemented - hash = NotImplemented - - def sign(self, key, msg): - """Sign the ``msg`` using ``key``.""" - try: - signer = key.signer(self.padding, self.hash) - except AttributeError as error: - logger.debug(error, exc_info=True) - raise errors.Error("Public key cannot be used for signing") - except ValueError as error: # digest too large - logger.debug(error, exc_info=True) - raise errors.Error(str(error)) - signer.update(msg) - try: - return signer.finalize() - except ValueError as error: - logger.debug(error, exc_info=True) - raise errors.Error(str(error)) - - def verify(self, key, msg, sig): - """Verify the ``msg` and ``sig`` using ``key``.""" - verifier = key.verifier(sig, self.padding, self.hash) - verifier.update(msg) - try: - verifier.verify() - except cryptography.exceptions.InvalidSignature as error: - logger.debug(error, exc_info=True) - return False - else: - return True - - -class _JWARS(_JWARSA, JWASignature): - - def __init__(self, name, hash_): - super(_JWARS, self).__init__(name) - self.padding = padding.PKCS1v15() - self.hash = hash_() - - -class _JWAPS(_JWARSA, JWASignature): - - def __init__(self, name, hash_): - super(_JWAPS, self).__init__(name) - self.padding = padding.PSS( - mgf=padding.MGF1(hash_()), - salt_length=padding.PSS.MAX_LENGTH) - self.hash = hash_() - - -class _JWAES(JWASignature): # pylint: disable=abstract-class-not-used - - # TODO: implement ES signatures - - def sign(self, key, msg): # pragma: no cover - raise NotImplementedError() - - def verify(self, key, msg, sig): # pragma: no cover - raise NotImplementedError() - - -HS256 = JWASignature.register(_JWAHS('HS256', hashes.SHA256)) -HS384 = JWASignature.register(_JWAHS('HS384', hashes.SHA384)) -HS512 = JWASignature.register(_JWAHS('HS512', hashes.SHA512)) - -RS256 = JWASignature.register(_JWARS('RS256', hashes.SHA256)) -RS384 = JWASignature.register(_JWARS('RS384', hashes.SHA384)) -RS512 = JWASignature.register(_JWARS('RS512', hashes.SHA512)) - -PS256 = JWASignature.register(_JWAPS('PS256', hashes.SHA256)) -PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384)) -PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512)) - -ES256 = JWASignature.register(_JWAES('ES256')) -ES384 = JWASignature.register(_JWAES('ES384')) -ES512 = JWASignature.register(_JWAES('ES512')) diff --git a/acme/acme/jose/jwa_test.py b/acme/acme/jose/jwa_test.py deleted file mode 100644 index 3328d083a..000000000 --- a/acme/acme/jose/jwa_test.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Tests for acme.jose.jwa.""" -import unittest - -from acme import test_util - -from acme.jose import errors - - -RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') -RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') -RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem') - - -class JWASignatureTest(unittest.TestCase): - """Tests for acme.jose.jwa.JWASignature.""" - - def setUp(self): - from acme.jose.jwa import JWASignature - - class MockSig(JWASignature): - # pylint: disable=missing-docstring,too-few-public-methods - # pylint: disable=abstract-class-not-used - def sign(self, key, msg): - raise NotImplementedError() # pragma: no cover - - def verify(self, key, msg, sig): - raise NotImplementedError() # pragma: no cover - - # pylint: disable=invalid-name - self.Sig1 = MockSig('Sig1') - self.Sig2 = MockSig('Sig2') - - def test_eq(self): - self.assertEqual(self.Sig1, self.Sig1) - - def test_ne(self): - self.assertNotEqual(self.Sig1, self.Sig2) - - def test_ne_other_type(self): - self.assertNotEqual(self.Sig1, 5) - - def test_repr(self): - self.assertEqual('Sig1', repr(self.Sig1)) - self.assertEqual('Sig2', repr(self.Sig2)) - - def test_to_partial_json(self): - self.assertEqual(self.Sig1.to_partial_json(), 'Sig1') - self.assertEqual(self.Sig2.to_partial_json(), 'Sig2') - - def test_from_json(self): - from acme.jose.jwa import JWASignature - from acme.jose.jwa import RS256 - self.assertTrue(JWASignature.from_json('RS256') is RS256) - - -class JWAHSTest(unittest.TestCase): # pylint: disable=too-few-public-methods - - def test_it(self): - from acme.jose.jwa import HS256 - sig = ( - b"\xceR\xea\xcd\x94\xab\xcf\xfb\xe0\xacA.:\x1a'\x08i\xe2\xc4" - b"\r\x85+\x0e\x85\xaeUZ\xd4\xb3\x97zO" - ) - self.assertEqual(HS256.sign(b'some key', b'foo'), sig) - self.assertTrue(HS256.verify(b'some key', b'foo', sig) is True) - self.assertTrue(HS256.verify(b'some key', b'foo', sig + b'!') is False) - - -class JWARSTest(unittest.TestCase): - - def test_sign_no_private_part(self): - from acme.jose.jwa import RS256 - self.assertRaises( - errors.Error, RS256.sign, RSA512_KEY.public_key(), b'foo') - - def test_sign_key_too_small(self): - from acme.jose.jwa import RS256 - from acme.jose.jwa import PS256 - self.assertRaises(errors.Error, RS256.sign, RSA256_KEY, b'foo') - self.assertRaises(errors.Error, PS256.sign, RSA256_KEY, b'foo') - - def test_rs(self): - from acme.jose.jwa import RS256 - sig = ( - b'|\xc6\xb2\xa4\xab(\x87\x99\xfa*:\xea\xf8\xa0N&}\x9f\x0f\xc0O' - b'\xc6t\xa3\xe6\xfa\xbb"\x15Y\x80Y\xe0\x81\xb8\x88)\xba\x0c\x9c' - b'\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99' - b'\xd2\xb9.>}\xfd' - ) - self.assertEqual(RS256.sign(RSA512_KEY, b'foo'), sig) - self.assertTrue(RS256.verify(RSA512_KEY.public_key(), b'foo', sig)) - self.assertFalse(RS256.verify( - RSA512_KEY.public_key(), b'foo', sig + b'!')) - - def test_ps(self): - from acme.jose.jwa import PS256 - sig = PS256.sign(RSA1024_KEY, b'foo') - self.assertTrue(PS256.verify(RSA1024_KEY.public_key(), b'foo', sig)) - self.assertFalse(PS256.verify( - RSA1024_KEY.public_key(), b'foo', sig + b'!')) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py deleted file mode 100644 index 54423f670..000000000 --- a/acme/acme/jose/jwk.py +++ /dev/null @@ -1,281 +0,0 @@ -"""JSON Web Key.""" -import abc -import binascii -import json -import logging - -import cryptography.exceptions -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes # type: ignore -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec # type: ignore -from cryptography.hazmat.primitives.asymmetric import rsa - -import six - -from acme.jose import errors -from acme.jose import json_util -from acme.jose import util - - -logger = logging.getLogger(__name__) - - -class JWK(json_util.TypedJSONObjectWithFields): - # pylint: disable=too-few-public-methods - """JSON Web Key.""" - type_field_name = 'kty' - TYPES = {} # type: dict - cryptography_key_types = () # type: tuple - """Subclasses should override.""" - - required = NotImplemented - """Required members of public key's representation as defined by JWK/JWA.""" - - _thumbprint_json_dumps_params = { - # "no whitespace or line breaks before or after any syntactic - # elements" - 'indent': None, - 'separators': (',', ':'), - # "members ordered lexicographically by the Unicode [UNICODE] - # code points of the member names" - 'sort_keys': True, - } - - def thumbprint(self, hash_function=hashes.SHA256): - """Compute JWK Thumbprint. - - https://tools.ietf.org/html/rfc7638 - - :returns bytes: - - """ - digest = hashes.Hash(hash_function(), backend=default_backend()) - digest.update(json.dumps( - dict((k, v) for k, v in six.iteritems(self.to_json()) - if k in self.required), - **self._thumbprint_json_dumps_params).encode()) - return digest.finalize() - - @abc.abstractmethod - def public_key(self): # pragma: no cover - """Generate JWK with public key. - - For symmetric cryptosystems, this would return ``self``. - - """ - raise NotImplementedError() - - @classmethod - def _load_cryptography_key(cls, data, password=None, backend=None): - backend = default_backend() if backend is None else backend - exceptions = {} - - # private key? - for loader in (serialization.load_pem_private_key, - serialization.load_der_private_key): - try: - return loader(data, password, backend) - except (ValueError, TypeError, - cryptography.exceptions.UnsupportedAlgorithm) as error: - exceptions[loader] = error - - # public key? - for loader in (serialization.load_pem_public_key, - serialization.load_der_public_key): - try: - return loader(data, backend) - except (ValueError, - cryptography.exceptions.UnsupportedAlgorithm) as error: - exceptions[loader] = error - - # no luck - raise errors.Error('Unable to deserialize key: {0}'.format(exceptions)) - - @classmethod - def load(cls, data, password=None, backend=None): - """Load serialized key as JWK. - - :param str data: Public or private key serialized as PEM or DER. - :param str password: Optional password. - :param backend: A `.PEMSerializationBackend` and - `.DERSerializationBackend` provider. - - :raises errors.Error: if unable to deserialize, or unsupported - JWK algorithm - - :returns: JWK of an appropriate type. - :rtype: `JWK` - - """ - try: - key = cls._load_cryptography_key(data, password, backend) - except errors.Error as error: - logger.debug('Loading symmetric key, asymmetric failed: %s', error) - return JWKOct(key=data) - - if cls.typ is not NotImplemented and not isinstance( - key, cls.cryptography_key_types): - raise errors.Error('Unable to deserialize {0} into {1}'.format( - key.__class__, cls.__class__)) - for jwk_cls in six.itervalues(cls.TYPES): - if isinstance(key, jwk_cls.cryptography_key_types): - return jwk_cls(key=key) - raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__)) - - -@JWK.register -class JWKES(JWK): # pragma: no cover - # pylint: disable=abstract-class-not-used - """ES JWK. - - .. warning:: This is not yet implemented! - - """ - typ = 'ES' - cryptography_key_types = ( - ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) - required = ('crv', JWK.type_field_name, 'x', 'y') - - def fields_to_partial_json(self): - raise NotImplementedError() - - @classmethod - def fields_from_json(cls, jobj): - raise NotImplementedError() - - def public_key(self): - raise NotImplementedError() - - -@JWK.register -class JWKOct(JWK): - """Symmetric JWK.""" - typ = 'oct' - __slots__ = ('key',) - required = ('k', JWK.type_field_name) - - def fields_to_partial_json(self): - # TODO: An "alg" member SHOULD also be present to identify the - # algorithm intended to be used with the key, unless the - # application uses another means or convention to determine - # the algorithm used. - return {'k': json_util.encode_b64jose(self.key)} - - @classmethod - def fields_from_json(cls, jobj): - return cls(key=json_util.decode_b64jose(jobj['k'])) - - def public_key(self): - return self - - -@JWK.register -class JWKRSA(JWK): - """RSA JWK. - - :ivar key: `cryptography.hazmat.primitives.rsa.RSAPrivateKey` - or `cryptography.hazmat.primitives.rsa.RSAPublicKey` wrapped - in `.ComparableRSAKey` - - """ - typ = 'RSA' - cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) - __slots__ = ('key',) - required = ('e', JWK.type_field_name, 'n') - - def __init__(self, *args, **kwargs): - if 'key' in kwargs and not isinstance( - kwargs['key'], util.ComparableRSAKey): - kwargs['key'] = util.ComparableRSAKey(kwargs['key']) - super(JWKRSA, self).__init__(*args, **kwargs) - - @classmethod - def _encode_param(cls, data): - """Encode Base64urlUInt. - - :type data: long - :rtype: unicode - - """ - def _leading_zeros(arg): - if len(arg) % 2: - return '0' + arg - return arg - - return json_util.encode_b64jose(binascii.unhexlify( - _leading_zeros(hex(data)[2:].rstrip('L')))) - - @classmethod - def _decode_param(cls, data): - """Decode Base64urlUInt.""" - try: - return int(binascii.hexlify(json_util.decode_b64jose(data)), 16) - except ValueError: # invalid literal for long() with base 16 - raise errors.DeserializationError() - - def public_key(self): - return type(self)(key=self.key.public_key()) - - @classmethod - def fields_from_json(cls, jobj): - # pylint: disable=invalid-name - n, e = (cls._decode_param(jobj[x]) for x in ('n', 'e')) - public_numbers = rsa.RSAPublicNumbers(e=e, n=n) - if 'd' not in jobj: # public key - key = public_numbers.public_key(default_backend()) - else: # private key - d = cls._decode_param(jobj['d']) - if ('p' in jobj or 'q' in jobj or 'dp' in jobj or - 'dq' in jobj or 'qi' in jobj or 'oth' in jobj): - # "If the producer includes any of the other private - # key parameters, then all of the others MUST be - # present, with the exception of "oth", which MUST - # only be present when more than two prime factors - # were used." - p, q, dp, dq, qi, = all_params = tuple( - jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi')) - if tuple(param for param in all_params if param is None): - raise errors.Error( - 'Some private parameters are missing: {0}'.format( - all_params)) - p, q, dp, dq, qi = tuple( - cls._decode_param(x) for x in all_params) - - # TODO: check for oth - else: - # cryptography>=0.8 - p, q = rsa.rsa_recover_prime_factors(n, e, d) - dp = rsa.rsa_crt_dmp1(d, p) - dq = rsa.rsa_crt_dmq1(d, q) - qi = rsa.rsa_crt_iqmp(p, q) - - key = rsa.RSAPrivateNumbers( - p, q, d, dp, dq, qi, public_numbers).private_key( - default_backend()) - - return cls(key=key) - - def fields_to_partial_json(self): - # pylint: disable=protected-access - if isinstance(self.key._wrapped, rsa.RSAPublicKey): - numbers = self.key.public_numbers() - params = { - 'n': numbers.n, - 'e': numbers.e, - } - else: # rsa.RSAPrivateKey - private = self.key.private_numbers() - public = self.key.public_key().public_numbers() - params = { - 'n': public.n, - 'e': public.e, - 'd': private.d, - 'p': private.p, - 'q': private.q, - 'dp': private.dmp1, - 'dq': private.dmq1, - 'qi': private.iqmp, - } - return dict((key, self._encode_param(value)) - for key, value in six.iteritems(params)) diff --git a/acme/acme/jose/jwk_test.py b/acme/acme/jose/jwk_test.py deleted file mode 100644 index eea5793bf..000000000 --- a/acme/acme/jose/jwk_test.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Tests for acme.jose.jwk.""" -import binascii -import unittest - -from acme import test_util - -from acme.jose import errors -from acme.jose import json_util -from acme.jose import util - - -DSA_PEM = test_util.load_vector('dsa512_key.pem') -RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') -RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') - - -class JWKTest(unittest.TestCase): - """Tests for acme.jose.jwk.JWK.""" - - def test_load(self): - from acme.jose.jwk import JWK - self.assertRaises(errors.Error, JWK.load, DSA_PEM) - - def test_load_subclass_wrong_type(self): - from acme.jose.jwk import JWKRSA - self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM) - - -class JWKTestBaseMixin(object): - """Mixin test for JWK subclass tests.""" - - thumbprint = NotImplemented - - def test_thumbprint_private(self): - self.assertEqual(self.thumbprint, self.jwk.thumbprint()) - - def test_thumbprint_public(self): - self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint()) - - -class JWKOctTest(unittest.TestCase, JWKTestBaseMixin): - """Tests for acme.jose.jwk.JWKOct.""" - - thumbprint = (b"\xf3\xe7\xbe\xa8`\xd2\xdap\xe9}\x9c\xce>" - b"\xd0\xfcI\xbe\xcd\x92'\xd4o\x0e\xf41\xea" - b"\x8e(\x8a\xb2i\x1c") - - def setUp(self): - from acme.jose.jwk import JWKOct - self.jwk = JWKOct(key=b'foo') - self.jobj = {'kty': 'oct', 'k': json_util.encode_b64jose(b'foo')} - - def test_to_partial_json(self): - self.assertEqual(self.jwk.to_partial_json(), self.jobj) - - def test_from_json(self): - from acme.jose.jwk import JWKOct - self.assertEqual(self.jwk, JWKOct.from_json(self.jobj)) - - def test_from_json_hashable(self): - from acme.jose.jwk import JWKOct - hash(JWKOct.from_json(self.jobj)) - - def test_load(self): - from acme.jose.jwk import JWKOct - self.assertEqual(self.jwk, JWKOct.load(b'foo')) - - def test_public_key(self): - self.assertTrue(self.jwk.public_key() is self.jwk) - - -class JWKRSATest(unittest.TestCase, JWKTestBaseMixin): - """Tests for acme.jose.jwk.JWKRSA.""" - # pylint: disable=too-many-instance-attributes - - thumbprint = (b'\x83K\xdc#3\x98\xca\x98\xed\xcb\x80\x80<\x0c' - b'\xf0\x95\xb9H\xb2*l\xbd$\xe5&|O\x91\xd4 \xb0Y') - - def setUp(self): - from acme.jose.jwk import JWKRSA - self.jwk256 = JWKRSA(key=RSA256_KEY.public_key()) - self.jwk256json = { - 'kty': 'RSA', - 'e': 'AQAB', - 'n': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk', - } - # pylint: disable=protected-access - self.jwk256_not_comparable = JWKRSA( - key=RSA256_KEY.public_key()._wrapped) - self.jwk512 = JWKRSA(key=RSA512_KEY.public_key()) - self.jwk512json = { - 'kty': 'RSA', - 'e': 'AQAB', - 'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5' - '80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q', - } - self.private = JWKRSA(key=RSA256_KEY) - self.private_json_small = self.jwk256json.copy() - self.private_json_small['d'] = ( - 'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE') - self.private_json = self.jwk256json.copy() - self.private_json.update({ - 'd': 'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE', - 'p': 'zUVNZn4lLLBD1R6NE8TKNQ', - 'q': 'wcfKfc7kl5jfqXArCRSURQ', - 'dp': 'CWJFq43QvT5Bm5iN8n1okQ', - 'dq': 'bHh2u7etM8LKKCF2pY2UdQ', - 'qi': 'oi45cEkbVoJjAbnQpFY87Q', - }) - self.jwk = self.private - - def test_init_auto_comparable(self): - self.assertTrue(isinstance( - self.jwk256_not_comparable.key, util.ComparableRSAKey)) - self.assertEqual(self.jwk256, self.jwk256_not_comparable) - - def test_encode_param_zero(self): - from acme.jose.jwk import JWKRSA - # pylint: disable=protected-access - # TODO: move encode/decode _param to separate class - self.assertEqual('AA', JWKRSA._encode_param(0)) - - def test_equals(self): - self.assertEqual(self.jwk256, self.jwk256) - self.assertEqual(self.jwk512, self.jwk512) - - def test_not_equals(self): - self.assertNotEqual(self.jwk256, self.jwk512) - self.assertNotEqual(self.jwk512, self.jwk256) - - def test_load(self): - from acme.jose.jwk import JWKRSA - self.assertEqual(self.private, JWKRSA.load( - test_util.load_vector('rsa256_key.pem'))) - - def test_public_key(self): - self.assertEqual(self.jwk256, self.private.public_key()) - - def test_to_partial_json(self): - self.assertEqual(self.jwk256.to_partial_json(), self.jwk256json) - self.assertEqual(self.jwk512.to_partial_json(), self.jwk512json) - self.assertEqual(self.private.to_partial_json(), self.private_json) - - def test_from_json(self): - from acme.jose.jwk import JWK - self.assertEqual( - self.jwk256, JWK.from_json(self.jwk256json)) - self.assertEqual( - self.jwk512, JWK.from_json(self.jwk512json)) - self.assertEqual(self.private, JWK.from_json(self.private_json)) - - def test_from_json_private_small(self): - from acme.jose.jwk import JWK - self.assertEqual(self.private, JWK.from_json(self.private_json_small)) - - def test_from_json_missing_one_additional(self): - from acme.jose.jwk import JWK - del self.private_json['q'] - self.assertRaises(errors.Error, JWK.from_json, self.private_json) - - def test_from_json_hashable(self): - from acme.jose.jwk import JWK - hash(JWK.from_json(self.jwk256json)) - - def test_from_json_non_schema_errors(self): - # valid against schema, but still failing - from acme.jose.jwk import JWK - self.assertRaises(errors.DeserializationError, JWK.from_json, - {'kty': 'RSA', 'e': 'AQAB', 'n': ''}) - self.assertRaises(errors.DeserializationError, JWK.from_json, - {'kty': 'RSA', 'e': 'AQAB', 'n': '1'}) - - def test_thumbprint_go_jose(self): - # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk.go#L155 - # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L331-L344 - # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L384 - from acme.jose.jwk import JWKRSA - key = JWKRSA.json_loads("""{ - "kty": "RSA", - "kid": "bilbo.baggins@hobbiton.example", - "use": "sig", - "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", - "e": "AQAB" -}""") - self.assertEqual( - binascii.hexlify(key.thumbprint()), - b"f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932") - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/jws.py b/acme/acme/jose/jws.py deleted file mode 100644 index 5f446e4b1..000000000 --- a/acme/acme/jose/jws.py +++ /dev/null @@ -1,433 +0,0 @@ -"""JOSE Web Signature.""" -import argparse -import base64 -import sys - -import OpenSSL -import six - -from acme.jose import b64 -from acme.jose import errors -from acme.jose import json_util -from acme.jose import jwa -from acme.jose import jwk -from acme.jose import util - - -class MediaType(object): - """MediaType field encoder/decoder.""" - - PREFIX = 'application/' - """MIME Media Type and Content Type prefix.""" - - @classmethod - def decode(cls, value): - """Decoder.""" - # 4.1.10 - if '/' not in value: - if ';' in value: - raise errors.DeserializationError('Unexpected semi-colon') - return cls.PREFIX + value - return value - - @classmethod - def encode(cls, value): - """Encoder.""" - # 4.1.10 - if ';' not in value: - assert value.startswith(cls.PREFIX) - return value[len(cls.PREFIX):] - return value - - -class Header(json_util.JSONObjectWithFields): - """JOSE Header. - - .. warning:: This class supports **only** Registered Header - Parameter Names (as defined in section 4.1 of the - protocol). If you need Public Header Parameter Names (4.2) - or Private Header Parameter Names (4.3), you must subclass - and override :meth:`from_json` and :meth:`to_partial_json` - appropriately. - - .. warning:: This class does not support any extensions through - the "crit" (Critical) Header Parameter (4.1.11) and as a - conforming implementation, :meth:`from_json` treats its - occurrence as an error. Please subclass if you seek for - a different behaviour. - - :ivar x5tS256: "x5t#S256" - :ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`. - :ivar str cty: Content-Type, inc. :const:`MediaType.PREFIX`. - - """ - alg = json_util.Field( - 'alg', decoder=jwa.JWASignature.from_json, omitempty=True) - jku = json_util.Field('jku', omitempty=True) - jwk = json_util.Field('jwk', decoder=jwk.JWK.from_json, omitempty=True) - kid = json_util.Field('kid', omitempty=True) - x5u = json_util.Field('x5u', omitempty=True) - x5c = json_util.Field('x5c', omitempty=True, default=()) - x5t = json_util.Field( - 'x5t', decoder=json_util.decode_b64jose, omitempty=True) - x5tS256 = json_util.Field( - 'x5t#S256', decoder=json_util.decode_b64jose, omitempty=True) - typ = json_util.Field('typ', encoder=MediaType.encode, - decoder=MediaType.decode, omitempty=True) - cty = json_util.Field('cty', encoder=MediaType.encode, - decoder=MediaType.decode, omitempty=True) - crit = json_util.Field('crit', omitempty=True, default=()) - - def not_omitted(self): - """Fields that would not be omitted in the JSON object.""" - return dict((name, getattr(self, name)) - for name, field in six.iteritems(self._fields) - if not field.omit(getattr(self, name))) - - def __add__(self, other): - if not isinstance(other, type(self)): - raise TypeError('Header cannot be added to: {0}'.format( - type(other))) - - not_omitted_self = self.not_omitted() - not_omitted_other = other.not_omitted() - - if set(not_omitted_self).intersection(not_omitted_other): - raise TypeError('Addition of overlapping headers not defined') - - not_omitted_self.update(not_omitted_other) - return type(self)(**not_omitted_self) # pylint: disable=star-args - - def find_key(self): - """Find key based on header. - - .. todo:: Supports only "jwk" header parameter lookup. - - :returns: (Public) key found in the header. - :rtype: .JWK - - :raises acme.jose.errors.Error: if key could not be found - - """ - if self.jwk is None: - raise errors.Error('No key found') - return self.jwk - - @crit.decoder - def crit(unused_value): - # pylint: disable=missing-docstring,no-self-argument,no-self-use - raise errors.DeserializationError( - '"crit" is not supported, please subclass') - - # x5c does NOT use JOSE Base64 (4.1.6) - - @x5c.encoder # type: ignore - def x5c(value): # pylint: disable=missing-docstring,no-self-argument - return [base64.b64encode(OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) for cert in value] - - @x5c.decoder # type: ignore - def x5c(value): # pylint: disable=missing-docstring,no-self-argument - try: - return tuple(util.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, - base64.b64decode(cert))) for cert in value) - except OpenSSL.crypto.Error as error: - raise errors.DeserializationError(error) - - -class Signature(json_util.JSONObjectWithFields): - """JWS Signature. - - :ivar combined: Combined Header (protected and unprotected, - :class:`Header`). - :ivar unicode protected: JWS protected header (Jose Base-64 decoded). - :ivar header: JWS Unprotected Header (:class:`Header`). - :ivar str signature: The signature. - - """ - header_cls = Header - - __slots__ = ('combined',) - protected = json_util.Field('protected', omitempty=True, default='') - header = json_util.Field( - 'header', omitempty=True, default=header_cls(), - decoder=header_cls.from_json) - signature = json_util.Field( - 'signature', decoder=json_util.decode_b64jose, - encoder=json_util.encode_b64jose) - - @protected.encoder # type: ignore - def protected(value): # pylint: disable=missing-docstring,no-self-argument - # wrong type guess (Signature, not bytes) | pylint: disable=no-member - return json_util.encode_b64jose(value.encode('utf-8')) - - @protected.decoder # type: ignore - def protected(value): # pylint: disable=missing-docstring,no-self-argument - return json_util.decode_b64jose(value).decode('utf-8') - - def __init__(self, **kwargs): - if 'combined' not in kwargs: - kwargs = self._with_combined(kwargs) - super(Signature, self).__init__(**kwargs) - assert self.combined.alg is not None - - @classmethod - def _with_combined(cls, kwargs): - assert 'combined' not in kwargs - header = kwargs.get('header', cls._fields['header'].default) - protected = kwargs.get('protected', cls._fields['protected'].default) - - if protected: - combined = header + cls.header_cls.json_loads(protected) - else: - combined = header - - kwargs['combined'] = combined - return kwargs - - @classmethod - def _msg(cls, protected, payload): - return (b64.b64encode(protected.encode('utf-8')) + b'.' + - b64.b64encode(payload)) - - def verify(self, payload, key=None): - """Verify. - - :param JWK key: Key used for verification. - - """ - key = self.combined.find_key() if key is None else key - return self.combined.alg.verify( - key=key.key, sig=self.signature, - msg=self._msg(self.protected, payload)) - - @classmethod - def sign(cls, payload, key, alg, include_jwk=True, - protect=frozenset(), **kwargs): - """Sign. - - :param JWK key: Key for signature. - - """ - assert isinstance(key, alg.kty) - - header_params = kwargs - header_params['alg'] = alg - if include_jwk: - header_params['jwk'] = key.public_key() - - assert set(header_params).issubset(cls.header_cls._fields) - assert protect.issubset(cls.header_cls._fields) - - protected_params = {} - for header in protect: - if header in header_params: - protected_params[header] = header_params.pop(header) - if protected_params: - # pylint: disable=star-args - protected = cls.header_cls(**protected_params).json_dumps() - else: - protected = '' - - header = cls.header_cls(**header_params) # pylint: disable=star-args - signature = alg.sign(key.key, cls._msg(protected, payload)) - - return cls(protected=protected, header=header, signature=signature) - - def fields_to_partial_json(self): - fields = super(Signature, self).fields_to_partial_json() - if not fields['header'].not_omitted(): - del fields['header'] - return fields - - @classmethod - def fields_from_json(cls, jobj): - fields = super(Signature, cls).fields_from_json(jobj) - fields_with_combined = cls._with_combined(fields) - if 'alg' not in fields_with_combined['combined'].not_omitted(): - raise errors.DeserializationError('alg not present') - return fields_with_combined - - -class JWS(json_util.JSONObjectWithFields): - """JSON Web Signature. - - :ivar str payload: JWS Payload. - :ivar str signature: JWS Signatures. - - """ - __slots__ = ('payload', 'signatures') - - signature_cls = Signature - - def verify(self, key=None): - """Verify.""" - return all(sig.verify(self.payload, key) for sig in self.signatures) - - @classmethod - def sign(cls, payload, **kwargs): - """Sign.""" - return cls(payload=payload, signatures=( - cls.signature_cls.sign(payload=payload, **kwargs),)) - - @property - def signature(self): - """Get a singleton signature. - - :rtype: `signature_cls` - - """ - assert len(self.signatures) == 1 - return self.signatures[0] - - def to_compact(self): - """Compact serialization. - - :rtype: bytes - - """ - assert len(self.signatures) == 1 - - assert 'alg' not in self.signature.header.not_omitted() - # ... it must be in protected - - return ( - b64.b64encode(self.signature.protected.encode('utf-8')) + - b'.' + - b64.b64encode(self.payload) + - b'.' + - b64.b64encode(self.signature.signature)) - - @classmethod - def from_compact(cls, compact): - """Compact deserialization. - - :param bytes compact: - - """ - try: - protected, payload, signature = compact.split(b'.') - except ValueError: - raise errors.DeserializationError( - 'Compact JWS serialization should comprise of exactly' - ' 3 dot-separated components') - - sig = cls.signature_cls( - protected=b64.b64decode(protected).decode('utf-8'), - signature=b64.b64decode(signature)) - return cls(payload=b64.b64decode(payload), signatures=(sig,)) - - def to_partial_json(self, flat=True): # pylint: disable=arguments-differ - assert self.signatures - payload = json_util.encode_b64jose(self.payload) - - if flat and len(self.signatures) == 1: - ret = self.signatures[0].to_partial_json() - ret['payload'] = payload - return ret - else: - return { - 'payload': payload, - 'signatures': self.signatures, - } - - @classmethod - def from_json(cls, jobj): - if 'signature' in jobj and 'signatures' in jobj: - raise errors.DeserializationError('Flat mixed with non-flat') - elif 'signature' in jobj: # flat - return cls(payload=json_util.decode_b64jose(jobj.pop('payload')), - signatures=(cls.signature_cls.from_json(jobj),)) - else: - return cls(payload=json_util.decode_b64jose(jobj['payload']), - signatures=tuple(cls.signature_cls.from_json(sig) - for sig in jobj['signatures'])) - - -class CLI(object): - """JWS CLI.""" - - @classmethod - def sign(cls, args): - """Sign.""" - key = args.alg.kty.load(args.key.read()) - args.key.close() - if args.protect is None: - args.protect = [] - if args.compact: - args.protect.append('alg') - - sig = JWS.sign(payload=sys.stdin.read().encode(), key=key, alg=args.alg, - protect=set(args.protect)) - - if args.compact: - six.print_(sig.to_compact().decode('utf-8')) - else: # JSON - six.print_(sig.json_dumps_pretty()) - - @classmethod - def verify(cls, args): - """Verify.""" - if args.compact: - sig = JWS.from_compact(sys.stdin.read().encode()) - else: # JSON - try: - sig = JWS.json_loads(sys.stdin.read()) - except errors.Error as error: - six.print_(error) - return -1 - - if args.key is not None: - assert args.kty is not None - key = args.kty.load(args.key.read()).public_key() - args.key.close() - else: - key = None - - sys.stdout.write(sig.payload) - return not sig.verify(key=key) - - @classmethod - def _alg_type(cls, arg): - return jwa.JWASignature.from_json(arg) - - @classmethod - def _header_type(cls, arg): - assert arg in Signature.header_cls._fields - return arg - - @classmethod - def _kty_type(cls, arg): - assert arg in jwk.JWK.TYPES - return jwk.JWK.TYPES[arg] - - @classmethod - def run(cls, args=sys.argv[1:]): - """Parse arguments and sign/verify.""" - parser = argparse.ArgumentParser() - parser.add_argument('--compact', action='store_true') - - subparsers = parser.add_subparsers() - parser_sign = subparsers.add_parser('sign') - parser_sign.set_defaults(func=cls.sign) - parser_sign.add_argument( - '-k', '--key', type=argparse.FileType('rb'), required=True) - parser_sign.add_argument( - '-a', '--alg', type=cls._alg_type, default=jwa.RS256) - parser_sign.add_argument( - '-p', '--protect', action='append', type=cls._header_type) - - parser_verify = subparsers.add_parser('verify') - parser_verify.set_defaults(func=cls.verify) - parser_verify.add_argument( - '-k', '--key', type=argparse.FileType('rb'), required=False) - parser_verify.add_argument( - '--kty', type=cls._kty_type, required=False) - - parsed = parser.parse_args(args) - return parsed.func(parsed) - - -if __name__ == '__main__': - exit(CLI.run()) # pragma: no cover diff --git a/acme/acme/jose/jws_test.py b/acme/acme/jose/jws_test.py deleted file mode 100644 index ec91f6a1b..000000000 --- a/acme/acme/jose/jws_test.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Tests for acme.jose.jws.""" -import base64 -import unittest - -import mock -import OpenSSL - -from acme import test_util - -from acme.jose import errors -from acme.jose import json_util -from acme.jose import jwa -from acme.jose import jwk - - -CERT = test_util.load_comparable_cert('cert.pem') -KEY = jwk.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - - -class MediaTypeTest(unittest.TestCase): - """Tests for acme.jose.jws.MediaType.""" - - def test_decode(self): - from acme.jose.jws import MediaType - self.assertEqual('application/app', MediaType.decode('application/app')) - self.assertEqual('application/app', MediaType.decode('app')) - self.assertRaises( - errors.DeserializationError, MediaType.decode, 'app;foo') - - def test_encode(self): - from acme.jose.jws import MediaType - self.assertEqual('app', MediaType.encode('application/app')) - self.assertEqual('application/app;foo', - MediaType.encode('application/app;foo')) - - -class HeaderTest(unittest.TestCase): - """Tests for acme.jose.jws.Header.""" - - def setUp(self): - from acme.jose.jws import Header - self.header1 = Header(jwk='foo') - self.header2 = Header(jwk='bar') - self.crit = Header(crit=('a', 'b')) - self.empty = Header() - - def test_add_non_empty(self): - from acme.jose.jws import Header - self.assertEqual(Header(jwk='foo', crit=('a', 'b')), - self.header1 + self.crit) - - def test_add_empty(self): - self.assertEqual(self.header1, self.header1 + self.empty) - self.assertEqual(self.header1, self.empty + self.header1) - - def test_add_overlapping_error(self): - self.assertRaises(TypeError, self.header1.__add__, self.header2) - - def test_add_wrong_type_error(self): - self.assertRaises(TypeError, self.header1.__add__, 'xxx') - - def test_crit_decode_always_errors(self): - from acme.jose.jws import Header - self.assertRaises(errors.DeserializationError, Header.from_json, - {'crit': ['a', 'b']}) - - def test_x5c_decoding(self): - from acme.jose.jws import Header - header = Header(x5c=(CERT, CERT)) - jobj = header.to_partial_json() - cert_asn1 = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped) - cert_b64 = base64.b64encode(cert_asn1) - self.assertEqual(jobj, {'x5c': [cert_b64, cert_b64]}) - self.assertEqual(header, Header.from_json(jobj)) - jobj['x5c'][0] = base64.b64encode(b'xxx' + cert_asn1) - self.assertRaises(errors.DeserializationError, Header.from_json, jobj) - - def test_find_key(self): - self.assertEqual('foo', self.header1.find_key()) - self.assertEqual('bar', self.header2.find_key()) - self.assertRaises(errors.Error, self.crit.find_key) - - -class SignatureTest(unittest.TestCase): - """Tests for acme.jose.jws.Signature.""" - - def test_from_json(self): - from acme.jose.jws import Header - from acme.jose.jws import Signature - self.assertEqual( - Signature(signature=b'foo', header=Header(alg=jwa.RS256)), - Signature.from_json( - {'signature': 'Zm9v', 'header': {'alg': 'RS256'}})) - - def test_from_json_no_alg_error(self): - from acme.jose.jws import Signature - self.assertRaises(errors.DeserializationError, - Signature.from_json, {'signature': 'foo'}) - - -class JWSTest(unittest.TestCase): - """Tests for acme.jose.jws.JWS.""" - - def setUp(self): - self.privkey = KEY - self.pubkey = self.privkey.public_key() - - from acme.jose.jws import JWS - self.unprotected = JWS.sign( - payload=b'foo', key=self.privkey, alg=jwa.RS256) - self.protected = JWS.sign( - payload=b'foo', key=self.privkey, alg=jwa.RS256, - protect=frozenset(['jwk', 'alg'])) - self.mixed = JWS.sign( - payload=b'foo', key=self.privkey, alg=jwa.RS256, - protect=frozenset(['alg'])) - - def test_pubkey_jwk(self): - self.assertEqual(self.unprotected.signature.combined.jwk, self.pubkey) - self.assertEqual(self.protected.signature.combined.jwk, self.pubkey) - self.assertEqual(self.mixed.signature.combined.jwk, self.pubkey) - - def test_sign_unprotected(self): - self.assertTrue(self.unprotected.verify()) - - def test_sign_protected(self): - self.assertTrue(self.protected.verify()) - - def test_sign_mixed(self): - self.assertTrue(self.mixed.verify()) - - def test_compact_lost_unprotected(self): - compact = self.mixed.to_compact() - self.assertEqual( - b'eyJhbGciOiAiUlMyNTYifQ.Zm9v.OHdxFVj73l5LpxbFp1AmYX4yJM0Pyb' - b'_893n1zQjpim_eLS5J1F61lkvrCrCDErTEJnBGOGesJ72M7b6Ve1cAJA', - compact) - - from acme.jose.jws import JWS - mixed = JWS.from_compact(compact) - - self.assertNotEqual(self.mixed, mixed) - self.assertEqual( - set(['alg']), set(mixed.signature.combined.not_omitted())) - - def test_from_compact_missing_components(self): - from acme.jose.jws import JWS - self.assertRaises(errors.DeserializationError, JWS.from_compact, b'.') - - def test_json_omitempty(self): - protected_jobj = self.protected.to_partial_json(flat=True) - unprotected_jobj = self.unprotected.to_partial_json(flat=True) - - self.assertTrue('protected' not in unprotected_jobj) - self.assertTrue('header' not in protected_jobj) - - unprotected_jobj['header'] = unprotected_jobj['header'].to_json() - - from acme.jose.jws import JWS - self.assertEqual(JWS.from_json(protected_jobj), self.protected) - self.assertEqual(JWS.from_json(unprotected_jobj), self.unprotected) - - def test_json_flat(self): - jobj_to = { - 'signature': json_util.encode_b64jose( - self.mixed.signature.signature), - 'payload': json_util.encode_b64jose(b'foo'), - 'header': self.mixed.signature.header, - 'protected': json_util.encode_b64jose( - self.mixed.signature.protected.encode('utf-8')), - } - jobj_from = jobj_to.copy() - jobj_from['header'] = jobj_from['header'].to_json() - - self.assertEqual(self.mixed.to_partial_json(flat=True), jobj_to) - from acme.jose.jws import JWS - self.assertEqual(self.mixed, JWS.from_json(jobj_from)) - - def test_json_not_flat(self): - jobj_to = { - 'signatures': (self.mixed.signature,), - 'payload': json_util.encode_b64jose(b'foo'), - } - jobj_from = jobj_to.copy() - jobj_from['signatures'] = [jobj_to['signatures'][0].to_json()] - - self.assertEqual(self.mixed.to_partial_json(flat=False), jobj_to) - from acme.jose.jws import JWS - self.assertEqual(self.mixed, JWS.from_json(jobj_from)) - - def test_from_json_mixed_flat(self): - from acme.jose.jws import JWS - self.assertRaises(errors.DeserializationError, JWS.from_json, - {'signatures': (), 'signature': 'foo'}) - - def test_from_json_hashable(self): - from acme.jose.jws import JWS - hash(JWS.from_json(self.mixed.to_json())) - - -class CLITest(unittest.TestCase): - - def setUp(self): - self.key_path = test_util.vector_path('rsa512_key.pem') - - def test_unverified(self): - from acme.jose.jws import CLI - with mock.patch('sys.stdin') as sin: - sin.read.return_value = '{"payload": "foo", "signature": "xxx"}' - with mock.patch('sys.stdout'): - self.assertEqual(-1, CLI.run(['verify'])) - - def test_json(self): - from acme.jose.jws import CLI - - with mock.patch('sys.stdin') as sin: - sin.read.return_value = 'foo' - with mock.patch('sys.stdout') as sout: - CLI.run(['sign', '-k', self.key_path, '-a', 'RS256', - '-p', 'jwk']) - sin.read.return_value = sout.write.mock_calls[0][1][0] - self.assertEqual(0, CLI.run(['verify'])) - - def test_compact(self): - from acme.jose.jws import CLI - - with mock.patch('sys.stdin') as sin: - sin.read.return_value = 'foo' - with mock.patch('sys.stdout') as sout: - CLI.run(['--compact', 'sign', '-k', self.key_path]) - sin.read.return_value = sout.write.mock_calls[0][1][0] - self.assertEqual(0, CLI.run([ - '--compact', 'verify', '--kty', 'RSA', - '-k', self.key_path])) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/util.py b/acme/acme/jose/util.py deleted file mode 100644 index 26b7e0c5a..000000000 --- a/acme/acme/jose/util.py +++ /dev/null @@ -1,226 +0,0 @@ -"""JOSE utilities.""" -import collections - -from cryptography.hazmat.primitives.asymmetric import rsa -import OpenSSL -import six - - -class abstractclassmethod(classmethod): - # pylint: disable=invalid-name,too-few-public-methods - """Descriptor for an abstract classmethod. - - It augments the :mod:`abc` framework with an abstract - classmethod. This is implemented as :class:`abc.abstractclassmethod` - in the standard Python library starting with version 3.2. - - This particular implementation, allegedly based on Python 3.3 source - code, is stolen from - http://stackoverflow.com/questions/11217878/python-2-7-combine-abc-abstractmethod-and-classmethod. - - """ - __isabstractmethod__ = True - - def __init__(self, target): - target.__isabstractmethod__ = True - super(abstractclassmethod, self).__init__(target) - - -class ComparableX509(object): # pylint: disable=too-few-public-methods - """Wrapper for OpenSSL.crypto.X509** objects that supports __eq__. - - :ivar wrapped: Wrapped certificate or certificate request. - :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. - - """ - def __init__(self, wrapped): - assert isinstance(wrapped, OpenSSL.crypto.X509) or isinstance( - wrapped, OpenSSL.crypto.X509Req) - self.wrapped = wrapped - - def __getattr__(self, name): - return getattr(self.wrapped, name) - - def _dump(self, filetype=OpenSSL.crypto.FILETYPE_ASN1): - """Dumps the object into a buffer with the specified encoding. - - :param int filetype: The desired encoding. Should be one of - `OpenSSL.crypto.FILETYPE_ASN1`, - `OpenSSL.crypto.FILETYPE_PEM`, or - `OpenSSL.crypto.FILETYPE_TEXT`. - - :returns: Encoded X509 object. - :rtype: str - - """ - if isinstance(self.wrapped, OpenSSL.crypto.X509): - func = OpenSSL.crypto.dump_certificate - else: # assert in __init__ makes sure this is X509Req - func = OpenSSL.crypto.dump_certificate_request - return func(filetype, self.wrapped) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return NotImplemented - # pylint: disable=protected-access - return self._dump() == other._dump() - - def __hash__(self): - return hash((self.__class__, self._dump())) - - def __ne__(self, other): - return not self == other - - def __repr__(self): - return '<{0}({1!r})>'.format(self.__class__.__name__, self.wrapped) - - -class ComparableKey(object): # pylint: disable=too-few-public-methods - """Comparable wrapper for `cryptography` keys. - - See https://github.com/pyca/cryptography/issues/2122. - - """ - __hash__ = NotImplemented - - def __init__(self, wrapped): - self._wrapped = wrapped - - def __getattr__(self, name): - return getattr(self._wrapped, name) - - def __eq__(self, other): - # pylint: disable=protected-access - if (not isinstance(other, self.__class__) or - self._wrapped.__class__ is not other._wrapped.__class__): - return NotImplemented - elif hasattr(self._wrapped, 'private_numbers'): - return self.private_numbers() == other.private_numbers() - elif hasattr(self._wrapped, 'public_numbers'): - return self.public_numbers() == other.public_numbers() - else: - return NotImplemented - - def __ne__(self, other): - return not self == other - - def __repr__(self): - return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped) - - def public_key(self): - """Get wrapped public key.""" - return self.__class__(self._wrapped.public_key()) - - -class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods - """Wrapper for `cryptography` RSA keys. - - Wraps around: - - `cryptography.hazmat.primitives.asymmetric.RSAPrivateKey` - - `cryptography.hazmat.primitives.asymmetric.RSAPublicKey` - - """ - - def __hash__(self): - # public_numbers() hasn't got stable hash! - # https://github.com/pyca/cryptography/issues/2143 - if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization): - priv = self.private_numbers() - pub = priv.public_numbers - return hash((self.__class__, priv.p, priv.q, priv.dmp1, - priv.dmq1, priv.iqmp, pub.n, pub.e)) - elif isinstance(self._wrapped, rsa.RSAPublicKeyWithSerialization): - pub = self.public_numbers() - return hash((self.__class__, pub.n, pub.e)) - - -class ImmutableMap(collections.Mapping, collections.Hashable): # type: ignore - # pylint: disable=too-few-public-methods - """Immutable key to value mapping with attribute access.""" - - __slots__ = () - """Must be overridden in subclasses.""" - - def __init__(self, **kwargs): - if set(kwargs) != set(self.__slots__): - raise TypeError( - '__init__() takes exactly the following arguments: {0} ' - '({1} given)'.format(', '.join(self.__slots__), - ', '.join(kwargs) if kwargs else 'none')) - for slot in self.__slots__: - object.__setattr__(self, slot, kwargs.pop(slot)) - - def update(self, **kwargs): - """Return updated map.""" - items = dict(self) - items.update(kwargs) - return type(self)(**items) # pylint: disable=star-args - - def __getitem__(self, key): - try: - return getattr(self, key) - except AttributeError: - raise KeyError(key) - - def __iter__(self): - return iter(self.__slots__) - - def __len__(self): - return len(self.__slots__) - - def __hash__(self): - return hash(tuple(getattr(self, slot) for slot in self.__slots__)) - - def __setattr__(self, name, value): - raise AttributeError("can't set attribute") - - def __repr__(self): - return '{0}({1})'.format(self.__class__.__name__, ', '.join( - '{0}={1!r}'.format(key, value) - for key, value in six.iteritems(self))) - - -class frozendict(collections.Mapping, collections.Hashable): # type: ignore - # pylint: disable=invalid-name,too-few-public-methods - """Frozen dictionary.""" - __slots__ = ('_items', '_keys') - - def __init__(self, *args, **kwargs): - if kwargs and not args: - items = dict(kwargs) - elif len(args) == 1 and isinstance(args[0], collections.Mapping): - items = args[0] - else: - raise TypeError() - # TODO: support generators/iterators - - object.__setattr__(self, '_items', items) - object.__setattr__(self, '_keys', tuple(sorted(six.iterkeys(items)))) - - def __getitem__(self, key): - return self._items[key] - - def __iter__(self): - return iter(self._keys) - - def __len__(self): - return len(self._items) - - def _sorted_items(self): - return tuple((key, self[key]) for key in self._keys) - - def __hash__(self): - return hash(self._sorted_items()) - - def __getattr__(self, name): - try: - return self._items[name] - except KeyError: - raise AttributeError(name) - - def __setattr__(self, name, value): - raise AttributeError("can't set attribute") - - def __repr__(self): - return 'frozendict({0})'.format(', '.join('{0}={1!r}'.format( - key, value) for key, value in self._sorted_items())) diff --git a/acme/acme/jose/util_test.py b/acme/acme/jose/util_test.py deleted file mode 100644 index 0038a6cc1..000000000 --- a/acme/acme/jose/util_test.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Tests for acme.jose.util.""" -import functools -import unittest - -import six - -from acme import test_util - - -class ComparableX509Test(unittest.TestCase): - """Tests for acme.jose.util.ComparableX509.""" - - def setUp(self): - # test_util.load_comparable_{csr,cert} return ComparableX509 - self.req1 = test_util.load_comparable_csr('csr.pem') - self.req2 = test_util.load_comparable_csr('csr.pem') - self.req_other = test_util.load_comparable_csr('csr-san.pem') - - self.cert1 = test_util.load_comparable_cert('cert.pem') - self.cert2 = test_util.load_comparable_cert('cert.pem') - self.cert_other = test_util.load_comparable_cert('cert-san.pem') - - def test_getattr_proxy(self): - self.assertTrue(self.cert1.has_expired()) - - def test_eq(self): - self.assertEqual(self.req1, self.req2) - self.assertEqual(self.cert1, self.cert2) - - def test_ne(self): - self.assertNotEqual(self.req1, self.req_other) - self.assertNotEqual(self.cert1, self.cert_other) - - def test_ne_wrong_types(self): - self.assertNotEqual(self.req1, 5) - self.assertNotEqual(self.cert1, 5) - - def test_hash(self): - self.assertEqual(hash(self.req1), hash(self.req2)) - self.assertNotEqual(hash(self.req1), hash(self.req_other)) - - self.assertEqual(hash(self.cert1), hash(self.cert2)) - self.assertNotEqual(hash(self.cert1), hash(self.cert_other)) - - def test_repr(self): - for x509 in self.req1, self.cert1: - self.assertEqual(repr(x509), - ''.format(x509.wrapped)) - - -class ComparableRSAKeyTest(unittest.TestCase): - """Tests for acme.jose.util.ComparableRSAKey.""" - - def setUp(self): - # test_utl.load_rsa_private_key return ComparableRSAKey - self.key = test_util.load_rsa_private_key('rsa256_key.pem') - self.key_same = test_util.load_rsa_private_key('rsa256_key.pem') - self.key2 = test_util.load_rsa_private_key('rsa512_key.pem') - - def test_getattr_proxy(self): - self.assertEqual(256, self.key.key_size) - - def test_eq(self): - self.assertEqual(self.key, self.key_same) - - def test_ne(self): - self.assertNotEqual(self.key, self.key2) - - def test_ne_different_types(self): - self.assertNotEqual(self.key, 5) - - def test_ne_not_wrapped(self): - # pylint: disable=protected-access - self.assertNotEqual(self.key, self.key_same._wrapped) - - def test_ne_no_serialization(self): - from acme.jose.util import ComparableRSAKey - self.assertNotEqual(ComparableRSAKey(5), ComparableRSAKey(5)) - - def test_hash(self): - self.assertTrue(isinstance(hash(self.key), int)) - self.assertEqual(hash(self.key), hash(self.key_same)) - self.assertNotEqual(hash(self.key), hash(self.key2)) - - def test_repr(self): - self.assertTrue(repr(self.key).startswith( - '=0.6) # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', + # formerly known as acme.jose: + 'josepy>=1.0.0', # Connection.set_tlsext_host_name (>=0.13) 'mock', 'PyOpenSSL>=0.13', @@ -74,10 +76,5 @@ setup( 'dev': dev_extras, 'docs': docs_extras, }, - entry_points={ - 'console_scripts': [ - 'jws = acme.jose.jws:CLI.run', - ], - }, test_suite='acme', ) diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 2405110c5..ca667465c 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -5,11 +5,10 @@ import sys import unittest import augeas +import josepy as jose import mock import zope.component -from acme import jose - from certbot.display import util as display_util from certbot.plugins import common diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index af951aa6a..4155944bd 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -6,7 +6,8 @@ import re import shutil import tarfile -from acme import jose +import josepy as jose + from acme import test_util from certbot import constants diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 6e1b0d8ff..7b32d8e82 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -5,11 +5,10 @@ import pkg_resources import tempfile import unittest +import josepy as jose import mock import zope.component -from acme import jose - from certbot import configuration from certbot.tests import util as test_util diff --git a/certbot/account.py b/certbot/account.py index 389f96791..41e980097 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -7,13 +7,13 @@ import shutil import socket from cryptography.hazmat.primitives import serialization +import josepy as jose import pyrfc3339 import pytz import six import zope.component from acme import fields as acme_fields -from acme import jose from acme import messages from certbot import errors diff --git a/certbot/achallenges.py b/certbot/achallenges.py index f39bb4cec..6535a6b63 100644 --- a/certbot/achallenges.py +++ b/certbot/achallenges.py @@ -19,8 +19,9 @@ Note, that all annotated challenges act as a proxy objects:: """ import logging +import josepy as jose + from acme import challenges -from acme import jose logger = logging.getLogger(__name__) diff --git a/certbot/client.py b/certbot/client.py index ed70fda71..b735421f5 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -5,13 +5,13 @@ import platform from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa +import josepy as jose import OpenSSL import zope.component from acme import client as acme_client from acme import crypto_util as acme_crypto_util from acme import errors as acme_errors -from acme import jose from acme import messages import certbot diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 112ef7c85..3ae16529d 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -14,9 +14,9 @@ import six import zope.component from cryptography.hazmat.backends import default_backend from cryptography import x509 +import josepy as jose from acme import crypto_util as acme_crypto_util -from acme import jose from certbot import errors from certbot import interfaces @@ -368,7 +368,7 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in - `acme.jose.ComparableX509`). + :class:`josepy.util.ComparableX509`). """ # XXX: returns empty string when no chain is available, which diff --git a/certbot/main.py b/certbot/main.py index 72af7fbba..1c6432fd9 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -6,9 +6,9 @@ import os import sys import configobj +import josepy as jose import zope.component -from acme import jose from acme import errors as acme_errors import certbot diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index 420d15679..002d2f225 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -9,7 +9,7 @@ import OpenSSL import pkg_resources import zope.interface -from acme.jose import util as jose_util +from josepy import util as jose_util from certbot import constants from certbot import crypto_util diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index 8ce68bbb5..1a1ca7dcb 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -5,11 +5,11 @@ import shutil import tempfile import unittest +import josepy as jose import mock import OpenSSL from acme import challenges -from acme import jose from certbot import achallenges from certbot import crypto_util diff --git a/certbot/plugins/dns_test_common.py b/certbot/plugins/dns_test_common.py index d8cd29404..54b656b20 100644 --- a/certbot/plugins/dns_test_common.py +++ b/certbot/plugins/dns_test_common.py @@ -3,10 +3,10 @@ import os import configobj +import josepy as jose import mock import six from acme import challenges -from acme import jose from certbot import achallenges from certbot.tests import acme_util diff --git a/certbot/plugins/dns_test_common_lexicon.py b/certbot/plugins/dns_test_common_lexicon.py index f9c5735e8..a221cf1bf 100644 --- a/certbot/plugins/dns_test_common_lexicon.py +++ b/certbot/plugins/dns_test_common_lexicon.py @@ -1,7 +1,7 @@ """Base test class for DNS authenticators built on Lexicon.""" +import josepy as jose import mock -from acme import jose from requests.exceptions import HTTPError, RequestException from certbot import errors diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 1ae731e42..5227bc59e 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -3,11 +3,11 @@ import argparse import socket import unittest +import josepy as jose import mock import six from acme import challenges -from acme import jose from certbot import achallenges from certbot import errors diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index 92160bdfa..36e2ffba6 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -10,11 +10,11 @@ import stat import tempfile import unittest +import josepy as jose import mock import six from acme import challenges -from acme import jose from certbot import achallenges from certbot import errors diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index 7245ad6a1..8ebda56af 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -6,10 +6,10 @@ import shutil import stat import unittest +import josepy as jose import mock import pytz -from acme import jose from acme import messages from certbot import errors diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py index f0549666a..53a2f214a 100644 --- a/certbot/tests/acme_util.py +++ b/certbot/tests/acme_util.py @@ -1,10 +1,10 @@ """ACME utilities for testing.""" import datetime +import josepy as jose import six from acme import challenges -from acme import jose from acme import messages from certbot import auth_handler diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 09c4a50ca..204f46323 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -4,11 +4,11 @@ import shutil import tempfile import unittest +import josepy as jose import OpenSSL import mock from acme import errors as acme_errors -from acme import jose from certbot import account from certbot import errors diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index cb0fb32e3..57d82f839 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -4,10 +4,10 @@ import os import sys import unittest +import josepy as jose import mock import zope.component -from acme import jose from acme import messages from certbot import account diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 1f690df26..04b71dcc7 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -11,11 +11,10 @@ import unittest import datetime import pytz +import josepy as jose import six from six.moves import reload_module # pylint: disable=import-error -from acme import jose - from certbot import account from certbot import cli from certbot import constants diff --git a/certbot/tests/util.py b/certbot/tests/util.py index c43b44522..ddd4a1aec 100644 --- a/certbot/tests/util.py +++ b/certbot/tests/util.py @@ -14,11 +14,10 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import mock import OpenSSL +import josepy as jose import six from six.moves import reload_module # pylint: disable=import-error -from acme import jose - from certbot import constants from certbot import interfaces from certbot import storage diff --git a/tools/deactivate.py b/tools/deactivate.py index 5facc8436..d43b84552 100644 --- a/tools/deactivate.py +++ b/tools/deactivate.py @@ -18,10 +18,10 @@ import sys from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization +import josepy as jose from acme import client as acme_client from acme import errors as acme_errors -from acme import jose from acme import messages DIRECTORY = os.getenv('DIRECTORY', 'http://localhost:4000/directory') From 0e92d4ea98e44bfc9f1797269c6998195dea5f8a Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 11 Dec 2017 21:50:56 +0200 Subject: [PATCH 40/67] Parse variables without whitespace separator correctly in CentOS family of distributions (#5318) --- certbot-apache/certbot_apache/apache_util.py | 4 ++++ certbot-apache/certbot_apache/tests/centos_test.py | 2 ++ .../tests/testdata/centos7_apache/apache/sysconfig/httpd | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/apache_util.py b/certbot-apache/certbot_apache/apache_util.py index b4a24f137..f03c9da87 100644 --- a/certbot-apache/certbot_apache/apache_util.py +++ b/certbot-apache/certbot_apache/apache_util.py @@ -93,4 +93,8 @@ def parse_define_file(filepath, varname): if v == "-D" and len(a_opts) >= i+2: var_parts = a_opts[i+1].partition("=") return_vars[var_parts[0]] = var_parts[2] + elif len(v) > 2 and v.startswith("-D"): + # Found var with no whitespace separator + var_parts = v[2:].partition("=") + return_vars[var_parts[0]] = var_parts[2] return return_vars diff --git a/certbot-apache/certbot_apache/tests/centos_test.py b/certbot-apache/certbot_apache/tests/centos_test.py index 7ca47a4d5..d7a2a2fd9 100644 --- a/certbot-apache/certbot_apache/tests/centos_test.py +++ b/certbot-apache/certbot_apache/tests/centos_test.py @@ -118,6 +118,8 @@ class MultipleVhostsTestCentOS(util.ApacheTest): self.assertTrue("mock_define_too" in self.config.parser.variables.keys()) self.assertTrue("mock_value" in self.config.parser.variables.keys()) self.assertEqual("TRUE", self.config.parser.variables["mock_value"]) + self.assertTrue("MOCK_NOSEP" in self.config.parser.variables.keys()) + self.assertEqual("NOSEP_VAL", self.config.parser.variables["NOSEP_TWO"]) if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd index 0bf6b176c..4bcb300c2 100644 --- a/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd +++ b/certbot-apache/certbot_apache/tests/testdata/centos7_apache/apache/sysconfig/httpd @@ -14,7 +14,7 @@ # To pass additional options (for instance, -D definitions) to the # httpd binary at startup, set OPTIONS here. # -OPTIONS="-D mock_define -D mock_define_too -D mock_value=TRUE" +OPTIONS="-D mock_define -D mock_define_too -D mock_value=TRUE -DMOCK_NOSEP -DNOSEP_TWO=NOSEP_VAL" # # This setting ensures the httpd process is started in the "C" locale From 1b6005cc61f8b977af1bc5513994b4815280dd74 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 14 Dec 2017 18:15:42 -0800 Subject: [PATCH 41/67] Pin josepy in letsencrypt-auto (#5321) * pin josepy in le-auto * Put pinned versions in sorted order --- letsencrypt-auto-source/letsencrypt-auto | 11 +++++++---- .../pieces/dependency-requirements.txt | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 8d2e8a6b6..93e3e7b83 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -983,9 +983,16 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +josepy==1.0.1 \ + --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ + --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f packaging==16.8 \ @@ -1062,10 +1069,6 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -# Using an older version of mock here prevents regressions of #5276. -mock==1.3.0 \ - --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ - --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 # Contains the requirements for the letsencrypt package. # diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index dec7ae7d0..0e2cec984 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -105,9 +105,16 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +josepy==1.0.1 \ + --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ + --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +# Using an older version of mock here prevents regressions of #5276. +mock==1.3.0 \ + --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ + --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f packaging==16.8 \ @@ -184,7 +191,3 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -# Using an older version of mock here prevents regressions of #5276. -mock==1.3.0 \ - --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \ - --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 From a1aea021e7a587ea9396b2ebbfcfaec10411ab86 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 18 Dec 2017 12:31:36 -0800 Subject: [PATCH 42/67] Pin dependencies in oldest tests (#5316) * Add tools/merge_requirements.py * Revert "Fix oldest tests by pinning Google DNS deps (#5000)" This reverts commit f68fba2be2fc342dd72deaaf048ab79e5a8fc2be. * Add tools/oldest_constraints.txt * Remove oldest constraints from tox.ini * Rename dev constraints file * Update tools/pip_install.sh * Update install_and_test.sh * Fix pip_install.sh * Don't cat when you can cp * Add ng-httpsclient to dev constraints for oldest tests * Bump tested setuptools version * Update dev_constraints comment * Better document oldest dependencies * test against oldest versions we say we require * Update dev constraints * Properly handle empty lines * Update constraints gen in pip_install * Remove duplicated zope.component * Reduce pyasn1-modules dependency * Remove blank line * pin back google-api-python-client * pin back uritemplate * pin josepy for oldest tests * Undo changes to install_and_test.sh * Update install_and_test.sh description * use split instead of partition --- ...ip_constraints.txt => dev_constraints.txt} | 25 ++++---- tools/install_and_test.sh | 5 +- tools/merge_requirements.py | 61 +++++++++++++++++++ tools/oldest_constraints.txt | 51 ++++++++++++++++ tools/pip_install.sh | 31 ++++++---- tox.ini | 36 +---------- 6 files changed, 151 insertions(+), 58 deletions(-) rename tools/{pip_constraints.txt => dev_constraints.txt} (71%) create mode 100755 tools/merge_requirements.py create mode 100644 tools/oldest_constraints.txt diff --git a/tools/pip_constraints.txt b/tools/dev_constraints.txt similarity index 71% rename from tools/pip_constraints.txt rename to tools/dev_constraints.txt index cacec37d6..afc362ff8 100644 --- a/tools/pip_constraints.txt +++ b/tools/dev_constraints.txt @@ -1,16 +1,15 @@ # Specifies Python package versions for packages not specified in -# letsencrypt-auto's requirements file. We should avoid listing packages in -# both places because if both files are used as constraints for the same pip -# invocation, some constraints may be ignored due to pip's lack of dependency -# resolution. +# letsencrypt-auto's requirements file. alabaster==0.7.10 apipkg==1.4 +asn1crypto==0.22.0 astroid==1.3.5 +attrs==17.3.0 Babel==2.5.1 backports.shutil-get-terminal-size==1.0.0 boto3==1.4.7 botocore==1.7.41 -cloudflare==1.8.1 +cloudflare==1.5.1 coverage==4.4.2 decorator==4.1.2 dns-lexicon==2.1.14 @@ -19,7 +18,7 @@ docutils==0.14 execnet==1.5.0 future==0.16.0 futures==3.1.1 -google-api-python-client==1.6.4 +google-api-python-client==1.5 httplib2==0.10.3 imagesize==0.7.1 ipdb==0.10.3 @@ -27,20 +26,22 @@ ipython==5.5.0 ipython-genutils==0.2.0 Jinja2==2.9.6 jmespath==0.9.3 +josepy==1.0.1 +logger==1.4 logilab-common==1.4.1 MarkupSafe==1.0 -oauth2client==4.1.2 +ndg-httpsclient==0.3.2 +oauth2client==2.0.0 pathlib2==2.3.0 pexpect==4.2.1 pickleshare==0.7.4 -pkg-resources==0.0.0 pkginfo==1.4.1 pluggy==0.5.2 prompt-toolkit==1.0.15 ptyprocess==0.5.2 py==1.4.34 -pyasn1==0.3.7 -pyasn1-modules==0.1.5 +pyasn1==0.1.9 +pyasn1-modules==0.0.10 Pygments==2.2.0 pylint==1.4.2 pytest==3.2.5 @@ -48,7 +49,7 @@ pytest-cov==2.5.1 pytest-forked==0.2 pytest-xdist==1.20.1 python-dateutil==2.6.1 -python-digitalocean==1.12 +python-digitalocean==1.11 PyYAML==3.12 repoze.sphinx.autointerface==0.8 requests-file==1.4.2 @@ -65,6 +66,6 @@ tox==2.9.1 tqdm==4.19.4 traitlets==4.3.2 twine==1.9.1 -uritemplate==3.0.0 +uritemplate==0.6 virtualenv==15.1.0 wcwidth==0.1.7 diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh index d57f0974e..25b6d548a 100755 --- a/tools/install_and_test.sh +++ b/tools/install_and_test.sh @@ -2,8 +2,9 @@ # pip installs the requested packages in editable mode and runs unit tests on # them. Each package is installed and tested in the order they are provided # before the script moves on to the next package. If CERTBOT_NO_PIN is set not -# set to 1, packages are installed using certbot-auto's requirements file as -# constraints. +# set to 1, packages are installed using pinned versions of all of our +# dependencies. See pip_install.sh for more information on the versions pinned +# to. if [ "$CERTBOT_NO_PIN" = 1 ]; then pip_install="pip install -q -e" diff --git a/tools/merge_requirements.py b/tools/merge_requirements.py new file mode 100755 index 000000000..c8fb95351 --- /dev/null +++ b/tools/merge_requirements.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +"""Merges multiple Python requirements files into one file. + +Requirements files specified later take precedence over earlier ones. Only +simple SomeProject==1.2.3 format is currently supported. + +""" + +from __future__ import print_function + +import sys + + +def read_file(file_path): + """Reads in a Python requirements file. + + :param str file_path: path to requirements file + + :returns: mapping from a project to its pinned version + :rtype: dict + + """ + d = {} + with open(file_path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + project, version = line.split('==') + if not version: + raise ValueError("Unexpected syntax '{0}'".format(line)) + d[project] = version + return d + + +def print_requirements(requirements): + """Prints requirements to stdout. + + :param dict requirements: mapping from a project to its pinned version + + """ + print('\n'.join('{0}=={1}'.format(k, v) + for k, v in sorted(requirements.items()))) + + +def merge_requirements_files(*files): + """Merges multiple requirements files together and prints the result. + + Requirement files specified later in the list take precedence over earlier + files. + + :param tuple files: paths to requirements files + + """ + d = {} + for f in files: + d.update(read_file(f)) + print_requirements(d) + + +if __name__ == '__main__': + merge_requirements_files(*sys.argv[1:]) diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt new file mode 100644 index 000000000..de2b83ad8 --- /dev/null +++ b/tools/oldest_constraints.txt @@ -0,0 +1,51 @@ +# This file contains the oldest versions of our dependencies we say we require +# in our packages or versions we need to support to maintain compatibility with +# the versions included in the various Linux distros where we are packaged. + +# CentOS/RHEL 7 EPEL constraints +cffi==1.6.0 +chardet==2.2.1 +configobj==4.7.2 +ipaddress==1.0.16 +mock==1.0.1 +ndg-httpsclient==0.3.2 +ply==3.4 +pyasn1==0.1.9 +pycparser==2.14 +pyOpenSSL==0.13.1 +pyparsing==1.5.6 +pyRFC3339==1.0 +python-augeas==0.5.0 +six==1.9.0 +# setuptools 0.9.8 is the actual version packaged, but some other dependencies +# in this file require setuptools>=1.0 and there are no relevant changes for us +# between these versions. +setuptools==1.0.0 +urllib3==1.10.2 +zope.component==4.1.0 +zope.event==4.0.3 +zope.interface==4.0.5 + +# Debian Jessie Backports constraints +PyICU==1.8 +colorama==0.3.2 +enum34==1.0.3 +html5lib==0.999 +idna==2.0 +pbr==1.8.0 +pytz==2012rc0 + +# Our setup.py constraints +cloudflare==1.5.1 +cryptography==1.2.0 +google-api-python-client==1.5 +oauth2client==2.0 +parsedatetime==1.3 +pyparsing==1.5.5 +python-digitalocean==1.11 +requests[security]==2.4.1 + +# Ubuntu Xenial constraints +ConfigArgParse==0.10.0 +funcsigs==0.4 +zope.hookable==4.0.4 diff --git a/tools/pip_install.sh b/tools/pip_install.sh index fafd58e54..d2aae4a43 100755 --- a/tools/pip_install.sh +++ b/tools/pip_install.sh @@ -1,17 +1,26 @@ -#!/bin/sh -e -# pip installs packages using pinned package versions +#!/bin/bash -e +# pip installs packages using pinned package versions. If CERTBOT_OLDEST is set +# to 1, a combination of tools/oldest_constraints.txt and +# tools/dev_constraints.txt is used, otherwise, a combination of certbot-auto's +# requirements file and tools/dev_constraints.txt is used. The other file +# always takes precedence over tools/dev_constraints.txt. # get the root of the Certbot repo -my_path=$("$(dirname $0)/readlink.py" $0) -repo_root=$(dirname $(dirname $my_path)) -requirements="$repo_root/letsencrypt-auto-source/pieces/dependency-requirements.txt" -certbot_auto_constraints=$(mktemp) -trap "rm -f $certbot_auto_constraints" EXIT -# extract pinned requirements without hashes -sed -n -e 's/^\([^[:space:]]*==[^[:space:]]*\).*$/\1/p' $requirements > $certbot_auto_constraints -dev_constraints="$(dirname $my_path)/pip_constraints.txt" +tools_dir=$(dirname $("$(dirname $0)/readlink.py" $0)) +dev_constraints="$tools_dir/dev_constraints.txt" +merge_reqs="$tools_dir/merge_requirements.py" +test_constraints=$(mktemp) +trap "rm -f $test_constraints" EXIT + +if [ "$CERTBOT_OLDEST" = 1 ]; then + cp "$tools_dir/oldest_constraints.txt" "$test_constraints" +else + repo_root=$(dirname "$tools_dir") + certbot_requirements="$repo_root/letsencrypt-auto-source/pieces/dependency-requirements.txt" + sed -n -e 's/^\([^[:space:]]*==[^[:space:]]*\).*$/\1/p' "$certbot_requirements" > "$test_constraints" +fi set -x # install the requested packages using the pinned requirements as constraints -pip install -q --constraint $certbot_auto_constraints --constraint $dev_constraints "$@" +pip install -q --constraint <("$merge_reqs" "$dev_constraints" "$test_constraints") "$@" diff --git a/tox.ini b/tox.ini index bb421daa5..6ebf681ed 100644 --- a/tox.ini +++ b/tox.ini @@ -11,9 +11,8 @@ envlist = modification,py{26,33,34,35,36},cover,lint pip_install = {toxinidir}/tools/pip_install_editable.sh # pip installs the requested packages in editable mode and runs unit tests on # them. Each package is installed and tested in the order they are provided -# before the script moves on to the next package. If CERTBOT_NO_PIN is set not -# set to 1, packages are installed using certbot-auto's requirements file as -# constraints. +# before the script moves on to the next package. All dependencies are pinned +# to a specific version for increased stability for developers. install_and_test = {toxinidir}/tools/install_and_test.sh py26_packages = acme[dev] \ @@ -82,36 +81,7 @@ commands = {[testenv]commands} setenv = {[testenv]setenv} - CERTBOT_NO_PIN=1 -deps = - PyOpenSSL==0.13 - cffi==1.5.2 - configargparse==0.10.0 - configargparse==0.10.0 - configobj==4.7.2 - cryptography==1.2.3 - enum34==0.9.23 - google-api-python-client==1.5 - idna==2.0 - ipaddress==1.0.16 - mock==1.0.1 - ndg-httpsclient==0.3.2 - oauth2client==2.0 - parsedatetime==1.4 - pyasn1-modules==0.0.5 - pyasn1==0.1.9 - pyparsing==1.5.6 - pyrfc3339==1.0 - pytest==3.2.5 - python-augeas==0.4.1 - pytz==2012c - requests[security]==2.6.0 - setuptools==0.9.8 - six==1.9.0 - urllib3==1.10 - zope.component==4.0.2 - zope.event==4.0.1 - zope.interface==4.0.5 + CERTBOT_OLDEST=1 [testenv:py27_install] basepython = python2.7 From d6b11fea722ab71584a2bd50cb731a5f67b0e375 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 19 Dec 2017 16:16:45 -0800 Subject: [PATCH 43/67] More pip dependency resolution workarounds (#5339) * remove pyopenssl and six deps * remove outdated tox.ini dep requirement --- setup.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index ee108c514..ce505a62e 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,9 @@ readme = read_file(os.path.join(here, 'README.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst')) version = meta['version'] -# Please update tox.ini when modifying dependency version requirements -# This package relies on requests, however, it isn't specified here to avoid -# masking the more specific request requirements in acme. See -# https://github.com/pypa/pip/issues/988 for more info. +# This package relies on PyOpenSSL, requests, and six, however, it isn't +# specified here to avoid masking the more specific request requirements in +# acme. See https://github.com/pypa/pip/issues/988 for more info. install_requires = [ 'acme=={0}'.format(version), # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but @@ -44,13 +43,11 @@ install_requires = [ 'cryptography>=1.2', # load_pem_x509_certificate 'mock', 'parsedatetime>=1.3', # Calendar.parseDT - 'PyOpenSSL', 'pyrfc3339', 'pytz', # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: 'setuptools>=1.0', - 'six', 'zope.component', 'zope.interface', ] From ed2168aaa8c8a7e1bef449e60167b53d501d173a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 21 Dec 2017 16:55:21 -0800 Subject: [PATCH 44/67] Fix auto_tests on systems with new bootstrappers (#5348) --- letsencrypt-auto-source/tests/auto_test.py | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 2fa03105d..156466c82 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -30,6 +30,10 @@ sys.path.insert(0, dirname(tests_dir())) from build import build as build_le_auto +BOOTSTRAP_FILENAME = 'certbot-auto-bootstrap-version.txt' +"""Name of the file where certbot-auto saves its bootstrap version.""" + + class RequestHandler(BaseHTTPRequestHandler): """An HTTPS request handler which is quiet and serves a specific folder.""" @@ -296,17 +300,31 @@ class AutoTests(TestCase): def test_phase2_upgrade(self): """Test a phase-2 upgrade without a phase-1 upgrade.""" - with temp_paths() as (le_auto_path, venv_dir): - resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, - 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} - with serving(resources) as base_url: + resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), + 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, + 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} + with serving(resources) as base_url: + pip_find_links=join(tests_dir(), 'fake-letsencrypt', 'dist') + with temp_paths() as (le_auto_path, venv_dir): + install_le_auto(self.NEW_LE_AUTO, le_auto_path) + + # Create venv saving the correct bootstrap script version + out, err = run_le_auto(le_auto_path, venv_dir, base_url, + PIP_FIND_LINKS=pip_find_links) + self.assertFalse('Upgrading certbot-auto ' in out) + self.assertTrue('Creating virtual environment...' in out) + with open(join(venv_dir, BOOTSTRAP_FILENAME)) as f: + bootstrap_version = f.read() + + # Create a new venv with an old letsencrypt version + with temp_paths() as (le_auto_path, venv_dir): venv_bin = join(venv_dir, 'bin') makedirs(venv_bin) set_le_script_version(venv_dir, '0.0.1') + with open(join(venv_dir, BOOTSTRAP_FILENAME), 'w') as f: + f.write(bootstrap_version) install_le_auto(self.NEW_LE_AUTO, le_auto_path) - pip_find_links=join(tests_dir(), 'fake-letsencrypt', 'dist') out, err = run_le_auto(le_auto_path, venv_dir, base_url, PIP_FIND_LINKS=pip_find_links) From 5388842e5b3868e29caf545fb771a23e7fce4143 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 3 Jan 2018 17:49:22 -0800 Subject: [PATCH 45/67] Fix pytest on macOS in Travis (#5360) * Add tools/pytest.sh * pass TRAVIS through in tox.ini * Use tools/pytest.sh to run pytest * Add quiet to pytest.ini * ignore pytest cache --- .gitignore | 3 +++ pytest.ini | 2 ++ tools/install_and_test.sh | 2 +- tools/pytest.sh | 15 +++++++++++++++ tox.cover.sh | 3 ++- tox.ini | 9 +++++++++ 6 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 pytest.ini create mode 100755 tools/pytest.sh diff --git a/.gitignore b/.gitignore index b63e40d1c..e018cf938 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ tests/letstest/*.pem tests/letstest/venv/ .venv + +# pytest cache +/.cache diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..b64550cb7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --quiet diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh index 25b6d548a..0d39e0594 100755 --- a/tools/install_and_test.sh +++ b/tools/install_and_test.sh @@ -23,5 +23,5 @@ for requirement in "$@" ; do # See https://travis-ci.org/certbot/certbot/jobs/308774157#L1333. pkg=$(echo "$pkg" | tr - _) fi - pytest --numprocesses auto --quiet --pyargs $pkg + "$(dirname $0)/pytest.sh" --pyargs $pkg done diff --git a/tools/pytest.sh b/tools/pytest.sh new file mode 100755 index 000000000..8e3619d5d --- /dev/null +++ b/tools/pytest.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Runs pytest with the provided arguments, adding --numprocesses to the command +# line. This argument is set to "auto" if the environmnent variable TRAVIS is +# not set, otherwise, it is set to 2. This works around +# https://github.com/pytest-dev/pytest-xdist/issues/9. Currently every Travis +# environnment provides two cores. See +# https://docs.travis-ci.com/user/reference/overview/#Virtualization-environments. + +if ${TRAVIS:-false}; then + NUMPROCESSES="2" +else + NUMPROCESSES="auto" +fi + +pytest --numprocesses "$NUMPROCESSES" "$@" diff --git a/tox.cover.sh b/tox.cover.sh index 2b5a3cf19..bc0e5a8bf 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -51,7 +51,8 @@ cover () { fi pkg_dir=$(echo "$1" | tr _ -) - pytest --cov "$pkg_dir" --cov-append --cov-report= --numprocesses auto --pyargs "$1" + pytest="$(dirname $0)/tools/pytest.sh" + "$pytest" --cov "$pkg_dir" --cov-append --cov-report= --pyargs "$1" coverage report --fail-under="$min" --include="$pkg_dir/*" --show-missing } diff --git a/tox.ini b/tox.ini index 6ebf681ed..20f5cda32 100644 --- a/tox.ini +++ b/tox.ini @@ -61,6 +61,7 @@ commands = deps = setuptools==36.8.0 wheel==0.29.0 +passenv = TRAVIS [testenv] commands = @@ -69,12 +70,16 @@ commands = setenv = PYTHONPATH = {toxinidir} PYTHONHASHSEED = 0 +passenv = + {[testenv:py26]passenv} [testenv:py33] commands = {[testenv]commands} deps = wheel==0.29.0 +passenv = + {[testenv]passenv} [testenv:py27-oldest] commands = @@ -82,6 +87,8 @@ commands = setenv = {[testenv]setenv} CERTBOT_OLDEST=1 +passenv = + {[testenv]passenv} [testenv:py27_install] basepython = python2.7 @@ -93,6 +100,8 @@ basepython = python2.7 commands = {[base]install_packages} ./tox.cover.sh +passenv = + {[testenv]passenv} [testenv:lint] # recent versions of pylint do not support Python 2.6 (#97, #187) From a7d00ee21b454115fc0ce831b13f7902d4b62c37 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 4 Jan 2018 13:59:29 -0800 Subject: [PATCH 46/67] print as a string (#5359) --- certbot-nginx/certbot_nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index e9d4e36d4..8af474c5e 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -182,7 +182,7 @@ class NginxConfigurator(common.Installer): self.parser.add_server_directives(vhost, cert_directives, replace=True) logger.info("Deployed Certificate to VirtualHost %s for %s", - vhost.filep, vhost.names) + vhost.filep, ", ".join(vhost.names)) self.save_notes += ("Changed vhost at %s with addresses of %s\n" % (vhost.filep, From a3a66cd25d8340e982481e7adf4a521c09f0f35e Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 5 Jan 2018 00:36:16 +0200 Subject: [PATCH 47/67] Use apache2ctl modules for Gentoo systems. (#5349) * Do not call Apache binary for module reset in cleanup() * Use apache2ctl modules for Gentoo --- .../certbot_apache/override_gentoo.py | 8 +++ .../certbot_apache/tests/gentoo_test.py | 49 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/certbot-apache/certbot_apache/override_gentoo.py b/certbot-apache/certbot_apache/override_gentoo.py index d4d4e96b9..92f1d4a20 100644 --- a/certbot-apache/certbot_apache/override_gentoo.py +++ b/certbot-apache/certbot_apache/override_gentoo.py @@ -49,6 +49,7 @@ class GentooParser(parser.ApacheParser): def update_runtime_variables(self): """ Override for update_runtime_variables for custom parsing """ self.parse_sysconfig_var() + self.update_modules() def parse_sysconfig_var(self): """ Parses Apache CLI options from Gentoo configuration file """ @@ -56,3 +57,10 @@ class GentooParser(parser.ApacheParser): "APACHE2_OPTS") for k in defines.keys(): self.variables[k] = defines[k] + + def update_modules(self): + """Get loaded modules from httpd process, and add them to DOM""" + mod_cmd = [self.configurator.constant("apache_cmd"), "modules"] + matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module") + for mod in matches: + self.add_mod(mod.strip()) diff --git a/certbot-apache/certbot_apache/tests/gentoo_test.py b/certbot-apache/certbot_apache/tests/gentoo_test.py index 0f2b96818..cfbaffac7 100644 --- a/certbot-apache/certbot_apache/tests/gentoo_test.py +++ b/certbot-apache/certbot_apache/tests/gentoo_test.py @@ -2,6 +2,8 @@ import os import unittest +import mock + from certbot_apache import override_gentoo from certbot_apache import obj from certbot_apache.tests import util @@ -46,9 +48,10 @@ class MultipleVhostsTestGentoo(util.ApacheTest): config_root=config_root, vhost_root=vhost_root) - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir, - os_info="gentoo") + with mock.patch("certbot_apache.override_gentoo.GentooParser.update_runtime_variables"): + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir, + os_info="gentoo") self.vh_truth = get_vh_truth( self.temp_dir, "gentoo_apache/apache") @@ -78,9 +81,47 @@ class MultipleVhostsTestGentoo(util.ApacheTest): self.config.parser.apacheconfig_filep = os.path.realpath( os.path.join(self.config.parser.root, "../conf.d/apache2")) self.config.parser.variables = {} - self.config.parser.update_runtime_variables() + with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"): + self.config.parser.update_runtime_variables() for define in defines: self.assertTrue(define in self.config.parser.variables.keys()) + @mock.patch("certbot_apache.parser.ApacheParser.parse_from_subprocess") + def test_no_binary_configdump(self, mock_subprocess): + """Make sure we don't call binary dumps other than modules from Apache + as this is not supported in Gentoo currently""" + + with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"): + self.config.parser.update_runtime_variables() + self.config.parser.reset_modules() + self.assertFalse(mock_subprocess.called) + + self.config.parser.update_runtime_variables() + self.config.parser.reset_modules() + self.assertTrue(mock_subprocess.called) + + @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") + def test_opportunistic_httpd_runtime_parsing(self, mock_get): + mod_val = ( + 'Loaded Modules:\n' + ' mock_module (static)\n' + ' another_module (static)\n' + ) + def mock_get_cfg(command): + """Mock httpd process stdout""" + if command == ['apache2ctl', 'modules']: + return mod_val + mock_get.side_effect = mock_get_cfg + self.config.parser.modules = set() + + with mock.patch("certbot.util.get_os_info") as mock_osi: + # Make sure we have the have the CentOS httpd constants + mock_osi.return_value = ("gentoo", "123") + self.config.parser.update_runtime_variables() + + self.assertEquals(mock_get.call_count, 1) + self.assertEquals(len(self.config.parser.modules), 4) + self.assertTrue("mod_another.c" in self.config.parser.modules) + if __name__ == "__main__": unittest.main() # pragma: no cover From a1713c0b79b99108ae3a1233cb3e3dc3bef2908a Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 5 Jan 2018 21:08:38 +0200 Subject: [PATCH 48/67] Broader git ignore for pytest cache files (#5361) Make gitignore take pytest cache directories in to account, even if they reside in subdirectories. If pytest is run for a certain module, ie. `pytest certbot-apache` the cache directory is created under `certbot-apache` directory. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e018cf938..a01d2e1c7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ tests/letstest/venv/ .venv # pytest cache -/.cache +.cache From 18f6deada8dca329c1a797bcf5b88dbdcbd18cf7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 5 Jan 2018 19:27:00 -0800 Subject: [PATCH 49/67] Fix letsencrypt-auto name and long forms of -n (#5375) --- letsencrypt-auto-source/letsencrypt-auto | 8 +++++--- letsencrypt-auto-source/letsencrypt-auto.template | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 93e3e7b83..46cb51822 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -68,10 +68,12 @@ for arg in "$@" ; do NO_BOOTSTRAP=1;; --help) HELP=1;; - --noninteractive|--non-interactive|renew) - ASSUME_YES=1;; + --noninteractive|--non-interactive) + NONINTERACTIVE=1;; --quiet) QUIET=1;; + renew) + ASSUME_YES=1;; --verbose) VERBOSE=1;; -[!-]*) @@ -93,7 +95,7 @@ done if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 + NONINTERACTIVE=1 HELP=0 fi diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 4eef10c80..861ef0a6e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -68,10 +68,12 @@ for arg in "$@" ; do NO_BOOTSTRAP=1;; --help) HELP=1;; - --noninteractive|--non-interactive|renew) - ASSUME_YES=1;; + --noninteractive|--non-interactive) + NONINTERACTIVE=1;; --quiet) QUIET=1;; + renew) + ASSUME_YES=1;; --verbose) VERBOSE=1;; -[!-]*) @@ -93,7 +95,7 @@ done if [ $BASENAME = "letsencrypt-auto" ]; then # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 + NONINTERACTIVE=1 HELP=0 fi From 8585cdd86190c5da753cae1a691e66e586fd2bde Mon Sep 17 00:00:00 2001 From: ohemorange Date: Mon, 8 Jan 2018 13:57:04 -0800 Subject: [PATCH 50/67] Deprecate Python2.6 by using Python3 on CentOS/RHEL 6 (#5329) * If there's no python or there's only python2.6 on red hat systems, install python3 * Always check for python2.6 * address style, documentation, nits * factor out all initialization code * fix up python version return value when no python installed * add no python error and exit * document DeterminePythonVersion parameters * build letsencrypt-auto * close brace * build leauto * fix syntax errors * set USE_PYTHON_3 for all cases * rip out NOCRASH * replace NOCRASH, update LE_PYTHON set logic * use built-in venv for py3 * switch to LE_PYTHON not affecting bootstrap selection and not overwriting LE_PYTHON * python3ify fetch.py * get fetch.py working with python2 and 3 * don't verify server certificates in fetch.py HttpsGetter * Use SSLContext and an environment variable so that our tests continue to never verify server certificates. * typo * build * remove commented out code * address review comments * add documentation for YES_FLAG and QUIET_FLAG * Add tests to centos6 Dockerfile to make sure we install python3 if and only if appropriate to do so. --- letsencrypt-auto-source/Dockerfile.centos6 | 3 +- letsencrypt-auto-source/letsencrypt-auto | 205 +++++++++++++----- .../letsencrypt-auto.template | 67 ++++-- .../pieces/bootstrappers/rpm_common.sh | 75 +------ .../pieces/bootstrappers/rpm_common_base.sh | 78 +++++++ .../pieces/bootstrappers/rpm_python3.sh | 23 ++ letsencrypt-auto-source/pieces/fetch.py | 34 ++- letsencrypt-auto-source/tests/auto_test.py | 2 + .../tests/centos6_tests.sh | 65 ++++++ 9 files changed, 405 insertions(+), 147 deletions(-) create mode 100644 letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh create mode 100644 letsencrypt-auto-source/pieces/bootstrappers/rpm_python3.sh create mode 100644 letsencrypt-auto-source/tests/centos6_tests.sh diff --git a/letsencrypt-auto-source/Dockerfile.centos6 b/letsencrypt-auto-source/Dockerfile.centos6 index 8c1a4b353..47eb48f50 100644 --- a/letsencrypt-auto-source/Dockerfile.centos6 +++ b/letsencrypt-auto-source/Dockerfile.centos6 @@ -33,4 +33,5 @@ COPY . /home/lea/certbot/letsencrypt-auto-source USER lea WORKDIR /home/lea -CMD ["pytest", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] +RUN sudo chmod +x certbot/letsencrypt-auto-source/tests/centos6_tests.sh +CMD sudo certbot/letsencrypt-auto-source/tests/centos6_tests.sh diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 46cb51822..f1361d8ea 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -246,15 +246,29 @@ DeprecationBootstrap() { fi } - +# Sets LE_PYTHON to Python version string and PYVER to the first two +# digits of the python version DeterminePythonVersion() { - for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do - # Break (while keeping the LE_PYTHON value) if found. - $EXISTS "$LE_PYTHON" > /dev/null && break - done + # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + if [ -n "$USE_PYTHON_3" ]; then + for LE_PYTHON in "$LE_PYTHON" python3; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + else + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + fi if [ "$?" != "0" ]; then - error "Cannot find any Pythons; please install one!" - exit 1 + if [ "$1" != "NOCRASH" ]; then + error "Cannot find any Pythons; please install one!" + exit 1 + else + PYVER=0 + return 0 + fi fi export LE_PYTHON @@ -386,23 +400,19 @@ BootstrapDebCommon() { fi } -# If new packages are installed by BootstrapRpmCommon below, this version -# number must be increased. -BOOTSTRAP_RPM_COMMON_VERSION=1 - -BootstrapRpmCommon() { - # Tested with: - # - Fedora 20, 21, 22, 23 (x64) - # - Centos 7 (x64: on DigitalOcean droplet) - # - CentOS 7 Minimal install in a Hyper-V VM - # - CentOS 6 (EPEL must be installed manually) +# If new packages are installed by BootstrapRpmCommonBase below, version +# numbers in rpm_common.sh and rpm_python3.sh must be increased. +# Sets TOOL to the name of the package manager +# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG. +# Enables EPEL if applicable and possible. +InitializeRPMCommonBase() { if type dnf 2>/dev/null then - tool=dnf + TOOL=dnf elif type yum 2>/dev/null then - tool=yum + TOOL=yum else error "Neither yum nor dnf found. Aborting bootstrap!" @@ -410,15 +420,15 @@ BootstrapRpmCommon() { fi if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" + YES_FLAG="-y" fi if [ "$QUIET" = 1 ]; then QUIET_FLAG='--quiet' fi - if ! $tool list *virtualenv >/dev/null 2>&1; then + if ! $TOOL list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $tool list epel-release >/dev/null 2>&1; then + if ! $TOOL list epel-release >/dev/null 2>&1; then error "Enable the EPEL repository and try running Certbot again." exit 1 fi @@ -430,11 +440,17 @@ BootstrapRpmCommon() { /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." sleep 1s fi - if ! $tool install $yes_flag $QUIET_FLAG epel-release; then + if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then error "Could not enable EPEL. Aborting bootstrap!" exit 1 fi fi +} + +BootstrapRpmCommonBase() { + # Arguments: whitespace-delimited python packages to install + + InitializeRPMCommonBase # This call is superfluous in practice pkgs=" gcc @@ -446,10 +462,39 @@ BootstrapRpmCommon() { ca-certificates " - # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $tool list python >/dev/null 2>&1; then + # Add the python packages + pkgs="$pkgs + $1 + " + + if $TOOL list installed "httpd" >/dev/null 2>&1; then pkgs="$pkgs - python + mod_ssl + " + fi + + if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then + error "Could not install OS dependencies. Aborting bootstrap!" + exit 1 + fi +} + +# If new packages are installed by BootstrapRpmCommon below, this version +# number must be increased. +BOOTSTRAP_RPM_COMMON_VERSION=1 + +BootstrapRpmCommon() { + # Tested with: + # - Fedora 20, 21, 22, 23 (x64) + # - Centos 7 (x64: on DigitalOcean droplet) + # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 + + InitializeRPMCommonBase + + # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. + if $TOOL list python >/dev/null 2>&1; then + python_pkgs="$python python-devel python-virtualenv python-tools @@ -457,9 +502,8 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $tool list python2 >/dev/null 2>&1; then - pkgs="$pkgs - python2 + elif $TOOL list python2 >/dev/null 2>&1; then + python_pkgs="$python2 python2-libs python2-setuptools python2-devel @@ -470,8 +514,7 @@ BootstrapRpmCommon() { # Some distros and older versions of current distros use a "python27" # instead of the "python" or "python-" naming convention. else - pkgs="$pkgs - python27 + python_pkgs="$python27 python27-devel python27-virtualenv python27-tools @@ -479,16 +522,31 @@ BootstrapRpmCommon() { " fi - if $tool list installed "httpd" >/dev/null 2>&1; then - pkgs="$pkgs - mod_ssl - " - fi + BootstrapRpmCommonBase "$python_pkgs" +} - if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then - error "Could not install OS dependencies. Aborting bootstrap!" +# If new packages are installed by BootstrapRpmPython3 below, this version +# number must be increased. +BOOTSTRAP_RPM_PYTHON3_VERSION=1 + +BootstrapRpmPython3() { + # Tested with: + # - CentOS 6 + + InitializeRPMCommonBase + + # EPEL uses python34 + if $TOOL list python34 >/dev/null 2>&1; then + python_pkgs="python34 + python34-devel + python34-tools + " + else + error "No supported Python package available to install. Aborting bootstrap!" exit 1 fi + + BootstrapRpmCommonBase "$python_pkgs" } # If new packages are installed by BootstrapSuseCommon below, this version @@ -717,11 +775,24 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + prev_le_python="$LE_PYTHON" + unset LE_PYTHON + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -eq 26 ]; then + Bootstrap() { + BootstrapMessage "RedHat-based OSes that will use Python3" + BootstrapRpmPython3 + } + USE_PYTHON_3=1 + BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" + else + Bootstrap() { + BootstrapMessage "RedHat-based OSes" + BootstrapRpmCommon + } + BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + fi + export LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -860,10 +931,18 @@ if [ "$1" = "--le-auto-phase2" ]; then say "Creating virtual environment..." DeterminePythonVersion rm -rf "$VENV_PATH" - if [ "$VERBOSE" = 1 ]; then - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + if [ "$PYVER" -le 27 ]; then + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi else - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + if [ "$VERBOSE" = 1 ]; then + "$LE_PYTHON" -m venv "$VENV_PATH" + else + "$LE_PYTHON" -m venv "$VENV_PATH" > /dev/null + fi fi if [ -n "$BOOTSTRAP_VERSION" ]; then @@ -1358,17 +1437,22 @@ On failure, return non-zero. """ -from __future__ import print_function +from __future__ import print_function, unicode_literals from distutils.version import LooseVersion from json import loads from os import devnull, environ from os.path import dirname, join import re +import ssl from subprocess import check_call, CalledProcessError from sys import argv, exit -from urllib2 import build_opener, HTTPHandler, HTTPSHandler -from urllib2 import HTTPError, URLError +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler + from urllib2 import HTTPError, URLError +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler + from urllib.error import HTTPError, URLError PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq @@ -1390,8 +1474,11 @@ class HttpsGetter(object): def __init__(self): """Build an HTTPS opener.""" # Based on pip 1.4.1's URLOpener - # This verifies certs on only Python >=2.7.9. - self._opener = build_opener(HTTPSHandler()) + # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. + if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): + self._opener = build_opener(HTTPSHandler(context=create_CERT_NONE_context())) + else: + self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: for handler in self._opener.handlers: if isinstance(handler, HTTPHandler): @@ -1413,7 +1500,7 @@ class HttpsGetter(object): def write(contents, dir, filename): """Write something to a file in a certain directory.""" - with open(join(dir, filename), 'w') as file: + with open(join(dir, filename), 'wb') as file: file.write(contents) @@ -1421,13 +1508,13 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/certbot/json'))) + 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8')) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in metadata['releases'].iterkeys() + in iter(metadata['releases'].keys()) if re.match('^[0-9.]+$', r))) @@ -1444,7 +1531,7 @@ def verified_new_le_auto(get, tag, temp_dir): 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') - write(PUBLIC_KEY, temp_dir, 'public_key.pem') + write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem') try: with open(devnull, 'w') as dev_null: check_call(['openssl', 'dgst', '-sha256', '-verify', @@ -1459,6 +1546,14 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) +def create_CERT_NONE_context(): + """Create a SSLContext object to not check hostname.""" + # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.verify_mode = ssl.CERT_NONE + return context + + def main(): get = HttpsGetter().get flag = argv[1] diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 861ef0a6e..f4c1b202f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -246,15 +246,29 @@ DeprecationBootstrap() { fi } - +# Sets LE_PYTHON to Python version string and PYVER to the first two +# digits of the python version DeterminePythonVersion() { - for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do - # Break (while keeping the LE_PYTHON value) if found. - $EXISTS "$LE_PYTHON" > /dev/null && break - done + # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + if [ -n "$USE_PYTHON_3" ]; then + for LE_PYTHON in "$LE_PYTHON" python3; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + else + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + $EXISTS "$LE_PYTHON" > /dev/null && break + done + fi if [ "$?" != "0" ]; then - error "Cannot find any Pythons; please install one!" - exit 1 + if [ "$1" != "NOCRASH" ]; then + error "Cannot find any Pythons; please install one!" + exit 1 + else + PYVER=0 + return 0 + fi fi export LE_PYTHON @@ -267,7 +281,9 @@ DeterminePythonVersion() { } {{ bootstrappers/deb_common.sh }} +{{ bootstrappers/rpm_common_base.sh }} {{ bootstrappers/rpm_common.sh }} +{{ bootstrappers/rpm_python3.sh }} {{ bootstrappers/suse_common.sh }} {{ bootstrappers/arch_common.sh }} {{ bootstrappers/gentoo_common.sh }} @@ -298,11 +314,24 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then - Bootstrap() { - BootstrapMessage "RedHat-based OSes" - BootstrapRpmCommon - } - BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + prev_le_python="$LE_PYTHON" + unset LE_PYTHON + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -eq 26 ]; then + Bootstrap() { + BootstrapMessage "RedHat-based OSes that will use Python3" + BootstrapRpmPython3 + } + USE_PYTHON_3=1 + BOOTSTRAP_VERSION="BootstrapRpmPython3 $BOOTSTRAP_RPM_PYTHON3_VERSION" + else + Bootstrap() { + BootstrapMessage "RedHat-based OSes" + BootstrapRpmCommon + } + BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" + fi + export LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -441,10 +470,18 @@ if [ "$1" = "--le-auto-phase2" ]; then say "Creating virtual environment..." DeterminePythonVersion rm -rf "$VENV_PATH" - if [ "$VERBOSE" = 1 ]; then - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + if [ "$PYVER" -le 27 ]; then + if [ "$VERBOSE" = 1 ]; then + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" + else + virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + fi else - virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null + if [ "$VERBOSE" = 1 ]; then + "$LE_PYTHON" -m venv "$VENV_PATH" + else + "$LE_PYTHON" -m venv "$VENV_PATH" > /dev/null + fi fi if [ -n "$BOOTSTRAP_VERSION" ]; then diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 5b120a9e6..80d55a393 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -7,61 +7,13 @@ BootstrapRpmCommon() { # - Fedora 20, 21, 22, 23 (x64) # - Centos 7 (x64: on DigitalOcean droplet) # - CentOS 7 Minimal install in a Hyper-V VM - # - CentOS 6 (EPEL must be installed manually) + # - CentOS 6 - if type dnf 2>/dev/null - then - tool=dnf - elif type yum 2>/dev/null - then - tool=yum - - else - error "Neither yum nor dnf found. Aborting bootstrap!" - exit 1 - fi - - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if [ "$QUIET" = 1 ]; then - QUIET_FLAG='--quiet' - fi - - if ! $tool list *virtualenv >/dev/null 2>&1; then - echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $tool list epel-release >/dev/null 2>&1; then - error "Enable the EPEL repository and try running Certbot again." - exit 1 - fi - if [ "$ASSUME_YES" = 1 ]; then - /bin/echo -n "Enabling the EPEL repository in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." - sleep 1s - fi - if ! $tool install $yes_flag $QUIET_FLAG epel-release; then - error "Could not enable EPEL. Aborting bootstrap!" - exit 1 - fi - fi - - pkgs=" - gcc - augeas-libs - openssl - openssl-devel - libffi-devel - redhat-rpm-config - ca-certificates - " + InitializeRPMCommonBase # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $tool list python >/dev/null 2>&1; then - pkgs="$pkgs - python + if $TOOL list python >/dev/null 2>&1; then + python_pkgs="$python python-devel python-virtualenv python-tools @@ -69,9 +21,8 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $tool list python2 >/dev/null 2>&1; then - pkgs="$pkgs - python2 + elif $TOOL list python2 >/dev/null 2>&1; then + python_pkgs="$python2 python2-libs python2-setuptools python2-devel @@ -82,8 +33,7 @@ BootstrapRpmCommon() { # Some distros and older versions of current distros use a "python27" # instead of the "python" or "python-" naming convention. else - pkgs="$pkgs - python27 + python_pkgs="$python27 python27-devel python27-virtualenv python27-tools @@ -91,14 +41,5 @@ BootstrapRpmCommon() { " fi - if $tool list installed "httpd" >/dev/null 2>&1; then - pkgs="$pkgs - mod_ssl - " - fi - - if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then - error "Could not install OS dependencies. Aborting bootstrap!" - exit 1 - fi + BootstrapRpmCommonBase "$python_pkgs" } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh new file mode 100644 index 000000000..d7a9f3133 --- /dev/null +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh @@ -0,0 +1,78 @@ +# If new packages are installed by BootstrapRpmCommonBase below, version +# numbers in rpm_common.sh and rpm_python3.sh must be increased. + +# Sets TOOL to the name of the package manager +# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG. +# Enables EPEL if applicable and possible. +InitializeRPMCommonBase() { + if type dnf 2>/dev/null + then + TOOL=dnf + elif type yum 2>/dev/null + then + TOOL=yum + + else + error "Neither yum nor dnf found. Aborting bootstrap!" + exit 1 + fi + + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='--quiet' + fi + + if ! $TOOL list *virtualenv >/dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if ! $TOOL list epel-release >/dev/null 2>&1; then + error "Enable the EPEL repository and try running Certbot again." + exit 1 + fi + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s + fi + if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then + error "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi +} + +BootstrapRpmCommonBase() { + # Arguments: whitespace-delimited python packages to install + + InitializeRPMCommonBase # This call is superfluous in practice + + pkgs=" + gcc + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + + # Add the python packages + pkgs="$pkgs + $1 + " + + if $TOOL list installed "httpd" >/dev/null 2>&1; then + pkgs="$pkgs + mod_ssl + " + fi + + if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then + error "Could not install OS dependencies. Aborting bootstrap!" + exit 1 + fi +} diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_python3.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_python3.sh new file mode 100644 index 000000000..b011a7235 --- /dev/null +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_python3.sh @@ -0,0 +1,23 @@ +# If new packages are installed by BootstrapRpmPython3 below, this version +# number must be increased. +BOOTSTRAP_RPM_PYTHON3_VERSION=1 + +BootstrapRpmPython3() { + # Tested with: + # - CentOS 6 + + InitializeRPMCommonBase + + # EPEL uses python34 + if $TOOL list python34 >/dev/null 2>&1; then + python_pkgs="python34 + python34-devel + python34-tools + " + else + error "No supported Python package available to install. Aborting bootstrap!" + exit 1 + fi + + BootstrapRpmCommonBase "$python_pkgs" +} diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index 8f34351c9..ae72a299b 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -11,17 +11,22 @@ On failure, return non-zero. """ -from __future__ import print_function +from __future__ import print_function, unicode_literals from distutils.version import LooseVersion from json import loads from os import devnull, environ from os.path import dirname, join import re +import ssl from subprocess import check_call, CalledProcessError from sys import argv, exit -from urllib2 import build_opener, HTTPHandler, HTTPSHandler -from urllib2 import HTTPError, URLError +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler + from urllib2 import HTTPError, URLError +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler + from urllib.error import HTTPError, URLError PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq @@ -43,8 +48,11 @@ class HttpsGetter(object): def __init__(self): """Build an HTTPS opener.""" # Based on pip 1.4.1's URLOpener - # This verifies certs on only Python >=2.7.9. - self._opener = build_opener(HTTPSHandler()) + # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. + if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): + self._opener = build_opener(HTTPSHandler(context=create_CERT_NONE_context())) + else: + self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: for handler in self._opener.handlers: if isinstance(handler, HTTPHandler): @@ -66,7 +74,7 @@ class HttpsGetter(object): def write(contents, dir, filename): """Write something to a file in a certain directory.""" - with open(join(dir, filename), 'w') as file: + with open(join(dir, filename), 'wb') as file: file.write(contents) @@ -74,13 +82,13 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/certbot/json'))) + 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8')) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in metadata['releases'].iterkeys() + in iter(metadata['releases'].keys()) if re.match('^[0-9.]+$', r))) @@ -97,7 +105,7 @@ def verified_new_le_auto(get, tag, temp_dir): 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') - write(PUBLIC_KEY, temp_dir, 'public_key.pem') + write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem') try: with open(devnull, 'w') as dev_null: check_call(['openssl', 'dgst', '-sha256', '-verify', @@ -112,6 +120,14 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) +def create_CERT_NONE_context(): + """Create a SSLContext object to not check hostname.""" + # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.verify_mode = ssl.CERT_NONE + return context + + def main(): get = HttpsGetter().get flag = argv[1] diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 156466c82..d187452a1 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -202,6 +202,7 @@ LsIVPBuy9IcgHidUQ96hJnoPsDCWsHwX62495QKEarauyKQrJzFes0EY95orDM47 Z5o/NDiQB11m91yNB0MmPYY9QSbnOA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68 iQIDAQAB -----END PUBLIC KEY-----""", + NO_CERT_VERIFY='1', **kwargs) env.update(d) return out_and_err( @@ -349,6 +350,7 @@ class AutoTests(TestCase): self.assertTrue("Couldn't verify signature of downloaded " "certbot-auto." in exc.output) else: + print(out) self.fail('Signature check on certbot-auto erroneously passed.') def test_pip_failure(self): diff --git a/letsencrypt-auto-source/tests/centos6_tests.sh b/letsencrypt-auto-source/tests/centos6_tests.sh new file mode 100644 index 000000000..e3ebbaec5 --- /dev/null +++ b/letsencrypt-auto-source/tests/centos6_tests.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Start by making sure your system is up-to-date: +yum update > /dev/null +yum install -y centos-release-scl > /dev/null +yum install -y python27 > /dev/null 2> /dev/null + +# we're going to modify env variables, so do this in a subshell +( +source /opt/rh/python27/enable + +# ensure python 3 isn't installed +python3 --version 2> /dev/null +RESULT=$? +if [ $RESULT -eq 0 ]; then + error "Python3 is already installed." + exit 1 +fi + +# ensure python2.7 is available +python2.7 --version 2> /dev/null +RESULT=$? +if [ $RESULT -ne 0 ]; then + error "Python3 is not available." + exit 1 +fi + +# bootstrap, but don't install python 3. +certbot/letsencrypt-auto-source/letsencrypt-auto --no-self-upgrade -n > /dev/null 2> /dev/null + +# ensure python 3 isn't installed +python3 --version 2> /dev/null +RESULT=$? +if [ $RESULT -eq 0 ]; then + error "letsencrypt-auto installed Python3 even though Python2.7 is present." + exit 1 +fi + +echo "" +echo "PASSED: Did not upgrade to Python3 when Python2.7 is present." +) + +# ensure python2.7 isn't available +python2.7 --version 2> /dev/null +RESULT=$? +if [ $RESULT -eq 0 ]; then + error "Python2.7 is still available." + exit 1 +fi + +# bootstrap, this time installing python3 +certbot/letsencrypt-auto-source/letsencrypt-auto --no-self-upgrade -n > /dev/null 2> /dev/null + +# ensure python 3 is installed +python3 --version > /dev/null +RESULT=$? +if [ $RESULT -ne 0 ]; then + error "letsencrypt-auto failed to install Python3 when only Python2.6 is present." + exit 1 +fi + +echo "PASSED: Successfully upgraded to Python3 when only Python2.6 is present." +echo "" + +# test using python3 +pytest -v -s certbot/letsencrypt-auto-source/tests From 24ddc65cd4765e82067ab5fd963ffe81348e1f02 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Jan 2018 17:02:20 -0800 Subject: [PATCH 51/67] Allow non-interactive revocation without deleting certificates (#5386) * Add --delete-after-revoke flags * Use delete_after_revoke value * Add delete_after_revoke unit tests * Add integration tests for delete-after-revoke. --- certbot/cli.py | 12 ++++++++++ certbot/constants.py | 1 + certbot/main.py | 8 ++++--- certbot/tests/cli_test.py | 14 +++++++++++ certbot/tests/main_test.py | 46 ++++++++++++++++++++++++++++-------- tests/boulder-integration.sh | 9 +++++-- 6 files changed, 75 insertions(+), 15 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 622462278..f0fa7eb7e 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1220,6 +1220,18 @@ def _create_subparsers(helpful): key=constants.REVOCATION_REASONS.get)), action=_EncodeReasonAction, default=flag_default("reason"), help="Specify reason for revoking certificate. (default: unspecified)") + helpful.add("revoke", + "--delete-after-revoke", action="store_true", + default=flag_default("delete_after_revoke"), + help="Delete certificates after revoking them.") + helpful.add("revoke", + "--no-delete-after-revoke", action="store_false", + dest="delete_after_revoke", + default=flag_default("delete_after_revoke"), + help="Do not delete certificates after revoking them. This " + "option should be used with caution because the 'renew' " + "subcommand will attempt to renew undeleted revoked " + "certificates.") helpful.add("rollback", "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), diff --git a/certbot/constants.py b/certbot/constants.py index 0ac82dafe..a6878824b 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -71,6 +71,7 @@ CLI_DEFAULTS = dict( user_agent_comment=None, csr=None, reason=0, + delete_after_revoke=None, rollback_checkpoints=1, init=False, prepare=False, diff --git a/certbot/main.py b/certbot/main.py index 1c6432fd9..e25e030aa 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -536,9 +536,11 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b display = zope.component.getUtility(interfaces.IDisplay) reporter_util = zope.component.getUtility(interfaces.IReporter) - msg = ("Would you like to delete the cert(s) you just revoked?") - attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No", - force_interactive=True, default=True) + attempt_deletion = config.delete_after_revoke + if attempt_deletion is None: + msg = ("Would you like to delete the cert(s) you just revoked?") + attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No", + force_interactive=True, default=True) if not attempt_deletion: reporter_util.add_message("Not deleting revoked certs.", reporter_util.LOW_PRIORITY) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 2fce412e2..c5935d722 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -164,6 +164,8 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue("--cert-path" in out) self.assertTrue("--key-path" in out) self.assertTrue("--reason" in out) + self.assertTrue("--delete-after-revoke" in out) + self.assertTrue("--no-delete-after-revoke" in out) out = self._help_output(['-h', 'config_changes']) self.assertTrue("--cert-path" not in out) @@ -412,6 +414,18 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods def test_no_directory_hooks_unset(self): self.assertTrue(self.parse([]).directory_hooks) + def test_delete_after_revoke(self): + namespace = self.parse(["--delete-after-revoke"]) + self.assertTrue(namespace.delete_after_revoke) + + def test_delete_after_revoke_default(self): + namespace = self.parse([]) + self.assertEqual(namespace.delete_after_revoke, None) + + def test_no_delete_after_revoke(self): + namespace = self.parse(["--no-delete-after-revoke"]) + self.assertFalse(namespace.delete_after_revoke) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 04b71dcc7..b1d58542f 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -298,25 +298,29 @@ class RevokeTest(test_util.TempDirTestCase): self._call() self.assertFalse(mock_delete.called) -class DeleteIfAppropriateTest(unittest.TestCase): +class DeleteIfAppropriateTest(test_util.ConfigTestCase): """Tests for certbot.main._delete_if_appropriate """ - def setUp(self): - self.config = mock.Mock() - self.config.namespace = mock.Mock() - self.config.namespace.noninteractive_mode = False - def _call(self, mock_config): from certbot.main import _delete_if_appropriate _delete_if_appropriate(mock_config) - @mock.patch('certbot.cert_manager.delete') + def _test_delete_opt_out_common(self, mock_get_utility): + with mock.patch('certbot.cert_manager.delete') as mock_delete: + self._call(self.config) + mock_delete.assert_not_called() + self.assertTrue(mock_get_utility().add_message.called) + @test_util.patch_get_utility() - def test_delete_opt_out(self, mock_get_utility, mock_delete): + def test_delete_flag_opt_out(self, mock_get_utility): + self.config.delete_after_revoke = False + self._test_delete_opt_out_common(mock_get_utility) + + @test_util.patch_get_utility() + def test_delete_prompt_opt_out(self, mock_get_utility): util_mock = mock_get_utility() util_mock.yesno.return_value = False - self._call(self.config) - mock_delete.assert_not_called() + self._test_delete_opt_out_common(mock_get_utility) # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @@ -397,6 +401,28 @@ class DeleteIfAppropriateTest(unittest.TestCase): self._call(config) self.assertEqual(mock_delete.call_count, 1) + # pylint: disable=too-many-arguments + @mock.patch('certbot.storage.renewal_file_for_certname') + @mock.patch('certbot.cert_manager.match_and_check_overlaps') + @mock.patch('certbot.storage.full_archive_path') + @mock.patch('certbot.cert_manager.cert_path_to_lineage') + @mock.patch('certbot.cert_manager.delete') + @test_util.patch_get_utility() + def test_opt_in_deletion(self, mock_get_utility, mock_delete, + mock_cert_path_to_lineage, mock_full_archive_dir, + mock_match_and_check_overlaps, mock_renewal_file_for_certname): + # pylint: disable = unused-argument + config = self.config + config.namespace.delete_after_revoke = True + config.cert_path = "/some/reasonable/path" + config.certname = "" + mock_cert_path_to_lineage.return_value = "example.com" + mock_full_archive_dir.return_value = "" + mock_match_and_check_overlaps.return_value = "" + self._call(config) + self.assertEqual(mock_delete.call_count, 1) + self.assertFalse(mock_get_utility().yesno.called) + # pylint: disable=too-many-arguments @mock.patch('certbot.storage.renewal_file_for_certname') @mock.patch('certbot.cert_manager.match_and_check_overlaps') diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 1e0b7754b..e1aad4336 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -345,9 +345,14 @@ common auth --must-staple --domains "must-staple.le.wtf" openssl x509 -in "${root}/conf/live/must-staple.le.wtf/cert.pem" -text | grep '1.3.6.1.5.5.7.1.24' # revoke by account key -common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" +common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" --delete-after-revoke # revoke renewed -common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem" +common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem" --no-delete-after-revoke +if [ ! -d "$root/conf/live/le1.wtf" ]; then + echo "cert deleted when --no-delete-after-revoke was used!" + exit 1 +fi +common delete --cert-name le1.wtf # revoke by cert key common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \ --key-path "$root/conf/live/le2.wtf/privkey.pem" From e02adec26b9018a3c7fae40fdc13086cd336b05c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 8 Jan 2018 17:38:03 -0800 Subject: [PATCH 52/67] Have letsencrypt-auto do a real upgrade in leauto-upgrades option 2 (#5390) * Make leauto_upgrades do a real upgrade * Cleanup vars and output * Sleep until the server is ready * add simple_http_server.py * Use a randomly assigned port * s/realpath/readlink * wait for server before getting port * s/localhost/all interfaces --- .../letstest/scripts/test_leauto_upgrades.sh | 47 +++++++++++++++++-- tools/simple_http_server.py | 26 ++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) create mode 100755 tools/simple_http_server.py diff --git a/tests/letstest/scripts/test_leauto_upgrades.sh b/tests/letstest/scripts/test_leauto_upgrades.sh index cb659786e..a83cbd826 100755 --- a/tests/letstest/scripts/test_leauto_upgrades.sh +++ b/tests/letstest/scripts/test_leauto_upgrades.sh @@ -15,19 +15,56 @@ if ! command -v git ; then exit 1 fi fi -BRANCH=`git rev-parse --abbrev-ref HEAD` # 0.5.0 is the oldest version of letsencrypt-auto that can be used because it's # the first version that pins package versions, properly supports # --no-self-upgrade, and works with newer versions of pip. -git checkout -f v0.5.0 +git checkout -f v0.5.0 letsencrypt-auto if ! ./letsencrypt-auto -v --debug --version --no-self-upgrade 2>&1 | grep 0.5.0 ; then echo initial installation appeared to fail exit 1 fi -git checkout -f "$BRANCH" -EXPECTED_VERSION=$(grep -m1 LE_AUTO_VERSION letsencrypt-auto | cut -d\" -f2) -if ! ./letsencrypt-auto -v --debug --version --no-self-upgrade 2>&1 | grep $EXPECTED_VERSION ; then +# Now that python and openssl have been installed, we can set up a fake server +# to provide a new version of letsencrypt-auto. First, we start the server and +# directory to be served. +MY_TEMP_DIR=$(mktemp -d) +PORT_FILE="$MY_TEMP_DIR/port" +SERVER_PATH=$(tools/readlink.py tools/simple_http_server.py) +cd "$MY_TEMP_DIR" +"$SERVER_PATH" 0 > $PORT_FILE & +SERVER_PID=$! +trap 'kill "$SERVER_PID" && rm -rf "$MY_TEMP_DIR"' EXIT +cd ~- + +# Then, we set up the files to be served. +FAKE_VERSION_NUM="99.99.99" +echo "{\"releases\": {\"$FAKE_VERSION_NUM\": null}}" > "$MY_TEMP_DIR/json" +LE_AUTO_SOURCE_DIR="$MY_TEMP_DIR/v$FAKE_VERSION_NUM" +NEW_LE_AUTO_PATH="$LE_AUTO_SOURCE_DIR/letsencrypt-auto" +mkdir "$LE_AUTO_SOURCE_DIR" +cp letsencrypt-auto-source/letsencrypt-auto "$LE_AUTO_SOURCE_DIR/letsencrypt-auto" +SIGNING_KEY="letsencrypt-auto-source/tests/signing.key" +openssl dgst -sha256 -sign "$SIGNING_KEY" -out "$NEW_LE_AUTO_PATH.sig" "$NEW_LE_AUTO_PATH" + +# Next, we wait for the server to start and get the port number. +sleep 5s +SERVER_PORT=$(sed -n 's/.*port \([0-9]\+\).*/\1/p' "$PORT_FILE") + +# Finally, we set the necessary certbot-auto environment variables. +export LE_AUTO_DIR_TEMPLATE="http://localhost:$SERVER_PORT/%s/" +export LE_AUTO_JSON_URL="http://localhost:$SERVER_PORT/json" +export LE_AUTO_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMoSzLYQ7E1sdSOkwelg +tzKIh2qi3bpXuYtcfFC0XrvWig071NwIj+dZiT0OLZ2hPispEH0B7ISuuWg1ll7G +hFW0VdbxL6JdGzS2ShNWkX9hE9z+j8VqwDPOBn3ZHm03qwpYkBDwQib3KqOdYbTT +uUtJmmGcuk3a9Aq/sCT6DdfmTSdP5asdQYwIcaQreDrOosaS84DTWI3IU+UYJVgl +LsIVPBuy9IcgHidUQ96hJnoPsDCWsHwX62495QKEarauyKQrJzFes0EY95orDM47 +Z5o/NDiQB11m91yNB0MmPYY9QSbnOA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68 +iQIDAQAB +-----END PUBLIC KEY----- +" + +if ! ./letsencrypt-auto -v --debug --version || ! diff letsencrypt-auto letsencrypt-auto-source/letsencrypt-auto ; then echo upgrade appeared to fail exit 1 fi diff --git a/tools/simple_http_server.py b/tools/simple_http_server.py new file mode 100755 index 000000000..26bf231b7 --- /dev/null +++ b/tools/simple_http_server.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +"""A version of Python 2.x's SimpleHTTPServer that flushes its output.""" +from BaseHTTPServer import HTTPServer +from SimpleHTTPServer import SimpleHTTPRequestHandler +import sys + +def serve_forever(port=0): + """Spins up an HTTP server on all interfaces and the given port. + + A message is printed to stdout specifying the address and port being used + by the server. + + :param int port: port number to use. + + """ + server = HTTPServer(('', port), SimpleHTTPRequestHandler) + print 'Serving HTTP on {0} port {1} ...'.format(*server.server_address) + sys.stdout.flush() + server.serve_forever() + + +if __name__ == '__main__': + kwargs = {} + if len(sys.argv) > 1: + kwargs['port'] = int(sys.argv[1]) + serve_forever(**kwargs) From d557475bb6f921b7bdb0459d77a162d673044dce Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 9 Jan 2018 07:46:21 -0800 Subject: [PATCH 53/67] update Apache ciphersuites (#5383) --- certbot-apache/certbot_apache/centos-options-ssl-apache.conf | 2 +- certbot-apache/certbot_apache/constants.py | 2 ++ certbot-apache/certbot_apache/options-ssl-apache.conf | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf index 17ae1be76..56c946a4e 100644 --- a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf @@ -8,7 +8,7 @@ SSLEngine on # Intermediate configuration, tweak to your needs SSLProtocol all -SSLv2 -SSLv3 -SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA +SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS SSLHonorCipherOrder on SSLOptions +StrictRequire diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index a13ca04a6..fd6a9eb11 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -16,6 +16,8 @@ ALL_SSL_OPTIONS_HASHES = [ '4066b90268c03c9ba0201068eaa39abbc02acf9558bb45a788b630eb85dadf27', 'f175e2e7c673bd88d0aff8220735f385f916142c44aa83b09f1df88dd4767a88', 'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b', + '80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791', + 'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082', ] """SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC""" diff --git a/certbot-apache/certbot_apache/options-ssl-apache.conf b/certbot-apache/certbot_apache/options-ssl-apache.conf index 950a02a8b..8113ee81e 100644 --- a/certbot-apache/certbot_apache/options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/options-ssl-apache.conf @@ -8,7 +8,7 @@ SSLEngine on # Intermediate configuration, tweak to your needs SSLProtocol all -SSLv2 -SSLv3 -SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA +SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS SSLHonorCipherOrder on SSLCompression off From 62ffcf53738760f901d8ad3b31bff3855d1648c6 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 9 Jan 2018 17:48:05 +0200 Subject: [PATCH 54/67] Fix macOS builds for Python2.7 in Travis (#5378) * Add OSX Python2 tests * Make sure python2 is originating from homebrew on macOS * Upgrade the already installed python2 instead of trying to reinstall --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3d41bfa4b..35666d8e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ cache: - $HOME/.cache/pip before_install: - - '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas python3)' + - '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas python3 && brew upgrade python && brew link python)' before_script: - 'if [ $TRAVIS_OS_NAME = osx ] ; then ulimit -n 1024 ; fi' From 288c4d956cf59015590bc24e045138335b46a40d Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 9 Jan 2018 18:28:52 +0200 Subject: [PATCH 55/67] Automatically install updates in test script (#5394) --- letsencrypt-auto-source/tests/centos6_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/tests/centos6_tests.sh b/letsencrypt-auto-source/tests/centos6_tests.sh index e3ebbaec5..23b1e16e8 100644 --- a/letsencrypt-auto-source/tests/centos6_tests.sh +++ b/letsencrypt-auto-source/tests/centos6_tests.sh @@ -1,6 +1,6 @@ #!/bin/bash # Start by making sure your system is up-to-date: -yum update > /dev/null +yum update -y > /dev/null yum install -y centos-release-scl > /dev/null yum install -y python27 > /dev/null 2> /dev/null From 887a6bcfce73f7b3c556720ccbbc470bf8855606 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 9 Jan 2018 15:40:26 -0800 Subject: [PATCH 56/67] Handle need to rebootstrap before fetch.py (#5389) * Fix #5387 * Add test for #5387 * remove LE_PYTHON * Use environment variable to reduce line length --- letsencrypt-auto-source/letsencrypt-auto | 20 +++++++++++++------ .../letsencrypt-auto.template | 20 +++++++++++++------ .../tests/centos6_tests.sh | 12 +++++++++-- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index f1361d8ea..5f46e3a31 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -246,10 +246,14 @@ DeprecationBootstrap() { fi } +MIN_PYTHON_VERSION="2.6" +MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') # Sets LE_PYTHON to Python version string and PYVER to the first two # digits of the python version DeterminePythonVersion() { # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + # + # If no Python is found, PYVER is set to 0. if [ -n "$USE_PYTHON_3" ]; then for LE_PYTHON in "$LE_PYTHON" python3; do # Break (while keeping the LE_PYTHON value) if found. @@ -273,10 +277,12 @@ DeterminePythonVersion() { export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ "$PYVER" -lt 26 ]; then - error "You have an ancient version of Python entombed in your operating system..." - error "This isn't going to work; you'll need at least version 2.6." - exit 1 + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + if [ "$1" != "NOCRASH" ]; then + error "You have an ancient version of Python entombed in your operating system..." + error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION." + exit 1 + fi fi } @@ -1575,8 +1581,10 @@ if __name__ == '__main__': UNLIKELY_EOF # --------------------------------------------------------------------------- - DeterminePythonVersion - if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." + elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index f4c1b202f..7c3cbac08 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -246,10 +246,14 @@ DeprecationBootstrap() { fi } +MIN_PYTHON_VERSION="2.6" +MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') # Sets LE_PYTHON to Python version string and PYVER to the first two # digits of the python version DeterminePythonVersion() { # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python + # + # If no Python is found, PYVER is set to 0. if [ -n "$USE_PYTHON_3" ]; then for LE_PYTHON in "$LE_PYTHON" python3; do # Break (while keeping the LE_PYTHON value) if found. @@ -273,10 +277,12 @@ DeterminePythonVersion() { export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ "$PYVER" -lt 26 ]; then - error "You have an ancient version of Python entombed in your operating system..." - error "This isn't going to work; you'll need at least version 2.6." - exit 1 + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + if [ "$1" != "NOCRASH" ]; then + error "You have an ancient version of Python entombed in your operating system..." + error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION." + exit 1 + fi fi } @@ -586,8 +592,10 @@ else {{ fetch.py }} UNLIKELY_EOF # --------------------------------------------------------------------------- - DeterminePythonVersion - if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + DeterminePythonVersion "NOCRASH" + if [ "$PYVER" -lt "$MIN_PYVER" ]; then + error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." + elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." diff --git a/letsencrypt-auto-source/tests/centos6_tests.sh b/letsencrypt-auto-source/tests/centos6_tests.sh index 23b1e16e8..a0e96edf8 100644 --- a/letsencrypt-auto-source/tests/centos6_tests.sh +++ b/letsencrypt-auto-source/tests/centos6_tests.sh @@ -4,6 +4,8 @@ yum update -y > /dev/null yum install -y centos-release-scl > /dev/null yum install -y python27 > /dev/null 2> /dev/null +LE_AUTO="certbot/letsencrypt-auto-source/letsencrypt-auto" + # we're going to modify env variables, so do this in a subshell ( source /opt/rh/python27/enable @@ -25,7 +27,7 @@ if [ $RESULT -ne 0 ]; then fi # bootstrap, but don't install python 3. -certbot/letsencrypt-auto-source/letsencrypt-auto --no-self-upgrade -n > /dev/null 2> /dev/null +"$LE_AUTO" --no-self-upgrade -n > /dev/null 2> /dev/null # ensure python 3 isn't installed python3 --version 2> /dev/null @@ -47,8 +49,14 @@ if [ $RESULT -eq 0 ]; then exit 1 fi +# Skip self upgrade due to Python 3 not being available. +if ! "$LE_AUTO" 2>&1 | grep -q "WARNING: couldn't find Python"; then + echo "Python upgrade failure warning not printed!" + exit 1 +fi + # bootstrap, this time installing python3 -certbot/letsencrypt-auto-source/letsencrypt-auto --no-self-upgrade -n > /dev/null 2> /dev/null +"$LE_AUTO" --no-self-upgrade -n > /dev/null 2> /dev/null # ensure python 3 is installed python3 --version > /dev/null From f5a02714cd8b610db52ac04e62685bba9c081a47 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Tue, 9 Jan 2018 16:11:04 -0800 Subject: [PATCH 57/67] Add deprecation warning for Python 2.6 (#5391) * Add deprecation warning for Python 2.6 * Allow disabling Python 2.6 warning --- acme/acme/__init__.py | 13 +++++++------ certbot/main.py | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/acme/acme/__init__.py b/acme/acme/__init__.py index 618dda200..5850fa955 100644 --- a/acme/acme/__init__.py +++ b/acme/acme/__init__.py @@ -13,9 +13,10 @@ supported version: `draft-ietf-acme-01`_. import sys import warnings -if sys.version_info[:2] == (3, 3): - warnings.warn( - "Python 3.3 support will be dropped in the next release of " - "acme. Please upgrade your Python version.", - PendingDeprecationWarning, - ) #pragma: no cover +for (major, minor) in [(2, 6), (3, 3)]: + if sys.version_info[:2] == (major, minor): + warnings.warn( + "Python {0}.{1} support will be dropped in the next release of " + "acme. Please upgrade your Python version.".format(major, minor), + DeprecationWarning, + ) #pragma: no cover diff --git a/certbot/main.py b/certbot/main.py index e25e030aa..32dd69256 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -4,6 +4,7 @@ import functools import logging.handlers import os import sys +import warnings import configobj import josepy as jose @@ -1217,9 +1218,17 @@ def main(cli_args=sys.argv[1:]): # Let plugins_cmd be run as un-privileged user. if config.func != plugins_cmd: raise - if sys.version_info[:2] == (3, 3): - logger.warning("Python 3.3 support will be dropped in the next release " - "of Certbot - please upgrade your Python version.") + deprecation_fmt = ( + "Python %s.%s support will be dropped in the next " + "release of Certbot - please upgrade your Python version.") + # We use the warnings system for Python 2.6 and logging for Python 3 + # because DeprecationWarnings are only reported by default in Python <= 2.6 + # and warnings can be disabled by the user. + if sys.version_info[:2] == (2, 6): + warning = deprecation_fmt % sys.version_info[:2] + warnings.warn(warning, DeprecationWarning) + elif sys.version_info[:2] == (3, 3): + logger.warning(deprecation_fmt, *sys.version_info[:2]) set_displayer(config) From 6eb459354fc28433ac2eabc3be62c809df66a4a1 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Tue, 9 Jan 2018 16:48:16 -0800 Subject: [PATCH 58/67] Address erikrose's comments on #5329 (#5400) --- letsencrypt-auto-source/letsencrypt-auto | 13 ++++++++----- letsencrypt-auto-source/letsencrypt-auto.template | 5 ++++- .../pieces/bootstrappers/rpm_common_base.sh | 2 +- letsencrypt-auto-source/pieces/fetch.py | 6 +++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 5f46e3a31..712ef6813 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -254,7 +254,7 @@ DeterminePythonVersion() { # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python # # If no Python is found, PYVER is set to 0. - if [ -n "$USE_PYTHON_3" ]; then + if [ "$USE_PYTHON_3" = 1 ]; then for LE_PYTHON in "$LE_PYTHON" python3; do # Break (while keeping the LE_PYTHON value) if found. $EXISTS "$LE_PYTHON" > /dev/null && break @@ -443,7 +443,7 @@ InitializeRPMCommonBase() { sleep 1s /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 second..." sleep 1s fi if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then @@ -781,6 +781,9 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then + # Run DeterminePythonVersion to decide on the basis of available Python versions + # whether to use 2.x or 3.x on RedHat-like systems. + # Then, revert LE_PYTHON to its previous state. prev_le_python="$LE_PYTHON" unset LE_PYTHON DeterminePythonVersion "NOCRASH" @@ -1482,7 +1485,7 @@ class HttpsGetter(object): # Based on pip 1.4.1's URLOpener # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): - self._opener = build_opener(HTTPSHandler(context=create_CERT_NONE_context())) + self._opener = build_opener(HTTPSHandler(context=cert_none_context())) else: self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: @@ -1520,7 +1523,7 @@ def latest_stable_version(get): # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in iter(metadata['releases'].keys()) + in metadata['releases'].keys() if re.match('^[0-9.]+$', r))) @@ -1552,7 +1555,7 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) -def create_CERT_NONE_context(): +def cert_none_context(): """Create a SSLContext object to not check hostname.""" # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 7c3cbac08..b06ac9c80 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -254,7 +254,7 @@ DeterminePythonVersion() { # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python # # If no Python is found, PYVER is set to 0. - if [ -n "$USE_PYTHON_3" ]; then + if [ "$USE_PYTHON_3" = 1 ]; then for LE_PYTHON in "$LE_PYTHON" python3; do # Break (while keeping the LE_PYTHON value) if found. $EXISTS "$LE_PYTHON" > /dev/null && break @@ -320,6 +320,9 @@ elif [ -f /etc/mageia-release ]; then } BOOTSTRAP_VERSION="BootstrapMageiaCommon $BOOTSTRAP_MAGEIA_COMMON_VERSION" elif [ -f /etc/redhat-release ]; then + # Run DeterminePythonVersion to decide on the basis of available Python versions + # whether to use 2.x or 3.x on RedHat-like systems. + # Then, revert LE_PYTHON to its previous state. prev_le_python="$LE_PYTHON" unset LE_PYTHON DeterminePythonVersion "NOCRASH" diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh index d7a9f3133..326ad8b3f 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common_base.sh @@ -35,7 +35,7 @@ InitializeRPMCommonBase() { sleep 1s /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 second..." sleep 1s fi if ! $TOOL install $YES_FLAG $QUIET_FLAG epel-release; then diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index ae72a299b..1515fe353 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -50,7 +50,7 @@ class HttpsGetter(object): # Based on pip 1.4.1's URLOpener # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set. if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'): - self._opener = build_opener(HTTPSHandler(context=create_CERT_NONE_context())) + self._opener = build_opener(HTTPSHandler(context=cert_none_context())) else: self._opener = build_opener(HTTPSHandler()) # Strip out HTTPHandler to prevent MITM spoof: @@ -88,7 +88,7 @@ def latest_stable_version(get): # The regex is a sufficient regex for picking out prereleases for most # packages, LE included. return str(max(LooseVersion(r) for r - in iter(metadata['releases'].keys()) + in metadata['releases'].keys() if re.match('^[0-9.]+$', r))) @@ -120,7 +120,7 @@ def verified_new_le_auto(get, tag, temp_dir): "certbot-auto.", exc) -def create_CERT_NONE_context(): +def cert_none_context(): """Create a SSLContext object to not check hostname.""" # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this. context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) From 00634394f2e077ec5e621acd869e370f8619cd08 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 9 Jan 2018 21:16:44 -0800 Subject: [PATCH 59/67] Only respect LE_PYTHON inside USE_PYTHON_3 if we know a user must have set it version 2 (#5402) * stop exporting LE_PYTHON * unset LE_PYTHON sometimes --- letsencrypt-auto-source/letsencrypt-auto | 8 ++++++-- letsencrypt-auto-source/letsencrypt-auto.template | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 712ef6813..0ad089acd 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -274,7 +274,6 @@ DeterminePythonVersion() { return 0 fi fi - export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` if [ "$PYVER" -lt "$MIN_PYVER" ]; then @@ -801,7 +800,7 @@ elif [ -f /etc/redhat-release ]; then } BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" fi - export LE_PYTHON="$prev_le_python" + LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -906,6 +905,10 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg SetPrevBootstrapVersion + if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then + unset LE_PYTHON + fi + INSTALLED_VERSION="none" if [ -d "$VENV_PATH" ]; then # If the selected Bootstrap function isn't a noop and it differs from the @@ -1412,6 +1415,7 @@ else # upgrading. Phase 1 checks the version of the latest release of # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. + export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index b06ac9c80..5b8b1c164 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -274,7 +274,6 @@ DeterminePythonVersion() { return 0 fi fi - export LE_PYTHON PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` if [ "$PYVER" -lt "$MIN_PYVER" ]; then @@ -340,7 +339,7 @@ elif [ -f /etc/redhat-release ]; then } BOOTSTRAP_VERSION="BootstrapRpmCommon $BOOTSTRAP_RPM_COMMON_VERSION" fi - export LE_PYTHON="$prev_le_python" + LE_PYTHON="$prev_le_python" elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then Bootstrap() { BootstrapMessage "openSUSE-based OSes" @@ -445,6 +444,10 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg SetPrevBootstrapVersion + if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then + unset LE_PYTHON + fi + INSTALLED_VERSION="none" if [ -d "$VENV_PATH" ]; then # If the selected Bootstrap function isn't a noop and it differs from the @@ -571,6 +574,7 @@ else # upgrading. Phase 1 checks the version of the latest release of # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. + export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then From 3acf5d1ef909b2e775588a7e045bb00728e2de83 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Jan 2018 12:10:21 -0800 Subject: [PATCH 60/67] Fix rebootstraping with old venvs (#5392) * Fix rebootstrapping before venv move * add regression test * dedupe test * Cleanup case when two venvs exist. * Add clarifying comment * Add double venv test to leauto_upgrades * Fix logic with the help of coffee * redirect stderr * pass VENV_PATH through sudo * redirect stderr --- letsencrypt-auto-source/letsencrypt-auto | 24 ++++++++++-- .../letsencrypt-auto.template | 24 ++++++++++-- .../letstest/scripts/test_leauto_upgrades.sh | 37 ++++++++++++++++++- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 0ad089acd..b86517bb6 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -897,7 +897,11 @@ TempDir() { mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS } - +# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise, +# returns a non-zero number. +OldVenvExists() { + [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] +} if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -910,13 +914,21 @@ if [ "$1" = "--le-auto-phase2" ]; then fi INSTALLED_VERSION="none" - if [ -d "$VENV_PATH" ]; then + if [ -d "$VENV_PATH" ] || OldVenvExists; then # If the selected Bootstrap function isn't a noop and it differs from the # previously used version if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then # if non-interactive mode or stdin and stdout are connected to a terminal if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then - rm -rf "$VENV_PATH" + if [ -d "$VENV_PATH" ]; then + rm -rf "$VENV_PATH" + fi + # In the case the old venv was just a symlink to the new one, + # OldVenvExists is now false because we deleted the venv at VENV_PATH. + if OldVenvExists; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi RerunWithArgs "$@" else error "Skipping upgrade because new OS dependencies may need to be installed." @@ -926,6 +938,10 @@ if [ "$1" = "--le-auto-phase2" ]; then error "install any required packages." # Set INSTALLED_VERSION to be the same so we don't update the venv INSTALLED_VERSION="$LE_AUTO_VERSION" + # Continue to use OLD_VENV_PATH if the new venv doesn't exist + if [ ! -d "$VENV_PATH" ]; then + VENV_BIN="$OLD_VENV_PATH/bin" + fi fi elif [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings @@ -1418,7 +1434,7 @@ else export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then - if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if ! OldVenvExists; then if [ "$HELP" = 1 ]; then echo "$USAGE" exit 0 diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 5b8b1c164..96e5c2db0 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -436,7 +436,11 @@ TempDir() { mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS } - +# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise, +# returns a non-zero number. +OldVenvExists() { + [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] +} if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -449,13 +453,21 @@ if [ "$1" = "--le-auto-phase2" ]; then fi INSTALLED_VERSION="none" - if [ -d "$VENV_PATH" ]; then + if [ -d "$VENV_PATH" ] || OldVenvExists; then # If the selected Bootstrap function isn't a noop and it differs from the # previously used version if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then # if non-interactive mode or stdin and stdout are connected to a terminal if [ \( "$NONINTERACTIVE" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then - rm -rf "$VENV_PATH" + if [ -d "$VENV_PATH" ]; then + rm -rf "$VENV_PATH" + fi + # In the case the old venv was just a symlink to the new one, + # OldVenvExists is now false because we deleted the venv at VENV_PATH. + if OldVenvExists; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi RerunWithArgs "$@" else error "Skipping upgrade because new OS dependencies may need to be installed." @@ -465,6 +477,10 @@ if [ "$1" = "--le-auto-phase2" ]; then error "install any required packages." # Set INSTALLED_VERSION to be the same so we don't update the venv INSTALLED_VERSION="$LE_AUTO_VERSION" + # Continue to use OLD_VENV_PATH if the new venv doesn't exist + if [ ! -d "$VENV_PATH" ]; then + VENV_BIN="$OLD_VENV_PATH/bin" + fi fi elif [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings @@ -577,7 +593,7 @@ else export PHASE_1_VERSION="$LE_AUTO_VERSION" if [ ! -f "$VENV_BIN/letsencrypt" ]; then - if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if ! OldVenvExists; then if [ "$HELP" = 1 ]; then echo "$USAGE" exit 0 diff --git a/tests/letstest/scripts/test_leauto_upgrades.sh b/tests/letstest/scripts/test_leauto_upgrades.sh index a83cbd826..51472f2e6 100755 --- a/tests/letstest/scripts/test_leauto_upgrades.sh +++ b/tests/letstest/scripts/test_leauto_upgrades.sh @@ -64,10 +64,45 @@ iQIDAQAB -----END PUBLIC KEY----- " -if ! ./letsencrypt-auto -v --debug --version || ! diff letsencrypt-auto letsencrypt-auto-source/letsencrypt-auto ; then +if [ $(python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//') -eq 26 ]; then + if command -v python3; then + echo "Didn't expect Python 3 to be installed!" + exit 1 + fi + cp letsencrypt-auto cb-auto + if ! ./cb-auto -v --debug --version 2>&1 | grep 0.5.0 ; then + echo "Certbot shouldn't have updated to a new version!" + exit 1 + fi + if [ -d "/opt/eff.org" ]; then + echo "New directory shouldn't have been created!" + exit 1 + fi + # Create a 2nd venv at the new path to ensure we properly handle this case + export VENV_PATH="/opt/eff.org/certbot/venv" + if ! sudo -E ./letsencrypt-auto -v --debug --version --no-self-upgrade 2>&1 | grep 0.5.0 ; then + echo second installation appeared to fail + exit 1 + fi + unset VENV_PATH + EXPECTED_VERSION=$(grep -m1 LE_AUTO_VERSION certbot-auto | cut -d\" -f2) + if ! ./cb-auto -v --debug --version -n 2>&1 | grep "$EXPECTED_VERSION" ; then + echo "Certbot didn't upgrade as expected!" + exit 1 + fi + if ! command -v python3; then + echo "Python3 wasn't properly installed" + exit 1 + fi + if [ "$(/opt/eff.org/certbot/venv/bin/python -V 2>&1 | cut -d" " -f 2 | cut -d. -f1)" != 3 ]; then + echo "Python3 wasn't used in venv!" + exit 1 + fi +elif ! ./letsencrypt-auto -v --debug --version || ! diff letsencrypt-auto letsencrypt-auto-source/letsencrypt-auto ; then echo upgrade appeared to fail exit 1 fi + echo upgrade appeared to be successful if [ "$(tools/readlink.py ${XDG_DATA_HOME:-~/.local/share}/letsencrypt)" != "/opt/eff.org/certbot/venv" ]; then From 39472f88dead13782f2d84e65135cf11b96258bb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Jan 2018 13:26:31 -0800 Subject: [PATCH 61/67] reduce ipdb version (#5408) --- tools/dev_constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index afc362ff8..47440241d 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -21,7 +21,7 @@ futures==3.1.1 google-api-python-client==1.5 httplib2==0.10.3 imagesize==0.7.1 -ipdb==0.10.3 +ipdb==0.10.2 ipython==5.5.0 ipython-genutils==0.2.0 Jinja2==2.9.6 From 9e952081014b9545ce6ddb8b6ecc86a51bf94131 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Jan 2018 18:34:45 -0800 Subject: [PATCH 62/67] Factor out common challengeperformer logic (#5413) --- .../certbot_apache/tests/tls_sni_01_test.py | 4 +- .../certbot_nginx/tests/tls_sni_01_test.py | 2 +- certbot/plugins/common.py | 42 ++++++++++++---- certbot/plugins/common_test.py | 48 ++++++++++++------- 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py index 6c37c2ecc..42fb3021b 100644 --- a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py +++ b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py @@ -16,8 +16,8 @@ from six.moves import xrange # pylint: disable=redefined-builtin, import-error class TlsSniPerformTest(util.ApacheTest): """Test the ApacheTlsSni01 challenge.""" - auth_key = common_test.TLSSNI01Test.auth_key - achalls = common_test.TLSSNI01Test.achalls + auth_key = common_test.AUTH_KEY + achalls = common_test.ACHALLS def setUp(self): # pylint: disable=arguments-differ super(TlsSniPerformTest, self).setUp() diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 32a5ed7d2..61ee293fa 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -20,7 +20,7 @@ from certbot_nginx.tests import util class TlsSniPerformTest(util.NginxTest): """Test the NginxTlsSni01 challenge.""" - account_key = common_test.TLSSNI01Test.auth_key + account_key = common_test.AUTH_KEY achalls = [ achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index 002d2f225..c281534ca 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -315,23 +315,28 @@ class Addr(object): return result -class TLSSNI01(object): - """Abstract base for TLS-SNI-01 challenge performers""" +class ChallengePerformer(object): + """Abstract base for challenge performers. + + :ivar configurator: Authenticator and installer plugin + :ivar achalls: Annotated challenges + :vartype achalls: `list` of `.KeyAuthorizationAnnotatedChallenge` + :ivar indices: Holds the indices of challenges from a larger array + so the user of the class doesn't have to. + :vartype indices: `list` of `int` + + """ def __init__(self, configurator): self.configurator = configurator self.achalls = [] self.indices = [] - self.challenge_conf = os.path.join( - configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf") - # self.completed = 0 def add_chall(self, achall, idx=None): - """Add challenge to TLSSNI01 object to perform at once. + """Store challenge to be performed when perform() is called. :param .KeyAuthorizationAnnotatedChallenge achall: Annotated - TLSSNI01 challenge. - + challenge. :param int idx: index to challenge in a larger array """ @@ -339,6 +344,27 @@ class TLSSNI01(object): if idx is not None: self.indices.append(idx) + def perform(self): + """Perform all added challenges. + + :returns: challenge respones + :rtype: `list` of `acme.challenges.KeyAuthorizationChallengeResponse` + + + """ + raise NotImplementedError() + + +class TLSSNI01(ChallengePerformer): + # pylint: disable=abstract-method + """Abstract base for TLS-SNI-01 challenge performers""" + + def __init__(self, configurator): + super(TLSSNI01, self).__init__(configurator) + self.challenge_conf = os.path.join( + configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf") + # self.completed = 0 + def get_cert_path(self, achall): """Returns standardized name for challenge certificate. diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index 1a1ca7dcb..103a12499 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -18,6 +18,17 @@ from certbot import errors from certbot.tests import acme_util from certbot.tests import util as test_util +AUTH_KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) +ACHALLS = [ + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.TLSSNI01(token=b'token1'), "pending"), + domain="encryption-example.demo", account_key=AUTH_KEY), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.TLSSNI01(token=b'token2'), "pending"), + domain="certbot.demo", account_key=AUTH_KEY), +] class NamespaceFunctionsTest(unittest.TestCase): """Tests for certbot.plugins.common.*_namespace functions.""" @@ -261,21 +272,27 @@ class AddrTest(unittest.TestCase): self.assertEqual(set_c, set_d) +class ChallengePerformerTest(unittest.TestCase): + """Tests for certbot.plugins.common.ChallengePerformer.""" + + def setUp(self): + configurator = mock.MagicMock() + + from certbot.plugins.common import ChallengePerformer + self.performer = ChallengePerformer(configurator) + + def test_add_chall(self): + self.performer.add_chall(ACHALLS[0], 0) + self.assertEqual(1, len(self.performer.achalls)) + self.assertEqual([0], self.performer.indices) + + def test_perform(self): + self.assertRaises(NotImplementedError, self.performer.perform) + + class TLSSNI01Test(unittest.TestCase): """Tests for certbot.plugins.common.TLSSNI01.""" - auth_key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) - achalls = [ - achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.chall_to_challb( - challenges.TLSSNI01(token=b'token1'), "pending"), - domain="encryption-example.demo", account_key=auth_key), - achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.chall_to_challb( - challenges.TLSSNI01(token=b'token2'), "pending"), - domain="certbot.demo", account_key=auth_key), - ] - def setUp(self): self.tempdir = tempfile.mkdtemp() configurator = mock.MagicMock() @@ -288,11 +305,6 @@ class TLSSNI01Test(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) - def test_add_chall(self): - self.sni.add_chall(self.achalls[0], 0) - self.assertEqual(1, len(self.sni.achalls)) - self.assertEqual([0], self.sni.indices) - def test_setup_challenge_cert(self): # This is a helper function that can be used for handling # open context managers more elegantly. It avoids dealing with @@ -325,7 +337,7 @@ class TLSSNI01Test(unittest.TestCase): OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) def test_get_z_domain(self): - achall = self.achalls[0] + achall = ACHALLS[0] self.assertEqual(self.sni.get_z_domain(achall), achall.response(achall.account_key).z_domain.decode("utf-8")) From 2ba334a18202926e4e6b4f166a18457861bba031 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Jan 2018 20:14:56 -0800 Subject: [PATCH 63/67] Add basic HTTP01 support to Apache * Add a simple version of HTTP01 * remove cert from chall name * make directory work on 2.2 * cleanup challenges when finished * import shutil * fixup perform and cleanup tests * Add tests for http_01.py --- certbot-apache/certbot_apache/configurator.py | 36 ++++-- certbot-apache/certbot_apache/http_01.py | 94 +++++++++++++++ .../certbot_apache/tests/configurator_test.py | 70 +++++++---- .../certbot_apache/tests/http_01_test.py | 112 ++++++++++++++++++ .../certbot_apache/tests/tls_sni_01_test.py | 2 +- 5 files changed, 278 insertions(+), 36 deletions(-) create mode 100644 certbot-apache/certbot_apache/http_01.py create mode 100644 certbot-apache/certbot_apache/tests/http_01_test.py diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 5a33346ea..60441f30c 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -24,9 +24,10 @@ from certbot_apache import apache_util from certbot_apache import augeas_configurator from certbot_apache import constants from certbot_apache import display_ops -from certbot_apache import tls_sni_01 +from certbot_apache import http_01 from certbot_apache import obj from certbot_apache import parser +from certbot_apache import tls_sni_01 from collections import defaultdict @@ -163,6 +164,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "ensure-http-header": self._set_http_header, "staple-ocsp": self._enable_ocsp_stapling} + # This will be set during the perform function + self.http_doer = None + @property def mod_ssl_conf(self): """Full absolute path to SSL configuration file.""" @@ -1855,7 +1859,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.TLSSNI01] + return [challenges.TLSSNI01, challenges.HTTP01] def perform(self, achalls): """Perform the configuration related challenge. @@ -1867,16 +1871,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ self._chall_out.update(achalls) responses = [None] * len(achalls) - chall_doer = tls_sni_01.ApacheTlsSni01(self) + self.http_doer = http_01.ApacheHttp01(self) + sni_doer = tls_sni_01.ApacheTlsSni01(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. - chall_doer.add_chall(achall, i) + if isinstance(achall.chall, challenges.HTTP01): + self.http_doer.add_chall(achall, i) + else: # tls-sni-01 + sni_doer.add_chall(achall, i) - sni_response = chall_doer.perform() - if sni_response: + http_response = self.http_doer.perform() + sni_response = sni_doer.perform() + if http_response or sni_response: # Must reload in order to activate the challenges. # Handled here because we may be able to load up other challenge # types @@ -1886,14 +1895,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # of identifying when the new configuration is being used. time.sleep(3) - # 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(sni_response): - responses[chall_doer.indices[i]] = resp + self._update_responses(responses, http_response, self.http_doer) + self._update_responses(responses, sni_response, sni_doer) return responses + def _update_responses(self, responses, chall_response, chall_doer): + # 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(chall_response): + responses[chall_doer.indices[i]] = resp + def cleanup(self, achalls): """Revert all challenges.""" self._chall_out.difference_update(achalls) @@ -1903,6 +1916,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.revert_challenge_config() self.restart() self.parser.reset_modules() + self.http_doer.cleanup() 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.""" diff --git a/certbot-apache/certbot_apache/http_01.py b/certbot-apache/certbot_apache/http_01.py new file mode 100644 index 000000000..410e8e9a6 --- /dev/null +++ b/certbot-apache/certbot_apache/http_01.py @@ -0,0 +1,94 @@ +"""A class that performs HTTP-01 challenges for Apache""" +import logging +import os +import shutil +import tempfile + +from certbot.plugins import common + +logger = logging.getLogger(__name__) + +class ApacheHttp01(common.TLSSNI01): + """Class that performs HTPP-01 challenges within the Apache configurator.""" + + CONFIG_TEMPLATE24 = """\ +Alias /.well-known/acme-challenge {0} + + + Require all granted + + +""" + + CONFIG_TEMPLATE22 = """\ +Alias /.well-known/acme-challenge {0} + + + Order allow,deny + Allow from all + + +""" + + def __init__(self, *args, **kwargs): + super(ApacheHttp01, self).__init__(*args, **kwargs) + self.challenge_conf = os.path.join( + self.configurator.conf("challenge-location"), + "le_http_01_challenge.conf") + self.challenge_dir = None + + def perform(self): + """Perform all HTTP-01 challenges.""" + if not self.achalls: + return [] + # Save any changes to the configuration as a precaution + # About to make temporary changes to the config + self.configurator.save("Changes before challenge setup", True) + + responses = self._set_up_challenges() + self._mod_config() + # Save reversible changes + self.configurator.save("HTTP Challenge", True) + + return responses + + def cleanup(self): + """Cleanup the challenge directory.""" + shutil.rmtree(self.challenge_dir, ignore_errors=True) + self.challenge_dir = None + + def _mod_config(self): + self.configurator.parser.add_include( + self.configurator.parser.loc["default"], self.challenge_conf) + self.configurator.reverter.register_file_creation( + True, self.challenge_conf) + + if self.configurator.version < (2, 4): + config_template = self.CONFIG_TEMPLATE22 + else: + config_template = self.CONFIG_TEMPLATE24 + config_text = config_template.format(self.challenge_dir) + + logger.debug("writing a config file with text:\n %s", config_text) + with open(self.challenge_conf, "w") as new_conf: + new_conf.write(config_text) + + def _set_up_challenges(self): + self.challenge_dir = tempfile.mkdtemp() + os.chmod(self.challenge_dir, 0o755) + + responses = [] + for achall in self.achalls: + responses.append(self._set_up_challenge(achall)) + + return responses + + def _set_up_challenge(self, achall): + response, validation = achall.response_and_validation() + + name = os.path.join(self.challenge_dir, achall.chall.encode("token")) + with open(name, 'wb') as f: + f.write(validation.encode()) + os.chmod(name, 0o644) + + return response diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 4f85e1e3f..1620013c8 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -676,23 +676,33 @@ class MultipleVhostsTest(util.ApacheTest): self.config._add_name_vhost_if_necessary(self.vh_truth[0]) self.assertEqual(self.config.add_name_vhost.call_count, 2) + @mock.patch("certbot_apache.configurator.http_01.ApacheHttp01.perform") @mock.patch("certbot_apache.configurator.tls_sni_01.ApacheTlsSni01.perform") @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") - def test_perform(self, mock_restart, mock_perform): + def test_perform(self, mock_restart, mock_tls_perform, mock_http_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - account_key, achall1, achall2 = self.get_achalls() + account_key, achalls = self.get_key_and_achalls() - expected = [ - achall1.response(account_key), - achall2.response(account_key), - ] + all_expected = [] + http_expected = [] + tls_expected = [] + for achall in achalls: + response = achall.response(account_key) + if isinstance(achall.chall, challenges.HTTP01): + http_expected.append(response) + else: + tls_expected.append(response) + all_expected.append(response) - mock_perform.return_value = expected - responses = self.config.perform([achall1, achall2]) + mock_http_perform.return_value = http_expected + mock_tls_perform.return_value = tls_expected - self.assertEqual(mock_perform.call_count, 1) - self.assertEqual(responses, expected) + responses = self.config.perform(achalls) + + self.assertEqual(mock_http_perform.call_count, 1) + self.assertEqual(mock_tls_perform.call_count, 1) + self.assertEqual(responses, all_expected) self.assertEqual(mock_restart.call_count, 1) @@ -700,30 +710,38 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") def test_cleanup(self, mock_cfg, mock_restart): mock_cfg.return_value = "" - _, achall1, achall2 = self.get_achalls() + _, achalls = self.get_key_and_achalls() + self.config.http_doer = mock.MagicMock() - self.config._chall_out.add(achall1) # pylint: disable=protected-access - self.config._chall_out.add(achall2) # pylint: disable=protected-access + for achall in achalls: + self.config._chall_out.add(achall) # pylint: disable=protected-access - self.config.cleanup([achall1]) - self.assertFalse(mock_restart.called) - - self.config.cleanup([achall2]) - self.assertTrue(mock_restart.called) + for i, achall in enumerate(achalls): + self.config.cleanup([achall]) + if i == len(achalls) - 1: + self.assertTrue(mock_restart.called) + self.assertTrue(self.config.http_doer.cleanup.called) + else: + self.assertFalse(mock_restart.called) + self.assertFalse(self.config.http_doer.cleanup.called) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") def test_cleanup_no_errors(self, mock_cfg, mock_restart): mock_cfg.return_value = "" - _, achall1, achall2 = self.get_achalls() + _, achalls = self.get_key_and_achalls() + self.config.http_doer = mock.MagicMock() - self.config._chall_out.add(achall1) # pylint: disable=protected-access + for achall in achalls: + self.config._chall_out.add(achall) # pylint: disable=protected-access - self.config.cleanup([achall2]) + self.config.cleanup([achalls[-1]]) self.assertFalse(mock_restart.called) + self.assertFalse(self.config.http_doer.cleanup.called) - self.config.cleanup([achall1, achall2]) + self.config.cleanup(achalls) self.assertTrue(mock_restart.called) + self.assertTrue(self.config.http_doer.cleanup.called) @mock.patch("certbot.util.run_script") def test_get_version(self, mock_script): @@ -1151,7 +1169,7 @@ class MultipleVhostsTest(util.ApacheTest): not_rewriterule = "NotRewriteRule ^ ..." self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule)) - def get_achalls(self): + def get_key_and_achalls(self): """Return testing achallenges.""" account_key = self.rsa512jwk achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( @@ -1166,8 +1184,12 @@ class MultipleVhostsTest(util.ApacheTest): token=b"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"), "pending"), domain="certbot.demo", account_key=account_key) + achall3 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=(b'x' * 16)), "pending"), + domain="example.org", account_key=account_key) - return account_key, achall1, achall2 + return account_key, (achall1, achall2, achall3) def test_make_addrs_sni_ready(self): self.config.version = (2, 2) diff --git a/certbot-apache/certbot_apache/tests/http_01_test.py b/certbot-apache/certbot_apache/tests/http_01_test.py new file mode 100644 index 000000000..11121fae2 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/http_01_test.py @@ -0,0 +1,112 @@ +"""Test for certbot_apache.http_01.""" +import os +import unittest + +from acme import challenges + +from certbot import achallenges + +from certbot.tests import acme_util + +from certbot_apache.tests import util + + +NUM_ACHALLS = 3 + + +class ApacheHttp01TestMeta(type): + """Generates parmeterized tests for testing perform.""" + def __new__(mcs, name, bases, class_dict): + + def _gen_test(num_achalls, minor_version): + def _test(self): + achalls = self.achalls[:num_achalls] + self.config.version = (2, minor_version) + self.common_perform_test(achalls) + return _test + + for i in range(1, NUM_ACHALLS + 1): + for j in (2, 4): + test_name = "test_perform_{0}_{1}".format(i, j) + class_dict[test_name] = _gen_test(i, j) + return type.__new__(mcs, name, bases, class_dict) + + +class ApacheHttp01Test(util.ApacheTest): + """Test for certbot_apache.http_01.ApacheHttp01.""" + + __metaclass__ = ApacheHttp01TestMeta + + def setUp(self, *args, **kwargs): + super(ApacheHttp01Test, self).setUp(*args, **kwargs) + self.maxDiff = None + + self.account_key = self.rsa512jwk + self.achalls = [] + for i in range(NUM_ACHALLS): + self.achalls.append( + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=((chr(ord('a') + i) * 16))), + "pending"), + domain="example{0}.com".format(i), + account_key=self.account_key)) + + from certbot_apache.http_01 import ApacheHttp01 + self.http = ApacheHttp01(self.config) + + def test_empty_perform(self): + self.assertFalse(self.http.perform()) + + def common_perform_test(self, achalls): + """Tests perform with the given achalls.""" + for achall in achalls: + self.http.add_chall(achall) + + expected_response = [ + achall.response(self.account_key) for achall in achalls] + self.assertEqual(self.http.perform(), expected_response) + + self.assertTrue(os.path.isdir(self.http.challenge_dir)) + self._has_min_permissions(self.http.challenge_dir, 0o755) + self._test_challenge_conf() + + for achall in achalls: + self._test_challenge_file(achall) + + challenge_dir = self.http.challenge_dir + self.http.cleanup() + self.assertFalse(os.path.exists(challenge_dir)) + + def _test_challenge_conf(self): + self.assertEqual( + len(self.config.parser.find_dir( + "Include", self.http.challenge_conf)), 1) + + with open(self.http.challenge_conf) as f: + conf_contents = f.read() + + alias_fmt = "Alias /.well-known/acme-challenge {0}" + alias = alias_fmt.format(self.http.challenge_dir) + self.assertTrue(alias in conf_contents) + if self.config.version < (2, 4): + self.assertTrue("Allow from all" in conf_contents) + else: + self.assertTrue("Require all granted" in conf_contents) + + def _test_challenge_file(self, achall): + name = os.path.join(self.http.challenge_dir, achall.chall.encode("token")) + validation = achall.validation(self.account_key) + + self._has_min_permissions(name, 0o644) + with open(name, 'rb') as f: + self.assertEqual(f.read(), validation.encode()) + + def _has_min_permissions(self, path, min_mode): + """Tests the given file has at least the permissions in mode.""" + st_mode = os.stat(path).st_mode + self.assertEqual(st_mode, st_mode | min_mode) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py index 42fb3021b..8cea97f04 100644 --- a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py +++ b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py @@ -1,6 +1,6 @@ """Test for certbot_apache.tls_sni_01.""" -import unittest import shutil +import unittest import mock From fa97877cfb058a8a6eda0e86b079876230bcfb7d Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 11 Jan 2018 14:46:48 +0200 Subject: [PATCH 64/67] Make sure that Apache is listening on port 80 and has mod_alias * Ensure that mod_alias is enabled * Make sure we listen to port http01_port --- certbot-apache/certbot_apache/configurator.py | 62 +++++++++++++++---- certbot-apache/certbot_apache/http_01.py | 11 ++++ .../certbot_apache/tests/configurator_test.py | 38 +++++++++++- .../certbot_apache/tests/http_01_test.py | 11 ++++ certbot-apache/certbot_apache/tests/util.py | 1 + 5 files changed, 111 insertions(+), 12 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 60441f30c..3b91617e5 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -740,28 +740,40 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ - # If nonstandard port, add service definition for matching - if port != "443": + self.prepare_https_modules(temp) + self.ensure_listen(port, https=True) + + def ensure_listen(self, port, https=False): + """Make sure that Apache is listening on the port. Checks if the + Listen statement for the port already exists, and adds it to the + configuration if necessary. + + :param str port: Port number to check and add Listen for if not in + place already + :param bool https: If the port will be used for HTTPS + + """ + + # If HTTPS requested for nonstandard port, add service definition + if https and port != "443": port_service = "%s %s" % (port, "https") else: port_service = port - self.prepare_https_modules(temp) # Check for Listen # Note: This could be made to also look for ip:443 combo listens = [self.parser.get_arg(x).split()[0] for x in self.parser.find_dir("Listen")] - # In case no Listens are set (which really is a broken apache config) - if not listens: - listens = ["80"] - # Listen already in place if self._has_port_already(listens, port): return listen_dirs = set(listens) + if not listens: + listen_dirs.add(port_service) + for listen in listens: # For any listen statement, check if the machine also listens on # Port 443. If not, add such a listen statement. @@ -776,11 +788,39 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if "%s:%s" % (ip, port_service) not in listen_dirs and ( "%s:%s" % (ip, port_service) not in listen_dirs): listen_dirs.add("%s:%s" % (ip, port_service)) - self._add_listens(listen_dirs, listens, port) + if https: + self._add_listens_https(listen_dirs, listens, port) + else: + self._add_listens_http(listen_dirs, listens, port) - def _add_listens(self, listens, listens_orig, port): - """Helper method for prepare_server_https to figure out which new - listen statements need adding + def _add_listens_http(self, listens, listens_orig, port): + """Helper method for ensure_listen to figure out which new + listen statements need adding for listening HTTP on port + + :param set listens: Set of all needed Listen statements + :param list listens_orig: List of existing listen statements + :param string port: Port number we're adding + """ + + new_listens = listens.difference(listens_orig) + + if port in new_listens: + # We have wildcard, skip the rest + self.parser.add_dir(parser.get_aug_path(self.parser.loc["listen"]), + "Listen", port) + self.save_notes += "Added Listen %s directive to %s\n" % ( + port, self.parser.loc["listen"]) + else: + for listen in new_listens: + self.parser.add_dir(parser.get_aug_path( + self.parser.loc["listen"]), "Listen", listen.split(" ")) + self.save_notes += ("Added Listen %s directive to " + "%s\n") % (listen, + self.parser.loc["listen"]) + + def _add_listens_https(self, listens, listens_orig, port): + """Helper method for ensure_listen to figure out which new + listen statements need adding for listening HTTPS on port :param set listens: Set of all needed Listen statements :param list listens_orig: List of existing listen statements diff --git a/certbot-apache/certbot_apache/http_01.py b/certbot-apache/certbot_apache/http_01.py index 410e8e9a6..9c25b1f17 100644 --- a/certbot-apache/certbot_apache/http_01.py +++ b/certbot-apache/certbot_apache/http_01.py @@ -45,6 +45,10 @@ Alias /.well-known/acme-challenge {0} # About to make temporary changes to the config self.configurator.save("Changes before challenge setup", True) + self.configurator.ensure_listen(str( + self.configurator.config.http01_port)) + self.prepare_http01_modules() + responses = self._set_up_challenges() self._mod_config() # Save reversible changes @@ -57,6 +61,13 @@ Alias /.well-known/acme-challenge {0} shutil.rmtree(self.challenge_dir, ignore_errors=True) self.challenge_dir = None + def prepare_http01_modules(self): + """Make sure that we have the needed modules available for http01""" + + if self.configurator.conf("handle-modules"): + if "alias_module" not in self.configurator.parser.modules: + self.configurator.enable_mod("alias", temp=True) + def _mod_config(self): self.configurator.parser.add_include( self.configurator.parser.loc["default"], self.challenge_conf) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 1620013c8..ea3867061 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -424,6 +424,43 @@ class MultipleVhostsTest(util.ApacheTest): self.assertTrue(self.config.parser.find_dir( "NameVirtualHost", "*:80")) + def test_add_listen_80(self): + mock_find = mock.Mock() + mock_add_dir = mock.Mock() + mock_find.return_value = [] + self.config.parser.find_dir = mock_find + self.config.parser.add_dir = mock_add_dir + self.config.ensure_listen("80") + self.assertTrue(mock_add_dir.called) + self.assertTrue(mock_find.called) + self.assertEqual(mock_add_dir.call_args[0][1], "Listen") + self.assertEqual(mock_add_dir.call_args[0][2], "80") + + def test_add_listen_80_named(self): + mock_find = mock.Mock() + mock_find.return_value = ["test1", "test2", "test3"] + mock_get = mock.Mock() + mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"] + mock_add_dir = mock.Mock() + + self.config.parser.find_dir = mock_find + self.config.parser.get_arg = mock_get + self.config.parser.add_dir = mock_add_dir + + self.config.ensure_listen("80") + self.assertEqual(mock_add_dir.call_count, 0) + + # Reset return lists and inputs + mock_add_dir.reset_mock() + mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"] + + # Test + self.config.ensure_listen("8080") + self.assertEqual(mock_add_dir.call_count, 3) + self.assertTrue(mock_add_dir.called) + self.assertEqual(mock_add_dir.call_args[0][1], "Listen") + self.assertEqual(mock_add_dir.call_args[0][2], ['1.2.3.4:8080']) + def test_prepare_server_https(self): mock_enable = mock.Mock() self.config.enable_mod = mock_enable @@ -435,7 +472,6 @@ class MultipleVhostsTest(util.ApacheTest): # This will test the Add listen self.config.parser.find_dir = mock_find self.config.parser.add_dir_to_ifmodssl = mock_add_dir - self.config.prepare_server_https("443") # Changing the order these modules are enabled breaks the reverter self.assertEqual(mock_enable.call_args_list[0][0][0], "socache_shmcb") diff --git a/certbot-apache/certbot_apache/tests/http_01_test.py b/certbot-apache/certbot_apache/tests/http_01_test.py index 11121fae2..f52b9d0cc 100644 --- a/certbot-apache/certbot_apache/tests/http_01_test.py +++ b/certbot-apache/certbot_apache/tests/http_01_test.py @@ -1,4 +1,5 @@ """Test for certbot_apache.http_01.""" +import mock import os import unittest @@ -53,11 +54,21 @@ class ApacheHttp01Test(util.ApacheTest): account_key=self.account_key)) from certbot_apache.http_01 import ApacheHttp01 + self.config.parser.modules.add("mod_alias.c") + self.config.parser.modules.add("alias_module") self.http = ApacheHttp01(self.config) def test_empty_perform(self): self.assertFalse(self.http.perform()) + @mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod") + def test_add_alias_module(self, mock_enmod): + self.config.parser.modules.remove("alias_module") + self.config.parser.modules.remove("mod_alias.c") + self.http.prepare_http01_modules() + self.assertTrue(mock_enmod.called) + self.assertEqual(mock_enmod.call_args[0][0], "alias") + def common_perform_test(self, achalls): """Tests perform with the given achalls.""" for achall in achalls: diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index ca667465c..8f5162629 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -103,6 +103,7 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc apache_challenge_location=config_path, backup_dir=backups, config_dir=config_dir, + http01_port="80", temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) From f0f5defb6ff9fcc54db4bbc977950610b22cd155 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 11 Jan 2018 09:27:30 -0800 Subject: [PATCH 65/67] Address minor concerns with Apache HTTP-01 * enable other modules * change port type * remove maxDiff from test class * update port comment * add -f to a2dismod --- certbot-apache/certbot_apache/configurator.py | 2 +- certbot-apache/certbot_apache/http_01.py | 10 ++++- .../certbot_apache/override_debian.py | 2 +- .../certbot_apache/tests/http_01_test.py | 39 ++++++++++++++++--- certbot-apache/certbot_apache/tests/util.py | 2 +- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 3b91617e5..8d6995211 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -776,7 +776,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): for listen in listens: # For any listen statement, check if the machine also listens on - # Port 443. If not, add such a listen statement. + # the given port. If not, add such a listen statement. if len(listen.split(":")) == 1: # Its listening to all interfaces if port not in listen_dirs and port_service not in listen_dirs: diff --git a/certbot-apache/certbot_apache/http_01.py b/certbot-apache/certbot_apache/http_01.py index 9c25b1f17..804644975 100644 --- a/certbot-apache/certbot_apache/http_01.py +++ b/certbot-apache/certbot_apache/http_01.py @@ -65,8 +65,14 @@ Alias /.well-known/acme-challenge {0} """Make sure that we have the needed modules available for http01""" if self.configurator.conf("handle-modules"): - if "alias_module" not in self.configurator.parser.modules: - self.configurator.enable_mod("alias", temp=True) + needed_modules = ["alias"] + if self.configurator.version < (2, 4): + needed_modules.append("authz_host") + else: + needed_modules.append("authz_core") + for mod in needed_modules: + if mod + "_module" not in self.configurator.parser.modules: + self.configurator.enable_mod(mod, temp=True) def _mod_config(self): self.configurator.parser.add_include( diff --git a/certbot-apache/certbot_apache/override_debian.py b/certbot-apache/certbot_apache/override_debian.py index 6e2e34ba9..02dffc3f7 100644 --- a/certbot-apache/certbot_apache/override_debian.py +++ b/certbot-apache/certbot_apache/override_debian.py @@ -140,5 +140,5 @@ class DebianConfigurator(configurator.ApacheConfigurator): "a2dismod are configured correctly for certbot.") self.reverter.register_undo_command( - temp, [self.conf("dismod"), mod_name]) + temp, [self.conf("dismod"), "-f", mod_name]) util.run_script([self.conf("enmod"), mod_name]) diff --git a/certbot-apache/certbot_apache/tests/http_01_test.py b/certbot-apache/certbot_apache/tests/http_01_test.py index f52b9d0cc..4e2a5faff 100644 --- a/certbot-apache/certbot_apache/tests/http_01_test.py +++ b/certbot-apache/certbot_apache/tests/http_01_test.py @@ -40,7 +40,6 @@ class ApacheHttp01Test(util.ApacheTest): def setUp(self, *args, **kwargs): super(ApacheHttp01Test, self).setUp(*args, **kwargs) - self.maxDiff = None self.account_key = self.rsa512jwk self.achalls = [] @@ -53,21 +52,51 @@ class ApacheHttp01Test(util.ApacheTest): domain="example{0}.com".format(i), account_key=self.account_key)) + modules = ["alias", "authz_core", "authz_host"] + for mod in modules: + self.config.parser.modules.add("mod_{0}.c".format(mod)) + self.config.parser.modules.add(mod + "_module") + from certbot_apache.http_01 import ApacheHttp01 - self.config.parser.modules.add("mod_alias.c") - self.config.parser.modules.add("alias_module") self.http = ApacheHttp01(self.config) def test_empty_perform(self): self.assertFalse(self.http.perform()) @mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod") - def test_add_alias_module(self, mock_enmod): + def test_enable_modules_22(self, mock_enmod): + self.config.version = (2, 2) + self.config.parser.modules.remove("authz_host_module") + self.config.parser.modules.remove("mod_authz_host.c") + + enmod_calls = self.common_enable_modules_test(mock_enmod) + self.assertEqual(enmod_calls[0][0][0], "authz_host") + + @mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod") + def test_enable_modules_24(self, mock_enmod): + self.config.parser.modules.remove("authz_core_module") + self.config.parser.modules.remove("mod_authz_core.c") + + enmod_calls = self.common_enable_modules_test(mock_enmod) + self.assertEqual(enmod_calls[0][0][0], "authz_core") + + def common_enable_modules_test(self, mock_enmod): + """Tests enabling mod_alias and other modules.""" self.config.parser.modules.remove("alias_module") self.config.parser.modules.remove("mod_alias.c") + self.http.prepare_http01_modules() + self.assertTrue(mock_enmod.called) - self.assertEqual(mock_enmod.call_args[0][0], "alias") + calls = mock_enmod.call_args_list + other_calls = [] + for call in calls: + if "alias" != call[0][0]: + other_calls.append(call) + + # If these lists are equal, we never enabled mod_alias + self.assertNotEqual(calls, other_calls) + return other_calls def common_perform_test(self, achalls): """Tests perform with the given achalls.""" diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 8f5162629..1ba1e2c34 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -103,7 +103,7 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc apache_challenge_location=config_path, backup_dir=backups, config_dir=config_dir, - http01_port="80", + http01_port=80, temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) From 28dad825afae8a079811114d72a53a9833155d6c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 11 Jan 2018 20:44:40 +0200 Subject: [PATCH 66/67] Do not try to remove temp dir if it wasn't created --- certbot-apache/certbot_apache/http_01.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/http_01.py b/certbot-apache/certbot_apache/http_01.py index 804644975..87721d290 100644 --- a/certbot-apache/certbot_apache/http_01.py +++ b/certbot-apache/certbot_apache/http_01.py @@ -58,7 +58,8 @@ Alias /.well-known/acme-challenge {0} def cleanup(self): """Cleanup the challenge directory.""" - shutil.rmtree(self.challenge_dir, ignore_errors=True) + if self.challenge_dir: + shutil.rmtree(self.challenge_dir, ignore_errors=True) self.challenge_dir = None def prepare_http01_modules(self): From 2cb9d9e2aa332e4fd53aab32ad1d3946438a0dd3 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Thu, 11 Jan 2018 17:06:23 -0800 Subject: [PATCH 67/67] Implement HTTP-01 challenge for Nginx (#5414) * get http01 challenge working * support multiple challenge types in configurator.py * update existing nginx tests * lint * refactor NginxHttp01 and NginxTlsSni01 to both now inherit from NginxChallengePerformer * remove TODO * challenges_test tests with both tlssni01 and http01 * Make challenges.py more abstract to make lint happier * add pylint disables to the tests to make pylint happier about the inheritance and abstraction situation * no need to cover raise NotImplementedError() lines * python3 compatibility * test that http01 perform is called * only remove ssl from addresses during http01 * Initialize addrs_to_add * Change Nginx http01 to modify server block so the site doesn't stop serving while getting a cert * pass existing unit tests * rename sni --> http01 in unit tests * lint * fix configurator test * select an http block instead of https * properly test for port number * use domains that have matching addresses * remove debugger * remove access_log and error_log cruft that wasn't being executed * continue to return None from choose_redirect_vhost when create_if_no_match is False * add nginx integration test --- certbot-nginx/certbot_nginx/configurator.py | 35 ++++-- certbot-nginx/certbot_nginx/http_01.py | 106 ++++++++++++++++ certbot-nginx/certbot_nginx/parser.py | 4 +- .../certbot_nginx/tests/configurator_test.py | 14 ++- .../certbot_nginx/tests/http_01_test.py | 113 ++++++++++++++++++ certbot-nginx/certbot_nginx/tests/util.py | 1 + .../tests/boulder-integration.conf.sh | 6 +- certbot-nginx/tests/boulder-integration.sh | 19 ++- 8 files changed, 272 insertions(+), 26 deletions(-) create mode 100644 certbot-nginx/certbot_nginx/http_01.py create mode 100644 certbot-nginx/certbot_nginx/tests/http_01_test.py diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 8af474c5e..a77cf2bc3 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -26,6 +26,7 @@ from certbot_nginx import constants from certbot_nginx import nginxparser from certbot_nginx import parser from certbot_nginx import tls_sni_01 +from certbot_nginx import http_01 logger = logging.getLogger(__name__) @@ -208,7 +209,8 @@ class NginxConfigurator(common.Installer): :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 + when there is no match found. If we can't choose a default, raise a + MisconfigurationError. :returns: ssl vhost associated with name :rtype: :class:`~certbot_nginx.obj.VirtualHost` @@ -366,7 +368,7 @@ class NginxConfigurator(common.Installer): return sorted(matches, key=lambda x: x['rank']) - def choose_redirect_vhost(self, target_name, port): + def choose_redirect_vhost(self, target_name, port, create_if_no_match=False): """Chooses a single virtual host for redirect enhancement. Chooses the vhost most closely matching target_name that is @@ -380,12 +382,19 @@ class NginxConfigurator(common.Installer): :param str target_name: domain name :param str port: port number + :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: vhost associated with name :rtype: :class:`~certbot_nginx.obj.VirtualHost` """ matches = self._get_redirect_ranked_matches(target_name, port) - return self._select_best_name_match(matches) + vhost = self._select_best_name_match(matches) + if not vhost and create_if_no_match: + vhost = self._vhost_from_duplicated_default(target_name) + return vhost def _get_redirect_ranked_matches(self, target_name, port): """Gets a ranked list of plaintextish port-listening vhosts matching target_name @@ -394,7 +403,7 @@ class NginxConfigurator(common.Installer): Rank by how well these match target_name. :param str target_name: The name to match - :param str port: port number + :param str port: port number as a string :returns: list of dicts containing the vhost, the matching name, and the numerical rank :rtype: list @@ -840,7 +849,7 @@ class NginxConfigurator(common.Installer): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.TLSSNI01] + return [challenges.TLSSNI01, challenges.HTTP01] # Entry point in main.py for performing challenges def perform(self, achalls): @@ -853,15 +862,20 @@ class NginxConfigurator(common.Installer): """ self._chall_out += len(achalls) responses = [None] * len(achalls) - chall_doer = tls_sni_01.NginxTlsSni01(self) + sni_doer = tls_sni_01.NginxTlsSni01(self) + 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. - chall_doer.add_chall(achall, i) + if isinstance(achall.chall, challenges.HTTP01): + http_doer.add_chall(achall, i) + else: # tls-sni-01 + sni_doer.add_chall(achall, i) - sni_response = chall_doer.perform() + sni_response = sni_doer.perform() + 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() @@ -869,8 +883,9 @@ class NginxConfigurator(common.Installer): # 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(sni_response): - responses[chall_doer.indices[i]] = resp + for chall_response, chall_doer in ((sni_response, sni_doer), (http_response, http_doer)): + for i, resp in enumerate(chall_response): + responses[chall_doer.indices[i]] = resp return responses diff --git a/certbot-nginx/certbot_nginx/http_01.py b/certbot-nginx/certbot_nginx/http_01.py new file mode 100644 index 000000000..1f1e37891 --- /dev/null +++ b/certbot-nginx/certbot_nginx/http_01.py @@ -0,0 +1,106 @@ +"""A class that performs HTTP-01 challenges for Nginx""" + +import logging +import os + +from acme import challenges + +from certbot.plugins import common + + +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 http-01 Challenges, + TLS-SNI-01 Challenges belong in the response array. This is an + optional utility. + + """ + + 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 _add_bucket_directive(self): + """Modifies Nginx config to include server_names_hash_bucket_size directive.""" + 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) + break + + def _mod_config(self): + """Modifies Nginx config to handle challenges. + + """ + self._add_bucket_directive() + + for achall in self.achalls: + self._mod_server_block(achall) + + def _get_validation_path(self, achall): + return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, achall.chall.encode("token")) + + def _mod_server_block(self, achall): + """Modifies a server block to respond to a challenge. + + :param achall: Annotated HTTP-01 challenge + :type achall: + :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge` + + """ + vhost = self.configurator.choose_redirect_vhost(achall.domain, + '%i' % self.configurator.config.http01_port, create_if_no_match=True) + 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]]]] + + self.configurator.parser.add_server_directives(vhost, + location_directive, replace=False) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 9f13bc59f..5497f7e63 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -524,7 +524,7 @@ def _is_ssl_on_directive(entry): def _add_directives(directives, replace, block): """Adds or replaces directives in a config block. - When replace=False, it's an error to try and add a directive that already + When replace=False, it's an error to try and add a nonrepeatable directive that already exists in the config block with a conflicting value. When replace=True and a directive with the same name already exists in the @@ -545,7 +545,7 @@ def _add_directives(directives, replace, block): INCLUDE = 'include' -REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE]) +REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'location']) COMMENT = ' managed by Certbot' COMMENT_BLOCK = [' ', '#', COMMENT] diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index e708b159a..7475df40c 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -100,7 +100,7 @@ class NginxConfiguratorTest(util.NginxTest): errors.PluginError, self.config.enhance, 'myhost', 'unknown_enhancement') def test_get_chall_pref(self): - self.assertEqual([challenges.TLSSNI01], + self.assertEqual([challenges.TLSSNI01, challenges.HTTP01], self.config.get_chall_pref('myhost')) def test_save(self): @@ -291,9 +291,11 @@ class NginxConfiguratorTest(util.NginxTest): parsed_migration_conf[0]) @mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") + @mock.patch("certbot_nginx.configurator.http_01.NginxHttp01.perform") @mock.patch("certbot_nginx.configurator.NginxConfigurator.restart") @mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config") - def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_perform): + def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_http_perform, + mock_tls_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( @@ -304,7 +306,7 @@ class NginxConfiguratorTest(util.NginxTest): ), domain="localhost", account_key=self.rsa512jwk) achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.TLSSNI01(token=b"m8TdO1qik4JVFtgPPurJmg"), + chall=challenges.HTTP01(token=b"m8TdO1qik4JVFtgPPurJmg"), uri="https://ca.org/chall1_uri", status=messages.Status("pending"), ), domain="example.com", account_key=self.rsa512jwk) @@ -314,10 +316,12 @@ class NginxConfiguratorTest(util.NginxTest): achall2.response(self.rsa512jwk), ] - mock_perform.return_value = expected + mock_tls_perform.return_value = expected[:1] + mock_http_perform.return_value = expected[1:] responses = self.config.perform([achall1, achall2]) - self.assertEqual(mock_perform.call_count, 1) + self.assertEqual(mock_tls_perform.call_count, 1) + self.assertEqual(mock_http_perform.call_count, 1) self.assertEqual(responses, expected) self.config.cleanup([achall1, achall2]) diff --git a/certbot-nginx/certbot_nginx/tests/http_01_test.py b/certbot-nginx/certbot_nginx/tests/http_01_test.py new file mode 100644 index 000000000..0f764e92e --- /dev/null +++ b/certbot-nginx/certbot_nginx/tests/http_01_test.py @@ -0,0 +1,113 @@ +"""Tests for certbot_nginx.http_01""" +import unittest +import shutil + +import mock +import six + +from acme import challenges + +from certbot import achallenges + +from certbot.plugins import common_test +from certbot.tests import acme_util + +from certbot_nginx.tests import util + + +class HttpPerformTest(util.NginxTest): + """Test the NginxHttp01 challenge.""" + + account_key = common_test.AUTH_KEY + achalls = [ + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01(token=b"kNdwjwOeX0I_A8DXt9Msmg"), "pending"), + domain="www.example.com", account_key=account_key), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.HTTP01( + token=b"\xba\xa9\xda?