Merge branch 'jk/maint-http-half-auth-push'
authorJunio C Hamano <gitster@pobox.com>
Fri, 7 Sep 2012 18:09:49 +0000 (11:09 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 7 Sep 2012 18:09:50 +0000 (11:09 -0700)
Pushing to smart HTTP server with recent Git fails without having
the username in the URL to force authentication, if the server is
configured to allow GET anonymously, while requiring authentication
for POST.

* jk/maint-http-half-auth-push:
http: prompt for credentials on failed POST
http: factor out http error code handling
t: test http access to "half-auth" repositories
t: test basic smart-http authentication
t/lib-httpd: recognize */smart/* repos as smart-http
t/lib-httpd: only route auth/dumb to dumb repos
t5550: factor out http auth setup
t5550: put auth-required repo in auth/dumb

http.c
http.h
remote-curl.c
t/lib-httpd.sh
t/lib-httpd/apache.conf
t/t5540-http-push.sh
t/t5541-http-push.sh
t/t5550-http-fetch.sh
t/t5551-http-fetch.sh
diff --git a/http.c b/http.c
index 18bc6bf7bf17bab9037fade3af2151da6b298b2d..9bac1d89fdb639f6991bfe9f683ccf7bd5ada4cb 100644 (file)
--- a/http.c
+++ b/http.c
@@ -745,6 +745,35 @@ char *get_remote_object_url(const char *url, const char *hex,
        return strbuf_detach(&buf, NULL);
 }
 
+int handle_curl_result(struct active_request_slot *slot)
+{
+       struct slot_results *results = slot->results;
+
+       if (results->curl_result == CURLE_OK) {
+               credential_approve(&http_auth);
+               return HTTP_OK;
+       } else if (missing_target(results))
+               return HTTP_MISSING_TARGET;
+       else if (results->http_code == 401) {
+               if (http_auth.username && http_auth.password) {
+                       credential_reject(&http_auth);
+                       return HTTP_NOAUTH;
+               } else {
+                       credential_fill(&http_auth);
+                       init_curl_http_auth(slot->curl);
+                       return HTTP_REAUTH;
+               }
+       } else {
+#if LIBCURL_VERSION_NUM >= 0x070c00
+               if (!curl_errorstr[0])
+                       strlcpy(curl_errorstr,
+                               curl_easy_strerror(results->curl_result),
+                               sizeof(curl_errorstr));
+#endif
+               return HTTP_ERROR;
+       }
+}
+
 /* http_request() targets */
 #define HTTP_REQUEST_STRBUF    0
 #define HTTP_REQUEST_FILE      1
@@ -792,28 +821,7 @@ static int http_request(const char *url, void *result, int target, int options)
 
        if (start_active_slot(slot)) {
                run_active_slot(slot);
-               if (results.curl_result == CURLE_OK)
-                       ret = HTTP_OK;
-               else if (missing_target(&results))
-                       ret = HTTP_MISSING_TARGET;
-               else if (results.http_code == 401) {
-                       if (http_auth.username && http_auth.password) {
-                               credential_reject(&http_auth);
-                               ret = HTTP_NOAUTH;
-                       } else {
-                               credential_fill(&http_auth);
-                               init_curl_http_auth(slot->curl);
-                               ret = HTTP_REAUTH;
-                       }
-               } else {
-#if LIBCURL_VERSION_NUM >= 0x070c00
-                       if (!curl_errorstr[0])
-                               strlcpy(curl_errorstr,
-                                       curl_easy_strerror(results.curl_result),
-                                       sizeof(curl_errorstr));
-#endif
-                       ret = HTTP_ERROR;
-               }
+               ret = handle_curl_result(slot);
        } else {
                error("Unable to start HTTP request for %s", url);
                ret = HTTP_START_FAILED;
@@ -822,9 +830,6 @@ static int http_request(const char *url, void *result, int target, int options)
        curl_slist_free_all(headers);
        strbuf_release(&buf);
 
-       if (ret == HTTP_OK)
-               credential_approve(&http_auth);
-
        return ret;
 }
 
diff --git a/http.h b/http.h
index 915c2862a65bdbf42472099f9ed53ca68c2ccfd9..12de25597df4077a52a44dace83093091514c10d 100644 (file)
--- a/http.h
+++ b/http.h
@@ -78,6 +78,7 @@ extern int start_active_slot(struct active_request_slot *slot);
 extern void run_active_slot(struct active_request_slot *slot);
 extern void finish_active_slot(struct active_request_slot *slot);
 extern void finish_all_active_slots(void);
+extern int handle_curl_result(struct active_request_slot *slot);
 
 #ifdef USE_CURL_MULTI
 extern void fill_active_slots(void);
index 04a9d6277db8fac78cb223a906b12744aab32193..3ec474fc631eb3b433b3cadbd74aed4b83cba724 100644 (file)
@@ -362,16 +362,17 @@ static size_t rpc_in(char *ptr, size_t eltsize,
 
 static int run_slot(struct active_request_slot *slot)
 {
-       int err = 0;
+       int err;
        struct slot_results results;
 
        slot->results = &results;
        slot->curl_result = curl_easy_perform(slot->curl);
        finish_active_slot(slot);
 
-       if (results.curl_result != CURLE_OK) {
-               err |= error("RPC failed; result=%d, HTTP code = %ld",
-                       results.curl_result, results.http_code);
+       err = handle_curl_result(slot);
+       if (err != HTTP_OK && err != HTTP_REAUTH) {
+               error("RPC failed; result=%d, HTTP code = %ld",
+                     results.curl_result, results.http_code);
        }
 
        return err;
@@ -436,9 +437,11 @@ static int post_rpc(struct rpc_state *rpc)
        }
 
        if (large_request) {
-               err = probe_rpc(rpc);
-               if (err)
-                       return err;
+               do {
+                       err = probe_rpc(rpc);
+               } while (err == HTTP_REAUTH);
+               if (err != HTTP_OK)
+                       return -1;
        }
 
        slot = get_active_slot();
@@ -525,7 +528,11 @@ static int post_rpc(struct rpc_state *rpc)
        curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, rpc_in);
        curl_easy_setopt(slot->curl, CURLOPT_FILE, rpc);
 
-       err = run_slot(slot);
+       do {
+               err = run_slot(slot);
+       } while (err == HTTP_REAUTH && !large_request && !use_gzip);
+       if (err != HTTP_OK)
+               err = -1;
 
        curl_slist_free_all(headers);
        free(gzip_body);
index d773542680d9f3c3d77e52871877560a9e545a26..02f442bfadbb1239785a456cddc50223da4f3834 100644 (file)
@@ -167,3 +167,42 @@ test_http_push_nonff() {
                test_i18ngrep "Updates were rejected because" output
        '
 }
+
+setup_askpass_helper() {
+       test_expect_success 'setup askpass helper' '
+               write_script "$TRASH_DIRECTORY/askpass" <<-\EOF &&
+               echo >>"$TRASH_DIRECTORY/askpass-query" "askpass: $*" &&
+               cat "$TRASH_DIRECTORY/askpass-response"
+               EOF
+               GIT_ASKPASS="$TRASH_DIRECTORY/askpass" &&
+               export GIT_ASKPASS &&
+               export TRASH_DIRECTORY
+       '
+}
+
+set_askpass() {
+       >"$TRASH_DIRECTORY/askpass-query" &&
+       echo "$*" >"$TRASH_DIRECTORY/askpass-response"
+}
+
+expect_askpass() {
+       dest=$HTTPD_DEST
+       {
+               case "$1" in
+               none)
+                       ;;
+               pass)
+                       echo "askpass: Password for 'http://$2@$dest': "
+                       ;;
+               both)
+                       echo "askpass: Username for 'http://$dest': "
+                       echo "askpass: Password for 'http://$2@$dest': "
+                       ;;
+               *)
+                       false
+                       ;;
+               esac
+       } >"$TRASH_DIRECTORY/askpass-expect" &&
+       test_cmp "$TRASH_DIRECTORY/askpass-expect" \
+                "$TRASH_DIRECTORY/askpass-query"
+}
index 36b1596a10c04b5c5c095e96e8e247463eb3f022..49d5d877ceeeba3738b0939f14d059c2a75ff3ae 100644 (file)
@@ -46,24 +46,22 @@ PassEnv GIT_VALGRIND
 PassEnv GIT_VALGRIND_OPTIONS
 
 Alias /dumb/ www/
-Alias /auth/ www/auth/
+Alias /auth/dumb/ www/auth/dumb/
 
-<Location /smart/>
+<LocationMatch /smart/>
        SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
        SetEnv GIT_HTTP_EXPORT_ALL
-</Location>
-<Location /smart_noexport/>
+</LocationMatch>
+<LocationMatch /smart_noexport/>
        SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
-</Location>
-<Location /smart_custom_env/>
+</LocationMatch>
+<LocationMatch /smart_custom_env/>
        SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
        SetEnv GIT_HTTP_EXPORT_ALL
        SetEnv GIT_COMMITTER_NAME "Custom User"
        SetEnv GIT_COMMITTER_EMAIL custom@example.com
-</Location>
-ScriptAlias /smart/ ${GIT_EXEC_PATH}/git-http-backend/
-ScriptAlias /smart_noexport/ ${GIT_EXEC_PATH}/git-http-backend/
-ScriptAlias /smart_custom_env/ ${GIT_EXEC_PATH}/git-http-backend/
+</LocationMatch>
+ScriptAliasMatch /smart_*[^/]*/(.*) ${GIT_EXEC_PATH}/git-http-backend/$1
 <Directory ${GIT_EXEC_PATH}>
        Options FollowSymlinks
 </Directory>
@@ -94,6 +92,13 @@ SSLEngine On
        Require valid-user
 </Location>
 
+<LocationMatch "^/auth-push/.*/git-receive-pack$">
+       AuthType Basic
+       AuthName "git-auth"
+       AuthUserFile passwd
+       Require valid-user
+</LocationMatch>
+
 <IfDefine DAV>
        LoadModule dav_module modules/mod_dav.so
        LoadModule dav_fs_module modules/mod_dav_fs.so
index 1eea64765608dc4a2e4b19eec4155890306e5d59..f141f2d1da3ca8186ccc7a742bddd06f2e7f8970 100755 (executable)
@@ -46,15 +46,7 @@ test_expect_success 'create password-protected repository' '
               "$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git"
 '
 
-test_expect_success 'setup askpass helper' '
-       cat >askpass <<-\EOF &&
-       #!/bin/sh
-       echo user@host
-       EOF
-       chmod +x askpass &&
-       GIT_ASKPASS="$PWD/askpass" &&
-       export GIT_ASKPASS
-'
+setup_askpass_helper
 
 test_expect_success 'clone remote repository' '
        cd "$ROOT_PATH" &&
@@ -162,6 +154,7 @@ test_http_push_nonff "$HTTPD_DOCUMENT_ROOT_PATH"/test_repo.git \
 
 test_expect_success 'push to password-protected repository (user in URL)' '
        test_commit pw-user &&
+       set_askpass user@host &&
        git push "$HTTPD_URL_USER/auth/dumb/test_repo.git" HEAD &&
        git rev-parse --verify HEAD >expect &&
        git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git" \
@@ -169,9 +162,15 @@ test_expect_success 'push to password-protected repository (user in URL)' '
        test_cmp expect actual
 '
 
+test_expect_failure 'user was prompted only once for password' '
+       expect_askpass pass user@host
+'
+
 test_expect_failure 'push to password-protected repository (no user in URL)' '
        test_commit pw-nouser &&
+       set_askpass user@host &&
        git push "$HTTPD_URL/auth/dumb/test_repo.git" HEAD &&
+       expect_askpass both user@host
        git rev-parse --verify HEAD >expect &&
        git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git" \
                rev-parse --verify HEAD >actual &&
index 624633aced58169ac1b821fb44d3ad5f25996894..4b4b4a604f3075dec1056b31da5cccc0c4ed5a9a 100755 (executable)
@@ -36,6 +36,8 @@ test_expect_success 'setup remote repository' '
        mv test_repo.git "$HTTPD_DOCUMENT_ROOT_PATH"
 '
 
+setup_askpass_helper
+
 cat >exp <<EOF
 GET  /smart/test_repo.git/info/refs?service=git-upload-pack HTTP/1.1 200
 POST /smart/test_repo.git/git-upload-pack HTTP/1.1 200
@@ -269,5 +271,29 @@ test_expect_success 'http push respects GIT_COMMITTER_* in reflog' '
        test_cmp expect actual
 '
 
+test_expect_success 'push over smart http with auth' '
+       cd "$ROOT_PATH/test_repo_clone" &&
+       echo push-auth-test >expect &&
+       test_commit push-auth-test &&
+       set_askpass user@host &&
+       git push "$HTTPD_URL"/auth/smart/test_repo.git &&
+       git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
+               log -1 --format=%s >actual &&
+       expect_askpass both user@host &&
+       test_cmp expect actual
+'
+
+test_expect_success 'push to auth-only-for-push repo' '
+       cd "$ROOT_PATH/test_repo_clone" &&
+       echo push-half-auth >expect &&
+       test_commit push-half-auth &&
+       set_askpass user@host &&
+       git push "$HTTPD_URL"/auth-push/smart/test_repo.git &&
+       git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/test_repo.git" \
+               log -1 --format=%s >actual &&
+       expect_askpass both user@host &&
+       test_cmp expect actual
+'
+
 stop_httpd
 test_done
index b06f817af32483631b3ec297246e156400bc6b93..16ef0419e9e4ea8d42aebd63c5bc5ec7f63d3732 100755 (executable)
@@ -41,68 +41,34 @@ test_expect_success 'clone http repository' '
 '
 
 test_expect_success 'create password-protected repository' '
-       mkdir "$HTTPD_DOCUMENT_ROOT_PATH/auth/" &&
+       mkdir -p "$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/" &&
        cp -Rf "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" \
-              "$HTTPD_DOCUMENT_ROOT_PATH/auth/repo.git"
-'
-
-test_expect_success 'setup askpass helpers' '
-       cat >askpass <<-EOF &&
-       #!/bin/sh
-       echo >>"$PWD/askpass-query" "askpass: \$*" &&
-       cat "$PWD/askpass-response"
-       EOF
-       chmod +x askpass &&
-       GIT_ASKPASS="$PWD/askpass" &&
-       export GIT_ASKPASS
-'
-
-expect_askpass() {
-       dest=$HTTPD_DEST
-       {
-               case "$1" in
-               none)
-                       ;;
-               pass)
-                       echo "askpass: Password for 'http://$2@$dest': "
-                       ;;
-               both)
-                       echo "askpass: Username for 'http://$dest': "
-                       echo "askpass: Password for 'http://$2@$dest': "
-                       ;;
-               *)
-                       false
-                       ;;
-               esac
-       } >askpass-expect &&
-       test_cmp askpass-expect askpass-query
-}
+              "$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/repo.git"
+'
+
+setup_askpass_helper
 
 test_expect_success 'cloning password-protected repository can fail' '
-       >askpass-query &&
-       echo wrong >askpass-response &&
-       test_must_fail git clone "$HTTPD_URL/auth/repo.git" clone-auth-fail &&
+       set_askpass wrong &&
+       test_must_fail git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-fail &&
        expect_askpass both wrong
 '
 
 test_expect_success 'http auth can use user/pass in URL' '
-       >askpass-query &&
-       echo wrong >askpass-response &&
-       git clone "$HTTPD_URL_USER_PASS/auth/repo.git" clone-auth-none &&
+       set_askpass wrong &&
+       git clone "$HTTPD_URL_USER_PASS/auth/dumb/repo.git" clone-auth-none &&
        expect_askpass none
 '
 
 test_expect_success 'http auth can use just user in URL' '
-       >askpass-query &&
-       echo user@host >askpass-response &&
-       git clone "$HTTPD_URL_USER/auth/repo.git" clone-auth-pass &&
+       set_askpass user@host &&
+       git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-pass &&
        expect_askpass pass user@host
 '
 
 test_expect_success 'http auth can request both user and pass' '
-       >askpass-query &&
-       echo user@host >askpass-response &&
-       git clone "$HTTPD_URL/auth/repo.git" clone-auth-both &&
+       set_askpass user@host &&
+       git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-both &&
        expect_askpass both user@host
 '
 
@@ -112,25 +78,22 @@ test_expect_success 'http auth respects credential helper config' '
                echo username=user@host
                echo password=user@host
        }; f" &&
-       >askpass-query &&
-       echo wrong >askpass-response &&
-       git clone "$HTTPD_URL/auth/repo.git" clone-auth-helper &&
+       set_askpass wrong &&
+       git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-helper &&
        expect_askpass none
 '
 
 test_expect_success 'http auth can get username from config' '
        test_config_global "credential.$HTTPD_URL.username" user@host &&
-       >askpass-query &&
-       echo user@host >askpass-response &&
-       git clone "$HTTPD_URL/auth/repo.git" clone-auth-user &&
+       set_askpass user@host &&
+       git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-user &&
        expect_askpass pass user@host
 '
 
 test_expect_success 'configured username does not override URL' '
        test_config_global "credential.$HTTPD_URL.username" wrong &&
-       >askpass-query &&
-       echo user@host >askpass-response &&
-       git clone "$HTTPD_URL_USER/auth/repo.git" clone-auth-user2 &&
+       set_askpass user@host &&
+       git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-user2 &&
        expect_askpass pass user@host
 '
 
index 91eaf53d1d30f7719e93e88b674f60b6c6eaa376..2db5c3564181818efdf885188d7cc597024c6f12 100755 (executable)
@@ -27,6 +27,8 @@ test_expect_success 'create http-accessible bare repository' '
        git push public master:master
 '
 
+setup_askpass_helper
+
 cat >exp <<EOF
 > GET /smart/repo.git/info/refs?service=git-upload-pack HTTP/1.1
 > Accept: */*
@@ -109,6 +111,24 @@ test_expect_success 'follow redirects (302)' '
        git clone $HTTPD_URL/smart-redir-temp/repo.git --quiet repo-t
 '
 
+test_expect_success 'clone from password-protected repository' '
+       echo two >expect &&
+       set_askpass user@host &&
+       git clone --bare "$HTTPD_URL/auth/smart/repo.git" smart-auth &&
+       expect_askpass both user@host &&
+       git --git-dir=smart-auth log -1 --format=%s >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'clone from auth-only-for-push repository' '
+       echo two >expect &&
+       set_askpass wrong &&
+       git clone --bare "$HTTPD_URL/auth-push/smart/repo.git" smart-noauth &&
+       expect_askpass none &&
+       git --git-dir=smart-noauth log -1 --format=%s >actual &&
+       test_cmp expect actual
+'
+
 test -n "$GIT_TEST_LONG" && test_set_prereq EXPENSIVE
 
 test_expect_success EXPENSIVE 'create 50,000 tags in the repo' '