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.
- website_agent_server-0.1.0/LICENSE +21 -0
- website_agent_server-0.1.0/PKG-INFO +123 -0
- website_agent_server-0.1.0/README.md +105 -0
- website_agent_server-0.1.0/pyproject.toml +18 -0
- website_agent_server-0.1.0/website_agent_server/__init__.py +3 -0
- website_agent_server-0.1.0/website_agent_server/__main__.py +159 -0
- website_agent_server-0.1.0/website_agent_server/auth.py +67 -0
- website_agent_server-0.1.0/website_agent_server/browser.py +2757 -0
- website_agent_server-0.1.0/website_agent_server/config.py +44 -0
- website_agent_server-0.1.0/website_agent_server/main.py +522 -0
- website_agent_server-0.1.0/website_agent_server/url_policy.py +181 -0
|
@@ -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,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="/")
|