Skip to content

Commit 02d6bef

Browse files
authored
Merge branch 'main' into dependabot/pip/requirements/certifi-2024.7.4
2 parents c3c3d5e + 2f07e43 commit 02d6bef

File tree

8 files changed

+223
-5
lines changed

8 files changed

+223
-5
lines changed

docs/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ Name Description De
5555
==================================== ===================================== ==========================
5656
``DEBUG_TB_ENABLED`` Enable the toolbar? ``app.debug``
5757
``DEBUG_TB_HOSTS`` Whitelist of hosts to display toolbar any host
58+
``DEBUG_TB_ROUTES_HOST`` The host to associate with toolbar ``None``
59+
routes (where its assets are served
60+
from), or the sentinel value `*` to
61+
serve from the same host as the
62+
current request (ie any host). This
63+
is only required if Flask is
64+
configured to use `host_matching`.
5865
``DEBUG_TB_INTERCEPT_REDIRECTS`` Should intercept redirects? ``True``
5966
``DEBUG_TB_PANELS`` List of module/class names of panels enable all built-in panels
6067
``DEBUG_TB_PROFILER_ENABLED`` Enable the profiler on all requests ``False``, user-enabled

requirements/build.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ pyproject-hooks==1.1.0
1414
# via build
1515
tomli==2.0.1
1616
# via build
17-
zipp==3.18.1
17+
zipp==3.19.1
1818
# via importlib-metadata

requirements/dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ werkzeug==3.0.3
245245
# -r tests.txt
246246
# -r typing.txt
247247
# flask
248-
zipp==3.18.1
248+
zipp==3.19.1
249249
# via
250250
# -r docs.txt
251251
# -r tests.txt

requirements/docs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,5 @@ sphinxcontrib-serializinghtml==1.1.5
5959
# via sphinx
6060
urllib3==2.2.2
6161
# via requests
62-
zipp==3.18.1
62+
zipp==3.19.1
6363
# via importlib-metadata

requirements/tests.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@ typing-extensions==4.11.0
4444
# via sqlalchemy
4545
werkzeug==3.0.3
4646
# via flask
47-
zipp==3.18.1
47+
zipp==3.19.1
4848
# via importlib-metadata

requirements/typing.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ typing-extensions==4.11.0
6060
# sqlalchemy
6161
werkzeug==3.0.3
6262
# via flask
63-
zipp==3.18.1
63+
zipp==3.19.1
6464
# via importlib-metadata
6565

6666
# The following packages are considered to be unsafe in a requirements file:

src/flask_debugtoolbar/__init__.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class DebugToolbarExtension:
5959

6060
def __init__(self, app: Flask | None = None) -> None:
6161
self.app = app
62+
self.toolbar_routes_host: str | None = None
63+
6264
# Support threads running `flask.copy_current_request_context` without
6365
# poping toolbar during `teardown_request`
6466
self.debug_toolbars_var: ContextVar[dict[Request, DebugToolbar]] = ContextVar(
@@ -97,6 +99,8 @@ def init_app(self, app: Flask) -> None:
9799
"var to be set"
98100
)
99101

102+
self._validate_and_configure_toolbar_routes_host(app)
103+
100104
DebugToolbar.load_panels(app)
101105

102106
app.before_request(self.process_request)
@@ -110,6 +114,7 @@ def init_app(self, app: Flask) -> None:
110114
"/_debug_toolbar/static/<path:filename>",
111115
"_debug_toolbar.static",
112116
self.send_static_file,
117+
host=self.toolbar_routes_host,
113118
)
114119

115120
app.register_blueprint(module, url_prefix="/_debug_toolbar/views")
@@ -118,6 +123,7 @@ def _default_config(self, app: Flask) -> dict[str, t.Any]:
118123
return {
119124
"DEBUG_TB_ENABLED": app.debug,
120125
"DEBUG_TB_HOSTS": (),
126+
"DEBUG_TB_ROUTES_HOST": None,
121127
"DEBUG_TB_INTERCEPT_REDIRECTS": True,
122128
"DEBUG_TB_PANELS": (
123129
"flask_debugtoolbar.panels.versions.VersionDebugPanel",
@@ -135,6 +141,61 @@ def _default_config(self, app: Flask) -> dict[str, t.Any]:
135141
"SQLALCHEMY_RECORD_QUERIES": app.debug,
136142
}
137143

144+
def _validate_and_configure_toolbar_routes_host(self, app: Flask) -> None:
145+
toolbar_routes_host = app.config["DEBUG_TB_ROUTES_HOST"]
146+
if app.url_map.host_matching and not toolbar_routes_host:
147+
import warnings
148+
149+
warnings.warn(
150+
"Flask-DebugToolbar requires DEBUG_TB_ROUTES_HOST to be set if Flask "
151+
"is running in `host_matching` mode. Static assets for the toolbar "
152+
"will not be served correctly unless this is set.",
153+
stacklevel=1,
154+
)
155+
156+
if toolbar_routes_host:
157+
if not app.url_map.host_matching:
158+
raise ValueError(
159+
"`DEBUG_TB_ROUTES_HOST` should only be set if your Flask app is "
160+
"using `host_matching`."
161+
)
162+
163+
if toolbar_routes_host.strip() == "*":
164+
toolbar_routes_host = "<toolbar_routes_host>"
165+
elif "<" in toolbar_routes_host and ">" in toolbar_routes_host:
166+
raise ValueError(
167+
"`DEBUG_TB_ROUTES_HOST` must either be a host name with no "
168+
"variables, to serve all Flask-DebugToolbar assets from a single "
169+
"host, or `*` to match the current request's host."
170+
)
171+
172+
# Automatically inject `toolbar_routes_host` into `url_for` calls for
173+
# the toolbar's `send_static_file` method.
174+
@app.url_defaults
175+
def inject_toolbar_routes_host_if_required(
176+
endpoint: str, values: dict[str, t.Any]
177+
) -> None:
178+
if app.url_map.is_endpoint_expecting(endpoint, "toolbar_routes_host"):
179+
values.setdefault("toolbar_routes_host", request.host)
180+
181+
# Automatically strip `toolbar_routes_host` from the endpoint values so
182+
# that the `send_static_host` method doesn't receive that parameter,
183+
# as it's not actually required internally.
184+
@app.url_value_preprocessor
185+
def strip_toolbar_routes_host_from_static_endpoint(
186+
endpoint: str | None, values: dict[str, t.Any] | None
187+
) -> None:
188+
if (
189+
endpoint
190+
and values
191+
and app.url_map.is_endpoint_expecting(
192+
endpoint, "toolbar_routes_host"
193+
)
194+
):
195+
values.pop("toolbar_routes_host", None)
196+
197+
self.toolbar_routes_host = toolbar_routes_host
198+
138199
def dispatch_request(self) -> t.Any:
139200
"""Modified version of ``Flask.dispatch_request`` to call
140201
:meth:`process_view`.

tests/test_toolbar.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from __future__ import annotations
22

3+
import typing as t
4+
from unittest.mock import MagicMock
5+
from unittest.mock import patch
6+
7+
import pytest
38
from flask import Flask
9+
from flask import Response
410
from flask.testing import FlaskClient
511

12+
from flask_debugtoolbar import DebugToolbarExtension
13+
614

715
def load_app(name: str) -> FlaskClient:
816
app: Flask = __import__(name).app
@@ -15,3 +23,145 @@ def test_basic_app() -> None:
1523
index = app.get("/")
1624
assert index.status_code == 200
1725
assert b'<div id="flDebug"' in index.data
26+
27+
28+
def app_with_config(
29+
app_config: dict[str, t.Any], toolbar_config: dict[str, t.Any]
30+
) -> Flask:
31+
app = Flask(__name__, **app_config)
32+
app.config["DEBUG"] = True
33+
app.config["SECRET_KEY"] = "abc123"
34+
35+
for key, value in toolbar_config.items():
36+
app.config[key] = value
37+
38+
DebugToolbarExtension(app)
39+
40+
return app
41+
42+
43+
def test_toolbar_is_host_matching_but_flask_is_not() -> None:
44+
with pytest.raises(ValueError) as e:
45+
app_with_config(
46+
app_config=dict(host_matching=False),
47+
toolbar_config=dict(
48+
DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST="myapp.com"
49+
),
50+
)
51+
52+
assert str(e.value) == (
53+
"`DEBUG_TB_ROUTES_HOST` should only be set if your Flask app is "
54+
"using `host_matching`."
55+
)
56+
57+
58+
def test_flask_is_host_matching_but_toolbar_is_not() -> None:
59+
with pytest.warns(UserWarning) as record:
60+
app_with_config(
61+
app_config=dict(host_matching=True, static_host="static.com"),
62+
toolbar_config=dict(DEBUG_TB_ENABLED=True),
63+
)
64+
65+
assert isinstance(record[0].message, UserWarning)
66+
assert record[0].message.args[0] == (
67+
"Flask-DebugToolbar requires DEBUG_TB_ROUTES_HOST to be set if Flask "
68+
"is running in `host_matching` mode. Static assets for the toolbar "
69+
"will not be served correctly unless this is set."
70+
)
71+
72+
73+
def test_toolbar_host_variables_rejected() -> None:
74+
with pytest.raises(ValueError) as e:
75+
app_with_config(
76+
app_config=dict(host_matching=True, static_host="static.com"),
77+
toolbar_config=dict(
78+
DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST="<host>.com"
79+
),
80+
)
81+
82+
assert str(e.value) == (
83+
"`DEBUG_TB_ROUTES_HOST` must either be a host name with no "
84+
"variables, to serve all Flask-DebugToolbar assets from a single "
85+
"host, or `*` to match the current request's host."
86+
)
87+
88+
89+
def test_toolbar_in_host_mode_injects_toolbar_html() -> None:
90+
app = app_with_config(
91+
app_config=dict(host_matching=True, static_host="static.com"),
92+
toolbar_config=dict(DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST="myapp.com"),
93+
)
94+
95+
@app.route("/", host="myapp.com")
96+
def index() -> str:
97+
return "<html><head></head><body>OK</body></html>"
98+
99+
with app.test_client() as client:
100+
with app.app_context():
101+
response = client.get("/", headers={"Host": "myapp.com"})
102+
assert '<div id="flDebug" ' in response.text
103+
104+
105+
@pytest.mark.parametrize(
106+
"tb_routes_host, request_host, expected_static_path",
107+
(
108+
("myapp.com", "myapp.com", "/_debug_toolbar/static/"),
109+
("toolbar.com", "myapp.com", "http://toolbar.com/_debug_toolbar/static/"),
110+
("*", "myapp.com", "/_debug_toolbar/static/"),
111+
),
112+
)
113+
def test_toolbar_injects_expected_static_path_for_host(
114+
tb_routes_host: str, request_host: str, expected_static_path: str
115+
) -> None:
116+
app = app_with_config(
117+
app_config=dict(host_matching=True, static_host="static.com"),
118+
toolbar_config=dict(DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST=tb_routes_host),
119+
)
120+
121+
@app.route("/", host=request_host)
122+
def index() -> str:
123+
return "<html><head></head><body>OK</body></html>"
124+
125+
with app.test_client() as client:
126+
with app.app_context():
127+
response = client.get("/", headers={"Host": request_host})
128+
129+
assert (
130+
"""<script type="text/javascript">"""
131+
f"""var DEBUG_TOOLBAR_STATIC_PATH = '{expected_static_path}'"""
132+
"""</script>"""
133+
) in response.text
134+
135+
136+
@patch(
137+
"flask.helpers.werkzeug.utils.send_from_directory",
138+
return_value=Response(b"some-file", mimetype="text/css", status=200),
139+
)
140+
@pytest.mark.parametrize(
141+
"tb_routes_host, request_host, expected_status_code",
142+
(
143+
("toolbar.com", "toolbar.com", 200),
144+
("toolbar.com", "myapp.com", 404),
145+
("toolbar.com", "static.com", 404),
146+
("*", "toolbar.com", 200),
147+
("*", "myapp.com", 200),
148+
("*", "static.com", 200),
149+
),
150+
)
151+
def test_toolbar_serves_assets_based_on_host_configuration(
152+
mock_send_from_directory: MagicMock,
153+
tb_routes_host: str,
154+
request_host: str,
155+
expected_status_code: int,
156+
) -> None:
157+
app = app_with_config(
158+
app_config=dict(host_matching=True, static_host="static.com"),
159+
toolbar_config=dict(DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST=tb_routes_host),
160+
)
161+
162+
with app.test_client() as client:
163+
with app.app_context():
164+
response = client.get(
165+
"/_debug_toolbar/static/js/toolbar.js", headers={"Host": request_host}
166+
)
167+
assert response.status_code == expected_status_code

0 commit comments

Comments
 (0)