#!/bin/dash default_command="$HOME/samizdat-default-command" if ! [ "$default_command" ] || ! [ -e "$default_command" ] then default_command=password_authentication fi authorize() { local authtype authdata forced_command comment authorized_keys_line [ "$SSH_USER_AUTH" -a -f "$SSH_USER_AUTH" ] || return read authtype authdata < "$SSH_USER_AUTH" || return [ "$authtype" = publickey ] || return forced_command="command=\"${0} authorize-full-access\",no-port-forwarding" comment="samizdat: password-authenticated from ${SSH_CONNECTION%% *}" authorized_keys_line="$forced_command $authdata $comment" sentinel='Samizdat - YES WE CAN' su - "$USER" -c 'mkdir -p "$HOME"/.ssh; touch "$HOME"/.ssh/authorized_keys' && add_before_sentinel "$sentinel" \ "$authorized_keys_line" \ "$HOME"/.ssh/authorized_keys } add_before_sentinel() { local sentinel="$1" add_me="$2" target="$3" sed -i.samizdat~ \ -e "/$sentinel/i $add_me" \ "$target" } password_authentication() { [ "$USER" ] || { echo 'Error: no $USER' >&2; exit 1; } [ "$SSH_CLIENT_DOMAIN" ] || { echo 'Error: no $SSH_CLIENT_DOMAIN' >&2; exit 1; } tty=$(tty) && [ "$tty" != 'not a tty' ] || tty= if [ "$SSH_ORIGINAL_COMMAND" ]; then msg='You are not authorized to execute the command: %s\n' if ! [ "$tty" ]; then msg="$msg"'To authorize your public key via password, reconnect without a command, or with a terminal attached.\n' msg="$msg"'To attach a terminal, use the "-t" option to ssh.\n' else msg="$msg"'To authorize your public key and execute the command, enter your password.\n' fi printf "\n$msg\n" "$SSH_ORIGINAL_COMMAND" >&2 if ! [ "$tty" ]; then exit 1 fi else msg='You are not authorized to log in.\n' msg="$msg"'To authorize your public key and log in, enter your password.\n' printf "\n$msg\n" >&2 fi authorize || exit $? # TODO: blacklist after too many authentication failures if [ "$SSH_ORIGINAL_COMMAND" ]; then exec "$SSH_ORIGINAL_COMMAND" else exec $(getent passwd "$USER"|cut -d: -f7) -i fi exit $? # exec failed } die() { echo "Error: $*" >&2; exit 1; } dequote() { # Sorry about the slashes. The perl would be: s{ \\(.) | '([^']+)' }{ $1$2 }gx git_dir=$(echo -n "$git_dir" | sed -e "s/\\\\\\(.\\)\\|'\\([^']\\+\\)'/\\1\\2/g") } homedir_expand() { git_dir=$(homedir_expand_arg "$git_dir") && [ "$git_dir" ] || die "Could not expand home directory. HOME=$HOME, USER=$USER, id=$(id)" } homedir_expand_arg() { [ "$HOME" ] || die '$HOME is not set.' case "$1" in \~) echo "$HOME";; \~/*) echo "${HOME}${1#\~}";; \~*) local u u=${1#\~} u=${u%%/*} u=$(getent passwd "$u"|cut -f6 -d:) && [ "$u" ] || return 1 echo "$u/${1#*/}";; /*) echo "$1";; *) echo "$HOME/$1";; esac } initialize_git() { local git_dir="$1" anonymous="$2" if ! [ -e "$git_dir" ]; then mkdir -p "$(dirname "$git_dir")" git init --bare "$git_dir" >&2 if [ "$anonymous" ]; then git --git-dir "$git_dir" config samizdat.allow-anonymous-access true git --git-dir "$git_dir" config samizdat.anonymous-ssh-owner "$anonymous" fi fi } is_gitdir() { git rev-parse --resolve-git-dir "$1" >/dev/null 2>&1; } deny() { echo 'Error: permission denied.' >&2; exit 1; } valid_new_public_repo() { local git_dir="$1" [ ! -e "$git_dir" ] || return [ "$HOME" -a -d "$HOME"/public_git ] || return local dirname="$(dirname "$git_dir")" case "$git_dir" in *.git) ;; *) echo 'Error: public repos must be named *.git' >&2 return 1 ;; esac case "$dirname" in $HOME/public_git) return 0 ;; $HOME/public_git/*) # Ensure that no parent directory is named *.git # Also enforce a maximum depth of 4. # Valid: public_git/a/b/c/d.git # Invalid: public_git/a/b/c/d/e.git local n relative="${git_dir#$HOME/public_git/}" for n in 1 2 3 4; do local topmost="${relative%%/*}" case "$topmost" in "$relative") return 0;; *.git) return 1;; esac relative=${relative#$topmost/} done echo 'Error: directories nest too deep' >&2 return 1 ;; *) return 1 ;; esac } check_if_ssh_user_owns_repository() { git --git-dir "$git_dir" config --get-all samizdat.anonymous-ssh-owner | grep -xqF "$SSH_CLIENT_DOMAIN" } ssh_user_owns_repository() { if [ -z "$SSH_USER_OWNS_REPOSITORY" ]; then check_if_ssh_user_owns_repository SSH_USER_OWNS_REPOSITORY=$? fi return $SSH_USER_OWNS_REPOSITORY } is_public_repository() { case "$git_dir" in */../*) false;; "$HOME"/public_git/*) true;; *) false;; esac } authorized() { # TODO: check SSH_CLIENT_DOMAIN against a blacklist ssh_user_owns_repository && return is_public_repository && return test "$(git --git-dir "$1" config --bool --get samizdat.allow-anonymous-access)" = true 2>/dev/null && return 0 # TODO: check SSH_CLIENT_DOMAIN against a whitelist } maybe_initialize_heads() { [ "$GIT_NAMESPACE" ] || die 'Programmer error' local head heads heads=$git_dir/refs/namespaces/$GIT_NAMESPACE/refs/heads head=$git_dir/refs/namespaces/$GIT_NAMESPACE/HEAD mkdir -p "$heads" || die 'failed to mkdir $heads' [ -e "$head" ] || cp -T "$git_dir"/HEAD "$head" } if [ "$1" = "authorize-full-access" ]; then case "$SSH_ORIGINAL_COMMAND" in git-receive-pack\ *) git_cmd=git-receive-pack git_dir="${SSH_ORIGINAL_COMMAND#git-receive-pack }" dequote homedir_expand initialize_git "$git_dir" exec "$git_cmd" "$git_dir" ;; "") shell=$(getent passwd $USER|cut -d: -f7) argv0=-${shell##*/} if which chpst >/dev/null 2>&1; then exec chpst -b "$argv0" "$shell" else exec "$shell" fi ;; *) exec /bin/sh -c "$SSH_ORIGINAL_COMMAND" ;; esac exit fi eval "$(samizdat-ssh-uid)" || die eval if [ $# -gt 0 ] then exec "$@" exit fi # TODO: call password_authentication on all authorization failures #echo "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" >&2 case "$SSH_ORIGINAL_COMMAND" in git-upload-pack\ *|git-receive-pack\ *) # set three variables # 1. git_cmd # 2. git_dir # 3. git_ns (optional) git_cmd=${SSH_ORIGINAL_COMMAND%%\ *} git_dir=${SSH_ORIGINAL_COMMAND#*\ } dequote homedir_expand case "$git_dir" in $HOME/????????????????????????????????????????????????.cryptonomic.net/public_git/*) IFS=/ set -- "${git_dir#$HOME}" git_ns_subdir=${git_dir#$HOME/} git_ns=${git_ns_subdir%%/*} git_dir=$HOME/${git_ns_subdir#$git_ns/} ;; $HOME/git_namespace/*/public_git/*) git_ns_subdir=${git_dir#$HOME/git_namespace/} git_ns=${git_ns_subdir%%/*} git_dir=$HOME/${git_ns_subdir#*/} ;; esac ;; rsync\ --server\ --sender\ -de.LsfxC\ .\ public_git/|rsync\ --server\ --sender\ -de.LsfxC\ .\ public_git/|rsync\ --server\ --sender\ -de.Lsf\ .\ public_git/) #echo "$SSH_ORIGINAL_COMMAND" >&2 [ -d "$HOME"/public_git ] || { password_authentication; exit 1; } exec $SSH_ORIGINAL_COMMAND #exec rrsync -ro "$HOME"/public_git exit 1 ;; rsync\ --server\ --sender\ *) #echo "$SSH_ORIGINAL_COMMAND" >&2 [ -d "$HOME"/public_rsync ] || { password_authentication; exit 1; } exec rrsync -ro "$HOME"/public_rsync exit 1 ;; rsync\ --server\ *) [ -d "$HOME"/incoming_rsync -a "${SSH_CLIENT_DOMAIN}" ] || { password_authentication; exit 1; } destdir=$HOME/incoming_rsync/$SSH_CLIENT_DOMAIN/ mkdir -p "$destdir" && exec rrsync "$destdir" exit 1 ;; *) #password_authentication $default_command exit ;; esac if [ "$git_cmd" = 'git-upload-pack' ]; then case "$git_dir" in $HOME/public_git/*|public_git/*) is_gitdir "$git_dir" || git_dir="$git_dir/.git" if ! is_gitdir "$git_dir"; then # git rev-parse --resolve-git-dir "${git_dir%/.git}" # show git's error message deny fi if [ "$git_ns" -a -e "$git_dir/refs/namespaces/$git_ns" ]; then export GIT_NAMESPACE="$git_ns" maybe_initialize_heads fi exec "$git_cmd" "$git_dir" ;; esac elif [ "$git_cmd" = 'git-receive-pack' ]; then if [ ! -d "$git_dir" ]; then if valid_new_public_repo "$git_dir"; then initialize_git "$git_dir" "$SSH_CLIENT_DOMAIN" else deny fi fi fi if authorized "$git_dir"; then if [ "$git_cmd" = 'git-receive-pack' ]; then if ! ssh_user_owns_repository then export GIT_NAMESPACE="$SSH_CLIENT_DOMAIN" maybe_initialize_heads printf '%s:%s\n' 'd@cryptonomic.net' "$GIT_NAMESPACE/${git_dir#${HOME}/}" >&2 fi fi exec "$git_cmd" "$git_dir" else $default_command exit # echo 'Error: git access is unauthorized' >&2; exit 1 # unreached fi