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

1  2 
http.c
t/lib-httpd.sh
t/lib-httpd/apache.conf
t/t5541-http-push.sh
t/t5551-http-fetch.sh
diff --combined http.c
index 18bc6bf7bf17bab9037fade3af2151da6b298b2d,7c4a4072f24647bd3ab6cb6c24142d31c595bb86..9bac1d89fdb639f6991bfe9f683ccf7bd5ada4cb
--- 1/http.c
--- 2/http.c
+++ b/http.c
@@@ -4,7 -4,6 +4,7 @@@
  #include "run-command.h"
  #include "url.h"
  #include "credential.h"
 +#include "version.h"
  
  int active_requests;
  int http_is_verbose;
@@@ -300,7 -299,7 +300,7 @@@ static CURL *get_curl_handle(void
                curl_easy_setopt(result, CURLOPT_VERBOSE, 1);
  
        curl_easy_setopt(result, CURLOPT_USERAGENT,
 -              user_agent ? user_agent : GIT_HTTP_USER_AGENT);
 +              user_agent ? user_agent : git_user_agent());
  
        if (curl_ftp_no_epsv)
                curl_easy_setopt(result, CURLOPT_FTP_USE_EPSV, 0);
@@@ -745,6 -744,33 +745,35 @@@ char *get_remote_object_url(const char 
        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 -818,7 +821,7 @@@ static int http_request(const char *url
  
        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;
        curl_slist_free_all(headers);
        strbuf_release(&buf);
  
-       if (ret == HTTP_OK)
-               credential_approve(&http_auth);
        return ret;
  }
  
@@@ -920,7 -922,7 +925,7 @@@ static char *fetch_pack_index(unsigned 
        tmp = strbuf_detach(&buf, NULL);
  
        if (http_get_file(url, tmp, 0) != HTTP_OK) {
 -              error("Unable to get pack index %s\n", url);
 +              error("Unable to get pack index %s", url);
                free(tmp);
                tmp = NULL;
        }
diff --combined t/lib-httpd.sh
index d773542680d9f3c3d77e52871877560a9e545a26,0f31ef9be487759829c141031f8925167b135537..02f442bfadbb1239785a456cddc50223da4f3834
@@@ -43,10 -43,6 +43,10 @@@ TEST_PATH="$TEST_DIRECTORY"/lib-http
  HTTPD_ROOT_PATH="$PWD"/httpd
  HTTPD_DOCUMENT_ROOT_PATH=$HTTPD_ROOT_PATH/www
  
 +# hack to suppress apache PassEnv warnings
 +GIT_VALGRIND=$GIT_VALGRIND; export GIT_VALGRIND
 +GIT_VALGRIND_OPTIONS=$GIT_VALGRIND_OPTIONS; export GIT_VALGRIND_OPTIONS
 +
  if ! test -x "$LIB_HTTPD_PATH"
  then
        skip_all="skipping test, no web server found at '$LIB_HTTPD_PATH'"
@@@ -167,3 -163,42 +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"
+ }
diff --combined t/lib-httpd/apache.conf
index 36b1596a10c04b5c5c095e96e8e247463eb3f022,ec8618dfde3b106c79f1d056bc0ed085075fc8c8..49d5d877ceeeba3738b0939f14d059c2a75ff3ae
@@@ -42,30 -42,25 +42,28 @@@ ErrorLog error.lo
  </IfModule>
  </IfVersion>
  
 +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 None
 +      Options FollowSymlinks
  </Directory>
  <Files ${GIT_EXEC_PATH}/git-http-backend>
        Options ExecCGI
@@@ -94,6 -89,13 +92,13 @@@ SSLEngine O
        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
diff --combined t/t5541-http-push.sh
index 624633aced58169ac1b821fb44d3ad5f25996894,ef6d6b6e4e1a06e31780cf2a4261a632f5779987..4b4b4a604f3075dec1056b31da5cccc0c4ed5a9a
@@@ -36,6 -36,8 +36,8 @@@ test_expect_success 'setup remote repos
        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
@@@ -64,10 -66,7 +66,10 @@@ test_expect_success 'no empty path comp
  
  test_expect_success 'clone remote repository' '
        rm -rf test_repo_clone &&
 -      git clone $HTTPD_URL/smart/test_repo.git test_repo_clone
 +      git clone $HTTPD_URL/smart/test_repo.git test_repo_clone &&
 +      (
 +              cd test_repo_clone && git config push.default matching
 +      )
  '
  
  test_expect_success 'push to remote repository (standard)' '
@@@ -269,5 -268,29 +271,29 @@@ test_expect_success 'http push respect
        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
diff --combined t/t5551-http-fetch.sh
index 91eaf53d1d30f7719e93e88b674f60b6c6eaa376,7380f2a2dd3f22d65a10389d8197af94f9ea4123..2db5c3564181818efdf885188d7cc597024c6f12
@@@ -27,6 -27,8 +27,8 @@@ test_expect_success 'create http-access
        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,12 -111,30 +111,30 @@@ test_expect_success 'follow redirects (
        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' '
        (
        cd "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
 -      for i in `seq 50000`
 +      for i in `test_seq 50000`
        do
                echo "commit refs/heads/too-many-refs"
                echo "mark :$i"
        done | git fast-import --export-marks=marks &&
  
        # now assign tags to all the dangling commits we created above
 -      tag=$(perl -e "print \"bla\" x 30") &&
 +      tag=$("$PERL_PATH" -e "print \"bla\" x 30") &&
        sed -e "s/^:\(.\+\) \(.\+\)$/\2 refs\/tags\/$tag-\1/" <marks >>packed-refs
        )
  '