]> jfr.im git - dlqueue.git/blob - venv/lib/python3.11/site-packages/werkzeug/middleware/http_proxy.py
init: venv aand flask
[dlqueue.git] / venv / lib / python3.11 / site-packages / werkzeug / middleware / http_proxy.py
1 """
2 Basic HTTP Proxy
3 ================
4
5 .. autoclass:: ProxyMiddleware
6
7 :copyright: 2007 Pallets
8 :license: BSD-3-Clause
9 """
10 from __future__ import annotations
11
12 import typing as t
13 from http import client
14 from urllib.parse import quote
15 from urllib.parse import urlsplit
16
17 from ..datastructures import EnvironHeaders
18 from ..http import is_hop_by_hop_header
19 from ..wsgi import get_input_stream
20
21 if t.TYPE_CHECKING:
22 from _typeshed.wsgi import StartResponse
23 from _typeshed.wsgi import WSGIApplication
24 from _typeshed.wsgi import WSGIEnvironment
25
26
27 class ProxyMiddleware:
28 """Proxy requests under a path to an external server, routing other
29 requests to the app.
30
31 This middleware can only proxy HTTP requests, as HTTP is the only
32 protocol handled by the WSGI server. Other protocols, such as
33 WebSocket requests, cannot be proxied at this layer. This should
34 only be used for development, in production a real proxy server
35 should be used.
36
37 The middleware takes a dict mapping a path prefix to a dict
38 describing the host to be proxied to::
39
40 app = ProxyMiddleware(app, {
41 "/static/": {
42 "target": "http://127.0.0.1:5001/",
43 }
44 })
45
46 Each host has the following options:
47
48 ``target``:
49 The target URL to dispatch to. This is required.
50 ``remove_prefix``:
51 Whether to remove the prefix from the URL before dispatching it
52 to the target. The default is ``False``.
53 ``host``:
54 ``"<auto>"`` (default):
55 The host header is automatically rewritten to the URL of the
56 target.
57 ``None``:
58 The host header is unmodified from the client request.
59 Any other value:
60 The host header is overwritten with the value.
61 ``headers``:
62 A dictionary of headers to be sent with the request to the
63 target. The default is ``{}``.
64 ``ssl_context``:
65 A :class:`ssl.SSLContext` defining how to verify requests if the
66 target is HTTPS. The default is ``None``.
67
68 In the example above, everything under ``"/static/"`` is proxied to
69 the server on port 5001. The host header is rewritten to the target,
70 and the ``"/static/"`` prefix is removed from the URLs.
71
72 :param app: The WSGI application to wrap.
73 :param targets: Proxy target configurations. See description above.
74 :param chunk_size: Size of chunks to read from input stream and
75 write to target.
76 :param timeout: Seconds before an operation to a target fails.
77
78 .. versionadded:: 0.14
79 """
80
81 def __init__(
82 self,
83 app: WSGIApplication,
84 targets: t.Mapping[str, dict[str, t.Any]],
85 chunk_size: int = 2 << 13,
86 timeout: int = 10,
87 ) -> None:
88 def _set_defaults(opts: dict[str, t.Any]) -> dict[str, t.Any]:
89 opts.setdefault("remove_prefix", False)
90 opts.setdefault("host", "<auto>")
91 opts.setdefault("headers", {})
92 opts.setdefault("ssl_context", None)
93 return opts
94
95 self.app = app
96 self.targets = {
97 f"/{k.strip('/')}/": _set_defaults(v) for k, v in targets.items()
98 }
99 self.chunk_size = chunk_size
100 self.timeout = timeout
101
102 def proxy_to(
103 self, opts: dict[str, t.Any], path: str, prefix: str
104 ) -> WSGIApplication:
105 target = urlsplit(opts["target"])
106 # socket can handle unicode host, but header must be ascii
107 host = target.hostname.encode("idna").decode("ascii")
108
109 def application(
110 environ: WSGIEnvironment, start_response: StartResponse
111 ) -> t.Iterable[bytes]:
112 headers = list(EnvironHeaders(environ).items())
113 headers[:] = [
114 (k, v)
115 for k, v in headers
116 if not is_hop_by_hop_header(k)
117 and k.lower() not in ("content-length", "host")
118 ]
119 headers.append(("Connection", "close"))
120
121 if opts["host"] == "<auto>":
122 headers.append(("Host", host))
123 elif opts["host"] is None:
124 headers.append(("Host", environ["HTTP_HOST"]))
125 else:
126 headers.append(("Host", opts["host"]))
127
128 headers.extend(opts["headers"].items())
129 remote_path = path
130
131 if opts["remove_prefix"]:
132 remote_path = remote_path[len(prefix) :].lstrip("/")
133 remote_path = f"{target.path.rstrip('/')}/{remote_path}"
134
135 content_length = environ.get("CONTENT_LENGTH")
136 chunked = False
137
138 if content_length not in ("", None):
139 headers.append(("Content-Length", content_length)) # type: ignore
140 elif content_length is not None:
141 headers.append(("Transfer-Encoding", "chunked"))
142 chunked = True
143
144 try:
145 if target.scheme == "http":
146 con = client.HTTPConnection(
147 host, target.port or 80, timeout=self.timeout
148 )
149 elif target.scheme == "https":
150 con = client.HTTPSConnection(
151 host,
152 target.port or 443,
153 timeout=self.timeout,
154 context=opts["ssl_context"],
155 )
156 else:
157 raise RuntimeError(
158 "Target scheme must be 'http' or 'https', got"
159 f" {target.scheme!r}."
160 )
161
162 con.connect()
163 # safe = https://url.spec.whatwg.org/#url-path-segment-string
164 # as well as percent for things that are already quoted
165 remote_url = quote(remote_path, safe="!$&'()*+,/:;=@%")
166 querystring = environ["QUERY_STRING"]
167
168 if querystring:
169 remote_url = f"{remote_url}?{querystring}"
170
171 con.putrequest(environ["REQUEST_METHOD"], remote_url, skip_host=True)
172
173 for k, v in headers:
174 if k.lower() == "connection":
175 v = "close"
176
177 con.putheader(k, v)
178
179 con.endheaders()
180 stream = get_input_stream(environ)
181
182 while True:
183 data = stream.read(self.chunk_size)
184
185 if not data:
186 break
187
188 if chunked:
189 con.send(b"%x\r\n%s\r\n" % (len(data), data))
190 else:
191 con.send(data)
192
193 resp = con.getresponse()
194 except OSError:
195 from ..exceptions import BadGateway
196
197 return BadGateway()(environ, start_response)
198
199 start_response(
200 f"{resp.status} {resp.reason}",
201 [
202 (k.title(), v)
203 for k, v in resp.getheaders()
204 if not is_hop_by_hop_header(k)
205 ],
206 )
207
208 def read() -> t.Iterator[bytes]:
209 while True:
210 try:
211 data = resp.read(self.chunk_size)
212 except OSError:
213 break
214
215 if not data:
216 break
217
218 yield data
219
220 return read()
221
222 return application
223
224 def __call__(
225 self, environ: WSGIEnvironment, start_response: StartResponse
226 ) -> t.Iterable[bytes]:
227 path = environ["PATH_INFO"]
228 app = self.app
229
230 for prefix, opts in self.targets.items():
231 if path.startswith(prefix):
232 app = self.proxy_to(opts, path, prefix)
233 break
234
235 return app(environ, start_response)