diff options
Diffstat (limited to 'src/samizdat-ssh-command')
-rwxr-xr-x | src/samizdat-ssh-command | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/src/samizdat-ssh-command b/src/samizdat-ssh-command new file mode 100755 index 0000000..94679f2 --- /dev/null +++ b/src/samizdat-ssh-command | |||
@@ -0,0 +1,317 @@ | |||
1 | #!/bin/dash | ||
2 | authorize() | ||
3 | { | ||
4 | local pkey line authorized_keys_line cmd | ||
5 | [ "$SSH_USER_AUTH" -a -f "$SSH_USER_AUTH" ] || return | ||
6 | read pkey line < "$SSH_USER_AUTH" || return | ||
7 | [ "$pkey" = publickey ] || return | ||
8 | |||
9 | authorized_keys_line="$line samizdat: password-authenticated from ${SSH_CONNECTION%% *}" | ||
10 | sentinel='Samizdat - YES WE CAN' | ||
11 | |||
12 | su - "$USER" -c 'mkdir -p "$HOME"/.ssh; touch "$HOME"/.ssh/authorized_keys' && | ||
13 | add_before_sentinel "$sentinel" \ | ||
14 | "$authorized_keys_line" \ | ||
15 | "$HOME"/.ssh/authorized_keys | ||
16 | } | ||
17 | add_before_sentinel() | ||
18 | { | ||
19 | local sentinel="$1" add_me="$2" target="$3" | ||
20 | sed -i.samizdat~ \ | ||
21 | -e "/$sentinel/i $add_me" \ | ||
22 | "$target" | ||
23 | } | ||
24 | |||
25 | password_authentication() | ||
26 | { | ||
27 | [ "$USER" ] || { echo 'Error: no $USER' >&2; exit 1; } | ||
28 | [ "$SSH_REMOTE_KEY" ] || { echo 'Error: no $SSH_REMOTE_KEY' >&2; exit 1; } | ||
29 | |||
30 | tty=$(tty) && [ "$tty" != 'not a tty' ] || tty= | ||
31 | |||
32 | if [ "$SSH_ORIGINAL_COMMAND" ]; then | ||
33 | msg='You are not authorized to execute the command: %s\n' | ||
34 | if ! [ "$tty" ]; then | ||
35 | msg="$msg"'To authorize your public key via password, reconnect without a command, or with a terminal attached.\n' | ||
36 | msg="$msg"'To attach a terminal, use the "-t" option to ssh.\n' | ||
37 | else | ||
38 | msg="$msg"'To authorize your public key and execute the command, enter your password.\n' | ||
39 | fi | ||
40 | printf "\n$msg\n" "$SSH_ORIGINAL_COMMAND" >&2 | ||
41 | if ! [ "$tty" ]; then | ||
42 | exit 1 | ||
43 | fi | ||
44 | else | ||
45 | msg='You are not authorized to log in.\n' | ||
46 | msg="$msg"'To authorize your public key and log in, enter your password.\n' | ||
47 | printf "\n$msg\n" >&2 | ||
48 | fi | ||
49 | authorize || exit $? | ||
50 | # TODO: blacklist after too many authentication failures | ||
51 | if [ "$SSH_ORIGINAL_COMMAND" ]; then | ||
52 | exec "$SSH_ORIGINAL_COMMAND" | ||
53 | else | ||
54 | exec $(getent passwd "$USER"|cut -d: -f7) -i | ||
55 | fi | ||
56 | exit $? # exec failed | ||
57 | } | ||
58 | |||
59 | die() { echo "Error: $*" >&2; exit 1; } | ||
60 | |||
61 | dequote() | ||
62 | { | ||
63 | # Sorry about the slashes. The perl would be: s{ \\(.) | '([^']+)' }{ $1$2 }gx | ||
64 | git_dir=$(echo -n "$git_dir" | sed -e "s/\\\\\\(.\\)\\|'\\([^']\\+\\)'/\\1\\2/g") | ||
65 | } | ||
66 | |||
67 | homedir_expand() | ||
68 | { | ||
69 | git_dir=$(homedir_expand_arg "$git_dir") && | ||
70 | [ "$git_dir" ] || die "Could not expand home directory. HOME=$HOME, USER=$USER, id=$(id)" | ||
71 | } | ||
72 | |||
73 | homedir_expand_arg() | ||
74 | { | ||
75 | [ "$HOME" ] || die '$HOME is not set.' | ||
76 | case "$1" in | ||
77 | \~) echo "$HOME";; | ||
78 | \~/*) echo "${HOME}${1#\~}";; | ||
79 | \~*) | ||
80 | local u | ||
81 | u=${1#\~} | ||
82 | u=${u%%/*} | ||
83 | u=$(getent passwd "$u"|cut -f6 -d:) && [ "$u" ] || return 1 | ||
84 | echo "$u/${1#*/}";; | ||
85 | /*) echo "$1";; | ||
86 | *) echo "$HOME/$1";; | ||
87 | esac | ||
88 | } | ||
89 | |||
90 | initialize_git() | ||
91 | { | ||
92 | local git_dir="$1" anonymous="$2" | ||
93 | if ! [ -e "$git_dir" ]; then | ||
94 | mkdir -p "$(dirname "$git_dir")" | ||
95 | git init --bare "$git_dir" >&2 | ||
96 | if [ "$anonymous" ]; then | ||
97 | git --git-dir "$git_dir" config samizdat.allow-anonymous-access true | ||
98 | git --git-dir "$git_dir" config samizdat.anonymous-ssh-owner "$anonymous" | ||
99 | fi | ||
100 | fi | ||
101 | } | ||
102 | |||
103 | is_gitdir() { git rev-parse --resolve-git-dir "$1" >/dev/null 2>&1; } | ||
104 | |||
105 | deny() { echo 'Error: permission denied.' >&2; exit 1; } | ||
106 | |||
107 | valid_new_public_repo() | ||
108 | { | ||
109 | local git_dir="$1" | ||
110 | [ ! -e "$git_dir" ] || return | ||
111 | [ "$HOME" -a -d "$HOME"/public_git ] || return | ||
112 | local dirname="$(dirname "$git_dir")" | ||
113 | |||
114 | case "$git_dir" in | ||
115 | *.git) ;; | ||
116 | *) | ||
117 | echo 'Error: public repos must be named *.git' >&2 | ||
118 | return 1 ;; | ||
119 | esac | ||
120 | |||
121 | case "$dirname" in | ||
122 | $HOME/public_git) return 0 ;; | ||
123 | $HOME/public_git/*) | ||
124 | # Ensure that no parent directory is named *.git | ||
125 | # Also enforce a maximum depth of 4. | ||
126 | # Valid: public_git/a/b/c/d.git | ||
127 | # Invalid: public_git/a/b/c/d/e.git | ||
128 | local n relative="${git_dir#$HOME/public_git/}" | ||
129 | for n in 1 2 3 4; do | ||
130 | local topmost="${relative%%/*}" | ||
131 | case "$topmost" in | ||
132 | "$relative") return 0;; | ||
133 | *.git) return 1;; | ||
134 | esac | ||
135 | relative=${relative#$topmost/} | ||
136 | done | ||
137 | echo 'Error: directories nest too deep' >&2 | ||
138 | return 1 | ||
139 | ;; | ||
140 | *) return 1 ;; | ||
141 | esac | ||
142 | } | ||
143 | |||
144 | check_if_ssh_user_owns_repository() | ||
145 | { | ||
146 | git --git-dir "$git_dir" config --get-all samizdat.anonymous-ssh-owner | grep -xqF "$SSH_REMOTE_FINGERPRINT_TRIMMED" | ||
147 | } | ||
148 | ssh_user_owns_repository() | ||
149 | { | ||
150 | if [ -z "$SSH_USER_OWNS_REPOSITORY" ]; then | ||
151 | check_if_ssh_user_owns_repository | ||
152 | SSH_USER_OWNS_REPOSITORY=$? | ||
153 | fi | ||
154 | return $SSH_USER_OWNS_REPOSITORY | ||
155 | } | ||
156 | |||
157 | is_public_repository() | ||
158 | { | ||
159 | case "$git_dir" in | ||
160 | */../*) false;; | ||
161 | "$HOME"/public_git/*) true;; | ||
162 | *) false;; | ||
163 | esac | ||
164 | } | ||
165 | |||
166 | authorized() | ||
167 | { | ||
168 | # TODO: check SSH_REMOTE_FINGERPRINT against a blacklist | ||
169 | ssh_user_owns_repository && return | ||
170 | is_public_repository && return | ||
171 | test "$(git --git-dir "$1" config --bool --get samizdat.allow-anonymous-access)" = true 2>/dev/null && return 0 | ||
172 | # TODO: check SSH_REMOTE_FINGERPRINT against a whitelist | ||
173 | } | ||
174 | |||
175 | maybe_initialize_heads() | ||
176 | { | ||
177 | [ "$GIT_NAMESPACE" ] || die 'Programmer error' | ||
178 | heads=$git_dir/refs/namespaces/$GIT_NAMESPACE/refs/heads | ||
179 | mkdir -p "$heads" | ||
180 | found_file=$(find "$heads" -type f -print -quit) | ||
181 | [ "$found_file" ] && return | ||
182 | [ -e "$git_dir/refs/heads/master" ] && cp "$git_dir/refs/heads/master" "$heads" | ||
183 | # TODO: copy actual file 'HEAD' and whatever it references | ||
184 | } | ||
185 | |||
186 | |||
187 | if [ "$1" = "authorize-full-access" ]; then | ||
188 | case "$SSH_ORIGINAL_COMMAND" in | ||
189 | git-receive-pack\ *) | ||
190 | git_cmd=git-receive-pack | ||
191 | git_dir="${SSH_ORIGINAL_COMMAND#git-receive-pack }" | ||
192 | dequote | ||
193 | homedir_expand | ||
194 | initialize_git "$git_dir" | ||
195 | exec "$git_cmd" "$git_dir" | ||
196 | ;; | ||
197 | "") | ||
198 | shell=$(getent passwd $USER|cut -d: -f7) | ||
199 | argv0=-${shell##*/} | ||
200 | exec chpst -b "$argv0" "$shell" | ||
201 | ;; | ||
202 | *) | ||
203 | exec /bin/sh -c "$SSH_ORIGINAL_COMMAND" | ||
204 | ;; | ||
205 | esac | ||
206 | fi | ||
207 | |||
208 | |||
209 | (exec >&2 | ||
210 | #env | grep '^SSH_' | ||
211 | #cat "${SSH_USER_AUTH}" | ||
212 | #ssh-keygen -l -f "${SSH_USER_AUTH}" | ||
213 | #sed -i -e 's/^publickey //' "${SSH_USER_AUTH}" | ||
214 | #set -x | ||
215 | #ssh-keygen -r . -f "${SSH_USER_AUTH}" | sed -ne 's/^. IN SSHFP [0-9]* 1 /SHA1:/p' | ||
216 | ) | ||
217 | |||
218 | USE_MD5_SIG= | ||
219 | if [ "$USE_MD5_SIG" ]; then | ||
220 | SSH_REMOTE_FINGERPRINT_TRIMMED=$(echo $SSH_REMOTE_FINGERPRINT|tr -d :) | ||
221 | else | ||
222 | sed -i -e 's/^publickey //' "${SSH_USER_AUTH}" || die "error rewriting SSH_USER_AUTH file" | ||
223 | SSH_REMOTE_FINGERPRINT_TRIMMED=$(ssh-keygen -r . -f "${SSH_USER_AUTH}" | sed -ne 's/^. IN SSHFP [0-9]* 1 //p') | ||
224 | fi | ||
225 | |||
226 | # TODO: call password_authentication on all authorization failures | ||
227 | |||
228 | #echo "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" >&2 | ||
229 | case "$SSH_ORIGINAL_COMMAND" in | ||
230 | git-upload-pack\ *|git-receive-pack\ *) | ||
231 | # set three variables | ||
232 | # 1. git_cmd | ||
233 | # 2. git_dir | ||
234 | # 3. git_ns (optional) | ||
235 | |||
236 | git_cmd=${SSH_ORIGINAL_COMMAND%%\ *} | ||
237 | git_dir=${SSH_ORIGINAL_COMMAND#*\ } | ||
238 | |||
239 | dequote | ||
240 | homedir_expand | ||
241 | |||
242 | case "$git_dir" in | ||
243 | $HOME/git_namespace/*/public_git/*) | ||
244 | git_ns_subdir=${git_dir#$HOME/git_namespace/} | ||
245 | git_ns=${git_ns_subdir%%/*} | ||
246 | git_dir=$HOME/${git_ns_subdir#*/} | ||
247 | ;; | ||
248 | esac | ||
249 | |||
250 | ;; | ||
251 | rsync\ --server\ --sender\ -de.LsfxC\ .\ public_git/|rsync\ --server\ --sender\ -de.LsfxC\ .\ public_git/|rsync\ --server\ --sender\ -de.Lsf\ .\ public_git/) | ||
252 | #echo "$SSH_ORIGINAL_COMMAND" >&2 | ||
253 | [ -d "$HOME"/public_git ] || { password_authentication; exit 1; } | ||
254 | exec $SSH_ORIGINAL_COMMAND | ||
255 | #exec rrsync -ro "$HOME"/public_git | ||
256 | exit 1 | ||
257 | ;; | ||
258 | rsync\ --server\ --sender\ *) | ||
259 | #echo "$SSH_ORIGINAL_COMMAND" >&2 | ||
260 | [ -d "$HOME"/public_rsync ] || { password_authentication; exit 1; } | ||
261 | exec rrsync -ro "$HOME"/public_rsync | ||
262 | exit 1 | ||
263 | ;; | ||
264 | rsync\ --server\ *) | ||
265 | [ -d "$HOME"/incoming_rsync -a "${SSH_REMOTE_FINGERPRINT_TRIMMED}" ] || { password_authentication; exit 1; } | ||
266 | destdir=$HOME/incoming_rsync/$SSH_REMOTE_FINGERPRINT_TRIMMED/ | ||
267 | mkdir -p "$destdir" && exec rrsync "$destdir" | ||
268 | exit 1 | ||
269 | ;; | ||
270 | *) | ||
271 | password_authentication | ||
272 | exit 1 # unreached | ||
273 | ;; | ||
274 | esac | ||
275 | |||
276 | if [ "$git_cmd" = 'git-upload-pack' ]; then | ||
277 | case "$git_dir" in | ||
278 | $HOME/public_git/*|public_git/*) | ||
279 | is_gitdir "$git_dir" || git_dir="$git_dir/.git" | ||
280 | if ! is_gitdir "$git_dir"; then | ||
281 | # git rev-parse --resolve-git-dir "${git_dir%/.git}" # show git's error message | ||
282 | deny | ||
283 | fi | ||
284 | if [ "$git_ns" -a -e "$git_dir/refs/namespaces/$git_ns" ]; then | ||
285 | export GIT_NAMESPACE="$git_ns" | ||
286 | # maybe_initialize_heads | ||
287 | fi | ||
288 | exec "$git_cmd" "$git_dir" | ||
289 | ;; | ||
290 | esac | ||
291 | |||
292 | elif [ "$git_cmd" = 'git-receive-pack' ]; then | ||
293 | |||
294 | if [ ! -d "$git_dir" ]; then | ||
295 | if valid_new_public_repo "$git_dir"; then | ||
296 | initialize_git "$git_dir" "$SSH_REMOTE_FINGERPRINT_TRIMMED" | ||
297 | else | ||
298 | deny | ||
299 | fi | ||
300 | fi | ||
301 | |||
302 | fi | ||
303 | |||
304 | if authorized "$git_dir"; then | ||
305 | if [ "$git_cmd" = 'git-receive-pack' ]; then | ||
306 | if ! ssh_user_owns_repository | ||
307 | then | ||
308 | export GIT_NAMESPACE="$SSH_REMOTE_FINGERPRINT_TRIMMED" | ||
309 | maybe_initialize_heads | ||
310 | printf '%s:%s\n' 'd@cryptonomic.net' "git_namespace/$GIT_NAMESPACE/${git_dir#${HOME}/}" >&2 | ||
311 | fi | ||
312 | fi | ||
313 | exec "$git_cmd" "$git_dir" | ||
314 | else | ||
315 | password_authentication | ||
316 | echo 'Error: git access is unauthorized' >&2; exit 1 # unreached | ||
317 | fi | ||