Skip to content

Commit e9d11e9

Browse files
authored
Merge pull request #1 from moreonion/first-version
First version, mostly copied from moflask
2 parents 368db0a + d9b9b21 commit e9d11e9

8 files changed

Lines changed: 321 additions & 12 deletions

File tree

.github/workflows/test.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Run tests
2+
3+
on:
4+
push:
5+
branches:
6+
- "main"
7+
pull_request:
8+
branches:
9+
- "main"
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
15+
strategy:
16+
matrix:
17+
# Run in all these versions of Python
18+
python-version: ["3.11", "3.13"]
19+
20+
steps:
21+
# Checkout the latest code from the repo
22+
- name: Checkout repo
23+
uses: actions/checkout@v3
24+
# Setup which version of Python to use
25+
- name: Set Up Python ${{ matrix.python-version }}
26+
uses: actions/setup-python@v4
27+
with:
28+
python-version: ${{ matrix.python-version }}
29+
cache: pip
30+
# Display the Python version being used
31+
- name: Display Python version
32+
run: python -c "import sys; print(sys.version)"
33+
# Install the package.
34+
- name: Install package
35+
run: pip install -r requirements-dev.txt; pip install -e .
36+
- name: Run tests
37+
run: pytest
38+
- name: Run linters
39+
uses: pre-commit/action@v3.0.0

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# WSGI Proxyfix
2+
3+
This is a modified version of the ProxyFix found in werkzeug. Instead of counting the number of trusted proxies it peels away all the “trusted” IP-addresses until it arrives at the first untrusted one.
4+
5+
6+
## Usage
7+
8+
```python
9+
import proxyfix from impact_stack
10+
11+
# Flask
12+
app.wsgi_app = proxyfix.ProxyFix.from_config(app.config.get).wrap(app.wsgi_app)
13+
14+
# Django
15+
import functools
16+
from django.conf import settings
17+
from django.core.wsgi import wsgi_application
18+
19+
application = get_wsgi_application()
20+
config_getter = functools.partial(getattr, settings)
21+
application = proxyfix.ProxyFix.from_config(config_getter).wrap(application)
22+
```

impact_stack/proxyfix/__init__.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,92 @@
1-
"""Boilerplate project module."""
1+
"""WSGI ProxyFix middleware."""
22

3-
HELLO = "hello world"
3+
import typing as t
4+
from ipaddress import ip_address
5+
6+
7+
def _split(string):
8+
return string.split(",") if string else []
9+
10+
11+
class ProxyFix:
12+
"""This is a slightly modified version of werkzeug's ProxyFix.
13+
14+
Instead of using a fixed number of proxies it uses a list of trusted IP
15+
addresses.
16+
17+
This middleware can be applied to add HTTP proxy support to an
18+
application that was not designed with HTTP proxies in mind. It
19+
sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers.
20+
21+
The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in
22+
the WSGI environment as `werkzeug.proxy_fix.orig_remote_addr` and
23+
`werkzeug.proxy_fix.orig_http_host`.
24+
25+
Args:
26+
proxies: List of IP-addreses which’s X-Forwarded headers should be trusted.
27+
"""
28+
29+
@classmethod
30+
def from_config(cls, config_getter: t.Callable[[str, t.Any], t.Any]):
31+
"""Create a new instance from a config getter function."""
32+
return cls(config_getter("PROXYFIX_TRUSTED", ["127.0.0.1"]))
33+
34+
def __init__(self, proxies: t.Iterable[str]):
35+
"""Create a new instance by passing the list of trusted proxies."""
36+
self.trusted = frozenset(ip_address(p.strip()) for p in proxies)
37+
38+
def get_remote_addr(self, forwarded_for: list[str]):
39+
"""Select the first “untrusted” remote addr.
40+
41+
Values to X-Forwarded-For are expected to be appended so the inner proxy layers are to the
42+
right. The innermost untrusted IP is returned.
43+
"""
44+
previous = None
45+
for ip_str in reversed(forwarded_for):
46+
ip_str = ip_str.strip()
47+
try:
48+
if ip_address(ip_str) not in self.trusted:
49+
return ip_str
50+
except ValueError:
51+
return previous
52+
previous = ip_str
53+
return previous
54+
55+
def update_environ(self, environ):
56+
"""Update the WSGI environment according to the headers."""
57+
env = environ.get
58+
remote_addr = env("REMOTE_ADDR")
59+
if not remote_addr:
60+
return
61+
62+
try:
63+
remote_addr_ip = ip_address(remote_addr)
64+
except ValueError:
65+
remote_addr_ip = ip_address("127.0.0.1")
66+
67+
environ.update(
68+
{
69+
"werkzeug.proxy_fix.orig_wsgi_url_scheme": env("wsgi.url_scheme"),
70+
"werkzeug.proxy_fix.orig_remote_addr": env("REMOTE_ADDR"),
71+
"werkzeug.proxy_fix.orig_http_host": env("HTTP_HOST"),
72+
}
73+
)
74+
75+
if remote_addr_ip in self.trusted:
76+
if forwarded_host := env("HTTP_X_FORWARDED_HOST", ""):
77+
environ["HTTP_HOST"] = forwarded_host
78+
if forwarded_proto := env("HTTP_X_FORWARDED_PROTO", ""):
79+
https = "https" in forwarded_proto.lower()
80+
environ["wsgi.url_scheme"] = "https" if https else "http"
81+
forwarded_for = _split(env("HTTP_X_FORWARDED_FOR", ""))
82+
if remote_addr := self.get_remote_addr(forwarded_for):
83+
environ["REMOTE_ADDR"] = remote_addr
84+
85+
def wrap(self, wsgi_app):
86+
"""Wrap a wsgi app with this middleware."""
87+
88+
def wrapped(environ, start_response):
89+
self.update_environ(environ)
90+
return wsgi_app(environ, start_response)
91+
92+
return wrapped

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dev = [
1212
"pylint",
1313
"pytest",
1414
"pytest-cov",
15+
"twine",
1516
]
1617

1718
[build-system]

requirements-dev.txt

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,79 @@
66
#
77
astroid==3.3.11
88
# via pylint
9+
backports-tarfile==1.2.0
10+
# via jaraco-context
911
black==25.1.0
1012
# via impact-stack-proxyfix (pyproject.toml)
13+
certifi==2025.8.3
14+
# via requests
15+
cffi==1.17.1
16+
# via cryptography
1117
cfgv==3.4.0
1218
# via pre-commit
19+
charset-normalizer==3.4.3
20+
# via requests
1321
click==8.2.1
1422
# via black
1523
coverage[toml]==7.9.2
1624
# via pytest-cov
25+
cryptography==45.0.6
26+
# via secretstorage
1727
dill==0.4.0
1828
# via pylint
1929
distlib==0.3.9
2030
# via virtualenv
31+
docutils==0.22
32+
# via readme-renderer
2133
filelock==3.18.0
2234
# via virtualenv
35+
id==1.5.0
36+
# via twine
2337
identify==2.6.12
2438
# via pre-commit
39+
idna==3.10
40+
# via requests
41+
importlib-metadata==8.7.0
42+
# via keyring
2543
iniconfig==2.1.0
2644
# via pytest
2745
isort==6.0.1
2846
# via
2947
# impact-stack-proxyfix (pyproject.toml)
3048
# pylint
49+
jaraco-classes==3.4.0
50+
# via keyring
51+
jaraco-context==6.0.1
52+
# via keyring
53+
jaraco-functools==4.3.0
54+
# via keyring
55+
jeepney==0.9.0
56+
# via
57+
# keyring
58+
# secretstorage
59+
keyring==25.6.0
60+
# via twine
61+
markdown-it-py==4.0.0
62+
# via rich
3163
mccabe==0.7.0
3264
# via pylint
65+
mdurl==0.1.2
66+
# via markdown-it-py
67+
more-itertools==10.7.0
68+
# via
69+
# jaraco-classes
70+
# jaraco-functools
3371
mypy-extensions==1.1.0
3472
# via black
73+
nh3==0.3.0
74+
# via readme-renderer
3575
nodeenv==1.9.1
3676
# via pre-commit
3777
packaging==25.0
3878
# via
3979
# black
4080
# pytest
81+
# twine
4182
pathspec==0.12.1
4283
# via black
4384
platformdirs==4.3.8
@@ -51,8 +92,13 @@ pluggy==1.6.0
5192
# pytest-cov
5293
pre-commit==4.2.0
5394
# via impact-stack-proxyfix (pyproject.toml)
95+
pycparser==2.22
96+
# via cffi
5497
pygments==2.19.2
55-
# via pytest
98+
# via
99+
# pytest
100+
# readme-renderer
101+
# rich
56102
pylint==3.3.7
57103
# via impact-stack-proxyfix (pyproject.toml)
58104
pytest==8.4.1
@@ -63,7 +109,30 @@ pytest-cov==6.2.1
63109
# via impact-stack-proxyfix (pyproject.toml)
64110
pyyaml==6.0.2
65111
# via pre-commit
112+
readme-renderer==44.0
113+
# via twine
114+
requests==2.32.5
115+
# via
116+
# id
117+
# requests-toolbelt
118+
# twine
119+
requests-toolbelt==1.0.0
120+
# via twine
121+
rfc3986==2.0.0
122+
# via twine
123+
rich==14.1.0
124+
# via twine
125+
secretstorage==3.3.3
126+
# via keyring
66127
tomlkit==0.13.3
67128
# via pylint
129+
twine==6.1.0
130+
# via impact-stack-proxyfix (pyproject.toml)
131+
urllib3==2.5.0
132+
# via
133+
# requests
134+
# twine
68135
virtualenv==20.31.2
69136
# via pre-commit
137+
zipp==3.23.0
138+
# via importlib-metadata

tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""Tests for the boilerplate_project."""
1+
"""Tests for the impact-stack-rest library."""

tests/example_test.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)