From bcd9a8cdd7c37fb655764a3b12c7e567598d088d Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Fri, 5 Oct 2018 17:35:08 +0200 Subject: [PATCH] Switch from oslo.middleware to internal proxy converter --- gnocchi/gnocchi-config-generator.conf | 1 - gnocchi/opts.py | 3 +- gnocchi/rest/api-paste.ini | 10 +- gnocchi/rest/app.py | 4 +- gnocchi/rest/http_proxy_to_wsgi.py | 116 ++++++++++++++++++ gnocchi/tests/functional/fixtures.py | 2 + .../gabbits/http-proxy-to-wsgi.yaml | 16 +++ 7 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 gnocchi/rest/http_proxy_to_wsgi.py create mode 100644 gnocchi/tests/functional/gabbits/http-proxy-to-wsgi.yaml diff --git a/gnocchi/gnocchi-config-generator.conf b/gnocchi/gnocchi-config-generator.conf index ab1752dd5..5d0c49321 100644 --- a/gnocchi/gnocchi-config-generator.conf +++ b/gnocchi/gnocchi-config-generator.conf @@ -3,7 +3,6 @@ wrap_width = 79 namespace = gnocchi namespace = oslo.middleware.cors namespace = oslo.middleware.healthcheck -namespace = oslo.middleware.http_proxy_to_wsgi namespace = oslo.policy namespace = cotyledon namespace = keystonemiddleware.auth_token diff --git a/gnocchi/opts.py b/gnocchi/opts.py index d86ab5196..e2d8f9c6f 100644 --- a/gnocchi/opts.py +++ b/gnocchi/opts.py @@ -22,6 +22,7 @@ import gnocchi.archive_policy import gnocchi.common.redis import gnocchi.indexer +import gnocchi.rest.http_proxy_to_wsgi import gnocchi.storage import gnocchi.storage.ceph import gnocchi.storage.file @@ -192,7 +193,7 @@ def list_opts(): default=10, min=0, help='Number of seconds before timeout when attempting ' 'to do some operations.'), - ) + API_OPTS, + ) + API_OPTS + gnocchi.rest.http_proxy_to_wsgi.OPTS, ), ("storage", _STORAGE_OPTS), ("incoming", _INCOMING_OPTS), diff --git a/gnocchi/rest/api-paste.ini b/gnocchi/rest/api-paste.ini index 2b6df8532..aa40553b0 100644 --- a/gnocchi/rest/api-paste.ini +++ b/gnocchi/rest/api-paste.ini @@ -17,13 +17,13 @@ use = egg:Paste#urlmap /healthcheck = healthcheck [pipeline:gnocchiv1+noauth] -pipeline = http_proxy_to_wsgi gnocchiv1 +pipeline = gnocchiv1 [pipeline:gnocchiv1+keystone] -pipeline = http_proxy_to_wsgi keystone_authtoken gnocchiv1 +pipeline = keystone_authtoken gnocchiv1 [pipeline:gnocchiversions_pipeline] -pipeline = http_proxy_to_wsgi gnocchiversions +pipeline = gnocchiversions [app:gnocchiversions] paste.app_factory = gnocchi.rest.app:app_factory @@ -37,10 +37,6 @@ root = gnocchi.rest.api.V1Controller use = egg:keystonemiddleware#auth_token oslo_config_project = gnocchi -[filter:http_proxy_to_wsgi] -use = egg:oslo.middleware#http_proxy_to_wsgi -oslo_config_project = gnocchi - [app:healthcheck] use = egg:oslo.middleware#healthcheck oslo_config_project = gnocchi diff --git a/gnocchi/rest/app.py b/gnocchi/rest/app.py index d3ab7717c..983846623 100644 --- a/gnocchi/rest/app.py +++ b/gnocchi/rest/app.py @@ -35,6 +35,7 @@ from gnocchi import incoming as gnocchi_incoming from gnocchi import indexer as gnocchi_indexer from gnocchi import json +from gnocchi.rest import http_proxy_to_wsgi from gnocchi import storage as gnocchi_storage @@ -178,7 +179,8 @@ def load_app(conf, not_implemented_middleware=True): appname = "gnocchi+" + conf.api.auth_mode app = deploy.loadapp("config:" + cfg_path, name=appname, global_conf={'configkey': configkey}) - return cors.CORS(app, conf=conf) + return http_proxy_to_wsgi.HTTPProxyToWSGI( + cors.CORS(app, conf=conf), conf=conf) def _setup_app(root, conf, not_implemented_middleware): diff --git a/gnocchi/rest/http_proxy_to_wsgi.py b/gnocchi/rest/http_proxy_to_wsgi.py new file mode 100644 index 000000000..9b86360e2 --- /dev/null +++ b/gnocchi/rest/http_proxy_to_wsgi.py @@ -0,0 +1,116 @@ +# -*- encoding: utf-8 -*- +# +# 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 + +import webob.dec +import webob.request +import webob.response + + +OPTS = ( + cfg.BoolOpt('enable_proxy_headers_parsing', + deprecated_group="oslo_middleware", + default=False, + help="Whether the application is behind a proxy or not. " + "This determines if the middleware should parse the " + "headers or not."), +) + + +class NoContentTypeResponse(webob.response.Response): + + default_content_type = None # prevents webob assigning content type + + +class NoContentTypeRequest(webob.request.Request): + + ResponseClass = NoContentTypeResponse + + +class HTTPProxyToWSGI(object): + """HTTP proxy to WSGI termination middleware. + + This middleware overloads WSGI environment variables with the one provided + by the remote HTTP reverse proxy. + + """ + + def __init__(self, application, conf=None): + """Base middleware constructor + + :param conf: a cfg.ConfigOpts object + """ + self.application = application + self.oslo_conf = conf + + @webob.dec.wsgify(RequestClass=NoContentTypeRequest) + def __call__(self, req): + self.process_request(req) + return req.get_response(self.application) + + @staticmethod + def _parse_rfc7239_header(header): + """Parses RFC7239 Forward headers. + + e.g. for=192.0.2.60;proto=http, for=192.0.2.60;by=203.0.113.43 + + """ + result = [] + for proxy in header.split(","): + entry = {} + for d in proxy.split(";"): + key, _, value = d.partition("=") + entry[key.lower().strip()] = value.strip() + result.append(entry) + return result + + def process_request(self, req): + if not self.oslo_conf.api.enable_proxy_headers_parsing: + return + fwd_hdr = req.environ.get("HTTP_FORWARDED") + if fwd_hdr: + proxies = self._parse_rfc7239_header(fwd_hdr) + # Let's use the value from the first proxy + if proxies: + proxy = proxies[0] + + forwarded_proto = proxy.get("proto") + if forwarded_proto: + req.environ['wsgi.url_scheme'] = forwarded_proto + + forwarded_host = proxy.get("host") + if forwarded_host: + req.environ['HTTP_HOST'] = forwarded_host + + forwarded_for = proxy.get("for") + if forwarded_for: + req.environ['REMOTE_ADDR'] = forwarded_for + + else: + # World before RFC7239 + forwarded_proto = req.environ.get("HTTP_X_FORWARDED_PROTO") + if forwarded_proto: + req.environ['wsgi.url_scheme'] = forwarded_proto + + forwarded_host = req.environ.get("HTTP_X_FORWARDED_HOST") + if forwarded_host: + req.environ['HTTP_HOST'] = forwarded_host + + forwarded_for = req.environ.get("HTTP_X_FORWARDED_FOR") + if forwarded_for: + req.environ['REMOTE_ADDR'] = forwarded_for + + v = req.environ.get("HTTP_X_FORWARDED_PREFIX") + if v: + req.environ['SCRIPT_NAME'] = v + req.environ['SCRIPT_NAME'] diff --git a/gnocchi/tests/functional/fixtures.py b/gnocchi/tests/functional/fixtures.py index 6bcdf9dbe..dc0146d96 100644 --- a/gnocchi/tests/functional/fixtures.py +++ b/gnocchi/tests/functional/fixtures.py @@ -171,6 +171,8 @@ def start_fixture(self): # Set pagination to a testable value conf.set_override('max_limit', 7, 'api') + conf.set_override('enable_proxy_headers_parsing', True, group="api") + self.index = index self.coord = metricd.get_coordinator_and_start(str(uuid.uuid4()), diff --git a/gnocchi/tests/functional/gabbits/http-proxy-to-wsgi.yaml b/gnocchi/tests/functional/gabbits/http-proxy-to-wsgi.yaml new file mode 100644 index 000000000..368a620be --- /dev/null +++ b/gnocchi/tests/functional/gabbits/http-proxy-to-wsgi.yaml @@ -0,0 +1,16 @@ +fixtures: + - ConfigFixture + +defaults: + request_headers: + content-type: application/json + # User foobar + authorization: "basic Zm9vYmFyOg==" + +tests: + - name: test HTTP proxy headers + GET: / + request_headers: + Forwarded: for=192.0.2.60;proto=http;host=foobar + response_json_paths: + $.versions[0].links[0].href: http://foobar/gnocchi/v1/