v0.0.35 - Selective Tor onion routing (tor:// scheme) + install.sh

This commit is contained in:
Laan Tungir
2026-07-17 11:30:28 -04:00
parent 072915ef51
commit 6c1d309aac
14 changed files with 1337 additions and 43 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/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 := 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
# 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)
+1 -1
View File
@@ -1 +1 @@
0.0.34
0.0.35
Executable
+462
View File
@@ -0,0 +1,462 @@
#!/bin/bash
set -euo pipefail
# install.sh — curl | bash installer for sovereign_browser
#
# Installs:
# * apt runtime deps (WebKitGTK 4.1, libsoup-3, tor, build toolchain)
# * FIPS (built from source from https://github.com/jmcorgan/fips) with CAP_NET_ADMIN
# * sovereign_browser prebuilt binary from Gitea releases (source-build fallback)
# * a `sovereign-browser` wrapper that detaches the GUI
#
# Tor and FIPS are installed by default — they are NOT optional.
#
# Usage:
# curl -fsSL <url> | bash
# curl -fsSL <url> | bash -s -- --build-from-source
# ./install.sh [options]
# --- Config --------------------------------------------------------------
GITEA_API="https://git.laantungir.net/api/v1/repos/laantungir/sovereign_browser"
GITEA_DOWNLOAD_BASE="https://git.laantungir.net/laantungir/sovereign_browser/releases/download"
FIPS_REPO="https://github.com/jmcorgan/fips.git"
SB_REPO="https://git.laantungir.net/laantungir/sovereign_browser.git"
INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local}"
BIN_NAME="sovereign_browser"
# Runtime apt packages (non-dev). git/build-essential/pkg-config are included
# because they're needed for the FIPS source build and the source-build fallback.
APT_PACKAGES=(
libwebkit2gtk-4.1-0 libsoup-3.0-0 libqrencode0 libsqlite3-0
libsecp256k1-1 libssl3 libcurl4 zlib1g
tor git build-essential pkg-config
)
# --- Output helpers ------------------------------------------------------
if [[ -t 2 && -z "${NO_COLOR:-}" ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' NC=''
fi
print_info() { echo -e "${BLUE}[INFO]${NC} $*" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $*" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $*" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die() { print_error "$*"; exit 1; }
# --- Privilege helper ----------------------------------------------------
# Returns "sudo" if the target path is not user-writable, else "".
sudo_for() {
local target="$1"
if [[ -w "$target" ]]; then
echo ""
else
command -v sudo >/dev/null 2>&1 || die "sudo is required to write to $target but is not available."
echo "sudo"
fi
}
# --- Usage ---------------------------------------------------------------
show_usage() {
cat <<'EOF'
sovereign_browser installer
Usage: curl -fsSL <url> | bash
or: curl -fsSL <url> | bash -s -- --build-from-source
or: ./install.sh [options]
Options:
--build-from-source Build from source instead of downloading prebuilt binary
--prefix <dir> Install prefix (default: /usr/local)
--yes Skip confirmation prompts
-h, --help Show this help
EOF
}
# --- Args ----------------------------------------------------------------
BUILD_FROM_SOURCE=false
ASSUME_YES=false
while [[ $# -gt 0 ]]; do
case "$1" in
--build-from-source) BUILD_FROM_SOURCE=true; shift ;;
--prefix) INSTALL_PREFIX="${2:-}"; shift 2 ;;
--yes|-y) ASSUME_YES=true; shift ;;
-h|--help) show_usage; exit 0 ;;
*) die "Unknown option: $1 (try --help)" ;;
esac
done
# --- Platform detection --------------------------------------------------
detect_platform() {
print_info "Detecting platform..."
local os arch
os="$(uname -s)"
arch="$(uname -m)"
[[ "$os" == "Linux" ]] || die "This installer only supports Linux (got: $os)."
if [[ "$arch" != "x86_64" ]]; then
if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
die "arm64 is not supported for the prebuilt binary. Re-run with --build-from-source (arm64 source builds are untested)."
fi
die "Unsupported architecture: $arch (x86_64 only)."
fi
local distro_id="" version_id=""
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
distro_id="${ID:-}"
version_id="${VERSION_ID:-}"
fi
local distro_ok=false
if [[ "$distro_id" == "debian" ]]; then
if [[ -n "$version_id" ]] && awk -v v="$version_id" 'BEGIN{exit !(v+0 >= 13)}'; then
distro_ok=true
fi
elif [[ "$distro_id" == "ubuntu" ]]; then
if [[ -n "$version_id" ]] && awk -v v="$version_id" 'BEGIN{exit !(v+0 >= 24.04)}'; then
distro_ok=true
fi
fi
if $distro_ok; then
print_info "Distro: $distro_id $version_id (supported)."
else
print_warning "Distro '$distro_id $version_id' is not explicitly supported."
print_warning "WebKitGTK 4.1 runtime libs are required. If apt cannot find libwebkit2gtk-4.1-0, use Debian 13 (trixie)+ or Ubuntu 24.04+."
fi
}
# --- apt dependencies ----------------------------------------------------
install_deps() {
print_info "Installing apt dependencies (runtime libs, tor, build toolchain)..."
if ! command -v sudo >/dev/null 2>&1 && [[ $EUID -ne 0 ]]; then
die "sudo is required for apt-get but is not available (not running as root)."
fi
local SUDO=""
[[ $EUID -ne 0 ]] && SUDO="sudo"
$SUDO apt-get update
if ! $SUDO apt-get install -y "${APT_PACKAGES[@]}"; then
print_error "apt-get install failed. One or more packages could not be found."
print_error "This usually means your distro is too old and lacks WebKitGTK 4.1."
print_error "Use Debian 13 (trixie) or Ubuntu 24.04+, then re-run this installer."
exit 1
fi
print_success "Dependencies installed."
}
# --- FIPS ----------------------------------------------------------------
install_fips() {
print_info "Building and installing FIPS from $FIPS_REPO (required, not optional)..."
local tmp
tmp="$(mktemp -d)"
# Register cleanup for this temp dir (in addition to the global trap).
trap "rm -rf '$tmp' 2>/dev/null || true" RETURN
if ! git clone --depth 1 "$FIPS_REPO" "$tmp/fips"; then
die "Failed to clone FIPS repo: $FIPS_REPO"
fi
local fips_dir="$tmp/fips"
local built=false
if [[ -x "$fips_dir/build.sh" ]]; then
print_info "FIPS: running ./build.sh"
if ( cd "$fips_dir" && ./build.sh ); then built=true; fi
fi
if ! $built && [[ -f "$fips_dir/Makefile" ]]; then
print_info "FIPS: running make"
if ( cd "$fips_dir" && make ); then built=true; fi
fi
if ! $built && [[ -f "$fips_dir/CMakeLists.txt" ]]; then
print_info "FIPS: running cmake"
if ( cd "$fips_dir" && cmake -B build && cmake --build build ); then built=true; fi
fi
if ! $built; then
die "Could not build FIPS (no build.sh, Makefile, or CMakeLists.txt succeeded). FIPS is required."
fi
# Locate the built fips binary.
local fips_bin=""
for cand in "$fips_dir/fips" "$fips_dir/build/fips" "$fips_dir/build/src/fips"; do
if [[ -x "$cand" ]] && file "$cand" 2>/dev/null | grep -qi ELF; then
fips_bin="$cand"
break
fi
done
if [[ -z "$fips_bin" ]]; then
# Last resort: search for any executable named fips.
fips_bin="$(find "$fips_dir" -type f -name fips -executable 2>/dev/null | head -1 || true)"
fi
[[ -n "$fips_bin" ]] || die "FIPS build succeeded but no 'fips' binary was found."
local prefix_bin="$INSTALL_PREFIX/bin"
local SUDO
SUDO="$(sudo_for "$INSTALL_PREFIX")"
$SUDO mkdir -p "$prefix_bin"
$SUDO cp -f "$fips_bin" "$prefix_bin/fips"
$SUDO chmod +x "$prefix_bin/fips"
print_success "FIPS binary installed to $prefix_bin/fips"
# setcap for CAP_NET_ADMIN (needed to create a TUN interface in managed mode).
local setcap_cmd=""
if command -v setcap >/dev/null 2>&1; then
setcap_cmd="setcap"
elif command -v sudo >/dev/null 2>&1 && $SUDO command -v setcap >/dev/null 2>&1; then
setcap_cmd="$SUDO setcap"
fi
if [[ -n "$setcap_cmd" ]]; then
if $setcap_cmd cap_net_admin+ep "$prefix_bin/fips" 2>/dev/null; then
print_success "FIPS granted CAP_NET_ADMIN."
else
# setcap exists but failed — try with sudo explicitly.
if $SUDO setcap cap_net_admin+ep "$prefix_bin/fips" 2>/dev/null; then
print_success "FIPS granted CAP_NET_ADMIN."
else
print_warning "setcap failed. FIPS managed mode (TUN) won't work without CAP_NET_ADMIN."
print_warning "Install 'libcap2-bin' and re-run, or run a system FIPS instance and set fips_mode=attach."
fi
fi
else
print_warning "'setcap' not found (libcap2-bin not installed)."
print_warning "FIPS managed mode won't work without CAP_NET_ADMIN."
print_warning "Install libcap2-bin (apt-get install -y libcap2-bin) and re-run, or run a system FIPS instance and set fips_mode=attach."
fi
}
# --- Prebuilt binary download -------------------------------------------
download_binary() {
# Outputs the downloaded binary path on stdout. Returns non-zero on failure
# so the caller can fall back to a source build.
print_info "Querying Gitea for latest release..."
local api_json tag url out
if ! api_json="$(curl -fsSL "$GITEA_API/releases/latest" 2>/dev/null)"; then
print_warning "Could not reach Gitea releases API (no release yet, or network issue)."
return 1
fi
tag="$(printf '%s' "$api_json" | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')"
if [[ -z "$tag" ]]; then
print_warning "Could not parse tag_name from Gitea release JSON."
return 1
fi
url="$GITEA_DOWNLOAD_BASE/$tag/$BIN_NAME"
out="$(mktemp -t sovereign_browser_download.XXXXXX)"
print_info "Downloading prebuilt binary: $url"
if ! curl -fsSL -o "$out" "$url"; then
rm -f "$out" 2>/dev/null || true
print_warning "Download failed for $url"
return 1
fi
if ! file "$out" 2>/dev/null | grep -qi ELF; then
rm -f "$out" 2>/dev/null || true
print_warning "Downloaded file is not an ELF binary."
return 1
fi
printf '%s\n' "$out"
print_success "Downloaded sovereign_browser $tag."
}
# --- Source build fallback -----------------------------------------------
build_from_source() {
print_info "Building sovereign_browser from source ($SB_REPO)..."
local tmp
tmp="$(mktemp -d)"
trap "rm -rf '$tmp' 2>/dev/null || true" RETURN
if ! git clone --depth 1 "$SB_REPO" "$tmp/sovereign_browser"; then
die "Failed to clone sovereign_browser repo: $SB_REPO"
fi
if ! ( cd "$tmp/sovereign_browser" && make ); then
die "Source build failed (make). Ensure build deps are installed."
fi
[[ -x "$tmp/sovereign_browser/$BIN_NAME" ]] || die "Build finished but ./$BIN_NAME was not produced."
local out
out="$(mktemp -t sovereign_browser_built.XXXXXX)"
cp -f "$tmp/sovereign_browser/$BIN_NAME" "$out"
chmod +x "$out"
printf '%s\n' "$out"
print_success "Built sovereign_browser from source."
}
# --- Install binary + wrapper -------------------------------------------
install_binary() {
local src="$1"
local prefix_bin="$INSTALL_PREFIX/bin"
local SUDO
SUDO="$(sudo_for "$INSTALL_PREFIX")"
print_info "Installing binary to $prefix_bin/$BIN_NAME"
$SUDO mkdir -p "$prefix_bin"
$SUDO cp -f "$src" "$prefix_bin/$BIN_NAME"
$SUDO chmod +x "$prefix_bin/$BIN_NAME"
# Write the kebab-case wrapper that detaches the GUI.
local wrapper="$prefix_bin/sovereign-browser"
local real_bin="$prefix_bin/$BIN_NAME"
print_info "Installing wrapper to $wrapper"
# Write to a temp file then move with sudo if needed.
local wtmp
wtmp="$(mktemp -t sovereign-browser-wrapper.XXXXXX)"
cat > "$wtmp" <<WRAPPER_EOF
#!/bin/bash
# sovereign-browser — user-facing wrapper that detaches the GUI.
# Mirrors a subset of browser.sh: start | stop | restart | status | log
set -euo pipefail
SB_BIN="$real_bin"
SB_DATA_DIR="\${HOME}/.sovereign_browser"
SB_PID_FILE="\${SB_DATA_DIR}/browser.pid"
SB_LOG_FILE="\${SB_DATA_DIR}/browser.log"
sb_start() {
mkdir -p "\$SB_DATA_DIR"
# If already running, just print status.
if [[ -f "\$SB_PID_FILE" ]] && kill -0 "\$(cat "\$SB_PID_FILE" 2>/dev/null)" 2>/dev/null; then
echo "sovereign_browser already running (PID \$(cat "\$SB_PID_FILE"))" >&2
return 0
fi
nohup "\$SB_BIN" "\$@" > "\$SB_LOG_FILE" 2>&1 &
local pid=\$!
echo "\$pid" > "\$SB_PID_FILE"
disown "\$pid" 2>/dev/null || true
echo "sovereign_browser started (PID \$pid)" >&2
}
sb_stop() {
if [[ ! -f "\$SB_PID_FILE" ]]; then
echo "sovereign_browser is not running (no PID file)" >&2
return 0
fi
local pid
pid="\$(cat "\$SB_PID_FILE" 2>/dev/null || true)"
if [[ -n "\$pid" ]] && kill -0 "\$pid" 2>/dev/null; then
kill "\$pid" 2>/dev/null || true
echo "sovereign_browser stopped (PID \$pid)" >&2
else
echo "sovereign_browser was not running (stale PID file)" >&2
fi
rm -f "\$SB_PID_FILE" 2>/dev/null || true
}
sb_status() {
if [[ -f "\$SB_PID_FILE" ]] && kill -0 "\$(cat "\$SB_PID_FILE" 2>/dev/null)" 2>/dev/null; then
echo "sovereign_browser is running (PID \$(cat "\$SB_PID_FILE"))" >&2
return 0
else
echo "sovereign_browser is not running" >&2
return 1
fi
}
sb_log() {
tail -n 50 "\$SB_LOG_FILE" 2>/dev/null || echo "(no log file yet)" >&2
}
case "\${1:-start}" in
start) shift 2>/dev/null || true; sb_start "\$@" ;;
stop) sb_stop ;;
restart) sb_stop; shift 2>/dev/null || true; sb_start "\$@" ;;
status) sb_status ;;
log) sb_log ;;
*) sb_start "\$@" ;; # default: treat all args as browser args
esac
WRAPPER_EOF
$SUDO mv -f "$wtmp" "$wrapper"
$SUDO chmod +x "$wrapper"
print_success "Wrapper installed: $wrapper"
if [[ ":${PATH}:" != *":$prefix_bin:"* ]]; then
print_warning "$prefix_bin is not in your PATH. Add it: export PATH=\"$prefix_bin:\$PATH\""
fi
}
# --- Main ----------------------------------------------------------------
main() {
detect_platform
# Summary + confirmation
local method="prebuilt binary from Gitea"
$BUILD_FROM_SOURCE && method="source build from $SB_REPO"
cat >&2 <<EOF
${BLUE}[INFO]${NC} sovereign_browser will be installed:
${BLUE}[INFO]${NC} Install prefix : $INSTALL_PREFIX
${BLUE}[INFO]${NC} Binary method : $method
${BLUE}[INFO]${NC} FIPS : built from source ($FIPS_REPO), CAP_NET_ADMIN set
${BLUE}[INFO]${NC} Tor : apt package 'tor'
${BLUE}[INFO]${NC} apt deps : ${APT_PACKAGES[*]}
EOF
if ! $ASSUME_YES; then
echo -e "${YELLOW}[WARNING]${NC} This will run apt-get install (sudo) and clone/build FIPS. Continue? [y/N] " >&2
read -r reply || reply=""
reply="${reply:-n}"
if [[ ! "${reply,,}" =~ ^y(es)?$ ]]; then
die "Aborted by user."
fi
fi
install_deps
install_fips
local bin_path=""
if $BUILD_FROM_SOURCE; then
bin_path="$(build_from_source)"
else
if ! bin_path="$(download_binary)"; then
print_warning "Prebuilt binary unavailable — falling back to source build."
bin_path="$(build_from_source)"
fi
fi
install_binary "$bin_path"
rm -f "$bin_path" 2>/dev/null || true
print_success "sovereign_browser installed."
cat >&2 <<EOF
${GREEN}[SUCCESS]${NC} Next steps:
Run: sovereign-browser start --login-method generate
Or: sovereign-browser start --login-method generate --url https://example.com
Data dir: ~/.sovereign_browser/
Logs: sovereign-browser log
Stop: sovereign-browser stop
Status: sovereign-browser status
EOF
}
main "$@"
+293
View File
@@ -0,0 +1,293 @@
# Selective Tor Routing — .onion Only
## Goal
sovereign_browser should make all address types "just work":
- `http://` / `https://` → direct (clearnet)
- `file://` → direct (local)
- `.onion` → through Tor SOCKS proxy
- `.fips` → through FIPS TUN interface
- `nostr://` → direct to relays (or through Tor if relay is `.onion`)
Tor and FIPS are **enabled by default**. Tor provides a SOCKS proxy that
is used **only** for `.onion` addresses. Regular internet traffic is
never routed through Tor.
This is NOT Tor Browser. It's a browser where `.onion` addresses just
work because Tor is running in the background.
---
## Current State
The current implementation (in `src/net_services.c`) sets WebKit's proxy
to route **all** HTTP/HTTPS through Tor when Tor is enabled (fail-closed
mode). This is the wrong default for sovereign_browser's vision — it
would break clearnet access and isn't what we want.
---
## Design
### Traffic Routing
| URL pattern | Route | How |
|-------------|-------|-----|
| `http://example.com` | Direct | WebKit default networking, no proxy |
| `https://example.com` | Direct | WebKit default networking, no proxy |
| `file:///path` | Direct | Local file access |
| `http://something.onion` | Tor SOCKS | Custom scheme handler |
| `https://something.onion` | Tor SOCKS | Custom scheme handler |
| `http://npub1abc.fips/` | FIPS TUN | DNS resolves to fd00::/8, TUN routes |
| `nostr://npub1...` | Direct to relays | nostr:// scheme handler |
| `sovereign://...` | Direct (internal) | sovereign:// scheme handler |
### WebKit Proxy Configuration
**Always `WEBKIT_NETWORK_PROXY_MODE_NO_PROXY`.** WebKit never uses a
proxy for its default network stack. All proxying is done via custom
URI scheme handlers.
This is a change from the current code, which sets `CUSTOM` mode with
Tor's SOCKS endpoint. We remove that entirely.
### .onion Handling
#### Approach: Intercept + custom scheme
1. **`on_decide_policy` in `tab_manager.c`** intercepts navigation to
`.onion` URLs (both `http://*.onion` and `https://*.onion`).
2. **Rewrite to `tor://` scheme:** The intercepted URL is rewritten:
- `http://abc.onion/path?q=1``tor://abc.onion/path?q=1`
- `https://abc.onion/path``tor://abc.onion/path`
- The scheme handler knows to use Tor's SOCKS proxy.
3. **`tor://` URI scheme handler** (new `src/tor_scheme.c`):
- Registered via `webkit_web_context_register_uri_scheme(ctx, "tor", ...)`
- Parses the `.onion` hostname, port, path, query from the URI
- Fetches the content via Tor's SOCKS proxy using libcurl with
`CURLOPT_PROXY` set to Tor's SOCKS endpoint
- Returns the response (headers + body) to WebKit via
`webkit_uri_scheme_request_finish()`
- Supports both HTTP and HTTPS over .onion (Tor handles the TLS)
4. **URL bar:** When the user types `abc.onion` in the URL bar,
`normalize_url()` should recognize `.onion` and prepend `http://`
(or `https://`), then the `on_decide_policy` interception handles
the rest.
5. **Links on pages:** When a web page has `<a href="http://abc.onion">`,
clicking it triggers `on_decide_policy`, which intercepts and
rewrites to `tor://abc.onion`.
### Why a custom scheme handler instead of WebKit proxy?
WebKitGTK's proxy API (`WebKitNetworkProxySettings`) only supports
"proxy everything, ignore these hosts" — not "proxy only these hosts."
Setting a proxy for everything would route clearnet through Tor, which
we don't want. A custom scheme handler gives us precise control: only
`.onion` traffic goes through Tor.
The downside is that the custom scheme handler must handle HTTP
semantics (GET, POST, headers, cookies, redirects) itself. However,
libcurl handles all of this — we just pass the request to curl with
the SOCKS proxy configured and return the response.
### libcurl for .onion fetches
The `tor://` scheme handler uses libcurl (already a dependency — used
in `nostr_scheme.c` for NIP-11 fetches) with:
```c
curl_easy_setopt(curl, CURLOPT_URL, "http://abc.onion/path?q=1");
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://127.0.0.1:9050");
curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
```
`socks5h://` means curl sends the hostname to the SOCKS proxy (Tor
resolves it — no DNS leak). `CURLPROXY_SOCKS5_HOSTNAME` ensures the
.onion hostname is resolved by Tor, not locally.
For HTTPS over .onion, curl handles TLS normally — Tor just provides
the transport.
### Request handling
The scheme handler must handle:
1. **GET requests** — fetch URL via curl, return body + content type
2. **POST requests** — WebKit custom scheme handlers don't directly
expose POST bodies. This is a known WebKitGTK limitation. For Phase 1,
POST to .onion may not work (GET is sufficient for most .onion sites
and Nostr relay web interfaces). A future enhancement could use a
WebExtension for POST body access.
3. **Redirects** — curl follows redirects by default
(`CURLOPT_FOLLOWLOCATION`). The final URL is returned.
4. **Headers** — curl captures response headers. We pass through
Content-Type so WebKit renders correctly (HTML, JSON, images, etc.).
5. **Cookies** — .onion cookies are handled by curl's cookie engine
(separate from WebKit's cookie jar). For Phase 1, this is acceptable.
Future: share cookies between WebKit and the Tor handler.
6. **Timeouts** — 30 second timeout for .onion (Tor can be slow).
### Async handling
Like `nostr_scheme.c`, the `tor://` handler uses `GTask` to run the
curl fetch on a worker thread and finish the WebKit request on the main
context. This prevents blocking the UI.
### Tor SOCKS endpoint
The Tor SOCKS endpoint is determined by the existing Tor service
management in `net_services.c` / `tor_control.c`:
- **Attached Tor:** use the discovered SOCKS endpoint
(e.g., `socks5://127.0.0.1:9050`)
- **Managed Tor:** use the private Unix socket
(e.g., `socks5://unix:/home/user/.sovereign_browser/tor/socks.sock`)
The `tor://` scheme handler queries `net_service_get_status(NET_SERVICE_TOR)`
to get the current SOCKS endpoint. If Tor is not ready, it returns an
error page.
### FIPS .onion relays
If a Nostr relay URL is `wss://relay.onion`, the C-side relay fetch
code (`relay_fetch.c`) would need to connect through Tor's SOCKS proxy.
This is a future enhancement — the nostr_core_lib relay client would
need SOCKS support. For Phase 1, only WebKit .onion browsing is
supported.
---
## Changes Required
### 1. Change WebKit proxy to always NO_PROXY
In `src/net_services.c`, `net_services_refresh_proxy()`:
- **Remove** the current logic that sets `CUSTOM` proxy mode with Tor's
SOCKS endpoint.
- **Always** set `WEBKIT_NETWORK_PROXY_MODE_NO_PROXY`.
- Tor proxying is now handled entirely by the `tor://` scheme handler,
not by WebKit's proxy settings.
- Keep the function (it's called from various places) but simplify it
to always set NO_PROXY. Or remove the calls and just set NO_PROXY
once at startup.
### 2. Create `src/tor_scheme.c` / `src/tor_scheme.h`
New URI scheme handler for `tor://`:
```c
void tor_scheme_register(WebKitWebContext *ctx);
```
- Registers the `tor` URI scheme
- On request: parse the .onion URL, get Tor SOCKS endpoint from
`net_service_get_status(NET_SERVICE_TOR)`, fetch via libcurl with
SOCKS proxy, return response to WebKit
- Async via `GTask` (same pattern as `nostr_scheme.c`)
- Returns error JSON if Tor is not ready
### 3. Intercept .onion in `on_decide_policy`
In `src/tab_manager.c`, `on_decide_policy()`:
```c
/* Check if the navigation target is a .onion URL */
if (uri && (g_str_has_suffix(host, ".onion"))) {
/* Rewrite to tor:// scheme */
char *tor_url = g_strdup_printf("tor://%s", uri + strlen("http://"));
/* or preserve https:// → tor:// */
webkit_web_view_load_uri(webview, tor_url);
g_free(tor_url);
webkit_policy_decision_ignore(decision);
return TRUE;
}
```
Also intercept in `normalize_url()` so the URL bar shows the original
`http://abc.onion` form (not `tor://abc.onion`).
### 4. Register tor:// scheme in `main.c`
```c
tor_scheme_register(web_ctx);
```
### 5. Enable Tor and FIPS by default
In `src/settings.c`, change defaults:
```c
s->tor_enabled = TRUE; /* was FALSE */
s->fips_enabled = TRUE; /* was FALSE */
```
### 6. Update Makefile
Add `src/tor_scheme.c` to `SRC`.
### 7. Update `net_services_refresh_proxy()`
Simplify to always set NO_PROXY. Remove the fail-closed logic (no
longer needed — clearnet always works, .onion goes through the scheme
handler).
---
## Files
```
src/
├── tor_scheme.c # tor:// URI scheme handler (new)
├── tor_scheme.h # Public API (new)
├── net_services.c # Simplified: always NO_PROXY
├── tab_manager.c # .onion interception in on_decide_policy + normalize_url
├── main.c # Register tor:// scheme
├── settings.c # Tor/FIPS enabled by default
└── Makefile # Add tor_scheme.c
```
---
## Implementation Phases
### Phase 1: .onion browsing via tor:// scheme
1. Create `tor_scheme.c` — curl-based .onion fetcher with SOCKS proxy
2. Register `tor://` scheme in `main.c`
3. Intercept `.onion` in `on_decide_policy` and `normalize_url`
4. Simplify `net_services_refresh_proxy()` to always NO_PROXY
5. Enable Tor and FIPS by default in settings
6. Test: `http://duckduckgogg42tjsool4r5mn3r2onion.onion` loads through Tor
### Phase 2: Polish
1. Handle .onion redirects (curl follows, but URL bar should show final)
2. Error pages for .onion when Tor is not ready
3. Cookie handling for .onion (curl cookie jar)
4. Timeout handling (30s for .onion)
5. Content-type passthrough (HTML, JSON, images, CSS, JS)
### Phase 3: .onion Nostr relays (future)
1. Add SOCKS proxy support to relay fetch code
2. Allow `wss://relay.onion` in relay configuration
3. Route .onion relay connections through Tor
---
## Testing
1. Build and start browser
2. Verify `http://example.com` loads directly (not through Tor)
3. Verify `file:///path` works
4. If Tor binary is available:
- Verify `http://something.onion` loads through Tor
- Verify Tor log shows the .onion connection
5. If Tor binary is not available:
- Verify .onion URLs show "Tor not ready" error
- Verify clearnet still works fine
6. Verify FIPS .fips URLs still work (if FIPS is available)
7. Verify Tor and FIPS are enabled by default on fresh install
+4
View File
@@ -159,6 +159,10 @@ const mcp_tool_def_t tool_defs[] = {
"Reload the current page (bypassing cache).",
"{\"type\":\"object\",\"properties\":{}}"},
{"stop",
"Stop loading the current page. Mirrors the toolbar stop button.",
"{\"type\":\"object\",\"properties\":{}}"},
{"get_url",
"Get the current URL of the active tab.",
"{\"type\":\"object\",\"properties\":{}}"},
+20 -1
View File
@@ -510,7 +510,8 @@ static cJSON *tool_open(cJSON *params) {
typedef enum {
WEBVIEW_ACTION_BACK,
WEBVIEW_ACTION_FORWARD,
WEBVIEW_ACTION_RELOAD
WEBVIEW_ACTION_RELOAD,
WEBVIEW_ACTION_STOP
} webview_action_t;
typedef struct {
@@ -533,6 +534,9 @@ static gboolean webview_action_idle(gpointer user_data) {
case WEBVIEW_ACTION_RELOAD:
webkit_web_view_reload_bypass_cache(ctx->webview);
break;
case WEBVIEW_ACTION_STOP:
webkit_web_view_stop_loading(ctx->webview);
break;
}
}
g_free(ctx);
@@ -572,6 +576,20 @@ static cJSON *tool_reload(cJSON *params) {
return make_success(NULL);
}
/* Stop loading the active tab. Mirrors the toolbar stop button and the
* hamburger menu's Stop action. Useful for testing the per-tab
* reload/stop button state machine. */
static cJSON *tool_stop(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
if (!wv) return make_error("NO_TAB", "No active tab");
webview_action_idle_t *ctx = g_new(webview_action_idle_t, 1);
ctx->webview = wv;
ctx->action = WEBVIEW_ACTION_STOP;
g_idle_add(webview_action_idle, ctx);
return make_success(NULL);
}
static cJSON *tool_get_url(cJSON *params) {
(void)params;
WebKitWebView *wv = get_active_webview();
@@ -4104,6 +4122,7 @@ static tool_entry_t tool_table[] = {
{"back", tool_back, TRUE},
{"forward", tool_forward, TRUE},
{"reload", tool_reload, TRUE},
{"stop", tool_stop, TRUE},
{"get_url", tool_get_url, TRUE},
{"get_title", tool_get_title, TRUE},
+43 -4
View File
@@ -37,6 +37,7 @@
#include "login_dialog.h"
#include "nostr_bridge.h"
#include "nostr_scheme.h"
#include "tor_scheme.h"
#include "nostr_inject.h"
#include "history.h"
#include "settings.h"
@@ -192,10 +193,46 @@ void app_menu_security_strip_proxy(GtkCheckMenuItem *item, gpointer data) {
g_print("[menu] security strip: %s\n", active ? "ON" : "OFF");
}
void app_menu_fips_proxy(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
g_print("[menu] FIPS URI scheme: not yet implemented (roadmap)\n");
void app_menu_network_service_proxy(GtkCheckMenuItem *item, gpointer data) {
if (g_object_get_data(G_OBJECT(item), "network-toggle-sync") != NULL) return;
net_service_type_t type = (net_service_type_t)GPOINTER_TO_INT(data);
if (type != NET_SERVICE_TOR && type != NET_SERVICE_FIPS) {
g_printerr("[menu.network] Invalid network service type %d\n", (int)type);
return;
}
const char *name = type == NET_SERVICE_TOR ? "Tor" : "FIPS";
gboolean enabled = gtk_check_menu_item_get_active(item);
browser_settings_t *settings = settings_get_mutable();
gboolean *setting = type == NET_SERVICE_TOR
? &settings->tor_enabled : &settings->fips_enabled;
*setting = enabled;
settings_save_user();
int rc = enabled ? net_service_enable(type) : net_service_disable(type);
if (enabled && rc != 0) {
const net_service_t *status = net_service_get_status(type);
g_printerr("[menu.network] Failed to enable %s service: %s; reverting preference\n",
name, status && status->error_msg
? status->error_msg : "synchronous startup failure");
*setting = FALSE;
settings_save_user();
g_object_set_data(G_OBJECT(item), "network-toggle-sync",
GINT_TO_POINTER(1));
gtk_check_menu_item_set_active(item, FALSE);
g_object_set_data(G_OBJECT(item), "network-toggle-sync", NULL);
return;
}
if (rc != 0) {
g_printerr("[menu.network] Failed to disable %s service (preference remains disabled)\n",
name);
} else {
g_print("[menu.network] %s service %s; per-user preference saved\n",
name, enabled ? "enabled" : "disabled");
}
}
void app_menu_nostr_sign_proxy(GtkMenuItem *item, gpointer data) {
@@ -852,6 +889,7 @@ int main(int argc, char **argv) {
* which we can't set with the basic finish API). Without these, WebKit
* treats sovereign:// as a simple secure scheme with no CORS checks. */
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "sovereign");
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "tor");
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "file");
webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "file");
@@ -879,6 +917,7 @@ int main(int argc, char **argv) {
/* Register nostr:// entity pages before creating any webviews. */
nostr_scheme_register(web_ctx);
tor_scheme_register(web_ctx);
/* Vertical box: the tab manager's notebook fills the window. */
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+1 -22
View File
@@ -66,31 +66,10 @@ static int ensure_private_dir(const char *path, char *error, size_t error_size)
void net_services_refresh_proxy(void) {
if (!g_initialized) return;
net_service_t *tor = &g_services[NET_SERVICE_TOR];
WebKitWebContext *ctx = webkit_web_context_get_default();
WebKitWebsiteDataManager *dm = webkit_web_context_get_website_data_manager(ctx);
if (!net_service_is_enabled(NET_SERVICE_TOR)) {
webkit_website_data_manager_set_network_proxy_settings(
dm, WEBKIT_NETWORK_PROXY_MODE_NO_PROXY, NULL);
g_print("[net.service.tor] WebKit proxy disabled\n");
return;
}
const char *uri = "socks5://127.0.0.1:1";
if (tor->state == SERVICE_READY && tor->status.tor.socks_endpoint[0])
uri = tor->status.tor.socks_endpoint;
const gchar *ignore_with_fips[] = { "*.fips", NULL };
const gchar *ignore_empty[] = { NULL };
const gchar * const *ignore_hosts = net_service_is_ready(NET_SERVICE_FIPS)
? ignore_with_fips : ignore_empty;
WebKitNetworkProxySettings *proxy =
webkit_network_proxy_settings_new(uri, ignore_hosts);
webkit_website_data_manager_set_network_proxy_settings(
dm, WEBKIT_NETWORK_PROXY_MODE_CUSTOM, proxy);
webkit_network_proxy_settings_free(proxy);
g_print("[net.service.tor] WebKit proxy %s: %s%s\n",
tor->state == SERVICE_READY ? "ready" : "fail-closed", uri,
net_service_is_ready(NET_SERVICE_FIPS) ? " (bypass *.fips)" : "");
dm, WEBKIT_NETWORK_PROXY_MODE_NO_PROXY, NULL);
}
static gboolean pipe_log_cb(GIOChannel *channel, GIOCondition condition, gpointer data) {
+2 -2
View File
@@ -112,7 +112,7 @@ static void settings_set_defaults(browser_settings_t *s) {
s->agent_active_provider_name[0] = '\0';
memset(s->agent_providers, 0, sizeof(s->agent_providers));
s->tor_enabled = FALSE;
s->tor_enabled = TRUE;
snprintf(s->tor_mode, sizeof(s->tor_mode), "auto");
snprintf(s->tor_binary_path, sizeof(s->tor_binary_path), "tor");
s->tor_attach_socks[0] = '\0';
@@ -120,7 +120,7 @@ static void settings_set_defaults(browser_settings_t *s) {
snprintf(s->tor_data_dir, sizeof(s->tor_data_dir),
"~/.sovereign_browser/tor");
s->fips_enabled = FALSE;
s->fips_enabled = TRUE;
snprintf(s->fips_mode, sizeof(s->fips_mode), "auto");
snprintf(s->fips_binary_path, sizeof(s->fips_binary_path), "fips");
s->fips_control_socket[0] = '\0';
+195 -10
View File
@@ -18,6 +18,7 @@
#include "embedded_web_content.h"
#include "agent_snapshot.h" /* agent_js_eval_sync() for clear-and-reload */
#include "db.h"
#include "net_services.h"
#include "agent_chat.h" /* agent_chat_route_input() for ";" URL-bar shortcut */
#include <string.h>
@@ -47,7 +48,7 @@ extern void app_menu_switch_identity_proxy(GtkMenuItem *item, gpointer data);
extern void app_menu_lock_session_proxy(GtkMenuItem *item, gpointer data);
extern void app_menu_logout_proxy(GtkMenuItem *item, gpointer data);
extern void app_menu_security_strip_proxy(GtkCheckMenuItem *item, gpointer data);
extern void app_menu_fips_proxy(GtkMenuItem *item, gpointer data);
extern void app_menu_network_service_proxy(GtkCheckMenuItem *item, gpointer data);
extern void app_menu_nostr_sign_proxy(GtkMenuItem *item, gpointer data);
extern void app_menu_about_proxy(GtkMenuItem *item, gpointer data);
extern void on_menu_settings(GtkMenuItem *item, gpointer data);
@@ -237,6 +238,62 @@ static char *normalize_fips_url(const char *input) {
return result;
}
static gboolean authority_is_onion_host(const char *authority) {
if (authority == NULL || authority[0] == '\0') return FALSE;
char *hostport = g_strdup(authority);
char *host = hostport;
char *at = strrchr(host, '@');
if (at) host = at + 1;
if (host[0] == '[') {
g_free(hostport);
return FALSE;
}
char *colon = strrchr(host, ':');
if (colon && strchr(host, ':') == colon) *colon = '\0';
gboolean is_onion = g_str_has_suffix(host, ".onion");
g_free(hostport);
return is_onion;
}
static gboolean input_is_onion_without_scheme(const char *input) {
if (!input || strstr(input, "://") != NULL || strchr(input, ' ') != NULL)
return FALSE;
const char *end = strpbrk(input, "/?#");
size_t authority_len = end ? (size_t)(end - input) : strlen(input);
if (authority_len == 0) return FALSE;
char *authority = g_strndup(input, authority_len);
gboolean is_onion = authority_is_onion_host(authority);
g_free(authority);
return is_onion;
}
static gboolean uri_is_http_onion(const char *uri, const char **rest_out) {
if (uri == NULL) return FALSE;
const char *rest = NULL;
if (g_str_has_prefix(uri, "http://")) rest = uri + 7;
else if (g_str_has_prefix(uri, "https://")) rest = uri + 8;
else return FALSE;
const char *end = strpbrk(rest, "/?#");
size_t authority_len = end ? (size_t)(end - rest) : strlen(rest);
if (authority_len == 0) return FALSE;
char *authority = g_strndup(rest, authority_len);
gboolean is_onion = authority_is_onion_host(authority);
g_free(authority);
if (is_onion && rest_out) *rest_out = rest;
return is_onion;
}
static char *normalize_url(const char *input) {
if (input == NULL || input[0] == '\0') {
return NULL;
@@ -265,6 +322,9 @@ static char *normalize_url(const char *input) {
if (strncmp(input, "sovereign://", 12) == 0) {
return g_strdup(input);
}
if (input_is_onion_without_scheme(input)) {
return g_strdup_printf("http://%s", input);
}
return g_strdup_printf("https://%s", input);
}
/* Not a URL — treat as a search query. */
@@ -901,6 +961,18 @@ static gboolean on_decide_policy(WebKitWebView *webview,
}
}
/* Route .onion HTTP(S) traffic through tor:// so only onion addresses
* use Tor routing. Phase 1 intentionally maps both input schemes to
* tor:// and the handler currently fetches using http:// over Tor. */
const char *tor_rest = NULL;
if (uri && uri_is_http_onion(uri, &tor_rest)) {
char *tor_url = g_strdup_printf("tor://%s", tor_rest);
webkit_web_view_load_uri(webview, tor_url);
g_free(tor_url);
webkit_policy_decision_ignore(decision);
return TRUE;
}
/* Only intercept link clicks, not form submissions or reloads. */
WebKitNavigationType nav_type =
webkit_navigation_action_get_navigation_type(action);
@@ -1266,11 +1338,62 @@ static void on_forward_clicked(GtkButton *btn, gpointer user_data) {
}
}
/* Refresh button — left-click: normal reload. */
/* Update the per-tab refresh/stop button visual + tooltip to match the
* webview's current loading state. Called from on_load_changed,
* on_load_failed, and on_refresh_clicked so the button always reflects
* the actual WebKit is-loading state for *this* tab only. */
static void tab_refresh_button_update(tab_info_t *tab) {
if (tab == NULL || tab->refresh_btn == NULL || tab->webview == NULL) {
return;
}
gboolean loading = webkit_web_view_is_loading(tab->webview);
if (tab->refresh_shows_stop == loading) return;
tab->refresh_shows_stop = loading;
GtkButton *btn = GTK_BUTTON(tab->refresh_btn);
const gchar *icon_name = loading ? "process-stop-symbolic"
: "view-refresh-symbolic";
gtk_button_set_image(btn,
gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU));
/* Debug log: one line per state transition. G_LOG_LEVEL_INFO keeps it
* quiet in production (g_message would always print). Use g_print
* here for test observability since the rest of the file uses it. */
int index = tab_array_find(tab);
g_print("[tab %d] refresh button -> %s (icon=%s)\n",
index,
loading ? "STOP" : "RELOAD",
icon_name);
if (loading) {
gtk_widget_set_tooltip_text(tab->refresh_btn,
"Stop loading (right-click for hard reload options)");
} else {
gtk_widget_set_tooltip_text(tab->refresh_btn,
"Reload page (right-click for hard reload options)");
}
}
/* Refresh/Stop button — left-click branches on per-tab loading state:
* loading -> webkit_web_view_stop_loading()
* idle -> webkit_web_view_reload()
* The button visual is kept in sync with is_loading via
* tab_refresh_button_update() called from the load-changed/load-failed
* handlers, so this single handler covers both modes. */
static void on_refresh_clicked(GtkButton *btn, gpointer user_data) {
(void)btn;
tab_info_t *tab = (tab_info_t *)user_data;
if (tab && tab->webview) {
if (tab == NULL || tab->webview == NULL) return;
if (webkit_web_view_is_loading(tab->webview)) {
g_print("[tab] stop loading requested\n");
webkit_web_view_stop_loading(tab->webview);
/* WebKit fires load-failed (WEBKIT_LOAD_FAILED) for cancellations
* in most cases, which restores the button. Update defensively
* here too in case no signal arrives (e.g. already-finished load
* races) so the UI never gets stuck in the stop state. */
tab_refresh_button_update(tab);
} else {
webkit_web_view_reload(tab->webview);
}
}
@@ -1345,6 +1468,16 @@ static void on_load_changed(WebKitWebView *webview,
gpointer user_data) {
tab_info_t *tab = (tab_info_t *)user_data;
/* Per-tab reload/stop button: switch to the stop (X) visual as soon
* as a load begins, and back to the reload visual when it ends.
* WEBKIT_LOAD_STARTED is the canonical "loading now" signal; the
* button is also re-synced on FINISHED/FAILED below. This is per-tab
* (each tab has its own refresh_btn) so loading one tab never affects
* another tab's button. */
if (load_event == WEBKIT_LOAD_STARTED) {
tab_refresh_button_update(tab);
}
/* tab is NULL for webviews in auxiliary (new) windows, which don't
* have a tab_info_t. Skip the tab-UI updates (URL bar, favicon, tab
* title) but still record history on load-finished below. */
@@ -1394,6 +1527,9 @@ static void on_load_changed(WebKitWebView *webview,
* the real title when it arrives. */
g_print("[loaded] %s\n", uri ? uri : "(null)");
/* Restore the reload button visual now that loading is done. */
tab_refresh_button_update(tab);
/* 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. */
@@ -1428,6 +1564,12 @@ static gboolean on_load_failed(WebKitWebView *webview,
failing_uri ? failing_uri : "(null)",
error ? error->message : "(unknown)");
/* A failed load (including user-initiated cancellation via
* webkit_web_view_stop_loading()) ends the loading phase, so restore
* the reload button visual. This prevents stale stop state when the
* user clicks the X to cancel a load. */
tab_refresh_button_update(tab);
/* 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. */
@@ -2023,6 +2165,24 @@ static void on_bookmark_clicked(GtkButton *btn, gpointer user_data) {
/* ── Hamburger menu builder ───────────────────────────────────────── */
static void sync_network_menu_toggle(GtkWidget *item, gboolean active) {
g_object_set_data(G_OBJECT(item), "network-toggle-sync", GINT_TO_POINTER(1));
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), active);
g_object_set_data(G_OBJECT(item), "network-toggle-sync", NULL);
}
static void on_hamburger_menu_show(GtkWidget *menu, gpointer data) {
(void)data;
GtkWidget *tor_item = g_object_get_data(G_OBJECT(menu), "tor-toggle");
GtkWidget *fips_item = g_object_get_data(G_OBJECT(menu), "fips-toggle");
const browser_settings_t *settings = settings_get();
if (tor_item)
sync_network_menu_toggle(tor_item, settings->tor_enabled);
if (fips_item)
sync_network_menu_toggle(fips_item, settings->fips_enabled);
}
static GtkWidget *build_hamburger_menu(tab_info_t *tab) {
(void)tab;
GtkWidget *menu = gtk_menu_new();
@@ -2076,7 +2236,30 @@ static GtkWidget *build_hamburger_menu(tab_info_t *tab) {
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
gtk_separator_menu_item_new());
/* Roadmap group. */
/* Networking preferences. These are synchronized again whenever the
* menu opens so toggles in one tab are reflected by every tab. */
const browser_settings_t *settings = settings_get();
GtkWidget *item_tor =
gtk_check_menu_item_new_with_label("Tor-routed transport");
GtkWidget *item_fips =
gtk_check_menu_item_new_with_label("FIPS mesh");
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item_tor),
settings->tor_enabled);
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item_fips),
settings->fips_enabled);
g_signal_connect(item_tor, "toggled",
G_CALLBACK(app_menu_network_service_proxy),
GINT_TO_POINTER(NET_SERVICE_TOR));
g_signal_connect(item_fips, "toggled",
G_CALLBACK(app_menu_network_service_proxy),
GINT_TO_POINTER(NET_SERVICE_FIPS));
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_tor);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_fips);
g_object_set_data(G_OBJECT(menu), "tor-toggle", item_tor);
g_object_set_data(G_OBJECT(menu), "fips-toggle", item_fips);
g_signal_connect(menu, "show", G_CALLBACK(on_hamburger_menu_show), NULL);
/* Security/status group. */
GtkWidget *item_security =
gtk_check_menu_item_new_with_label("Security strip (SOP/CORS/certs)");
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item_security), FALSE);
@@ -2084,13 +2267,9 @@ static GtkWidget *build_hamburger_menu(tab_info_t *tab) {
G_CALLBACK(app_menu_security_strip_proxy), NULL);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_security);
GtkWidget *item_fips = gtk_menu_item_new_with_label("FIPS URI scheme…");
GtkWidget *item_nostr = gtk_menu_item_new_with_label("Nostr signing status");
g_signal_connect(item_fips, "activate",
G_CALLBACK(app_menu_fips_proxy), NULL);
g_signal_connect(item_nostr, "activate",
G_CALLBACK(app_menu_nostr_sign_proxy), NULL);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_fips);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_nostr);
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
@@ -2316,9 +2495,15 @@ static tab_info_t *tab_create(const char *url) {
tab->hamburger = build_hamburger_menu(tab);
gtk_box_pack_start(GTK_BOX(toolbar), tab->hamburger, FALSE, FALSE, 0);
/* Refresh button — left-click reloads, right-click shows a menu
* with hard reload options (bypass cache, clear cookies+reload, etc.). */
/* Refresh/Stop button — left-click reloads (or stops while loading),
* right-click shows a menu with hard reload options (bypass cache,
* clear cookies+reload, etc.). The icon/tooltip are kept in sync with
* the webview's is-loading state by tab_refresh_button_update(), so
* the same button acts as both reload and stop. Stored on
* tab->refresh_btn so the load handlers can update it per-tab. */
GtkWidget *refresh_btn = gtk_button_new();
tab->refresh_btn = refresh_btn;
tab->refresh_shows_stop = FALSE;
gtk_button_set_relief(GTK_BUTTON(refresh_btn), GTK_RELIEF_NONE);
gtk_button_set_image(GTK_BUTTON(refresh_btn),
gtk_image_new_from_icon_name("view-refresh-symbolic",
+2
View File
@@ -28,6 +28,8 @@ typedef struct {
GtkWidget *tab_label; /* composite label widget for the tab strip */
GtkWidget *title_label; /* child of tab_label */
GtkWidget *favicon; /* favicon image in tab_label */
GtkWidget *refresh_btn; /* toolbar reload/stop button (per-tab) */
gboolean refresh_shows_stop; /* cached visual state, avoids redundant updates */
char current_url[TAB_URL_MAX];
char title[TAB_TITLE_MAX];
} tab_info_t;
+291
View File
@@ -0,0 +1,291 @@
/*
* tor_scheme.c — asynchronous Tor-backed WebKitGTK handler for tor://
*/
#include "tor_scheme.h"
#include "net_services.h"
#include <curl/curl.h>
#include <glib.h>
#include <webkit2/webkit2.h>
#include <ctype.h>
#include <string.h>
#define TOR_FETCH_MAX_BYTES (16 * 1024 * 1024)
#define TOR_FETCH_TIMEOUT_SECONDS 30L
#define TOR_CONNECT_TIMEOUT_SECONDS 15L
typedef struct {
WebKitURISchemeRequest *request;
char *target_url;
char *proxy_url;
GString *response_body;
char *content_type;
char *error_html;
} tor_job_t;
typedef struct {
GString *body;
gboolean overflow;
} tor_body_t;
static char *html_escape(const char *input) {
if (!input) return g_strdup("");
return g_markup_escape_text(input, -1);
}
static void respond_html(WebKitURISchemeRequest *request, char *html) {
gsize len = strlen(html);
GInputStream *stream = g_memory_input_stream_new_from_data(html, len, g_free);
webkit_uri_scheme_request_finish(request, stream, len, "text/html");
g_object_unref(stream);
}
static void respond_error_page(WebKitURISchemeRequest *request,
const char *title,
const char *message) {
char *safe_title = html_escape(title ? title : "Error");
char *safe_message = html_escape(message ? message : "Request failed.");
char *html = g_strdup_printf(
"<!doctype html><html><head><meta charset=\"utf-8\">"
"<title>%s</title></head><body>"
"<h1>%s</h1><p>%s</p></body></html>",
safe_title, safe_title, safe_message);
respond_html(request, html);
g_free(safe_title);
g_free(safe_message);
}
static gboolean host_is_onion(const char *host) {
return host && *host && g_str_has_suffix(host, ".onion");
}
static char *proxy_to_socks5h(const char *endpoint) {
if (!endpoint || endpoint[0] == '\0') return NULL;
if (g_str_has_prefix(endpoint, "socks5h://")) return g_strdup(endpoint);
if (!g_str_has_prefix(endpoint, "socks5://")) return NULL;
return g_strdup_printf("socks5h://%s", endpoint + strlen("socks5://"));
}
static char *build_target_http_url(const char *uri, char **error_message) {
GError *error = NULL;
GUri *parsed = g_uri_parse(uri, G_URI_FLAGS_PARSE_RELAXED, &error);
if (!parsed) {
if (error_message)
*error_message = g_strdup(error ? error->message : "Invalid tor:// URI");
g_clear_error(&error);
return NULL;
}
const char *host = g_uri_get_host(parsed);
gint port = g_uri_get_port(parsed);
const char *path = g_uri_get_path(parsed);
const char *query = g_uri_get_query(parsed);
if (!host_is_onion(host)) {
if (error_message) {
*error_message = g_strdup("tor:// only supports .onion hostnames in Phase 1");
}
g_uri_unref(parsed);
return NULL;
}
if (!path || path[0] == '\0') path = "/";
/* Phase 1: always map tor:// to http:// over Tor SOCKS. HTTPS-over-onion
* support can be added in a future phase with an explicit signal. */
char *target = NULL;
if (port > 0) {
target = query
? g_strdup_printf("http://%s:%d%s?%s", host, port, path, query)
: g_strdup_printf("http://%s:%d%s", host, port, path);
} else {
target = query
? g_strdup_printf("http://%s%s?%s", host, path, query)
: g_strdup_printf("http://%s%s", host, path);
}
g_uri_unref(parsed);
return target;
}
static size_t tor_write_bounded(void *data, size_t size, size_t nmemb, void *user_data) {
tor_body_t *body = user_data;
size_t bytes = size * nmemb;
if (body->body->len + bytes > TOR_FETCH_MAX_BYTES) {
body->overflow = TRUE;
return 0;
}
g_string_append_len(body->body, data, bytes);
return bytes;
}
static size_t tor_header_capture(void *data, size_t size, size_t nmemb, void *user_data) {
tor_job_t *job = user_data;
size_t bytes = size * nmemb;
const char *line = (const char *)data;
if (job->content_type != NULL || bytes < strlen("Content-Type:")) return bytes;
if (g_ascii_strncasecmp(line, "Content-Type:", strlen("Content-Type:")) == 0) {
const char *value = line + strlen("Content-Type:");
while (*value == ' ' || *value == '\t') value++;
const char *end = line + bytes;
while (end > value && (end[-1] == '\r' || end[-1] == '\n' ||
end[-1] == ' ' || end[-1] == '\t')) {
end--;
}
if (end > value) job->content_type = g_strndup(value, (gsize)(end - value));
}
return bytes;
}
static gpointer tor_scheme_worker(gpointer data) {
tor_job_t *job = data;
CURL *curl = curl_easy_init();
tor_body_t body = { g_string_new(NULL), FALSE };
if (!curl) {
job->error_html = g_strdup(
"Failed to load .onion site: Could not initialize HTTP client.");
g_string_free(body.body, TRUE);
return job;
}
curl_easy_setopt(curl, CURLOPT_URL, job->target_url);
curl_easy_setopt(curl, CURLOPT_PROXY, job->proxy_url);
curl_easy_setopt(curl, CURLOPT_PROXYTYPE, (long)CURLPROXY_SOCKS5_HOSTNAME);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, TOR_FETCH_TIMEOUT_SECONDS);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, TOR_CONNECT_TIMEOUT_SECONDS);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "sovereign_browser/tor-scheme");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, tor_write_bounded);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, tor_header_capture);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, job);
CURLcode rc = curl_easy_perform(curl);
if (rc != CURLE_OK) {
const char *err = body.overflow
? "response exceeded maximum size"
: curl_easy_strerror(rc);
char *safe_err = html_escape(err);
job->error_html = g_strdup_printf("Failed to load .onion site: %s", safe_err);
g_free(safe_err);
g_string_free(body.body, TRUE);
curl_easy_cleanup(curl);
return job;
}
job->response_body = body.body;
if (!job->content_type || job->content_type[0] == '\0') {
g_free(job->content_type);
job->content_type = g_strdup("text/html");
}
curl_easy_cleanup(curl);
return job;
}
static void tor_job_free(tor_job_t *job) {
if (!job) return;
g_clear_object(&job->request);
g_free(job->target_url);
g_free(job->proxy_url);
if (job->response_body) g_string_free(job->response_body, TRUE);
g_free(job->content_type);
g_free(job->error_html);
g_free(job);
}
static void tor_scheme_worker_done(GObject *source_object,
GAsyncResult *result,
gpointer user_data) {
(void)source_object;
(void)user_data;
GTask *task = G_TASK(result);
tor_job_t *job = g_task_propagate_pointer(task, NULL);
if (job->error_html) {
respond_error_page(job->request, "Tor request failed", job->error_html);
tor_job_free(job);
return;
}
gsize len = job->response_body ? job->response_body->len : 0;
const char *data = (job->response_body && len > 0) ? job->response_body->str : "";
GInputStream *stream = g_memory_input_stream_new_from_data(g_memdup2(data, len),
len,
g_free);
webkit_uri_scheme_request_finish(job->request,
stream,
len,
job->content_type ? job->content_type : "text/html");
g_object_unref(stream);
tor_job_free(job);
}
static void tor_task_thread(GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable) {
(void)source_object;
(void)cancellable;
g_task_return_pointer(task, tor_scheme_worker(task_data), NULL);
}
static void on_tor_scheme(WebKitURISchemeRequest *request, gpointer user_data) {
(void)user_data;
const char *uri = webkit_uri_scheme_request_get_uri(request);
char *url_error = NULL;
char *target_url = build_target_http_url(uri, &url_error);
if (!target_url) {
respond_error_page(request,
"Invalid .onion URL",
url_error ? url_error : "Invalid tor:// URL.");
g_free(url_error);
return;
}
if (!net_service_is_ready(NET_SERVICE_TOR)) {
respond_error_page(request,
"Tor unavailable",
"Tor is not ready. Please wait for Tor to bootstrap and try again.");
g_free(target_url);
return;
}
const net_service_t *tor = net_service_get_status(NET_SERVICE_TOR);
const char *endpoint = tor ? tor->status.tor.socks_endpoint : NULL;
char *proxy_url = proxy_to_socks5h(endpoint);
if (!proxy_url) {
respond_error_page(request,
"Tor unavailable",
"Tor SOCKS endpoint is not available yet. Please try again shortly.");
g_free(target_url);
return;
}
tor_job_t *job = g_new0(tor_job_t, 1);
job->request = g_object_ref(request);
job->target_url = target_url;
job->proxy_url = proxy_url;
GTask *task = g_task_new(NULL, NULL, tor_scheme_worker_done, NULL);
g_task_set_task_data(task, job, NULL);
g_task_run_in_thread(task, tor_task_thread);
g_object_unref(task);
}
void tor_scheme_register(WebKitWebContext *ctx) {
g_return_if_fail(WEBKIT_IS_WEB_CONTEXT(ctx));
webkit_web_context_register_uri_scheme(ctx, "tor", on_tor_scheme, NULL, NULL);
}
+20
View File
@@ -0,0 +1,20 @@
/*
* tor_scheme.h — WebKitGTK tor:// URI scheme handler
*/
#ifndef TOR_SCHEME_H
#define TOR_SCHEME_H
#include <webkit2/webkit2.h>
#ifdef __cplusplus
extern "C" {
#endif
void tor_scheme_register(WebKitWebContext *ctx);
#ifdef __cplusplus
}
#endif
#endif /* TOR_SCHEME_H */
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
#define SB_VERSION "v0.0.34"
#define SB_VERSION "v0.0.35"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 34
#define SB_VERSION_PATCH 35
#endif /* SOVEREIGN_BROWSER_VERSION_H */