From 6c53f5d8ed2875a8ef17906d44db476bdbc580db Mon Sep 17 00:00:00 2001 From: sydneyli Date: Fri, 14 Jun 2019 13:44:50 -0700 Subject: [PATCH 1/3] Turn off session tickets for versions of Nginx that support it (#7092) * Turn off session tickets for versions of Nginx that support it In line with Mozilla's security recommendations. * Changelog. * Set version before installing config files * lint: remove unused import * windows testfix * another windows testfix? * Testing path of updating src file with old nginx * Fix windows, and make config update tests fail if update doesn't happen --- CHANGELOG.md | 2 +- certbot-nginx/MANIFEST.in | 1 + certbot-nginx/certbot_nginx/configurator.py | 29 ++++++++---- certbot-nginx/certbot_nginx/constants.py | 13 ++---- .../certbot_nginx/options-ssl-nginx-old.conf | 13 ++++++ .../certbot_nginx/options-ssl-nginx.conf | 1 + .../certbot_nginx/tests/configurator_test.py | 46 +++++++++++++++---- 7 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 certbot-nginx/certbot_nginx/options-ssl-nginx-old.conf diff --git a/CHANGELOG.md b/CHANGELOG.md index 9919910f7..6e76970b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Added -* +* Turn off session tickets for nginx plugin by default ### Changed diff --git a/certbot-nginx/MANIFEST.in b/certbot-nginx/MANIFEST.in index 2daca6738..8707f9443 100644 --- a/certbot-nginx/MANIFEST.in +++ b/certbot-nginx/MANIFEST.in @@ -3,3 +3,4 @@ include README.rst recursive-include docs * recursive-include certbot_nginx/tests/testdata * include certbot_nginx/options-ssl-nginx.conf +include certbot_nginx/options-ssl-nginx-old.conf diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 6da00e513..e078ad4cb 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -6,6 +6,8 @@ import subprocess import tempfile import time +import pkg_resources + import OpenSSL import zope.interface @@ -120,6 +122,14 @@ class NginxConfigurator(common.Installer): self.reverter.recovery_routine() + @property + def mod_ssl_conf_src(self): + """Full absolute path to SSL configuration file source.""" + config_filename = "options-ssl-nginx.conf" + if self.version < (1, 5, 9): + config_filename = "options-ssl-nginx-old.conf" + return pkg_resources.resource_filename("certbot_nginx", config_filename) + @property def mod_ssl_conf(self): """Full absolute path to SSL configuration file.""" @@ -130,6 +140,11 @@ class NginxConfigurator(common.Installer): """Full absolute path to digest of updated SSL configuration file.""" return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST) + def install_ssl_options_conf(self, options_ssl, options_ssl_digest): + """Copy Certbot's SSL options file into the system's config dir if required.""" + return common.install_version_controlled_file(options_ssl, options_ssl_digest, + self.mod_ssl_conf_src, constants.ALL_SSL_OPTIONS_HASHES) + # This is called in determine_authenticator and determine_installer def prepare(self): """Prepare the authenticator/installer. @@ -148,14 +163,14 @@ class NginxConfigurator(common.Installer): self.parser = parser.NginxParser(self.conf('server-root')) - install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest) - - self.install_ssl_dhparams() - # Set Version if self.version is None: self.version = self.get_version() + self.install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest) + + self.install_ssl_dhparams() + # Prevent two Nginx plugins from modifying a config at once try: util.lock_dir_until_exit(self.conf('server-root')) @@ -1131,12 +1146,6 @@ def nginx_restart(nginx_ctl, nginx_conf): time.sleep(1) -def install_ssl_options_conf(options_ssl, options_ssl_digest): - """Copy Certbot's SSL options file into the system's config dir if required.""" - return common.install_version_controlled_file(options_ssl, options_ssl_digest, - constants.MOD_SSL_CONF_SRC, constants.ALL_SSL_OPTIONS_HASHES) - - def _determine_default_server_root(): if os.environ.get("CERTBOT_DOCS") == "1": default_server_root = "%s or %s" % (constants.LINUX_SERVER_ROOT, diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py index 41716db0f..cec7acaf5 100644 --- a/certbot-nginx/certbot_nginx/constants.py +++ b/certbot-nginx/certbot_nginx/constants.py @@ -1,8 +1,6 @@ """nginx plugin constants.""" import platform -import pkg_resources - FREEBSD_DARWIN_SERVER_ROOT = "/usr/local/etc/nginx" LINUX_SERVER_ROOT = "/etc/nginx" @@ -21,14 +19,13 @@ CLI_DEFAULTS = dict( MOD_SSL_CONF_DEST = "options-ssl-nginx.conf" """Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" -MOD_SSL_CONF_SRC = pkg_resources.resource_filename( - "certbot_nginx", "options-ssl-nginx.conf") -"""Path to the nginx mod_ssl config file found in the Certbot -distribution.""" - UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-nginx-conf-digest.txt" """Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`.""" +SSL_OPTIONS_HASHES_NEW = [ + '63e2bddebb174a05c9d8a7cf2adf72f7af04349ba59a1a925fe447f73b2f1abf', +] +"""SHA256 hashes of the contents of versions of MOD_SSL_CONF_SRC for nginx >= 1.5.9""" ALL_SSL_OPTIONS_HASHES = [ '0f81093a1465e3d4eaa8b0c14e77b2a2e93568b0fc1351c2b87893a95f0de87c', @@ -37,7 +34,7 @@ ALL_SSL_OPTIONS_HASHES = [ '7f95624dd95cf5afc708b9f967ee83a24b8025dc7c8d9df2b556bbc64256b3ff', '394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d', '4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16', -] +] + SSL_OPTIONS_HASHES_NEW """SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC""" def os_constant(key): diff --git a/certbot-nginx/certbot_nginx/options-ssl-nginx-old.conf b/certbot-nginx/certbot_nginx/options-ssl-nginx-old.conf new file mode 100644 index 000000000..292d42984 --- /dev/null +++ b/certbot-nginx/certbot_nginx/options-ssl-nginx-old.conf @@ -0,0 +1,13 @@ +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. + +ssl_session_cache shared:le_nginx_SSL:1m; +ssl_session_timeout 1440m; + +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_prefer_server_ciphers on; + +ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"; diff --git a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf index 292d42984..57a332d2f 100644 --- a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf +++ b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf @@ -6,6 +6,7 @@ ssl_session_cache shared:le_nginx_SSL:1m; ssl_session_timeout 1440m; +ssl_session_tickets off; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 6c3f4f0cf..5e9d61a44 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -13,7 +13,6 @@ from certbot import errors from certbot.compat import os from certbot.tests import util as certbot_test_util -from certbot_nginx import constants from certbot_nginx import obj from certbot_nginx import parser from certbot_nginx.configurator import _redirect_block_for_domain @@ -883,12 +882,11 @@ class InstallSslOptionsConfTest(util.NginxTest): self.config_path, self.config_dir, self.work_dir, self.logs_dir) def _call(self): - from certbot_nginx.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): - from certbot_nginx.constants import MOD_SSL_CONF_SRC - return crypto_util.sha256sum(MOD_SSL_CONF_SRC) + return crypto_util.sha256sum(self.config.mod_ssl_conf_src) def _assert_current_file(self): self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) @@ -908,13 +906,33 @@ class InstallSslOptionsConfTest(util.NginxTest): self._call() self._assert_current_file() + def _mock_hash_except_ssl_conf_src(self, fake_hash): + # Write a bad file in place so that update tests fail if no update occurs. + # We're going to pretend this file (the currently installed conf file) + # actually hashes to `fake_hash` for the update tests. + with open(self.config.mod_ssl_conf, "w") as f: + f.write("bogus") + sha256 = crypto_util.sha256sum + def _hash(filename): + return sha256(filename) if filename == self.config.mod_ssl_conf_src else fake_hash + return _hash + def test_prev_file_updates_to_current(self): from certbot_nginx.constants import ALL_SSL_OPTIONS_HASHES - with mock.patch('certbot.crypto_util.sha256sum') as mock_sha256: - mock_sha256.return_value = ALL_SSL_OPTIONS_HASHES[0] + with mock.patch('certbot.crypto_util.sha256sum', + new=self._mock_hash_except_ssl_conf_src(ALL_SSL_OPTIONS_HASHES[0])): self._call() self._assert_current_file() + def test_prev_file_updates_to_current_old_nginx(self): + from certbot_nginx.constants import ALL_SSL_OPTIONS_HASHES, SSL_OPTIONS_HASHES_NEW + self.config.version = (1, 5, 8) + with mock.patch('certbot.crypto_util.sha256sum', + new=self._mock_hash_except_ssl_conf_src(ALL_SSL_OPTIONS_HASHES[0])): + self._call() + self._assert_current_file() + self.assertTrue(self._current_ssl_options_hash() not in SSL_OPTIONS_HASHES_NEW) + def test_manually_modified_current_file_does_not_update(self): with open(self.config.mod_ssl_conf, "a") as mod_ssl_conf: mod_ssl_conf.write("a new line for the wrong hash\n") @@ -922,7 +940,7 @@ class InstallSslOptionsConfTest(util.NginxTest): self._call() self.assertFalse(mock_logger.warning.called) self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) - self.assertEqual(crypto_util.sha256sum(constants.MOD_SSL_CONF_SRC), + self.assertEqual(crypto_util.sha256sum(self.config.mod_ssl_conf_src), self._current_ssl_options_hash()) self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf), self._current_ssl_options_hash()) @@ -937,7 +955,7 @@ class InstallSslOptionsConfTest(util.NginxTest): 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.MOD_SSL_CONF_SRC), + self.assertEqual(crypto_util.sha256sum(self.config.mod_ssl_conf_src), self._current_ssl_options_hash()) # only print warning once with mock.patch("certbot.plugins.common.logger") as mock_logger: @@ -950,6 +968,16 @@ class InstallSslOptionsConfTest(util.NginxTest): "Constants.ALL_SSL_OPTIONS_HASHES must be appended" " with the sha256 hash of self.config.mod_ssl_conf when it is updated.") + def test_old_nginx_version_uses_old_config(self): + self.config.version = (1, 5, 8) + self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src), + "options-ssl-nginx-old.conf") + self._call() + self._assert_current_file() + self.config.version = (1, 5, 9) + self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src), + "options-ssl-nginx.conf") + class DetermineDefaultServerRootTest(certbot_test_util.ConfigTestCase): """Tests for certbot_nginx.configurator._determine_default_server_root.""" From 20ca47dec6111b55fa1f59d1b17fc3b242469f06 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 14 Jun 2019 15:51:15 -0700 Subject: [PATCH 2/3] Bump stale threshold to 1 year. (#7149) While I expect stale bot will close out 150 - 250 issues, that'll still leave us with 400+ open issues. My concern is that with a threshold of 6 months, most of these 400 issues will be in the same state 6 months from now and stale bot will annoy people by asking them if their issue is still valid too frequently. Doubling the stale threshold to 1 year should mitigate this problem a bit I think. --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 6b317a4b5..9c095ebd6 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Configuration for https://github.com/marketplace/stale # Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 180 +daysUntilStale: 365 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. From 1df778859b7ace699c02039b269abd426058a237 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sat, 15 Jun 2019 01:28:14 +0200 Subject: [PATCH 3/3] [Windows|Linux] Use builtin Python proxy capabilities for Certbot-CI (#7156) This PR is a part of the actions necessary to make Certbot-CI work on Windows, in order to execute the integration tests on this platform. I initially used the fully-fledged HTTP proxy [Traefik](https://docs.traefik.io/) to distribute HTTP challenges among several pytest nodes, and so parallelize the integration tests. Traefik for this purpose is overkill. We just want to redirect the ACME server to a pytest node depending on the `Host` header, and we use here a production-grade HTTP proxy for that. However it was not a problem on Linux, as soon as you can have Docker, because this instance is deployed through it. But this becomes a problem for Windows, where Docker is not available everywhere, very compelling on its setup, and limited by the implemented network drivers. See my comments here https://github.com/letsencrypt/pebble/pull/240 for more details. Hopefully Python ships with everything needed to implement a simple HTTP proxy, with strictly what we need for the parallelization of integration tests. This PR implements this kind of HTTP proxy, and remove the coupling to Traefik. This PR has been tested successfully with integration tests on Pebble under Linux for Python 2.x and Python 3.x, and the proxy alone has been also tested successfully on Windows (no integration tests can be run for now on this platform). * Create a python proxy * Refactor proxy config * Working logic * Resolve from the path * Give proxy process to the ACMEServer context manager --- .../utils/acme_server.py | 107 ++++++------------ .../utils/constants.py | 1 - .../certbot_integration_tests/utils/proxy.py | 36 ++++++ 3 files changed, 71 insertions(+), 73 deletions(-) create mode 100644 certbot-ci/certbot_integration_tests/utils/proxy.py diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index 930755fc7..f8f4b2c69 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """Module to setup an ACME CA server environment able to run multiple tests in parallel""" from __future__ import print_function +import json import tempfile import time import os @@ -10,10 +11,9 @@ import sys from os.path import join import requests -import json import yaml -from certbot_integration_tests.utils import misc +from certbot_integration_tests.utils import misc, proxy from certbot_integration_tests.utils.constants import * @@ -23,13 +23,20 @@ class ACMEServer(object): (eg. challenges ports). ACMEServer is also a context manager, and so can be used to ensure ACME server is started/stopped upon context enter/exit. """ - def __init__(self, acme_xdist, start, stop): + def __init__(self, acme_xdist, start, server_cleanup): + self._proxy_process = None + self._server_cleanup = server_cleanup self.acme_xdist = acme_xdist self.start = start - self.stop = stop + + def stop(self): + if self._proxy_process: + self._proxy_process.terminate() + self._proxy_process.wait() + self._server_cleanup() def __enter__(self): - self.start() + self._proxy_process = self.start() return self.acme_xdist def __exit__(self, exc_type, exc_val, exc_tb): @@ -46,20 +53,21 @@ def setup_acme_server(acme_server, nodes, proxy=True): respectively start the server, and stop it with proper resources cleanup. :param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble) :param str[] nodes: list of node names that will be setup by pytest xdist - :param bool proxy: set to False to not start the Traefik proxy + :param bool proxy: set to False to not start the HTTP proxy :return: a properly configured ACMEServer instance :rtype: ACMEServer """ acme_type = 'pebble' if acme_server == 'pebble' else 'boulder' acme_xdist = _construct_acme_xdist(acme_server, nodes) - workspace, stop = _construct_workspace(acme_type) + workspace, server_cleanup = _construct_workspace(acme_type) def start(): - if proxy: - _prepare_traefik_proxy(workspace, acme_xdist) + proxy_process = _prepare_http_proxy(acme_xdist) if proxy else None _prepare_acme_server(workspace, acme_type, acme_xdist) - return ACMEServer(acme_xdist, start, stop) + return proxy_process + + return ACMEServer(acme_xdist, start, server_cleanup) def _construct_acme_xdist(acme_server, nodes): @@ -89,15 +97,14 @@ def _construct_workspace(acme_type): def cleanup(): """Cleanup function to call that will teardown relevant dockers and their configuration.""" - for instance in [acme_type, 'traefik']: - print('=> Tear down the {0} instance...'.format(instance)) - instance_path = join(workspace, instance) - try: - if os.path.isfile(join(instance_path, 'docker-compose.yml')): - _launch_command(['docker-compose', 'down'], cwd=instance_path) - except subprocess.CalledProcessError: - pass - print('=> Finished tear down of {0} instance.'.format(acme_type)) + print('=> Tear down the {0} instance...'.format(acme_type)) + instance_path = join(workspace, acme_type) + try: + if os.path.isfile(join(instance_path, 'docker-compose.yml')): + _launch_command(['docker-compose', 'down'], cwd=instance_path) + except subprocess.CalledProcessError: + pass + print('=> Finished tear down of {0} instance.'.format(acme_type)) if acme_type == 'boulder' and os.path.exists(os.path.join(workspace, 'boulder')): # Boulder docker generates build artifacts owned by root user with 0o744 permissions. @@ -155,60 +162,16 @@ def _prepare_acme_server(workspace, acme_type, acme_xdist): raise -def _prepare_traefik_proxy(workspace, acme_xdist): - """Configure and launch Traefik, the HTTP reverse proxy""" - print('=> Starting traefik instance deployment...') - instance_path = join(workspace, 'traefik') - traefik_subnet = '10.33.33' - try: - os.mkdir(instance_path) +def _prepare_http_proxy(acme_xdist): + """Configure and launch an HTTP proxy""" + print('=> Configuring the HTTP proxy...') + mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port) + for node, port in acme_xdist['http_port'].items()} + command = [sys.executable, proxy.__file__, str(HTTP_01_PORT), json.dumps(mapping)] + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + print('=> Finished configuring the HTTP proxy.') - with open(join(instance_path, 'docker-compose.yml'), 'w') as file_h: - file_h.write('''\ -version: '3' -services: - traefik: - image: traefik - command: --api --rest - ports: - - {http_01_port}:80 - - {traefik_api_port}:8080 - networks: - traefiknet: - ipv4_address: {traefik_subnet}.2 -networks: - traefiknet: - ipam: - config: - - subnet: {traefik_subnet}.0/24 -'''.format(traefik_subnet=traefik_subnet, - traefik_api_port=TRAEFIK_API_PORT, - http_01_port=HTTP_01_PORT)) - - _launch_command(['docker-compose', 'up', '--force-recreate', '-d'], cwd=instance_path) - - misc.check_until_timeout('http://localhost:{0}/api'.format(TRAEFIK_API_PORT)) - config = { - 'backends': { - node: { - 'servers': {node: {'url': 'http://{0}.1:{1}'.format(traefik_subnet, port)}} - } for node, port in acme_xdist['http_port'].items() - }, - 'frontends': { - node: { - 'backend': node, 'passHostHeader': True, - 'routes': {node: {'rule': 'HostRegexp: {{subdomain:.+}}.{0}.wtf'.format(node)}} - } for node in acme_xdist['http_port'].keys() - } - } - response = requests.put('http://localhost:{0}/api/providers/rest'.format(TRAEFIK_API_PORT), - data=json.dumps(config)) - response.raise_for_status() - - print('=> Finished traefik instance deployment.') - except BaseException: - print('Error while setting up traefik instance.') - raise + return process def _launch_command(command, cwd=os.getcwd()): diff --git a/certbot-ci/certbot_integration_tests/utils/constants.py b/certbot-ci/certbot_integration_tests/utils/constants.py index 5cf6fd776..9266e25ea 100644 --- a/certbot-ci/certbot_integration_tests/utils/constants.py +++ b/certbot-ci/certbot_integration_tests/utils/constants.py @@ -2,7 +2,6 @@ HTTP_01_PORT = 5002 TLS_ALPN_01_PORT = 5001 CHALLTESTSRV_PORT = 8055 -TRAEFIK_API_PORT = 8056 BOULDER_V1_DIRECTORY_URL = 'http://localhost:4000/directory' BOULDER_V2_DIRECTORY_URL = 'http://localhost:4001/directory' PEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir' diff --git a/certbot-ci/certbot_integration_tests/utils/proxy.py b/certbot-ci/certbot_integration_tests/utils/proxy.py new file mode 100644 index 000000000..69248c771 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/utils/proxy.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +import json +import sys +import re + +import requests +from six.moves import BaseHTTPServer + +from certbot_integration_tests.utils.misc import GracefulTCPServer + + +def _create_proxy(mapping): + class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def do_GET(self): + headers = {key.lower(): value for key, value in self.headers.items()} + backend = [backend for pattern, backend in mapping.items() + if re.match(pattern, headers['host'])][0] + response = requests.get(backend + self.path, headers=headers) + + self.send_response(response.status_code) + for key, value in response.headers.items(): + self.send_header(key, value) + self.end_headers() + self.wfile.write(response.content) + + return ProxyHandler + + +if __name__ == '__main__': + http_port = int(sys.argv[1]) + port_mapping = json.loads(sys.argv[2]) + httpd = GracefulTCPServer(('', http_port), _create_proxy(port_mapping)) + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass