From 0f183cf2e8fdccccd91f65baeb0cf8eefda6adbf Mon Sep 17 00:00:00 2001 From: TimSimmons Date: Fri, 28 Aug 2015 16:39:10 -0500 Subject: [PATCH] Correct generated links when behind an SSL terminating proxy Adds SSLMiddleware that checks a configurable header to set the correct url_scheme used to generate links when host headers are in use. Change-Id: Id864bb53d175c868fdee58c04fd2ea27ee188e08 Closes-Bug: #1490034 --- designate/api/middleware.py | 33 ++++++++++++++++ .../tests/unit/test_api/test_middleware.py | 39 +++++++++++++++++++ etc/designate/api-paste.ini | 20 ++++++---- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/designate/api/middleware.py b/designate/api/middleware.py index e212becd..4afda103 100644 --- a/designate/api/middleware.py +++ b/designate/api/middleware.py @@ -41,6 +41,16 @@ cfg.CONF.register_opts([ help='Enable API Maintenance Mode'), cfg.StrOpt('maintenance-mode-role', default='admin', help='Role allowed to bypass maintaince mode'), + cfg.StrOpt('secure-proxy-ssl-header', + default='X-Forwarded-Proto', + help="The HTTP Header that will be used to determine which " + "the original request protocol scheme was, even if it was " + "removed by an SSL terminating proxy."), + cfg.StrOpt('override-proto', + default=None, + help="A scheme that will be used to override " + "the request protocol scheme, even if it was " + "set by an SSL terminating proxy.") ], group='service:api') @@ -355,3 +365,26 @@ class APIv2ValidationErrorMiddleware(ValidationErrorMiddleware): def __init__(self, application): super(APIv2ValidationErrorMiddleware, self).__init__(application) self.api_version = 'API_v2' + + +class SSLMiddleware(base.Middleware): + """A middleware that replaces the request wsgi.url_scheme environment + variable with the value of HTTP header configured in + secure_proxy_ssl_header if exists in the incoming request. + This is useful if the server is behind a SSL termination proxy. + + Code nabbed from Heat. + """ + def __init__(self, application): + super(SSLMiddleware, self).__init__(application) + LOG.info(_LI('Starting designate ssl middleware')) + self.secure_proxy_ssl_header = 'HTTP_{0}'.format( + cfg.CONF['service:api'].secure_proxy_ssl_header.upper(). + replace('-', '_')) + self.override = cfg.CONF['service:api'].override_proto + + def process_request(self, request): + request.environ['wsgi.url_scheme'] = request.environ.get( + self.secure_proxy_ssl_header, request.environ['wsgi.url_scheme']) + if self.override: + request.environ['wsgi.url_scheme'] = self.override diff --git a/designate/tests/unit/test_api/test_middleware.py b/designate/tests/unit/test_api/test_middleware.py index ecd095bb..93e5b025 100644 --- a/designate/tests/unit/test_api/test_middleware.py +++ b/designate/tests/unit/test_api/test_middleware.py @@ -143,3 +143,42 @@ class KeystoneContextMiddlewareTest(oslotest.base.BaseTestCase): self.app(self.request) self.assertFalse(self.ctxt.edit_managed_records) + + +class SSLMiddlewareTest(oslotest.base.BaseTestCase): + def setUp(self): + super(SSLMiddlewareTest, self).setUp() + self.app = middleware.SSLMiddleware({}) + + self.request = FakeRequest() + + def test_bogus_header(self): + self.request.environ['wsgi.url_scheme'] = 'http' + # If someone sends something bogus, it will infect their self links + self.request.environ['HTTP_X_FORWARDED_PROTO'] = 'poo' + self.app(self.request) + + self.assertEqual(self.request.environ['wsgi.url_scheme'], 'poo') + + def test_http_header(self): + self.request.environ['wsgi.url_scheme'] = '' + self.request.environ['HTTP_X_FORWARDED_PROTO'] = 'http' + self.app(self.request) + + self.assertEqual(self.request.environ['wsgi.url_scheme'], 'http') + + def test_https_header(self): + self.request.environ['wsgi.url_scheme'] = 'http' + self.request.environ['HTTP_X_FORWARDED_PROTO'] = 'https' + self.app(self.request) + + self.assertEqual(self.request.environ['wsgi.url_scheme'], 'https') + + def test_override_proto(self): + self.request.environ['wsgi.url_scheme'] = 'http' + self.request.environ['HTTP_X_FORWARDED_PROTO'] = 'https' + self.app.override = 'poo' + + self.app(self.request) + + self.assertEqual(self.request.environ['wsgi.url_scheme'], 'poo') diff --git a/etc/designate/api-paste.ini b/etc/designate/api-paste.ini index e8f600d5..0888d00a 100644 --- a/etc/designate/api-paste.ini +++ b/etc/designate/api-paste.ini @@ -1,33 +1,36 @@ [composite:osapi_dns] use = egg:Paste#urlmap -/: osapi_dns_app_versions +/: osapi_dns_versions /v1: osapi_dns_v1 /v2: osapi_dns_v2 /admin: osapi_dns_admin +[pipeline:osapi_dns_versions] +pipeline = maintenance faultwrapper ssl osapi_dns_app_versions + [app:osapi_dns_app_versions] paste.app_factory = designate.api.versions:factory [composite:osapi_dns_v1] use = call:designate.api.middleware:auth_pipeline_factory -noauth = request_id noauthcontext maintenance validation_API_v1 faultwrapper normalizeuri osapi_dns_app_v1 -keystone = request_id authtoken keystonecontext maintenance validation_API_v1 faultwrapper normalizeuri osapi_dns_app_v1 +noauth = request_id noauthcontext maintenance validation_API_v1 faultwrapper ssl normalizeuri osapi_dns_app_v1 +keystone = request_id authtoken keystonecontext maintenance validation_API_v1 faultwrapper ssl normalizeuri osapi_dns_app_v1 [app:osapi_dns_app_v1] paste.app_factory = designate.api.v1:factory [composite:osapi_dns_v2] use = call:designate.api.middleware:auth_pipeline_factory -noauth = request_id faultwrapper validation_API_v2 noauthcontext maintenance normalizeuri osapi_dns_app_v2 -keystone = request_id faultwrapper validation_API_v2 authtoken keystonecontext maintenance normalizeuri osapi_dns_app_v2 +noauth = request_id faultwrapper ssl validation_API_v2 noauthcontext maintenance normalizeuri osapi_dns_app_v2 +keystone = request_id faultwrapper ssl validation_API_v2 authtoken keystonecontext maintenance normalizeuri osapi_dns_app_v2 [app:osapi_dns_app_v2] paste.app_factory = designate.api.v2:factory [composite:osapi_dns_admin] use = call:designate.api.middleware:auth_pipeline_factory -noauth = request_id faultwrapper noauthcontext maintenance normalizeuri osapi_dns_app_admin -keystone = request_id faultwrapper authtoken keystonecontext maintenance normalizeuri osapi_dns_app_admin +noauth = request_id faultwrapper ssl noauthcontext maintenance normalizeuri osapi_dns_app_admin +keystone = request_id faultwrapper ssl authtoken keystonecontext maintenance normalizeuri osapi_dns_app_admin [app:osapi_dns_app_admin] paste.app_factory = designate.api.admin:factory @@ -58,3 +61,6 @@ paste.filter_factory = designate.api.middleware:APIv1ValidationErrorMiddleware.f [filter:validation_API_v2] paste.filter_factory = designate.api.middleware:APIv2ValidationErrorMiddleware.factory + +[filter:ssl] +paste.filter_factory = designate.api.middleware:SSLMiddleware.factory \ No newline at end of file