diff --git a/http.c b/http.c index 5bc2ad8b873a77..c3182f422a1063 100644 --- a/http.c +++ b/http.c @@ -139,6 +139,7 @@ static unsigned long empty_auth_useless = CURLAUTH_BASIC | CURLAUTH_DIGEST_IE | CURLAUTH_DIGEST; +static int empty_auth_try_negotiate; static struct curl_slist *pragma_header; static struct string_list extra_http_headers = STRING_LIST_INIT_DUP; @@ -704,6 +705,22 @@ static void init_curl_http_auth(CURL *result) } } +void http_reauth_prepare(int all_capabilities) +{ + /* + * If we deferred stripping Negotiate to give empty auth a + * chance (auto mode), skip credential_fill on this retry so + * that init_curl_http_auth() sends empty credentials and + * libcurl can attempt Negotiate with the system ticket cache. + */ + if (empty_auth_try_negotiate && + !http_auth.password && !http_auth.credential && + (http_auth_methods & CURLAUTH_GSSNEGOTIATE)) + return; + + credential_fill(the_repository, &http_auth, all_capabilities); +} + /* *var must be free-able */ static void var_override(char **var, char *value) { @@ -1954,7 +1971,18 @@ static int handle_curl_result(struct slot_results *results) } return HTTP_NOAUTH; } else { - http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE; + if (curl_empty_auth == -1 && + !empty_auth_try_negotiate && + (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) { + /* + * In auto mode, give Negotiate a chance via + * empty auth before stripping it. If it fails, + * we will strip it on the next 401. + */ + empty_auth_try_negotiate = 1; + } else { + http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE; + } if (results->auth_avail) { http_auth_methods &= results->auth_avail; http_auth_methods_restricted = 1; @@ -2462,7 +2490,7 @@ static int http_request_recoverable(const char *url, sleep(retry_delay); } } else if (ret == HTTP_REAUTH) { - credential_fill(the_repository, &http_auth, 1); + http_reauth_prepare(1); } /* diff --git a/http.h b/http.h index f9ee888c3ed67e..729c51904d39ad 100644 --- a/http.h +++ b/http.h @@ -76,6 +76,12 @@ extern int http_is_verbose; extern ssize_t http_post_buffer; extern struct credential http_auth; +/** + * Prepare for an HTTP re-authentication retry. This fills credentials + * via credential_fill() so the next request can include them. + */ +void http_reauth_prepare(int all_capabilities); + extern char curl_errorstr[CURL_ERROR_SIZE]; enum http_follow_config { diff --git a/remote-curl.c b/remote-curl.c index aba60d571282d3..affdb880f7b3bf 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece do { err = probe_rpc(rpc, &results); if (err == HTTP_REAUTH) - credential_fill(the_repository, &http_auth, 0); + http_reauth_prepare(0); } while (err == HTTP_REAUTH); if (err != HTTP_OK) return -1; @@ -1068,7 +1068,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece rpc->any_written = 0; err = run_slot(slot, NULL); if (err == HTTP_REAUTH && !large_request) { - credential_fill(the_repository, &http_auth, 0); + http_reauth_prepare(0); curl_slist_free_all(headers); goto retry; } diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh index 35e6f4b397da7c..911c716aaff2f4 100755 --- a/t/t5563-simple-http-auth.sh +++ b/t/t5563-simple-http-auth.sh @@ -748,4 +748,78 @@ test_expect_success NTLM 'access using NTLM auth' ' git ls-remote "$HTTPD_URL/ntlm_auth/repo.git" ' +test_lazy_prereq SPNEGO 'curl --version | grep -qi "SPNEGO\|GSS-API\|Kerberos\|negotiate"' + +test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' ' + test_when_finished "per_test_cleanup" && + + set_credential_reply get <<-EOF && + username=alice + password=secret-passwd + EOF + + # Basic base64(alice:secret-passwd) + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF && + id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA== + EOF + + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF && + id=1 status=200 + id=default response=WWW-Authenticate: Negotiate + id=default response=WWW-Authenticate: Basic realm="example.com" + EOF + + test_config_global credential.helper test-helper && + GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-auto" \ + git -c http.emptyAuth=auto \ + ls-remote "$HTTPD_URL/custom_auth/repo.git" && + + # In auto mode with a Negotiate+Basic server, there should be + # three 401 responses: (1) initial no-auth request, (2) empty-auth + # retry where Negotiate fails (no Kerberos ticket), (3) libcurl + # internal Negotiate retry. The fourth attempt uses Basic + # credentials from credential_fill and succeeds. + grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-auto" >actual_401s && + test_line_count = 3 actual_401s && + + expect_credential_query get <<-EOF + capability[]=authtype + capability[]=state + protocol=http + host=$HTTPD_DEST + wwwauth[]=Negotiate + wwwauth[]=Basic realm="example.com" + EOF +' + +test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' ' + test_when_finished "per_test_cleanup" && + + set_credential_reply get <<-EOF && + username=alice + password=secret-passwd + EOF + + # Basic base64(alice:secret-passwd) + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF && + id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA== + EOF + + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF && + id=1 status=200 + id=default response=WWW-Authenticate: Negotiate + id=default response=WWW-Authenticate: Basic realm="example.com" + EOF + + test_config_global credential.helper test-helper && + GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-false" \ + git -c http.emptyAuth=false \ + ls-remote "$HTTPD_URL/custom_auth/repo.git" && + + # With emptyAuth=false, Negotiate is stripped immediately and + # credential_fill is called right away. Only one 401 response. + grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-false" >actual_401s && + test_line_count = 1 actual_401s +' + test_done