Compare commits

..
5 Commits
22 changed files with 972 additions and 15 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ NOSTR_LIB = ./nostr_core_lib/libnostr_core_x64.a
NOSTR_DEPS = -lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm
BIN := sovereign_browser
SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/nostr_url.c src/nostr_scheme.c src/history.c src/settings.c src/net_services.c src/tor_control.c src/tor_scheme.c src/fips_control.c src/tab_manager.c src/session.c src/agent_server.c src/agent_login.c src/agent_snapshot.c src/agent_tools.c src/agent_mcp.c src/cli.c src/qr.c src/db.c src/relay_fetch.c src/bookmarks.c src/shortcuts.c src/settings_sync.c src/search.c src/profile.c src/agent_fs_tools.c src/agent_llm.c src/agent_loop.c src/agent_chat_store.c src/agent_chat.c src/agent_conversations.c src/agent_skills.c src/embedded_web_content.c src/process_info.c src/perf_probe.c src/webkit_data.c src/web_context.c src/site_downloader.c
SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/nostr_url.c src/nostr_scheme.c src/history.c src/settings.c src/net_services.c src/tor_control.c src/tor_scheme.c src/fips_control.c src/tab_manager.c src/session.c src/agent_server.c src/agent_login.c src/agent_snapshot.c src/agent_tools.c src/agent_mcp.c src/cli.c src/qr.c src/db.c src/relay_fetch.c src/bookmarks.c src/shortcuts.c src/settings_sync.c src/search.c src/profile.c src/agent_fs_tools.c src/agent_llm.c src/agent_loop.c src/agent_chat_store.c src/agent_chat.c src/agent_conversations.c src/agent_skills.c src/embedded_web_content.c src/process_info.c src/perf_probe.c src/webkit_data.c src/web_context.c src/site_downloader.c src/local_scheme.c
# Web files embedded into the binary as C byte arrays.
WEB_FILES := $(shell find www -type f \( -name '*.html' -o -name '*.css' -o -name '*.js' \) 2>/dev/null)
+71
View File
@@ -96,6 +96,77 @@ Working browser with a broad feature set:
See [`docs/webkit-poc-findings.md`](docs/webkit-poc-findings.md) for the
friction report from the POC phase.
## Security restrictions overridden
This browser intentionally strips the traditional web security model. Trust
moves to the **Qube level** (VM isolation), so the browser itself does not
enforce same-origin policy, CORS, certificate validation, or storage quotas.
Below is the complete inventory of every restriction we've overridden and how.
### WebKitSettings (per-tab, in [`src/tab_manager.c`](src/tab_manager.c))
| Setting | Value | Effect |
|---------|-------|--------|
| `enable_developer_extras` | `TRUE` | Web Inspector / right-click Inspect Element |
| `enable_javascript` | `TRUE` | JavaScript enabled |
| `javascript_can_open_windows_automatically` | `TRUE` | `window.open()` without user gesture |
| `allow_file_access_from_file_urls` | `TRUE` | `file://` pages can fetch/XHR other local files |
| `allow_universal_access_from_file_urls` | `TRUE` | `file://` pages bypass same-origin policy entirely |
| `allow_modal_dialogs` | `TRUE` | `alert()`, `confirm()`, `prompt()` work |
| `hardware_acceleration_policy` | `ALWAYS` | Forces GPU compositing even on software renderers |
| `enable_smooth_scrolling` | `TRUE` | Smooth scroll animation |
### WebKitSecurityManager (per-context, in [`src/web_context.c`](src/web_context.c))
| Registration | Effect |
|-------------|--------|
| `register_uri_scheme_as_secure("sovereign")` | `sovereign://` treated as secure origin |
| `register_uri_scheme_as_secure("tor")` | `tor://` treated as secure origin |
| `register_uri_scheme_as_secure("file")` | `file://` treated as secure origin (normally insecure) |
| `register_uri_scheme_as_secure("ws")` | `ws://` treated as secure (allows plain WebSocket from secure pages) |
| `register_uri_scheme_as_local("file")` | `file://` treated as local (can load local resources) |
| `register_uri_scheme_as_secure("local")` | `local://` treated as secure origin |
| `register_uri_scheme_as_local("local")` | `local://` treated as local |
| `register_uri_scheme_as_cors_enabled("local")` | `local://` pages can fetch/XHR other local:// URLs without CORS |
### TLS Policy (in [`src/web_context.c`](src/web_context.c))
| Setting | Value | Effect |
|---------|-------|--------|
| `tls_errors_policy` | `IGNORE` | Accepts any TLS certificate, including self-signed/expired |
### CORS bypass for custom schemes (in [`src/nostr_inject.c`](src/nostr_inject.c))
The `window.nostr` shim uses **synchronous XMLHttpRequest** to
`sovereign://nostr/*` instead of `fetch()`. WebKitGTK enforces CORS on
`fetch()` and async XHR even for secure custom schemes, but **synchronous
XHR to secure custom schemes bypasses CORS**. This is a WebKitGTK quirk
we exploit intentionally.
### `file://` storage quota bypass via `local://` scheme
WebKit's WebCore assigns each `file://` URL a **unique opaque origin** with
a **0-byte storage quota**, silently breaking `localStorage` and `IndexedDB`
on local files. We bypass this with a custom [`local://`](src/local_scheme.c)
URI scheme that:
1. Maps `local:///absolute/path/to/file` to the local filesystem
2. Creates a **proper (non-opaque) SecurityOrigin** with the default storage
quota (typically 5-10 MB for localStorage, unlimited for IndexedDB)
3. Is registered as secure, local, and CORS-enabled
All `file://` navigations (URL bar, page links, `decide-policy`) are
automatically rewritten to `local://` by [`src/tab_manager.c`](src/tab_manager.c).
### Custom URI schemes registered
| Scheme | Handler | File |
|--------|---------|------|
| `sovereign://` | `on_sovereign_scheme` | [`src/nostr_bridge.c`](src/nostr_bridge.c) |
| `nostr://` | `on_nostr_scheme` | [`src/nostr_scheme.c`](src/nostr_scheme.c) |
| `tor://` | `on_tor_scheme` | [`src/tor_scheme.c`](src/tor_scheme.c) |
| `local://` | `on_local_scheme` | [`src/local_scheme.c`](src/local_scheme.c) |
## Install
One-command install on Debian 13 (trixie) or similar (x86_64, WebKitGTK 4.1):
+1 -1
View File
@@ -1 +1 @@
0.0.63
0.0.68
+14 -6
View File
@@ -240,18 +240,26 @@ cmd_start() {
fi
if [[ "$software_rendering" -eq 1 ]]; then
echo "[browser] Software rendering detected — disabling WebKit compositor."
export WEBKIT_DISABLE_COMPOSITING_MODE=1
export WEBKIT_DISABLE_DMABUF_RENDERER=1
# On llvmpipe/softpipe, WebKit compositing is still faster than the
# non-composited fallback (confirmed by A/B testing on Qubes llvmpipe).
# We no longer disable compositing — just log a note.
# The DMABUF renderer is still disabled since there's no real GPU.
echo "[browser] Software rendering detected (llvmpipe/softpipe) — compositing kept enabled for performance."
if [[ -z "${WEBKIT_DISABLE_DMABUF_RENDERER:-}" ]]; then
export WEBKIT_DISABLE_DMABUF_RENDERER=1
fi
# Allow override via pre-set env var for A/B testing.
if [[ -z "${WEBKIT_DISABLE_COMPOSITING_MODE:-}" ]]; then
export WEBKIT_DISABLE_COMPOSITING_MODE=0
fi
fi
# Build first
# Build first (use build.sh for proper version embedding)
echo "[browser] Building..."
if ! make -s 2>/dev/null; then
if ! ./build.sh 2>/dev/null; then
echo "[browser] Build failed!" >&2
return 1
fi
echo "[browser] Build OK."
# Launch fully detached:
# setsid — new session, detached from controlling terminal
+81
View File
@@ -0,0 +1,81 @@
# Debugging Responsiveness — sovereign_browser
## Symptoms (from user)
1. **Hover lag**: a page that highlights buttons on mouseover responds ~2× slower than in Brave.
2. **Load progress invisible until complete**: page loads "take a while" and show no progress until the download finishes.
## Confirmed root-cause candidates (from code review)
### A. Hover/scroll lag — WebKitGTK rendering pipeline
[`src/tab_manager.c:2912`](src/tab_manager.c:2912) configures `WebKitSettings` but only sets:
- `enable_developer_extras`, `enable_javascript`, `javascript_can_open_windows_automatically`
- `allow_file_access_from_file_urls`, `allow_universal_access_from_file_urls`, `allow_modal_dialogs`
**Not set** (left at WebKitGTK defaults):
- `enable_accelerated_2d_canvas` (default FALSE on many builds)
- `enable_smooth_scrolling` (default FALSE)
- compositing mode — controlled by env var `WEBKIT_DISABLE_COMPOSITING_MODE=1`, which if set forces non-accelerated compositing
WebKitGTK ships software-rendered by default on many distros; Chromium/Brave use GPU compositing + Skia by default. This alone explains ~2× paint lag on hover effects (each `:hover` triggers a repaint; software repaint is much slower).
### B. No load-progress UI
[`on_load_changed`](src/tab_manager.c:1635) handles `LOAD_STARTED` (→ stop icon), `LOAD_COMMITTED` (→ URL bar/title), `LOAD_FINISHED`/`LOAD_FAILED` (→ reload icon). There is **no** `notify::estimated-load-progress` signal handler and **no** progress bar/spinner in the tab strip. So between STARTED and FINISHED the user sees only a static stop icon — "nothing happens until done."
## Debugging plan (ordered: cheap → invasive)
### Step 1 — Confirm the rendering hypothesis with env vars (no code change)
Run the browser with GPU compositing forced on and compare hover responsiveness:
```bash
WEBKIT_DISABLE_COMPOSITING_MODE=0 ./browser.sh start --login-method generate --url <hover-test-page>
# vs
WEBKIT_DISABLE_COMPOSITING_MODE=1 ./browser.sh start --login-method generate --url <hover-test-page>
```
Also try `WEBKIT_FORCE_SANDBOX=0` only if the above is inconclusive (rules out sandbox-overhead noise). If forcing compositing on closes the gap with Brave, the fix is enabling accelerated settings + ensuring the compositor is active.
### Step 2 — Use the existing perf-probe to quantify
The browser already ships [`www/js/perf-probe.js`](www/js/perf-probe.js) (long-task observer, rAF FPS, heartbeat main-thread-blockage fallback for WebKitGTK). Enable it (Settings → perf_probe_enabled, or it's gated by `settings_get()->perf_probe_enabled` at [`tab_manager.c:1337`](src/tab_manager.c:1337)) and load the hover-test page. Compare:
- `cpu_busy_percent` (heartbeat blockage) — high = main thread is being starved
- `fps` — low fps during hover = paint-bound, points at rendering pipeline
- `active_timer_count` — high = page is timer-heavy (not our bug)
Call the MCP `processes.tabs` tool ([`agent_mcp.c:583`](src/agent_mcp.c:583)) to read the probe values per tab while hovering.
### Step 3 — System-level profiling to rule out the C/GTK main thread
Even if rendering is the cause, confirm the GTK main thread isn't also blocked. Two cheap checks:
```bash
# a) Is the UI process CPU-bound during hover?
top -p $(pgrep -f sovereign_browser) -H -d 0.5
# b) perf record the UI process for 10s while hovering, then look at hotspots
sudo perf record -p $(pgrep -f sovereign_browser) -g -- sleep 10
sudo perf report --no-children
```
Look for time in `webkit*`/`WebCore*`/`cairo`/`GLib` dispatchers. If time is dominated by `g_main_context_*` or our own handlers, the C layer is implicated; if it's `WebCore::paint`/`cairo`, it's the renderer.
### Step 4 — Check for main-thread blocking from sync JS eval
[`agent_js_eval_sync`](src/agent_snapshot.c:80) runs a nested `g_main_loop` on the default context. The agent loop ([`agent_loop.c:333`](src/agent_loop.c:333)) calls this from a background thread while pumping the main loop. If an agent/MCP tool is running during normal browsing, this can re-dispatch unrelated sources and cause UI hitches. Verify by checking whether sluggishness only occurs while an agent loop is active (watch `[agent-loop] iter` log lines). If correlated, the fix is to route sync JS eval through a dedicated `GMainContext` instead of the default one.
### Step 5 — Fix candidates (after diagnosis confirms which)
**Rendering (likely):**
- In [`src/tab_manager.c:2912`](src/tab_manager.c:2912) add:
```c
webkit_settings_set_enable_accelerated_2d_canvas(settings, TRUE);
webkit_settings_set_enable_smooth_scrolling(settings, TRUE);
```
and ensure `WEBKIT_DISABLE_COMPOSITING_MODE` is **not** set in the launch environment (check `browser.sh`).
- Verify GPU is actually being used: `WEBKIT_DEBUG=compositing` or check `glxinfo`/EGL logs.
**Load progress (confirmed gap):**
- Add a `notify::estimated-load-progress` handler on each webview that drives a thin progress bar (or the tab label background) between `LOAD_STARTED` and `LOAD_FINISHED`. WebKitGTK emits this continuously during fetch; wiring it gives the missing "loading" feedback. Add the handler next to the `load-changed` connect at [`src/tab_manager.c:3136`](src/tab_manager.c:3136).
## Recommended order
1. Step 1 (env-var A/B test) — fastest signal, no rebuild.
2. Step 2 (perf-probe) — quantifies, uses existing infra.
3. Step 3 (perf/top) — rules out C-layer blocking.
4. Step 4 — only if sluggishness correlates with agent activity.
5. Step 5 — implement the fix(es) confirmed by 14.
+2
View File
@@ -131,6 +131,8 @@ A fixed enum of browser actions the user can bind. Each has: id, display label,
| Action ID | Label | Default binding |
|----------------------|------------------------|---------------------------|
| `new_tab` | New tab | `<Control>t` |
| `new_window` | New window | `<Control>n` |
| `open_file` | Open file | `<Control>o` |
| `close_tab` | Close tab | `<Control>w` |
| `focus_url` | Focus URL bar | `<Control>l` |
| `next_tab` | Next tab | `<Control>Tab` |
+139
View File
@@ -0,0 +1,139 @@
# Security Restrictions Overridden & `file://` Storage Quota Fix
## Part 1 — Inventory of all security restrictions we've overridden
### WebKitSettings (per-tab, in [`src/tab_manager.c:2955`](src/tab_manager.c:2955))
| Setting | Value | Effect |
|---------|-------|--------|
| `enable_developer_extras` | `TRUE` | Enables Web Inspector / right-click Inspect Element |
| `enable_javascript` | `TRUE` | JavaScript enabled (default, but explicit) |
| `javascript_can_open_windows_automatically` | `TRUE` | Allows `window.open()` without user gesture |
| `allow_file_access_from_file_urls` | `TRUE` | `file://` pages can fetch/XHR other local files (normally blocked) |
| `allow_universal_access_from_file_urls` | `TRUE` | `file://` pages bypass same-origin policy entirely |
| `allow_modal_dialogs` | `TRUE` | `alert()`, `confirm()`, `prompt()` work |
| `hardware_acceleration_policy` | `ALWAYS` | Forces GPU compositing even on software renderers |
### WebKitSecurityManager (per-context, in [`src/web_context.c:207`](src/web_context.c:207))
| Registration | Effect |
|-------------|--------|
| `register_uri_scheme_as_secure("sovereign")` | `sovereign://` treated as secure origin (no mixed-content warnings) |
| `register_uri_scheme_as_secure("tor")` | `tor://` treated as secure origin |
| `register_uri_scheme_as_secure("file")` | `file://` treated as secure origin (normally insecure) |
| `register_uri_scheme_as_secure("ws")` | `ws://` treated as secure (allows plain WebSocket from secure pages) |
| `register_uri_scheme_as_local("file")` | `file://` treated as local (can load local resources) |
### TLS Policy (in [`src/web_context.c:218`](src/web_context.c:218))
| Setting | Value | Effect |
|---------|-------|--------|
| `tls_errors_policy` | `IGNORE` | Accepts any TLS certificate, including self-signed/expired |
### CORS bypass (in [`src/nostr_inject.c:38`](src/nostr_inject.c:38))
The `window.nostr` shim uses **synchronous XMLHttpRequest** to `sovereign://nostr/*` instead of `fetch()`. WebKitGTK enforces CORS on `fetch()` and async XHR even for secure custom schemes, but **synchronous XHR to secure custom schemes bypasses CORS**. This is a WebKitGTK quirk we exploit intentionally.
### Custom URI schemes registered
| Scheme | Handler | File |
|--------|---------|------|
| `sovereign://` | `on_sovereign_scheme` | [`src/nostr_bridge.c:4588`](src/nostr_bridge.c:4588) |
| `nostr://` | `on_nostr_scheme` | [`src/nostr_scheme.c:410`](src/nostr_scheme.c:410) |
| `tor://` | `on_tor_scheme` | [`src/tor_scheme.c:289`](src/tor_scheme.c:289) |
---
## Part 2 — The `file://` Storage Quota Problem
### Root cause
WebKit's WebCore layer assigns each `file://` URL a **unique opaque origin** with a **0-byte storage quota**. This is hardcoded in `WebCore::SecurityOrigin::createForFilePath()` and `WebCore::StorageQuotaManager`. Even though we've set `allow_file_access_from_file_urls=TRUE` and `allow_universal_access_from_file_urls=TRUE`, those settings affect **network access** (fetch/XHR/CORS), not **storage quotas**.
The result:
- `localStorage.setItem('key', 'value')` silently fails or throws `QuotaExceededError`
- `IndexedDB.open('db')` fails with `QuotaExceededError`
- `sessionStorage` works (it's in-memory per-tab, not quota-managed)
### No public WebKitGTK API to fix this
The WebKitGTK headers expose:
- `webkit_settings_set_enable_html5_local_storage()` — enables/disables localStorage entirely (default: TRUE)
- `webkit_website_data_manager_get_local_storage_directory()` — returns the on-disk path (deprecated)
There is **no public API** to set the storage quota for a given origin or to change how `file://` origins are classified.
### Solution: Custom `local://` URI scheme
The cleanest approach is to create a custom URI scheme that:
1. Has a **proper origin** (not opaque like `file://`)
2. Gets **unlimited storage quota** (the default for http/https origins)
3. Serves the same file content
#### Implementation plan
**Step 1 — Register a new `local://` scheme** in [`src/web_context.c`](src/web_context.c):
```c
// In configure_context():
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "local");
webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "local");
webkit_security_manager_register_uri_scheme_as_cors_enabled(sec_mgr, "local");
```
**Step 2 — Create a handler** that maps `local:///path/to/file` to the local filesystem:
```c
// New file: src/local_scheme.c
static void on_local_scheme(WebKitURISchemeRequest *request) {
const char *uri = webkit_uri_scheme_request_get_uri(request);
// Strip "local://" prefix to get the file path
const char *path = uri + 8; // "local://" = 8 chars
// Read the file and serve it
char *content;
gsize length;
if (g_file_get_contents(path, &content, &length, NULL)) {
const char *mime = g_content_type_guess(path, NULL, 0, NULL);
webkit_uri_scheme_request_finish(request, content, length, mime);
g_free(content);
}
}
```
**Step 3 — Intercept `file://` navigation** in the `decide-policy` handler ([`src/tab_manager.c`](src/tab_manager.c)) and rewrite to `local://`:
```c
// In on_decide_policy(), when navigation policy is requested:
if (g_str_has_prefix(uri, "file://")) {
// Rewrite file:///home/user/foo.html -> local:///home/user/foo.html
char *local_uri = g_strconcat("local", uri + 4, NULL); // "file" -> "local"
webkit_uri_request_set_uri(request, local_uri);
g_free(local_uri);
}
```
**Step 4 — Also rewrite `file://` URLs in the URL bar** when the user presses Enter, so typed paths get the `local://` scheme.
### Why this works
When a page is loaded from `local:///home/user/site/index.html`, WebKit creates a `SecurityOrigin` with scheme `local`, host `localhost` (or empty), and port `0`. This is a **valid, non-opaque origin** that gets the **default storage quota** (typically 5-10 MB for localStorage, unlimited for IndexedDB). The same origin is shared by all `local://` URLs, so pages can share localStorage.
### Alternative considered: Local HTTP server
We could run a minimal HTTP server on `localhost` using libsoup and serve files from `http://localhost:PORT/path`. This would also give proper origins with full storage. However, it adds:
- Port management (conflict with other services)
- Startup latency (server must be ready before pages load)
- Complexity (MIME type handling, directory listing, etc.)
The custom scheme approach is simpler and has no port/startup issues.
### Files to modify
| File | Change |
|------|--------|
| [`src/web_context.c:207`](src/web_context.c:207) | Register `local://` as secure, local, and CORS-enabled |
| `src/local_scheme.c` (new) | Custom scheme handler that reads files from disk |
| `src/local_scheme.h` (new) | Header for the handler |
| [`src/tab_manager.c`](src/tab_manager.c) | Intercept `file://` navigation in `decide-policy` and rewrite to `local://` |
| [`src/main.c`](src/main.c) | Register the `local://` scheme during startup |
| `Makefile` | Add `local_scheme.o` to the build |
+182
View File
@@ -0,0 +1,182 @@
/*
* local_scheme.c — local:// URI scheme handler for sovereign_browser
*
* Maps local:///absolute/path/to/file to the local filesystem. Unlike
* file://, this scheme creates a proper (non-opaque) SecurityOrigin in
* WebKit, so localStorage, IndexedDB, and other web storage APIs work
* with the default quota instead of the 0-byte quota that WebKit assigns
* to file:// origins.
*
* The handler reads the requested file from disk and serves it with the
* correct MIME type. Directory requests return a 404 (no directory listing).
*
* Security: this is intentionally unrestricted — the browser's security
* model is at the Qube level, not the browser level.
*/
#include "local_scheme.h"
#include <glib.h>
#include <string.h>
/* ── MIME type helper ───────────────────────────────────────────────── */
/* Guess the MIME type from a file path's extension. Falls back to
* application/octet-stream for unknown types. */
static const char *guess_mime(const char *path) {
const char *ext = strrchr(path, '.');
if (ext == NULL) return "application/octet-stream";
if (g_ascii_strcasecmp(ext, ".html") == 0 ||
g_ascii_strcasecmp(ext, ".htm") == 0)
return "text/html; charset=utf-8";
if (g_ascii_strcasecmp(ext, ".css") == 0)
return "text/css; charset=utf-8";
if (g_ascii_strcasecmp(ext, ".js") == 0)
return "application/javascript; charset=utf-8";
if (g_ascii_strcasecmp(ext, ".mjs") == 0)
return "text/javascript; charset=utf-8";
if (g_ascii_strcasecmp(ext, ".json") == 0)
return "application/json";
if (g_ascii_strcasecmp(ext, ".svg") == 0)
return "image/svg+xml";
if (g_ascii_strcasecmp(ext, ".png") == 0)
return "image/png";
if (g_ascii_strcasecmp(ext, ".jpg") == 0 ||
g_ascii_strcasecmp(ext, ".jpeg") == 0)
return "image/jpeg";
if (g_ascii_strcasecmp(ext, ".gif") == 0)
return "image/gif";
if (g_ascii_strcasecmp(ext, ".ico") == 0)
return "image/x-icon";
if (g_ascii_strcasecmp(ext, ".webp") == 0)
return "image/webp";
if (g_ascii_strcasecmp(ext, ".woff") == 0)
return "font/woff";
if (g_ascii_strcasecmp(ext, ".woff2") == 0)
return "font/woff2";
if (g_ascii_strcasecmp(ext, ".ttf") == 0)
return "font/ttf";
if (g_ascii_strcasecmp(ext, ".otf") == 0)
return "font/otf";
if (g_ascii_strcasecmp(ext, ".mp4") == 0)
return "video/mp4";
if (g_ascii_strcasecmp(ext, ".webm") == 0)
return "video/webm";
if (g_ascii_strcasecmp(ext, ".mp3") == 0)
return "audio/mpeg";
if (g_ascii_strcasecmp(ext, ".ogg") == 0)
return "audio/ogg";
if (g_ascii_strcasecmp(ext, ".pdf") == 0)
return "application/pdf";
if (g_ascii_strcasecmp(ext, ".txt") == 0)
return "text/plain; charset=utf-8";
if (g_ascii_strcasecmp(ext, ".xml") == 0)
return "application/xml";
if (g_ascii_strcasecmp(ext, ".wasm") == 0)
return "application/wasm";
return "application/octet-stream";
}
/* ── Scheme handler ─────────────────────────────────────────────────── */
static void on_local_scheme(WebKitURISchemeRequest *request, gpointer user_data) {
(void)user_data;
const char *uri = webkit_uri_scheme_request_get_uri(request);
/* Strip "local://" prefix to get the file path. The URI is
* local:///absolute/path, so we skip 8 characters ("local://")
* and the result is /absolute/path. */
const char *path_start = uri + 8;
if (path_start == NULL || path_start[0] == '\0') {
g_printerr("[local-scheme] Empty path in URI: %s\n", uri);
webkit_uri_scheme_request_finish_error(request, g_error_new_literal(
g_quark_from_static_string("local-scheme"), 1, "Empty path"));
return;
}
/* Strip query parameters (?...) from the path. Web pages and
* SharedWorkers may append ?v=... or other cache-busting params. */
char *path = g_strdup(path_start);
char *qmark = strchr(path, '?');
if (qmark) *qmark = '\0';
/* Resolve root-relative paths against the requesting page's directory.
* If the path is an absolute filesystem path (e.g. /home/user/...),
* use it as-is. If it's a root-relative path (e.g. /nostr-login-lite/...)
* that doesn't exist, try resolving it relative to the page's directory. */
if (path[0] == '/' && !g_file_test(path, G_FILE_TEST_EXISTS)) {
WebKitWebView *wv = webkit_uri_scheme_request_get_web_view(request);
if (wv) {
const char *page_uri = webkit_web_view_get_uri(wv);
if (page_uri && g_str_has_prefix(page_uri, "local://")) {
/* Get the page's directory. */
const char *page_path = page_uri + 8;
char *page_dir = g_strdup(page_path);
char *last_slash = strrchr(page_dir, '/');
if (last_slash) {
*(last_slash + 1) = '\0';
char *resolved = g_strconcat(page_dir, path + 1, NULL);
if (g_file_test(resolved, G_FILE_TEST_EXISTS)) {
g_print("[local-scheme] Resolved root-relative %s -> %s\n",
path, resolved);
g_free(path);
path = resolved;
} else {
g_free(resolved);
}
}
g_free(page_dir);
}
}
}
/* Read the file. */
gsize length = 0;
char *content = NULL;
GError *error = NULL;
if (!g_file_get_contents(path, &content, &length, &error)) {
g_printerr("[local-scheme] Failed to read %s: %s\n", path,
error ? error->message : "unknown error");
g_free(path);
if (error) g_error_free(error);
webkit_uri_scheme_request_finish_error(request, g_error_new_literal(
g_quark_from_static_string("local-scheme"), 2, "File not found"));
return;
}
/* Determine MIME type and serve. */
const char *mime_type = guess_mime(path);
g_print("[local-scheme] Serving %s (%s, %lu bytes)\n", path, mime_type,
(unsigned long)length);
g_free(path);
/* Wrap the content in a GInputStream. g_memory_input_stream_new_from_data
* takes ownership of the data via the GDestroyNotify callback. */
GInputStream *stream = g_memory_input_stream_new_from_data(
content, length, g_free);
webkit_uri_scheme_request_finish(request, stream, length, mime_type);
g_object_unref(stream);
}
/* ── Public API ─────────────────────────────────────────────────────── */
void local_scheme_setup(WebKitWebContext *ctx) {
g_return_if_fail(WEBKIT_IS_WEB_CONTEXT(ctx));
webkit_web_context_register_uri_scheme(ctx, "local", on_local_scheme,
NULL, NULL);
/* Register local:// as secure (no mixed-content warnings), local
* (can load local resources), and CORS-enabled (so fetch/XHR from
* local:// pages to other local:// URLs work without CORS headers). */
WebKitSecurityManager *sec_mgr =
webkit_web_context_get_security_manager(ctx);
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "local");
webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "local");
webkit_security_manager_register_uri_scheme_as_cors_enabled(sec_mgr, "local");
g_print("[local-scheme] Registered local:// scheme handler\n");
}
+36
View File
@@ -0,0 +1,36 @@
/*
* local_scheme.h — local:// URI scheme handler for sovereign_browser
*
* Registers a "local" URI scheme that maps local:///path/to/file to the
* local filesystem. Unlike file://, local:// creates a proper (non-opaque)
* SecurityOrigin in WebKit, which means localStorage, IndexedDB, and other
* web storage APIs work with the default quota instead of the 0-byte quota
* that WebKit assigns to file:// origins.
*
* Usage:
* local_scheme_setup(ctx) — register the handler on the WebKitWebContext
*
* Then load pages via local:///absolute/path/to/file.html instead of
* file:///absolute/path/to/file.html.
*/
#ifndef LOCAL_SCHEME_H
#define LOCAL_SCHEME_H
#include <webkit2/webkit2.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Register the local:// URI scheme handler on the given WebKitWebContext.
* Must be called once during startup, after the context is created.
*/
void local_scheme_setup(WebKitWebContext *ctx);
#ifdef __cplusplus
}
#endif
#endif /* LOCAL_SCHEME_H */
+6
View File
@@ -38,6 +38,7 @@
#include "nostr_bridge.h"
#include "nostr_scheme.h"
#include "tor_scheme.h"
#include "local_scheme.h"
#include "nostr_inject.h"
#include "history.h"
#include "settings.h"
@@ -533,6 +534,10 @@ gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
tab_manager_new_window_blank();
return TRUE;
case SHORTCUT_OPEN_FILE:
tab_manager_open_file();
return TRUE;
case SHORTCUT_CLOSE_TAB:
tab_manager_close_active();
return TRUE;
@@ -931,6 +936,7 @@ static WebKitWebContext *build_context_for_current_user(void) {
* on every fresh context. */
nostr_scheme_register(web_ctx);
tor_scheme_register(web_ctx);
local_scheme_setup(web_ctx);
/* Point tab_manager at the new context so subsequent new tabs (and
* the sidebar webview) are created from it. */
+4
View File
@@ -1255,6 +1255,10 @@ static void handle_settings_set(WebKitURISchemeRequest *request,
}
/* Trigger debounced NIP-78 sync. */
settings_sync_publish();
} else if (strcmp(key, "perf_probe.enabled") == 0) {
bs->perf_probe_enabled = (strcmp(value, "true") == 0 ||
strcmp(value, "1") == 0);
settings_save();
} else {
respond_error_json(request, -1, "Unknown key");
g_free(key); g_free(value);
+26 -2
View File
@@ -28,6 +28,10 @@
#define MAX_RELAYS 32
/* Relay query timeout in seconds. */
/* Forward declaration — defined below. */
static gboolean merge_settings_idle(gpointer data);
#define RELAY_TIMEOUT_SECONDS 15
/* Forward declaration — defined after relay_fetch_bootstrap. */
@@ -120,11 +124,18 @@ int relay_fetch_bootstrap(const char *pubkey_hex,
/* Store in SQLite first, then try to merge into local settings.
* settings_sync_merge_from_nostr checks the d-tag and only
* merges if it's the shared "user-settings" event (or the
* legacy "sovereign_browser" event, which is migrated). */
* legacy "sovereign_browser" event, which is migrated).
*
* IMPORTANT: The merge calls settings_load() which writes to
* the global g_settings struct. This must run on the main
* thread to avoid a data race with the GTK main loop. */
if (db_store_event(results[i]) == 0) {
stored++;
}
settings_sync_merge_from_nostr(results[i]);
cJSON *event_copy = cJSON_Duplicate(results[i], 1);
if (event_copy) {
g_idle_add(merge_settings_idle, event_copy);
}
} else {
if (db_store_event(results[i]) == 0) {
stored++;
@@ -208,6 +219,19 @@ static gboolean avatar_refresh_idle(gpointer data) {
return G_SOURCE_REMOVE;
}
/* Idle callback to merge NIP-78 settings on the main thread.
* settings_sync_merge_from_nostr calls settings_load() which writes to
* the global g_settings struct, so it must run on the GTK main thread
* to avoid data races with the main loop. Takes ownership of event_cjson. */
static gboolean merge_settings_idle(gpointer data) {
cJSON *event = (cJSON *)data;
if (event) {
settings_sync_merge_from_nostr(event);
cJSON_Delete(event);
}
return FALSE; /* run once */
}
/* ── Background thread ─────────────────────────────────────────────── */
gpointer relay_fetch_thread(gpointer data) {
+4
View File
@@ -30,6 +30,10 @@ static const shortcut_meta_t g_registry[SHORTCUT_COUNT] = {
"new_window", "New window",
"Open a new browser window", "<Control>n"
},
[SHORTCUT_OPEN_FILE] = {
"open_file", "Open file",
"Open a local file in the active tab", "<Control>o"
},
[SHORTCUT_CLOSE_TAB] = {
"close_tab", "Close tab",
"Close the active tab", "<Control>w"
+1
View File
@@ -29,6 +29,7 @@ extern "C" {
typedef enum {
SHORTCUT_NEW_TAB = 0,
SHORTCUT_NEW_WINDOW,
SHORTCUT_OPEN_FILE,
SHORTCUT_CLOSE_TAB,
SHORTCUT_FOCUS_URL,
SHORTCUT_NEXT_TAB,
+86 -3
View File
@@ -1074,6 +1074,18 @@ static gboolean on_decide_policy(WebKitWebView *webview,
}
}
/* Rewrite file:// URIs to local:// so pages get a proper SecurityOrigin
* with full storage quota (localStorage, IndexedDB). WebKit assigns a
* 0-byte quota to file:// origins, but local:// creates a non-opaque
* origin with the default storage quota. */
if (uri && strncmp(uri, "file://", 7) == 0) {
char *local_uri = g_strconcat("local", uri + 4, NULL); /* "file" -> "local" */
g_print("[decide-policy] Rewriting %s -> %s\n", uri, local_uri);
webkit_policy_decision_ignore(decision);
defer_load_uri(webview, local_uri); /* takes ownership */
return TRUE;
}
/* NIP-21 links use nostr:entity rather than nostr://entity. Normalize
* either form so WebKit consistently invokes our registered handler. */
if (uri && strncmp(uri, "nostr:", 6) == 0 &&
@@ -1484,6 +1496,14 @@ static void on_url_activate(GtkEntry *entry, gpointer user_data) {
char *url = normalize_url(text);
if (url != NULL) {
/* Rewrite file:// to local:// so pages get a proper SecurityOrigin
* with full storage quota (localStorage, IndexedDB). */
if (strncmp(url, "file://", 7) == 0) {
char *local_url = g_strconcat("local", url + 4, NULL);
g_print("[url-bar] Rewriting %s -> %s\n", url, local_url);
g_free(url);
url = local_url;
}
webkit_web_view_load_uri(tab->webview, url);
g_free(url);
}
@@ -1632,6 +1652,30 @@ static gboolean on_refresh_button_press(GtkWidget *widget,
return FALSE; /* Let left-click propagate to "clicked" signal */
}
/* ── Load progress bar ──────────────────────────────────────────────── *
* WebKitGTK emits notify::estimated-load-progress continuously during
* page fetch. We drive a thin progress bar between LOAD_STARTED and
* LOAD_FINISHED so the user sees activity even when the page takes a
* while to download. The bar is hidden on LOAD_FINISHED / LOAD_FAILED. */
static void on_load_progress_changed(WebKitWebView *webview,
GParamSpec *pspec,
gpointer user_data) {
(void)pspec;
tab_info_t *tab = (tab_info_t *)user_data;
if (tab == NULL || tab->progress_bar == NULL) return;
gdouble progress = webkit_web_view_get_estimated_load_progress(webview);
if (progress < 0.01) {
/* Not yet loading — keep hidden. */
return;
}
if (!gtk_widget_get_visible(tab->progress_bar)) {
gtk_widget_set_visible(tab->progress_bar, TRUE);
}
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(tab->progress_bar), progress);
}
static void on_load_changed(WebKitWebView *webview,
WebKitLoadEvent load_event,
gpointer user_data) {
@@ -1699,6 +1743,12 @@ static void on_load_changed(WebKitWebView *webview,
/* Restore the reload button visual now that loading is done. */
tab_refresh_button_update(tab);
/* Hide the load-progress bar. */
if (tab != NULL && tab->progress_bar != NULL &&
gtk_widget_get_visible(tab->progress_bar)) {
gtk_widget_set_visible(tab->progress_bar, FALSE);
}
/* Update tab title if already available (works for tabs in any
* window's notebook). The notify::title handler covers the case
* where the title arrives after load-finished. */
@@ -1739,6 +1789,12 @@ static gboolean on_load_failed(WebKitWebView *webview,
* user clicks the X to cancel a load. */
tab_refresh_button_update(tab);
/* Hide the load-progress bar on failure/cancellation. */
if (tab != NULL && tab->progress_bar != NULL &&
gtk_widget_get_visible(tab->progress_bar)) {
gtk_widget_set_visible(tab->progress_bar, FALSE);
}
/* Keep the failed URL in the address bar so the user can see what
* failed and edit/retry. Without this, WebKit reverts to about:blank
* and the user loses the URL they typed or clicked. */
@@ -2105,9 +2161,11 @@ static void on_menu_toggle_sidebar(GtkMenuItem *item, gpointer data) {
tab_manager_toggle_sidebar();
}
static void on_menu_open_file(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
/* Show the "Open File" dialog and load the chosen file into the active
* tab. Shared by the hamburger-menu callback and the Ctrl+O keyboard
* shortcut. Parents the dialog to the focused window so it appears over
* the window the user invoked it from. No-op if there is no window. */
void tab_manager_open_file(void) {
/* Parent the dialog to the currently focused window (g_active_window)
* rather than always the main window (g_window), so the file chooser
* appears over the window the user invoked it from. g_active_window is
@@ -2145,6 +2203,12 @@ static void on_menu_open_file(GtkMenuItem *item, gpointer data) {
gtk_widget_destroy(dialog);
}
static void on_menu_open_file(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
tab_manager_open_file();
}
static void on_menu_history_item(GtkMenuItem *item, gpointer data) {
(void)data;
tab_info_t *tab = tab_manager_get_active();
@@ -2916,6 +2980,13 @@ static tab_info_t *tab_create(const char *url) {
webkit_settings_set_allow_file_access_from_file_urls(settings, TRUE);
webkit_settings_set_allow_universal_access_from_file_urls(settings, TRUE);
webkit_settings_set_allow_modal_dialogs(settings, TRUE);
/* Rendering performance: force hardware acceleration always on and
* enable smooth scrolling. These are off by default in WebKitGTK on
* many distros but significantly improve hover/scroll responsiveness
* even on llvmpipe software rendering. */
webkit_settings_set_hardware_acceleration_policy(settings,
WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
webkit_settings_set_enable_smooth_scrolling(settings, TRUE);
/* Enhance native application/json documents at document end. The
* embedded file is not NUL-terminated, so copy it before passing it to
@@ -3099,6 +3170,16 @@ static tab_info_t *tab_create(const char *url) {
gtk_box_pack_start(GTK_BOX(tab->page), tab->bookmark_bar, FALSE, FALSE, 0);
bookmark_bar_refresh(tab);
/* Thin load-progress bar between the bookmark bar and the webview.
* Hidden by default; shown during page loads via the
* notify::estimated-load-progress signal. */
tab->progress_bar = gtk_progress_bar_new();
gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(tab->progress_bar), FALSE);
gtk_widget_set_size_request(tab->progress_bar, -1, 3);
gtk_widget_set_no_show_all(tab->progress_bar, TRUE);
gtk_widget_set_visible(tab->progress_bar, FALSE);
gtk_box_pack_start(GTK_BOX(tab->page), tab->progress_bar, FALSE, FALSE, 0);
/* Apply the active window's menu-bar visibility so a tab opened in a
* hidden-chrome window is also hidden. The tab strip itself is
* per-notebook (gtk_notebook_set_show_tabs), already set by the
@@ -3137,6 +3218,8 @@ static tab_info_t *tab_create(const char *url) {
G_CALLBACK(on_load_changed), tab);
g_signal_connect(tab->webview, "load-failed",
G_CALLBACK(on_load_failed), tab);
g_signal_connect(tab->webview, "notify::estimated-load-progress",
G_CALLBACK(on_load_progress_changed), tab);
g_signal_connect(tab->webview, "notify::favicon",
G_CALLBACK(on_favicon_changed), tab);
g_signal_connect(tab->webview, "notify::title",
+9
View File
@@ -31,6 +31,7 @@ typedef struct {
GtkWidget *refresh_btn; /* toolbar reload/stop button (per-tab) */
GtkWidget *toolbar; /* #main-toolbar (hamburger + nav + URL) */
GtkWidget *bookmark_bar; /* bookmarks toolbar (below URL toolbar) */
GtkWidget *progress_bar; /* thin load-progress bar (below bookmark bar, above webview) */
gboolean refresh_shows_stop; /* cached visual state, avoids redundant updates */
char current_url[TAB_URL_MAX];
char title[TAB_TITLE_MAX];
@@ -80,6 +81,14 @@ void tab_manager_open_in_new_window(int index);
*/
void tab_manager_new_window_blank(void);
/*
* Show the "Open File" dialog and load the chosen local file into the
* active tab via a file:// URI. The dialog is parented to the focused
* window. Used by the hamburger-menu "Open File…" item and the Ctrl+O
* keyboard shortcut. No-op if there is no window or active tab.
*/
void tab_manager_open_file(void);
/*
* Close the tab at the given index. If it's the last tab, the window
* destroy handler will fire (caller is responsible for quitting).
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
#define SB_VERSION "v0.0.63"
#define SB_VERSION "v0.0.68"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 63
#define SB_VERSION_PATCH 68
#endif /* SOVEREIGN_BROWSER_VERSION_H */
+123
View File
@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hover Responsiveness Test</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: sans-serif;
background: #1a1a2e;
color: #eee;
padding: 40px;
}
h1 { margin-bottom: 10px; }
p { margin-bottom: 30px; color: #aaa; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
max-width: 1000px;
}
.card {
background: #16213e;
border: 2px solid #0f3460;
border-radius: 12px;
padding: 24px 16px;
text-align: center;
cursor: pointer;
transition: background 0.05s, border-color 0.05s, transform 0.05s, box-shadow 0.05s;
user-select: none;
}
/* Hover effect: quick background + border + shadow change */
.card:hover {
background: #e94560;
border-color: #e94560;
transform: scale(1.05);
box-shadow: 0 0 20px rgba(233, 69, 96, 0.5);
}
.card:active {
transform: scale(0.98);
}
.card .icon {
font-size: 32px;
margin-bottom: 8px;
}
.card .label {
font-size: 14px;
font-weight: 600;
}
#fps-display {
position: fixed;
top: 12px;
right: 12px;
background: rgba(0,0,0,0.7);
color: #0f0;
font-family: monospace;
font-size: 18px;
padding: 6px 12px;
border-radius: 6px;
z-index: 999;
}
#instructions {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: #ff0;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 999;
}
</style>
</head>
<body>
<div id="fps-display">FPS: --</div>
<div id="instructions">Move mouse rapidly across the cards — watch hover highlight latency</div>
<h1>🖱️ Hover Responsiveness Test</h1>
<p>Each card has a <code>transition: 0.05s</code> on background, border, transform, and box-shadow.
Compare how quickly the hover highlight appears vs Brave/Chrome.</p>
<div class="grid" id="grid"></div>
<script>
// Generate 60 cards
var grid = document.getElementById('grid');
var icons = ['🌟','🔥','💎','⚡','🎯','🚀','🌈','🍀','🎨','🦋','🌺','⭐','💫','✨','🎪','🏆','🥇','💡','🔮','🎭'];
for (var i = 0; i < 60; i++) {
var card = document.createElement('div');
card.className = 'card';
card.innerHTML = '<div class="icon">' + icons[i % icons.length] + '</div><div class="label">Card ' + (i+1) + '</div>';
grid.appendChild(card);
}
// Simple FPS counter
var fpsEl = document.getElementById('fps-display');
var frameCount = 0;
var lastFpsTime = performance.now();
function rafLoop() {
frameCount++;
var now = performance.now();
if (now - lastFpsTime >= 1000) {
fpsEl.textContent = 'FPS: ' + frameCount;
frameCount = 0;
lastFpsTime = now;
}
requestAnimationFrame(rafLoop);
}
requestAnimationFrame(rafLoop);
</script>
</body>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SharedWorker importScripts Test</title>
<style>
body { font-family: sans-serif; background: #1a1a2e; color: #eee; padding: 40px; }
h1 { margin-bottom: 10px; }
.pass { color: #0f0; }
.fail { color: #f00; }
.info { color: #ff0; }
#output { background: #16213e; padding: 16px; border-radius: 8px; margin-top: 20px; font-family: monospace; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>🔄 SharedWorker importScripts Test</h1>
<p>Tests whether <code>importScripts()</code> works inside a SharedWorker loaded from <code>local://</code>.</p>
<div id="output">Running tests...</div>
<script>
var output = document.getElementById('output');
function log(msg, cls) {
output.innerHTML += '<span class="' + (cls || '') + '">' + msg + '</span>\n';
}
log('=== importScripts Test ===\n', 'info');
try {
var worker = new SharedWorker('shared-worker-import-test.js');
log('✓ SharedWorker constructor succeeded', 'pass');
worker.port.onmessage = function(e) {
log('✓ Received: ' + e.data, 'pass');
};
worker.port.onerror = function(e) {
log('✗ Worker error: ' + (e.message || 'unknown'), 'fail');
};
worker.port.postMessage('ping');
setTimeout(function() {
log('\n--- Done ---', 'info');
}, 3000);
} catch (e) {
log('✗ SharedWorker constructor threw: ' + e.message, 'fail');
}
</script>
</body>
</html>
@@ -0,0 +1,31 @@
/**
* SharedWorker test for importScripts() on local://.
* Tests whether importScripts() can load scripts from the local:// scheme.
*/
console.log('[Import-Test] Worker script executing');
console.log('[Import-Test] self.location.href:', self.location.href);
console.log('[Import-Test] self.location.origin:', self.location.origin);
// Try importScripts with a relative path
try {
importScripts('./shared-worker-test.js');
console.log('[Import-Test] importScripts succeeded');
} catch (e) {
console.log('[Import-Test] importScripts FAILED:', e.message);
}
self.onconnect = function(e) {
var port = e.ports[0];
port.postMessage('worker_ready');
port.onmessage = function(e) {
if (e.data === 'ping') {
port.postMessage('pong');
} else if (e.data === 'get_info') {
port.postMessage(JSON.stringify({
href: self.location.href,
origin: self.location.origin,
protocol: self.location.protocol
}));
}
};
};
+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SharedWorker Test</title>
<style>
body { font-family: sans-serif; background: #1a1a2e; color: #eee; padding: 40px; }
h1 { margin-bottom: 10px; }
.pass { color: #0f0; }
.fail { color: #f00; }
.info { color: #ff0; }
#output { background: #16213e; padding: 16px; border-radius: 8px; margin-top: 20px; font-family: monospace; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>🔄 SharedWorker Test</h1>
<p>Tests whether <code>SharedWorker</code> works when loaded from a <code>local://</code> URI.</p>
<div id="output">Running tests...</div>
<script>
var output = document.getElementById('output');
function log(msg, cls) {
output.innerHTML += '<span class="' + (cls || '') + '">' + msg + '</span>\n';
}
log('=== SharedWorker Test ===\n', 'info');
// Test 1: Is SharedWorker supported?
if (typeof SharedWorker !== 'undefined') {
log('✓ SharedWorker is supported by this browser', 'pass');
} else {
log('✗ SharedWorker is NOT supported by this browser', 'fail');
}
// Test 2: Try to create a SharedWorker
try {
var worker = new SharedWorker('shared-worker-test.js');
log('✓ SharedWorker constructor succeeded', 'pass');
worker.port.onmessage = function(e) {
log('✓ Received message from worker: ' + e.data, 'pass');
};
worker.port.onerror = function(e) {
log('✗ Worker error: ' + (e.message || 'unknown'), 'fail');
};
// Send a ping
worker.port.postMessage('ping');
// Timeout after 3 seconds
setTimeout(function() {
log('\n--- Timeout reached ---', 'info');
log('If you see "Received message from worker" above, SharedWorker works.', 'info');
log('If not, SharedWorker failed to load or communicate.', 'info');
}, 3000);
} catch (e) {
log('✗ SharedWorker constructor threw: ' + e.message, 'fail');
}
// Test 3: What is our current origin?
log('\n=== Origin Info ===', 'info');
log('location.href: ' + location.href, 'info');
log('location.origin: ' + (location.origin || 'null'), 'info');
log('location.protocol: ' + location.protocol, 'info');
</script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
/**
* Minimal SharedWorker test script.
* Responds to 'ping' with 'pong' and reports its own URL and origin.
*/
console.log('[SharedWorker-Test] Worker script executing...');
console.log('[SharedWorker-Test] self.location.href:', self.location.href);
console.log('[SharedWorker-Test] self.location.origin:', self.location.origin);
console.log('[SharedWorker-Test] self.location.protocol:', self.location.protocol);
self.onconnect = function(e) {
var port = e.ports[0];
console.log('[SharedWorker-Test] Connected from port');
port.postMessage('worker_ready');
port.onmessage = function(e) {
console.log('[SharedWorker-Test] Received:', e.data);
if (e.data === 'ping') {
port.postMessage('pong');
} else if (e.data === 'get_info') {
port.postMessage(JSON.stringify({
href: self.location.href,
origin: self.location.origin,
protocol: self.location.protocol
}));
}
};
};