website-agent-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GGN_2015
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: website-agent-server
3
+ Version: 0.1.0
4
+ Summary: A server-side browser proxy that lets clients operate websites without direct remote connections.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Dist: fastapi (>=0.115)
13
+ Requires-Dist: playwright (>=1.48)
14
+ Requires-Dist: python-multipart (>=0.0.9)
15
+ Requires-Dist: uvicorn[standard] (>=0.30)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Website Agent Server
19
+
20
+ English | [中文](README.zh-CN.md)
21
+
22
+ Website Agent Server is a Python server-side browser proxy. The client never loads the target website directly. It connects only to this server, receives rendered browser frames, and sends mouse, keyboard, input method, clipboard, wheel, file upload, and navigation events back to the server.
23
+
24
+ ## How It Works
25
+
26
+ - FastAPI serves the local control UI, HTTP API, and WebSocket endpoint.
27
+ - Playwright launches Chromium on the server.
28
+ - The target website runs inside the server-side browser context.
29
+ - The client receives binary WebSocket image frames: JPEG screenshots by default, or PNG frames when `--screenshot-quality 100` is used.
30
+ - User actions are replayed into Chromium by the server.
31
+ - IME text, paste, copy, cut, downloads, file chooser actions, cookie management, and ordinary media element audio are brokered through local server endpoints.
32
+
33
+ Because the remote page is never embedded as HTML in the client, page scripts, link clicks, images, XHR/fetch calls, WebSocket connections, and form submissions are performed by the server-side browser.
34
+
35
+ ## Setup
36
+
37
+ Create or reuse the repository-root virtual environment:
38
+
39
+ ```powershell
40
+ python -m venv venv
41
+ venv\Scripts\python.exe -m pip install -r requirements.txt
42
+ venv\Scripts\python.exe -m playwright install chromium
43
+ ```
44
+
45
+ ## Run
46
+
47
+ ```powershell
48
+ venv\Scripts\python.exe -m website_agent_server
49
+ ```
50
+
51
+ Open [http://127.0.0.1:8000](http://127.0.0.1:8000), enter a website URL, and operate the remote site through the rendered viewport. By default the server listens on all interfaces, so LAN clients can also connect with the server machine's LAN IP.
52
+
53
+ If a target URL is entered without an `http://` or `https://` prefix, the server first probes HTTPS. It uses HTTPS when the TLS service is available, otherwise it falls back to HTTP. The same rule applies to `--lock-url` and `/lock_url/...` paths.
54
+
55
+ You can lock only one client by putting the target URL in the server URL path:
56
+
57
+ ```text
58
+ http://127.0.0.1:8000/lock_url/https/example.com/path
59
+ ```
60
+
61
+ That client opens the target immediately and hides the browser option controls. Other clients that open `/` keep the normal URL picker. Query strings and fragments are preserved, for example `/lock_url/https/example.com/path?x=1#section`.
62
+
63
+ ## Command-Line Configuration
64
+
65
+ ```powershell
66
+ venv\Scripts\python.exe -m website_agent_server --port 8080 --headed
67
+ ```
68
+
69
+ Require a PIN before clients can use the proxy:
70
+
71
+ ```powershell
72
+ venv\Scripts\python.exe -m website_agent_server --pin 123456
73
+ ```
74
+
75
+ | Option | Default | Description |
76
+ | --- | --- | --- |
77
+ | `--host` | `0.0.0.0` | Server bind host. Use `127.0.0.1` to restrict access to this machine. |
78
+ | `--port` | `8000` | Server port. |
79
+ | `--headed` | disabled | Run Chromium with a visible browser window. |
80
+ | `--ignore-https-errors` | disabled | Ignore remote TLS certificate errors. |
81
+ | `--allow-private-hosts` | disabled | Allow navigation and resource requests to private, local, or reserved networks. |
82
+ | `--session-ttl-seconds` | `600` | Disconnected client session and client browser context lifetime. A client can reconnect to its cached browser session during this window. |
83
+ | `--navigation-timeout-ms` | `30000` | Navigation timeout. |
84
+ | `--frame-interval-seconds` | `0.18` | Screenshot streaming interval. |
85
+ | `--screenshot-quality` | `95` | Frame quality from 1 to 100. Values below 100 use JPEG; 100 uses PNG. |
86
+ | `--media-frame-interval-seconds` | `0.35` | Screenshot streaming interval while remote media is playing. |
87
+ | `--media-screenshot-quality` | `80` | JPEG quality while remote media is playing. Ignored when `--screenshot-quality 100` enables PNG. |
88
+ | `--min-viewport-width` | `320` | Minimum remote viewport width. |
89
+ | `--min-viewport-height` | `240` | Minimum remote viewport height. |
90
+ | `--max-viewport-width` | `1920` | Maximum remote viewport width. |
91
+ | `--max-viewport-height` | `1600` | Maximum remote viewport height. |
92
+ | `--data-dir` | `.agent-data` | Runtime downloads and temporary uploads directory. |
93
+ | `--pin` | disabled | Require this PIN before clients can access the proxy UI, API, or WebSocket. |
94
+ | `--lock-url` | disabled | Open this URL automatically and hide/disable browser option controls such as Back, Forward, Cookie, Quit, and address navigation. PIN authentication still applies when configured. |
95
+
96
+ Private and local network targets are blocked by default to reduce SSRF risk. Use `--allow-private-hosts` only when you trust the users who can access the proxy.
97
+
98
+ Because LAN access is enabled by default, prefer using a PIN:
99
+
100
+ ```powershell
101
+ venv\Scripts\python.exe -m website_agent_server --pin 123456
102
+ ```
103
+
104
+ If the proxy itself also needs to open LAN or localhost target URLs, enable private hosts explicitly:
105
+
106
+ ```powershell
107
+ venv\Scripts\python.exe -m website_agent_server --allow-private-hosts --pin 123456
108
+ ```
109
+
110
+ Each client receives exactly one server-side Playwright `BrowserContext`, keyed only by its local `session-uuid` cookie. Contexts are never shared by IP address, target host, port, URL path, or device class. If the same client opens another target URL before its UUID expires, the old page is closed and the same context is reused, including storage partitions, service workers, permissions, and other browser context state.
111
+
112
+ Mobile clients use a mobile Playwright browser profile with a narrow viewport, touch support, a mobile Chromium user agent, and mobile Client Hints so upstream responsive sites can select their mobile layout.
113
+
114
+ The local `session-uuid` cookie only identifies the Website Agent client, not the remote site. If the local page refreshes or the WebSocket drops, the server uses that cookie to reconnect the same client to its existing browser session. Disconnected browser sessions and idle client contexts are removed after `--session-ttl-seconds`, which is 10 minutes by default. When a UUID is removed, its BrowserContext, browsing history, cookies, localStorage, IndexedDB, download files, upload files, and in-memory session records are removed together.
115
+
116
+ Uvicorn's WebSocket ping keepalive is disabled because mobile browsers may suspend sockets while loading, switching apps, or sleeping. The client reconnect path and session TTL handle those drops without printing keepalive tracebacks.
117
+
118
+ Audio from ordinary server-side `<audio>` and `<video>` elements is captured in the page with `captureStream()` or a WebAudio fallback and forwarded over a dedicated WebSocket as WebM/Opus chunks, separate from screenshot frames. The server-side page is not relied on for audible playback. When remote media is playing, screenshot streaming automatically uses `--media-frame-interval-seconds` and `--media-screenshot-quality` to reduce CPU and network pressure. This does not cover DRM media, WebRTC calls, pure WebAudio graphs, browser UI sounds, or system-level mixed audio.
119
+
120
+ ## Limitations
121
+
122
+ This project proxies interaction by streaming rendered browser frames, not by rewriting HTML. That keeps remote network access on the server, but it also means the client sees a bitmap viewport rather than native DOM nodes. Browser extension APIs, local client certificates, DRM-protected media, and some system dialogs are outside the current scope.
123
+
@@ -0,0 +1,105 @@
1
+ # Website Agent Server
2
+
3
+ English | [中文](README.zh-CN.md)
4
+
5
+ Website Agent Server is a Python server-side browser proxy. The client never loads the target website directly. It connects only to this server, receives rendered browser frames, and sends mouse, keyboard, input method, clipboard, wheel, file upload, and navigation events back to the server.
6
+
7
+ ## How It Works
8
+
9
+ - FastAPI serves the local control UI, HTTP API, and WebSocket endpoint.
10
+ - Playwright launches Chromium on the server.
11
+ - The target website runs inside the server-side browser context.
12
+ - The client receives binary WebSocket image frames: JPEG screenshots by default, or PNG frames when `--screenshot-quality 100` is used.
13
+ - User actions are replayed into Chromium by the server.
14
+ - IME text, paste, copy, cut, downloads, file chooser actions, cookie management, and ordinary media element audio are brokered through local server endpoints.
15
+
16
+ Because the remote page is never embedded as HTML in the client, page scripts, link clicks, images, XHR/fetch calls, WebSocket connections, and form submissions are performed by the server-side browser.
17
+
18
+ ## Setup
19
+
20
+ Create or reuse the repository-root virtual environment:
21
+
22
+ ```powershell
23
+ python -m venv venv
24
+ venv\Scripts\python.exe -m pip install -r requirements.txt
25
+ venv\Scripts\python.exe -m playwright install chromium
26
+ ```
27
+
28
+ ## Run
29
+
30
+ ```powershell
31
+ venv\Scripts\python.exe -m website_agent_server
32
+ ```
33
+
34
+ Open [http://127.0.0.1:8000](http://127.0.0.1:8000), enter a website URL, and operate the remote site through the rendered viewport. By default the server listens on all interfaces, so LAN clients can also connect with the server machine's LAN IP.
35
+
36
+ If a target URL is entered without an `http://` or `https://` prefix, the server first probes HTTPS. It uses HTTPS when the TLS service is available, otherwise it falls back to HTTP. The same rule applies to `--lock-url` and `/lock_url/...` paths.
37
+
38
+ You can lock only one client by putting the target URL in the server URL path:
39
+
40
+ ```text
41
+ http://127.0.0.1:8000/lock_url/https/example.com/path
42
+ ```
43
+
44
+ That client opens the target immediately and hides the browser option controls. Other clients that open `/` keep the normal URL picker. Query strings and fragments are preserved, for example `/lock_url/https/example.com/path?x=1#section`.
45
+
46
+ ## Command-Line Configuration
47
+
48
+ ```powershell
49
+ venv\Scripts\python.exe -m website_agent_server --port 8080 --headed
50
+ ```
51
+
52
+ Require a PIN before clients can use the proxy:
53
+
54
+ ```powershell
55
+ venv\Scripts\python.exe -m website_agent_server --pin 123456
56
+ ```
57
+
58
+ | Option | Default | Description |
59
+ | --- | --- | --- |
60
+ | `--host` | `0.0.0.0` | Server bind host. Use `127.0.0.1` to restrict access to this machine. |
61
+ | `--port` | `8000` | Server port. |
62
+ | `--headed` | disabled | Run Chromium with a visible browser window. |
63
+ | `--ignore-https-errors` | disabled | Ignore remote TLS certificate errors. |
64
+ | `--allow-private-hosts` | disabled | Allow navigation and resource requests to private, local, or reserved networks. |
65
+ | `--session-ttl-seconds` | `600` | Disconnected client session and client browser context lifetime. A client can reconnect to its cached browser session during this window. |
66
+ | `--navigation-timeout-ms` | `30000` | Navigation timeout. |
67
+ | `--frame-interval-seconds` | `0.18` | Screenshot streaming interval. |
68
+ | `--screenshot-quality` | `95` | Frame quality from 1 to 100. Values below 100 use JPEG; 100 uses PNG. |
69
+ | `--media-frame-interval-seconds` | `0.35` | Screenshot streaming interval while remote media is playing. |
70
+ | `--media-screenshot-quality` | `80` | JPEG quality while remote media is playing. Ignored when `--screenshot-quality 100` enables PNG. |
71
+ | `--min-viewport-width` | `320` | Minimum remote viewport width. |
72
+ | `--min-viewport-height` | `240` | Minimum remote viewport height. |
73
+ | `--max-viewport-width` | `1920` | Maximum remote viewport width. |
74
+ | `--max-viewport-height` | `1600` | Maximum remote viewport height. |
75
+ | `--data-dir` | `.agent-data` | Runtime downloads and temporary uploads directory. |
76
+ | `--pin` | disabled | Require this PIN before clients can access the proxy UI, API, or WebSocket. |
77
+ | `--lock-url` | disabled | Open this URL automatically and hide/disable browser option controls such as Back, Forward, Cookie, Quit, and address navigation. PIN authentication still applies when configured. |
78
+
79
+ Private and local network targets are blocked by default to reduce SSRF risk. Use `--allow-private-hosts` only when you trust the users who can access the proxy.
80
+
81
+ Because LAN access is enabled by default, prefer using a PIN:
82
+
83
+ ```powershell
84
+ venv\Scripts\python.exe -m website_agent_server --pin 123456
85
+ ```
86
+
87
+ If the proxy itself also needs to open LAN or localhost target URLs, enable private hosts explicitly:
88
+
89
+ ```powershell
90
+ venv\Scripts\python.exe -m website_agent_server --allow-private-hosts --pin 123456
91
+ ```
92
+
93
+ Each client receives exactly one server-side Playwright `BrowserContext`, keyed only by its local `session-uuid` cookie. Contexts are never shared by IP address, target host, port, URL path, or device class. If the same client opens another target URL before its UUID expires, the old page is closed and the same context is reused, including storage partitions, service workers, permissions, and other browser context state.
94
+
95
+ Mobile clients use a mobile Playwright browser profile with a narrow viewport, touch support, a mobile Chromium user agent, and mobile Client Hints so upstream responsive sites can select their mobile layout.
96
+
97
+ The local `session-uuid` cookie only identifies the Website Agent client, not the remote site. If the local page refreshes or the WebSocket drops, the server uses that cookie to reconnect the same client to its existing browser session. Disconnected browser sessions and idle client contexts are removed after `--session-ttl-seconds`, which is 10 minutes by default. When a UUID is removed, its BrowserContext, browsing history, cookies, localStorage, IndexedDB, download files, upload files, and in-memory session records are removed together.
98
+
99
+ Uvicorn's WebSocket ping keepalive is disabled because mobile browsers may suspend sockets while loading, switching apps, or sleeping. The client reconnect path and session TTL handle those drops without printing keepalive tracebacks.
100
+
101
+ Audio from ordinary server-side `<audio>` and `<video>` elements is captured in the page with `captureStream()` or a WebAudio fallback and forwarded over a dedicated WebSocket as WebM/Opus chunks, separate from screenshot frames. The server-side page is not relied on for audible playback. When remote media is playing, screenshot streaming automatically uses `--media-frame-interval-seconds` and `--media-screenshot-quality` to reduce CPU and network pressure. This does not cover DRM media, WebRTC calls, pure WebAudio graphs, browser UI sounds, or system-level mixed audio.
102
+
103
+ ## Limitations
104
+
105
+ This project proxies interaction by streaming rendered browser frames, not by rewriting HTML. That keeps remote network access on the server, but it also means the client sees a bitmap viewport rather than native DOM nodes. Browser extension APIs, local client certificates, DRM-protected media, and some system dialogs are outside the current scope.
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "website-agent-server"
3
+ version = "0.1.0"
4
+ description = "A server-side browser proxy that lets clients operate websites without direct remote connections."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "fastapi>=0.115",
9
+ "uvicorn[standard]>=0.30",
10
+ "playwright>=1.48",
11
+ "python-multipart>=0.0.9",
12
+ ]
13
+
14
+ [project.scripts]
15
+ website-agent-server = "website_agent_server.__main__:main"
16
+
17
+ [tool.pytest.ini_options]
18
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """Website Agent Server package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import asyncio
5
+ from pathlib import Path
6
+
7
+ import uvicorn
8
+
9
+ from .config import settings
10
+ from .url_policy import HostAccessPolicy
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(
15
+ prog="website-agent-server",
16
+ description="Run the server-side browser proxy.",
17
+ )
18
+ parser.add_argument("--host", default=settings.host, help="Server bind host.")
19
+ parser.add_argument("--port", type=int, default=settings.port, help="Server port.")
20
+ parser.add_argument(
21
+ "--headed",
22
+ action="store_true",
23
+ help="Run Chromium with a visible browser window.",
24
+ )
25
+ parser.add_argument(
26
+ "--ignore-https-errors",
27
+ action="store_true",
28
+ help="Ignore remote TLS certificate errors.",
29
+ )
30
+ parser.add_argument(
31
+ "--allow-private-hosts",
32
+ action="store_true",
33
+ help="Allow private, local, and reserved network targets.",
34
+ )
35
+ parser.add_argument(
36
+ "--session-ttl-seconds",
37
+ type=int,
38
+ default=settings.session_ttl_seconds,
39
+ help="Idle session lifetime in seconds.",
40
+ )
41
+ parser.add_argument(
42
+ "--navigation-timeout-ms",
43
+ type=int,
44
+ default=settings.navigation_timeout_ms,
45
+ help="Navigation timeout in milliseconds.",
46
+ )
47
+ parser.add_argument(
48
+ "--frame-interval-seconds",
49
+ type=float,
50
+ default=settings.frame_interval_seconds,
51
+ help="Screenshot streaming interval in seconds.",
52
+ )
53
+ parser.add_argument(
54
+ "--screenshot-quality",
55
+ type=int,
56
+ default=settings.screenshot_quality,
57
+ help="Screenshot quality from 1 to 100. Values below 100 use JPEG; 100 uses PNG.",
58
+ )
59
+ parser.add_argument(
60
+ "--media-frame-interval-seconds",
61
+ type=float,
62
+ default=settings.media_frame_interval_seconds,
63
+ help="Screenshot streaming interval while remote media is playing.",
64
+ )
65
+ parser.add_argument(
66
+ "--media-screenshot-quality",
67
+ type=int,
68
+ default=settings.media_screenshot_quality,
69
+ help="JPEG screenshot quality while remote media is playing. Ignored when screenshot quality is 100.",
70
+ )
71
+ parser.add_argument(
72
+ "--min-viewport-width",
73
+ type=int,
74
+ default=settings.min_viewport_width,
75
+ help="Minimum remote viewport width.",
76
+ )
77
+ parser.add_argument(
78
+ "--min-viewport-height",
79
+ type=int,
80
+ default=settings.min_viewport_height,
81
+ help="Minimum remote viewport height.",
82
+ )
83
+ parser.add_argument(
84
+ "--max-viewport-width",
85
+ type=int,
86
+ default=settings.max_viewport_width,
87
+ help="Maximum remote viewport width.",
88
+ )
89
+ parser.add_argument(
90
+ "--max-viewport-height",
91
+ type=int,
92
+ default=settings.max_viewport_height,
93
+ help="Maximum remote viewport height.",
94
+ )
95
+ parser.add_argument(
96
+ "--data-dir",
97
+ type=Path,
98
+ default=settings.data_dir,
99
+ help="Runtime downloads and temporary uploads directory.",
100
+ )
101
+ parser.add_argument(
102
+ "--pin",
103
+ default=None,
104
+ help="Require this PIN before clients can use the proxy.",
105
+ )
106
+ parser.add_argument(
107
+ "--lock-url",
108
+ default=None,
109
+ help="Lock the UI to this initial URL and disable browser option controls.",
110
+ )
111
+ return parser
112
+
113
+
114
+ async def apply_args(args: argparse.Namespace) -> None:
115
+ settings.host = args.host
116
+ settings.port = args.port
117
+ settings.headless = not args.headed
118
+ settings.ignore_https_errors = args.ignore_https_errors
119
+ settings.allow_private_hosts = args.allow_private_hosts
120
+ settings.session_ttl_seconds = args.session_ttl_seconds
121
+ settings.navigation_timeout_ms = args.navigation_timeout_ms
122
+ settings.frame_interval_seconds = args.frame_interval_seconds
123
+ settings.screenshot_quality = max(1, min(100, args.screenshot_quality))
124
+ settings.media_frame_interval_seconds = max(0.05, args.media_frame_interval_seconds)
125
+ settings.media_screenshot_quality = max(1, min(99, args.media_screenshot_quality))
126
+ settings.min_viewport_width = args.min_viewport_width
127
+ settings.min_viewport_height = args.min_viewport_height
128
+ settings.max_viewport_width = args.max_viewport_width
129
+ settings.max_viewport_height = args.max_viewport_height
130
+ settings.data_dir = args.data_dir.resolve()
131
+ settings.pin = args.pin
132
+ if args.lock_url:
133
+ lock_url_policy = HostAccessPolicy(settings.allow_private_hosts)
134
+ settings.lock_url = await lock_url_policy.ensure_navigation_url_allowed(
135
+ args.lock_url,
136
+ verify_https=not settings.ignore_https_errors,
137
+ )
138
+ else:
139
+ settings.lock_url = None
140
+
141
+
142
+ def main() -> None:
143
+ parser = build_parser()
144
+ args = parser.parse_args()
145
+ try:
146
+ asyncio.run(apply_args(args))
147
+ except ValueError as exc:
148
+ parser.error(str(exc))
149
+ uvicorn.run(
150
+ "website_agent_server.main:app",
151
+ host=settings.host,
152
+ port=settings.port,
153
+ reload=False,
154
+ ws_ping_interval=None,
155
+ )
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import hmac
4
+ import secrets
5
+ from hashlib import sha256
6
+
7
+ from fastapi import HTTPException, Request, WebSocket, status
8
+ from starlette.responses import Response
9
+
10
+ from .config import Settings
11
+
12
+
13
+ class PinAuth:
14
+ def __init__(self, settings: Settings) -> None:
15
+ self.settings = settings
16
+ self._secret = secrets.token_urlsafe(32)
17
+
18
+ @property
19
+ def enabled(self) -> bool:
20
+ return bool(self.settings.pin)
21
+
22
+ def verify_pin(self, pin: str) -> bool:
23
+ expected = self.settings.pin or ""
24
+ return hmac.compare_digest(pin, expected)
25
+
26
+ def token(self) -> str:
27
+ if not self.settings.pin:
28
+ return ""
29
+ digest = hmac.new(
30
+ self._secret.encode("utf-8"),
31
+ self.settings.pin.encode("utf-8"),
32
+ sha256,
33
+ ).hexdigest()
34
+ return digest
35
+
36
+ def is_request_allowed(self, request: Request) -> bool:
37
+ if not self.enabled:
38
+ return True
39
+ token = request.cookies.get(self.settings.auth_cookie_name, "")
40
+ return hmac.compare_digest(token, self.token())
41
+
42
+ def require_request(self, request: Request) -> None:
43
+ if not self.is_request_allowed(request):
44
+ raise HTTPException(
45
+ status_code=status.HTTP_401_UNAUTHORIZED,
46
+ detail="PIN required.",
47
+ )
48
+
49
+ def is_websocket_allowed(self, websocket: WebSocket) -> bool:
50
+ if not self.enabled:
51
+ return True
52
+ token = websocket.cookies.get(self.settings.auth_cookie_name, "")
53
+ return hmac.compare_digest(token, self.token())
54
+
55
+ def set_cookie(self, response: Response) -> None:
56
+ response.set_cookie(
57
+ self.settings.auth_cookie_name,
58
+ self.token(),
59
+ max_age=self.settings.auth_cookie_max_age,
60
+ httponly=True,
61
+ samesite="lax",
62
+ secure=False,
63
+ path="/",
64
+ )
65
+
66
+ def clear_cookie(self, response: Response) -> None:
67
+ response.delete_cookie(self.settings.auth_cookie_name, path="/")