Skip to content

Commit a73496b

Browse files
committed
registry: add auto-detection via HTTP query
Add the ability to let host providers additionally inspect a HTTP response message to determine and identify a supported service/endpoint. If the simple enumeration of providers matching against the static `InputArguments` does not produce a match (at each priority level), then check against the lazily evaluated HEAD call to the remote URL.
1 parent 8a48eb0 commit a73496b

File tree

5 files changed

+76
-16
lines changed

5 files changed

+76
-16
lines changed

src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Net;
6+
using System.Net.Http;
67
using System.Threading.Tasks;
78
using Microsoft.Git.CredentialManager;
89
using Microsoft.Git.CredentialManager.Authentication.OAuth;
@@ -53,7 +54,17 @@ public bool IsSupported(InputArguments input)
5354
return (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") ||
5455
StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https")) &&
5556
hostName.EndsWith(BitbucketConstants.BitbucketBaseUrlHost, StringComparison.OrdinalIgnoreCase);
57+
}
58+
59+
public bool IsSupported(HttpResponseMessage response)
60+
{
61+
if (response is null)
62+
{
63+
return false;
64+
}
5665

66+
// TODO: identify Bitbucket on-prem instances from the HTTP response
67+
return false;
5768
}
5869

5970
public async Task<ICredential> GetCredentialAsync(InputArguments input)

src/shared/GitHub/GitHubHostProvider.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33
using System;
44
using System.Collections.Generic;
5+
using System.Net.Http;
56
using System.Threading.Tasks;
67
using Microsoft.Git.CredentialManager;
78
using Microsoft.Git.CredentialManager.Authentication.OAuth;
@@ -90,6 +91,17 @@ public override bool IsSupported(InputArguments input)
9091
return false;
9192
}
9293

94+
public override bool IsSupported(HttpResponseMessage response)
95+
{
96+
if (response is null)
97+
{
98+
return false;
99+
}
100+
101+
// Look for a known GitHub.com/GHES header
102+
return response.Headers.Contains("X-GitHub-Request-Id");
103+
}
104+
93105
public override string GetServiceName(InputArguments input)
94106
{
95107
var baseUri = new Uri(base.GetServiceName(input));

src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Net.Http;
67
using System.Threading.Tasks;
78
using Microsoft.Git.CredentialManager;
89
using Microsoft.Git.CredentialManager.Authentication;
@@ -58,6 +59,12 @@ public bool IsSupported(InputArguments input)
5859
UriHelpers.IsAzureDevOpsHost(hostName);
5960
}
6061

62+
public bool IsSupported(HttpResponseMessage response)
63+
{
64+
// Azure DevOps Server (TFS) is handled by the generic provider, which supports basic auth, and WIA detection.
65+
return false;
66+
}
67+
6168
public async Task<ICredential> GetCredentialAsync(InputArguments input)
6269
{
6370
string service = GetServiceName(input);
@@ -83,7 +90,7 @@ public async Task<ICredential> GetCredentialAsync(InputArguments input)
8390
return credential;
8491
}
8592

86-
public virtual Task StoreCredentialAsync(InputArguments input)
93+
public Task StoreCredentialAsync(InputArguments input)
8794
{
8895
string service = GetServiceName(input);
8996

src/shared/Microsoft.Git.CredentialManager/HostProvider.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Net.Http;
67
using System.Threading.Tasks;
78

89
namespace Microsoft.Git.CredentialManager
@@ -34,6 +35,13 @@ public interface IHostProvider : IDisposable
3435
/// <returns>True if the provider supports the Git credential request, false otherwise.</returns>
3536
bool IsSupported(InputArguments input);
3637

38+
/// <summary>
39+
/// Determine if the <see cref="HttpResponseMessage"/> identifies a recognized Git hosting provider.
40+
/// </summary>
41+
/// <param name="response">Response message of an endpoint query.</param>
42+
/// <returns>True if the provider supports the host provider at the endpoint, false otherwise.</returns>
43+
bool IsSupported(HttpResponseMessage response);
44+
3745
/// <summary>
3846
/// Get a credential for accessing the remote Git repository on this hosting service.
3947
/// </summary>
@@ -78,6 +86,11 @@ protected HostProvider(ICommandContext context)
7886

7987
public abstract bool IsSupported(InputArguments input);
8088

89+
public virtual bool IsSupported(HttpResponseMessage response)
90+
{
91+
return false;
92+
}
93+
8194
/// <summary>
8295
/// Return a string that uniquely identifies the service that a credential should be stored against.
8396
/// </summary>

src/shared/Microsoft.Git.CredentialManager/HostProviderRegistry.cs

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Net.Http;
67
using System.Threading.Tasks;
78

89
namespace Microsoft.Git.CredentialManager
@@ -45,7 +46,7 @@ public interface IHostProviderRegistry : IDisposable
4546

4647
/// <summary>
4748
/// Host provider registry where each provider is queried by priority order until the first
48-
/// provider that supports the credential query is found.
49+
/// provider that supports the credential query or matches the endpoint query is found.
4950
/// </summary>
5051
public class HostProviderRegistry : IHostProviderRegistry
5152
{
@@ -87,7 +88,7 @@ public void Register(IHostProvider hostProvider, HostProviderPriority priority)
8788
providers.Add(hostProvider);
8889
}
8990

90-
public Task<IHostProvider> GetProviderAsync(InputArguments input)
91+
public async Task<IHostProvider> GetProviderAsync(InputArguments input)
9192
{
9293
IHostProvider provider;
9394

@@ -111,7 +112,7 @@ public Task<IHostProvider> GetProviderAsync(InputArguments input)
111112
}
112113
else
113114
{
114-
return Task.FromResult(provider);
115+
return provider;
115116
}
116117
}
117118
}
@@ -137,7 +138,7 @@ public Task<IHostProvider> GetProviderAsync(InputArguments input)
137138
}
138139
else
139140
{
140-
return Task.FromResult(provider);
141+
return provider;
141142
}
142143
}
143144
}
@@ -147,35 +148,51 @@ public Task<IHostProvider> GetProviderAsync(InputArguments input)
147148
//
148149
_context.Trace.WriteLine("Performing auto-detection of host provider.");
149150

150-
IHostProvider MatchProvider(HostProviderPriority priority)
151+
var uri = input.GetRemoteUri();
152+
var queryResponse = new Lazy<Task<HttpResponseMessage>>(() =>
153+
{
154+
_context.Trace.WriteLine("Querying remote URL for host provider auto-detection.");
155+
return HttpClient.HeadAsync(uri);
156+
});
157+
158+
async Task<IHostProvider> MatchProviderAsync(HostProviderPriority priority)
151159
{
152160
if (_hostProviders.TryGetValue(priority, out ICollection<IHostProvider> providers))
153161
{
154162
_context.Trace.WriteLine($"Checking against {providers.Count} host providers registered with priority '{priority}'.");
155163

164+
// Try matching using the static Git input arguments first (cheap)
156165
if (providers.TryGetFirst(x => x.IsSupported(input), out IHostProvider match))
157166
{
158167
return match;
159168
}
160-
}
161169

162-
return null;
163-
}
170+
HttpResponseMessage response = await queryResponse.Value;
164171

165-
provider = MatchProvider(HostProviderPriority.High) ??
166-
MatchProvider(HostProviderPriority.Normal) ??
167-
MatchProvider(HostProviderPriority.Low);
172+
// Try matching using the HTTP response from a query to the remote URL (expensive)
173+
if (providers.TryGetFirst(x => x.IsSupported(response), out match))
174+
{
175+
return match;
176+
}
177+
}
168178

169-
if (provider is null)
170-
{
171-
throw new Exception("No host provider available to service this request.");
179+
return null;
172180
}
173181

174-
return Task.FromResult(provider);
182+
// Match providers starting with the highest priority
183+
return await MatchProviderAsync(HostProviderPriority.High) ??
184+
await MatchProviderAsync(HostProviderPriority.Normal) ??
185+
await MatchProviderAsync(HostProviderPriority.Low) ??
186+
throw new Exception("No host provider available to service this request.");
175187
}
176188

189+
private HttpClient _httpClient;
190+
private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient();
191+
177192
public void Dispose()
178193
{
194+
_httpClient?.Dispose();
195+
179196
// Dispose of all registered providers to give them a chance to clean up and release any resources
180197
foreach (IHostProvider provider in _hostProviders.Values)
181198
{

0 commit comments

Comments
 (0)