The click action of a notification was a free-form shell string run through `bash -lc`, safe only when every sender shell-quoted every interpolated value perfectly. One slip is RCE: a hostile yt-dlp video title forged an output record and injected an mpv option into the click command (mehmetince.net RCE, partially addressed by #7847). Add a parameterized transport: omarchy-notification-send gains --exec-arg (repeatable), encoding a JSON argv into the omarchy-exec-argv hint. The shell runs it with Quickshell.execDetached(argv) and no shell, so data an attacker controls is only ever one argument and can never be reparsed as a command. The shell fails closed on a malformed argv hint. The legacy free-form --exec string is retained but honored only from Omarchy's own omarchy-action toasts, and deprecated. Migrate all in-repo callers (screenshot, screen recording, taildrop receive, migrate-notify, crash-watch, yt-dlp host) to --exec-arg. Update docs and tests.
100 lines
2.6 KiB
Bash
Executable File
100 lines
2.6 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Save incoming Taildrop files and announce them
|
|
# omarchy:args=[--once] [directory]
|
|
# omarchy:examples=omarchy tailscale receive | omarchy tailscale receive --once ~/Desktop
|
|
|
|
set -euo pipefail
|
|
|
|
once=false
|
|
if [[ ${1:-} == "--once" ]]; then
|
|
once=true
|
|
shift
|
|
fi
|
|
|
|
dir="${1:-${XDG_DOWNLOAD_DIR:-$HOME/Downloads}}"
|
|
|
|
# Taildrop lands in a staging directory next door rather than straight in the
|
|
# downloads directory: waiting for a delivery can take hours, and everything
|
|
# else that shows up meanwhile is somebody else's file. Same filesystem, so
|
|
# handing the finished file over is a rename.
|
|
staging="$dir/.omarchy-taildrop"
|
|
mkdir -p "$staging"
|
|
|
|
# Take the name by linking to it rather than by looking and then renaming.
|
|
# link(2) refuses an existing name, so nothing can land on the chosen one in
|
|
# the gap between the two. Staging shares the filesystem with the downloads
|
|
# directory, so the link always resolves and unlinking the staged name
|
|
# finishes the move. Prints the name it took.
|
|
claim_path() {
|
|
local staged="$1" name="${staged##*/}" base ext candidate index=0
|
|
|
|
base="${name%.*}"
|
|
ext="${name#"$base"}"
|
|
[[ -z $base ]] && { base="$name"; ext=""; }
|
|
|
|
while (( index < 1000 )); do
|
|
if (( index == 0 )); then
|
|
candidate="$dir/$name"
|
|
else
|
|
candidate="$dir/$base-$index$ext"
|
|
fi
|
|
|
|
if ln -- "$staged" "$candidate" 2>/dev/null; then
|
|
rm -f -- "$staged"
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
|
|
# Only a taken name is worth another spin. Anything else failed the link
|
|
# itself, and the file keeps its place in staging for the next run.
|
|
[[ -e $candidate ]] || return 1
|
|
|
|
((index++))
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
announce() {
|
|
local path="$1"
|
|
local name="${path##*/}"
|
|
local args=("Received $name" "Saved to ${dir/#$HOME/~}")
|
|
|
|
case "${name,,}" in
|
|
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
|
args+=(--image "$path")
|
|
;;
|
|
*)
|
|
args+=(-g )
|
|
;;
|
|
esac
|
|
|
|
# Announcing is best-effort: the file is already delivered, and under `set -e`
|
|
# a notification outage would otherwise kill the long-running receiver.
|
|
omarchy-notification-send "${args[@]}" --exec-arg xdg-open --exec-arg "$path" || true
|
|
}
|
|
|
|
deliver() {
|
|
local staged target
|
|
|
|
while IFS= read -r staged; do
|
|
target=$(claim_path "$staged") || continue
|
|
announce "$target"
|
|
done < <(find "$staging" -mindepth 1 -maxdepth 1)
|
|
}
|
|
|
|
# Anything left staged by an interrupted run still deserves delivering.
|
|
deliver
|
|
|
|
while true; do
|
|
if ! tailscale file get --wait --conflict=rename "$staging"; then
|
|
$once && exit 1
|
|
sleep 10
|
|
continue
|
|
fi
|
|
|
|
deliver
|
|
$once && exit 0
|
|
done
|