1 """Middleware to handle forwarded data by a reverse proxy."""
3 from __future__
import annotations
5 from collections.abc
import Awaitable, Callable
6 from ipaddress
import IPv4Network, IPv6Network, ip_address
9 from aiohttp.hdrs
import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
10 from aiohttp.web
import Application, HTTPBadRequest, Request, StreamResponse, middleware
11 from hass_nabucasa
import remote
15 _LOGGER = logging.getLogger(__name__)
21 use_x_forwarded_for: bool |
None,
22 trusted_proxies: list[IPv4Network | IPv6Network],
24 """Create forwarded middleware for the app.
26 Process IP addresses, proto and host information in the forwarded for headers.
28 `X-Forwarded-For: <client>, <proxy1>, <proxy2>`
29 e.g., `X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178`
31 We go through the list from the right side, and skip all entries that are in our
32 trusted proxies list. The first non-trusted IP is used as the client IP. If all
33 items in the X-Forwarded-For are trusted, including the most left item (client),
34 the most left item is used. In the latter case, the client connection originated
35 from an IP that is also listed as a trusted proxy IP or network.
37 `X-Forwarded-Proto: <client>, <proxy1>, <proxy2>`
38 e.g., `X-Forwarded-Proto: https, http, http`
39 OR `X-Forwarded-Proto: https` (one entry, even with multiple proxies)
41 The X-Forwarded-Proto is determined based on the corresponding entry of the
42 X-Forwarded-For header that is used/chosen as the client IP. However,
43 some proxies, for example, Kubernetes NGINX ingress, only retain one element
44 in the X-Forwarded-Proto header. In that case, we'll just use what we have.
46 `X-Forwarded-Host: <host>`
47 e.g., `X-Forwarded-Host: example.com`
49 If the previous headers are processed successfully, and the X-Forwarded-Host is
50 present, it will be used.
53 - If no X-Forwarded-For header is found, the processing of all headers is skipped.
54 - Throw HTTP 400 status when untrusted connected peer provides
55 X-Forwarded-For headers.
56 - If multiple instances of X-Forwarded-For, X-Forwarded-Proto or
57 X-Forwarded-Host are found, an HTTP 400 status code is thrown.
58 - If malformed or invalid (IP) data in X-Forwarded-For header is found,
59 an HTTP 400 status code is thrown.
60 - The connected client peer on the socket of the incoming connection,
61 must be trusted for any processing to take place.
62 - If the number of elements in X-Forwarded-Proto does not equal 1 or
63 is equal to the number of elements in X-Forwarded-For, an HTTP 400
64 status code is thrown.
65 - If an empty X-Forwarded-Host is provided, an HTTP 400 status code is thrown.
66 - If an empty X-Forwarded-Proto is provided, or an empty element in the list,
67 an HTTP 400 status code is thrown.
71 async
def forwarded_middleware(
72 request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
74 """Process forwarded data by a reverse proxy."""
76 if remote.is_cloud_request.get():
77 return await handler(request)
80 forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, [])
81 if not forwarded_for_headers:
83 return await handler(request)
87 request.transport
is None
88 or request.transport.get_extra_info(
"peername")
is None
91 return await handler(request)
93 connected_ip = ip_address(request.transport.get_extra_info(
"peername")[0])
96 if not use_x_forwarded_for:
99 "A request from a reverse proxy was received from %s, but your "
100 "HTTP integration is not set-up for reverse proxies"
107 if not any(connected_ip
in trusted_proxy
for trusted_proxy
in trusted_proxies):
109 "Received X-Forwarded-For header from an untrusted proxy %s",
115 if len(forwarded_for_headers) > 1:
117 "Too many headers for X-Forwarded-For: %s", forwarded_for_headers
122 forwarded_for_split =
list(reversed(forwarded_for_headers[0].split(
",")))
124 forwarded_for = [ip_address(addr.strip())
for addr
in forwarded_for_split]
125 except ValueError
as err:
127 "Invalid IP address in X-Forwarded-For: %s", forwarded_for_headers[0]
129 raise HTTPBadRequest
from err
131 overrides: dict[str, str] = {}
134 forwarded_for_index = 0
135 for forwarded_ip
in forwarded_for:
136 if any(forwarded_ip
in trusted_proxy
for trusted_proxy
in trusted_proxies):
137 forwarded_for_index += 1
139 overrides[
"remote"] =
str(forwarded_ip)
143 forwarded_for_index = -1
144 overrides[
"remote"] =
str(forwarded_for[-1])
147 forwarded_proto_headers: list[str] = request.headers.getall(
148 X_FORWARDED_PROTO, []
150 if forwarded_proto_headers:
151 if len(forwarded_proto_headers) > 1:
153 "Too many headers for X-Forward-Proto: %s", forwarded_proto_headers
157 forwarded_proto_split =
list(
158 reversed(forwarded_proto_headers[0].split(
","))
160 forwarded_proto = [proto.strip()
for proto
in forwarded_proto_split]
163 if "" in forwarded_proto:
165 "Empty item received in X-Forward-Proto header: %s",
166 forwarded_proto_headers[0],
172 if len(forwarded_proto)
not in (1, len(forwarded_for)):
175 "Incorrect number of elements in X-Forward-Proto. Expected 1 or"
179 len(forwarded_proto),
180 forwarded_proto_headers[0],
187 overrides[
"scheme"] = forwarded_proto[-1]
188 if len(forwarded_proto) != 1:
189 overrides[
"scheme"] = forwarded_proto[forwarded_for_index]
192 forwarded_host_headers: list[str] = request.headers.getall(X_FORWARDED_HOST, [])
193 if forwarded_host_headers:
195 if len(forwarded_host_headers) > 1:
197 "Too many headers for X-Forwarded-Host: %s", forwarded_host_headers
201 forwarded_host = forwarded_host_headers[0].strip()
202 if not forwarded_host:
203 _LOGGER.error(
"Empty value received in X-Forward-Host header")
206 overrides[
"host"] = forwarded_host
209 request = request.clone(**overrides)
210 return await handler(request)
212 app.middlewares.append(forwarded_middleware)
None async_setup_forwarded(Application app, bool|None use_x_forwarded_for, list[IPv4Network|IPv6Network] trusted_proxies)