Files
DemonGatanjieu-Anomalous_Mo…/api/local_guard.py
T
DemonGatanjieuandClaude Opus 5.5 b261968e79 Show the versions of one model together and switch between them
Files a scan found on the same Civitai model page (metadata model_id; never a similar
file name) share one card in the model list, the newest version shown or the one picked
last (kept in this browser). The card's version label opens the shared action menu to
switch. The details list the versions here under the name, and "All versions on Civitai"
(GET /anomalous/download/versions, this computer only, asked only when pressed) lists
the page's versions to open or download beside this one, checked by SHA-256, a taken
file name getting the version's name added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 04:44:10 +08:00

119 lines
5.1 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).
ComfyUI on a rented cloud server is only ever opened from another device. An empty file named
`allow_remote_changes` in the plugin's folder, made on that server, lets every device that can
open the page change things too (may_change); it is read once, when ComfyUI starts, and the
page cannot make or remove it. MCP keeps to this computer either way."""
import ipaddress
import os
import shlex
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/tts_version/releases",
"/anomalous/import/folders",
"/anomalous/civitai_check",
"/anomalous/download/versions", # asks Civitai
"/anomalous/usage/check", # reads every output and saved workflow
})
# Routes that answer other devices themselves (MCP replies in JSON-RPC).
OWN_CHECK = frozenset({"/anomalous/mcp"})
READS = ("GET", "HEAD", "OPTIONS")
REMOTE_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "allow_remote_changes")
REMOTE_CHANGES = os.path.isfile(REMOTE_FILE)
if REMOTE_CHANGES:
print("[Anomalous Browser] allow_remote_changes 文件存在:任何能打开页面的设备都能扫描、移动、删除模型和改设置。"
" The allow_remote_changes file is there: every device that can open the page may change things.", flush=True)
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 may_change(request):
"""The request may change things: it comes from this computer, or remote changes are allowed."""
return REMOTE_CHANGES or is_local(request)
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 may_change(request):
return refusal()
return await handler(request)
def switch_command():
"""The line to paste into the server's terminal that makes the allow_remote_changes file, or
removes it when it is there. On Windows `echo off >` makes the file in cmd and PowerShell
alike (only that it exists counts)."""
if os.name == "nt":
return f'del "{REMOTE_FILE}"' if REMOTE_CHANGES else f'echo off > "{REMOTE_FILE}"'
return ("rm -f " if REMOTE_CHANGES else "touch ") + shlex.quote(REMOTE_FILE)
async def api_access(request):
"""GET /anomalous/access - whether this page is on this computer and whether it may change
things: {"local": bool, "changes": bool}; for another device also "command", the line that
turns remote changes on or off (it only runs where someone types it, on the server)."""
local = is_local(request)
answer = {"local": local, "changes": may_change(request)}
if not local:
answer["command"] = switch_command()
return web.json_response(answer)