htaccess: split and update for relocated cgiroot
[girocco.git] / bin / git-http-backend-verify
blob989743edcdfc9c898829c3cd2caa5fc733990e3e
1 #!/bin/sh
3 # Abort any push early if the pushing user doesn't have any push permissions
4 # at all. This avoids unnecessary traffic and unpacked object pollution.
6 # Set GIT_HTTP_BACKEND_BIN to change the default http-backend binary from
7 # the default of Config.pm $git_http_backend_bin (which itself has a default
8 # of "/usr/lib/git-core/git-http-backend")
10 # Note that GIT_PROJECT_ROOT is automatically set to $cfg_reporoot and exported
11 # any incoming value for it will be ignored.
13 # Note that GIT_HTTP_EXPORT_ALL is automatically set to 1 and exported.
15 # Also prevents standard error output from git-http-backend cluttering up the
16 # server's log unless GIT_HTTP_BACKEND_SHOW_ERRORS is set to a non-empty value.
18 # Bundle fetches are handled in this script as well.
20 set -e
22 . @basedir@/shlib.sh
24 unset GIT_USER_AGENT
25 unset GIT_HTTP_USER_AGENT
26 if [ -n "$defined_cfg_git_server_ua" ]; then
27 GIT_USER_AGENT="$cfg_git_server_ua"
28 export GIT_USER_AGENT
29 GIT_HTTP_USER_AGENT="$cfg_git_server_ua"
30 export GIT_HTTP_USER_AGENT
33 [ -z "$GIT_HTTP_BACKEND_BIN" ] || cfg_git_http_backend_bin="$GIT_HTTP_BACKEND_BIN"
34 [ -n "$cfg_git_http_backend_bin" ] ||
35 cfg_git_http_backend_bin="$var_git_exec_path/git-http-backend"
37 GIT_PROJECT_ROOT="$cfg_reporoot"
38 GIT_HTTP_EXPORT_ALL=1
39 export GIT_PROJECT_ROOT
40 export GIT_HTTP_EXPORT_ALL
42 # This script is called for both fetch and push.
43 # Only the following conditions trigger a push permissions check:
45 # 1. REQUEST_METHOD=GET
46 # and PATH_INFO ends with "/info/refs"
47 # and QUERY_STRING has "service=git-receive-pack"
49 # 2. REQUEST_METHOD=POST
50 # and PATH_INFO ends with "/git-receive-pack"
52 # Note that there is no check for PATH_INFO being under a certain root as
53 # GIT_PROJECT_ROOT will be exported and set so all PATH_INFO values are
54 # effectively forced under the desired root.
56 # The REQUEST_METHOD is validated. For smart HTTP requests only the project
57 # name is extracted and validated and the corresponding project directory must
58 # exist under $cfg_reporoot. Non-smart HTTP fetch requests (GET or HEAD) are
59 # passed on unchanged and unchecked.
61 errorhdrsct()
63 _ct="$1"; shift
64 printf '%s\r\n' "Status: $1 $2"
65 printf '%s\r\n' "Expires: Fri, 01 Jan 1980 00:00:00 GMT"
66 printf '%s\r\n' "Pragma: no-cache"
67 printf '%s\r\n' "Cache-Control: no-cache,max-age=0,must-revalidate"
68 [ -z "$3" ] || printf '%s\r\n' "$3"
69 printf '%s\r\n' "Content-Type: $_ct"
70 printf '\r\n'
73 errorhdrs()
75 errorhdrsct 'text/plain' "$@"
78 msglines()
80 while [ $# -gt 0 ]; do
81 printf '%s\n' "$1"
82 shift
83 done
86 internalerr()
88 errorhdrs 500 "Internal Server Error"
89 if [ $# -eq 0 ]; then
90 msglines "Internal Server Error"
91 echo "Internal Server Error" >&2
92 else
93 msglines "$@"
94 while [ $# -gt 0 ]; do
95 printf '%s\n' "$1" >&2
96 shift
97 done
99 exit 0
102 methodnotallowed()
104 errorhdrs 405 "Method Not Allowed" "Allow: GET,HEAD,POST"
105 if [ $# -eq 0 ]; then
106 msglines "Method Not Allowed"
107 else
108 msglines "$@"
110 exit 0
113 forbidden()
115 errorhdrs 403 Forbidden
116 if [ $# -eq 0 ]; then
117 msglines "Forbidden"
118 else
119 msglines "$@"
121 exit 0
124 notfound()
126 errorhdrs 404 "Not Found"
127 if [ $# -eq 0 ]; then
128 msglines "Not Found"
129 else
130 msglines "$@"
132 exit 0
135 needsauth()
137 errorhdrs 401 "Authorization Required"
138 if [ $# -eq 0 ]; then
139 msglines "Authorization Required"
140 else
141 msglines "$@"
143 exit 0
146 # Single argument is an absolute PATH (NOT a URI) to 302 redirect to
147 # The appropriate http pull URL path prefix is automatically inserted
148 redir()
150 _pullurl="$cfg_httpbundleurl"
151 [ -n "$_pullurl" ] || _pullurl="$cfg_httpspushurl"
152 [ -n "$_pullurl" ] || _pullurl="$cfg_httppullurl"
153 _absbase="${_pullurl%/}/"
154 _absbase="${absbase##*://}"
155 _absbase="${absbase##*/}"
156 [ -z "$_absbase" ] || _absbase="/$_absbase"
157 _loc="https"
158 [ "$HTTPS" = "on" ] || _loc="http"
159 _loc="$_loc://$SERVER_NAME"
160 [ "$HTTPS" != "on" -o "$SERVER_PORT" = "443" ] || _loc="$_loc:$SERVER_PORT"
161 [ "$HTTPS" = "on" -o "$SERVER_PORT" = "80" ] || _loc="$_loc:$SERVER_PORT"
162 _loc="$_loc$_absbase"
163 case "$1" in /*) :;; *) _loc="$_loc/";; esac
164 _loc="$_loc$1"
165 errorhdrsct 'text/html' 302 "Found" "Location: $_loc"
166 if [ "$REQUEST_METHOD" != "HEAD" ]; then
167 printf '<p>Temporarily redirected to <a href="%s">%s</a></p>\n' \
168 "$_loc" "$_loc"
170 exit 0
173 # A quick sanity check
174 if [ -z "$cfg_git_http_backend_bin" ] || ! [ -x "$cfg_git_http_backend_bin" ]; then
175 internalerr "bad cfg_git_http_backend_bin: $cfg_git_http_backend_bin"
176 exit 1
178 case "$cfg_reporoot" in /?*) :;; *)
179 internalerr "bad reporoot: $cfg_reporoot"
180 exit 1
181 esac
182 [ -n "$GIT_PROJECT_ROOT" ] || { internalerr 'GIT_PROJECT_ROOT must be set'; exit 1; }
184 PATH="$(dirname "$cfg_git_http_backend_bin"):$PATH"
185 export PATH
187 digit='[0-9]'
188 digit6="$digit$digit$digit$digit$digit$digit"
189 digit8="$digit6$digit$digit"
190 proj=
191 smart=
192 bundle=
193 suffix=
194 needsauthcheck=
195 pathcheck="${PATH_INFO#/}"
196 if [ "$REQUEST_METHOD" = "GET" -o "$REQUEST_METHOD" = "HEAD" ]; then
197 # We do not currently validate non-smart GET/HEAD requests.
198 # There are only 8 possible suffix values that need to be allowed for
199 # non-smart HTTP GET/HEAD fetches (see http-backend.c):
200 # /HEAD
201 # /info/refs
202 # /objects/info/alternates
203 # /objects/info/http-alternates
204 # /objects/info/packs
205 # /objects/[0-9a-f]{2}/[0-9a-f]{38}
206 # /objects/pack/pack-[0-9a-f]{40}.idx
207 # /objects/pack/pack-[0-9a-f]{40}.pack
208 # We do, however, need to recognize a /*.bundle fetch so that
209 # we can properly handle it.
210 case "$pathcheck" in
211 *"/info/refs")
212 proj="${pathcheck%/info/refs}"
213 case "&$QUERY_STRING&" in
214 *"&service=git-receive-pack&"*)
215 smart=1
216 needsauthcheck=1
217 suffix=info/refs
219 *"&service=git-upload-pack&"*)
220 smart=1
221 suffix=info/refs
223 esac
225 */*[!./].bundle)
226 bundle=1
227 smart=1
228 proj="${pathcheck%/*.bundle}"
229 suffix="${pathcheck#$proj/}"
231 esac
232 elif [ "$REQUEST_METHOD" = "POST" ]; then
233 case "$pathcheck" in
234 *"/git-receive-pack")
235 smart=1
236 needsauthcheck=1
237 proj="${pathcheck%/git-receive-pack}"
238 suffix=git-receive-pack
240 *"/git-upload-pack")
241 smart=1
242 proj="${pathcheck%/git-upload-pack}"
243 suffix=git-upload-pack
246 forbidden
247 exit 1
249 esac
250 else
251 methodnotallowed
252 exit 1
255 # Reject any project names that start with _ or contain ..
256 case "$pathcheck" in _*|*..*)
257 forbidden
258 esac
260 if [ -n "$smart" ]; then
261 # add a missing trailing .git
262 case "$proj" in
263 *.git) :;;
265 proj="$proj.git"
266 esac
268 projbare="${proj%.git}"
269 reporoot="$cfg_reporoot"
270 dir="$reporoot/$proj"
272 # Valid project names never end in .git (we add that automagically), so a valid
273 # fork can never have .git at the end of any path component except the last.
274 # We check this to avoid a situation where a certain collection of pushed refs
275 # could be mistaken for a GIT_DIR. Git would ultimately complain, but some
276 # undesirable things could happen along the way.
278 # Remove the leading $reporoot and trailing .git to get a test string
279 testpath="${dir#$reporoot/}"
280 testpath="${testpath%.git}"
281 case "$testpath/" in *.[Gg][Ii][Tt]/*|_*)
282 forbidden
283 exit 1
284 esac
286 if ! [ -d "$dir" ] || ! [ -f "$dir/HEAD" ] || ! [ -d "$dir/objects" ]; then
287 forbidden
288 exit 1
292 if [ -n "$bundle" ]; then
293 # We support two kinds of bundles:
294 # 1) /path/to/foo.git/clone.bundle
295 # 2) /path/to/foo.git/foo-????????.bundle
296 # The first ALWAYS returns a 302 or 404 response
297 # The second ALWAYS returns a 404 or success
298 isredir=
299 projbase="${projbare##*/}"
300 case "$suffix" in
301 "clone.bundle")
302 isredir=1
304 "$projbase-"$octet4".bundle")
307 forbidden
308 exit 1
309 esac
310 if [ -n "$isredir" ]; then
311 # A bundles/latest symlink must exist and
312 # point to an existing file in the same directory
313 # matching the magic format (\d{8}_\d{6}-$octet4)
314 if ! [ -L "$dir/bundles/latest" -a -f "$dir/bundles/latest" ]; then
315 notfound
316 exit 0
318 linked="$(readlink "$dir/bundles/latest")" || { notfound; exit 0; }
319 case "$linked" in ${digit8}_$digit6-$octet4) :;; *)
320 notfound
321 exit 0
322 esac
323 bundlefile="$dir/bundles/$linked"
324 linked="$projbase-${linked#????????_??????-}"
325 else
326 bundleid="${suffix%.bundle}"
327 bundleid="${bundleid##*-}"
328 bundlepat="${digit8}_$digit6-$bundleid"
329 bundlefile="$(echo "$dir/bundles/"$bundlepat 2>/dev/null || :)"
330 if [ "$dir/bundles/$bundlepat" = "$bundlefile" ] || ! [ -f "$bundlefile" ]; then
331 notfound
332 exit 0
336 read -r bundlehdr || :
337 read -r bundlepck || :
338 } <"$bundlefile"; } 2>/dev/null
339 [ -n "$bundlehdr" -a -n "$bundlepck" ] || { notfound; exit 0; }
340 # Non-absolute paths are relative to the repository's objects/pack dir
341 case "$bundlehdr" in /*) :;; *)
342 bundlehdr="$dir/objects/pack/$bundlehdr"
343 esac
344 case "$bundlepck" in /*) :;; *)
345 bundlepck="$dir/objects/pack/$bundlepck"
346 esac
347 [ -f "$bundlehdr" -a -f "$bundlepck" ] || { notfound; exit 0; }
348 [ -s "$bundlehdr" -o -s "$bundlepck" ] || { notfound; exit 0; }
349 [ -z "$isredir" ] || { redir "/$proj/$linked.bundle"; exit 0; }
350 exec "$cfg_basedir/bin/rangecgi" -c 'application/x-git-bundle' -e 180 \
351 "$bundlehdr" "$bundlepck"
352 internalerr "exec failed: $cfg_basedir/bin/rangecgi"
353 exit 1
356 if [ -z "$needsauthcheck" ] || [ -z "$smart" ]; then
357 if [ -n "$GIT_HTTP_BACKEND_SHOW_ERRORS" ]; then
358 exec "$cfg_git_http_backend_bin" "$@"
359 else
360 exec "$cfg_git_http_backend_bin" "$@" 2>/dev/null
362 internalerr "exec failed: $cfg_git_http_backend_bin"
363 exit 1
366 if ! [ -f "$dir/.nofetch" ]; then
367 forbidden "The $proj project is a mirror and may not be pushed to, sorry"
368 exit 1
371 authuser="${REMOTE_USER#/UID=}"
372 authuuid="${authuser}"
373 authuser="${authuser%/dnQualifier=*}"
374 authuuid="${authuuid#$authuser}"
375 authuuid="${authuuid#/dnQualifier=}"
376 if [ -z "$authuser" ]; then
377 needsauth "Only authenticated users may push, sorry"
378 exit 1
380 if [ "$authuser" != "mob" -o "$cfg_mob" != "mob" ]; then
381 if ! useruuid="$("$cfg_basedir/bin/get_user_uuid" "$authuser")" || [ "$useruuid" != "$authuuid" ]; then
382 forbidden "The user '$authuser' certificate being used is no longer valid." \
383 "You may download a new user certificate at $cfg_webadmurl/edituser.cgi"
384 exit 1
388 if ! "$cfg_basedir/bin/can_user_push_http" "$projbare" "$authuser"; then
389 # If mob is enabled and mob has push permissions and
390 # the current user is not the mob then it's a personal mob push
391 # presuming the special mob directory has been set up
392 if [ "$cfg_mob" = "mob" -a "$authuser" != "mob" -a -d "$cfg_reporoot/$proj/mob" ] &&
393 "$cfg_basedir/bin/can_user_push_http" "$projbare" "mob"; then
395 umask 113
396 > "$cfg_chroot/etc/sshactive/${authuser},"
397 mv -f "$cfg_chroot/etc/sshactive/${authuser}," "$cfg_chroot/etc/sshactive/${authuser}"
398 ! [ -e "$dir/.delaygc" ] || > "$dir/.allowgc" || :
400 export PATH_INFO="/$proj/mob/$suffix"
401 if [ -n "$GIT_HTTP_BACKEND_SHOW_ERRORS" ]; then
402 exec "$cfg_git_http_backend_bin" "$@"
403 else
404 exec "$cfg_git_http_backend_bin" "$@" 2>/dev/null
406 internalerr "exec failed: $cfg_git_http_backend_bin"
407 exit 1
409 forbidden "The user '$authuser' does not have push permissions for project '$proj'." \
410 "You may adjust push permissions at $cfg_webadmurl/editproj.cgi?name=$proj"
411 exit 1
415 umask 113
416 > "$cfg_chroot/etc/sshactive/${authuser},"
417 mv -f "$cfg_chroot/etc/sshactive/${authuser}," "$cfg_chroot/etc/sshactive/${authuser}"
418 ! [ -e "$dir/.delaygc" ] || > "$dir/.allowgc" || :
420 if [ -n "$GIT_HTTP_BACKEND_SHOW_ERRORS" ]; then
421 exec "$cfg_git_http_backend_bin" "$@"
422 else
423 exec "$cfg_git_http_backend_bin" "$@" 2>/dev/null
425 internalerr "exec failed: $cfg_git_http_backend_bin"
426 exit 1