From 8fb037d4d4b4b38968fbfde4916f3b3271ee46a6 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 11 Nov 2025 18:13:47 -0500 Subject: [PATCH] wrap all internal api calls with auth check and use token when possible Signed-off-by: Vladimir Mandic --- .eslintrc.json | 1 + CHANGELOG.md | 1 + TODO.md | 5 +++++ javascript/authWrap.js | 20 ++++++++++++++++++++ javascript/civitai.js | 2 +- javascript/contextMenus.js | 2 +- javascript/extraNetworks.js | 2 +- javascript/gallery.js | 6 +++--- javascript/gpu.js | 2 +- javascript/history.js | 2 +- javascript/loader.js | 2 +- javascript/logMonitor.js | 4 ++-- javascript/logger.js | 6 +++--- javascript/monitor.js | 2 +- javascript/settings.js | 2 +- javascript/ui.js | 2 +- modules/api/api.py | 7 +++++-- modules/api/middleware.py | 3 ++- modules/ui_extra_networks.py | 8 ++++---- 19 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 javascript/authWrap.js diff --git a/.eslintrc.json b/.eslintrc.json index af846df83..90efb00d3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -50,6 +50,7 @@ }, "globals": { "panzoom": "readonly", + "authFetch": "readonly", "log": "readonly", "debug": "readonly", "error": "readonly", diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6523740..39064590d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ TBD - update global lint rules - chrono: switch to official pipeline - pipeline: add optional preprocess and postprocess hooks + - auth: wrap all internal api calls with auth check and use token when possible - **Fixes** - hires: strength save/load in metadata, thanks @awsr - imgi2img: fix initial scale tab, thanks @awsr diff --git a/TODO.md b/TODO.md index 9405eb231..c87839c25 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,11 @@ - +## Kanvas + +- server-side mask handling vs ui mask handling +- outpaint visible image edge seam + ## Internal - UI: New inpaint/outpaint interface diff --git a/javascript/authWrap.js b/javascript/authWrap.js new file mode 100644 index 000000000..d5f78f7b6 --- /dev/null +++ b/javascript/authWrap.js @@ -0,0 +1,20 @@ +let user = null; +let token = null; + +async function authFetch(url, options = {}) { + if (!token) { + const res = await fetch(`${window.subpath}/token`); + if (res.ok) { + const data = await res.json(); + user = data.user; + token = data.token; + } + } + if (user && token) { + if (!options.headers) options.headers = {}; + const encoded = btoa(`${user}:${token}`); + options.headers.Authorization = `Basic ${encoded}`; + } + const res = await fetch(url, options); + return res; +} diff --git a/javascript/civitai.js b/javascript/civitai.js index 71e957380..cf86c069c 100644 --- a/javascript/civitai.js +++ b/javascript/civitai.js @@ -73,7 +73,7 @@ async function modelCardClick(id) { log('modelCardClick id', id); const el = gradioApp().getElementById('model-details') || gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome'); if (!el) return; - const res = await fetch(`${window.api}/civitai?model_id=${encodeURI(id)}`); + const res = await authFetch(`${window.api}/civitai?model_id=${encodeURI(id)}`); if (!res || res.status !== 200) { error(`modelCardClick: id=${id} status=${res ? res.status : 'unknown'}`); return; diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js index 96c57a231..26d10d522 100644 --- a/javascript/contextMenus.js +++ b/javascript/contextMenus.js @@ -132,7 +132,7 @@ const getStatus = async () => { log('progressInternal:', data); if (el) el.innerText += '\nProgress internal:\n' + JSON.stringify(data, null, 2); // eslint-disable-line prefer-template } - res = await fetch('./sdapi/v1/progress?skip_current_image=true', { method: 'GET', headers }); + res = await authFetch('./sdapi/v1/progress?skip_current_image=true', { method: 'GET', headers }); if (res?.ok) { data = await res.json(); log('progressAPI:', data); diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 204422d78..5e884dba5 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -368,7 +368,7 @@ function selectHistory(id) { const headers = new Headers(); headers.set('Content-Type', 'application/json'); const init = { method: 'POST', body: { name: id }, headers }; - fetch(`${window.api}/history`, { method: 'POST', body: JSON.stringify({ name: id }), headers }); + authFetch(`${window.api}/history`, { method: 'POST', body: JSON.stringify({ name: id }), headers }); } let enDirty = false; diff --git a/javascript/gallery.js b/javascript/gallery.js index 38797551b..da4da5fd3 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -185,7 +185,7 @@ async function delayFetchThumb(fn) { while (outstanding > 16) await new Promise((resolve) => setTimeout(resolve, 50)); // eslint-disable-line no-promise-executor-return outstanding++; const ts = Date.now().toString(); - const res = await fetch(`${window.api}/browser/thumb?file=${encodeURI(fn)}&ts=${ts}`, { priority: 'low' }); + const res = await authFetch(`${window.api}/browser/thumb?file=${encodeURI(fn)}&ts=${ts}`, { priority: 'low' }); if (!res.ok) { error(`fetchThumb: ${res.statusText}`); outstanding--; @@ -552,7 +552,7 @@ async function fetchFilesHT(evt) { updateStatusWithSort(`Folder: ${evt.target.name} | in-progress`); let numFiles = 0; - const res = await fetch(`${window.api}/browser/files?folder=${encodeURI(evt.target.name)}`); + const res = await authFetch(`${window.api}/browser/files?folder=${encodeURI(evt.target.name)}`); if (!res || res.status !== 200) { updateStatusWithSort(`Folder: ${evt.target.name} | failed: ${res?.statusText}`); return; @@ -639,7 +639,7 @@ async function pruneImages() { async function galleryVisible() { // if (el.folders.children.length > 0) return; - const res = await fetch(`${window.api}/browser/folders`); + const res = await authFetch(`${window.api}/browser/folders`); if (!res || res.status !== 200) return; el.folders.innerHTML = ''; url = res.url.split('/sdapi')[0].replace('http', 'ws'); // update global url as ws need fqdn diff --git a/javascript/gpu.js b/javascript/gpu.js index 724299375..6e73c7fc9 100644 --- a/javascript/gpu.js +++ b/javascript/gpu.js @@ -30,7 +30,7 @@ async function updateGPU() { const gpuEl = document.getElementById('gpu'); const gpuTable = document.getElementById('gpu-table'); try { - const res = await fetch(`${window.api}/gpu`); + const res = await authFetch(`${window.api}/gpu`); if (!res.ok) { clearInterval(gpuInterval); gpuEl.style.display = 'none'; diff --git a/javascript/history.js b/javascript/history.js index 2a40b0859..c7429ff2b 100644 --- a/javascript/history.js +++ b/javascript/history.js @@ -3,7 +3,7 @@ const ioTypes = ['load', 'save']; function refreshHistory() { log('refreshHistory'); - fetch(`${window.api}/history`, { priority: 'low' }).then((res) => { + authFetch(`${window.api}/history`, { priority: 'low' }).then((res) => { const timeline = document.getElementById('history_timeline'); const table = document.getElementById('history_table'); timeline.innerHTML = ''; diff --git a/javascript/loader.js b/javascript/loader.js index 6d0ddb160..597854fd1 100644 --- a/javascript/loader.js +++ b/javascript/loader.js @@ -52,7 +52,7 @@ async function createSplash() { } const imgEl = `
`; document.getElementById('splash').insertAdjacentHTML('afterbegin', imgEl); - fetch(`${window.api}/motd`) + authFetch(`${window.api}/motd`) .then((res) => res.text()) .then((text) => { const motdEl = document.getElementById('motd'); diff --git a/javascript/logMonitor.js b/javascript/logMonitor.js index 859d9d40b..c3bcf5673 100644 --- a/javascript/logMonitor.js +++ b/javascript/logMonitor.js @@ -74,7 +74,7 @@ async function logMonitor() { if (!logMonitorEl) return; const atBottom = logMonitorEl.scrollHeight <= (logMonitorEl.scrollTop + logMonitorEl.clientHeight); try { - const res = await fetch(`${window.api}/log?clear=True`); + const res = await authFetch(`${window.api}/log?clear=True`); if (res?.ok) { logMonitorStatus = true; const lines = await res.json(); @@ -123,7 +123,7 @@ async function initLogMonitor() { `; el.style.display = 'none'; - fetch(`${window.api}/start?agent=${encodeURI(navigator.userAgent)}`); + authFetch(`${window.api}/start?agent=${encodeURI(navigator.userAgent)}`); logMonitor(); log('initLogMonitor'); } diff --git a/javascript/logger.js b/javascript/logger.js index d917178bb..c4ac1d78d 100644 --- a/javascript/logger.js +++ b/javascript/logger.js @@ -54,11 +54,11 @@ const xhrInternal = (xhrObj, data, handler = undefined, errorHandler = undefined try { const json = JSON.parse(xhrObj.responseText); if (handler) handler(json); - } catch (e) { - error(`xhr.onreadystatechange: ${e}`); + } catch { + // error(`xhr.onreadystatechange: ${e}`); } } else { - err(`xhr.onreadystatechange: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`); + // err(`xhr.onreadystatechange: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`); } } }; diff --git a/javascript/monitor.js b/javascript/monitor.js index 276e4bc0e..6cd815945 100644 --- a/javascript/monitor.js +++ b/javascript/monitor.js @@ -23,7 +23,7 @@ async function updateIndicator(online, data, msg) { async function monitorConnection() { try { - const res = await fetch(`${window.api}/version`); + const res = await authFetch(`${window.api}/version`); const data = await res.json(); const url = res.url.split('/sdapi')[0].replace('http', 'ws'); // update global url as ws need fqdn const ws = new WebSocket(`${url}/queue/join`); diff --git a/javascript/settings.js b/javascript/settings.js index 540e65af0..96bdd24a9 100644 --- a/javascript/settings.js +++ b/javascript/settings.js @@ -164,7 +164,7 @@ async function initModels() { const el = gradioApp().getElementById('main_info'); const en = gradioApp().getElementById('txt2img_extra_networks'); if (!el || !en) return; - const req = await fetch(`${window.api}/sd-models`); + const req = await authFetch(`${window.api}/sd-models`); const res = req.ok ? await req.json() : []; log('initModels', res.length); const ready = () => ` diff --git a/javascript/ui.js b/javascript/ui.js index 8c5cd6630..a89c3a249 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -463,7 +463,7 @@ function monitorServerStatus() { function restartReload() { document.body.style = 'background: #222222; font-size: 1rem; font-family:monospace; margin-top:20%; color:lightgray; text-align:center'; document.body.innerHTML = '

Server shutdown in progress...

'; - fetch(`${window.api}/progress?skip_current_image=true`) + authFetch(`${window.api}/progress?skip_current_image=true`) .then((res) => setTimeout(restartReload, 1000)) .catch((e) => setTimeout(monitorServerStatus, 500)); return []; diff --git a/modules/api/api.py b/modules/api/api.py index ca03f3433..ed43190ff 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -117,8 +117,8 @@ class Api: from modules.civitai import api_civitai api_civitai.register_api() - def add_api_route(self, path: str, fn, **kwargs): - if self.credentials: + def add_api_route(self, path: str, fn, auth: bool = True, **kwargs): + if auth and self.credentials: deps = list(kwargs.get('dependencies', [])) deps.append(Depends(self.auth)) kwargs['dependencies'] = deps @@ -132,6 +132,9 @@ class Api: if credentials.username in self.credentials: if compare_digest(credentials.password, self.credentials[credentials.username]): return True + if hasattr(self.app, 'tokens') and (self.app.tokens is not None): + if credentials.password in self.app.tokens.keys(): + return True shared.log.error(f'API authentication: user="{credentials.username}" password="{credentials.password}"') raise HTTPException(status_code=401, detail="Unauthorized", headers={"WWW-Authenticate": "Basic"}) diff --git a/modules/api/middleware.py b/modules/api/middleware.py index 6270a9ff2..8dc10e31e 100644 --- a/modules/api/middleware.py +++ b/modules/api/middleware.py @@ -81,7 +81,8 @@ def setup_middleware(app: FastAPI, cmd_opts): if err['code'] == 404 and 'file=html/' in req.url.path: # dont spam with locales return JSONResponse(status_code=err['code'], content=jsonable_encoder(err)) - log.error(f"API error: {req.method}: {req.url} {err}") + if not any([req.url.path.endswith(x) for x in ignore_endpoints]): # noqa C419 # pylint: disable=use-a-generator + log.error(f"API error: {req.method}: {req.url} {err}") if not isinstance(e, HTTPException) and err['error'] != 'TypeError': # do not print backtrace on known httpexceptions errors.display(e, 'HTTP API', [anyio, fastapi, uvicorn, starlette]) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index d5d5804cb..cf7035847 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -115,10 +115,10 @@ def init_api(): return JSONResponse(obj) shared.api.add_api_route("/sdapi/v1/network", get_network, methods=["GET"]) - shared.api.add_api_route("/sdapi/v1/network/thumb", fetch_file, methods=["GET"]) - shared.api.add_api_route("/sdapi/v1/network/metadata", get_metadata, methods=["GET"]) - shared.api.add_api_route("/sdapi/v1/network/info", get_info, methods=["GET"]) - shared.api.add_api_route("/sdapi/v1/network/desc", get_desc, methods=["GET"]) + shared.api.add_api_route("/sdapi/v1/network/thumb", fetch_file, methods=["GET"], auth=False) + shared.api.add_api_route("/sdapi/v1/network/metadata", get_metadata, methods=["GET"], auth=False) + shared.api.add_api_route("/sdapi/v1/network/info", get_info, methods=["GET"], auth=False) + shared.api.add_api_route("/sdapi/v1/network/desc", get_desc, methods=["GET"], auth=False) class DateTimeEncoder(json.JSONEncoder):