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
129 changes: 77 additions & 52 deletions GVFS/GVFS.Common/Git/GitAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,34 +183,98 @@ public bool TryGetCredentials(ITracer tracer, out string credentialString, out s
return true;
}

/// <summary>
/// Initialize authentication by probing the server. Determines whether
/// anonymous access is supported and, if not, fetches credentials.
/// Callers that also need the GVFS config should use
/// <see cref="TryInitializeAndQueryGVFSConfig"/> instead to avoid a
/// redundant HTTP round-trip.
/// </summary>
public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string errorMessage)
{
// Delegate to the combined method, discarding the config result.
// This avoids duplicating the anonymous-probe + credential-fetch logic.
return this.TryInitializeAndQueryGVFSConfig(
tracer,
enlistment,
new RetryConfig(),
out _,
out errorMessage);
}

/// <summary>
/// Combines authentication initialization with the GVFS config query,
/// eliminating a redundant HTTP round-trip. The anonymous probe and
/// config query use the same request to /gvfs/config:
/// 1. Config query → /gvfs/config → 200 (anonymous) or 401
/// 2. If 401: credential fetch, then retry → 200
/// This saves one HTTP request compared to probing auth separately
/// and then querying config, and reuses the same TCP/TLS connection.
/// </summary>
public bool TryInitializeAndQueryGVFSConfig(
ITracer tracer,
Enlistment enlistment,
RetryConfig retryConfig,
out ServerGVFSConfig serverGVFSConfig,
out string errorMessage)
{
if (this.isInitialized)
{
throw new InvalidOperationException("Already initialized");
}

serverGVFSConfig = null;
errorMessage = null;

bool isAnonymous;
if (!this.TryAnonymousQuery(tracer, enlistment, out isAnonymous))
using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig))
{
errorMessage = $"Unable to determine if authentication is required";
return false;
}
HttpStatusCode? httpStatus;

if (!isAnonymous &&
!this.TryCallGitCredential(tracer, out errorMessage))
{
// 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 _))
{
this.IsAnonymous = true;
this.isInitialized = true;
tracer.RelatedInfo("{0}: Anonymous access succeeded, config obtained in one request", nameof(this.TryInitializeAndQueryGVFSConfig));
return true;
}

if (httpStatus != HttpStatusCode.Unauthorized)
{
errorMessage = "Unable to query /gvfs/config";
tracer.RelatedWarning("{0}: Config query failed with status {1}", nameof(this.TryInitializeAndQueryGVFSConfig), httpStatus?.ToString() ?? "None");
return false;
}

// Server requires authentication — fetch credentials
this.IsAnonymous = false;

if (!this.TryCallGitCredential(tracer, out errorMessage))
{
tracer.RelatedWarning("{0}: Credential fetch failed: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage);
return false;
}

this.isInitialized = true;

// Retry with credentials using the same ConfigHttpRequestor (reuses HttpClient/connection)
if (configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out errorMessage))
{
tracer.RelatedInfo("{0}: Config obtained with credentials", nameof(this.TryInitializeAndQueryGVFSConfig));
return true;
}

tracer.RelatedWarning("{0}: Config query failed with credentials: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage);
return false;
}

this.IsAnonymous = isAnonymous;
this.isInitialized = true;
return true;
}

public bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage)
/// <summary>
/// Test-only initialization that skips the network probe and goes
/// straight to credential fetch. Not for production use.
/// </summary>
internal bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage)
{
if (this.isInitialized)
{
Expand Down Expand Up @@ -267,45 +331,6 @@ private static bool TryParseCredentialString(string credentialString, out string
return false;
}

private bool TryAnonymousQuery(ITracer tracer, Enlistment enlistment, out bool isAnonymous)
{
bool querySucceeded;
using (ITracer anonymousTracer = tracer.StartActivity("AttemptAnonymousAuth", EventLevel.Informational))
{
HttpStatusCode? httpStatus;

using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(anonymousTracer, enlistment, new RetryConfig()))
{
ServerGVFSConfig gvfsConfig;
const bool LogErrors = false;
if (configRequestor.TryQueryGVFSConfig(LogErrors, out gvfsConfig, out httpStatus, out _))
{
querySucceeded = true;
isAnonymous = true;
}
else if (httpStatus == HttpStatusCode.Unauthorized)
{
querySucceeded = true;
isAnonymous = false;
}
else
{
querySucceeded = false;
isAnonymous = false;
}
}

anonymousTracer.Stop(new EventMetadata
{
{ "HttpStatus", httpStatus.HasValue ? ((int)httpStatus).ToString() : "None" },
{ "QuerySucceeded", querySucceeded },
{ "IsAnonymous", isAnonymous },
});
}

return querySucceeded;
}

private DateTime GetNextAuthAttemptTime()
{
if (this.numberOfAttempts <= 1)
Expand Down
58 changes: 58 additions & 0 deletions GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;

namespace GVFS.Common.Git
{
[Flags]
public enum GitCoreGVFSFlags
{
// GVFS_SKIP_SHA_ON_INDEX
// Disables the calculation of the sha when writing the index
SkipShaOnIndex = 1 << 0,

// GVFS_BLOCK_COMMANDS
// Blocks git commands that are not allowed in a GVFS/Scalar repo
BlockCommands = 1 << 1,

// GVFS_MISSING_OK
// Normally git write-tree ensures that the objects referenced by the
// directory exist in the object database.This option disables this check.
MissingOk = 1 << 2,

// GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT
// When marking entries to remove from the index and the working
// directory this option will take into account what the
// skip-worktree bit was set to so that if the entry has the
// skip-worktree bit set it will not be removed from the working
// directory. This will allow virtualized working directories to
// detect the change to HEAD and use the new commit tree to show
// the files that are in the working directory.
NoDeleteOutsideSparseCheckout = 1 << 3,

// GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK
// While performing a fetch with a virtual file system we know
// that there will be missing objects and we don't want to download
// them just because of the reachability of the commits. We also
// don't want to download a pack file with commits, trees, and blobs
// since these will be downloaded on demand. This flag will skip the
// checks on the reachability of objects during a fetch as well as
// the upload pack so that extraneous objects don't get downloaded.
FetchSkipReachabilityAndUploadPack = 1 << 4,

// 1 << 5 has been deprecated

// GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS
// With a virtual file system we only know the file size before any
// CRLF or smudge/clean filters processing is done on the client.
// To prevent file corruption due to truncation or expansion with
// garbage at the end, these filters must not run when the file
// is first accessed and brought down to the client. Git.exe can't
// currently tell the first access vs subsequent accesses so this
// flag just blocks them from occurring at all.
BlockFiltersAndEolConversions = 1 << 6,

// GVFS_PREFETCH_DURING_FETCH
// While performing a `git fetch` command, use the gvfs-helper to
// perform a "prefetch" of commits and trees.
PrefetchDuringFetch = 1 << 7,
}
}
Loading
Loading