mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-09 12:27:46 +00:00
add tests for reverse proxies
This commit is contained in:
@@ -37,7 +37,7 @@ except ImportError: # pragma: no cover
|
||||
App = FlaskApp
|
||||
Api = FlaskApi
|
||||
|
||||
if sys.version_info[0] >= 3: # pragma: no cover
|
||||
if sys.version_info >= (3, 5, 3): # pragma: no cover
|
||||
try:
|
||||
from .apis.aiohttp_api import AioHttpApi
|
||||
from .apps.aiohttp_app import AioHttpApp
|
||||
|
||||
@@ -10,6 +10,7 @@ directly from users on the web!
|
||||
|
||||
import connexion
|
||||
|
||||
|
||||
# adapted from http://flask.pocoo.org/snippets/35/
|
||||
class ReverseProxied(object):
|
||||
'''Wrap the application in this middleware and configure the
|
||||
|
||||
@@ -4,11 +4,13 @@ example of aiohttp connexion running behind a path-altering reverse-proxy
|
||||
'''
|
||||
|
||||
import json
|
||||
|
||||
import connexion
|
||||
from yarl import URL
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_remotes.x_forwarded import XForwardedBase
|
||||
from aiohttp_remotes.exceptions import RemoteError, TooManyHeaders
|
||||
from aiohttp_remotes.x_forwarded import XForwardedBase
|
||||
from yarl import URL
|
||||
|
||||
X_FORWARDED_PATH = "X-Forwarded-Path"
|
||||
|
||||
|
||||
5
setup.py
5
setup.py
@@ -47,9 +47,10 @@ tests_require = [
|
||||
swagger_ui_require
|
||||
]
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
if sys.version_info >= (3, 5, 3):
|
||||
tests_require.extend(aiohttp_require)
|
||||
tests_require.append('pytest-aiohttp')
|
||||
tests_require.append('aiohttp-remotes')
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
@@ -61,7 +62,7 @@ class PyTest(TestCommand):
|
||||
self.cov = None
|
||||
self.pytest_args = ['--cov', 'connexion', '--cov-report', 'term-missing', '-v']
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
if sys.version_info < (3, 5, 3):
|
||||
self.pytest_args.append('--cov-config=py2-coveragerc')
|
||||
self.pytest_args.append('--ignore=tests/aiohttp')
|
||||
else:
|
||||
|
||||
132
tests/aiohttp/test_aiohttp_reverse_proxy.py
Normal file
132
tests/aiohttp/test_aiohttp_reverse_proxy.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import asyncio
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_remotes.exceptions import RemoteError, TooManyHeaders
|
||||
from aiohttp_remotes.x_forwarded import XForwardedBase
|
||||
from connexion import AioHttpApp
|
||||
from yarl import URL
|
||||
|
||||
X_FORWARDED_PATH = "X-Forwarded-Path"
|
||||
|
||||
|
||||
class XPathForwarded(XForwardedBase):
|
||||
|
||||
def __init__(self, num=1):
|
||||
self._num = num
|
||||
|
||||
def get_forwarded_path(self, headers):
|
||||
forwarded_host = headers.getall(X_FORWARDED_PATH, [])
|
||||
if len(forwarded_host) > 1:
|
||||
raise TooManyHeaders(X_FORWARDED_PATH)
|
||||
return forwarded_host[0] if forwarded_host else None
|
||||
|
||||
@web.middleware
|
||||
async def middleware(self, request, handler):
|
||||
try:
|
||||
overrides = {}
|
||||
headers = request.headers
|
||||
|
||||
forwarded_for = self.get_forwarded_for(headers)
|
||||
if forwarded_for:
|
||||
overrides['remote'] = str(forwarded_for[-self._num])
|
||||
|
||||
proto = self.get_forwarded_proto(headers)
|
||||
if proto:
|
||||
overrides['scheme'] = proto[-self._num]
|
||||
|
||||
host = self.get_forwarded_host(headers)
|
||||
if host is not None:
|
||||
overrides['host'] = host
|
||||
|
||||
prefix = self.get_forwarded_path(headers)
|
||||
if prefix is not None:
|
||||
prefix = '/' + prefix.strip('/') + '/'
|
||||
request_path = URL(request.path.lstrip('/'))
|
||||
overrides['rel_url'] = URL(prefix).join(request_path)
|
||||
|
||||
request = request.clone(**overrides)
|
||||
|
||||
return await handler(request)
|
||||
except RemoteError as exc:
|
||||
exc.log(request)
|
||||
await self.raise_error(request)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_swagger_json_behind_proxy(simple_api_spec_dir, aiohttp_client):
|
||||
""" Verify the swagger.json file is returned with base_path updated
|
||||
according to X-Forwarded-Path header. """
|
||||
app = AioHttpApp(__name__, port=5001,
|
||||
specification_dir=simple_api_spec_dir,
|
||||
debug=True)
|
||||
api = app.add_api('swagger.yaml')
|
||||
|
||||
aio = app.app
|
||||
reverse_proxied = XPathForwarded()
|
||||
aio.middlewares.append(reverse_proxied.middleware)
|
||||
|
||||
app_client = yield from aiohttp_client(app.app)
|
||||
headers = {'X-Forwarded-Path': '/behind/proxy'}
|
||||
|
||||
swagger_ui = yield from app_client.get('/v1.0/ui/', headers=headers)
|
||||
assert swagger_ui.status == 200
|
||||
assert b'url = "/behind/proxy/v1.0/swagger.json"' in (
|
||||
yield from swagger_ui.read()
|
||||
)
|
||||
|
||||
swagger_json = yield from app_client.get('/v1.0/swagger.json',
|
||||
headers=headers)
|
||||
assert swagger_json.status == 200
|
||||
assert swagger_json.headers.get('Content-Type') == 'application/json'
|
||||
json_ = yield from swagger_json.json()
|
||||
|
||||
assert api.specification.raw['basePath'] == '/v1.0', \
|
||||
"Original specifications should not have been changed"
|
||||
|
||||
assert json_.get('basePath') == '/behind/proxy/v1.0', \
|
||||
"basePath should contains original URI"
|
||||
|
||||
json_['basePath'] = api.specification.raw['basePath']
|
||||
assert api.specification.raw == json_, \
|
||||
"Only basePath should have been updated"
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_openapi_json_behind_proxy(simple_api_spec_dir, aiohttp_client):
|
||||
""" Verify the swagger.json file is returned with base_path updated
|
||||
according to X-Forwarded-Path header. """
|
||||
app = AioHttpApp(__name__, port=5001,
|
||||
specification_dir=simple_api_spec_dir,
|
||||
debug=True)
|
||||
|
||||
api = app.add_api('openapi.yaml')
|
||||
|
||||
aio = app.app
|
||||
reverse_proxied = XPathForwarded()
|
||||
aio.middlewares.append(reverse_proxied.middleware)
|
||||
|
||||
app_client = yield from aiohttp_client(app.app)
|
||||
headers = {'X-Forwarded-Path': '/behind/proxy'}
|
||||
|
||||
swagger_ui = yield from app_client.get('/v1.0/ui/', headers=headers)
|
||||
assert swagger_ui.status == 200
|
||||
assert b'url: "/behind/proxy/v1.0/openapi.json"' in (
|
||||
yield from swagger_ui.read()
|
||||
)
|
||||
|
||||
swagger_json = yield from app_client.get('/v1.0/openapi.json',
|
||||
headers=headers)
|
||||
assert swagger_json.status == 200
|
||||
assert swagger_json.headers.get('Content-Type') == 'application/json'
|
||||
json_ = yield from swagger_json.json()
|
||||
|
||||
assert json_.get('servers', [{}])[0].get('url') == '/behind/proxy/v1.0', \
|
||||
"basePath should contains original URI"
|
||||
|
||||
url = api.specification.raw.get('servers', [{}])[0].get('url')
|
||||
assert url != '/behind/proxy/v1.0', \
|
||||
"Original specifications should not have been changed"
|
||||
|
||||
json_['servers'] = api.specification.raw.get('servers')
|
||||
assert api.specification.raw == json_, \
|
||||
"Only there servers block should have been updated"
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
from struct import unpack
|
||||
|
||||
import yaml
|
||||
from werkzeug.test import Client, EnvironBuilder
|
||||
|
||||
from connexion.apps.flask_app import FlaskJSONEncoder
|
||||
@@ -43,6 +44,36 @@ def test_app(simple_app):
|
||||
assert greeting_response['greeting'] == 'Hello jsantos'
|
||||
|
||||
|
||||
def test_openapi_yaml_behind_proxy(reverse_proxied_app):
|
||||
""" Verify the swagger.json file is returned with base_path updated
|
||||
according to X-Original-URI header.
|
||||
"""
|
||||
app_client = reverse_proxied_app.app.test_client()
|
||||
|
||||
headers = {'X-Forwarded-Path': '/behind/proxy'}
|
||||
|
||||
swagger_ui = app_client.get('/v1.0/ui/', headers=headers)
|
||||
assert swagger_ui.status_code == 200
|
||||
|
||||
openapi_yaml = app_client.get(
|
||||
'/v1.0/' + reverse_proxied_app._spec_file,
|
||||
headers=headers
|
||||
)
|
||||
assert openapi_yaml.status_code == 200
|
||||
assert openapi_yaml.headers.get('Content-Type') == 'text/yaml'
|
||||
spec = yaml.load(openapi_yaml.data.decode('utf-8'))
|
||||
|
||||
if reverse_proxied_app._spec_file == 'swagger.yaml':
|
||||
assert b'url = "/behind/proxy/v1.0/swagger.json"' in swagger_ui.data
|
||||
assert spec.get('basePath') == '/behind/proxy/v1.0', \
|
||||
"basePath should contains original URI"
|
||||
else:
|
||||
assert b'url: "/behind/proxy/v1.0/openapi.json"' in swagger_ui.data
|
||||
url = spec.get('servers', [{}])[0].get('url')
|
||||
assert url == '/behind/proxy/v1.0', \
|
||||
"basePath should contains original URI"
|
||||
|
||||
|
||||
def test_produce_decorator(simple_app):
|
||||
app_client = simple_app.app.test_client()
|
||||
|
||||
|
||||
@@ -128,6 +128,44 @@ def simple_openapi_app(request):
|
||||
return build_app_from_fixture('simple', request.param, validate_responses=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", params=SPECS)
|
||||
def reverse_proxied_app(request):
|
||||
|
||||
# adapted from http://flask.pocoo.org/snippets/35/
|
||||
class ReverseProxied(object):
|
||||
|
||||
def __init__(self, app, script_name=None, scheme=None, server=None):
|
||||
self.app = app
|
||||
self.script_name = script_name
|
||||
self.scheme = scheme
|
||||
self.server = server
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
script_name = environ.get('HTTP_X_FORWARDED_PATH', '') or self.script_name
|
||||
if script_name:
|
||||
environ['SCRIPT_NAME'] = "/" + script_name.lstrip("/")
|
||||
path_info = environ['PATH_INFO']
|
||||
if path_info.startswith(script_name):
|
||||
environ['PATH_INFO_OLD'] = path_info
|
||||
environ['PATH_INFO'] = path_info[len(script_name):]
|
||||
scheme = environ.get('HTTP_X_SCHEME', '') or self.scheme
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
server = environ.get('HTTP_X_FORWARDED_SERVER', '') or self.server
|
||||
if server:
|
||||
environ['HTTP_HOST'] = server
|
||||
return self.app(environ, start_response)
|
||||
|
||||
app = build_app_from_fixture('simple', request.param, validate_responses=True)
|
||||
flask_app = app.app
|
||||
proxied = ReverseProxied(
|
||||
flask_app.wsgi_app,
|
||||
script_name='/reverse_proxied/'
|
||||
)
|
||||
flask_app.wsgi_app = proxied
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", params=SPECS)
|
||||
def snake_case_app(request):
|
||||
return build_app_from_fixture('snake_case', request.param,
|
||||
|
||||
Reference in New Issue
Block a user