#!/bin/sh # --------------------------------------------------------------------------- # tilde - manage user-submitted scripts and apps # Copyright 2018, Ben Harris # # GPL-3.0-or-later. See LICENSE for the full licence text. # --------------------------------------------------------------------------- set -u umask 077 # Preserve users' PATH, especially for "tilde run". Sanitize only root commands. if [ "$(/usr/bin/id -u)" -eq 0 ]; then PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export PATH fi PROGNAME=${0##*/} VERSION=0.2.0 ROOT=/tilde BIN=$ROOT/bin DESCRIPTIONS=$ROOT/descriptions PENDING=$ROOT/pending-submissions APPROVED=$ROOT/approved META=$ROOT/metadata REVIEW=$ROOT/review REJECTED=$ROOT/rejected LOCK=$ROOT/.launcher.lock MAX_SCRIPT=52428800 MAX_DESCRIPTION=16384 WORK= fail() { printf '%s: %s\n' "$PROGNAME" "$1" >&2 exit 1 } warn() { printf '%s: %s\n' "$PROGNAME" "$1" >&2 } cleanup() { if [ -n "$WORK" ]; then rm -rf -- "$WORK" WORK= fi } trap 'cleanup' EXIT trap 'exit 130' INT trap 'exit 143' TERM HUP require_root() { [ "$(id -u)" -eq 0 ] || fail 'this command requires root' } valid_name() { case ${1-} in ''|.*|*[!a-zA-Z0-9_.+-]*) return 1 ;; *) return 0 ;; esac } check_name() { valid_name "${1-}" || fail 'invalid script name (use letters, numbers, _, -, + or .; not starting with .)' case $1 in tilde|about|description|list|ls|submit|update|approve|revoke|run|help|apropos|pending|status|migrate|init|version) fail "$1 is a reserved name" ;; esac } valid_user() { case ${1-} in ''|.*|*[!a-zA-Z0-9_.-]*) return 1 ;; *) return 0 ;; esac } # Never trust pre-existing filesystem entries when operating as root. trusted_dir() { [ -d "$1" ] && [ ! -L "$1" ] || fail "not a directory: $1" [ "$(stat -c %u -- "$1")" = 0 ] || fail "directory must be owned by root: $1" mode=$(stat -c %a -- "$1") || fail "cannot stat $1" [ $((0$mode & 0022)) -eq 0 ] || fail "directory is writable by group/others: $1" } storage_ready() { trusted_dir "$ROOT" trusted_dir "$BIN" trusted_dir "$DESCRIPTIONS" trusted_dir "$APPROVED" trusted_dir "$META" trusted_dir "$REJECTED" trusted_dir "$REVIEW" [ $((0$(stat -c %a -- "$REVIEW") & 0077)) -eq 0 ] || fail 'review directory must be private (0700)' [ -d "$PENDING" ] && [ ! -L "$PENDING" ] || fail "invalid pending directory" [ "$(stat -c %u -- "$PENDING")" = 0 ] || fail "pending directory must be root-owned" # The pending parent must be sticky when it is publicly writable. mode=$(stat -c %a -- "$PENDING") || fail 'cannot stat pending directory' [ $((0$mode & 01000)) -ne 0 ] || fail 'pending directory needs the sticky bit' } init_storage() { require_root for dir in "$ROOT" "$BIN" "$DESCRIPTIONS" "$APPROVED" "$META" "$REJECTED" "$REVIEW" "$PENDING"; do [ ! -L "$dir" ] || fail "refusing symlink: $dir" if [ -e "$dir" ]; then [ -d "$dir" ] || fail "not a directory: $dir" if [ "$dir" = "$PENDING" ]; then [ "$(stat -c %u -- "$dir")" = 0 ] || fail "directory must be root-owned: $dir" else trusted_dir "$dir" fi else mkdir -m 0755 -- "$dir" || fail "cannot create $dir" fi done chmod 0700 "$REVIEW" || fail 'cannot secure review directory' chmod 1733 "$PENDING" || fail 'cannot secure pending directory' storage_ready printf 'Launcher directories are ready. Existing scripts were not changed.\n' } admin_lock() { require_root storage_ready [ ! -L "$LOCK" ] || fail 'lock file must not be a symlink' (umask 077; : >> "$LOCK") || fail 'cannot open lock file' [ "$(stat -c %u -- "$LOCK")" = 0 ] || fail 'lock file must be root-owned' exec 9>"$LOCK" || fail 'cannot lock launcher' flock -n 9 || fail 'another launcher administration is in progress' } prompt() { while :; do printf '%s [y/n]: ' "$1" IFS= read -r answer || return 1 case $answer in y|Y) return 0 ;; n|N) return 1 ;; *) printf 'Please answer y or n.\n' ;; esac done } ask_line() { printf '%s' "$1" IFS= read -r INPUT || fail 'input cancelled' } help_message() { cat < Show description and author (alias: description) submit Submit a new script from ~/bin update