With ComfyUI started with --listen, other devices could use most of AMB's actions: delete and rename models, save and delete workflows and prompts, start downloads and scans, export a backup of all data, translate with the user's key, change settings, switch the plugin version. A probe of all routes as a LAN device found 9 of 112 refusing. api/local_guard.py is one middleware in front of every /anomalous/ route: anything but GET answers only this computer unless it is a read sent as POST (OPEN_POSTS); a GET is open unless it is more than a read (LOCAL_GETS: backup file, GitHub releases, local folders). Local = loopback peer, no proxy headers, loopback Host (DNS rebinding is refused too). Refusals are 403 local_only with a bilingual message; GET /anomalous/access lets the page show a "view only" chip (remote_view.js). AGENTS.md rule 4 now names it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
"""What only this computer may do. ComfyUI started with --listen lets other devices on the
|
|
network open the page: they may look (lists, searches, pictures, covers), but every change
|
|
under /anomalous/ (writing, moving and deleting files, downloads, scans, translations, backups,
|
|
version switches, saved settings) answers only requests from the computer ComfyUI runs on
|
|
(AGENTS.md 安全底线, rule 4). One middleware decides it for all routes: a request other than
|
|
GET is local-only unless it is a read sent as POST (OPEN_POSTS); a GET is open unless it is not
|
|
a plain read (LOCAL_GETS). A request is local when it comes from a loopback address, through no
|
|
proxy, naming a loopback host; checking the Host also keeps out a page that reached this server
|
|
under another name (DNS rebinding)."""
|
|
|
|
import ipaddress
|
|
import urllib.parse
|
|
|
|
from aiohttp import web
|
|
|
|
PREFIX = "/anomalous/"
|
|
PROXY_HEADERS = ("Forwarded", "X-Forwarded-For", "X-Real-IP", "X-Forwarded-Host")
|
|
LOCAL_NAMES = ("localhost",)
|
|
# Reads sent as POST (their lists are long): open to other devices too. They write nothing.
|
|
OPEN_POSTS = frozenset({
|
|
"/anomalous/resolve_paths_to_previews",
|
|
"/anomalous/resolve_hash_batch",
|
|
"/anomalous/inspect_image_material",
|
|
})
|
|
# GETs that are more than a read: a backup of all your data, a call to GitHub, your folders.
|
|
LOCAL_GETS = frozenset({
|
|
"/anomalous/backup/file",
|
|
"/anomalous/version/releases",
|
|
"/anomalous/import/folders",
|
|
})
|
|
# Routes that answer other devices themselves (MCP replies in JSON-RPC).
|
|
OWN_CHECK = frozenset({"/anomalous/mcp"})
|
|
READS = ("GET", "HEAD", "OPTIONS")
|
|
MESSAGE = ("只能在运行 ComfyUI 的电脑上操作,别的设备只能浏览。"
|
|
" Only the computer ComfyUI runs on can do this; other devices can only look.")
|
|
|
|
|
|
def loopback_host(name):
|
|
if not name:
|
|
return False
|
|
name = name.strip("[]").lower()
|
|
if name in LOCAL_NAMES:
|
|
return True
|
|
try:
|
|
return ipaddress.ip_address(name).is_loopback
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def is_local(request):
|
|
"""The request comes from this computer, directly, and names a loopback host."""
|
|
if not loopback_host(request.remote or ""):
|
|
return False
|
|
if any(header in request.headers for header in PROXY_HEADERS):
|
|
return False
|
|
return loopback_host(urllib.parse.urlsplit("//" + request.headers.get("Host", "")).hostname)
|
|
|
|
|
|
def needs_local(method, path):
|
|
"""Whether a request to `path` with `method` must come from this computer."""
|
|
if not path.startswith(PREFIX) or path in OWN_CHECK:
|
|
return False
|
|
if method.upper() in READS:
|
|
return path in LOCAL_GETS
|
|
return path not in OPEN_POSTS
|
|
|
|
|
|
def refusal():
|
|
return web.json_response({"status": "error", "success": False, "error": "local_only", "code": "local_only",
|
|
"message": MESSAGE}, status=403)
|
|
|
|
|
|
@web.middleware
|
|
async def local_only_middleware(request, handler):
|
|
if needs_local(request.method, request.path) and not is_local(request):
|
|
return refusal()
|
|
return await handler(request)
|
|
|
|
|
|
async def api_access(request):
|
|
"""GET /anomalous/access - whether this page may change things: {"local": bool}."""
|
|
return web.json_response({"local": is_local(request)})
|