-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdev_server.py
More file actions
141 lines (118 loc) · 4.8 KB
/
Copy pathdev_server.py
File metadata and controls
141 lines (118 loc) · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import time
import urllib.parse
import urllib.request
import urllib.error
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from ipaddress import ip_address
PORT = int(os.environ.get("PORT", "8000"))
PROXY_CACHE_TTL_SEC = 300 # 5 min
USER_AGENT = "dash-dev-proxy/1.0 (+local)"
UPSTREAM_TIMEOUT_SEC = int(os.environ.get("PROXY_TIMEOUT", "25"))
_cache: dict[str, tuple[float, dict[str, str], bytes]] = {}
def _is_private_host(hostname: str) -> bool:
h = (hostname or "").strip().lower()
if h in {"localhost", "127.0.0.1", "::1"}:
return True
# Best-effort: block raw IPs in private ranges
try:
ip = ip_address(h)
return ip.is_private or ip.is_loopback or ip.is_link_local
except Exception:
return False
def _validate_target(url: str) -> tuple[bool, str]:
try:
parsed = urllib.parse.urlparse(url)
except Exception:
return False, "URL inválido"
if parsed.scheme not in {"http", "https"}:
return False, "Apenas http/https são permitidos"
if not parsed.netloc:
return False, "Host em falta"
host = parsed.hostname or ""
if _is_private_host(host):
return False, "Host privado/local não permitido"
return True, ""
class Handler(SimpleHTTPRequestHandler):
def end_headers(self) -> None:
# Allow dev CORS for same-machine browser
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
super().end_headers()
def do_OPTIONS(self) -> None:
self.send_response(HTTPStatus.NO_CONTENT)
self.end_headers()
def do_GET(self) -> None:
if self.path.startswith("/proxy"):
self._handle_proxy()
return
super().do_GET()
def _handle_proxy(self) -> None:
q = urllib.parse.urlparse(self.path).query
params = urllib.parse.parse_qs(q)
target = (params.get("url", [""])[0] or "").strip()
ok, msg = _validate_target(target)
if not ok:
self._send_json({"ok": False, "error": msg}, status=HTTPStatus.BAD_REQUEST)
return
now = time.time()
cached = _cache.get(target)
if cached and now - cached[0] < PROXY_CACHE_TTL_SEC:
headers, body = cached[1], cached[2]
self._send_bytes(body, headers=headers, status=HTTPStatus.OK)
return
req = urllib.request.Request(
target,
headers={
"User-Agent": USER_AGENT,
# Forward Accept from browser when available (useful for content negotiation).
"Accept": self.headers.get("Accept") or "*/*",
},
method="GET",
)
try:
with urllib.request.urlopen(req, timeout=UPSTREAM_TIMEOUT_SEC) as resp:
body = resp.read()
headers = {}
ct = resp.headers.get("Content-Type")
if ct:
headers["Content-Type"] = ct
_cache[target] = (now, headers, body)
self._send_bytes(body, headers=headers, status=HTTPStatus.OK)
except urllib.error.HTTPError as e:
# Forward upstream status/body as-is (so the browser sees 4xx/5xx instead of 502)
try:
body = e.read()
except Exception:
body = b""
headers = {}
ct = getattr(e, "headers", None) and e.headers.get("Content-Type")
if ct:
headers["Content-Type"] = ct
self._send_bytes(body, headers=headers, status=int(getattr(e, "code", 502) or 502))
except Exception as e:
self._send_json({"ok": False, "error": str(e)}, status=HTTPStatus.BAD_GATEWAY)
def _send_bytes(self, body: bytes, headers: dict[str, str] | None = None, status: int = 200) -> None:
self.send_response(status)
if headers:
for k, v in headers.items():
self.send_header(k, v)
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _send_json(self, payload: dict, status: int = 200) -> None:
body = json.dumps(payload).encode("utf-8")
self._send_bytes(body, headers={"Content-Type": "application/json; charset=utf-8"}, status=status)
def main() -> None:
with ThreadingHTTPServer(("0.0.0.0", PORT), Handler) as httpd:
print(f"Dev server: http://localhost:{PORT}")
print("Proxy endpoint: /proxy?url=https://example.com")
httpd.serve_forever()
if __name__ == "__main__":
main()