Scope server routes by base path
This commit is contained in:
@@ -47,7 +47,7 @@ Summary:
|
||||
1. Copy repo to your server.
|
||||
2. Build client and publish `client/dist/` to your web root/subdirectory.
|
||||
3. Configure server `config.toml` and run it via `systemd`.
|
||||
4. Add Apache `/ws` websocket proxy from `deploy/apache/chgrid-vhost-snippet.conf`.
|
||||
4. Add base-path-scoped websocket/auth proxy routes from `deploy/apache/chgrid-vhost-snippet.conf`.
|
||||
|
||||
## Key Paths
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Maintainer-controlled web client version metadata.
|
||||
window.CHGRID_RELEASE_VERSION = "0.1.0";
|
||||
window.CHGRID_BUILD_REVISION = "R341";
|
||||
window.CHGRID_BUILD_REVISION = "R342";
|
||||
window.CHGRID_WEB_VERSION = `${window.CHGRID_RELEASE_VERSION} ${window.CHGRID_BUILD_REVISION}`;
|
||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||
|
||||
@@ -236,8 +236,8 @@ const SYSTEM_SOUND_URLS = {
|
||||
logout: withBase('sounds/logout.ogg'),
|
||||
notify: withBase('sounds/notify.ogg'),
|
||||
} as const;
|
||||
const AUTH_SESSION_COOKIE_SET_URL = '/auth/session/set';
|
||||
const AUTH_SESSION_COOKIE_CLEAR_URL = '/auth/session/clear';
|
||||
const AUTH_SESSION_COOKIE_SET_URL = withBase('auth/session/set');
|
||||
const AUTH_SESSION_COOKIE_CLEAR_URL = withBase('auth/session/clear');
|
||||
const AUTH_SESSION_COOKIE_CLIENT_HEADER = 'X-Chgrid-Auth-Client';
|
||||
const ACTION_SOUND_URL = withBase('sounds/action.ogg');
|
||||
const FOOTSTEP_SOUND_URLS = Array.from({ length: 11 }, (_, index) => withBase(`sounds/step-${index + 1}.ogg`));
|
||||
@@ -313,7 +313,7 @@ let activeTeleport:
|
||||
| null = null;
|
||||
|
||||
const signalingProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const signalingUrl = `${signalingProtocol}://${window.location.host}/ws`;
|
||||
const signalingUrl = `${signalingProtocol}://${window.location.host}${withBase('ws')}`;
|
||||
const signaling = new SignalingClient(signalingUrl, handleSignalingStatus);
|
||||
|
||||
const peerManager = new PeerManager(
|
||||
|
||||
@@ -79,21 +79,19 @@ cd "$REPO_ROOT"
|
||||
Expected proxy endpoints:
|
||||
|
||||
```apache
|
||||
ProxyPass /ws ws://127.0.0.1:8765
|
||||
ProxyPassReverse /ws ws://127.0.0.1:8765
|
||||
ProxyPass /auth/session/ http://127.0.0.1:8765/auth/session/
|
||||
ProxyPassReverse /auth/session/ http://127.0.0.1:8765/auth/session/
|
||||
ProxyPass /chgrid/ws ws://127.0.0.1:8765/chgrid/ws
|
||||
ProxyPassReverse /chgrid/ws ws://127.0.0.1:8765/chgrid/ws
|
||||
ProxyPass /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/
|
||||
ProxyPassReverse /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/
|
||||
```
|
||||
|
||||
The websocket server enforces browser origin matching against `CHGRID_HOST_ORIGIN`, so the public site origin must match that env var exactly.
|
||||
The `server.base_path` value in `config.toml` must match the published client path and the proxy paths above.
|
||||
|
||||
What each route does:
|
||||
- `/ws`: websocket signaling (presence, movement, item actions, chat, voice signaling).
|
||||
- `/auth/session/set`: called by client after successful login to set `HttpOnly` session cookie.
|
||||
- `/auth/session/clear`: called by client on logout/session-reset to clear `HttpOnly` session cookie.
|
||||
|
||||
Important:
|
||||
- Keep `/auth/session/*` at domain root even when the app is served from a subpath like `/chgrid`.
|
||||
- `<base_path>ws`: websocket signaling (presence, movement, item actions, chat, voice signaling).
|
||||
- `<base_path>auth/session/set`: called by client after successful login to set the instance-scoped `HttpOnly` session cookie.
|
||||
- `<base_path>auth/session/clear`: called by client on logout/session-reset to clear the instance-scoped `HttpOnly` session cookie.
|
||||
|
||||
After Apache changes, reload Apache using your host's command.
|
||||
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
# SetEnv CHGRID_HOST_ORIGIN https://example.com
|
||||
|
||||
# Proxy websocket signaling endpoint to local Python service.
|
||||
# `/ws` is used by the browser signaling client for realtime packets.
|
||||
ProxyPass /ws ws://127.0.0.1:8765
|
||||
ProxyPassReverse /ws ws://127.0.0.1:8765
|
||||
# Replace `/chgrid/` with the same value configured in `server.base_path`.
|
||||
ProxyPass /chgrid/ws ws://127.0.0.1:8765/chgrid/ws
|
||||
ProxyPassReverse /chgrid/ws ws://127.0.0.1:8765/chgrid/ws
|
||||
# Proxy auth cookie helper endpoints to local Python service.
|
||||
# These root-scoped paths are required even when the app is hosted under `/chgrid`.
|
||||
# The client calls `/auth/session/set` after login and `/auth/session/clear` on logout/session-reset.
|
||||
ProxyPass /auth/session/ http://127.0.0.1:8765/auth/session/
|
||||
ProxyPassReverse /auth/session/ http://127.0.0.1:8765/auth/session/
|
||||
# These paths should live under the same instance base path.
|
||||
ProxyPass /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/
|
||||
ProxyPassReverse /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/
|
||||
|
||||
# Ensure HTML entrypoint is never cached so version updates are picked up quickly.
|
||||
<LocationMatch "^/chgrid/?$|^/chgrid/index\\.html$">
|
||||
|
||||
@@ -57,6 +57,7 @@ except ModuleNotFoundError: # pragma: no cover - compatibility fallback
|
||||
config_path = Path(sys.argv[1])
|
||||
host = "127.0.0.1"
|
||||
port = 8765
|
||||
base_path = "/"
|
||||
if config_path.exists():
|
||||
with config_path.open("rb") as fp:
|
||||
data = tomllib.load(fp)
|
||||
@@ -72,7 +73,9 @@ if config_path.exists():
|
||||
port = int(server.get("port", port))
|
||||
except (TypeError, ValueError):
|
||||
port = 8765
|
||||
print(f"http://{host}:{port}/auth/session/check")
|
||||
raw_base_path = str(server.get("base_path", base_path)).strip() or "/"
|
||||
base_path = "/" if raw_base_path == "/" else f"/{raw_base_path.strip('/')}/"
|
||||
print(f"http://{host}:{port}{base_path}auth/session/check")
|
||||
PY
|
||||
)"
|
||||
escaped_host_origin=${CHGRID_HOST_ORIGIN//\\/\\\\}
|
||||
|
||||
@@ -21,12 +21,13 @@ Open: `http://localhost:5173`
|
||||
Defaults:
|
||||
- Server reads `config.toml` automatically when present.
|
||||
- Server default bind/port is `127.0.0.1:8765`.
|
||||
- Server default base path is `/` for local/dev; production subpath deploys should set `server.base_path` to match the published client path such as `/chgrid/`.
|
||||
- Server defaults to TLS-required unless you set `network.allow_insecure_ws=true` or pass `--allow-insecure-ws` for local/dev.
|
||||
- Client dev default is `localhost:5173`.
|
||||
- Auth requires `CHGRID_AUTH_SECRET` in environment.
|
||||
- Browser-origin enforcement requires `CHGRID_HOST_ORIGIN` in environment.
|
||||
- A starter env file is available at `server/.env.sample`.
|
||||
- Saved login uses server-managed `HttpOnly` cookie (`chgrid_session_token`) via `GET /auth/session/set` and `GET /auth/session/clear` (both require `X-Chgrid-Auth-Client: 1`).
|
||||
- Saved login uses instance-scoped server-managed `HttpOnly` cookie helpers under the configured base path (for example `/chgrid/auth/session/set` and `/chgrid/auth/session/clear`) and both require `X-Chgrid-Auth-Client: 1`.
|
||||
|
||||
## Quick Restarts
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
||||
- `policy` (`usernameMinLength`, `usernameMaxLength`, `passwordMinLength`, `passwordMaxLength`)
|
||||
- `auth_required.authPolicy`: server auth limits advertised before login/register submit.
|
||||
- `auth_result.authPolicy`: server auth limits echoed on auth success/failure responses.
|
||||
- `auth_result.sessionToken` is used by the client to call server HTTP endpoint `GET /auth/session/set` (`Authorization: Bearer <sessionToken>`, `X-Chgrid-Auth-Client: 1`) so the server can issue `Set-Cookie: chgrid_session_token=...; HttpOnly`.
|
||||
- `auth_result.sessionToken` is used by the client to call the instance-scoped HTTP endpoint `GET <base_path>auth/session/set` (`Authorization: Bearer <sessionToken>`, `X-Chgrid-Auth-Client: 1`) so the server can issue `Set-Cookie: chgrid_session_token=...; HttpOnly`.
|
||||
- `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing.
|
||||
- `welcome.worldConfig.movementTickMs`: server movement-rate window used for client movement pacing.
|
||||
- `welcome.worldConfig.movementMaxStepsPerTick`: max allowed grid steps per movement window.
|
||||
@@ -126,9 +126,10 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
||||
- Server persists account state (last nickname + last position) and restores spawn from that state on auth login/resume.
|
||||
- Server also supports websocket handshake cookie resume:
|
||||
- accepts browser sockets only when websocket `Origin` matches `CHGRID_HOST_ORIGIN`
|
||||
- websocket and auth helper routes are scoped under the configured `server.base_path`
|
||||
- reads `chgrid_session_token` from websocket `Cookie` header
|
||||
- attempts resume before sending `auth_required`
|
||||
- exposes `GET /auth/session/clear` to expire the `HttpOnly` cookie (`X-Chgrid-Auth-Client: 1` and matching `Origin` required)
|
||||
- exposes `GET <base_path>auth/session/clear` to expire the `HttpOnly` cookie (`X-Chgrid-Auth-Client: 1` and matching `Origin` required)
|
||||
- Server applies auth hardening before accepting login/register/resume:
|
||||
- login/register PBKDF2 work runs off the event loop in bounded worker concurrency
|
||||
- repeated auth failures are rate-limited by IP and IP+identity windows
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
1. User clicks connect.
|
||||
2. Client validates auth form and sets up local media.
|
||||
3. Client connects signaling websocket from the configured app origin.
|
||||
4. Server accepts the socket only when the browser `Origin` matches `CHGRID_HOST_ORIGIN`, then attempts cookie-based session resume from websocket handshake cookie (`chgrid_session_token`).
|
||||
4. Server accepts the socket only on the configured instance websocket path and when the browser `Origin` matches `CHGRID_HOST_ORIGIN`, then attempts cookie-based session resume from websocket handshake cookie (`chgrid_session_token`).
|
||||
5. If resume does not authenticate, server sends `auth_required`.
|
||||
- includes `authPolicy` limits for username/password.
|
||||
6. Client sends `auth_login` or `auth_register` (or explicit `auth_resume` if provided by caller).
|
||||
7. Server sends `auth_result`.
|
||||
- includes role + permissions for authenticated session.
|
||||
8. Client persists authenticated session into a server-managed `HttpOnly` cookie via `GET /auth/session/set` (`Authorization: Bearer <sessionToken>`, `X-Chgrid-Auth-Client: 1`), and clears it via `GET /auth/session/clear` (`X-Chgrid-Auth-Client: 1`) on logout/session errors.
|
||||
- the optional PHP media proxy validates that same cookie through `GET /auth/session/check` before relaying media
|
||||
8. Client persists authenticated session into instance-scoped server-managed `HttpOnly` cookie helpers under the active app base path via `GET <base_path>auth/session/set` (`Authorization: Bearer <sessionToken>`, `X-Chgrid-Auth-Client: 1`), and clears it via `GET <base_path>auth/session/clear` on logout/session errors.
|
||||
- the optional PHP media proxy validates that same cookie through `GET <base_path>auth/session/check` before relaying media
|
||||
9. Server sends `welcome` with users/items snapshot.
|
||||
10. Client:
|
||||
- applies `welcome.worldConfig.gridSize` for authoritative grid bounds/rendering
|
||||
|
||||
@@ -13,6 +13,7 @@ class ServerConfigSection(BaseModel):
|
||||
|
||||
bind_ip: str = "127.0.0.1"
|
||||
port: int = 8765
|
||||
base_path: str = "/"
|
||||
|
||||
|
||||
class NetworkConfigSection(BaseModel):
|
||||
|
||||
@@ -133,9 +133,10 @@ RADIO_METADATA_TIMEOUT_S = 6.0
|
||||
CLOCK_ANNOUNCE_POLL_INTERVAL_S = 1.0
|
||||
AUTH_SESSION_COOKIE_NAME = "chgrid_session_token"
|
||||
AUTH_SESSION_COOKIE_MAX_AGE_SECONDS = 14 * 24 * 60 * 60
|
||||
AUTH_SESSION_COOKIE_SET_PATH = "/auth/session/set"
|
||||
AUTH_SESSION_COOKIE_CLEAR_PATH = "/auth/session/clear"
|
||||
AUTH_SESSION_COOKIE_CHECK_PATH = "/auth/session/check"
|
||||
AUTH_SESSION_COOKIE_SET_PATH = "auth/session/set"
|
||||
AUTH_SESSION_COOKIE_CLEAR_PATH = "auth/session/clear"
|
||||
AUTH_SESSION_COOKIE_CHECK_PATH = "auth/session/check"
|
||||
WEBSOCKET_PATH = "ws"
|
||||
AUTH_SESSION_COOKIE_CLIENT_HEADER = "X-Chgrid-Auth-Client"
|
||||
AUTH_LOGIN_FAILURE_MESSAGE = "We couldn't log you in. Check your details and try again."
|
||||
AUTH_RESUME_FAILURE_MESSAGE = "We couldn't restore your session. Please log in again."
|
||||
@@ -162,6 +163,7 @@ class SignalingServer:
|
||||
state_save_debounce_ms: int = 200,
|
||||
state_save_max_delay_ms: int = 1000,
|
||||
host_origin: str | None = None,
|
||||
base_path: str = "/",
|
||||
):
|
||||
"""Initialize runtime state, TLS context, and item service."""
|
||||
|
||||
@@ -190,6 +192,11 @@ class SignalingServer:
|
||||
self.instance_id = str(uuid.uuid4())
|
||||
self.server_version = self._resolve_server_version()
|
||||
self.host_origin = normalize_origin(host_origin, field_name="host origin") if host_origin else None
|
||||
self.base_path = self._normalize_base_path(base_path)
|
||||
self.websocket_path = self._base_path_join(WEBSOCKET_PATH)
|
||||
self.auth_session_cookie_set_path = self._base_path_join(AUTH_SESSION_COOKIE_SET_PATH)
|
||||
self.auth_session_cookie_clear_path = self._base_path_join(AUTH_SESSION_COOKIE_CLEAR_PATH)
|
||||
self.auth_session_cookie_check_path = self._base_path_join(AUTH_SESSION_COOKIE_CHECK_PATH)
|
||||
self.state_save_debounce_ms = max(1, int(state_save_debounce_ms))
|
||||
self.state_save_max_delay_ms = max(self.state_save_debounce_ms, int(state_save_max_delay_ms))
|
||||
self._pending_state_save_handle: asyncio.TimerHandle | None = None
|
||||
@@ -263,6 +270,23 @@ class SignalingServer:
|
||||
"passwordMaxLength": self.auth_service.password_max_length,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_base_path(value: str) -> str:
|
||||
"""Normalize one instance base path to leading/trailing slash form."""
|
||||
|
||||
text = str(value).strip()
|
||||
if not text or text == "/":
|
||||
return "/"
|
||||
return f"/{text.strip('/')}/"
|
||||
|
||||
def _base_path_join(self, suffix: str) -> str:
|
||||
"""Join one instance-relative route suffix to the configured base path."""
|
||||
|
||||
token = suffix.lstrip("/")
|
||||
if self.base_path == "/":
|
||||
return f"/{token}"
|
||||
return f"{self.base_path}{token}"
|
||||
|
||||
def _session_cookie_secure(self, request: HttpRequest | None = None) -> bool:
|
||||
"""Return True when session cookies should be marked Secure."""
|
||||
|
||||
@@ -278,7 +302,7 @@ class SignalingServer:
|
||||
|
||||
secure = "; Secure" if self._session_cookie_secure(request) else ""
|
||||
return (
|
||||
f"{AUTH_SESSION_COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; "
|
||||
f"{AUTH_SESSION_COOKIE_NAME}={token}; Path={self.base_path}; HttpOnly; SameSite=Lax; "
|
||||
f"Max-Age={AUTH_SESSION_COOKIE_MAX_AGE_SECONDS}{secure}"
|
||||
)
|
||||
|
||||
@@ -286,7 +310,7 @@ class SignalingServer:
|
||||
"""Build Set-Cookie header value that expires the session cookie."""
|
||||
|
||||
secure = "; Secure" if self._session_cookie_secure(request) else ""
|
||||
return f"{AUTH_SESSION_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0{secure}"
|
||||
return f"{AUTH_SESSION_COOKIE_NAME}=; Path={self.base_path}; HttpOnly; SameSite=Lax; Max-Age=0{secure}"
|
||||
|
||||
def _origin_allowed(self, request: HttpRequest) -> bool:
|
||||
"""Return whether one auth helper HTTP request comes from the configured app origin."""
|
||||
@@ -316,8 +340,18 @@ class SignalingServer:
|
||||
"""Handle lightweight same-origin auth cookie set/clear HTTP endpoints."""
|
||||
|
||||
path = request.path.split("?", 1)[0]
|
||||
if path not in {AUTH_SESSION_COOKIE_SET_PATH, AUTH_SESSION_COOKIE_CLEAR_PATH, AUTH_SESSION_COOKIE_CHECK_PATH}:
|
||||
auth_paths = {
|
||||
self.auth_session_cookie_set_path,
|
||||
self.auth_session_cookie_clear_path,
|
||||
self.auth_session_cookie_check_path,
|
||||
}
|
||||
if path == self.websocket_path:
|
||||
return None
|
||||
if path not in auth_paths:
|
||||
headers = Headers()
|
||||
headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||
headers["Cache-Control"] = "no-store"
|
||||
return HttpResponse(404, "Not Found", headers, b"not found")
|
||||
|
||||
headers = Headers()
|
||||
headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||
@@ -328,7 +362,7 @@ class SignalingServer:
|
||||
if not self._origin_allowed(request):
|
||||
return HttpResponse(403, "Forbidden", headers, b"origin not allowed")
|
||||
|
||||
if path == AUTH_SESSION_COOKIE_CHECK_PATH:
|
||||
if path == self.auth_session_cookie_check_path:
|
||||
cookie_header = str(request.headers.get("Cookie", "")).strip()
|
||||
token = self._cookie_value(cookie_header, AUTH_SESSION_COOKIE_NAME)
|
||||
if not token:
|
||||
@@ -339,7 +373,7 @@ class SignalingServer:
|
||||
return HttpResponse(401, "Unauthorized", headers, b"invalid session")
|
||||
return HttpResponse(204, "No Content", headers, b"")
|
||||
|
||||
if path == AUTH_SESSION_COOKIE_CLEAR_PATH:
|
||||
if path == self.auth_session_cookie_clear_path:
|
||||
headers["Set-Cookie"] = self._clear_session_cookie_header(request=request)
|
||||
return HttpResponse(200, "OK", headers, b"cleared")
|
||||
|
||||
@@ -1415,6 +1449,11 @@ class SignalingServer:
|
||||
LOGGER.info("websocket opened id=%s", client.id)
|
||||
|
||||
try:
|
||||
request = getattr(websocket, "request", None)
|
||||
request_path = str(getattr(request, "path", "")).split("?", 1)[0]
|
||||
if request_path != self.websocket_path:
|
||||
await websocket.close()
|
||||
return
|
||||
cookie_token = self._session_token_from_websocket_cookie(websocket)
|
||||
if cookie_token:
|
||||
await self._handle_auth_packet(
|
||||
@@ -3245,5 +3284,6 @@ def run() -> None:
|
||||
state_save_debounce_ms=config.storage.state_save_debounce_ms,
|
||||
state_save_max_delay_ms=config.storage.state_save_max_delay_ms,
|
||||
host_origin=host_origin,
|
||||
base_path=config.server.base_path,
|
||||
)
|
||||
asyncio.run(server.start())
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
bind_ip = "127.0.0.1"
|
||||
# Listen port for signaling websocket server.
|
||||
port = 8765
|
||||
# Public base path for this grid instance. Examples: "/", "/chgrid/", "/ttgrid/".
|
||||
base_path = "/"
|
||||
|
||||
[network]
|
||||
# Maximum inbound websocket message size in bytes.
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.config import load_config
|
||||
def test_load_config_defaults_when_path_none() -> None:
|
||||
cfg = load_config(None)
|
||||
assert cfg.server.bind_ip == "127.0.0.1"
|
||||
assert cfg.server.base_path == "/"
|
||||
assert cfg.network.allow_insecure_ws is False
|
||||
assert cfg.storage.state_file == "runtime/items.json"
|
||||
assert cfg.storage.state_save_debounce_ms == 200
|
||||
@@ -43,3 +44,18 @@ state_save_max_delay_ms = 900
|
||||
cfg = load_config(config_path)
|
||||
assert cfg.storage.state_save_debounce_ms == 150
|
||||
assert cfg.storage.state_save_max_delay_ms == 900
|
||||
|
||||
|
||||
def test_load_config_reads_server_base_path(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "config.toml"
|
||||
config_path.write_text(
|
||||
"""
|
||||
[network]
|
||||
allow_insecure_ws = true
|
||||
|
||||
[server]
|
||||
base_path = "/ttgrid/"
|
||||
""".strip()
|
||||
)
|
||||
cfg = load_config(config_path)
|
||||
assert cfg.server.base_path == "/ttgrid/"
|
||||
|
||||
@@ -24,13 +24,17 @@ def _request(path: str, headers: dict[str, str] | None = None) -> Request:
|
||||
return Request(path=path, headers=values)
|
||||
|
||||
|
||||
def _server() -> SignalingServer:
|
||||
return SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com", base_path="/chgrid/")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_cookie_set_endpoint_sets_httponly_cookie() -> None:
|
||||
server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com")
|
||||
server = _server()
|
||||
username = f"user_{uuid.uuid4().hex[:8]}"
|
||||
session = server.auth_service.register(username, "password99")
|
||||
request = _request(
|
||||
AUTH_SESSION_COOKIE_SET_PATH,
|
||||
server.auth_session_cookie_set_path,
|
||||
headers={
|
||||
AUTH_SESSION_COOKIE_CLIENT_HEADER: "1",
|
||||
"Authorization": f"Bearer {session.token}",
|
||||
@@ -44,15 +48,16 @@ async def test_session_cookie_set_endpoint_sets_httponly_cookie() -> None:
|
||||
assert response.status_code == 200
|
||||
set_cookie = response.headers.get("Set-Cookie", "")
|
||||
assert f"{AUTH_SESSION_COOKIE_NAME}=" in set_cookie
|
||||
assert "Path=/chgrid/" in set_cookie
|
||||
assert "HttpOnly" in set_cookie
|
||||
assert "SameSite=Lax" in set_cookie
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_cookie_clear_endpoint_expires_cookie() -> None:
|
||||
server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com")
|
||||
server = _server()
|
||||
request = _request(
|
||||
AUTH_SESSION_COOKIE_CLEAR_PATH,
|
||||
server.auth_session_cookie_clear_path,
|
||||
headers={AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Origin": "https://example.com"},
|
||||
)
|
||||
|
||||
@@ -68,11 +73,11 @@ async def test_session_cookie_clear_endpoint_expires_cookie() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_cookie_check_endpoint_accepts_valid_cookie() -> None:
|
||||
server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com")
|
||||
server = _server()
|
||||
username = f"user_{uuid.uuid4().hex[:8]}"
|
||||
session = server.auth_service.register(username, "password99")
|
||||
request = _request(
|
||||
AUTH_SESSION_COOKIE_CHECK_PATH,
|
||||
server.auth_session_cookie_check_path,
|
||||
headers={
|
||||
AUTH_SESSION_COOKIE_CLIENT_HEADER: "1",
|
||||
"Cookie": f"{AUTH_SESSION_COOKIE_NAME}={session.token}",
|
||||
@@ -88,9 +93,9 @@ async def test_session_cookie_check_endpoint_accepts_valid_cookie() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_cookie_check_endpoint_rejects_missing_cookie() -> None:
|
||||
server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com")
|
||||
server = _server()
|
||||
request = _request(
|
||||
AUTH_SESSION_COOKIE_CHECK_PATH,
|
||||
server.auth_session_cookie_check_path,
|
||||
headers={AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Origin": "https://example.com"},
|
||||
)
|
||||
|
||||
@@ -102,9 +107,9 @@ async def test_session_cookie_check_endpoint_rejects_missing_cookie() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_cookie_helpers_reject_wrong_origin() -> None:
|
||||
server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com")
|
||||
server = _server()
|
||||
request = _request(
|
||||
AUTH_SESSION_COOKIE_CLEAR_PATH,
|
||||
server.auth_session_cookie_clear_path,
|
||||
headers={AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Origin": "https://evil.example.com"},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user