You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

152 lines
6.1 KiB

"""Local web preview with caching disabled for editable frontend files."""
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from http.client import HTTPConnection, HTTPSConnection
from pathlib import Path
from urllib.parse import urlsplit
from urllib.request import Request, urlopen
from urllib.error import HTTPError
import argparse
class PreviewHandler(SimpleHTTPRequestHandler):
# 后端地址,可通过命令行参数覆盖;默认本机 8009
backend_host = "127.0.0.1"
backend_port = 8009
backend_scheme = "http"
def proxy_api(self):
length = int(self.headers.get("Content-Length", "0"))
if length > 1024 * 1024 * 1024:
self.send_error(413, "Upload too large")
return
body = self.rfile.read(length) if length else None
headers = {}
for name in ("Content-Type", "access_token"):
if self.headers.get(name):
headers[name] = self.headers[name]
connection = None
response = None
try:
if self.backend_scheme == "https":
request = Request(
f"https://{self.backend_host}:{self.backend_port}{self.path}",
data=body, headers=headers, method=self.command,
)
try:
response = urlopen(request, timeout=120)
except HTTPError as error:
response = error
else:
connection = HTTPConnection(self.backend_host, self.backend_port, timeout=120)
connection.request(self.command, self.path, body=body, headers=headers)
response = connection.getresponse()
content = response.read()
self.send_response(response.status if connection else response.code)
for name in ("Content-Type", "Cache-Control"):
value = response.getheader(name) if connection else response.headers.get(name)
if value:
self.send_header(name, value)
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
except OSError:
self.send_error(502, "Backend unavailable")
finally:
if response:
response.close()
if connection:
connection.close()
def proxy_media(self):
# /media/<path> -> https://video.yoyoeco.com/<path>
target = self.path[len("/media"):] or "/"
headers = {}
if self.headers.get("Range"):
headers["Range"] = self.headers["Range"]
connection = HTTPSConnection("video.yoyoeco.com", 443, timeout=60)
try:
connection.request("GET", target, headers=headers)
response = connection.getresponse()
self.send_response(response.status)
for name in ("Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", "Cache-Control"):
value = response.getheader(name)
if value:
self.send_header(name, value)
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
while True:
chunk = response.read(64 * 1024)
if not chunk:
break
self.wfile.write(chunk)
except OSError:
self.send_error(502, "Media unavailable")
finally:
connection.close()
def do_GET(self):
if self.path.startswith("/api/"):
return self.proxy_api()
if self.path.startswith("/media/"):
return self.proxy_media()
return super().do_GET()
def do_POST(self):
if self.path.startswith("/api/"):
return self.proxy_api()
self.send_error(404)
def do_DELETE(self):
if self.path.startswith("/api/"):
return self.proxy_api()
self.send_error(404)
def do_PUT(self):
if self.path.startswith("/api/"):
return self.proxy_api()
self.send_error(404)
def do_PATCH(self):
if self.path.startswith("/api/"):
return self.proxy_api()
self.send_error(404)
def do_OPTIONS(self):
if self.path.startswith("/api/"):
return self.proxy_api()
self.send_error(404)
def end_headers(self):
if self.path.split("?", 1)[0].endswith((".html", ".js", ".css")):
self.send_header("Cache-Control", "no-store, max-age=0")
super().end_headers()
def main():
parser = argparse.ArgumentParser(description="Serve the Youthome web preview")
parser.add_argument("--port", type=int, default=4173)
parser.add_argument("--backend-host", default="127.0.0.1", help="Backend host that /api/* is proxied to")
parser.add_argument("--backend-port", type=int, default=8009, help="Backend port that /api/* is proxied to")
parser.add_argument("--backend-url", help="Full HTTP(S) backend origin; overrides host and port")
args = parser.parse_args()
if args.backend_url:
backend = urlsplit(args.backend_url)
if backend.scheme not in ("http", "https") or not backend.hostname or backend.path not in ("", "/") or backend.query or backend.fragment:
parser.error("--backend-url must be an HTTP(S) origin without a path")
PreviewHandler.backend_scheme = backend.scheme
PreviewHandler.backend_host = backend.hostname
PreviewHandler.backend_port = backend.port or (443 if backend.scheme == "https" else 80)
else:
PreviewHandler.backend_host = args.backend_host
PreviewHandler.backend_port = args.backend_port
handler = partial(PreviewHandler, directory=str(Path(__file__).resolve().parent))
with ThreadingHTTPServer(("127.0.0.1", args.port), handler) as server:
print(f"Youthome web preview: http://127.0.0.1:{args.port}/pages/feed.html", flush=True)
print(f" /api/* -> {PreviewHandler.backend_scheme}://{PreviewHandler.backend_host}:{PreviewHandler.backend_port}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()