mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-07-26 18:17:30 +08:00
fix(security): harden CSRF with Content-Type gate and expand E2E coverage (#2818)
Defense-in-depth over GET→POST alone: reject the three CORS-safelisted simple-form Content-Types (x-www-form-urlencoded, multipart/form-data, text/plain) on 16 no-body POST handlers (glob + legacy) to block <form method=POST> CSRF that bypasses method-only gating. Move comfyui_switch_version to a JSON body so the preflight requirement applies. Split db_mode/policy/update/channel_url_list into GET(read) + POST(write). Tighten do_fix (high → high+) and gate three previously-ungated config setters at middle. Resynchronize openapi.yaml (27 paths, 30 operations, ComfyUISwitchVersionParams as a shared $ref component). Add E2E harness variants, Playwright config, CSRF/secgate suites, 39-endpoint coverage, and a CHANGELOG. Breaking: legacy per-op POST routes (install/uninstall/fix/disable/update/ reinstall/abort_current) are removed; callers already use queue/batch. Legacy /manager/notice (v1) is removed; /v2/manager/notice is retained. Reported-by: XlabAI Team of Tencent Xuanwu Lab CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
This commit is contained in:
@@ -26,7 +26,15 @@ def start():
|
||||
logging.info("[ComfyUI-Manager] Legacy UI is enabled.")
|
||||
nodes.EXTENSION_WEB_DIRS['comfyui-manager-legacy'] = os.path.join(os.path.dirname(__file__), 'js')
|
||||
except Exception as e:
|
||||
print("Error enabling legacy ComfyUI Manager frontend:", e)
|
||||
# WI-V: upgraded silent `print` to a proper logging.error with
|
||||
# traceback so future legacy-UI load failures are visible in
|
||||
# the log, not swallowed. The original `print` could be lost
|
||||
# depending on how stdout is captured.
|
||||
import traceback
|
||||
logging.error(
|
||||
"[ComfyUI-Manager] Error enabling legacy frontend: "
|
||||
f"{type(e).__name__}: {e}\n{traceback.format_exc()}"
|
||||
)
|
||||
core = None
|
||||
else:
|
||||
from .glob import manager_server # noqa: F401
|
||||
|
||||
@@ -1,10 +1,74 @@
|
||||
"""Security helpers for CSRF protection and Content-Type gating.
|
||||
|
||||
reject_simple_form_post() is applied ONLY to POST handlers that do not consume
|
||||
a request body (e.g., snapshot/save, queue/reset, queue/start, reboot). These
|
||||
are vulnerable to cross-origin <form method=POST> attacks because the server
|
||||
accepts the request without parsing any body — the attacker needs no ability
|
||||
to forge a valid payload, only to point a hidden form at the URL.
|
||||
|
||||
Handlers that DO read a body via ``await request.json()`` (install/git_url,
|
||||
install/pip, queue/install_model, db_mode POST, policy/update POST,
|
||||
channel_url_list POST, queue/batch, queue/task, import_fail_info, etc.) are
|
||||
NOT gated here — a cross-origin <form method=POST> cannot forge a valid JSON
|
||||
body because the browser refuses to send ``application/json`` without a CORS
|
||||
preflight, which this server rejects by not responding with an appropriate
|
||||
Access-Control-Allow-Origin.
|
||||
|
||||
DO NOT add the gate to body-reading handlers (redundant + UX-breaking).
|
||||
DO NOT remove the gate from no-body handlers (this is the bypass vector).
|
||||
"""
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
is_personal_cloud_mode = False
|
||||
handler_policy = {}
|
||||
|
||||
|
||||
# CORS "simple request" Content-Type set per Fetch spec §3.2.3. Browsers send
|
||||
# <form method=POST> submissions with one of these three MIME types and do NOT
|
||||
# trigger a CORS preflight, so a malicious cross-origin page can silently POST
|
||||
# into state-changing endpoints if we only gate on HTTP method. Blocking these
|
||||
# three Content-Types on our mutation endpoints forces any non-same-origin POST
|
||||
# to use a non-simple Content-Type (e.g. application/json), which triggers a
|
||||
# preflight that this server rejects (no Access-Control-Allow-Origin response).
|
||||
_SIMPLE_FORM_CONTENT_TYPES = frozenset({
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data',
|
||||
'text/plain',
|
||||
})
|
||||
|
||||
|
||||
def reject_simple_form_post(request) -> Optional[web.Response]:
|
||||
"""Reject Content-Types that enable preflight-less <form method=POST> CSRF.
|
||||
|
||||
These 3 MIME types are the complete CORS "simple request" Content-Type set
|
||||
(Fetch spec §3.2.3 "CORS-safelisted request-header"). Blocking them
|
||||
eliminates the <form method=POST> cross-origin CSRF vector, because any
|
||||
other Content-Type triggers a browser-enforced CORS preflight — and this
|
||||
server does not answer preflights with ``Access-Control-Allow-Origin``,
|
||||
effectively blocking cross-origin requests that use non-simple types.
|
||||
|
||||
Returns:
|
||||
web.Response(status=400) when the request has a simple-form
|
||||
Content-Type that must be rejected. None when the request is allowed
|
||||
to proceed (no body, application/json, or any non-simple Content-Type).
|
||||
|
||||
Note:
|
||||
aiohttp's ``request.content_type`` normalizes the header (lower-cases,
|
||||
strips parameters), so a ``multipart/form-data; boundary=----X`` header
|
||||
is compared as ``multipart/form-data``.
|
||||
"""
|
||||
if request.content_type in _SIMPLE_FORM_CONTENT_TYPES:
|
||||
return web.Response(
|
||||
status=400,
|
||||
text='Invalid Content-Type for this endpoint. Use application/json or omit body.',
|
||||
)
|
||||
return None
|
||||
|
||||
class HANDLER_POLICY(Enum):
|
||||
MULTIPLE_REMOTE_BAN_NON_LOCAL = 1
|
||||
MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD = 2
|
||||
|
||||
@@ -57,7 +57,7 @@ from .generated_models import (
|
||||
EnablePackParams,
|
||||
UpdateAllQueryParams,
|
||||
UpdateComfyUIQueryParams,
|
||||
ComfyUISwitchVersionQueryParams,
|
||||
ComfyUISwitchVersionParams,
|
||||
QueueStatus,
|
||||
ManagerMappings,
|
||||
ModelMetadata,
|
||||
@@ -121,7 +121,7 @@ __all__ = [
|
||||
"EnablePackParams",
|
||||
"UpdateAllQueryParams",
|
||||
"UpdateComfyUIQueryParams",
|
||||
"ComfyUISwitchVersionQueryParams",
|
||||
"ComfyUISwitchVersionParams",
|
||||
"QueueStatus",
|
||||
"ManagerMappings",
|
||||
"ModelMetadata",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: openapi.yaml
|
||||
# timestamp: 2025-07-31T04:52:26+00:00
|
||||
# timestamp: 2026-04-19T04:33:23+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -229,8 +229,8 @@ class UpdateComfyUIQueryParams(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ComfyUISwitchVersionQueryParams(BaseModel):
|
||||
ver: str = Field(..., description="Version to switch to")
|
||||
class ComfyUISwitchVersionParams(BaseModel):
|
||||
ver: str = Field(..., description="Target ComfyUI version tag")
|
||||
client_id: str = Field(
|
||||
..., description="Client identifier that initiated the request"
|
||||
)
|
||||
@@ -502,6 +502,22 @@ class TaskHistoryItem(BaseModel):
|
||||
end_time: Optional[datetime] = Field(
|
||||
None, description="ISO timestamp when task execution ended"
|
||||
)
|
||||
params: Optional[
|
||||
Union[
|
||||
InstallPackParams,
|
||||
UpdatePackParams,
|
||||
UpdateAllPacksParams,
|
||||
UpdateComfyUIParams,
|
||||
FixPackParams,
|
||||
UninstallPackParams,
|
||||
DisablePackParams,
|
||||
EnablePackParams,
|
||||
ModelMetadata,
|
||||
]
|
||||
] = Field(
|
||||
None,
|
||||
description="Original task parameters (mirrors QueueTaskItem.params); null if unavailable",
|
||||
)
|
||||
|
||||
|
||||
class TaskStateMessage(BaseModel):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_HIGH_P = "ERROR: To use this action, '--listen' must be set to a local IP and security_level must be 'normal-' or lower. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower."
|
||||
|
||||
@@ -44,8 +44,13 @@ from ..common.enums import NetworkMode, SecurityLevel, DBMode
|
||||
from ..common import context
|
||||
|
||||
|
||||
version_code = [4, 2]
|
||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||
try:
|
||||
from importlib.metadata import version as _pkg_version
|
||||
_raw_version = _pkg_version("comfyui-manager")
|
||||
except Exception:
|
||||
_raw_version = "unknown"
|
||||
|
||||
version_str = f"V{_raw_version}"
|
||||
|
||||
|
||||
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
|
||||
@@ -2033,6 +2038,10 @@ def install_manager_requirements(repo_path):
|
||||
Install packages from manager_requirements.txt if it exists.
|
||||
This is specifically for ComfyUI's manager_requirements.txt.
|
||||
"""
|
||||
if os.environ.get("COMFYUI_MANAGER_SKIP_MANAGER_REQUIREMENTS", "").lower() in ("1", "true", "yes"):
|
||||
logging.info("[ComfyUI-Manager] Skipping manager_requirements.txt install (COMFYUI_MANAGER_SKIP_MANAGER_REQUIREMENTS set)")
|
||||
return
|
||||
|
||||
manager_requirements_path = os.path.join(repo_path, "manager_requirements.txt")
|
||||
if not os.path.exists(manager_requirements_path):
|
||||
return
|
||||
|
||||
@@ -80,13 +80,14 @@ from ..data_models import (
|
||||
SecurityLevel,
|
||||
UpdateAllQueryParams,
|
||||
UpdateComfyUIQueryParams,
|
||||
ComfyUISwitchVersionQueryParams,
|
||||
ComfyUISwitchVersionParams,
|
||||
)
|
||||
|
||||
from .constants import (
|
||||
model_dir_name_map,
|
||||
SECURITY_MESSAGE_MIDDLE,
|
||||
SECURITY_MESSAGE_MIDDLE_P,
|
||||
SECURITY_MESSAGE_HIGH_P,
|
||||
)
|
||||
|
||||
if not manager_util.is_manager_pip_package():
|
||||
@@ -335,6 +336,7 @@ class TaskQueue:
|
||||
status=status,
|
||||
batch_id=self.batch_id,
|
||||
end_time=now,
|
||||
params=item.params,
|
||||
)
|
||||
|
||||
# Force cache refresh for successful pack-modifying operations
|
||||
@@ -656,8 +658,7 @@ class TaskQueue:
|
||||
def _get_manager_version(self) -> str:
|
||||
"""Get ComfyUI Manager version."""
|
||||
try:
|
||||
version_code = getattr(core, "version_code", [4, 0])
|
||||
return f"V{version_code[0]}.{version_code[1]}"
|
||||
return core.version_str
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -886,11 +887,13 @@ async def task_worker():
|
||||
res = core.unified_manager.unified_update(node_name, node_ver)
|
||||
|
||||
if res.ver == "unknown":
|
||||
# unknown_active_nodes[node_id] = (url, fullpath) — url can be
|
||||
# None when git_utils.git_url() in manager_core can't determine
|
||||
# the remote URL. Downstream branches at L901/904 already
|
||||
# handle url is None, so we just need a None-safe title.
|
||||
# Harmonized with legacy/manager_server.py equivalent (WI #252).
|
||||
url = core.unified_manager.unknown_active_nodes[node_name][0]
|
||||
try:
|
||||
title = os.path.basename(url)
|
||||
except Exception:
|
||||
title = node_name
|
||||
title = os.path.basename(url) if url else node_name
|
||||
else:
|
||||
url = core.unified_manager.cnr_map[node_name].get("repository")
|
||||
title = core.unified_manager.cnr_map[node_name]["name"]
|
||||
@@ -966,8 +969,12 @@ async def task_worker():
|
||||
return "An error occurred while updating 'comfyui'."
|
||||
|
||||
async def do_fix(params: FixPackParams) -> str:
|
||||
if not security_utils.is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
# Align check with SECURITY_MESSAGE_HIGH_P (which names "high+"); the
|
||||
# previous 'high' gate allowed the operation while logging a message
|
||||
# that implied a stricter requirement — confusing and slightly too lax
|
||||
# for a state-mutating fix path. Legacy/do_fix was updated to match.
|
||||
if not security_utils.is_allowed_security_level('high+'):
|
||||
logging.error(SECURITY_MESSAGE_HIGH_P)
|
||||
return OperationResult.failed.value
|
||||
|
||||
node_name = params.node_name
|
||||
@@ -1346,6 +1353,19 @@ async def get_history(request):
|
||||
}
|
||||
history = filtered_history
|
||||
|
||||
# Serialize TaskHistoryItem pydantic models to dicts for JSON output.
|
||||
# aiohttp's json_response uses json.dumps which cannot serialize BaseModel
|
||||
# instances; convert via model_dump(mode='json') to handle datetime fields.
|
||||
def _to_serializable(obj):
|
||||
if hasattr(obj, "model_dump"):
|
||||
return obj.model_dump(mode="json")
|
||||
return obj
|
||||
|
||||
if isinstance(history, dict):
|
||||
history = {k: _to_serializable(v) for k, v in history.items()}
|
||||
else:
|
||||
history = _to_serializable(history)
|
||||
|
||||
return web.json_response({"history": history}, content_type="application/json")
|
||||
|
||||
except Exception as e:
|
||||
@@ -1408,8 +1428,11 @@ async def fetch_updates(request):
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/update_all")
|
||||
@routes.post("/v2/manager/queue/update_all")
|
||||
async def update_all(request: web.Request) -> web.Response:
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
try:
|
||||
# Validate query parameters using Pydantic model
|
||||
query_params = UpdateAllQueryParams.model_validate(dict(request.rel_url.query))
|
||||
@@ -1518,8 +1541,11 @@ async def get_snapshot_list(request):
|
||||
return web.json_response({"items": items}, content_type="application/json")
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/remove")
|
||||
@routes.post("/v2/snapshot/remove")
|
||||
async def remove_snapshot(request):
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
if not security_utils.is_allowed_security_level("middle"):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
@@ -1540,8 +1566,11 @@ async def remove_snapshot(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/restore")
|
||||
@routes.post("/v2/snapshot/restore")
|
||||
async def restore_snapshot(request):
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
if not security_utils.is_allowed_security_level("middle+"):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_P)
|
||||
return web.Response(status=403)
|
||||
@@ -1582,8 +1611,11 @@ async def get_current_snapshot_api(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/save")
|
||||
@routes.post("/v2/snapshot/save")
|
||||
async def save_snapshot(request):
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
try:
|
||||
await core.save_snapshot_with_postfix("snapshot")
|
||||
return web.Response(status=200)
|
||||
@@ -1715,8 +1747,11 @@ async def import_fail_info_bulk(request):
|
||||
return web.Response(status=500, text="Internal server error")
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/reset")
|
||||
@routes.post("/v2/manager/queue/reset")
|
||||
async def reset_queue(request):
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
logging.debug("[ComfyUI-Manager] Queue reset requested")
|
||||
task_queue.wipe_queue()
|
||||
return web.Response(status=200)
|
||||
@@ -1775,8 +1810,11 @@ async def queue_count(request):
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/start")
|
||||
@routes.post("/v2/manager/queue/start")
|
||||
async def queue_start(request):
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
logging.debug("[ComfyUI-Manager] Queue start requested")
|
||||
started = task_queue.start_worker()
|
||||
|
||||
@@ -1788,9 +1826,12 @@ async def queue_start(request):
|
||||
return web.Response(status=201) # Already in-progress
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/update_comfyui")
|
||||
@routes.post("/v2/manager/queue/update_comfyui")
|
||||
async def update_comfyui(request):
|
||||
"""Queue a ComfyUI update based on the configured update policy."""
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
try:
|
||||
# Validate query parameters using Pydantic model
|
||||
query_params = UpdateComfyUIQueryParams.model_validate(
|
||||
@@ -1837,17 +1878,26 @@ async def comfyui_versions(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/comfyui_manager/comfyui_switch_version")
|
||||
@routes.post("/v2/comfyui_manager/comfyui_switch_version")
|
||||
async def comfyui_switch_version(request):
|
||||
try:
|
||||
# Validate query parameters using Pydantic model
|
||||
query_params = ComfyUISwitchVersionQueryParams.model_validate(
|
||||
dict(request.rel_url.query)
|
||||
)
|
||||
# Body-reading handler — Content-Type gate omitted per
|
||||
# comfyui_manager/common/manager_security.py module policy: a cross-origin
|
||||
# <form method=POST> cannot forge a valid application/json body because
|
||||
# the browser would trigger a CORS preflight that this server refuses.
|
||||
if not security_utils.is_allowed_security_level("high+"):
|
||||
logging.error(SECURITY_MESSAGE_HIGH_P)
|
||||
return web.Response(status=403)
|
||||
|
||||
target_version = query_params.ver
|
||||
client_id = query_params.client_id
|
||||
ui_id = query_params.ui_id
|
||||
try:
|
||||
# Parse and validate JSON body (previously read from query string).
|
||||
# ComfyUISwitchVersionParams is reused — the field set is
|
||||
# identical for body and query; only the transport changed.
|
||||
json_data = await request.json()
|
||||
params = ComfyUISwitchVersionParams.model_validate(json_data)
|
||||
|
||||
target_version = params.ver
|
||||
client_id = params.client_id
|
||||
ui_id = params.ui_id
|
||||
|
||||
# Create update-comfyui task with target version
|
||||
task = QueueTaskItem(
|
||||
@@ -1859,6 +1909,8 @@ async def comfyui_switch_version(request):
|
||||
|
||||
task_queue.put(task)
|
||||
return web.Response(status=200)
|
||||
except json.JSONDecodeError:
|
||||
return web.Response(status=400, text="Invalid JSON body")
|
||||
except ValidationError as e:
|
||||
return web.json_response(
|
||||
{"error": "Validation error", "details": e.errors()}, status=400
|
||||
@@ -1902,51 +1954,97 @@ async def install_model(request):
|
||||
|
||||
@routes.get("/v2/manager/db_mode")
|
||||
async def db_mode(request):
|
||||
if "value" in request.rel_url.query:
|
||||
environment_utils.set_db_mode(request.rel_url.query["value"])
|
||||
core.write_config()
|
||||
else:
|
||||
return web.Response(text=core.get_config()["db_mode"], status=200)
|
||||
return web.Response(text=core.get_config()["db_mode"], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@routes.post("/v2/manager/db_mode")
|
||||
async def set_db_mode_api(request):
|
||||
# Config writes are at the same risk tier as uninstall/update — apply the
|
||||
# 'middle' gate consistent with snapshot/remove, etc. Content-Type gate is
|
||||
# NOT applied here: this handler consumes application/json and a
|
||||
# cross-origin <form method=POST> cannot forge that without triggering
|
||||
# CORS preflight (see module docstring in common/manager_security.py).
|
||||
if not security_utils.is_allowed_security_level("middle"):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
try:
|
||||
data = await request.json()
|
||||
environment_utils.set_db_mode(data["value"])
|
||||
core.write_config()
|
||||
return web.Response(status=200)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return web.Response(status=400, text="Invalid request")
|
||||
except ValueError as e:
|
||||
return web.Response(status=400, text=str(e))
|
||||
|
||||
|
||||
@routes.get("/v2/manager/policy/update")
|
||||
async def update_policy(request):
|
||||
if "value" in request.rel_url.query:
|
||||
environment_utils.set_update_policy(request.rel_url.query["value"])
|
||||
core.write_config()
|
||||
else:
|
||||
return web.Response(text=core.get_config()["update_policy"], status=200)
|
||||
return web.Response(text=core.get_config()["update_policy"], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@routes.post("/v2/manager/policy/update")
|
||||
async def set_update_policy_api(request):
|
||||
# See set_db_mode_api above for gate rationale.
|
||||
if not security_utils.is_allowed_security_level("middle"):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
try:
|
||||
data = await request.json()
|
||||
environment_utils.set_update_policy(data["value"])
|
||||
core.write_config()
|
||||
return web.Response(status=200)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return web.Response(status=400, text="Invalid request")
|
||||
except ValueError as e:
|
||||
return web.Response(status=400, text=str(e))
|
||||
|
||||
|
||||
@routes.get("/v2/manager/channel_url_list")
|
||||
async def channel_url_list(request):
|
||||
channels = core.get_channel_dict()
|
||||
if "value" in request.rel_url.query:
|
||||
channel_url = channels.get(request.rel_url.query["value"])
|
||||
if channel_url is not None:
|
||||
core.get_config()["channel_url"] = channel_url
|
||||
core.write_config()
|
||||
else:
|
||||
selected = "custom"
|
||||
selected_url = core.get_config()["channel_url"]
|
||||
selected = "custom"
|
||||
selected_url = core.get_config()["channel_url"]
|
||||
|
||||
for name, url in channels.items():
|
||||
if url == selected_url:
|
||||
selected = name
|
||||
break
|
||||
for name, url in channels.items():
|
||||
if url == selected_url:
|
||||
selected = name
|
||||
break
|
||||
|
||||
res = {"selected": selected, "list": core.get_channel_list()}
|
||||
return web.json_response(res, status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
res = {"selected": selected, "list": core.get_channel_list()}
|
||||
return web.json_response(res, status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/reboot")
|
||||
def restart(self):
|
||||
@routes.post("/v2/manager/channel_url_list")
|
||||
async def set_channel_url(request):
|
||||
# See set_db_mode_api above for gate rationale.
|
||||
if not security_utils.is_allowed_security_level("middle"):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
try:
|
||||
data = await request.json()
|
||||
channels = core.get_channel_dict()
|
||||
channel_url = channels.get(data["value"])
|
||||
if channel_url is None:
|
||||
# Reject unknown channel name explicitly instead of silent no-op.
|
||||
# Parity with set_db_mode / set_update_policy whitelist enforcement.
|
||||
return web.Response(
|
||||
status=400,
|
||||
text=f"Invalid channel name {data['value']!r}; "
|
||||
f"must be one of {sorted(channels.keys())}",
|
||||
)
|
||||
core.get_config()["channel_url"] = channel_url
|
||||
core.write_config()
|
||||
return web.Response(status=200)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return web.Response(status=400, text="Invalid request")
|
||||
|
||||
|
||||
@routes.post("/v2/manager/reboot")
|
||||
def restart(request):
|
||||
rejection = security_utils.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
if not security_utils.is_allowed_security_level("middle"):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
|
||||
@@ -91,11 +91,23 @@ def print_comfyui_version():
|
||||
)
|
||||
|
||||
|
||||
ALLOWED_UPDATE_POLICIES = ("stable", "stable-comfyui", "nightly", "nightly-comfyui")
|
||||
ALLOWED_DB_MODES = ("cache", "channel", "local", "remote")
|
||||
|
||||
|
||||
def set_update_policy(mode):
|
||||
if mode not in ALLOWED_UPDATE_POLICIES:
|
||||
raise ValueError(
|
||||
f"Invalid update_policy {mode!r}; must be one of {ALLOWED_UPDATE_POLICIES}"
|
||||
)
|
||||
core.get_config()["update_policy"] = mode
|
||||
|
||||
|
||||
def set_db_mode(mode):
|
||||
if mode not in ALLOWED_DB_MODES:
|
||||
raise ValueError(
|
||||
f"Invalid db_mode {mode!r}; must be one of {ALLOWED_DB_MODES}"
|
||||
)
|
||||
core.get_config()["db_mode"] = mode
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,18 @@ from comfyui_manager.common.manager_security import (
|
||||
is_loopback,
|
||||
is_safe_path_target,
|
||||
get_safe_file_path,
|
||||
reject_simple_form_post,
|
||||
)
|
||||
|
||||
# Re-export for backward compatibility
|
||||
__all__ = ['is_loopback', 'is_safe_path_target', 'get_safe_file_path', 'is_allowed_security_level', 'get_risky_level']
|
||||
__all__ = [
|
||||
'is_loopback',
|
||||
'is_safe_path_target',
|
||||
'get_safe_file_path',
|
||||
'reject_simple_form_post',
|
||||
'is_allowed_security_level',
|
||||
'get_risky_level',
|
||||
]
|
||||
|
||||
|
||||
def is_allowed_security_level(level):
|
||||
|
||||
@@ -52,7 +52,7 @@ async function tryInstallCustomNode(event) {
|
||||
}
|
||||
}
|
||||
|
||||
let response = await api.fetchApi("/v2/manager/reboot");
|
||||
let response = await api.fetchApi("/v2/manager/reboot", { method: 'POST' });
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
return false;
|
||||
|
||||
@@ -628,14 +628,23 @@ async function switchComfyUI() {
|
||||
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
|
||||
if(selected_version == 'nightly') {
|
||||
update_policy_combo.value = 'nightly-comfyui';
|
||||
api.fetchApi('/v2/manager/policy/update?value=nightly-comfyui');
|
||||
api.fetchApi('/v2/manager/policy/update', { method: 'POST', body: JSON.stringify({value: 'nightly-comfyui'}) });
|
||||
}
|
||||
else {
|
||||
update_policy_combo.value = 'stable-comfyui';
|
||||
api.fetchApi('/v2/manager/policy/update?value=stable-comfyui');
|
||||
api.fetchApi('/v2/manager/policy/update', { method: 'POST', body: JSON.stringify({value: 'stable-comfyui'}) });
|
||||
}
|
||||
|
||||
let response = await api.fetchApi(`/v2/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
|
||||
let response = await api.fetchApi('/v2/comfyui_manager/comfyui_switch_version', {
|
||||
method: 'POST',
|
||||
cache: "no-store",
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ver: selected_version,
|
||||
client_id: api.clientId,
|
||||
ui_id: api.clientId,
|
||||
}),
|
||||
});
|
||||
if (response.status == 200) {
|
||||
infoToast(`ComfyUI version is switched to ${selected_version}`);
|
||||
}
|
||||
@@ -797,7 +806,7 @@ function restartOrStop() {
|
||||
rebootAPI();
|
||||
}
|
||||
else {
|
||||
api.fetchApi('/v2/manager/queue/reset');
|
||||
api.fetchApi('/v2/manager/queue/reset', { method: 'POST' });
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
}
|
||||
@@ -951,7 +960,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
.then(data => { this.datasrc_combo.value = data; });
|
||||
|
||||
this.datasrc_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/db_mode?value=${event.target.value}`);
|
||||
api.fetchApi('/v2/manager/db_mode', { method: 'POST', body: JSON.stringify({value: event.target.value}) });
|
||||
});
|
||||
|
||||
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
|
||||
@@ -973,7 +982,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
channel_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/channel_url_list?value=${event.target.value}`);
|
||||
api.fetchApi('/v2/manager/channel_url_list', { method: 'POST', body: JSON.stringify({value: event.target.value}) });
|
||||
});
|
||||
|
||||
channel_combo.value = data.selected;
|
||||
@@ -1037,7 +1046,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
});
|
||||
|
||||
update_policy_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/policy/update?value=${event.target.value}`);
|
||||
api.fetchApi('/v2/manager/policy/update', { method: 'POST', body: JSON.stringify({value: event.target.value}) });
|
||||
});
|
||||
|
||||
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
|
||||
|
||||
@@ -172,7 +172,7 @@ export function rebootAPI() {
|
||||
customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => {
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
api.fetchApi("/v2/manager/reboot");
|
||||
api.fetchApi("/v2/manager/reboot", { method: 'POST' });
|
||||
}
|
||||
catch(exception) {}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ export class CustomNodesManager {
|
||||
|
||||
".cn-manager-stop": {
|
||||
click: () => {
|
||||
api.fetchApi('/v2/manager/queue/reset');
|
||||
api.fetchApi('/v2/manager/queue/reset', { method: 'POST' });
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -170,7 +170,7 @@ export class ModelManager {
|
||||
|
||||
".cmm-manager-stop": {
|
||||
click: () => {
|
||||
api.fetchApi('/v2/manager/queue/reset');
|
||||
api.fetchApi('/v2/manager/queue/reset', { method: 'POST' });
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ loadCss("./snapshot.css");
|
||||
async function restore_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
const response = await api.fetchApi(`/v2/snapshot/restore?target=${target}`, { cache: "no-store" });
|
||||
const response = await api.fetchApi(`/v2/snapshot/restore?target=${target}`, { method: 'POST', cache: "no-store" });
|
||||
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
@@ -37,7 +37,7 @@ async function restore_snapshot(target) {
|
||||
async function remove_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
const response = await api.fetchApi(`/v2/snapshot/remove?target=${target}`, { cache: "no-store" });
|
||||
const response = await api.fetchApi(`/v2/snapshot/remove?target=${target}`, { method: 'POST', cache: "no-store" });
|
||||
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
@@ -63,7 +63,7 @@ async function remove_snapshot(target) {
|
||||
|
||||
async function save_current_snapshot() {
|
||||
try {
|
||||
const response = await api.fetchApi('/v2/snapshot/save', { cache: "no-store" });
|
||||
const response = await api.fetchApi('/v2/snapshot/save', { method: 'POST', cache: "no-store" });
|
||||
app.ui.dialog.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,13 @@ from ..common.enums import NetworkMode, SecurityLevel, DBMode
|
||||
from ..common import context
|
||||
|
||||
|
||||
version_code = [4, 2]
|
||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||
try:
|
||||
from importlib.metadata import version as _pkg_version
|
||||
_raw_version = _pkg_version("comfyui-manager")
|
||||
except Exception:
|
||||
_raw_version = "unknown"
|
||||
|
||||
version_str = f"V{_raw_version}"
|
||||
|
||||
|
||||
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main"
|
||||
|
||||
@@ -39,6 +39,7 @@ comfyui_tag = None
|
||||
|
||||
SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_HIGH_P = "ERROR: To use this action, '--listen' must be set to a local IP and security_level must be 'normal-' or lower. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower."
|
||||
@@ -489,8 +490,12 @@ async def task_worker():
|
||||
res = core.unified_manager.unified_update(node_name, node_ver)
|
||||
|
||||
if res.ver == 'unknown':
|
||||
# unknown_active_nodes[node_id] = (url, fullpath) — url can be
|
||||
# None when git_utils.git_url() in manager_core can't determine
|
||||
# the remote URL. Downstream branches at L504/507 already
|
||||
# handle url is None, so we just need a None-safe title.
|
||||
url = core.unified_manager.unknown_active_nodes[node_name][0]
|
||||
title = os.path.basename(url)
|
||||
title = os.path.basename(url) if url else node_name
|
||||
else:
|
||||
url = core.unified_manager.cnr_map[node_name].get('repository')
|
||||
title = core.unified_manager.cnr_map[node_name]['name']
|
||||
@@ -549,6 +554,14 @@ async def task_worker():
|
||||
return "An error occurred while updating 'comfyui'."
|
||||
|
||||
async def do_fix(item) -> str:
|
||||
# Align check level with SECURITY_MESSAGE_HIGH_P (which names "high+"),
|
||||
# matching the parallel update in comfyui_manager/glob/manager_server.py
|
||||
# do_fix. Prior combo of `'high' + HIGH_P` logged a stricter-sounding
|
||||
# message than the gate actually enforced.
|
||||
if not is_allowed_security_level('high+'):
|
||||
logging.error(SECURITY_MESSAGE_HIGH_P)
|
||||
return 'failed'
|
||||
|
||||
ui_id, node_name, node_ver = item
|
||||
|
||||
try:
|
||||
@@ -896,8 +909,11 @@ async def fetch_updates(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/update_all")
|
||||
@routes.post("/v2/manager/queue/update_all")
|
||||
async def update_all(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
json_data = dict(request.rel_url.query)
|
||||
return await _update_all(json_data)
|
||||
|
||||
@@ -1155,8 +1171,11 @@ async def get_snapshot_list(request):
|
||||
return web.json_response({'items': items}, content_type='application/json')
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/remove")
|
||||
@routes.post("/v2/snapshot/remove")
|
||||
async def remove_snapshot(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
@@ -1178,8 +1197,11 @@ async def remove_snapshot(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/restore")
|
||||
@routes.post("/v2/snapshot/restore")
|
||||
async def restore_snapshot(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
if not is_allowed_security_level('middle+'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_P)
|
||||
return web.Response(status=403)
|
||||
@@ -1217,8 +1239,11 @@ async def get_current_snapshot_api(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/save")
|
||||
@routes.post("/v2/snapshot/save")
|
||||
async def save_snapshot(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
try:
|
||||
await core.save_snapshot_with_postfix('snapshot')
|
||||
return web.Response(status=200)
|
||||
@@ -1357,14 +1382,12 @@ async def import_fail_info_bulk(request):
|
||||
return web.Response(status=500, text="Internal server error")
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/reinstall")
|
||||
async def reinstall_custom_node(request):
|
||||
await uninstall_custom_node(request)
|
||||
await install_custom_node(request)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/reset")
|
||||
@routes.post("/v2/manager/queue/reset")
|
||||
async def reset_queue(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
global task_batch_queue
|
||||
global temp_queue_batch
|
||||
|
||||
@@ -1375,19 +1398,6 @@ async def reset_queue(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/abort_current")
|
||||
async def abort_queue(request):
|
||||
global task_batch_queue
|
||||
global temp_queue_batch
|
||||
|
||||
with task_worker_lock:
|
||||
temp_queue_batch = []
|
||||
if len(task_batch_queue) > 0:
|
||||
task_batch_queue[0].abort()
|
||||
task_batch_queue.popleft()
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/status")
|
||||
async def queue_count(request):
|
||||
@@ -1413,13 +1423,6 @@ async def queue_count(request):
|
||||
'is_processing': is_processing})
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/install")
|
||||
async def install_custom_node(request):
|
||||
json_data = await request.json()
|
||||
print(f"install={json_data}")
|
||||
return await _install_custom_node(json_data)
|
||||
|
||||
|
||||
async def _install_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle+'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_P)
|
||||
@@ -1482,8 +1485,11 @@ async def _install_custom_node(json_data):
|
||||
|
||||
task_worker_thread:threading.Thread = None
|
||||
|
||||
@routes.get("/v2/manager/queue/start")
|
||||
@routes.post("/v2/manager/queue/start")
|
||||
async def queue_start(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
with task_worker_lock:
|
||||
finalize_temp_queue_batch()
|
||||
return _queue_start()
|
||||
@@ -1500,12 +1506,6 @@ def _queue_start():
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/fix")
|
||||
async def fix_custom_node(request):
|
||||
json_data = await request.json()
|
||||
return await _fix_custom_node(json_data)
|
||||
|
||||
|
||||
async def _fix_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_GENERAL)
|
||||
@@ -1557,12 +1557,6 @@ async def install_custom_node_pip(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/uninstall")
|
||||
async def uninstall_custom_node(request):
|
||||
json_data = await request.json()
|
||||
return await _uninstall_custom_node(json_data)
|
||||
|
||||
|
||||
async def _uninstall_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
@@ -1583,12 +1577,6 @@ async def _uninstall_custom_node(json_data):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/update")
|
||||
async def update_custom_node(request):
|
||||
json_data = await request.json()
|
||||
return await _update_custom_node(json_data)
|
||||
|
||||
|
||||
async def _update_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
@@ -1607,8 +1595,11 @@ async def _update_custom_node(json_data):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/update_comfyui")
|
||||
@routes.post("/v2/manager/queue/update_comfyui")
|
||||
async def update_comfyui(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
is_stable = core.get_config()['update_policy'] != 'nightly-comfyui'
|
||||
temp_queue_batch.append(("update-comfyui", ('comfyui', is_stable)))
|
||||
return web.Response(status=200)
|
||||
@@ -1625,24 +1616,28 @@ async def comfyui_versions(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/comfyui_manager/comfyui_switch_version")
|
||||
@routes.post("/v2/comfyui_manager/comfyui_switch_version")
|
||||
async def comfyui_switch_version(request):
|
||||
try:
|
||||
if "ver" in request.rel_url.query:
|
||||
core.switch_comfyui(request.rel_url.query['ver'])
|
||||
# Body-reading handler — Content-Type gate omitted per
|
||||
# comfyui_manager/common/manager_security.py module policy: a cross-origin
|
||||
# <form method=POST> cannot forge a valid application/json body because
|
||||
# the browser would trigger a CORS preflight that this server refuses.
|
||||
if not is_allowed_security_level('high+'):
|
||||
logging.error(SECURITY_MESSAGE_HIGH_P)
|
||||
return web.Response(status=403)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
ver = data.get('ver')
|
||||
if not ver:
|
||||
return web.Response(status=400, text="missing 'ver' field")
|
||||
core.switch_comfyui(ver)
|
||||
return web.Response(status=200)
|
||||
except json.JSONDecodeError:
|
||||
return web.Response(status=400, text="Invalid JSON body")
|
||||
except Exception as e:
|
||||
logging.error(f"ComfyUI update fail: {e}", file=sys.stderr)
|
||||
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/disable")
|
||||
async def disable_node(request):
|
||||
json_data = await request.json()
|
||||
await _disable_node(json_data)
|
||||
return web.Response(status=200)
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
async def _disable_node(json_data):
|
||||
@@ -1712,48 +1707,88 @@ async def _install_model(json_data):
|
||||
|
||||
@routes.get("/v2/manager/db_mode")
|
||||
async def db_mode(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_db_mode(request.rel_url.query['value'])
|
||||
core.write_config()
|
||||
else:
|
||||
return web.Response(text=core.get_config()['db_mode'], status=200)
|
||||
return web.Response(text=core.get_config()['db_mode'], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@routes.post("/v2/manager/db_mode")
|
||||
async def set_db_mode_api(request):
|
||||
# Config writes are at the same risk tier as uninstall/update — apply the
|
||||
# 'middle' gate consistent with snapshot/remove, etc. Content-Type gate is
|
||||
# NOT applied here: this handler consumes application/json and a
|
||||
# cross-origin <form method=POST> cannot forge that without triggering
|
||||
# CORS preflight (see module docstring in common/manager_security.py).
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
try:
|
||||
data = await request.json()
|
||||
set_db_mode(data['value'])
|
||||
core.write_config()
|
||||
return web.Response(status=200)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return web.Response(status=400, text='Invalid request')
|
||||
|
||||
|
||||
@routes.get("/v2/manager/policy/update")
|
||||
async def update_policy(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_update_policy(request.rel_url.query['value'])
|
||||
core.write_config()
|
||||
else:
|
||||
return web.Response(text=core.get_config()['update_policy'], status=200)
|
||||
return web.Response(text=core.get_config()['update_policy'], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@routes.post("/v2/manager/policy/update")
|
||||
async def set_update_policy_api(request):
|
||||
# See set_db_mode_api above for gate rationale.
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
try:
|
||||
data = await request.json()
|
||||
set_update_policy(data['value'])
|
||||
core.write_config()
|
||||
return web.Response(status=200)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return web.Response(status=400, text='Invalid request')
|
||||
|
||||
|
||||
@routes.get("/v2/manager/channel_url_list")
|
||||
async def channel_url_list(request):
|
||||
channels = core.get_channel_dict()
|
||||
if "value" in request.rel_url.query:
|
||||
channel_url = channels.get(request.rel_url.query['value'])
|
||||
if channel_url is not None:
|
||||
core.get_config()['channel_url'] = channel_url
|
||||
core.write_config()
|
||||
else:
|
||||
selected = 'custom'
|
||||
selected_url = core.get_config()['channel_url']
|
||||
selected = 'custom'
|
||||
selected_url = core.get_config()['channel_url']
|
||||
|
||||
for name, url in channels.items():
|
||||
if url == selected_url:
|
||||
selected = name
|
||||
break
|
||||
for name, url in channels.items():
|
||||
if url == selected_url:
|
||||
selected = name
|
||||
break
|
||||
|
||||
res = {'selected': selected,
|
||||
'list': core.get_channel_list()}
|
||||
return web.json_response(res, status=200)
|
||||
res = {'selected': selected,
|
||||
'list': core.get_channel_list()}
|
||||
return web.json_response(res, status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@routes.post("/v2/manager/channel_url_list")
|
||||
async def set_channel_url(request):
|
||||
# See set_db_mode_api above for gate rationale.
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
try:
|
||||
data = await request.json()
|
||||
channels = core.get_channel_dict()
|
||||
channel_url = channels.get(data['value'])
|
||||
if channel_url is None:
|
||||
# Reject unknown channel name explicitly instead of silent no-op.
|
||||
# Parity with glob set_channel_url (comfyui_manager/glob/manager_server.py)
|
||||
# and with set_db_mode / set_update_policy whitelist enforcement.
|
||||
return web.Response(
|
||||
status=400,
|
||||
text=f"Invalid channel name {data['value']!r}; "
|
||||
f"must be one of {sorted(channels.keys())}",
|
||||
)
|
||||
core.get_config()['channel_url'] = channel_url
|
||||
core.write_config()
|
||||
return web.Response(status=200)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return web.Response(status=400, text='Invalid request')
|
||||
|
||||
|
||||
def add_target_blank(html_text):
|
||||
@@ -1817,13 +1852,12 @@ async def get_notice(request):
|
||||
|
||||
|
||||
# legacy /manager/notice
|
||||
@routes.get("/manager/notice")
|
||||
async def get_notice_legacy(request):
|
||||
return web.Response(text="""<font color="red">Starting from ComfyUI-Manager V4.0+, it should be installed via pip.<BR><BR>Please remove the ComfyUI-Manager installed in the <font color="white">'custom_nodes'</font> directory.</font>""", status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/reboot")
|
||||
def restart(self):
|
||||
@routes.post("/v2/manager/reboot")
|
||||
def restart(request):
|
||||
rejection = manager_security.reject_simple_form_post(request)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
@@ -1937,9 +1971,13 @@ if not os.path.exists(context.manager_config_path):
|
||||
|
||||
|
||||
# policy setup
|
||||
manager_security.add_handler_policy(reinstall_custom_node, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(install_custom_node, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(fix_custom_node, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
# WI-V: removed stale references to reinstall_custom_node / install_custom_node /
|
||||
# fix_custom_node — these legacy handlers were deleted in prior refactors
|
||||
# (likely the CSRF POST-conversion / security level work). Their remaining
|
||||
# add_handler_policy() calls raised NameError at module import, aborting
|
||||
# `comfyui_manager.start()`'s legacy branch before EXTENSION_WEB_DIRS could
|
||||
# register the legacy-UI JS directory — which is why the legacy Manager
|
||||
# button never rendered in the ComfyUI toolbar for Playwright tests.
|
||||
manager_security.add_handler_policy(install_custom_node_git_url, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(install_custom_node_pip, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(install_model, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
|
||||
Reference in New Issue
Block a user