Skip to content

Instantly share code, notes, and snippets.

@EleotleCram
Last active May 22, 2025 16:21
Show Gist options
  • Save EleotleCram/c0e6164fedb8dce3b885d3f26be7fd90 to your computer and use it in GitHub Desktop.
Save EleotleCram/c0e6164fedb8dce3b885d3f26be7fd90 to your computer and use it in GitHub Desktop.
ppng.io - Securely transfer files or directories using a piping server
#!/bin/bash
set -euo pipefail
# ppng.io - Securely transfer files or directories using a piping server
#
# Learn more about the underlying principles at:
# https://github.com/nwtgck/piping-server#readme
die() { echo "Error: $1" >&2; exit 1; }
# Ensure symlink usage if SELF is called ppng
SELF_PATH="$0"
SELF_NAME=$(basename "$SELF_PATH")
if [[ "$SELF_NAME" == "ppng" ]]; then
die "Error: Don't run this script directly. Use a symlink named after the server (e.g., ppng.io)"
fi
require_commands() {
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd"
done
}
require_commands curl openssl tar gzip grep xargs
detect_server_url() {
local host="$SELF_NAME"
for proto in https http; do
for port in 443 8088 80; do
url="${proto}://${host}:${port}"
if curl --silent --head --max-time 1 "$url/" | grep -qi "200 OK"; then
echo "$url"
return 0
fi
done
done
die "Failed to detect a reachable piping server for $host"
}
SECRETS_DIR="${HOME}/.ppng/secrets"
SECRET_FILE="${SECRETS_DIR}/${SELF_NAME}.secret"
CHANNEL_FILE="${HOME}/.ppng/channel"
WORDLIST_URL="https://gist.githubusercontent.com/sts10/7dead5ec2ca9b30200edfa29010da3d5/raw/f616189341b01fe79c2ed29ad0d191476edfb88d/8192.txt"
UPDATE_URL="https://gist.githubusercontent.com/EleotleCram/c0e6164fedb8dce3b885d3f26be7fd90/raw/ppng.io"
perform_update() {
TMP_FILE=$(mktemp)
curl -fsSL "$UPDATE_URL" -o "$TMP_FILE" || die "Failed to download update"
chmod +x "$TMP_FILE"
# Resolve real file path if SELF_PATH is a symlink
if [[ -L "$SELF_PATH" ]]; then
REAL_PATH=$(readlink "$SELF_PATH")
[[ "$REAL_PATH" != /* ]] && REAL_PATH="$(dirname "$SELF_PATH")/$REAL_PATH"
else
REAL_PATH="$SELF_PATH"
fi
mv "$TMP_FILE" "$REAL_PATH" || die "Failed to update script"
echo "Updated $REAL_PATH"
}
usage() {
cat <<EOF
Usage: $SELF_NAME <put|get|--server|--update> [FILE|DIR|.]
Commands:
put [FILE|DIR] Encrypt and send the specified file or directory.
If no FILE or DIR is given, reads from stdin.
get [DIR] Download, decrypt and:
- if DIR is an existing directory, extract into it
- otherwise, output decrypted data to stdout
--server Start a local piping-server on port 8088.
--update Update to the latest version (will honour symlinks).
--help, -h Show this help message.
Examples:
$SELF_NAME put myfile.txt
$SELF_NAME get .
tar cv mydir | $SELF_NAME put
$SELF_NAME get | tar xv
$SELF_NAME --server
About:
This utility is a wrapper around a piping server, enabling secure and simple
encrypted file transfer using a shared secret and channel name.
Learn more about the underlying principles at:
https://github.com/nwtgck/piping-server#readme
EOF
}
fetch_wordlist() {
if [[ "$(declare -p WORDLIST 2>/dev/null)" =~ "declare -a" && ${#WORDLIST[@]} -gt 0 ]]; then
return 0
fi
mapfile -t WORDLIST < <(curl -fsSL "$WORDLIST_URL") || die "Failed to download wordlist"
}
generate_words() {
local count="$1"
fetch_wordlist
local words=()
for ((i = 0; i < count; i++)); do
words+=("${WORDLIST[$((RANDOM % 8192))]}")
done
echo "${words[*]}"
}
validate_words() {
local input=($1)
local expected_count="$2"
local strict_count="$3"
if [[ "$strict_count" == true && "${#input[@]}" -ne "$expected_count" ]]; then
return 1
fi
fetch_wordlist
for w in "${input[@]}"; do
if ! grep -qx "$w" <(printf "%s\n" "${WORDLIST[@]}"); then
return 1
fi
done
return 0
}
assert_words_file() {
local outfile="$1"
local word_count="$2"
local strict_count="$3"
[ -f "$outfile" ] && return
mkdir -p "$(dirname "$outfile")"
# Extract just the filename (last path component)
filename="$(basename "$outfile")"
# Remove all except extension if any (e.g. .secret)
type="${filename##*.}"
fetch_wordlist
echo "$(echo "$outfile" | sed "s@${HOME}@~@") not found."
echo "Choose:"
echo " (C)reate new ${type} file"
echo " (E)nter existing ${type}"
read -rp "Selection (C or E): " choice
choice=${choice,,}
if [[ "$choice" == c ]]; then
local phrase
phrase=$(generate_words "$word_count")
echo "Generated: $phrase"
echo "${phrase// /-}" > "$outfile"
chmod 600 "$outfile"
elif [[ "$choice" == e ]]; then
while true; do
read -rp "Enter the $word_count words of your $type: " user_input
normalized=$(echo "$user_input" | tr ',' ' ' | xargs | tr ' ' '-')
if validate_words "${normalized//-/ }" "$word_count" "$strict_count"; then
echo "$normalized" > "$outfile"
chmod 600 "$outfile"
break
else
echo "Invalid input: must be $word_count words from the wordlist."
fi
done
else
die "Invalid selection"
fi
}
setup_secret() {
assert_words_file "$SECRET_FILE" 5 true
}
setup_channel() {
assert_words_file "$CHANNEL_FILE" 3 false
}
start_piping_server() {
VERSION="v1.12.9-1"
BASE_URL="https://github.com/nwtgck/piping-server-pkg/releases/download/$VERSION"
CACHE_DIR="${HOME}/.ppng/piping-server"
PORT=8088
OS=$(uname | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$OS" in
linux)
case "$ARCH" in
x86_64) PKG="piping-server-pkg-linuxstatic-x64" ;;
arm64|aarch64) PKG="piping-server-pkg-linuxstatic-arm64" ;;
armv7l) PKG="piping-server-pkg-linuxstatic-armv7" ;;
*) die "Unsupported Linux architecture: $ARCH" ;;
esac
;;
darwin)
case "$ARCH" in
x86_64) PKG="piping-server-pkg-mac-x64" ;;
arm64) PKG="piping-server-pkg-mac-arm64" ;;
*) die "Unsupported macOS architecture: $ARCH" ;;
esac
;;
*)
die "Unsupported OS: $OS"
;;
esac
DEST_DIR="${CACHE_DIR}"
BIN="${DEST_DIR}/${PKG}/piping-server"
if [[ ! -x "$BIN" ]]; then
echo "Downloading piping-server for $OS/$ARCH..."
mkdir -p "$DEST_DIR"
curl -fsSL "${BASE_URL}/${PKG}.tar.gz" | tar -xz -C "$DEST_DIR"
if [[ "$OS" == "darwin" ]]; then
xattr -d com.apple.quarantine "$BIN" 2>/dev/null || true
fi
chmod +x "$BIN"
fi
echo "Starting piping server at http://localhost:$PORT"
exec "$BIN" --http-port=$PORT
}
COMMAND="${1:-}"
TARGET="${2:-}"
setup() {
[ -f "$SECRET_FILE" ] || setup_secret
[ -f "$CHANNEL_FILE" ] || setup_channel
}
case "$COMMAND" in
--help|-h|"")
usage
exit 0
;;
--update)
perform_update
;;
--server)
start_piping_server
;;
put)
setup
PPNG_URL="$(detect_server_url)/$(cat "$CHANNEL_FILE")"
if [[ -n "$TARGET" ]]; then
if [[ -d "$TARGET" || -f "$TARGET" ]]; then
tar -czf - "$TARGET" | openssl aes-256-cbc -e -salt -pbkdf2 -md sha256 -pass file:"$SECRET_FILE" | \
curl -T- "$PPNG_URL"
else
die "No such file or directory: $TARGET"
fi
else
cat - | gzip | openssl aes-256-cbc -e -salt -pbkdf2 -md sha256 -pass file:"$SECRET_FILE" | \
curl --silent -T- "$PPNG_URL"
fi
;;
get)
setup
PPNG_URL="$(detect_server_url)/$(cat "$CHANNEL_FILE")"
if [[ -n "$TARGET" && -d "$TARGET" ]]; then
# If target is an existing directory, extract there
curl "$PPNG_URL" | \
openssl aes-256-cbc -d -salt -pbkdf2 -md sha256 -pass file:"$SECRET_FILE" | \
tar -xzf - -C "$TARGET"
else
# Otherwise output decrypted data to stdout
curl --silent "$PPNG_URL" | \
openssl aes-256-cbc -d -salt -pbkdf2 -md sha256 -pass file:"$SECRET_FILE" | zcat
fi
;;
*)
die "Unknown command: $COMMAND"
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment