v0.0.66 - Release: local:// URI scheme for full localStorage/IndexedDB on local files, Security Restrictions Overridden section in README
This commit is contained in:
@@ -21,7 +21,7 @@ NOSTR_LIB = ./nostr_core_lib/libnostr_core_x64.a
|
|||||||
NOSTR_DEPS = -lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm
|
NOSTR_DEPS = -lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm
|
||||||
|
|
||||||
BIN := sovereign_browser
|
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 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)
|
WEB_FILES := $(shell find www -type f \( -name '*.html' -o -name '*.css' -o -name '*.js' \) 2>/dev/null)
|
||||||
|
|||||||
@@ -96,6 +96,77 @@ Working browser with a broad feature set:
|
|||||||
See [`docs/webkit-poc-findings.md`](docs/webkit-poc-findings.md) for the
|
See [`docs/webkit-poc-findings.md`](docs/webkit-poc-findings.md) for the
|
||||||
friction report from the POC phase.
|
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
|
## Install
|
||||||
|
|
||||||
One-command install on Debian 13 (trixie) or similar (x86_64, WebKitGTK 4.1):
|
One-command install on Debian 13 (trixie) or similar (x86_64, WebKitGTK 4.1):
|
||||||
|
|||||||
@@ -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 |
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* 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 "application/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 = uri + 8;
|
||||||
|
if (path == NULL || path[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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");
|
||||||
|
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);
|
||||||
|
|
||||||
|
/* 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");
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
#include "nostr_bridge.h"
|
#include "nostr_bridge.h"
|
||||||
#include "nostr_scheme.h"
|
#include "nostr_scheme.h"
|
||||||
#include "tor_scheme.h"
|
#include "tor_scheme.h"
|
||||||
|
#include "local_scheme.h"
|
||||||
#include "nostr_inject.h"
|
#include "nostr_inject.h"
|
||||||
#include "history.h"
|
#include "history.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
@@ -935,6 +936,7 @@ static WebKitWebContext *build_context_for_current_user(void) {
|
|||||||
* on every fresh context. */
|
* on every fresh context. */
|
||||||
nostr_scheme_register(web_ctx);
|
nostr_scheme_register(web_ctx);
|
||||||
tor_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
|
/* Point tab_manager at the new context so subsequent new tabs (and
|
||||||
* the sidebar webview) are created from it. */
|
* the sidebar webview) are created from it. */
|
||||||
|
|||||||
@@ -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
|
/* NIP-21 links use nostr:entity rather than nostr://entity. Normalize
|
||||||
* either form so WebKit consistently invokes our registered handler. */
|
* either form so WebKit consistently invokes our registered handler. */
|
||||||
if (uri && strncmp(uri, "nostr:", 6) == 0 &&
|
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);
|
char *url = normalize_url(text);
|
||||||
if (url != NULL) {
|
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);
|
webkit_web_view_load_uri(tab->webview, url);
|
||||||
g_free(url);
|
g_free(url);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -11,9 +11,9 @@
|
|||||||
#ifndef SOVEREIGN_BROWSER_VERSION_H
|
#ifndef SOVEREIGN_BROWSER_VERSION_H
|
||||||
#define SOVEREIGN_BROWSER_VERSION_H
|
#define SOVEREIGN_BROWSER_VERSION_H
|
||||||
|
|
||||||
#define SB_VERSION "v0.0.65"
|
#define SB_VERSION "v0.0.66"
|
||||||
#define SB_VERSION_MAJOR 0
|
#define SB_VERSION_MAJOR 0
|
||||||
#define SB_VERSION_MINOR 0
|
#define SB_VERSION_MINOR 0
|
||||||
#define SB_VERSION_PATCH 65
|
#define SB_VERSION_PATCH 66
|
||||||
|
|
||||||
#endif /* SOVEREIGN_BROWSER_VERSION_H */
|
#endif /* SOVEREIGN_BROWSER_VERSION_H */
|
||||||
|
|||||||
Reference in New Issue
Block a user