#!/bin/sh # drop.pew.sk receiver: waits for files sent with a transfer code and saves # them to ~/Drop// (or -d DIR). POSIX sh + curl, nothing to install. # # curl -fsSL https://drop.pew.sk/r.sh | sh -s -- [-d DIR] # # Every part is checked against the sender's SHA-256 before it is confirmed; # a part is written to disk only after the relay accepted that confirmation, # so a late or rejected part is never appended twice. set -u BASE=${DROP_BASE:-https://drop.pew.sk} CODE= DEST= die() { printf 'drop: %s\n' "$*" >&2; exit "${RC:-1}"; } while [ $# -gt 0 ]; do case $1 in -d) [ $# -ge 2 ] || die "-d needs a directory"; DEST=$2; shift 2 ;; -h|--help) sed -n '2,10p' "$0" 2>/dev/null || echo "usage: sh r.sh [-d DIR]"; exit 0 ;; -*) die "unknown option $1" ;; *) [ -z "$CODE" ] || die "one code only"; CODE=$1; shift ;; esac done [ -n "$CODE" ] || die "usage: sh r.sh [-d DIR]" printf '%s' "$CODE" | grep -Eq '^[a-hjkmnp-z2-9]{4}(-[a-hjkmnp-z2-9]{4}){3}$' || RC=2 die "not a transfer code: $CODE" command -v curl >/dev/null 2>&1 || die "curl is required" [ -n "$DEST" ] || DEST="$HOME/Drop/$CODE" mkdir -p "$DEST" || die "cannot create $DEST" if command -v sha256sum >/dev/null 2>&1; then sha() { sha256sum "$1" | cut -d' ' -f1; } elif command -v shasum >/dev/null 2>&1; then sha() { shasum -a 256 "$1" | cut -d' ' -f1; } elif command -v openssl >/dev/null 2>&1; then sha() { openssl dgst -sha256 -r "$1" | cut -d' ' -f1; } else die "need sha256sum, shasum or openssl"; fi TMP=$(mktemp -d "${TMPDIR:-/tmp}/drop.XXXXXX") || die "mktemp failed" trap 'rm -rf "$TMP"' EXIT trap 'exit 130' INT TERM header() { grep -i "^$1:" "$TMP/h" | tail -n 1 | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r'; } # Percent-decoding byte by byte; awk under LC_ALL=C prints raw bytes, so a # UTF-8 name comes out as the same UTF-8. urldecode() { printf '%s' "$1" | LC_ALL=C awk ' BEGIN { hex = "0123456789abcdef" } { s = $0; out = "" while ((i = index(s, "%")) > 0) { out = out substr(s, 1, i - 1) h = tolower(substr(s, i + 1, 2)) a = index(hex, substr(h, 1, 1)); b = index(hex, substr(h, 2, 1)) if (length(h) == 2 && a && b) { out = out sprintf("%c", (a - 1) * 16 + b - 1); s = substr(s, i + 3) } else { out = out "%"; s = substr(s, i + 1) } } printf "%s", out s }' } # The sender's name is a suggestion. Basename only, no control characters, # no leading dots (no hidden files, no "..", no "."), at most 200 characters. sanitize() { n=$(printf '%s' "$1" | tr -d '\000-\037\177') n=${n##*/} n=$(printf '%s' "$n" | sed 's/^\.*//' | cut -c1-200) [ -n "$n" ] || n="file-$2" printf '%s' "$n" } # report.txt taken -> report-1.txt, report-2.txt, ... free_name() { [ -e "$DEST/$1" ] || { printf '%s' "$1"; return; } case $1 in ?*.*) stem=${1%.*}; ext=".${1##*.}" ;; *) stem=$1; ext= ;; esac k=1 while [ -e "$DEST/$stem-$k$ext" ]; do k=$((k + 1)); done printf '%s' "$stem-$k$ext" } human() { awk -v b="$1" 'BEGIN { split("B KB MB GB TB", u, " "); i = 1 while (b >= 1000 && i < 5) { b /= 1000; i++ } printf (i == 1 ? "%d %s" : "%.1f %s"), b, u[i] }' } ack() { # id part ok sha -> prints "accepted" if the relay kept the part curl -sS --max-time 30 -X POST -H 'Content-Type: application/json' \ --data "{\"id\":\"$1\",\"part\":$2,\"ok\":$3,\"sha256\":\"$4\"}" \ "$BASE/api/$CODE/ack" 2>/dev/null | grep -q '"accepted":true' && echo accepted } printf 'drop: receiving into %s\n' "$DEST" printf 'drop: waiting for the sender (code %s)...\n' "$CODE" CUR_ID= CUR_NEXT=0 CUR_PARTIAL= CUR_NAME= FILES=0 BYTES=0 BACKOFF=1 while :; do status=$(curl -sS --max-time 75 -D "$TMP/h" -o "$TMP/body" -w '%{http_code}' "$BASE/api/$CODE/next" 2>"$TMP/err") rc=$? if [ $rc -ne 0 ] || [ "${status:-000}" -ge 500 ] 2>/dev/null; then [ $BACKOFF -ge 8 ] && printf 'drop: connection problem (%s), retrying in %ss\n' "${status:-curl $rc}" "$BACKOFF" >&2 sleep "$BACKOFF" BACKOFF=$((BACKOFF * 2)); [ $BACKOFF -gt 30 ] && BACKOFF=30 continue fi BACKOFF=1 case $status in 204) continue ;; 410) break ;; 404) RC=2 die "unknown or expired code: $CODE" ;; 200) ;; *) printf 'drop: unexpected HTTP %s, retrying\n' "$status" >&2; sleep 2; continue ;; esac id=$(header X-Drop-Id); part=$(header X-Drop-Part); parts=$(header X-Drop-Parts) size=$(header X-Drop-Size); want=$(header X-Drop-Sha256); length=$(header Content-Length) printf '%s' "$id" | grep -Eq '^[A-Za-z0-9_-]{1,64}$' || { echo "drop: bad X-Drop-Id, skipping" >&2; continue; } case "$part$parts$size" in *[!0-9]*|'') echo "drop: bad part headers, skipping" >&2; continue ;; esac got_len=$(wc -c < "$TMP/body" | tr -d ' ') got_sha=$(sha "$TMP/body") if [ "$got_len" != "$length" ] || [ "$got_sha" != "$want" ]; then printf 'drop: part %s of %s failed verification, asking for it again\n' "$((part + 1))" "$parts" >&2 ack "$id" "$part" false "$got_sha" >/dev/null continue fi if [ "$part" -eq 0 ]; then name=$(sanitize "$(urldecode "$(header X-Drop-Name)")" "$id") [ -n "$CUR_PARTIAL" ] && rm -f "$CUR_PARTIAL" CUR_ID=$id CUR_NEXT=0 CUR_NAME=$name CUR_PARTIAL="$DEST/.$name.partial" : > "$CUR_PARTIAL" || die "cannot write to $DEST" elif [ "$id" != "$CUR_ID" ] || [ "$part" -ne "$CUR_NEXT" ]; then # Only possible after this receiver restarted mid-file: the earlier parts # are gone, so refuse and let the sender fail loudly. echo "drop: got part $((part + 1)) of a file whose start this receiver never saw" >&2 ack "$id" "$part" false "$got_sha" >/dev/null continue fi [ "$(ack "$id" "$part" true "$got_sha")" = accepted ] || continue cat "$TMP/body" >> "$CUR_PARTIAL" || die "write failed (disk full?)" CUR_NEXT=$((part + 1)) [ "$parts" -gt 1 ] && printf ' %s part %s/%s\n' "$CUR_NAME" "$CUR_NEXT" "$parts" if [ "$CUR_NEXT" -eq "$parts" ]; then final=$(free_name "$CUR_NAME") mv "$CUR_PARTIAL" "$DEST/$final" || die "cannot rename to $final" printf '✔ %s %s\n' "$DEST/$final" "$(human "$size")" FILES=$((FILES + 1)) BYTES=$((BYTES + size)) CUR_ID= CUR_PARTIAL= CUR_NEXT=0 fi done if [ -n "$CUR_PARTIAL" ]; then rm -f "$CUR_PARTIAL" RC=1 die "transfer ended with $CUR_NAME incomplete ($FILES other file(s) saved)" fi printf 'drop: done, %s file(s), %s in %s\n' "$FILES" "$(human "$BYTES")" "$DEST" if [ "${DROP_NOTIFY:-1}" != 0 ] && [ -t 1 ] && [ "$(uname)" = Darwin ] && command -v osascript >/dev/null 2>&1; then osascript -e "display notification \"$FILES file(s) received\" with title \"drop.pew.sk\"" >/dev/null 2>&1 || true fi exit 0