Skip to content

Commit 514d260

Browse files
committed
improve subdomain and host matching
1 parent 7868bef commit 514d260

File tree

2 files changed

+68
-43
lines changed

2 files changed

+68
-43
lines changed

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Version 3.2.0
55

66
Unreleased
77

8+
- ``Map`` takes a ``subdomain_matching`` parameter to disable subdomain
9+
matching. In ``bind_to_environ``, the ``server_name`` parameter is not used
10+
if ``host_matching`` is enabled. If ``default_subdomain`` is set, it is used
11+
if a subdomain could not be determined. :issue:`3005`
12+
813

914
Version 3.1.3
1015
-------------

src/werkzeug/routing/map.py

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ class Map:
4646
arguments besides the `rules` as keyword arguments!
4747
4848
:param rules: sequence of url rules for this map.
49-
:param default_subdomain: The default subdomain for rules without a
50-
subdomain defined.
49+
:param default_subdomain: The default subdomain used by
50+
:meth:`bind_to_environ` and :meth:`bind` if the current subdomain can't
51+
be determined.
5152
:param strict_slashes: If a rule ends with a slash but the matched
5253
URL does not, redirect to the URL with a trailing slash.
5354
:param merge_slashes: Merge consecutive slashes when matching or
@@ -62,10 +63,16 @@ class Map:
6263
:param sort_parameters: If set to `True` the url parameters are sorted.
6364
See `url_encode` for more details.
6465
:param sort_key: The sort key function for `url_encode`.
65-
:param host_matching: if set to `True` it enables the host matching
66-
feature and disables the subdomain one. If
67-
enabled the `host` parameter to rules is used
68-
instead of the `subdomain` one.
66+
:param host_matching: Whether to route based on the full ``Host`` rather
67+
than subdomains of ``server_name``. Uses the ``host`` parameter for
68+
each :class:`Rule`.
69+
:param subdomain_matching: Whether to detect the subdomain from ``Host`` and
70+
``server_name``, and route based on it. Uses the ``subdomain`` parameter
71+
of each :class:`Rule`. Enabled by default, but always disabled if
72+
``host_matching`` is enabled.
73+
74+
.. versionchanged:: 3.1
75+
The ``subdomain_matching`` parameter was added.
6976
7077
.. versionchanged:: 3.0
7178
The ``charset`` and ``encoding_errors`` parameters were removed.
@@ -102,6 +109,8 @@ def __init__(
102109
sort_parameters: bool = False,
103110
sort_key: t.Callable[[t.Any], t.Any] | None = None,
104111
host_matching: bool = False,
112+
*,
113+
subdomain_matching: bool = True,
105114
) -> None:
106115
self._matcher = StateMachineMatcher(merge_slashes)
107116
self._rules_by_endpoint: dict[t.Any, list[Rule]] = {}
@@ -111,6 +120,7 @@ def __init__(
111120
self.default_subdomain = default_subdomain
112121
self.strict_slashes = strict_slashes
113122
self.redirect_defaults = redirect_defaults
123+
self.subdomain_matching = subdomain_matching and not host_matching
114124
self.host_matching = host_matching
115125

116126
self.converters = self.default_converters.copy()
@@ -255,42 +265,52 @@ def bind_to_environ(
255265
server_name: str | None = None,
256266
subdomain: str | None = None,
257267
) -> MapAdapter:
258-
"""Like :meth:`bind` but you can pass it an WSGI environment and it
259-
will fetch the information from that dictionary. Note that because of
260-
limitations in the protocol there is no way to get the current
261-
subdomain and real `server_name` from the environment. If you don't
262-
provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or
263-
`HTTP_HOST` if provided) as used `server_name` with disabled subdomain
264-
feature.
265-
266-
If `subdomain` is `None` but an environment and a server name is
267-
provided it will calculate the current subdomain automatically.
268-
Example: `server_name` is ``'example.com'`` and the `SERVER_NAME`
269-
in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated
270-
subdomain will be ``'staging.dev'``.
271-
272-
If the object passed as environ has an environ attribute, the value of
273-
this attribute is used instead. This allows you to pass request
274-
objects. Additionally `PATH_INFO` added as a default of the
275-
:class:`MapAdapter` so that you don't have to pass the path info to
276-
the match method.
268+
"""Call :meth:`bind` with information from the WSGI environ.
269+
``PATH_INFO`` is used so it doesn't need to be passed to
270+
:meth:`~.MapAdapter.match`.
271+
272+
The WSGI environ does not have information to determine what subdomain
273+
was accessed, so ``server_name`` or ``subdomain`` must be passed in for
274+
:attr:`subdomain_matching`. For example, if ``Host`` is
275+
``abc.example.test`` and ``server_name`` is ``example.test``,
276+
``subdomain`` is determined to be ``abc``. If the ``server_name`` is not
277+
a suffix of the current ``Host``, then :attr:`default_subdomain` or
278+
``"<invalid>"`` is used.
279+
280+
:param environ: The WSGI environ for the request. Can also be a
281+
``Request`` with an ``environ`` attribute.
282+
:param server_name: When subdomain matching is enabled and ``subdomain``
283+
is not given, the subdomain is determined by removing this
284+
``host:port`` as a suffix from the request's ``Host``. If the scheme
285+
is ``http``, ``https``, ``ws``, or ``wss``, the corresponding port
286+
80 or 443 will be removed.
287+
:param subdomain: Route using this subdomain rather than determining it
288+
using ``server_name``.
289+
290+
.. versionchanged:: 3.2
291+
If ``server_name`` is not a suffix of ``Host``,
292+
:attr:`default_subdomain` is used if set, rather than always
293+
``"<invalid>"``.
294+
295+
.. versionchanged:: 3.2
296+
``subdomain_matching`` can be disabled.
297+
298+
.. versionchanged:: 3.2
299+
``server_name`` is ignored if ``host_matching`` is enabled.
277300
278301
.. versionchanged:: 1.0.0
279-
If the passed server name specifies port 443, it will match
280-
if the incoming scheme is ``https`` without a port.
302+
If ``server_name`` specifies port 443, it will match if the scheme
303+
is ``https`` and ``Host`` does not specify a port.
281304
282-
.. versionchanged:: 1.0.0
283-
A warning is shown when the passed server name does not
284-
match the incoming WSGI server name.
305+
.. versionchanged:: 1.0
306+
A warning is shown when ``server_name`` is not a suffix of ``Host``.
285307
286308
.. versionchanged:: 0.8
287-
This will no longer raise a ValueError when an unexpected server
288-
name was passed.
309+
``"<invalid>"`` is used as the subdomain if ``server_name`` is not a
310+
suffix of ``Host``, rather than raising ``ValueError``.
289311
290312
.. versionchanged:: 0.5
291-
previously this method accepted a bogus `calculate_subdomain`
292-
parameter that did not have any effect. It was removed because
293-
of that.
313+
Removed the ``calculate_subdomain`` parameter which was not used.
294314
295315
:param environ: a WSGI environment.
296316
:param server_name: an optional server name hint (see above).
@@ -307,7 +327,7 @@ def bind_to_environ(
307327
if upgrade and env.get("HTTP_UPGRADE", "").lower() == "websocket":
308328
scheme = "wss" if scheme == "https" else "ws"
309329

310-
if server_name is None:
330+
if server_name is None or self.host_matching:
311331
server_name = wsgi_server_name
312332
else:
313333
server_name = server_name.lower()
@@ -318,24 +338,24 @@ def bind_to_environ(
318338
elif scheme in {"https", "wss"} and server_name.endswith(":443"):
319339
server_name = server_name[:-4]
320340

321-
if subdomain is None and not self.host_matching:
341+
if subdomain is None and self.subdomain_matching and not self.host_matching:
322342
cur_server_name = wsgi_server_name.split(".")
323343
real_server_name = server_name.split(".")
324344
offset = -len(real_server_name)
325345

326346
if cur_server_name[offset:] != real_server_name:
327-
# This can happen even with valid configs if the server was
328-
# accessed directly by IP address under some situations.
329-
# Instead of raising an exception like in Werkzeug 0.7 or
330-
# earlier we go by an invalid subdomain which will result
331-
# in a 404 error on matching.
347+
# Host does not have server_name as a suffix. This can happen if
348+
# the server was accessed by IP, or other names point to it in
349+
# DNS. Use a placeholder subdomain, which can result in a 404 on
350+
# matching or be detected in a wildcard endpoint.
332351
warnings.warn(
333352
f"Current server name {wsgi_server_name!r} doesn't match configured"
334353
f" server name {server_name!r}",
335354
stacklevel=2,
336355
)
337-
subdomain = "<invalid>"
356+
subdomain = self.default_subdomain or "<invalid>"
338357
else:
358+
# Remove server_name as a suffix from Host to get the subdomain.
339359
subdomain = ".".join(filter(None, cur_server_name[:offset]))
340360

341361
def _get_wsgi_string(name: str) -> str | None:

0 commit comments

Comments
 (0)