Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions tests/cli/test_frontend_agent_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ def test_agent_surface_rewrite_only_changes_the_agent_base_path() -> None:
assert f'src="{prefix}/hermes/assets/app.js"' in rewritten


def test_hermes_surface_rewrite_removes_private_gateway_query() -> None:
prefix = "/web/hermes/sessions/session-1/surface/token-1"
body = (
b'<script src="/hermes/assets/app.js?faasInstanceName=hermes-instance'
b'&amp;Authorization=hermes-secret&amp;theme=dark"></script>'
b'<link href="./assets/app.css?Authorization=hermes-secret" rel="stylesheet">'
b'<link href="./favicon.ico?faasInstanceName=hermes-instance" rel="icon">'
)

rewritten = _rewrite_body(body, "text/html", prefix, "hermes").decode()

assert f'src="{prefix}/hermes/assets/app.js?theme=dark"' in rewritten
assert 'href="./assets/app.css"' in rewritten
assert 'href="./favicon.ico"' in rewritten
assert "faasInstanceName" not in rewritten
assert "Authorization" not in rewritten
assert "hermes-instance" not in rewritten
assert "hermes-secret" not in rewritten


def test_agent_surface_proxy_keeps_endpoint_auth_server_side(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
45 changes: 42 additions & 3 deletions veadk/cli/frontend_agent_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import asyncio
import contextlib
import html
import re
from collections.abc import Callable
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

Expand All @@ -33,6 +35,11 @@

_MAX_BODY_BYTES = 16 * 1024 * 1024
_TEXT_CONTENT_TYPES = ("text/", "javascript", "json", "manifest", "xml")
_GATEWAY_QUERY_KEYS = frozenset({"authorization", "faasinstancename"})
_HERMES_QUERY_URL_PATTERN = re.compile(
rb"[^\s\"'`<>()]+\?[^\s\"'`<>()]+",
re.IGNORECASE,
)
_OPENCLAW_RESET_TAG = (
b"<script>try{for(const key of Object.keys(localStorage)){"
b"if(key.startsWith('openclaw.control.settings.v1'))localStorage.removeItem(key)"
Expand Down Expand Up @@ -77,6 +84,8 @@ def _rewrite_body(
) -> bytes:
if not any(marker in content_type for marker in _TEXT_CONTENT_TYPES):
return body
if kind == "hermes":
body = _strip_hermes_gateway_query(body)
source = f"/{kind}".encode()
replacement = f"{prefix}/{kind}".encode()
for quote in (b'"', b"'", b"`"):
Expand All @@ -87,6 +96,38 @@ def _rewrite_body(
return body


def _strip_hermes_gateway_query(body: bytes) -> bytes:
"""Keep private Hermes endpoint routing parameters out of browser URLs."""

def _rewrite_url(match: re.Match[bytes]) -> bytes:
try:
raw_url = html.unescape(match.group(0).decode("utf-8"))
except UnicodeDecodeError:
return match.group(0)
parsed = urlsplit(raw_url)
if not parsed.query:
return match.group(0)
query = parse_qsl(parsed.query, keep_blank_values=True)
public_query = [
(key, value)
for key, value in query
if key.lower() not in _GATEWAY_QUERY_KEYS
]
if len(public_query) == len(query):
return match.group(0)
return urlunsplit(
(
parsed.scheme,
parsed.netloc,
parsed.path,
urlencode(public_query),
parsed.fragment,
)
).encode("utf-8")

return _HERMES_QUERY_URL_PATTERN.sub(_rewrite_url, body)


def _rewrite_location(location: str, prefix: str) -> str:
if not location:
return location
Expand All @@ -97,15 +138,13 @@ def _rewrite_location(location: str, prefix: str) -> str:
[
(key, value)
for key, value in parse_qsl(parsed.query, keep_blank_values=True)
if key.lower() not in {"authorization", "faasinstancename"}
if key.lower() not in _GATEWAY_QUERY_KEYS
]
)
return urlunsplit(("", "", f"{prefix}{parsed.path}", safe_query, parsed.fragment))


def _rewrite_cookie(cookie: str, prefix: str) -> str:
import re

if re.search(r"(?i);\s*path=", cookie):
return re.sub(
r"(?i)(;\s*path=)(/[^;]*)",
Expand Down
Loading