Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serve correct openapi spec basepath when path is altered by reverse-proxy #823

Merged
merged 11 commits into from
Dec 17, 2019
34 changes: 28 additions & 6 deletions connexion/apis/aiohttp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@ def _set_base_path(self, base_path):
def normalize_string(string):
return re.sub(r'[^a-zA-Z0-9]', '_', string.strip('/'))

def _base_path_for_prefix(self, request):
"""
returns a modified basePath which includes the incoming request's
path prefix.
"""
base_path = self.base_path
if not request.path.startswith(self.base_path):
prefix = request.path.split(self.base_path)[0]
base_path = prefix + base_path
return base_path

def _spec_for_prefix(self, request):
"""
returns a spec with a modified basePath / servers block
which corresponds to the incoming request path.
This is needed when behind a path-altering reverse proxy.
"""
base_path = self._base_path_for_prefix(request)
return self.specification.with_base_path(base_path).raw

def add_openapi_json(self):
"""
Adds openapi json to {base_path}/openapi.json
Expand All @@ -135,7 +155,8 @@ def add_openapi_yaml(self):
if not self.options.openapi_spec_path.endswith("json"):
return

openapi_spec_path_yaml = self.options.openapi_spec_path[:-len("json")] + "yaml"
openapi_spec_path_yaml = \
self.options.openapi_spec_path[:-len("json")] + "yaml"
logger.debug('Adding spec yaml: %s/%s', self.base_path,
openapi_spec_path_yaml)
self.subapp.router.add_route(
Expand All @@ -145,19 +166,19 @@ def add_openapi_yaml(self):
)

@asyncio.coroutine
def _get_openapi_json(self, req):
def _get_openapi_json(self, request):
return web.Response(
status=200,
content_type='application/json',
body=self.jsonifier.dumps(self.specification.raw)
body=self.jsonifier.dumps(self._spec_for_prefix(request))
)

@asyncio.coroutine
def _get_openapi_yaml(self, req):
def _get_openapi_yaml(self, request):
return web.Response(
status=200,
content_type='text/yaml',
body=yamldumper(self.specification.raw)
body=yamldumper(self._spec_for_prefix(request))
)

def add_swagger_ui(self):
Expand Down Expand Up @@ -213,8 +234,9 @@ def redirect(request):
@aiohttp_jinja2.template('index.j2')
@asyncio.coroutine
def _get_swagger_ui_home(self, req):
base_path = self._base_path_for_prefix(req)
template_variables = {
'openapi_spec_url': (self.base_path + self.options.openapi_spec_path)
'openapi_spec_url': (base_path + self.options.openapi_spec_path)
}
if self.options.openapi_console_ui_config is not None:
template_variables['configUrl'] = 'swagger-ui-config.json'
Expand Down
39 changes: 29 additions & 10 deletions connexion/apis/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ def add_openapi_json(self):
logger.debug('Adding spec json: %s/%s', self.base_path,
self.options.openapi_spec_path)
endpoint_name = "{name}_openapi_json".format(name=self.blueprint.name)

self.blueprint.add_url_rule(self.options.openapi_spec_path,
endpoint_name,
lambda: flask.jsonify(self.specification.raw))
self._handlers.get_json_spec)

def add_openapi_yaml(self):
"""
Expand All @@ -46,18 +47,15 @@ def add_openapi_yaml(self):
if not self.options.openapi_spec_path.endswith("json"):
return

openapi_spec_path_yaml = self.options.openapi_spec_path[:-len("json")] + "yaml"
openapi_spec_path_yaml = \
self.options.openapi_spec_path[:-len("json")] + "yaml"
logger.debug('Adding spec yaml: %s/%s', self.base_path,
openapi_spec_path_yaml)
endpoint_name = "{name}_openapi_yaml".format(name=self.blueprint.name)
self.blueprint.add_url_rule(
openapi_spec_path_yaml,
endpoint_name,
lambda: FlaskApi._build_response(
status_code=200,
mimetype="text/yaml",
data=yamldumper(self.specification.raw)
)
self._handlers.get_yaml_spec
)

def add_swagger_ui(self):
Expand Down Expand Up @@ -119,7 +117,7 @@ def _add_operation_internal(self, method, path, operation):
def _handlers(self):
# type: () -> InternalHandlers
if not hasattr(self, '_internal_handlers'):
self._internal_handlers = InternalHandlers(self.base_path, self.options)
self._internal_handlers = InternalHandlers(self.base_path, self.options, self.specification)
return self._internal_handlers

@classmethod
Expand Down Expand Up @@ -262,18 +260,25 @@ class InternalHandlers(object):
Flask handlers for internally registered endpoints.
"""

def __init__(self, base_path, options):
def __init__(self, base_path, options, specification):
self.base_path = base_path
self.options = options
self.specification = specification

def console_ui_home(self):
"""
Home page of the OpenAPI Console UI.

:return:
"""
openapi_json_route_name = "{blueprint}.{prefix}_openapi_json"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In line 44 and here we do similar formatting for the register and to get the URL for the endpoint. Maybe we can do it once? I would like to avoid duplicated "..._openapi_json" string. Something could tie this two together closer, so we don't break things later. I know we have tests covering this, but if possible would be nice to remove this duplication. Sorry if this seems over-engineering.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rafaelcaricio - really appreciate your comments.
I agree with your point here, but I couldn't come up with a good idea for how to make it happen. Maybe you could provide a more specific example of what you have in mind?

escaped = flask_utils.flaskify_endpoint(self.base_path)
openapi_json_route_name = openapi_json_route_name.format(
blueprint=escaped,
prefix=escaped
)
template_variables = {
'openapi_spec_url': (self.base_path + self.options.openapi_spec_path)
'openapi_spec_url': flask.url_for(openapi_json_route_name)
}
if self.options.openapi_console_ui_config is not None:
template_variables['configUrl'] = 'swagger-ui-config.json'
Expand All @@ -289,3 +294,17 @@ def console_ui_static_files(self, filename):
# convert PosixPath to str
static_dir = str(self.options.openapi_console_ui_from_dir)
return flask.send_from_directory(static_dir, filename)

def get_json_spec(self):
return flask.jsonify(self._spec_for_prefix())

def get_yaml_spec(self):
return yamldumper(self._spec_for_prefix()), 200, {"Content-Type": "text/yaml"}

def _spec_for_prefix(self):
"""
Modify base_path in the spec based on incoming url
This fixes problems with reverse proxies changing the path.
"""
base_path = flask.url_for(flask.request.endpoint).rsplit("/", 1)[0]
return self.specification.with_base_path(base_path).raw
8 changes: 8 additions & 0 deletions connexion/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,20 @@ def enforce_string_keys(obj):
return Swagger2Specification(spec)
return OpenAPISpecification(spec)

def clone(self):
return type(self)(copy.deepcopy(self._raw_spec))

@classmethod
def load(cls, spec, arguments=None):
if not isinstance(spec, dict):
return cls.from_file(spec, arguments=arguments)
return cls.from_dict(spec)

def with_base_path(self, base_path):
new_spec = self.clone()
new_spec.base_path = base_path
return new_spec


class Swagger2Specification(Specification):
yaml_name = 'swagger.yaml'
Expand Down
58 changes: 58 additions & 0 deletions examples/openapi3/reverseproxy/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
=====================
Reverse Proxy Example
=====================

This example demonstrates how to run a connexion application behind a path-altering reverse proxy.

You can either set the path in your app, or set the ``X-Forwarded-Path`` header.

Running:

.. code-block:: bash

$ sudo pip3 install --upgrade connexion[swagger-ui] # install Connexion from PyPI
$ ./app.py

Now open your browser and go to http://localhost:8080/reverse_proxied/ui/ to see the Swagger UI.


You can also use the ``X-Forwarded-Path`` header to modify the reverse proxy path.
For example:

.. code-block:: bash

curl -H "X-Forwarded-Path: /banana/" http://localhost:8080/openapi.json

{
"servers" : [
{
"url" : "banana/"
}
],
"paths" : {
"/hello" : {
"get" : {
"responses" : {
"200" : {
"description" : "hello",
"content" : {
"text/plain" : {
"schema" : {
"type" : "string"
}
}
}
}
},
"operationId" : "app.hello",
"summary" : "say hi"
}
}
},
"openapi" : "3.0.0",
"info" : {
"version" : "1.0",
"title" : "Path-Altering Reverse Proxy Example"
}
}

79 changes: 79 additions & 0 deletions examples/openapi3/reverseproxy/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
'''
example of connexion running behind a path-altering reverse-proxy

NOTE this demo is not secure by default!!
dtkav marked this conversation as resolved.
Show resolved Hide resolved
You'll want to make sure these headers are coming from your proxy, and not
directly from users on the web!

'''
import logging

import connexion


# adapted from http://flask.pocoo.org/snippets/35/
class ReverseProxied(object):
'''Wrap the application in this middleware and configure the
reverse proxy to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.

In nginx:

location /proxied {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Path /proxied;
}

:param app: the WSGI application
:param script_name: override the default script name (path)
:param scheme: override the default scheme
:param server: override the default server
'''

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):
logging.warning(
"this demo is not secure by default!! "
"You'll want to make sure these headers are coming from your proxy, "
"and not directly from users on the web!"
)
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)


def hello():
return "hello"


if __name__ == '__main__':
app = connexion.FlaskApp(__name__)
app.add_api('openapi.yaml')
flask_app = app.app
proxied = ReverseProxied(
flask_app.wsgi_app,
script_name='/reverse_proxied/'
)
flask_app.wsgi_app = proxied
app.run(port=8080)
16 changes: 16 additions & 0 deletions examples/openapi3/reverseproxy/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
openapi: 3.0.0
info:
title: Path-Altering Reverse Proxy Example
version: '1.0'
paths:
/hello:
get:
summary: say hi
operationId: app.hello
responses:
'200':
description: hello
content:
text/plain:
schema:
type: string
58 changes: 58 additions & 0 deletions examples/openapi3/reverseproxy_aiohttp/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
=====================
Reverse Proxy Example
=====================

This example demonstrates how to run a connexion application behind a path-altering reverse proxy.

You can either set the path in your app, or set the ``X-Forwarded-Path`` header.

Running:

.. code-block:: bash

$ sudo pip3 install --upgrade connexion[swagger-ui] aiohttp-remotes
$ ./app.py

Now open your browser and go to http://localhost:8080/reverse_proxied/ui/ to see the Swagger UI.


You can also use the ``X-Forwarded-Path`` header to modify the reverse proxy path.
For example:

.. code-block:: bash

curl -H "X-Forwarded-Path: /banana/" http://localhost:8080/openapi.json

{
"servers" : [
{
"url" : "banana"
}
],
"paths" : {
"/hello" : {
"get" : {
"responses" : {
"200" : {
"description" : "hello",
"content" : {
"text/plain" : {
"schema" : {
"type" : "string"
}
}
}
}
},
"operationId" : "app.hello",
"summary" : "say hi"
}
}
},
"openapi" : "3.0.0",
"info" : {
"version" : "1.0",
"title" : "Path-Altering Reverse Proxy Example"
}
}

Loading