Merge branch 'jk/maint-http-half-auth-push'

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
This commit is contained in:
Junio C Hamano 2012-09-07 11:09:49 -07:00
commit e6dd70e9bf
9 changed files with 171 additions and 106 deletions

55
http.c
View file

@ -745,6 +745,35 @@ char *get_remote_object_url(const char *url, const char *hex,
return strbuf_detach(&buf, NULL); 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 */ /* http_request() targets */
#define HTTP_REQUEST_STRBUF 0 #define HTTP_REQUEST_STRBUF 0
#define HTTP_REQUEST_FILE 1 #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)) { if (start_active_slot(slot)) {
run_active_slot(slot); run_active_slot(slot);
if (results.curl_result == CURLE_OK) ret = handle_curl_result(slot);
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;
}
} else { } else {
error("Unable to start HTTP request for %s", url); error("Unable to start HTTP request for %s", url);
ret = HTTP_START_FAILED; 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); curl_slist_free_all(headers);
strbuf_release(&buf); strbuf_release(&buf);
if (ret == HTTP_OK)
credential_approve(&http_auth);
return ret; return ret;
} }

1
http.h
View file

@ -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 run_active_slot(struct active_request_slot *slot);
extern void finish_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 void finish_all_active_slots(void);
extern int handle_curl_result(struct active_request_slot *slot);
#ifdef USE_CURL_MULTI #ifdef USE_CURL_MULTI
extern void fill_active_slots(void); extern void fill_active_slots(void);

View file

@ -362,16 +362,17 @@ static size_t rpc_in(char *ptr, size_t eltsize,
static int run_slot(struct active_request_slot *slot) static int run_slot(struct active_request_slot *slot)
{ {
int err = 0; int err;
struct slot_results results; struct slot_results results;
slot->results = &results; slot->results = &results;
slot->curl_result = curl_easy_perform(slot->curl); slot->curl_result = curl_easy_perform(slot->curl);
finish_active_slot(slot); finish_active_slot(slot);
if (results.curl_result != CURLE_OK) { err = handle_curl_result(slot);
err |= error("RPC failed; result=%d, HTTP code = %ld", if (err != HTTP_OK && err != HTTP_REAUTH) {
results.curl_result, results.http_code); error("RPC failed; result=%d, HTTP code = %ld",
results.curl_result, results.http_code);
} }
return err; return err;
@ -436,9 +437,11 @@ static int post_rpc(struct rpc_state *rpc)
} }
if (large_request) { if (large_request) {
err = probe_rpc(rpc); do {
if (err) err = probe_rpc(rpc);
return err; } while (err == HTTP_REAUTH);
if (err != HTTP_OK)
return -1;
} }
slot = get_active_slot(); 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_WRITEFUNCTION, rpc_in);
curl_easy_setopt(slot->curl, CURLOPT_FILE, rpc); 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); curl_slist_free_all(headers);
free(gzip_body); free(gzip_body);

View file

@ -167,3 +167,42 @@ test_http_push_nonff() {
test_i18ngrep "Updates were rejected because" output 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"
}

View file

@ -46,24 +46,22 @@ PassEnv GIT_VALGRIND
PassEnv GIT_VALGRIND_OPTIONS PassEnv GIT_VALGRIND_OPTIONS
Alias /dumb/ www/ 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_EXEC_PATH ${GIT_EXEC_PATH}
SetEnv GIT_HTTP_EXPORT_ALL SetEnv GIT_HTTP_EXPORT_ALL
</Location> </LocationMatch>
<Location /smart_noexport/> <LocationMatch /smart_noexport/>
SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH} SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
</Location> </LocationMatch>
<Location /smart_custom_env/> <LocationMatch /smart_custom_env/>
SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH} SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
SetEnv GIT_HTTP_EXPORT_ALL SetEnv GIT_HTTP_EXPORT_ALL
SetEnv GIT_COMMITTER_NAME "Custom User" SetEnv GIT_COMMITTER_NAME "Custom User"
SetEnv GIT_COMMITTER_EMAIL custom@example.com SetEnv GIT_COMMITTER_EMAIL custom@example.com
</Location> </LocationMatch>
ScriptAlias /smart/ ${GIT_EXEC_PATH}/git-http-backend/ ScriptAliasMatch /smart_*[^/]*/(.*) ${GIT_EXEC_PATH}/git-http-backend/$1
ScriptAlias /smart_noexport/ ${GIT_EXEC_PATH}/git-http-backend/
ScriptAlias /smart_custom_env/ ${GIT_EXEC_PATH}/git-http-backend/
<Directory ${GIT_EXEC_PATH}> <Directory ${GIT_EXEC_PATH}>
Options FollowSymlinks Options FollowSymlinks
</Directory> </Directory>
@ -94,6 +92,13 @@ SSLEngine On
Require valid-user Require valid-user
</Location> </Location>
<LocationMatch "^/auth-push/.*/git-receive-pack$">
AuthType Basic
AuthName "git-auth"
AuthUserFile passwd
Require valid-user
</LocationMatch>
<IfDefine DAV> <IfDefine DAV>
LoadModule dav_module modules/mod_dav.so LoadModule dav_module modules/mod_dav.so
LoadModule dav_fs_module modules/mod_dav_fs.so LoadModule dav_fs_module modules/mod_dav_fs.so

View file

@ -46,15 +46,7 @@ test_expect_success 'create password-protected repository' '
"$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git" "$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git"
' '
test_expect_success 'setup askpass helper' ' setup_askpass_helper
cat >askpass <<-\EOF &&
#!/bin/sh
echo user@host
EOF
chmod +x askpass &&
GIT_ASKPASS="$PWD/askpass" &&
export GIT_ASKPASS
'
test_expect_success 'clone remote repository' ' test_expect_success 'clone remote repository' '
cd "$ROOT_PATH" && 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_expect_success 'push to password-protected repository (user in URL)' '
test_commit pw-user && test_commit pw-user &&
set_askpass user@host &&
git push "$HTTPD_URL_USER/auth/dumb/test_repo.git" HEAD && git push "$HTTPD_URL_USER/auth/dumb/test_repo.git" HEAD &&
git rev-parse --verify HEAD >expect && git rev-parse --verify HEAD >expect &&
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git" \ 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_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_expect_failure 'push to password-protected repository (no user in URL)' '
test_commit pw-nouser && test_commit pw-nouser &&
set_askpass user@host &&
git push "$HTTPD_URL/auth/dumb/test_repo.git" HEAD && git push "$HTTPD_URL/auth/dumb/test_repo.git" HEAD &&
expect_askpass both user@host
git rev-parse --verify HEAD >expect && git rev-parse --verify HEAD >expect &&
git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git" \ git --git-dir="$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/test_repo.git" \
rev-parse --verify HEAD >actual && rev-parse --verify HEAD >actual &&

View file

@ -36,6 +36,8 @@ test_expect_success 'setup remote repository' '
mv test_repo.git "$HTTPD_DOCUMENT_ROOT_PATH" mv test_repo.git "$HTTPD_DOCUMENT_ROOT_PATH"
' '
setup_askpass_helper
cat >exp <<EOF cat >exp <<EOF
GET /smart/test_repo.git/info/refs?service=git-upload-pack HTTP/1.1 200 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 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_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 stop_httpd
test_done test_done

View file

@ -41,68 +41,34 @@ test_expect_success 'clone http repository' '
' '
test_expect_success 'create password-protected 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" \ cp -Rf "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" \
"$HTTPD_DOCUMENT_ROOT_PATH/auth/repo.git" "$HTTPD_DOCUMENT_ROOT_PATH/auth/dumb/repo.git"
' '
test_expect_success 'setup askpass helpers' ' setup_askpass_helper
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
}
test_expect_success 'cloning password-protected repository can fail' ' test_expect_success 'cloning password-protected repository can fail' '
>askpass-query && set_askpass wrong &&
echo wrong >askpass-response && test_must_fail git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-fail &&
test_must_fail git clone "$HTTPD_URL/auth/repo.git" clone-auth-fail &&
expect_askpass both wrong expect_askpass both wrong
' '
test_expect_success 'http auth can use user/pass in URL' ' test_expect_success 'http auth can use user/pass in URL' '
>askpass-query && set_askpass wrong &&
echo wrong >askpass-response && git clone "$HTTPD_URL_USER_PASS/auth/dumb/repo.git" clone-auth-none &&
git clone "$HTTPD_URL_USER_PASS/auth/repo.git" clone-auth-none &&
expect_askpass none expect_askpass none
' '
test_expect_success 'http auth can use just user in URL' ' test_expect_success 'http auth can use just user in URL' '
>askpass-query && set_askpass user@host &&
echo user@host >askpass-response && git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-pass &&
git clone "$HTTPD_URL_USER/auth/repo.git" clone-auth-pass &&
expect_askpass pass user@host expect_askpass pass user@host
' '
test_expect_success 'http auth can request both user and pass' ' test_expect_success 'http auth can request both user and pass' '
>askpass-query && set_askpass user@host &&
echo user@host >askpass-response && git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-both &&
git clone "$HTTPD_URL/auth/repo.git" clone-auth-both &&
expect_askpass both user@host expect_askpass both user@host
' '
@ -112,25 +78,22 @@ test_expect_success 'http auth respects credential helper config' '
echo username=user@host echo username=user@host
echo password=user@host echo password=user@host
}; f" && }; f" &&
>askpass-query && set_askpass wrong &&
echo wrong >askpass-response && git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-helper &&
git clone "$HTTPD_URL/auth/repo.git" clone-auth-helper &&
expect_askpass none expect_askpass none
' '
test_expect_success 'http auth can get username from config' ' test_expect_success 'http auth can get username from config' '
test_config_global "credential.$HTTPD_URL.username" user@host && test_config_global "credential.$HTTPD_URL.username" user@host &&
>askpass-query && set_askpass user@host &&
echo user@host >askpass-response && git clone "$HTTPD_URL/auth/dumb/repo.git" clone-auth-user &&
git clone "$HTTPD_URL/auth/repo.git" clone-auth-user &&
expect_askpass pass user@host expect_askpass pass user@host
' '
test_expect_success 'configured username does not override URL' ' test_expect_success 'configured username does not override URL' '
test_config_global "credential.$HTTPD_URL.username" wrong && test_config_global "credential.$HTTPD_URL.username" wrong &&
>askpass-query && set_askpass user@host &&
echo user@host >askpass-response && git clone "$HTTPD_URL_USER/auth/dumb/repo.git" clone-auth-user2 &&
git clone "$HTTPD_URL_USER/auth/repo.git" clone-auth-user2 &&
expect_askpass pass user@host expect_askpass pass user@host
' '

View file

@ -27,6 +27,8 @@ test_expect_success 'create http-accessible bare repository' '
git push public master:master git push public master:master
' '
setup_askpass_helper
cat >exp <<EOF cat >exp <<EOF
> GET /smart/repo.git/info/refs?service=git-upload-pack HTTP/1.1 > GET /smart/repo.git/info/refs?service=git-upload-pack HTTP/1.1
> Accept: */* > Accept: */*
@ -109,6 +111,24 @@ test_expect_success 'follow redirects (302)' '
git clone $HTTPD_URL/smart-redir-temp/repo.git --quiet repo-t 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 -n "$GIT_TEST_LONG" && test_set_prereq EXPENSIVE
test_expect_success EXPENSIVE 'create 50,000 tags in the repo' ' test_expect_success EXPENSIVE 'create 50,000 tags in the repo' '