Skip to content

Commit

Permalink
Merge pull request #265: gvfs-helper: fix support for NTLM
Browse files Browse the repository at this point in the history
Fix NTLM support in gvfs-helper.

NTLM is handled magically by libcurl when we CURLAUTH_ANY is enabled AND
we passed empty username/password. This lets it negotiate with the server and
choose the best authentication scheme.
  • Loading branch information
derrickstolee authored and dscho committed May 20, 2020
2 parents 405df23 + 6c08fac commit 92aa019
Showing 1 changed file with 88 additions and 52 deletions.
140 changes: 88 additions & 52 deletions gvfs-helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -2729,6 +2729,84 @@ static void do_throttle_wait(struct gh__request_params *params,
}
}

static void set_main_creds_on_slot(struct active_request_slot *slot,
const struct credential *creds)
{
assert(creds == &gh__global.main_creds);

/*
* When talking to the main/origin server, we have 3 modes
* of operation:
*
* [1] The initial request is sent without loading creds
* and with ANY-AUTH set. (And the `":"` is a magic
* value.)
*
* This allows libcurl to negotiate for us if it can.
* For example, this allows NTLM to work by magic and
* we get 200s without ever seeing a 401. If libcurl
* cannot negotiate for us, it gives us a 401 (and all
* of the 401 code in this file responds to that).
*
* [2] A 401 retry will load the main creds and try again.
* This causes `creds->username`to be non-NULL (even
* if refers to a zero-length string). And we assume
* BASIC Authentication. (And a zero-length username
* is a convention for PATs, but then sometimes users
* put the PAT in their `username` field and leave the
* `password` field blank. And that works too.)
*
* [3] Subsequent requests on the same connection use
* whatever worked before.
*/
if (creds && creds->username) {
curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_easy_setopt(slot->curl, CURLOPT_USERNAME, creds->username);
curl_easy_setopt(slot->curl, CURLOPT_PASSWORD, creds->password);
} else {
curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_easy_setopt(slot->curl, CURLOPT_USERPWD, ":");
}
}

static void set_cache_server_creds_on_slot(struct active_request_slot *slot,
const struct credential *creds)
{
assert(creds == &gh__global.cache_creds);
assert(creds->username);

/*
* Things are weird when talking to a cache-server:
*
* [1] They don't send 401s on an auth error, rather they send
* a 400 (with a nice human-readable string in the html body).
* This prevents libcurl from doing any negotiation for us.
*
* [2] Cache-servers don't manage their own passwords, but
* rather require us to send the Basic Authentication
* username & password that we would send to the main
* server. (So yes, we have to get creds validated
* against the main server creds and substitute them when
* talking to the cache-server.)
*
* This means that:
*
* [a] We cannot support cache-servers that want to use NTLM.
*
* [b] If we want to talk to a cache-server, we have get the
* Basic Auth creds for the main server. And this may be
* problematic if the libcurl and/or the credential manager
* insists on using NTML and prevents us from getting them.
*
* So we never try AUTH-ANY and force Basic Auth (if possible).
*/
if (creds && creds->username) {
curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_easy_setopt(slot->curl, CURLOPT_USERNAME, creds->username);
curl_easy_setopt(slot->curl, CURLOPT_PASSWORD, creds->password);
}
}

/*
* Do a single HTTP request WITHOUT robust-retry, auth-retry or fallback.
*/
Expand Down Expand Up @@ -2794,37 +2872,10 @@ static void do_req(const char *url_base,
curl_easy_setopt(slot->curl, CURLOPT_HEADERFUNCTION, parse_resp_hdr);
curl_easy_setopt(slot->curl, CURLOPT_HEADERDATA, params);

if (creds && creds->username) {
/*
* Force CURL to respect the username/password we provide by
* turning off the AUTH-ANY negotiation stuff.
*
* That is, CURLAUTH_ANY causes CURL to NOT send the creds
* on an initial request in order to force a 401 and let it
* negotiate the best auth scheme and then retry.
*
* This is problematic when talking to the cache-servers
* because they send a 400 (with a "A valid Basic Auth..."
* message body) rather than a 401. This means that the
* the automatic retry will never happen. And even if we
* do force a retry, CURL still won't send the creds.
*
* So we turn it off and force it use our creds.
*/
curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_easy_setopt(slot->curl, CURLOPT_USERNAME, creds->username);
curl_easy_setopt(slot->curl, CURLOPT_PASSWORD, creds->password);
} else {
/*
* Turn on the AUTH-ANY negotiation. This only works
* with the main Git server (because the cache-server
* doesn't handle 401s).
*
* TODO Think about if we really need to handle this case.
* TODO Guard with "if (params->sever_type == __MAIN)"
*/
curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
}
if (params->server_type == GH__SERVER_TYPE__MAIN)
set_main_creds_on_slot(slot, creds);
else
set_cache_server_creds_on_slot(slot, creds);

if (params->progress_base_phase2_msg.len ||
params->progress_base_phase3_msg.len) {
Expand Down Expand Up @@ -2923,27 +2974,8 @@ static void do_req__to_main(const char *url_component,
params->server_type = GH__SERVER_TYPE__MAIN;

/*
* When talking to the main Git server, we do allow non-auth'd
* initial requests (mainly for testing, since production servers
* will usually always want creds), so we DO NOT force load the
* user's creds here and let the normal 401 mechanism handle things.
* This has a slight perf penalty -- if we're bulk fetching 4000
* objects from a production server, we're going to do a 100K+ byte
* POST just to get a 401 and then repeat the 100K+ byte POST
* with the creds.
*
* TODO Consider making a test or runtime flag to alter this.
* TODO If set/not-set, add a call here to pre-populate the creds.
* TODO
* TODO lookup_main_creds();
* TODO
*
* TODO Testing with GIT_TRACE_CURL shows that curl will *SOMETIME*
* TODO send an "Expect: 100-continue" and silently handle/eat the
* TODO "HTTP/1.1 100 Continue" before the POST-body is sent,
* TODO so maybe this isn't an issue. (Other than the extra
* TODO roundtrip for the first set of headers.) More testing here
* TODO should be performed.
* When talking to the main Git server, we DO NOT preload the
* creds before the first request.
*/

do_req__with_robust_retry(gh__global.main_url, url_component,
Expand All @@ -2968,6 +3000,10 @@ static void do_req__to_cache_server(const char *url_component,
{
params->server_type = GH__SERVER_TYPE__CACHE;

/*
* When talking to a cache-server, DO force load the creds.
* This implicitly preloads the creds to the main server.
*/
synthesize_cache_server_creds();

do_req__with_robust_retry(gh__global.cache_server_url, url_component,
Expand Down

0 comments on commit 92aa019

Please sign in to comment.