v0.0.35 - Selective Tor onion routing (tor:// scheme) + install.sh
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
|
||||
|
||||
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)
|
||||
|
||||
Executable
+462
@@ -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 "$@"
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user