mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-09 04:19:32 +00:00
add tests for reverse proxies
This commit is contained in:
@@ -37,7 +37,7 @@ except ImportError: # pragma: no cover
|
|||||||
App = FlaskApp
|
App = FlaskApp
|
||||||
Api = FlaskApi
|
Api = FlaskApi
|
||||||
|
|
||||||
if sys.version_info[0] >= 3: # pragma: no cover
|
if sys.version_info >= (3, 5, 3): # pragma: no cover
|
||||||
try:
|
try:
|
||||||
from .apis.aiohttp_api import AioHttpApi
|
from .apis.aiohttp_api import AioHttpApi
|
||||||
from .apps.aiohttp_app import AioHttpApp
|
from .apps.aiohttp_app import AioHttpApp
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ directly from users on the web!
|
|||||||
|
|
||||||
import connexion
|
import connexion
|
||||||
|
|
||||||
|
|
||||||
# adapted from http://flask.pocoo.org/snippets/35/
|
# adapted from http://flask.pocoo.org/snippets/35/
|
||||||
class ReverseProxied(object):
|
class ReverseProxied(object):
|
||||||
'''Wrap the application in this middleware and configure the
|
'''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 json
|
||||||
|
|
||||||
import connexion
|
import connexion
|
||||||
from yarl import URL
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_remotes.x_forwarded import XForwardedBase
|
|
||||||
from aiohttp_remotes.exceptions import RemoteError, TooManyHeaders
|
from aiohttp_remotes.exceptions import RemoteError, TooManyHeaders
|
||||||
|
from aiohttp_remotes.x_forwarded import XForwardedBase
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
X_FORWARDED_PATH = "X-Forwarded-Path"
|
X_FORWARDED_PATH = "X-Forwarded-Path"
|
||||||
|
|
||||||
|
|||||||
5
setup.py
5
setup.py
@@ -47,9 +47,10 @@ tests_require = [
|
|||||||
swagger_ui_require
|
swagger_ui_require
|
||||||
]
|
]
|
||||||
|
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info >= (3, 5, 3):
|
||||||
tests_require.extend(aiohttp_require)
|
tests_require.extend(aiohttp_require)
|
||||||
tests_require.append('pytest-aiohttp')
|
tests_require.append('pytest-aiohttp')
|
||||||
|
tests_require.append('aiohttp-remotes')
|
||||||
|
|
||||||
|
|
||||||
class PyTest(TestCommand):
|
class PyTest(TestCommand):
|
||||||
@@ -61,7 +62,7 @@ class PyTest(TestCommand):
|
|||||||
self.cov = None
|
self.cov = None
|
||||||
self.pytest_args = ['--cov', 'connexion', '--cov-report', 'term-missing', '-v']
|
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('--cov-config=py2-coveragerc')
|
||||||
self.pytest_args.append('--ignore=tests/aiohttp')
|
self.pytest_args.append('--ignore=tests/aiohttp')
|
||||||
else:
|
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
|
import json
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
|
import yaml
|
||||||
from werkzeug.test import Client, EnvironBuilder
|
from werkzeug.test import Client, EnvironBuilder
|
||||||
|
|
||||||
from connexion.apps.flask_app import FlaskJSONEncoder
|
from connexion.apps.flask_app import FlaskJSONEncoder
|
||||||
@@ -43,6 +44,36 @@ def test_app(simple_app):
|
|||||||
assert greeting_response['greeting'] == 'Hello jsantos'
|
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):
|
def test_produce_decorator(simple_app):
|
||||||
app_client = simple_app.app.test_client()
|
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)
|
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)
|
@pytest.fixture(scope="session", params=SPECS)
|
||||||
def snake_case_app(request):
|
def snake_case_app(request):
|
||||||
return build_app_from_fixture('snake_case', request.param,
|
return build_app_from_fixture('snake_case', request.param,
|
||||||
|
|||||||
Reference in New Issue
Block a user