Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ jobs:
Copy-Item apps/desktop/launcher/tronbrowser.cmd "$stage/tronbrowser.cmd"
Copy-Item apps/desktop/launcher/tron-tor-helper "$stage/tron-tor-helper"
Copy-Item apps/desktop/launcher/tron-windows.py "$stage/tron-windows.py"
Copy-Item apps/desktop/launcher/tron-w3bs.ps1 "$stage/tron-w3bs.ps1"
Copy-Item -Recurse apps/desktop/extensions/ai-sidebar "$stage/extensions/ai-sidebar"
# Same wholesale-copy problem build-release.sh has: the vitest files
# next to the extension sources ride along into the zip. Chrome never
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/w3bs-scheme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: w3bs:// handler

# The OS-level half of w3bs:// support: each platform's real URL dispatch
# (xdg-open, LaunchServices, ShellExecute) has to reach the launcher, which
# opens the w3bs.org resolver. Nothing here can be checked from Linux alone.
on:
push:
branches: [main]
paths:
- 'apps/desktop/launcher/**'
- 'apps/desktop/test/w3bs-scheme/**'
- '.github/workflows/w3bs-scheme.yml'
pull_request:
paths:
- 'apps/desktop/launcher/**'
- 'apps/desktop/test/w3bs-scheme/**'
- '.github/workflows/w3bs-scheme.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
posix:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- name: Install xdg-utils and gio
if: runner.os == 'Linux'
run: |
command -v xdg-open && command -v gio \
|| (sudo apt-get update -qq && sudo apt-get install -y -qq xdg-utils libglib2.0-bin)
- name: OS opens w3bs:// in TronBrowser
run: sh apps/desktop/test/w3bs-scheme/posix.sh

windows:
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- name: Windows opens w3bs:// in TronBrowser
shell: pwsh
run: ./apps/desktop/test/w3bs-scheme/windows.ps1
95 changes: 95 additions & 0 deletions apps/desktop/launcher/tron-w3bs-register
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/bin/sh
# Register TronBrowser as the handler for w3bs:// URIs with the OS, so a W3BS
# link from another app, or one typed in the address bar (Chromium hands any
# scheme it cannot load to the OS), comes back to `tron`, which opens it
# through the w3bs.org resolver.
#
# tron-w3bs-register <command>
#
# <command> is what the OS runs with the URI as its one argument: the launcher
# passes its own path. Idempotent, quiet and best-effort: the launcher runs it
# in the background on every start, so upgrades and moved installs heal
# themselves. A handler someone else registered (the w3bs.org inspector, say) is
# left alone. Windows registers from tron-w3bs.ps1 instead.
set -u

TARGET="${TRONBROWSER_W3BS_HANDLER_TARGET:-${1:-}}"
[ -n "$TARGET" ] || { echo "usage: tron-w3bs-register <command>" >&2; exit 2; }

linux() {
command -v xdg-mime >/dev/null 2>&1 || return 0
_cur="$(xdg-mime query default x-scheme-handler/w3bs 2>/dev/null || true)"
# xdg-mime reports tronbrowser.desktop merely because it declares the scheme,
# while GIO (GNOME, and what xdg-open defers to there) only honours an
# explicit default. So write ours unless another app holds it.
case "$_cur" in ''|tronbrowser.desktop) ;; *) return 0 ;; esac
# Only claim the scheme through a desktop entry that declares it; install.sh,
# the .deb/.rpm and the AppImage all write one named tronbrowser.desktop.
for _d in "${XDG_DATA_HOME:-$HOME/.local/share}" $(printf '%s' "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" | tr ':' ' '); do
if grep -qs 'x-scheme-handler/w3bs' "$_d/applications/tronbrowser.desktop"; then
xdg-mime default tronbrowser.desktop x-scheme-handler/w3bs 2>/dev/null || true
return 0
fi
done
}

# macOS routes a URL scheme to an app bundle that declares it, never to a
# script, and Chromium.app cannot be the one: it would hand w3bs:// straight
# back to the OS. So build a small faceless applet that declares w3bs and runs
# $TARGET with the URI.
macos() {
APP="${TRONBROWSER_W3BS_APP:-$HOME/Applications/TronBrowser Links.app}"
ID="dev.tronbrowser.links"
STAMP="$APP/Contents/Resources/tron-target"
if [ -f "$STAMP" ] && [ "$(cat "$STAMP" 2>/dev/null)" = "$TARGET" ]; then
return 0
fi
command -v osacompile >/dev/null 2>&1 || return 0
PB=/usr/libexec/PlistBuddy
LSREG=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister

_src="$(mktemp -t tron-w3bs)" || return 0
# AppleScript string literal: escape backslashes and double quotes.
_t="$(printf '%s' "$TARGET" | sed 's/\\/\\\\/g; s/"/\\"/g')"
cat > "$_src" <<EOF
on open location theURL
do shell script quoted form of "$_t" & " " & quoted form of theURL & " >/dev/null 2>&1 &"
end open location
on run
end run
EOF
mkdir -p "$(dirname "$APP")"
rm -rf "$APP"
osacompile -o "$APP" "$_src" || { rm -f "$_src"; return 0; }
rm -f "$_src"

_plist="$APP/Contents/Info.plist"
"$PB" -c "Set :CFBundleIdentifier $ID" "$_plist" 2>/dev/null || "$PB" -c "Add :CFBundleIdentifier string $ID" "$_plist"
"$PB" -c "Set :CFBundleName TronBrowser Links" "$_plist" 2>/dev/null || true
"$PB" -c "Add :LSUIElement bool true" "$_plist" 2>/dev/null || true
"$PB" -c "Delete :CFBundleURLTypes" "$_plist" 2>/dev/null || true
"$PB" -c "Add :CFBundleURLTypes array" "$_plist"
"$PB" -c "Add :CFBundleURLTypes:0 dict" "$_plist"
"$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLName string W3BS resource" "$_plist"
"$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$_plist"
"$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string w3bs" "$_plist"
printf '%s' "$TARGET" > "$STAMP"
# Editing Info.plist breaks the applet's ad-hoc signature, and Apple silicon
# refuses to run an app with a broken one.
codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || true
[ -x "$LSREG" ] && "$LSREG" -f "$APP" >/dev/null 2>&1 || true

# Become the default only when no other app has claimed w3bs.
osascript -l JavaScript >/dev/null 2>&1 <<EOF || true
ObjC.import('CoreServices');
const cur = $.LSCopyDefaultHandlerForURLScheme($('w3bs'));
const id = cur.isNil() ? '' : ObjC.unwrap(cur);
if (!id || id.toLowerCase() === '$ID') $.LSSetDefaultHandlerForURLScheme($('w3bs'), $('$ID'));
EOF
}

case "$(uname -s)" in
Darwin) macos ;;
Linux|*BSD) linux ;;
esac
exit 0
48 changes: 48 additions & 0 deletions apps/desktop/launcher/tron-w3bs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# w3bs:// on Windows. Two jobs:
#
# tron-w3bs.ps1 -Register point HKCU\Software\Classes\w3bs at this script
# tron-w3bs.ps1 <uri> open a W3BS URI through the w3bs.org resolver
#
# The OS runs this script, not tronbrowser.cmd, for a w3bs:// link: a URI from
# a web page must never be parsed by cmd.exe, where & and % mean something.
# tronbrowser.cmd runs -Register in the background on every start, so scoop,
# choco, winget and the ZIP all register, and an upgrade that moved the files
# re-points the handler. A handler someone else registered is left alone.
param(
[Parameter(Position = 0)][string]$Uri = '',
[switch]$Register
)
$ErrorActionPreference = 'Stop'
$script = $MyInvocation.MyCommand.Path
$here = Split-Path -Parent $script
$key = 'HKCU:\Software\Classes\w3bs'

if ($Register) {
$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$command = '"{0}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "{1}" "%1"' -f $ps, $script
$current = $null
if (Test-Path "$key\shell\open\command") {
$current = (Get-Item "$key\shell\open\command").GetValue('')
}
if ($current -eq $command) { exit 0 }
if ($current -and $current -notmatch 'tron-w3bs\.ps1') { exit 0 }
New-Item -Path "$key\shell\open\command" -Force | Out-Null
Set-Item -Path $key -Value 'URL:W3BS resource'
New-ItemProperty -Path $key -Name 'URL Protocol' -Value '' -PropertyType String -Force | Out-Null
Set-Item -Path "$key\shell\open\command" -Value $command
exit 0
}

$gateway = if ($env:TRONBROWSER_W3BS_GATEWAY) { $env:TRONBROWSER_W3BS_GATEWAY } else { 'https://w3bs.org/browse?q=' }
$u = $Uri.Trim()
# W3BS-URI-1 identifiers are lowercase letters, digits, - . / @ and the scheme:
# nothing a query string or cmd.exe needs escaped, so a well-formed one goes
# through as written. Anything else opens the resolver without it rather than
# smuggling % or & into the command line below.
if ($u -match '^(?i)w3bs://[a-z0-9][a-z0-9./@-]{0,500}$') {
$url = $gateway + $u
} else {
$url = $gateway -replace '\?q=$', ''
}
& (Join-Path $here 'tronbrowser.cmd') $url
exit $LASTEXITCODE
16 changes: 15 additions & 1 deletion apps/desktop/launcher/tronbrowser
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ while [ "$_i" -lt "$_argc" ]; do
_a="$1"; shift; _i=$((_i + 1))
case "$_a" in
w3bs:*|W3BS:*)
_a="$W3BS_GATEWAY$(printf '%s' "$_a" | od -An -v -tx1 | tr -d ' \n' | sed 's/\(..\)/%\1/g')" ;;
# Percent-encode like encodeURIComponent: unreserved characters stay.
_a="$W3BS_GATEWAY$(printf '%s' "$_a" | LC_ALL=C awk '
BEGIN { for (i = 1; i < 256; i++) ord[sprintf("%c", i)] = i }
{ for (i = 1; i <= length($0); i++) {
c = substr($0, i, 1)
if (c ~ /[A-Za-z0-9._~!*()\047-]/) printf "%s", c; else printf "%%%02X", ord[c]
} }')" ;;
esac
set -- "$@" "$_a"
done
Expand Down Expand Up @@ -774,6 +780,14 @@ if [ "$TOR" != "1" ] && [ -z "$MAC_APP" ] && [ -f "$DIR/tron-pwa" ]; then
fi
fi

# w3bs:// from other apps and the address bar reaches the OS, which must hand it
# back to this launcher (see the w3bs:// rewrite above). On every start, in the
# background, so package-manager installs that never ran install.sh register
# too and an upgrade that moved us re-points the handler.
if [ -f "$DIR/tron-w3bs-register" ]; then
sh "$DIR/tron-w3bs-register" "$DIR/$(basename "$SELF")" >/dev/null 2>&1 </dev/null &
fi

# --- Moshpit names over HTTPS ----------------------------------------------
# Chromium does not read /etc/ssl/certs. On Linux the only user-added trust it
# honours lives in the per-user NSS database at ~/.pki/nssdb — a path keyed off
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/launcher/tronbrowser.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ set "DIR=%~dp0"
set "DATA=%USERPROFILE%\.tronbrowser"
if defined TRONBROWSER_DATA set "DATA=%TRONBROWSER_DATA%"

rem `tron w3bs://...`: Chromium cannot load the scheme, so tron-w3bs.ps1 opens
rem it through the w3bs.org resolver (and calls back into this script).
set "TB_ARG1=%~1"
if /i "%TB_ARG1:~0,5%"=="w3bs:" (
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%DIR%tron-w3bs.ps1" "%TB_ARG1%"
exit /b %errorlevel%
)

rem Validate the interpreter: Windows may have a Store alias named python.exe.
set "PYTHON="
set "PYTHON_ARGS="
Expand Down Expand Up @@ -82,6 +90,10 @@ if not defined PYTHON (
if errorlevel 1 echo TronBrowser: network helper unavailable; ordinary browsing still works. >&2
)

rem Register w3bs:// with Windows (links from other apps and the address bar),
rem in the background so it never delays the window.
if exist "%DIR%tron-w3bs.ps1" start "" /b powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "%DIR%tron-w3bs.ps1" -Register

rem --enable-features=EnableTabMuting makes the tab audio indicator a clickable
rem mute/unmute control (media::kEnableTabMuting is DISABLED_BY_DEFAULT upstream;
rem stock Chrome only enables it via Finch, which an ungoogled build never gets).
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/scripts/build-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ stage() { # dest dir
# launcher starts it; it starts Tor / the Moshpit resolver only when asked).
install -m 0755 "$DESKTOP/launcher/tron-tor-helper" "$s/tron-tor-helper"
install -m 0644 "$DESKTOP/launcher/tron-windows.py" "$s/tron-windows.py"
# w3bs:// registration with the OS: the shim runs tron-w3bs-register in the
# background on Linux/macOS, tronbrowser.cmd runs tron-w3bs.ps1 on Windows.
install -m 0755 "$DESKTOP/launcher/tron-w3bs-register" "$s/tron-w3bs-register"
install -m 0644 "$DESKTOP/launcher/tron-w3bs.ps1" "$s/tron-w3bs.ps1"
# Repoints installed-web-app desktop icons at the launcher. The shim runs it
# on every start (the engine rewrites those files behind us); `tron pwa` is
# the manual handle.
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/test/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ describe('launcher flags', () => {
const url = new URL(opened[0]!);
expect(url.origin + url.pathname).toBe('https://w3bs.org/browse');
expect(url.searchParams.get('q')).toBe('w3bs://prompt/w3bs/research@1');
// Encoded the way encodeURIComponent does it, so the address bar stays readable.
expect(opened[0]).toBe('https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch%401');
expect(opened[1]).toBe('https://tronbrowser.dev');
});

Expand Down
104 changes: 104 additions & 0 deletions apps/desktop/test/w3bs-scheme/posix.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/bin/sh
# End to end on Linux and macOS: register w3bs:// the way the launcher does, ask
# the OS to open a W3BS URI (xdg-open / open, the same path a link from another
# app or the address bar takes), and check the engine was handed the w3bs.org
# resolver URL. The engine is a stub that records its argv, and HOME and the XDG
# dirs are scratch, so nothing on the machine running this is touched except,
# on macOS, LaunchServices learning about the scratch applet.
set -eu

REPO="$(CDPATH= cd -- "$(dirname -- "$0")/../../../.." && pwd)"
W="$(mktemp -d)"
REAL_HOME="$HOME"
export HOME="$W/home"
export XDG_DATA_HOME="$HOME/.local/share" XDG_CONFIG_HOME="$HOME/.config"
mkdir -p "$W/bin" "$XDG_DATA_HOME/applications" "$XDG_CONFIG_HOME"
cp "$REPO/apps/desktop/launcher/tronbrowser" "$REPO/apps/desktop/launcher/tron-w3bs-register" "$W/bin/"
ARGV="$W/argv"
URI='w3bs://prompt/w3bs/research@1'
WANT='https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch%401'
# LaunchServices will not route a URL to an app in a temporary directory, so on
# macOS the applet goes where real installs put it, under a test-only name.
# Same bundle id as the real one: run this on a disposable machine (CI).
APP="$REAL_HOME/Applications/TronBrowser Links (test).app"

# Named ungoogled-chromium so the launcher's de-googled check stays quiet.
cat > "$W/bin/ungoogled-chromium" <<EOF
#!/bin/sh
if [ "\${1:-}" = "--version" ]; then echo "Chromium 131.0.0.0"; exit 0; fi
for a in "\$@"; do printf '%s\n' "\$a"; done > "$ARGV.tmp" && mv "$ARGV.tmp" "$ARGV"
EOF
# What the OS runs. LaunchServices and xdg-open start it with their own bare
# environment, so the stub engine and scratch profile are set here.
cat > "$W/bin/tron" <<EOF
#!/bin/sh
export HOME="$HOME" TRONBROWSER_BROWSER="$W/bin/ungoogled-chromium" TRONBROWSER_DATA="$W/profile"
export TRONBROWSER_W3BS_HANDLER_TARGET="$W/bin/tron" TRONBROWSER_W3BS_APP="$APP"
exec sh "$W/bin/tronbrowser" "\$@"
EOF
chmod +x "$W/bin/"*

fail() { echo "FAIL: $*" >&2; exit 1; }

case "$(uname -s)" in
Darwin)
TRONBROWSER_W3BS_APP="$APP" sh "$W/bin/tron-w3bs-register" "$W/bin/tron"
PLIST="$APP/Contents/Info.plist"
[ -f "$PLIST" ] || fail "no handler app at $APP"
[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleURLTypes:0:CFBundleURLSchemes:0' "$PLIST")" = w3bs ] \
|| fail "handler app does not declare w3bs"
codesign --verify "$APP" || fail "handler app signature is broken"
# LaunchServices learns about the app asynchronously.
i=0
until open "$URI" 2>/dev/null; do
i=$((i + 1))
if [ "$i" -ge 20 ]; then
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump \
| grep -i -B3 -A3 'w3bs\|tronbrowser.links' >&2 || true
open "$URI" || true
fail "LaunchServices has no handler for w3bs"
fi
sleep 1
done
;;
*)
command -v xdg-open >/dev/null 2>&1 || fail "xdg-open is not installed"
cat > "$XDG_DATA_HOME/applications/tronbrowser.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=TronBrowser
Exec=$W/bin/tron %U
MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/w3bs;
EOF
# Someone else's handler is left alone (xdg-mime only reports a default
# whose desktop entry exists)...
printf '[Desktop Entry]\nType=Application\nName=Other\nExec=true %%U\nMimeType=x-scheme-handler/w3bs;\n' \
> "$XDG_DATA_HOME/applications/other.desktop"
xdg-mime default other.desktop x-scheme-handler/w3bs
sh "$W/bin/tron-w3bs-register" "$W/bin/tron"
[ "$(xdg-mime query default x-scheme-handler/w3bs)" = other.desktop ] \
|| fail "replaced another app's w3bs handler"
# ...and with none set, TronBrowser claims the scheme.
rm -f "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/other.desktop"
sh "$W/bin/tron-w3bs-register" "$W/bin/tron"
[ "$(xdg-mime query default x-scheme-handler/w3bs)" = tronbrowser.desktop ] \
|| fail "tronbrowser.desktop is not the w3bs handler"
# As on a GNOME desktop: xdg-open hands the URI to `gio open`, which looks
# the handler up in mimeapps.list. Generic mode is not used because
# xdg-utils 1.1.x (Ubuntu 24.04) misreads any scheme with a digit, like
# w3bs, as a file path. DISPLAY only has to be set; nothing connects to it.
command -v gio >/dev/null 2>&1 || fail "gio is not installed"
DISPLAY="${DISPLAY:-:99}" XDG_CURRENT_DESKTOP=GNOME xdg-open "$URI"
;;
esac

i=0
while [ ! -s "$ARGV" ] && [ "$i" -lt 60 ]; do sleep 1; i=$((i + 1)); done
[ -s "$ARGV" ] || fail "the engine was never started for $URI"
grep -qxF "$WANT" "$ARGV" || { cat "$ARGV" >&2; fail "the engine was not handed $WANT"; }
echo "ok: $URI -> $WANT"
if [ -d "$APP" ]; then
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -u "$APP" 2>/dev/null || true
rm -rf "$APP"
fi
rm -rf "$W"
Loading
Loading