fix: route WeKnora embed through backend prefix
This commit is contained in:
1 parent
6dbaf40f99
commit
54fd0e7316
5 files changed
+81
-17
No files matched your search
+18
-9
@@ -55,6 +55,23 @@ function postThemeToFrame(frame: HTMLIFrameElement | null) {
|
|||||||
frame.contentWindow.postMessage(collectThemePayload(), targetOrigin);
|
frame.contentWindow.postMessage(collectThemePayload(), targetOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WEKNORA_BACKEND_PREFIX = "/deerflow";
|
||||||
|
|
||||||
|
function buildFrameUrl(framePath: string) {
|
||||||
|
const backendBase = getBackendBaseURL().replace(/\/+$/, "");
|
||||||
|
const backendOrigin = backendBase
|
||||||
|
? new URL(backendBase, window.location.origin).origin
|
||||||
|
: window.location.origin;
|
||||||
|
if (/^https?:\/\//i.test(framePath)) return new URL(framePath);
|
||||||
|
if (framePath.startsWith("/")) {
|
||||||
|
const path = !framePath.startsWith(`${WEKNORA_BACKEND_PREFIX}/`)
|
||||||
|
? `${WEKNORA_BACKEND_PREFIX}${framePath}`
|
||||||
|
: framePath;
|
||||||
|
return new URL(path, backendOrigin);
|
||||||
|
}
|
||||||
|
return new URL(`${backendBase || backendOrigin}/${framePath.replace(/^\/+/, "")}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function WeKnoraKnowledgeDetailPage() {
|
export function WeKnoraKnowledgeDetailPage() {
|
||||||
const {
|
const {
|
||||||
knowledge_base_id: strategyKnowledgeBaseId,
|
knowledge_base_id: strategyKnowledgeBaseId,
|
||||||
@@ -88,15 +105,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
|||||||
.then((session) => {
|
.then((session) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const framePath = session.frame_path || "";
|
const framePath = session.frame_path || "";
|
||||||
const backendBase = getBackendBaseURL().replace(/\/+$/, "");
|
const nextUrl = buildFrameUrl(framePath);
|
||||||
const backendOrigin = backendBase
|
|
||||||
? new URL(backendBase, window.location.origin).origin
|
|
||||||
: window.location.origin;
|
|
||||||
const nextUrl = /^https?:\/\//i.test(framePath)
|
|
||||||
? new URL(framePath)
|
|
||||||
: framePath.startsWith("/")
|
|
||||||
? new URL(framePath, backendOrigin)
|
|
||||||
: new URL(`${backendBase || backendOrigin}/${framePath.replace(/^\/+/, "")}`);
|
|
||||||
nextUrl.searchParams.set("theme", collectThemePayload().theme);
|
nextUrl.searchParams.set("theme", collectThemePayload().theme);
|
||||||
setFrameSrc(nextUrl.toString());
|
setFrameSrc(nextUrl.toString());
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1647,6 +1647,7 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
|||||||
# /assets, /locales and /api/v1/*. Register that proxy last so it stays a
|
# /assets, /locales and /api/v1/*. Register that proxy last so it stays a
|
||||||
# true fallback and cannot intercept DeerFlow APIs.
|
# true fallback and cannot intercept DeerFlow APIs.
|
||||||
app.include_router(llmwiki.proxy_router)
|
app.include_router(llmwiki.proxy_router)
|
||||||
|
app.include_router(llmwiki.proxy_router, prefix="/deerflow")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ proxy_router = APIRouter(tags=["llmwiki-proxy"])
|
|||||||
|
|
||||||
_MAX_UPLOAD_BYTES = 100 * 1024 * 1024
|
_MAX_UPLOAD_BYTES = 100 * 1024 * 1024
|
||||||
_WEKNORA_ADMIN_SESSION_CACHE: dict[str, dict[str, Any]] = {}
|
_WEKNORA_ADMIN_SESSION_CACHE: dict[str, dict[str, Any]] = {}
|
||||||
|
_WEKNORA_PUBLIC_PREFIX = "/deerflow"
|
||||||
_HOP_BY_HOP_RESPONSE_HEADERS = {
|
_HOP_BY_HOP_RESPONSE_HEADERS = {
|
||||||
"connection",
|
"connection",
|
||||||
"content-encoding",
|
"content-encoding",
|
||||||
@@ -517,12 +518,7 @@ def _normalize_public_prefix(value: str | None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _request_public_prefix(request: Request) -> str:
|
def _request_public_prefix(request: Request) -> str:
|
||||||
headers = request.headers
|
return _WEKNORA_PUBLIC_PREFIX
|
||||||
for key in ("x-forwarded-prefix", "x-script-name"):
|
|
||||||
prefix = _normalize_public_prefix(headers.get(key))
|
|
||||||
if prefix:
|
|
||||||
return prefix
|
|
||||||
return _normalize_public_prefix(str(request.scope.get("root_path") or ""))
|
|
||||||
|
|
||||||
|
|
||||||
def _with_public_prefix(prefix: str, path: str) -> str:
|
def _with_public_prefix(prefix: str, path: str) -> str:
|
||||||
@@ -531,6 +527,14 @@ def _with_public_prefix(prefix: str, path: str) -> str:
|
|||||||
return f"{prefix}{path}" if prefix else path
|
return f"{prefix}{path}" if prefix else path
|
||||||
|
|
||||||
|
|
||||||
|
def _without_public_prefix(prefix: str, path: str) -> str:
|
||||||
|
if prefix and path.startswith(f"{prefix}/"):
|
||||||
|
return path[len(prefix):]
|
||||||
|
if prefix and path == prefix:
|
||||||
|
return "/"
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def _rewrite_weknora_html_public_paths(html: str, public_prefix: str) -> str:
|
def _rewrite_weknora_html_public_paths(html: str, public_prefix: str) -> str:
|
||||||
if not public_prefix:
|
if not public_prefix:
|
||||||
return html
|
return html
|
||||||
@@ -1538,7 +1542,10 @@ async def proxy_weknora_root_asset(request: Request) -> Response:
|
|||||||
return await _proxy_weknora_request(
|
return await _proxy_weknora_request(
|
||||||
request,
|
request,
|
||||||
target_base_url=runtime.web_base_url,
|
target_base_url=runtime.web_base_url,
|
||||||
upstream_path=str(request.scope.get("path") or request.url.path),
|
upstream_path=_without_public_prefix(
|
||||||
|
_request_public_prefix(request),
|
||||||
|
str(request.scope.get("path") or request.url.path),
|
||||||
|
),
|
||||||
ctx=ctx,
|
ctx=ctx,
|
||||||
is_api=False,
|
is_api=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class WeKnoraEmbedContext:
|
|||||||
|
|
||||||
|
|
||||||
def is_weknora_embed_proxy_path(path: str) -> bool:
|
def is_weknora_embed_proxy_path(path: str) -> bool:
|
||||||
|
path = path.removeprefix("/deerflow")
|
||||||
return (
|
return (
|
||||||
path == "/platform"
|
path == "/platform"
|
||||||
or path.startswith("/platform/")
|
or path.startswith("/platform/")
|
||||||
|
|||||||
@@ -173,15 +173,25 @@ def test_weknora_embed_cookie_bypasses_only_proxy_paths() -> None:
|
|||||||
cookies = {"deerflow_weknora_embed": raw_cookie}
|
cookies = {"deerflow_weknora_embed": raw_cookie}
|
||||||
|
|
||||||
assert auth_middleware._is_weknora_embed_proxy_request(_request("/platform/knowledge-bases/kb", cookies=cookies))
|
assert auth_middleware._is_weknora_embed_proxy_request(_request("/platform/knowledge-bases/kb", cookies=cookies))
|
||||||
|
assert auth_middleware._is_weknora_embed_proxy_request(
|
||||||
|
_request("/deerflow/platform/knowledge-bases/kb", cookies=cookies)
|
||||||
|
)
|
||||||
assert auth_middleware._is_weknora_embed_proxy_request(_request("/assets/index.js", cookies=cookies))
|
assert auth_middleware._is_weknora_embed_proxy_request(_request("/assets/index.js", cookies=cookies))
|
||||||
|
assert auth_middleware._is_weknora_embed_proxy_request(_request("/deerflow/assets/index.js", cookies=cookies))
|
||||||
assert auth_middleware._is_weknora_embed_proxy_request(_request("/config.js", cookies=cookies))
|
assert auth_middleware._is_weknora_embed_proxy_request(_request("/config.js", cookies=cookies))
|
||||||
assert auth_middleware._is_weknora_embed_proxy_request(
|
assert auth_middleware._is_weknora_embed_proxy_request(
|
||||||
_request("/tdesign-icons/0.4.1/fonts/index.js", cookies=cookies)
|
_request("/tdesign-icons/0.4.1/fonts/index.js", cookies=cookies)
|
||||||
)
|
)
|
||||||
assert auth_middleware._is_weknora_embed_proxy_request(_request("/api/v1/knowledge-search", cookies=cookies))
|
assert auth_middleware._is_weknora_embed_proxy_request(_request("/api/v1/knowledge-search", cookies=cookies))
|
||||||
|
assert auth_middleware._is_weknora_embed_proxy_request(
|
||||||
|
_request("/deerflow/api/v1/knowledge-search", cookies=cookies)
|
||||||
|
)
|
||||||
assert auth_middleware._is_weknora_embed_proxy_request(
|
assert auth_middleware._is_weknora_embed_proxy_request(
|
||||||
_request("/api/llmwiki/weknora-embed/api/v1/auth/me", cookies=cookies)
|
_request("/api/llmwiki/weknora-embed/api/v1/auth/me", cookies=cookies)
|
||||||
)
|
)
|
||||||
|
assert auth_middleware._is_weknora_embed_proxy_request(
|
||||||
|
_request("/deerflow/api/llmwiki/weknora-embed/api/v1/auth/me", cookies=cookies)
|
||||||
|
)
|
||||||
assert csrf_middleware.is_weknora_embed_proxy_request(_request("/api/v1/knowledge-search", cookies=cookies))
|
assert csrf_middleware.is_weknora_embed_proxy_request(_request("/api/v1/knowledge-search", cookies=cookies))
|
||||||
assert not auth_middleware._is_weknora_embed_proxy_request(
|
assert not auth_middleware._is_weknora_embed_proxy_request(
|
||||||
_request("/api/v1/knowledge-search", cookies={"deerflow_weknora_embed": "signed"})
|
_request("/api/v1/knowledge-search", cookies={"deerflow_weknora_embed": "signed"})
|
||||||
@@ -219,7 +229,7 @@ async def test_weknora_session_endpoint_sets_cookie_and_returns_platform_path(mo
|
|||||||
)
|
)
|
||||||
|
|
||||||
payload = json.loads(response.body)
|
payload = json.loads(response.body)
|
||||||
assert payload["frame_path"] == "/platform/knowledge-bases/remote-kb?tab=wiki"
|
assert payload["frame_path"] == "/deerflow/platform/knowledge-bases/remote-kb?tab=wiki"
|
||||||
cookie = response.headers["set-cookie"]
|
cookie = response.headers["set-cookie"]
|
||||||
assert "deerflow_weknora_embed=" in cookie
|
assert "deerflow_weknora_embed=" in cookie
|
||||||
assert "Secure" in cookie
|
assert "Secure" in cookie
|
||||||
@@ -259,6 +269,36 @@ async def test_weknora_session_endpoint_preserves_reverse_proxy_prefix(monkeypat
|
|||||||
assert payload["frame_path"] == "/deerflow/platform/knowledge-bases/remote-kb?tab=wiki"
|
assert payload["frame_path"] == "/deerflow/platform/knowledge-bases/remote-kb?tab=wiki"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_weknora_session_endpoint_uses_backend_prefix_when_frontend_has_different_prefix(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
from app.gateway.routers import llmwiki as llmwiki_router
|
||||||
|
|
||||||
|
async def authorized_mapping(*_: object, **__: object) -> tuple[dict[str, str], str, bool]:
|
||||||
|
return {"id": "local-kb", "weknora_id": "remote-kb", "name": "KB"}, "user-a", False
|
||||||
|
|
||||||
|
class Store:
|
||||||
|
async def get_authorized(self, *_: object, **__: object) -> dict[str, str]:
|
||||||
|
return {"id": "local-kb", "weknora_id": "remote-kb"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(llmwiki_router, "_runtime_for_iframe_proxy", lambda _: object())
|
||||||
|
monkeypatch.setattr(llmwiki_router, "_authorized_mapping", authorized_mapping)
|
||||||
|
monkeypatch.setattr(llmwiki_router, "_store", lambda _: Store())
|
||||||
|
|
||||||
|
response = await llmwiki_router.create_weknora_detail_session(
|
||||||
|
_request(
|
||||||
|
"/api/llmwiki/knowledge-bases/local-kb/weknora/session",
|
||||||
|
request_headers={"Referer": "https://example.test/magentweb/page/workspace/knowledge/local-kb"},
|
||||||
|
),
|
||||||
|
"local-kb",
|
||||||
|
tab="wiki",
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = json.loads(response.body)
|
||||||
|
assert payload["frame_path"] == "/deerflow/platform/knowledge-bases/remote-kb?tab=wiki"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_weknora_client_keeps_anonymous_mode_without_admin_credentials() -> None:
|
async def test_weknora_client_keeps_anonymous_mode_without_admin_credentials() -> None:
|
||||||
requests: list[httpx.Request] = []
|
requests: list[httpx.Request] = []
|
||||||
@@ -1002,6 +1042,12 @@ def test_weknora_iframe_proxy_is_fallback_after_deerflow_api_routes() -> None:
|
|||||||
== "/api/llmwiki/weknora-embed/api/v1/{proxied_path:path}"
|
== "/api/llmwiki/weknora-embed/api/v1/{proxied_path:path}"
|
||||||
)
|
)
|
||||||
assert _first_gateway_match("GET", "/platform/knowledge-bases/remote-kb") == "/platform/{proxied_path:path}"
|
assert _first_gateway_match("GET", "/platform/knowledge-bases/remote-kb") == "/platform/{proxied_path:path}"
|
||||||
|
assert (
|
||||||
|
_first_gateway_match("GET", "/deerflow/platform/knowledge-bases/remote-kb")
|
||||||
|
== "/deerflow/platform/{proxied_path:path}"
|
||||||
|
)
|
||||||
|
assert _first_gateway_match("GET", "/deerflow/assets/index.js") == "/deerflow/assets/{proxied_path:path}"
|
||||||
|
assert _first_gateway_match("GET", "/deerflow/api/v1/knowledge-search") == "/deerflow/api/v1/{proxied_path:path}"
|
||||||
|
|
||||||
|
|
||||||
def test_weknora_iframe_hides_current_outer_sidebar_shell() -> None:
|
def test_weknora_iframe_hides_current_outer_sidebar_shell() -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user