Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions GVFS/GVFS.Common/Git/GitAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,19 @@ public bool IsBackingOff
}
}

public bool IsAnonymous { get; private set; } = true;
/// <summary>
/// True only when the server is known to allow anonymous access.
/// </summary>
/// <remarks>
/// This defaults to false, because anonymous access is an affirmative
/// determination that requires a successful unauthenticated probe of
/// /gvfs/config. While this is true,
/// <see cref="Http.HttpRequestor.SendRequest"/> omits the Authorization
/// header and never calls <see cref="TryGetCredentials"/>, so a default of
/// true makes every request unauthenticated when the probe does not run or
/// does not complete.
/// </remarks>
public bool IsAnonymous { get; private set; }

/// <summary>
/// How long a caller of <see cref="TryGetCredentials"/> will wait for
Expand All @@ -66,6 +78,14 @@ public bool IsBackingOff
/// </summary>
internal int InitializationWaitTimeoutMs { get; set; } = BackgroundCredentialTimeoutMs;

/// <summary>
/// Test seam for the /gvfs/config probe. When null, production code uses
/// <see cref="ConfigHttpRequestor"/>. Unit tests substitute a fake so the
/// probe outcome - anonymous success, 401, or an indeterminate network
/// failure - can be driven deterministically.
/// </summary>
internal IGVFSConfigRequestor ConfigRequestorOverride { get; set; }

private GitSsl GitSsl { get; }

public void ApproveCredentials(ITracer tracer, string credentialString)
Expand Down Expand Up @@ -260,13 +280,21 @@ public bool TryInitializeAndQueryGVFSConfig(
errorMessage = null;
isAuthFailure = false;

using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig))
IGVFSConfigRequestor configRequestor = this.ConfigRequestorOverride ?? new ConfigHttpRequestor(tracer, enlistment, retryConfig);
using (configRequestor)
{
HttpStatusCode? httpStatus;

// First attempt without credentials. If anonymous access works,
// we get the config in a single request.
if (configRequestor.TryQueryGVFSConfig(false, out serverGVFSConfig, out httpStatus, out _))
//
// forceAnonymous is required, not incidental: this probe is what
// DETERMINES whether the server allows anonymous access, so it must
// not consult the answer it is computing. Without it, SendRequest
// sees IsAnonymous == false and calls TryGetCredentials, which waits
// for the initialization this very call stack is performing - a
// self-deadlock that stalls every mount until the wait times out.
if (configRequestor.TryQueryGVFSConfig(false, out serverGVFSConfig, out httpStatus, out _, forceAnonymous: true))
{
this.IsAnonymous = true;
this.MarkInitialized();
Expand All @@ -276,9 +304,42 @@ public bool TryInitializeAndQueryGVFSConfig(

if (httpStatus != HttpStatusCode.Unauthorized)
{
// The probe did not determine whether the server allows anonymous
// access. It failed for a reason unrelated to authentication - a
// timeout, a 5xx, or a socket error. Assume authentication is
// required. If the server does allow anonymous access it ignores
// the Authorization header we then send.
//
// Treating this as anonymous is unrecoverable for the life of the
// process: SendRequest would omit the Authorization header on every
// later request and never call TryGetCredentials, the Azure DevOps
// cache server answers 400 ("A valid Basic Authorization header is
// required."), a 400 is not retryable, RejectCredentials is a no-op
// because no credential was ever cached, and initialization is
// already latched - so re-initialization throws. Mount proceeds
// when a cache server is configured, so the repo stays mounted but
// cannot hydrate or enumerate until it is remounted.
// Assigning IsAnonymous here is defensive: the field already
// defaults to false and no earlier path in this method can set it
// true without returning, so this states the outcome explicitly
// rather than relying on the default staying false.
this.IsAnonymous = false;
this.MarkInitialized();
errorMessage = "Unable to query /gvfs/config";
tracer.RelatedWarning("{0}: Config query failed with status {1}", nameof(this.TryInitializeAndQueryGVFSConfig), httpStatus?.ToString() ?? "None");

// Emit this with Keywords.Telemetry so the population hitting an
// indeterminate probe is measurable in the field. The plain
// RelatedWarning(string, params object[]) overload traces with
// Keywords.None, which TelemetryDaemonEventListener filters out.
EventMetadata indeterminateMetadata = new EventMetadata(new Dictionary<string, object>
{
["Area"] = nameof(GitAuthentication),
["HttpStatus"] = httpStatus?.ToString() ?? "None",
});
tracer.RelatedWarning(
indeterminateMetadata,
$"{nameof(this.TryInitializeAndQueryGVFSConfig)}: Config query failed with status {httpStatus?.ToString() ?? "None"}; assuming authentication is required",
Keywords.Telemetry);
return false;
}

Expand Down
7 changes: 4 additions & 3 deletions GVFS/GVFS.Common/Http/ConfigHttpRequestor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace GVFS.Common.Http
{
public class ConfigHttpRequestor : HttpRequestor
public class ConfigHttpRequestor : HttpRequestor, IGVFSConfigRequestor
{
private readonly string repoUrl;

Expand All @@ -17,7 +17,7 @@ public ConfigHttpRequestor(ITracer tracer, Enlistment enlistment, RetryConfig re
this.repoUrl = enlistment.RepoUrl;
}

public bool TryQueryGVFSConfig(bool logErrors, out ServerGVFSConfig serverGVFSConfig, out HttpStatusCode? httpStatus, out string errorMessage)
public bool TryQueryGVFSConfig(bool logErrors, out ServerGVFSConfig serverGVFSConfig, out HttpStatusCode? httpStatus, out string errorMessage, bool forceAnonymous = false)
{
serverGVFSConfig = null;
httpStatus = null;
Expand Down Expand Up @@ -56,7 +56,8 @@ public bool TryQueryGVFSConfig(bool logErrors, out ServerGVFSConfig serverGVFSCo
gvfsConfigEndpoint,
HttpMethod.Get,
requestContent: null,
cancellationToken: CancellationToken.None))
cancellationToken: CancellationToken.None,
forceAnonymous: forceAnonymous))
{
if (response.HasErrors)
{
Expand Down
27 changes: 22 additions & 5 deletions GVFS/GVFS.Common/Http/HttpRequestor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,31 @@ public void Dispose()
}
}

/// <param name="forceAnonymous">
/// Sends the request without credentials regardless of the authentication
/// state. Required by the /gvfs/config probe that DETERMINES whether the
/// server allows anonymous access: that probe runs before initialization
/// completes, so it must not call <see cref="GitAuthentication.TryGetCredentials"/>,
/// which would wait for the initialization this very request is part of.
/// </param>
protected GitEndPointResponseData SendRequest(
long requestId,
Uri requestUri,
HttpMethod httpMethod,
string requestContent,
CancellationToken cancellationToken,
MediaTypeWithQualityHeaderValue acceptType = null)
MediaTypeWithQualityHeaderValue acceptType = null,
bool forceAnonymous = false)
{
// Resolve the anonymous decision once. Another thread can change
// IsAnonymous while this request is in flight, and the credential
// gate, the Authorization header, and the response handling below
// must all agree on a single value.
bool sendAnonymous = forceAnonymous || this.authentication.IsAnonymous;

string authString = null;
string errorMessage;
if (!this.authentication.IsAnonymous &&
if (!sendAnonymous &&
!this.authentication.TryGetCredentials(this.Tracer, out authString, out errorMessage))
{
return new GitEndPointResponseData(
Expand All @@ -127,7 +141,7 @@ protected GitEndPointResponseData SendRequest(

request.Headers.UserAgent.Add(this.userAgentHeader);

if (!this.authentication.IsAnonymous)
if (!sendAnonymous)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authString);
}
Expand Down Expand Up @@ -205,7 +219,7 @@ protected GitEndPointResponseData SendRequest(
string contentType = GetSingleHeaderOrEmpty(response.Content.Headers, "Content-Type");
responseMetadata.Add("ContentType", contentType);

if (!this.authentication.IsAnonymous)
if (!sendAnonymous)
{
this.authentication.ApproveCredentials(this.Tracer, authString);
}
Expand All @@ -227,8 +241,11 @@ protected GitEndPointResponseData SendRequest(
bool shouldRetry = ShouldRetry(response.StatusCode);

if (response.StatusCode == HttpStatusCode.Unauthorized &&
this.authentication.IsAnonymous)
sendAnonymous)
{
// The request carried no credentials, so there is nothing to
// reject. For the initial probe this is the definitive answer
// that the server requires authentication.
shouldRetry = false;
errorMessage = "Anonymous request was rejected with a 401";
}
Expand Down
33 changes: 33 additions & 0 deletions GVFS/GVFS.Common/Http/IGVFSConfigRequestor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Net;

namespace GVFS.Common.Http
{
/// <summary>
/// Queries the server's /gvfs/config endpoint. Extracted from
/// <see cref="ConfigHttpRequestor"/> so that
/// <see cref="Git.GitAuthentication.TryInitializeAndQueryGVFSConfig"/> can be
/// driven with a deterministic probe outcome in unit tests. Production code
/// always uses <see cref="ConfigHttpRequestor"/>.
/// </summary>
internal interface IGVFSConfigRequestor : IDisposable
{
/// <summary>
/// Queries /gvfs/config.
/// </summary>
/// <param name="logErrors">Whether to trace failures as errors.</param>
/// <param name="serverGVFSConfig">The parsed config when this returns true.</param>
/// <param name="httpStatus">
/// The HTTP status the server responded with, or null when the request
/// never produced an HTTP response (for example a DNS or socket failure).
/// </param>
/// <param name="errorMessage">The failure description when this returns false.</param>
/// <param name="forceAnonymous">
/// Sends the request without credentials regardless of the authentication
/// state. The probe that determines whether the server allows anonymous
/// access must set this.
/// </param>
/// <returns>True when the config was retrieved.</returns>
bool TryQueryGVFSConfig(bool logErrors, out ServerGVFSConfig serverGVFSConfig, out HttpStatusCode? httpStatus, out string errorMessage, bool forceAnonymous = false);
}
}
Loading
Loading