Last active
May 22, 2025 16:21
-
-
Save EleotleCram/c0e6164fedb8dce3b885d3f26be7fd90 to your computer and use it in GitHub Desktop.
ppng.io - Securely transfer files or directories using a piping server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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