生成器默认监听 0.0.0.0,局域网/手机可直接打开
- serve.py:--host 默认改 0.0.0.0;启动时列出所有网卡地址(0.0.0.0 不能直接点开); --host 127.0.0.1 仍是「只给本机」的收紧方式,README 说明公共网络用它。 - 改单线程为 ThreadingTCPServer:上传几 MB 的图会占住连接,单线程时 iframe 预览 和其它请求都得排队。 - 写接口加同源校验(Origin 与 Host 比对,无 Origin 放行):挡住同网段里「另一个网页 借访客浏览器发写请求」,curl / 脚本不受影响。 - index.html:提示文案不再写死 127.0.0.1,改用 location.host —— 用局域网地址打开时 不该反过来提示「请用 127.0.0.1 打开」。 - mock_s3.py 加 --host(默认仍回环),便于在别的设备上联调。
This commit is contained in:
+69
-15
@@ -10,12 +10,13 @@ file:// 下浏览器不允许父页面访问 iframe 的 DOM(即使同目录)
|
||||
fetch,各家 S3 兼容实现的 CORS 配置界面又都不一样。放到本机 Python 里代发,
|
||||
生成器只跟 127.0.0.1 说话:不碰 CORS,密钥不落盘、不进工程文件、不进导出 zip。
|
||||
|
||||
用法(在项目根目录执行任意一种):
|
||||
用法(在项目根目录执行):
|
||||
|
||||
python dev/theme-builder/serve.py
|
||||
python -m http.server 8777 # 只有预览,没有上传能力
|
||||
python dev/theme-builder/serve.py # 默认 0.0.0.0:8777,同局域网可访问
|
||||
python dev/theme-builder/serve.py --host 127.0.0.1 # 只给本机(公共网络用这个)
|
||||
python -m http.server 8777 # 只有静态预览,没有上传能力
|
||||
|
||||
然后打开 http://localhost:8777/dev/theme-builder/
|
||||
启动时终端会列出本机与各网卡的地址,照着打开即可。
|
||||
|
||||
这个脚本只读文件,不写任何东西,也不改项目配置。
|
||||
"""
|
||||
@@ -30,6 +31,7 @@ import http.server
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
import urllib.error
|
||||
@@ -459,6 +461,19 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
raise ValueError("请求体过大(%d 字节),一次别传太多素材" % n)
|
||||
return json.loads(self.rfile.read(n).decode("utf-8"))
|
||||
|
||||
def _same_origin(self):
|
||||
"""写接口只接受「页面自己发来的」请求。
|
||||
|
||||
监听 0.0.0.0 之后,同网段里的另一个网页可以借访客浏览器向这里发写请求
|
||||
(删对象、改前缀)。浏览器跨站请求一定带 Origin,比对它与 Host 是否同源
|
||||
就能挡掉这类「借刀」调用。curl / 脚本不带 Origin,照常放行。
|
||||
"""
|
||||
origin = self.headers.get("Origin")
|
||||
if not origin:
|
||||
return True
|
||||
host = (self.headers.get("Host") or "").strip().lower()
|
||||
return urllib.parse.urlsplit(origin).netloc.lower() == host
|
||||
|
||||
def do_GET(self):
|
||||
if self.path.split("?")[0] == "/api/s3/ping":
|
||||
# 生成器靠它判断「当前是不是 serve.py 起的」——python -m http.server 没有这个端点。
|
||||
@@ -469,6 +484,8 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
route = self.path.split("?")[0]
|
||||
if route not in ("/api/s3/test", "/api/s3/upload", "/api/s3/list", "/api/s3/delete"):
|
||||
return self._json({"ok": False, "error": "未知接口 %s" % route}, 404)
|
||||
if not self._same_origin():
|
||||
return self._json({"ok": False, "error": "拒绝跨站写请求(Origin 与访问地址不一致)"}, 403)
|
||||
try:
|
||||
payload = self._read_json()
|
||||
except Exception as e:
|
||||
@@ -498,10 +515,38 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
return self._json({"ok": False, "error": "%s: %s" % (type(e).__name__, e)})
|
||||
|
||||
|
||||
def local_addresses():
|
||||
"""本机所有可用于访问的 IPv4(回环排最后)。
|
||||
|
||||
绑 0.0.0.0 时监听地址本身不能直接点开(浏览器访问 http://0.0.0.0 行为各家不一),
|
||||
所以启动时把真实 IP 列出来,省得人去 ipconfig 里翻。
|
||||
"""
|
||||
out = []
|
||||
try:
|
||||
for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
||||
ip = info[4][0]
|
||||
if ip not in out and not ip.startswith("127."):
|
||||
out.append(ip)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# 不联网也会返回默认路由网卡的地址;这一步能补上 getaddrinfo 漏掉的情况
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
if ip and not ip.startswith("127.") and ip not in out:
|
||||
out.append(ip)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="TamaBox 主题生成器的本地预览服务")
|
||||
ap.add_argument("--port", type=int, default=8777, help="端口,默认 8777")
|
||||
ap.add_argument("--host", default="127.0.0.1", help="监听地址,默认 127.0.0.1")
|
||||
ap.add_argument("--host", default="0.0.0.0",
|
||||
help="监听地址,默认 0.0.0.0(同一局域网都能打开);只给本机用就写 127.0.0.1")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.isdir(os.path.join(PROJECT_ROOT, "outputs")):
|
||||
@@ -510,19 +555,28 @@ def main():
|
||||
" TEST_PREVIEW=1 go test ./templates -run TestGenPreview\n"
|
||||
)
|
||||
|
||||
if args.host not in ("127.0.0.1", "localhost", "::1"):
|
||||
sys.stderr.write(
|
||||
"⚠️ 监听在 %s:上传接口没有鉴权,会代任何能访问到它的人向对象存储写文件。\n"
|
||||
" 只在完全可信的内网这么做。\n" % args.host
|
||||
)
|
||||
wildcard = args.host in ("0.0.0.0", "::")
|
||||
if not wildcard and args.host not in ("127.0.0.1", "localhost", "::1"):
|
||||
sys.stderr.write("⚠️ 只监听 %s,仅该地址可达。\n" % args.host)
|
||||
|
||||
handler = functools.partial(Handler, directory=PROJECT_ROOT)
|
||||
socketserver.TCPServer.allow_reuse_address = True
|
||||
with socketserver.TCPServer((args.host, args.port), handler) as httpd:
|
||||
url = "http://%s:%d/dev/theme-builder/" % (args.host, args.port)
|
||||
socketserver.ThreadingTCPServer.allow_reuse_address = True
|
||||
# Threading:上传一张几 MB 的图会占住连接,单线程时 iframe 预览和其它请求都得排队。
|
||||
with socketserver.ThreadingTCPServer((args.host, args.port), handler) as httpd:
|
||||
httpd.daemon_threads = True
|
||||
print("项目根:%s" % PROJECT_ROOT)
|
||||
print("生成器:%s" % url)
|
||||
print("上传接口:/api/s3/test /api/s3/upload(密钥只在内存,不落盘)")
|
||||
if wildcard:
|
||||
for ip in local_addresses():
|
||||
print("生成器:http://%s:%d/dev/theme-builder/" % (ip, args.port))
|
||||
print("生成器:http://127.0.0.1:%d/dev/theme-builder/ (本机)" % args.port)
|
||||
print("提示:局域网里的其它设备用上面的 IP 地址打开;手机与电脑要在同一网段。")
|
||||
sys.stderr.write(
|
||||
"⚠️ 正在对局域网开放(%s:%d)。上传接口没有账号体系 —— 能打开这个端口的人,\n"
|
||||
" 都能用你填的密钥往对象存储写 / 删文件。公共网络(咖啡馆、宿舍、公司网)\n"
|
||||
" 请改用 --host 127.0.0.1。\n" % (args.host, args.port))
|
||||
else:
|
||||
print("生成器:http://%s:%d/dev/theme-builder/" % (args.host, args.port))
|
||||
print("上传接口:/api/s3/test /api/s3/upload /api/s3/list /api/s3/delete")
|
||||
print("Ctrl+C 停止")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
|
||||
Reference in New Issue
Block a user