summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2019-10-13 01:40:18 -0400
committerAndrew Cady <d@jerkface.net>2019-10-13 01:40:18 -0400
commit65d3fe442c35959f119604afda7b74d772ec5f98 (patch)
tree63986f0c9870869b1e379a6deed01c40a687b3ae /src
parent7cae2ef1c7760573d76d2a48874cfd118350042e (diff)
samizdat-ssh-command
Diffstat (limited to 'src')
-rwxr-xr-xsrc/samizdat-ssh-command317
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
2authorize()
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}
17add_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
25password_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
59die() { echo "Error: $*" >&2; exit 1; }
60
61dequote()
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
67homedir_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
73homedir_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
90initialize_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
103is_gitdir() { git rev-parse --resolve-git-dir "$1" >/dev/null 2>&1; }
104
105deny() { echo 'Error: permission denied.' >&2; exit 1; }
106
107valid_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
144check_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}
148ssh_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
157is_public_repository()
158{
159 case "$git_dir" in
160 */../*) false;;
161 "$HOME"/public_git/*) true;;
162 *) false;;
163 esac
164}
165
166authorized()
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
175maybe_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
187if [ "$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
206fi
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
218USE_MD5_SIG=
219if [ "$USE_MD5_SIG" ]; then
220 SSH_REMOTE_FINGERPRINT_TRIMMED=$(echo $SSH_REMOTE_FINGERPRINT|tr -d :)
221else
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')
224fi
225
226# TODO: call password_authentication on all authorization failures
227
228#echo "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" >&2
229case "$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 ;;
274esac
275
276if [ "$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
292elif [ "$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
302fi
303
304if 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"
314else
315 password_authentication
316 echo 'Error: git access is unauthorized' >&2; exit 1 # unreached
317fi