public Task <INetworkResponseMessage> HttpHeadAsync(TargetUri targetUri, NetworkRequestOptions options) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } return(Task.Run <INetworkResponseMessage>(async() => { using (var httpMessageHandler = GetHttpMessageHandler(targetUri, options)) using (var httpClient = GetHttpClient(targetUri, httpMessageHandler, options)) using (var requestMessage = new HttpRequestMessage(HttpMethod.Head, targetUri)) { // Copy the headers from the client into the message because the framework // will not do this when using `SendAsync`. foreach (var header in httpClient.DefaultRequestHeaders) { requestMessage.Headers.Add(header.Key, header.Value); } var httpMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); var response = new NetworkResponseMessage(httpMessage); if (httpMessage.Content != null) { await response.SetContent(httpMessage.Content); } return response; } })); }
internal async Task <TargetUri> GetIdentityServiceUri(TargetUri targetUri, Secret authorization) { const string LocationServiceUrlPathAndQuery = "_apis/ServiceDefinitions/LocationService2/951917AC-A960-4999-8464-E3F0AA25B381?api-version=1.0"; if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (authorization is null) { throw new ArgumentNullException(nameof(authorization)); } string tenantUrl = targetUri.ToString(false, true, false); if (targetUri.TargetUriContainsUsername) { string escapedUserInfo = Uri.EscapeUriString(targetUri.TargetUriUsername); tenantUrl = tenantUrl + escapedUserInfo + "/"; } var locationServiceUrl = tenantUrl + LocationServiceUrlPathAndQuery; var requestUri = new TargetUri(locationServiceUrl, targetUri.ProxyUri?.ToString()); var options = new NetworkRequestOptions(true) { Authorization = authorization, }; try { using (var response = await Network.HttpGetAsync(requestUri, options)) { if (response.IsSuccessStatusCode) { using (HttpContent content = response.Content) { string responseText = await content.ReadAsStringAsync(); Match match; if ((match = Regex.Match(responseText, @"\""location\""\:\""([^\""]+)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success) { string identityServiceUrl = match.Groups[1].Value; var idenitityServiceUri = new Uri(identityServiceUrl, UriKind.Absolute); return(new TargetUri(idenitityServiceUri, targetUri.ProxyUri)); } } } Trace.WriteLine($"failed to find Identity Service for '{targetUri}' via location service [{(int)response.StatusCode} {response.ReasonPhrase}]."); } } catch (Exception exception) { Trace.WriteException(exception); throw new VstsLocationServiceException($"Failed to find Identity Service for `{targetUri}`.", exception); } return(null); }
public async Task <Token> GeneratePersonalAccessToken(TargetUri targetUri, Token authorization, VstsTokenScope tokenScope, bool requireCompactToken, TimeSpan?tokenDuration = null) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (authorization is null) { throw new ArgumentNullException(nameof(authorization)); } if (tokenScope is null) { throw new ArgumentNullException(nameof(tokenScope)); } try { var requestUri = await CreatePersonalAccessTokenRequestUri(targetUri, authorization, requireCompactToken); var options = new NetworkRequestOptions(true) { Authorization = authorization, }; using (StringContent content = GetAccessTokenRequestBody(targetUri, tokenScope, tokenDuration)) using (var response = await Network.HttpPostAsync(requestUri, content, options)) { if (response.IsSuccessStatusCode) { string responseText = await response.Content.ReadAsStringAsync(); if (!string.IsNullOrWhiteSpace(responseText)) { // Find the 'token : <value>' portion of the result content, if any. Match tokenMatch = null; if ((tokenMatch = Regex.Match(responseText, @"\s*""token""\s*:\s*""([^\""]+)""\s*", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success) { string tokenValue = tokenMatch.Groups[1].Value; Token token = new Token(tokenValue, TokenType.Personal); Trace.WriteLine($"personal access token acquisition for '{targetUri}' succeeded."); return(token); } } } Trace.WriteLine($"failed to acquire personal access token for '{targetUri}' [{(int)response.StatusCode} {response.ReasonPhrase}]."); } } catch (Exception exception) { Trace.WriteException(exception); } Trace.WriteLine($"personal access token acquisition for '{targetUri}' failed."); return(null); }
public async Task <bool> PopulateTokenTargetId(TargetUri targetUri, Token authorization) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (authorization is null) { throw new ArgumentNullException(nameof(authorization)); } try { // Create an request to the VSTS deployment data end-point. var requestUri = GetConnectionDataUri(targetUri); var options = new NetworkRequestOptions(true) { Authorization = authorization, }; // Send the request and wait for the response. using (var response = await Network.HttpGetAsync(requestUri, options)) { if (response.IsSuccessStatusCode) { string content = await response.Content.ReadAsStringAsync(); Match match; if ((match = Regex.Match(content, @"""instanceId""\s*\:\s*""([^""]+)""", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)).Success && match.Groups.Count == 2) { string resultId = match.Groups[1].Value; if (Guid.TryParse(resultId, out Guid instanceId)) { Trace.WriteLine($"target identity is '{resultId}'."); authorization.TargetIdentity = instanceId; return(true); } } } Trace.WriteLine($"failed to acquire the token's target identity for `{targetUri}` [{(int)response.StatusCode} {response.ReasonPhrase}]."); } } catch (HttpRequestException exception) { Trace.WriteLine("failed to acquire the token's target identity for `{targetUri}`, an error happened before the server could respond."); Trace.WriteException(exception); } return(false); }
/// <summary> /// Validates that a `<see cref="Token"/>` is valid to grant access to the VSTS resource referenced by `<paramref name="targetUri"/>`. /// <para/> /// Returns `<see langword="true"/>` if successful; otherwise `<see langword="false"/>`. /// </summary> /// <param name="targetUri">URI of the VSTS resource.</param> /// <param name="token">`<see cref="Token"/>` expected to grant access to the VSTS resource.</param> public async Task <bool> ValidateToken(TargetUri targetUri, Token token) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (token is null) { throw new ArgumentNullException(nameof(token)); } // Personal access tokens are effectively credentials, treat them as such. if (token.Type == TokenType.Personal) { return(await ValidateCredentials(targetUri, (Credential)token)); } try { // Create an request to the VSTS deployment data end-point. var requestUri = GetConnectionDataUri(targetUri); var options = new NetworkRequestOptions(true) { Authorization = token, }; // Send the request and wait for the response. using (var response = await Network.HttpGetAsync(requestUri, options)) { if (!response.IsSuccessStatusCode) { // Even if the service responded, if the issue isn't a 400 class response then // the credentials were likely not rejected. if ((int)response.StatusCode >= 400 && (int)response.StatusCode < 500) { return(false); } Trace.WriteLine($"unable to validate credentials due to '{response.StatusCode}'."); return(true); } } } catch (Exception exception) { Trace.WriteLine($"! error: '{exception.Message}'."); }; Trace.WriteLine($"token validation for '{targetUri}' failed."); return(false); }
public static async Task <AuthenticationHeaderValue[]> GetHeaderValues(RuntimeContext context, TargetUri targetUri) { if (context is null) { throw new ArgumentNullException(nameof(context)); } if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (targetUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.Ordinal) || targetUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.Ordinal)) { try { // Configure the HTTP request to not choose an authentication strategy for us // because we want to deliver the complete payload to the caller. var options = new NetworkRequestOptions(false) { Flags = NetworkRequestOptionFlags.UseProxy, Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), }; var requestUri = targetUri; // Since we're more likely to get an accurate response from the actual-URL, // use it if available, otherwise use `targetUri` as supplied; but only if // the actual-URL differs from the query-URL. if (targetUri.ActualUri != null && targetUri.ActualUri != targetUri.QueryUri) { requestUri = targetUri.CreateWith(queryUri: targetUri.ActualUri); } // Make the request and return the response. using (var result = await context.Network.HttpHeadAsync(requestUri, options)) { return(result.Headers?.WwwAuthenticate?.ToArray() ?? NullResult); } } catch (Exception exception) { context.Trace.WriteLine("error testing targetUri for NTLM."); context.Trace.WriteException(exception); } } return(NullResult); }
public async Task <HttpResponseMessage> HttpGetAsync(TargetUri targetUri, NetworkRequestOptions options) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } // Craft the request header for the GitHub v3 API w/ credentials; using (var handler = GetHttpMessageHandler(targetUri, options)) using (var httpClient = GetHttpClient(targetUri, handler, options)) { return(await httpClient.GetAsync(targetUri)); } }
public async Task <bool> ValidateCredentials(TargetUri targetUri, Credential credentials) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (credentials is null) { throw new ArgumentNullException(nameof(credentials)); } try { // Create an request to the VSTS deployment data end-point. var requestUri = GetConnectionDataUri(targetUri); var options = new NetworkRequestOptions(true) { Authorization = credentials, }; // Send the request and wait for the response. using (var response = await Network.HttpGetAsync(requestUri, options)) { if (response.IsSuccessStatusCode) { return(true); } Trace.WriteLine($"credential validation for '{targetUri}' failed [{(int)response.StatusCode} {response.ReasonPhrase}]."); // Even if the service responded, if the issue isn't a 400 class response then // the credentials were likely not rejected. if ((int)response.StatusCode < 400 && (int)response.StatusCode >= 500) { return(true); } } } catch (Exception exception) { Trace.WriteException(exception); } Trace.WriteLine($"credential validation for '{targetUri}' failed."); return(false); }
public static async Task <AuthenticationHeaderValue[]> GetHeaderValues(RuntimeContext context, TargetUri targetUri) { if (context is null) { throw new ArgumentNullException(nameof(context)); } if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (targetUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.Ordinal) || targetUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.Ordinal)) { try { // Configure the HTTP request to not choose an authentication strategy for us // because we want to deliver the complete payload to the caller. var options = new NetworkRequestOptions(false) { Flags = NetworkRequestOptionFlags.UseProxy, Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), }; // Make the request and return the response. using (var result = await context.Network.HttpHeadAsync(targetUri, options)) { return(result.Headers?.WwwAuthenticate?.ToArray() ?? NullResult); } } catch (Exception exception) { context.Trace.WriteLine("error testing targetUri for NTLM."); context.Trace.WriteException(exception); } } return(NullResult); }
public Task <INetworkResponseMessage> HttpGetAsync(TargetUri targetUri, NetworkRequestOptions options) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } return(Task.Run <INetworkResponseMessage>(async() => { using (var handler = GetHttpMessageHandler(targetUri, options)) using (var httpClient = GetHttpClient(targetUri, handler, options)) { var httpMessage = await httpClient.GetAsync(targetUri); var response = new NetworkResponseMessage(httpMessage); if (httpMessage.Content != null) { await response.SetContent(httpMessage.Content); } return response; } })); }
public async Task <bool> ValidateToken(TargetUri targetUri, Token token) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (token is null) { throw new ArgumentNullException(nameof(token)); } // Personal access tokens are effectively credentials, treat them as such. if (token.Type == TokenType.Personal) { return(await ValidateCredentials(targetUri, (Credential)token)); } try { // Create an request to the VSTS deployment data end-point. var requestUri = GetConnectionDataUri(targetUri); var options = new NetworkRequestOptions(true) { Authorization = token, }; // Send the request and wait for the response. using (var response = await Network.HttpGetAsync(requestUri, options)) { if (!response.IsSuccessStatusCode) { // Even if the service responded, if the issue isn't a 400 class response then // the credentials were likely not rejected. if ((int)response.StatusCode < 400 && (int)response.StatusCode >= 500) { Trace.WriteLine($"unable to validate credentials for '{targetUri}', unexpected response [{(int)response.StatusCode} {response.ReasonPhrase}]."); return(true); } Trace.WriteLine($"credential validation for '{targetUri}' failed [{(int)response.StatusCode} {response.ReasonPhrase}]."); return(false); } } } catch (HttpRequestException exception) { // Since we're unable to invalidate the credentials, return optimistic results. // This avoid credential invalidation due to network instability, etc. Trace.WriteLine($"unable to validate credentials for '{targetUri}', failure occurred before server could respond."); Trace.WriteException(exception); return(true); } catch (Exception exception) { Trace.WriteException(exception); }; Trace.WriteLine($"token validation for '{targetUri}' failed."); return(false); }
public static async Task <Guid?> DetectAuthority(RuntimeContext context, TargetUri targetUri) { const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; if (context is null) { throw new ArgumentNullException(nameof(context)); } if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } var tenantId = Guid.Empty; if (VstsAzureAuthority.IsVstsUrl(targetUri)) { var tenantUrl = VstsAzureAuthority.GetTargetUrl(targetUri); context.Trace.WriteLine($"'{targetUri}' is a member '{tenantUrl}', checking AAD vs MSA."); if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttp) || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttps)) { // Read the cache from disk. var cache = await DeserializeTenantCache(context); // Check the cache for an existing value. if (cache.TryGetValue(tenantUrl, out tenantId)) { return(tenantId); } var options = new NetworkRequestOptions(false) { Flags = NetworkRequestOptionFlags.UseProxy, Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), }; try { var tenantUri = new TargetUri(tenantUrl, targetUri.ProxyUri?.ToString()); using (var response = await context.Network.HttpHeadAsync(tenantUri, options)) { if (response.Headers != null && response.Headers.TryGetValues(VstsResourceTenantHeader, out IEnumerable <string> values)) { foreach (string value in values) { // Try to find a non-empty value for the resource-tenant identity if (!string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out tenantId) && tenantId != Guid.Empty) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(context, cache); // Success, notify the caller return(tenantId); } } // Since we did not find a better identity, fallback to the default (Guid.Empty). return(tenantId); } else { context.Trace.WriteLine($"unable to get response from '{targetUri}' [{(int)response.StatusCode} {response.StatusCode}]."); } } } catch (HttpRequestException exception) { context.Trace.WriteLine($"unable to get response from '{targetUri}', an error occurred before the server could respond."); context.Trace.WriteException(exception); } } else { context.Trace.WriteLine($"detected non-http(s) based protocol: '{targetUri.Scheme}'."); } } if (StringComparer.OrdinalIgnoreCase.Equals(VstsBaseUrlHost, targetUri.Host)) { return(Guid.Empty); } // Fallback to basic authentication. return(null); }
private HttpClient GetHttpClient(TargetUri targetUri, HttpMessageHandler handler, NetworkRequestOptions options) { var httpClient = new HttpClient(handler); if (options != null) { if (options.Timeout > TimeSpan.Zero) { httpClient.Timeout = options.Timeout; } if (options.Headers != null) { foreach (var header in options.Headers) { httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); } } // Manually add the correct headers for the type of authentication that is happening because if // we rely on the framework to correctly write the headers neither GitHub nor VSTS authentication // works reliably. if (options.Authorization != null) { switch (options.Authorization) { case Token token: { // Different types of tokens are packed differently. switch (token.Type) { case TokenType.AzureAccess: case TokenType.BitbucketAccess: { // ADAL access tokens are packed into the Authorization header. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Value); } break; case TokenType.AzureFederated: { // Federated authentication tokens are sent as cookie(s). httpClient.DefaultRequestHeaders.Add("Cookie", token.Value); } break; case TokenType.Personal: { // Personal access tokens are designed to treated like credentials, // so treat them like credentials. var credentials = (Credential)token; // Credentials are packed into the 'Authorization' header as a base64 encoded pair. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials.ToBase64String()); } break; default: Trace.WriteLine("! unsupported token type, not appending an authentication header to the request."); break; } } break; case Credential credentials: { // Credentials are packed into the 'Authorization' header as a base64 encoded pair. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials.ToBase64String()); } break; } } } // Ensure that the user-agent string is set. if (httpClient.DefaultRequestHeaders.UserAgent is null || httpClient.DefaultRequestHeaders.UserAgent.Count == 0) { httpClient.DefaultRequestHeaders.Add("User-Agent", Global.UserAgent); } return(httpClient); }
private static HttpMessageHandler GetHttpMessageHandler(TargetUri targetUri, NetworkRequestOptions options) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } var handler = new HttpClientHandler(); if (options != null) { if ((options.Flags & NetworkRequestOptionFlags.AllowRedirections) != 0 && options.MaxRedirections <= 0) { var inner = new ArgumentOutOfRangeException(nameof(options.MaxRedirections)); throw new ArgumentException("`NetworkRequestOption.MaxRedirections` must be greater than zero with `NetworkRequestOptionFlags.AllowAutoRedirect`.", nameof(options), inner); } if (options.CookieContainer is null && (options.Flags & NetworkRequestOptionFlags.UseCookies) != 0) { var inner = new ArgumentNullException(nameof(options.CookieContainer)); throw new ArgumentException("`NetworkRequestOption.CookieContainer` cannot be null with `NetworkRequestOptionFlags.UseCookies`.", nameof(options), inner); } if ((options.Flags & NetworkRequestOptionFlags.UseCredentials) != 0 && options.Authorization is null) { var inner = new ArgumentNullException(nameof(options.Authorization)); throw new ArgumentException("`NetworkRequestOption.Authentication` cannot be null with `NetworkRequestOptionFlags.UseCredentials`.", nameof(options), inner); } if ((options.Flags & NetworkRequestOptionFlags.AllowRedirections) == 0 || options.MaxRedirections <= 0) { handler.AllowAutoRedirect = false; } else { handler.AllowAutoRedirect = true; handler.MaxAutomaticRedirections = options.MaxRedirections; } if ((options.Flags & NetworkRequestOptionFlags.UseCookies) != 0 && options.CookieContainer != null) { handler.CookieContainer = options.CookieContainer; handler.UseCookies = true; } if ((options.Flags & NetworkRequestOptionFlags.UseCredentials) != 0 && options.Authorization != null) { handler.UseDefaultCredentials = false; } handler.PreAuthenticate = (options.Flags & NetworkRequestOptionFlags.PreAuthenticate) != 0; if ((options.Flags & NetworkRequestOptionFlags.UseProxy) != 0 && targetUri.ProxyUri != null) { var proxy = GetHttpWebProxy(targetUri); if (proxy != null) { handler.Proxy = proxy; handler.UseProxy = true; } } } return(handler); }
public static async Task <Guid?> DetectAuthority(RuntimeContext context, TargetUri targetUri) { const string VstsBaseUrlHost = "visualstudio.com"; const string VstsResourceTenantHeader = "X-VSS-ResourceTenant"; BaseSecureStore.ValidateTargetUri(targetUri); var tenantId = Guid.Empty; if (targetUri.Host.EndsWith(VstsBaseUrlHost, StringComparison.OrdinalIgnoreCase)) { context.Trace.WriteLine($"'{targetUri}' is subdomain of '{VstsBaseUrlHost}', checking AAD vs MSA."); string tenant = null; if (StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttp) || StringComparer.OrdinalIgnoreCase.Equals(targetUri.Scheme, Uri.UriSchemeHttps)) { // Query the cache first. string tenantUrl = targetUri.ToString(); // Read the cache from disk. var cache = await DeserializeTenantCache(context); // Check the cache for an existing value. if (cache.TryGetValue(tenantUrl, out tenantId)) { return(tenantId); } var options = new NetworkRequestOptions(false) { Flags = NetworkRequestOptionFlags.UseProxy, Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), }; try { using (var response = await context.Network.HttpGetAsync(targetUri, options)) { if (response.IsSuccessStatusCode) { if (response.Headers.TryGetValues(VstsResourceTenantHeader, out IEnumerable <string> values)) { tenant = System.Linq.Enumerable.First(values); if (!string.IsNullOrWhiteSpace(tenant) && Guid.TryParse(tenant, out tenantId)) { // Update the cache. cache[tenantUrl] = tenantId; // Write the cache to disk. await SerializeTenantCache(context, cache); // Success, notify the caller return(tenantId); } } } else { context.Trace.WriteLine($"unable to get response from '{targetUri}', server responded with '{(int)response.StatusCode} {response.StatusCode}'."); } } } catch (HttpRequestException exception) { context.Trace.WriteLine($"unable to get response from '{targetUri}' due to '{exception.Message}'."); } } else { context.Trace.WriteLine($"detected non-https based protocol: '{targetUri.Scheme}'."); } } // Fallback to basic authentication. return(null); }
public async Task <HttpResponseMessage> HttpPostAsync(TargetUri targetUri, HttpContent content, NetworkRequestOptions options) { if (targetUri is null) { throw new ArgumentNullException(nameof(targetUri)); } if (content is null) { throw new ArgumentNullException(nameof(content)); } using (var handler = GetHttpMessageHandler(targetUri, options)) using (var httpClient = GetHttpClient(targetUri, handler, options)) { return(await httpClient.PostAsync(targetUri, content)); } }