Skip to content

HTTPS Mixed Content for swagger.json with gunicorn #188

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

Closed
jslay88 opened this issue Jul 29, 2020 · 20 comments · Fixed by #214
Closed

HTTPS Mixed Content for swagger.json with gunicorn #188

jslay88 opened this issue Jul 29, 2020 · 20 comments · Fixed by #214
Labels
bug Something isn't working

Comments

@jslay88
Copy link
Contributor

jslay88 commented Jul 29, 2020

I have noticed that swagger.json fails to load behind an HTTPS proxy (say, a kubernetes ingress controller) when using gunicorn to serve, but not uWSGI. This happens regardless of passing header X-Forwarded-Proto as recommended in the gunicorn documentation. While the issue probably lies with gunicorn, I have been able to make a slight change to the library which makes it work with gunicorn and uWSGI.

Changing api.specs_url to not use _external on url_for, allows it to return just the relative path to swagger.json, instead of a complete URI (/api/v1/swagger.json vs http://host.com/api/v1/swagger.json) and subsequently allows the JS to load swagger.json. This also falls in line with the behavior of other Django REST middlewares, as well as FastAPI.

return url_for(self.endpoint("specs"), _external=True)

Repro Steps

  1. Run a basic Flask App with Flask-RESTX using gunicorn, and access it from an HTTPS reverse proxy.

Expected Behavior

Ability to load the SwaggerUI from behind an HTTPS reverse proxy using gunicorn. Having the JS load path instead of URI by rendering out api.specs_url not using _external. Ergo, a relative path to swagger.json being passed to the swagger-ui template.

Actual Behavior

RESTX renders out the entire URI using HTTP (because the connection from proxy to gunicorn is HTTP) for the JS to load swagger.json for the SwaggerUI, thus causing a broken SwaggerUI because its trying to load insecure content on a secure site.

Error Messages/Stack Trace

Mixed Content: The page at 'https://host.com/api/v1/' was loaded over HTTPS, but requested an insecure resource 'http://host.com/api/v1/swagger.json'. This request has been blocked; the content must be served over HTTPS.

Environment

  • Python version 3.8
  • Flask version 1.1.2
  • Flask-RESTX version 0.2.0
  • Other installed Flask extensions None

Additional Context

Generally, websites use relative paths for their own content.

All of the other static content loaded by the SwaggerUI (CSS, JS bundles, etc) load with relative paths and work. Only swagger.json does not. This is because the swagger_static template filter does not use _external on url_for

return url_for("restx_doc.static", filename=filename)

@jslay88 jslay88 added the bug Something isn't working label Jul 29, 2020
@FloLaco
Copy link

FloLaco commented Jul 31, 2020

The flow between Kubernetes Ingress and your container is HTTP or HTTPS ?

@jslay88
Copy link
Contributor Author

jslay88 commented Jul 31, 2020 via email

@jhampson-dbre
Copy link

Are you using ProxyFix as mentioned under Flask Proxy Setups?

import logging
from flask import Flask, request, has_request_context
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)

app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)

@app.before_request
def log_request():
    app.logger.debug("Request Headers %s", request.headers)
    return None

api.init_app(app)

if __name__ == "__main__":
    app.run(host='0.0.0.0')

This fixed a similar issues I was having with swagger.json mixed content message.

@jslay88
Copy link
Contributor Author

jslay88 commented Jul 31, 2020 via email

@jhampson-dbre
Copy link

I just ran into the same issue with a very similar setup (Https reverse proxy with SSL termination at Kubernetes ingress). I'm also using Gunicorn and it works fine in combination with ProxyFix. From the Flask docs, I stumbled upon ProxyFix before messing with any Gunicorn config, but I will go back and take a look at them as an alternative. To your point, ProxyFix is a fairly kludgy solution.

@jhampson-dbre
Copy link

After removing ProxyFix and adding forwarded_allow_ips = "*" to my Gunicorn config file, swagger.json is loaded over https with no mixed content error. This is with gunicorn==20.0.4.

In our case, since the forwarded IP is from the kubernetes ingress, I'm not sure how you could provide the full list of IPs to whitelist more conservatively, but it maybe an acceptable risk if you know that all traffic is coming through the ingress.

Using relative path to load swagger.json is still an ideal solution. For a workaround in the middleware layer, both ProxyFix and forwarded_allow_ips have worked as expected in my environment.

@jslay88
Copy link
Contributor Author

jslay88 commented Aug 20, 2020

We have just made our own child api class using the RESTX Api class as parent, and overridden the specs_url property to get by for now.

from flask import Blueprint, url_for
from flask_restx import Api


class PatchedApi(Api):
    @property
    def specs_url(self):
        return url_for(self.endpoint('specs'))


api_blueprint = Blueprint('api_v1', __name__, url_prefix='/api/v1')
api = PatchedApi(api_blueprint, title='Test API', version='1.0.0')

@drheinheimer
Copy link

Thanks @jslay88! Just a minor correction: should be self.endpoint (singular, not plural). Yes, this issue is a bug.

@dgildeh
Copy link

dgildeh commented Aug 21, 2020

Hi everyone - just hit this error myself deploying a service to Google Cloud Run using Rest_X - the HTML source is trying to load http not https as required. Reading through comments will a fix be coming for this issue soon or do I need to configure something like ProxyFix or patch the API?

@jslay88
Copy link
Contributor Author

jslay88 commented Aug 28, 2020

Thanks @jslay88! Just a minor correction: should be self.endpoint (singular, not plural). Yes, this issue is a bug.

Sorry, was doing it from memory.

@drheinheimer
Copy link

Hi everyone - just hit this error myself deploying a service to Google Cloud Run using Rest_X - the HTML source is trying to load http not https as required. Reading through comments will a fix be coming for this issue soon or do I need to configure something like ProxyFix or patch the API?

@dgildeh did you resolve this? Patching the API as jslay88 is trivial, and is what the fix presumably would do anyway.

@jslay88
Copy link
Contributor Author

jslay88 commented Sep 2, 2020

Opened a PR to try and get things moving forward.

ziirish added a commit that referenced this issue Sep 2, 2020
Use relative path for `api.specs_url`.

Fix #188
@dgildeh
Copy link

dgildeh commented Sep 4, 2020

Hi everyone - just hit this error myself deploying a service to Google Cloud Run using Rest_X - the HTML source is trying to load http not https as required. Reading through comments will a fix be coming for this issue soon or do I need to configure something like ProxyFix or patch the API?

@dgildeh did you resolve this? Patching the API as jslay88 is trivial, and is what the fix presumably would do anyway.

No I didn't, I was waiting for an official fix which looks like we have now. I also hit another bug with the default error response not working locally (I don't have the issue to hand but saw several already created for this bug, but creating a default error handler doesn't work, potentially while in Debug mode locally as one issue raised, it renders HTML error instead of the JSON error I'm trying to send for all errors - if I specify an exception class it works as expected thought)

And finally I've instrumented OpenTelemetry traces to monitor performance, and seeing a lot of latency before and after my function runs, which may be Flask or I suspect the marshalling of the request/response. Haven't raised a ticket yet as not entirely sure where the latency is coming from yet.

With the bugs and latency issues I'm seeing I'm seriously considering moving to FastAPI which I discovered recently, and may be a better fit for my needs.

@jslay88
Copy link
Contributor Author

jslay88 commented Sep 4, 2020 via email

@neumannrf
Copy link

Hi @jslay88 !

I am in the exact same situation you mentioned in the issue (kubernetes ingress controller), but I am facing a slightly different (but probably related) problem:

If I switch from [email protected] to flask-restx@master (which incorporates your PR), although I stop getting that annoying mixed content error message, I now face a different error due to missing (404 NOT FOUND)

  • /swaggerui/droid-sans.css
  • /swaggerui/swagger-ui.css
  • /swaggerui/swagger-ui-bundle.js
  • /swaggerui/swagger-ui-standalone-preset.js

This error persists even in HTTP-only access with the latest flask-restx@master, but goes away if I revert to [email protected] (on HTTP-only, of course).

Have you ever had that issue before?

@j5awry
Copy link
Contributor

j5awry commented Jan 11, 2021

@neumannrf you need to install the JS frontend assets for Swagger. The JS packages are in the package.json. You can see an example install command in tasks.py. If the they're installed in the proper location with npm and you're still seeing the error, please open a new issue

@neumannrf
Copy link

Hi @j5awry! I don't understand what it is I have to do.

As you pointed out, all those commands are already incorporated in the flask-restx source code and they already work very well when I run the application in HTTP mode. So, to my understanding, the library is already installing "the JS frontend assets for Swagger" so I would not (in principle) have to do anything extra.

@j5awry
Copy link
Contributor

j5awry commented Jan 14, 2021

The issue I was considering is the pathing (and I didn't know your installation method, from source or from pypi). If you're pulling in from source, the assets aren't in the source (which is my guess since you're stating you're pulling from @master). Quick example:

pyenv virtualenv 3.8.5 test-restx-js
pyenv activate test-restx-js
python -m pip install git+https://github.com/python-restx/flask-restx.git@master
# copy the basic TODO from my local source as an example
cp ~/dev01/python-restx/flask-restx/examples/todo.py ./
python todo.py
 * Serving Flask app "todo" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 214-994-467
127.0.0.1 - - [14/Jan/2021 09:15:27] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Jan/2021 09:15:27] "GET /swaggerui/droid-sans.css HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:27] "GET /swaggerui/swagger-ui.css HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:27] "GET /swaggerui/swagger-ui-bundle.js HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:27] "GET /swaggerui/swagger-ui-standalone-preset.js HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:28] "GET /swaggerui/droid-sans.css HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:28] "GET /swaggerui/swagger-ui.css HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:28] "GET /swaggerui/swagger-ui-bundle.js HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:28] "GET /swaggerui/swagger-ui-standalone-preset.js HTTP/1.1" 404 -
127.0.0.1 - - [14/Jan/2021 09:15:28] "GET /swaggerui/favicon-16x16.png HTTP/1.1" 404 -

Compare to installing from Pypi, where we have the assets

pyenv virtualenv 3.8.5 test-restx-pypi
pyenv activate test-restx-pypi
pip install flask-restx
python todo.py
 * Serving Flask app "todo" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 233-386-969
127.0.0.1 - - [14/Jan/2021 09:19:33] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Jan/2021 09:19:33] "GET /swaggerui/droid-sans.css HTTP/1.1" 200 -
127.0.0.1 - - [14/Jan/2021 09:19:33] "GET /swaggerui/swagger-ui-bundle.js HTTP/1.1" 200 -
127.0.0.1 - - [14/Jan/2021 09:19:33] "GET /swaggerui/swagger-ui.css HTTP/1.1" 200 -
127.0.0.1 - - [14/Jan/2021 09:19:33] "GET /swaggerui/swagger-ui-standalone-preset.js HTTP/1.1" 200 -
127.0.0.1 - - [14/Jan/2021 09:19:33] "GET /swagger.json HTTP/1.1" 200 -

So you need to add the assets. You can do this using NPM and package.json in the source code:

$ npm install package.json
npm WARN deprecated [email protected]: Use pkg.json instead.
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN notsup Unsupported engine for [email protected]: wanted: {"node":">=0.10.0 <7"} (current: {"node":"12.14.1","npm":"6.13.4"})
npm WARN notsup Not compatible with your version of node/npm: [email protected]
npm WARN Invalid version: "0.2.1.dev"
npm WARN test-restx No description
npm WARN test-restx No repository field.
npm WARN test-restx No README data
npm WARN test-restx No license field.

+ [email protected]
added 77 packages from 43 contributors and audited 77 packages in 5.184s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

$ ls -al
total 44
drwxrwxr-x  3 jchittum jchittum  4096 Jan 14 09:49 .
drwxr-xr-x 48 jchittum jchittum  4096 Jan 14 09:13 ..
drwxrwxr-x 80 jchittum jchittum  4096 Jan 14 09:49 node_modules
-rw-rw-r--  1 jchittum jchittum   437 Jan 14 09:49 package.json
-rw-rw-r--  1 jchittum jchittum 22284 Jan 14 09:49 package-lock.json
-rw-rw-r--  1 jchittum jchittum  2508 Jan 14 09:14 todo.py
(test-restx-js) jchittum@j5awry-sys76:~/dev01/troubleshooting/test-restx$ python todo.py 
 * Serving Flask app "todo" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 214-994-467
127.0.0.1 - - [14/Jan/2021 09:49:34] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Jan/2021 09:49:34] "GET /swagger.json HTTP/1.1" 200 -

@jslay88
Copy link
Contributor Author

jslay88 commented Jan 23, 2021

Make sure that you are also setting gunicorn up correctly as well with --forwarded-allow-ips if deploying with it (you should be). If you are in a kubernetes cluster you can set this to *

https://docs.gunicorn.org/en/stable/settings.html#forwarded-allow-ips

@neumannrf
Copy link

Hi @j5awry ! I don't know why I did not get a notification with your last message.

I understand what I have to do now. It is a little bit more complicated to do this inside one of these language-specific cloud buildpacks. Typically, the buildpack that has pyenv, pip and python does not have npm, and vice-versa.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants