├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── .snyk ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.py ├── docker-compose.yml ├── flask_reverse_proxy_fix ├── __init__.py └── middleware │ └── __init__.py ├── manage.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── test_flask_reverse_proxy_middleware_path_prefix.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Set default charset, 4 space indentation 12 | [*.py] 13 | charset = utf-8 14 | indent_style = space 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea 3 | build 4 | dist 5 | flask_reverse_proxy_fix.egg-info 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # == Notes == 4 | 5 | # - GitLab automatically passes artifacts from previous stages by default 6 | # - Set required secret variables at: https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-reverse-proxy/settings/ci_cd 7 | 8 | # = Secret variables 9 | # - Variables are grouped by section in KEY: "value" format (e.g. FOO: "bar") 10 | # Sensitive values are represented by "[Sensitive]" 11 | # 12 | # - Snyk 13 | # > SNYK_TOKEN: "[Sensitive]" 14 | # 15 | # - PyPi (production) 16 | # > PYPI_PASSWORD: "[Sensitive]" 17 | # 18 | # - PyPi (staging) 19 | # > PYPI_TEST_PASSWORD: "[Sensitive]" 20 | 21 | # == Global settings == 22 | 23 | stages: 24 | - test 25 | - lint 26 | - build 27 | - publish 28 | 29 | variables: 30 | APP_NAME: flask-middleware-reverse-proxy 31 | SNYK_ORG: antarctica 32 | SNYK_PROJECT: flask-middleware-reverse-proxy 33 | PYPI_USERNAME: british-antarctic-survey 34 | PYPI_TEST_USERNAME: british-antarctic-survey 35 | 36 | image: 37 | name: docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-reverse-proxy:0.1.0-alpine 38 | entrypoint: [""] 39 | 40 | # == Jobs == 41 | 42 | test-app: 43 | stage: test 44 | variables: 45 | FLASK_ENV: testing 46 | script: 47 | - "flask test" 48 | 49 | dependencies-app: 50 | stage: lint 51 | image: 52 | name: antarctica/snyk-cli:python-3 53 | entrypoint: [""] 54 | script: 55 | - "pip install -r requirements.txt" 56 | - "snyk test" 57 | - "snyk monitor --project-name=$SNYK_PROJECT --org=$SNYK_ORG" 58 | only: 59 | - master 60 | 61 | pep8-app: 62 | stage: lint 63 | script: 64 | - "flake8 . --ignore=E501" 65 | 66 | bandit-app: 67 | stage: lint 68 | script: 69 | - "bandit -r ." 70 | 71 | build-app: 72 | stage: build 73 | script: 74 | - "python setup.py sdist bdist_wheel" 75 | artifacts: 76 | name: "$CI_BUILD_TOKEN-build" 77 | paths: 78 | - dist 79 | expire_in: 1 month 80 | 81 | publish-app-stage: 82 | stage: publish 83 | script: 84 | - "python -m twine upload --repository-url https://test.pypi.org/legacy/ --username $PYPI_TEST_USERNAME --password $PYPI_TEST_PASSWORD --disable-progress-bar --verbose dist/*" 85 | only: 86 | - master 87 | environment: 88 | name: staging 89 | url: https://test.pypi.org/project/flask-reverse-proxy-fix/ 90 | 91 | publish-app-prod: 92 | stage: publish 93 | script: 94 | - "python -m twine upload --username $PYPI_USERNAME --password $PYPI_PASSWORD --disable-progress-bar --verbose dist/*" 95 | only: 96 | - tags 97 | environment: 98 | name: production 99 | url: https://pypi.org/project/flask-reverse-proxy-fix/ 100 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.12.0 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-PYTHON-PYYAML-42159: 6 | - '*': 7 | reason: 'Required by other dependency, no known resolution' 8 | expires: 2019-10-28T00:00:00.000Z 9 | patch: {} 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Flask Reverse Proxy Middleware - Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Fixed 11 | 12 | * Refactored `app.py` into `app.py` and `manage.py` to prevent running an application on import 13 | * Improved versioning support in `setup.py` 14 | 15 | ## [0.2.1] - 2019-03-02 16 | 17 | ### Fixed 18 | 19 | * Correcting documentation for `REVERSE_PROXY_PATH` config option 20 | * Guarding against missing `REVERSE_PROXY_PATH` config option 21 | 22 | ## [0.2.0] - 2019-03-02 23 | 24 | ### Changed 25 | 26 | * Wrapping around Flask app rather than a WSGI app 27 | * Incorporating `werkzeug.contrib.fixers.ProxyFix` middleware 28 | * Improved handling of pre-release versions in `setup.py` when running in CI/CD 29 | 30 | ## [0.1.1] - 2019-03-01 31 | 32 | ### Fixed 33 | 34 | * Correcting prefix value in usage example to include a preceding forward slash 35 | 36 | ## [0.1.0] - 2019-02-24 37 | 38 | ### Added 39 | 40 | * Initial version based on middleware developed for the BAS People (Sensitive) API 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing policy 2 | 3 | We welcome contributions from both other NERC centres and from external entities under the following guidelines: 4 | 5 | * These are general guidelines used by default for projects that we make available, 6 | they do not represent any formal policy or notice 7 | 8 | * The preferred method of integrating contributions is through a Git pull request to the nominated repository 9 | 10 | * The preferred method of raising issues is through the nominated issue tracker 11 | 12 | * Follow any defined coding standards where specified, or any obvious conventions implied within the code 13 | 14 | Last updated: February 2016 15 | Contact address: webapps@bas.ac.uk (BAS Web & Applications Team) 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | 3 | LABEL maintainer = "Felix Fennell " 4 | 5 | # Setup project 6 | WORKDIR /usr/src/app 7 | 8 | ENV PYTHONPATH /usr/src/app 9 | ENV FLASK_APP manage.py 10 | ENV FLASK_ENV development 11 | 12 | # Setup project dependencies 13 | COPY requirements.txt /usr/src/app/ 14 | RUN pip install --upgrade pip && \ 15 | pip install -r requirements.txt --no-cache-dir 16 | 17 | # Setup runtime 18 | ENTRYPOINT [] 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | © UK Research and Innovation (UKRI), 2019, British Antarctic Survey. 4 | 5 | You may use and re-use this software and associated documentation files free of charge in any format or medium, under 6 | the terms of the Open Government Licence v3.0. 7 | 8 | You may obtain a copy of the Open Government Licence at http://www.nationalarchives.gov.uk/doc/open-government-licence/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Reverse Proxy Middleware 2 | 3 | Python Flask middleware for applications running under a reverse proxy. 4 | 5 | ## Purpose 6 | 7 | Currently this middleware supports correcting URLs generated by `Flask.url_for()` where a common prefix needs to be 8 | added to all URLs. 9 | 10 | For example: If client requests for an application are reverse proxied such that: `example.com/some-service/v1/foo` 11 | becomes `some-service-v1.internal/foo`, where `/foo` is a route within a Flask application `foo()`. 12 | 13 | Without this middleware, a call to `Flask.url_for('.foo')` would give: `/foo`. If returned to the client, as a *self* 14 | link for example, this would cause a request to `example.com/foo`, which would be invalid as the `/some-service/v1` 15 | prefix is missing. 16 | 17 | With this middleware, a call to `Flask.url_for('.foo')` would give: '/some-service/v1/foo', which will work if used by 18 | a client. 19 | 20 | This middleware is compatible with both relative and absolute URLs (i.e. `Flask.url_for('.foo')` and 21 | `Flask.url_for('.foo', _external=True)`. 22 | 23 | This middleware incorporates the 24 | [`werkzeug.contrib.fixers.ProxyFix`](http://werkzeug.pocoo.org/docs/0.14/contrib/fixers/#werkzeug.contrib.fixers.ProxyFix) 25 | and based on the 26 | [Fixing SCRIPT_NAME/url_scheme when behind reverse proxy](http://flask.pocoo.org/snippets/35/) Flask snippet. 27 | 28 | ## Installation 29 | 30 | This package can be installed using Pip from [PyPi](https://pypi.org/project/flask-reverse-proxy-fix): 31 | 32 | ``` 33 | $ pip install flask-reverse-proxy-fix 34 | ``` 35 | 36 | ## Usage 37 | 38 | This middleware requires one parameter, a Flask config option, `REVERSE_PROXY_PATH_PREFIX`, for the path prefix value. 39 | 40 | **Note:** The prefix value **SHOULD** include a preceding slash, it **SHOULD NOT** include a trailing slash (i.e. use 41 | `/foo` not `/foo/`). 42 | 43 | A minimal application would look like this: 44 | 45 | ```python 46 | from flask import Flask, url_for 47 | from flask_reverse_proxy_fix.middleware import ReverseProxyPrefixFix 48 | 49 | app = Flask(__name__) 50 | 51 | app.config['REVERSE_PROXY_PATH'] = '/foo' 52 | ReverseProxyPrefixFix(app) 53 | 54 | @app.route('/') 55 | def hello_world(): 56 | return url_for('.hello_world') 57 | ``` 58 | 59 | ## Developing 60 | 61 | A docker container ran through Docker Compose is used as a development environment for this project. It includes 62 | development only dependencies listed in `requirements.txt`, a local Flask application in `app.py` and 63 | [Integration tests](#integration-tests). 64 | 65 | Ensure classes and methods are defined within the `flask_reverse_proxy_fix` package. 66 | 67 | Ensure [Integration tests](#integration-tests) are written for any new feature, or changes to existing features. 68 | 69 | If you have access to the BAS GitLab instance, pull the Docker image from the BAS Docker Registry: 70 | 71 | ```shell 72 | $ docker login docker-registry.data.bas.ac.uk 73 | $ docker-compose pull 74 | 75 | # To run the local Flask application using the Flask development server 76 | $ docker-compose up 77 | 78 | # To start a shell 79 | $ docker-compose run app ash 80 | ``` 81 | 82 | ### Code Style 83 | 84 | PEP-8 style and formatting guidelines must be used for this project, with the exception of the 80 character line limit. 85 | 86 | [Flake8](http://flake8.pycqa.org/) is used to ensure compliance, and is ran on each commit through 87 | [Continuous Integration](#continuous-integration). 88 | 89 | To check compliance locally: 90 | 91 | ```shell 92 | $ docker-compose run app flake8 . --ignore=E501 93 | ``` 94 | 95 | ### Dependencies 96 | 97 | Development Python dependencies should be declared in `requirements.txt` to be included in the development environment. 98 | 99 | Runtime Python dependencies should be declared in `requirements.txt` and `setup.py` to also be installed as dependencies 100 | of this package in other applications. 101 | 102 | All dependencies should be periodically reviewed and update as new versions are released. 103 | 104 | ```shell 105 | $ docker-compose run app ash 106 | $ pip install [dependency]== 107 | # this will display a list of available versions, add the latest to `requirements.txt` and or `setup.py` 108 | $ exit 109 | $ docker-compose down 110 | $ docker-compose build 111 | ``` 112 | 113 | If you have access to the BAS GitLab instance, push the Docker image to the BAS Docker Registry: 114 | 115 | ```shell 116 | $ docker login docker-registry.data.bas.ac.uk 117 | $ docker-compose push 118 | ``` 119 | 120 | ### Dependency vulnerability scanning 121 | 122 | To ensure the security of this API, all dependencies are checked against 123 | [Snyk](https://app.snyk.io/org/antarctica/project/dd93f059-5ff6-4767-b8cb-f1090fef43c4) for vulnerabilities. 124 | 125 | **Warning:** Snyk relies on known vulnerabilities and can't check for issues that are not in it's database. As with all 126 | security tools, Snyk is an aid for spotting common mistakes, not a guarantee of secure code. 127 | 128 | Some vulnerabilities have been ignored in this project, see `.snyk` for definitions and the 129 | [Dependency exceptions](#dependency-vulnerability-exceptions) section for more information. 130 | 131 | Through [Continuous Integration](#continuous-integration), on each commit current dependencies are tested and a snapshot 132 | uploaded to Snyk. This snapshot is then monitored for vulnerabilities. 133 | 134 | #### Dependency vulnerability exceptions 135 | 136 | This project contains known vulnerabilities that have been ignored for a specific reason. 137 | 138 | * [Py-Yaml `yaml.load()` function allows Arbitrary Code Execution](https://snyk.io/vuln/SNYK-PYTHON-PYYAML-42159) 139 | * currently no known or planned resolution 140 | * indirect dependency, required through the `bandit` package 141 | * severity is rated *high* 142 | * risk judged to be *low* as we don't use the Yaml module in this application 143 | * ignored for 1 year for re-review 144 | 145 | ### Static security scanning 146 | 147 | To ensure the security of this API, source code is checked against [Bandit](https://github.com/PyCQA/bandit) for issues 148 | such as not sanitising user inputs or using weak cryptography. 149 | 150 | **Warning:** Bandit is a static analysis tool and can't check for issues that are only be detectable when running the 151 | application. As with all security tools, Bandit is an aid for spotting common mistakes, not a guarantee of secure code. 152 | 153 | Through [Continuous Integration](#continuous-integration), each commit is tested. 154 | 155 | To check locally: 156 | 157 | ```shell 158 | $ docker-compose run app bandit -r . 159 | ``` 160 | 161 | ## Testing 162 | 163 | ### Integration tests 164 | 165 | This project uses integration tests to ensure features work as expected and to guard against regressions and 166 | vulnerabilities. 167 | 168 | The Python [UnitTest](https://docs.python.org/3/library/unittest.html) library is used for running tests using Flask's 169 | test framework. Test cases are defined in files within `tests/` and are automatically loaded when using the 170 | `test` Flask CLI command included in the local Flask application in the development environment. 171 | 172 | Tests are automatically ran on each commit through [Continuous Integration](#continuous-integration). 173 | 174 | To run tests manually: 175 | 176 | ```shell 177 | $ docker-compose run -e FLASK_ENV=testing app flask test 178 | ``` 179 | 180 | To run tests using PyCharm: 181 | 182 | * *Run* -> *Edit Configurations* 183 | * *Add New Configuration* -> *Python Tests* -> *Unittests* 184 | 185 | In *Configuration* tab: 186 | 187 | * Script path: `[absolute path to project]/tests` 188 | * Python interpreter: *Project interpreter* (*app* service in project Docker Compose) 189 | * Working directory: `[absolute path to project]` 190 | * Path mappings: `[absolute path to project]=/usr/src/app` 191 | 192 | **Note:** This configuration can be also be used to debug tests (by choosing *debug* instead of *run*). 193 | 194 | ### Continuous Integration 195 | 196 | All commits will trigger a Continuous Integration process using GitLab's CI/CD platform, configured in `.gitlab-ci.yml`. 197 | 198 | This process will run the application [Integration tests](#integration-tests). 199 | 200 | Pip dependencies are also [checked and monitored for vulnerabilities](#dependency-vulnerability-scanning). 201 | 202 | ## Distribution 203 | 204 | Both source and binary versions of the package are build using [SetupTools](https://setuptools.readthedocs.io), which 205 | can then be published to the [Python package index](https://pypi.org/project/flask-reverse-proxy-fix/) for use in other 206 | applications. Package settings are defined in `setup.py`. 207 | 208 | This project is built and published to PyPi automatically through [Continuous Deployment](#continuous-deployment). 209 | 210 | To build the source and binary artefacts for this project manually: 211 | 212 | ```shell 213 | $ docker-compose run app ash 214 | # build package to /build, /dist and /flask_reverse_proxy_fix.egg-info 215 | $ python setup.py sdist bdist_wheel 216 | $ exit 217 | $ docker-compose down 218 | ``` 219 | 220 | To publish built artefacts for this project manually to [PyPi testing](https://test.pypi.org): 221 | 222 | ```shell 223 | $ docker-compose run app ash 224 | $ python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 225 | # project then available at: https://test.pypi.org/project/flask-reverse-proxy-fix/ 226 | $ exit 227 | $ docker-compose down 228 | ``` 229 | 230 | To publish manually to [PyPi](https://pypi.org): 231 | 232 | ```shell 233 | $ docker-compose run app ash 234 | $ python -m twine upload --repository-url https://pypi.org/legacy/ dist/* 235 | # project then available at: https://pypi.org/project/flask-reverse-proxy-fix/ 236 | $ exit 237 | $ docker-compose down 238 | ``` 239 | 240 | ### Continuous Deployment 241 | 242 | A Continuous Deployment process using GitLab's CI/CD platform is configured in `.gitlab-ci.yml`. This will: 243 | 244 | * build the source and binary artefacts for this project 245 | * publish built artefacts for this project to the relevant PyPi repository 246 | 247 | This process will deploy changes to [PyPi testing](https://test.pypi.org) on all commits to the *master* branch. 248 | 249 | This process will deploy changes to [PyPi](https://pypi.org) on all tagged commits. 250 | 251 | ## Release procedure 252 | 253 | ### At release 254 | 255 | 1. create a `release` branch 256 | 2. ensure the version is bumped as per semver in `setup.py` 257 | 3. close release in `CHANGELOG.md` 258 | 4. push changes, merge the `release` branch into `master` and tag with version 259 | 260 | The project will be built and published to PyPi automatically through [Continuous Deployment](#continuous-deployment). 261 | 262 | ## Feedback 263 | 264 | The maintainer of this project is the BAS Web & Applications Team, they can be contacted at: 265 | [servicedesk@bas.ac.uk](mailto:servicedesk@bas.ac.uk). 266 | 267 | ## Issue tracking 268 | 269 | This project uses issue tracking, see the 270 | [Issue tracker](https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-reverse-proxy/issues) for more 271 | information. 272 | 273 | **Note:** Read & write access to this issue tracker is restricted. Contact the project maintainer to request access. 274 | 275 | ## License 276 | 277 | © UK Research and Innovation (UKRI), 2019, British Antarctic Survey. 278 | 279 | You may use and re-use this software and associated documentation files free of charge in any format or medium, under 280 | the terms of the Open Government Licence v3.0. 281 | 282 | You may obtain a copy of the Open Government Licence at http://www.nationalarchives.gov.uk/doc/open-government-licence/ 283 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, url_for 2 | from flask_reverse_proxy_fix.middleware import ReverseProxyPrefixFix 3 | 4 | 5 | def create_app_with_middleware(): 6 | app = Flask(__name__) 7 | 8 | # Configure and load Middleware 9 | app.config['REVERSE_PROXY_PATH'] = '/test' 10 | ReverseProxyPrefixFix(app) 11 | 12 | @app.route("/sample") 13 | def sample(): 14 | payload = { 15 | 'links': { 16 | 'self': url_for('.sample', _external=True) 17 | } 18 | } 19 | 20 | return jsonify(payload) 21 | 22 | return app 23 | 24 | 25 | def create_app_without_middleware(): 26 | app = Flask(__name__) 27 | 28 | @app.route("/sample") 29 | def sample(): 30 | payload = { 31 | 'links': { 32 | 'self': url_for('.sample', _external=True) 33 | } 34 | } 35 | 36 | return jsonify(payload) 37 | 38 | return app 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | 4 | services: 5 | app: 6 | build: . 7 | image: docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-reverse-proxy:0.1.0-alpine 8 | command: flask run --host 0.0.0.0 --port 9000 9 | volumes: 10 | - .:/usr/src/app 11 | ports: 12 | - 9000:9000 13 | -------------------------------------------------------------------------------- /flask_reverse_proxy_fix/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antarctica/flask-reverse-proxy-fix/73178f0fe84816674c447615043bfce07c6f4924/flask_reverse_proxy_fix/__init__.py -------------------------------------------------------------------------------- /flask_reverse_proxy_fix/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask as App 2 | # noinspection PyPackageRequirements 3 | from werkzeug.contrib.fixers import ProxyFix 4 | 5 | 6 | class ReverseProxyPrefixFix(object): 7 | """ 8 | Flask middleware to ensure correct URLs are generated by Flask.url_for() where an application is under a reverse 9 | proxy. Specifically this middleware corrects URLs where a common prefix needs to be added to all URLs. 10 | 11 | For example: If client requests for an application are reverse proxied such that: 12 | `example.com/some-service/v1/foo` becomes `some-service-v1.internal/foo`, where `/foo` is a route within a Flask 13 | application `foo()`. 14 | 15 | Without this middleware, a call to `Flask.url_for('.foo')` would give: `/foo`. If returned to the client, as a 16 | 'self' link for example, this would cause a request to `example.com/foo`, which would be invalid as the 17 | `/some-service/v1` prefix is missing. 18 | 19 | With this middleware, a call to `Flask.url_for('.foo')` would give: '/some-service/v1/foo', which will work if used 20 | by a client. 21 | 22 | This middleware is compatible with both relative and absolute URLs (i.e. `Flask.url_for('.foo')` and 23 | `Flask.url_for('.foo', _external=True)`. 24 | 25 | This middleware incorporates the `werkzeug.contrib.fixers.ProxyFix` middleware [1] and is based on the 26 | 'Fixing SCRIPT_NAME/url_scheme when behind reverse proxy' Flask snippet [2]. 27 | 28 | Note: Ensure the prefix value includes a preceding slash, but not a trailing slash (i.e. use `/foo` not `/foo/`). 29 | 30 | [1] http://werkzeug.pocoo.org/docs/0.14/contrib/fixers/#werkzeug.contrib.fixers.ProxyFix 31 | [2] http://flask.pocoo.org/snippets/35/ 32 | """ 33 | def __init__(self, app: App): 34 | """ 35 | :type app: App 36 | :param app: Flask application 37 | """ 38 | self.app = app.wsgi_app 39 | self.prefix = None 40 | 41 | if 'REVERSE_PROXY_PATH' in app.config: 42 | self.prefix = app.config['REVERSE_PROXY_PATH'] 43 | 44 | self.app = ProxyFix(self.app) 45 | 46 | app.wsgi_app = self 47 | 48 | def __call__(self, environ, start_response): 49 | if self.prefix is not None: 50 | environ['SCRIPT_NAME'] = self.prefix 51 | path_info = environ['PATH_INFO'] 52 | if path_info.startswith(self.prefix): 53 | environ['PATH_INFO'] = path_info[len(self.prefix):] 54 | 55 | return self.app(environ, start_response) 56 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from app import create_app_with_middleware 5 | 6 | app = create_app_with_middleware() 7 | 8 | 9 | # Support running integration tests 10 | @app.cli.command() 11 | def test(): 12 | """Run integration tests.""" 13 | tests = unittest.TestLoader().discover(os.path.join(os.path.dirname(__file__), 'tests')) 14 | unittest.TextTestRunner(verbosity=2).run(tests) 15 | 16 | 17 | # Support PyCharm debugging 18 | if 'PYCHARM_HOSTED' in os.environ: 19 | # Exempting Bandit security issue (binding to all network interfaces) 20 | # 21 | # All interfaces option used because the network available within the container can vary across providers 22 | # This is only used when debugging with PyCharm. A standalone web server is used in production. 23 | app.run(host='0.0.0.0', port=9000, debug=True, use_debugger=False, use_reloader=False) # nosec 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bandit==1.5.1 2 | flake8==3.6.0 3 | Flask==1.0.2 4 | setuptools==40.8.0 5 | twine==1.13.0 6 | wheel==0.33.1 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | version = '0.2.1' 9 | 10 | # If a tagged commit, don't make a pre-release 11 | if 'CI_COMMIT_TAG' not in os.environ: 12 | version = f"{ version }.dev{ os.getenv('CI_PIPELINE_ID') or None }" 13 | 14 | setup( 15 | name="flask-reverse-proxy-fix", 16 | version=version, 17 | author="British Antarctic Survey", 18 | author_email="webapps@bas.ac.uk", 19 | description="Python Flask middleware for applications running under a reverse proxy", 20 | long_description=long_description, 21 | long_description_content_type="text/markdown", 22 | url="https://github.com/antarctica/flask-reverse-proxy-fix", 23 | license='Open Government Licence v3.0', 24 | install_requires=['flask'], 25 | packages=find_packages(exclude=['tests']), 26 | classifiers=[ 27 | "Programming Language :: Python :: 3", 28 | "Framework :: Flask", 29 | "Development Status :: 5 - Production/Stable", 30 | "License :: Other/Proprietary License", 31 | "Operating System :: OS Independent", 32 | "Intended Audience :: Developers" 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antarctica/flask-reverse-proxy-fix/73178f0fe84816674c447615043bfce07c6f4924/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_flask_reverse_proxy_middleware_path_prefix.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from http import HTTPStatus 3 | 4 | from flask_reverse_proxy_fix.middleware import ReverseProxyPrefixFix 5 | from app import create_app_with_middleware, create_app_without_middleware 6 | 7 | 8 | class FlaskReverseProxyMiddlewarePathPrefixTestCase(unittest.TestCase): 9 | def setUp(self): 10 | pass 11 | 12 | def tearDown(self): 13 | self.app_context.pop() 14 | 15 | def test_with_prefix(self): 16 | self.app = create_app_with_middleware() 17 | self.app_context = self.app.app_context() 18 | self.app_context.push() 19 | self.client = self.app.test_client() 20 | 21 | self.app.config['REVERSE_PROXY_PATH'] = '/foo' 22 | ReverseProxyPrefixFix(self.app) 23 | 24 | expected_url = 'http://localhost:9000/test/sample' 25 | 26 | response = self.client.get( 27 | '/sample', 28 | base_url='http://localhost:9000' 29 | ) 30 | json_response = response.get_json() 31 | self.assertEqual(response.status_code, HTTPStatus.OK) 32 | self.assertIn('links', json_response.keys()) 33 | self.assertIn('self', json_response['links'].keys()) 34 | self.assertEqual(expected_url, json_response['links']['self']) 35 | 36 | def test_without_prefix(self): 37 | self.app = create_app_without_middleware() 38 | self.app_context = self.app.app_context() 39 | self.app_context.push() 40 | self.client = self.app.test_client() 41 | 42 | expected_url = 'http://localhost:9000/sample' 43 | 44 | response = self.client.get( 45 | '/sample', 46 | base_url='http://localhost:9000' 47 | ) 48 | json_response = response.get_json() 49 | self.assertEqual(response.status_code, HTTPStatus.OK) 50 | self.assertIn('links', json_response.keys()) 51 | self.assertIn('self', json_response['links'].keys()) 52 | self.assertEqual(expected_url, json_response['links']['self']) 53 | --------------------------------------------------------------------------------