forked from progrium/gitreceive
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gitreceive
executable file
·181 lines (159 loc) · 6.36 KB
/
gitreceive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/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" <<EOF
#!/bin/bash
#URL=http://requestb.in/rlh4znrl
#echo "----> 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" <<EOF
#!/bin/bash
cat | $self hook
EOF
chmod +x "$hook_path"
cd - > /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 <username>
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 <command> [options]"
;;
esac
}
[[ "$0" == "$BASH_SOURCE" ]] && main $@