Wren Linux

wren-linux / extras / onionshare.sh

#!/bin/bash
# OnionShare (from Debian): share files, receive files, host a website or
# run a chat room over Tor, straight from this laptop, with no account and
# no server in between. The other side opens the address in Tor Browser.
#
# OnionShare starts its own private tor. Debian's package also installs
# the system tor service, which would connect to Tor directly the moment
# it's installed, so that service is masked (blocked) first and never runs.
# It first asks whether you need a bridge to reach Tor (built-in obfs4,
# your own obfs4 lines, Snowflake or webtunnel; OnionShare can't use meek on
# Debian). The choice is written into OnionShare's settings before its first
# start, so its first connection goes through the bridge.
#
# OnionShare's own tor can't run webtunnel. With that chosen, onionshare and
# onionshare-cli in ~/.local/bin start a private tor instead (bin/onionshare-tor),
# which OnionShare talks to through a socket only you can open, and lyrebird comes
# from the Tor Project's repository (lib/tor.sh: its key pinned, nothing else from it).
# Usage: bash onionshare.sh [--remove | --manual]
# summary: Share files, sites and chat over Tor
# security: some
# category: talk
# status: apt:onionshare-cli apt:onionshare
. "$(dirname -- "${BASH_SOURCE[0]}")/../lib.sh"
if [ "${1:-}" = --manual ]; then print_manual; exit; fi
need_user

SETTINGS="$HOME/.config/onionshare/onionshare.json"
# The private tor, for webtunnel: its torrc (with your bridge lines), data and socket
PRIVATE_TOR="$HOME/.local/state/wren/onionshare-tor"

# private_tor_off: back to OnionShare's own tor: the links, the torrc and the data gone
private_tor_off() {
    local c
    for c in onionshare onionshare-cli; do
        if [ -L "$BIN/$c" ] && [ "$(readlink -- "$BIN/$c")" = onionshare-tor ]; then rm -f "$BIN/$c"; fi
    done
    remove_bin onionshare-tor
    rm -rf "$PRIVATE_TOR"
}

# private_tor_on: a private tor for webtunnel. Nothing else is listening: no SOCKS port
# (OnionShare needs only its control socket), and no cookie, since the folder is the lock.
private_tor_on() {
    local c
    [[ ! $PRIVATE_TOR =~ [[:space:]\"\\] ]] || die "tor can't use a folder with spaces or quotes: $PRIVATE_TOR"
    # (a socket's path has a length limit)
    [ ${#PRIVATE_TOR} -le 90 ] || die "Your home folder's path is too long for tor's socket: $PRIVATE_TOR"
    mkdir -p "${PRIVATE_TOR%/*}"
    install -d -m 0700 "$PRIVATE_TOR" "$PRIVATE_TOR/data"
    {
        echo "# Written by Wren Linux extras/onionshare.sh: OnionShare's private tor (bin/onionshare-tor)."
        echo "DataDirectory $PRIVATE_TOR/data"
        echo "ControlSocket $PRIVATE_TOR/control"
        echo "CookieAuthentication 0"
        echo "SocksPort 0"
        echo "ClientOnly 1"
        echo "AvoidDiskWrites 1"
        echo "Log notice file $PRIVATE_TOR/log"
        tor_bridge_config
    } | install -m 0600 /dev/stdin "$PRIVATE_TOR/torrc"
    tor --defaults-torrc /dev/null -f "$PRIVATE_TOR/torrc" --verify-config >/dev/null 2>&1 ||
        die "tor doesn't accept the private settings in $PRIVATE_TOR/torrc."
    install_bin onionshare-tor
    for c in onionshare onionshare-cli; do
        if [ -e "$BIN/$c" ] && [ ! -L "$BIN/$c" ]; then
            cp -- "$BIN/$c" "$BIN/$c.bak"
            note "Old $BIN/$c kept as $BIN/$c.bak"
        fi
        ln -sfn onionshare-tor "$BIN/$c"
    done
    note "webtunnel is experimental here, and a dead bridge stops OnionShare: it never connects any other way."
}

if [ "${1:-}" = --remove ]; then
    apt_purge onionshare onionshare-cli
    private_tor_off
    remove_torproject_repo_if_unused
    note "Your settings stay in ~/.config/onionshare; received files in ~/OnionShare."
    note "The system tor service stays masked: sudo systemctl unmask tor.service tor@default.service"
    exit
fi

# Asked first, before anything is installed
say "Bridges"
old=()
if [ -f "$SETTINGS" ] && command -v python3 >/dev/null; then
    mapfile -t old < <(python3 -I - "$SETTINGS" <<'PY' 2>/dev/null
import json, sys
s = json.load(open(sys.argv[1]))
if s.get("bridges_enabled") and s.get("bridges_type") == "custom":
    print("\n".join(l for l in s.get("bridges_custom", "").split("\n") if l.strip()))
PY
    )
fi
# (webtunnel's are in the private tor's settings)
if [ -r "$PRIVATE_TOR/torrc" ]; then mapfile -t old < <(sed -n 's/^Bridge //p' "$PRIVATE_TOR/torrc"); fi
if [ ${#old[@]} -gt 0 ] && ask "Keep the ${#old[@]} bridge line(s) OnionShare already has?" y; then
    BRIDGES=("${old[@]}")
    BRIDGE_TYPE=obfs4
    [[ ${old[0]} != webtunnel\ * ]] || BRIDGE_TYPE=webtunnel
    BRIDGE_WANTED=true
else
    pick_bridges onionshare
fi

say "Blocking the system tor service"
if system_tor_wanted; then
    # extras/apt-tor.sh or extras/remote-access.sh runs it, with your bridge
    note "The system tor service stays on: apt over Tor or remote access uses it."
else
    sudo systemctl mask tor.service tor@default.service >/dev/null
    note "Masked tor.service and tor@default.service: installing tor won't start it."
fi

say "Installing OnionShare"
pkgs=(onionshare-cli obfs4proxy)
# OnionShare finds the Snowflake program by name
[ "$BRIDGE_TYPE" != snowflake ] || pkgs+=(snowflake-client)
if [ "$BRIDGE_TYPE" = webtunnel ]; then
    add_torproject_repo
    pkgs+=(lyrebird)
fi
if ask "Also install the graphical OnionShare app?" y; then
    # qt6-wayland: lets the Qt 6 app run on Wayland without XWayland
    pkgs+=(onionshare qt6-wayland)
fi
apt_install "${pkgs[@]}"
if ! system_tor_wanted && systemctl is-active --quiet tor.service tor@default.service; then
    warn "The system tor service is running anyway. Stop it: sudo systemctl stop tor@default.service"
fi

# The private tor is in place before the settings point at it, or gone once they don't
if [ "$BRIDGE_TYPE" = webtunnel ]; then private_tor_on; else private_tor_off; fi

# Only these settings are written; OnionShare fills in the rest. Its tor then
# connects through the bridge, from its very first start.
mkdir -p "$(dirname -- "$SETTINGS")"
python3 -I - "$SETTINGS" "$PRIVATE_TOR/control" "$BRIDGE_TYPE" "${BRIDGES[@]}" <<'PY'
import json, os, sys
path, control, kind, lines = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4:]
try:
    s = json.load(open(path))
except (OSError, ValueError):
    s = {}
s["connection_type"] = "bundled"
s["bridges_enabled"] = kind != "none"
if kind == "webtunnel":
    # (without the private tor running, this finds nothing: it doesn't connect at all)
    s["connection_type"] = "socket_file"
    s["socket_file_path"] = control
    s["auth_type"] = "no_auth"
    s["bridges_enabled"] = False
    s.pop("bridges_custom", None)
elif kind == "obfs4" and lines:
    s["bridges_type"] = "custom"
    s["bridges_custom"] = "\n".join(lines)
elif kind in ("obfs4-builtin", "snowflake"):
    s["bridges_type"] = "built-in"
    s["bridges_builtin_pt"] = kind.replace("-builtin", "")
tmp = path + ".tmp"
with open(tmp, "w") as f:
    json.dump(s, f, indent=2)
os.chmod(tmp, 0o600)
os.replace(tmp, path)
PY
if [ "$BRIDGE_TYPE" != none ]; then
    note "OnionShare's tor connects through a bridge: $(bridge_summary)."
elif $BRIDGE_WANTED; then
    warn "You said you need a bridge but none was set up: OnionShare connects to Tor directly. Run this again."
else
    note "No bridge: OnionShare's tor connects directly."
fi

cat <<'EOF'

    How to use onionshare-cli (it prints an .onion address and, unless
    --public, a private key; send both to the other person, who opens the
    address in Tor Browser):
      onionshare-cli report.pdf photos/      share files and folders; stops
                                             after the first download
      onionshare-cli --no-autostop-sharing report.pdf
                                             keep sharing after downloads
      onionshare-cli --auto-stop-timer 3600 report.pdf
                                             stop sharing after an hour
      onionshare-cli --receive               let people send you files
                                             (saved in ~/OnionShare)
      onionshare-cli --website ~/site/       host a static website
      onionshare-cli --chat                  start a private chat room
      onionshare-cli --public report.pdf     no private key: anyone with the
                                             address can open it
      onionshare-cli --qr report.pdf         also show the address as a QR code
    Press Ctrl+C to stop sharing.
EOF
if is_installed onionshare; then
    note "The graphical app: onionshare   (or Super+d)"
fi