#!/bin/bash readonly GITUSER="${GITUSER:-git}" readonly GITHOME="/home/$GITUSER" # Given a relative path, calculate the absolute path absolute_path() { pushd "$(dirname $1)" > /dev/null local abspath="$(pwd -P)" popd > /dev/null echo "$abspath/$(basename $1)" } # Create a Git user on the system, with home directory and an `.authorized_keys' file that contains the public keys # for all users that are allowed to push their repos here. User defaults to $GITUSER, which defaults to 'git'. setup_git_user() { declare home_dir="$1" git_user="$2" useradd -d "$home_dir" "$git_user" || true mkdir -p "$home_dir/.ssh" touch "$home_dir/.ssh/authorized_keys" chown -R "$git_user" "$home_dir" } # Creates a sample receiver script. This is the script that is triggered after a successful push. setup_receiver_script() { declare home_dir="$1" git_user="$2" local receiver_path="$home_dir/receiver" cat > "$receiver_path" < Posting to \$URL ..." #curl \\ # -X 'POST' \\ # -F "repository=\$1" \\ # -F "revision=\$2" \\ # -F "username=\$3" \\ # -F "fingerprint=\$4" \\ # -F contents=@- \\ # --silent \$URL EOF chmod +x "$receiver_path" chown "$git_user" "$receiver_path" } # Generate a shorter, but still unique, version of the public key associated with the user doing `git push' generate_fingerprint() { awk '{print $2}' | base64 -d | md5sum | awk '{print $1}' | sed -e 's/../:&/2g' } # Given a public key, add it to the .authorized_keys file with a 'forced command'. The 'forced command' is a syntax # specific to SSH's `.authorized_keys' file that allows you to specify a command that is run as soon as a user logs in. # Note that even though `git push' does not explicitly mention SSH, it is nevertheless using the SSH protocol under the # hood. # See: http://man.finalrewind.org/1/ssh-forcecommand/ install_authorized_key() { declare key="$1" name="$2" home_dir="$3" git_user="$4" self="$5" local fingerprint="$(echo "$key" | generate_fingerprint)" local forced_command="GITUSER=$git_user $self run $name $fingerprint" local key_options="command=\"$forced_command\",no-agent-forwarding,no-pty,no-user-rc,no-X11-forwarding,no-port-forwarding" echo "$key_options $key" >> "$home_dir/.ssh/authorized_keys" } # Remove the slash from the beginning of a path. Eg; '/twbs/bootstrap' becomes 'twbs/bootstrap' strip_root_slash() { local str="$(cat)" if [ "${str:0:1}" == "/" ]; then echo "$str" | cut -c 2- else echo "$str" fi } # Get the repo from the incoming SSH command. This is needed as the original intended response to `git push' is # overridden by the use of a 'forced command' (see install_authorized_key()). The forced command needs to know what repo # to act on. parse_repo_from_ssh_command() { awk '{print $2}' | sed -e "s/'\(.*\)'/\1/" | sed 's/\\'\''/'\''/g' | strip_root_slash } # Create a git-enabled folder ready to receive git activity, like `git push' ensure_bare_repo() { declare repo_path="$1" if [ ! -d "$repo_path" ]; then mkdir -p "$repo_path" cd "$repo_path" git init --bare > /dev/null cd - > /dev/null fi } # Create a Git pre-receive hook in a git repo that runs `gitreceive hook' when the repo receives a new git push ensure_prereceive_hook() { declare repo_path="$1" home_dir="$2" self="$3" local hook_path="$repo_path/hooks/pre-receive" cd "$home_dir" cat > "$hook_path" < /dev/null } # When a repo receives a push, its pre-receive hook is triggered. This in turn executes `gitreceive hook', which is a # wrapper around this function. The repo is updated and its working tree tarred so that it can be piped to # `$home_dir/receiver'. The receiver script is setup by `setup_receiver_script()'. trigger_receiver() { declare repo="$1" user="$2" fingerprint="$3" home_dir="$4" # oldrev, newrev, refname are a feature of the way in which Git executes the pre-receive hook. # See https://www.kernel.org/pub/software/scm/git/docs/githooks.html while read oldrev newrev refname; do # Only run this script for the master branch. You can remove this # if block if you wish to run it for others as well. [[ "$refname" == "refs/heads/master" ]] && \ git archive "$newrev" | "$home_dir/receiver" "$repo" "$newrev" "$user" "$fingerprint" done } # Places cursor at start of line, so that subsequent text replaces existing text. For example; # "remote: Updated branch 'master' of 'repo'. Deploying to dev." becomes # "------> Updated branch 'master' of 'repo'. Deploying to dev." strip_remote_prefix() { sed -u "s/^/"$'\e[1G'"/" } main() { # Be unforgiving about errors set -euo pipefail readonly SELF="$(absolute_path $0)" case "$1" in # Public commands init) # gitreceive init setup_git_user "$GITHOME" "$GITUSER" setup_receiver_script "$GITHOME" "$GITUSER" echo "Created receiver script in $GITHOME for user '$GITUSER'." ;; upload-key) # sudo gitreceive upload-key declare name="$2" local key="$(cat)" install_authorized_key "$key" "$name" "$GITHOME" "$GITUSER" "$SELF" echo "$key" | generate_fingerprint ;; # Internal commands # Called by the 'forced command' when the git user first authenticates against the server run) declare user="$2" fingerprint="$3" export RECEIVE_USER="$user" export RECEIVE_FINGERPRINT="$fingerprint" export RECEIVE_REPO="$(echo "$SSH_ORIGINAL_COMMAND" | parse_repo_from_ssh_command)" if [ ! $RECEIVE_REPO ]; then echo "ERROR: Arbitrary ssh prohibited!" exit 1 fi local repo_path="$GITHOME/$RECEIVE_REPO" ensure_bare_repo "$repo_path" ensure_prereceive_hook "$repo_path" "$GITHOME" "$SELF" cd "$GITHOME" # $SSH_ORIGINAL_COMMAND is set by `sshd'. It stores the originally intended command to be run by `git push'. In # our case it is overridden by the 'forced command', so we need to reinstate it now that the 'forced command' has # run. git-shell -c "$(echo "$SSH_ORIGINAL_COMMAND" | awk '{print $1}') '$RECEIVE_REPO'" ;; # Called by the pre-receive hook hook) trigger_receiver "$RECEIVE_REPO" "$RECEIVE_USER" "$RECEIVE_FINGERPRINT" "$GITHOME" | strip_remote_prefix ;; *) echo "Usage: gitreceive [options]" ;; esac } [[ "$0" == "$BASH_SOURCE" ]] && main $@