diff --git a/etc/murano/murano-paste.ini b/etc/murano/murano-paste.ini index f91ab51f..2e8fca8f 100644 --- a/etc/murano/murano-paste.ini +++ b/etc/murano/murano-paste.ini @@ -2,7 +2,7 @@ pipeline = cloudfoundryapi [pipeline:murano] -pipeline = request_id versionnegotiation faultwrap authtoken context rootapp +pipeline = request_id ssl versionnegotiation faultwrap authtoken context rootapp [filter:context] paste.filter_factory = murano.api.middleware.context:ContextMiddleware.factory @@ -35,3 +35,6 @@ paste.filter_factory = murano.api.middleware.fault:FaultWrapper.factory # Middleware to set x-openstack-request-id in http response header [filter:request_id] paste.filter_factory = oslo_middleware.request_id:RequestId.factory + +[filter:ssl] +paste.filter_factory = murano.api.middleware.ssl:SSLMiddleware.factory diff --git a/murano/api/middleware/ssl.py b/murano/api/middleware/ssl.py new file mode 100644 index 00000000..de1aafc9 --- /dev/null +++ b/murano/api/middleware/ssl.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log as logging + +from murano.common import wsgi + +ssl_middleware_opts = [ + 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 terminator proxy.") +] +cfg.CONF.register_opts(ssl_middleware_opts) +LOG = logging.getLogger(__name__) + + +class SSLMiddleware(wsgi.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. + """ + + @classmethod + def factory(cls, global_conf, **local_conf): + def filter(app): + return cls(app) + return filter + + def __init__(self, application): + super(SSLMiddleware, self).__init__(application) + self.secure_proxy_ssl_header = 'HTTP_{0}'.format( + cfg.CONF.secure_proxy_ssl_header.upper().replace('-', '_')) + + def process_request(self, req): + LOG.debug('Default url_scheme: {0}. {1}: {2}'.format( + req.environ['wsgi.url_scheme'], self.secure_proxy_ssl_header, + req.environ.get(self.secure_proxy_ssl_header))) + req.environ['wsgi.url_scheme'] = req.environ.get( + self.secure_proxy_ssl_header, req.environ['wsgi.url_scheme']) diff --git a/murano/opts.py b/murano/opts.py index d47211f6..0cdce6fa 100644 --- a/murano/opts.py +++ b/murano/opts.py @@ -18,6 +18,7 @@ import itertools import oslo_service.sslutils +import murano.api.middleware.ssl import murano.common.config import murano.common.wsgi @@ -43,6 +44,7 @@ _opt_lists = [ murano.common.config.bind_opts, murano.common.config.file_server, murano.common.wsgi.wsgi_opts, + murano.api.middleware.ssl.ssl_middleware_opts ])), ] diff --git a/murano/tests/unit/api/middleware/__init__.py b/murano/tests/unit/api/middleware/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/murano/tests/unit/api/middleware/test_ssl.py b/murano/tests/unit/api/middleware/test_ssl.py new file mode 100644 index 00000000..25151ab5 --- /dev/null +++ b/murano/tests/unit/api/middleware/test_ssl.py @@ -0,0 +1,46 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob + +from murano.api.middleware import ssl + +from murano.tests.unit import base + + +class SSLMiddlewareTest(base.MuranoTestCase): + + def test_ssl_middleware_default_forwarded_proto(self): + middleware = ssl.SSLMiddleware(None) + request = webob.Request.blank('/environments', + headers={'X-Forwarded-Proto': 'https'}) + middleware.process_request(request) + self.assertEqual('https', + request.environ['wsgi.url_scheme']) + + def test_ssl_middleware_custon_forwarded_proto(self): + self.override_config('secure_proxy_ssl_header', + 'X-My-Forwarded-Proto') + middleware = ssl.SSLMiddleware(None) + request = webob.Request.blank('/environments', + headers={ + 'X-My-Forwarded-Proto': 'https'}) + middleware.process_request(request) + self.assertEqual('https', + request.environ['wsgi.url_scheme']) + + def test_ssl_middleware_plain_request(self): + middleware = ssl.SSLMiddleware(None) + request = webob.Request.blank('/environments', headers={}) + middleware.process_request(request) + self.assertEqual('http', + request.environ['wsgi.url_scheme'])