200b84f49a
* Add Instances base (#6) * Use Uri for checks in GetAccounts function (#8) * Add integration and perf tests to sln (#9) * Remove perf tests (#10) * remove perf tests * do all unit tests * Code coverage (#11) * code coverage * enable codecov for GA * Update README.md * Update coverage and dependencies (#12) * Update coverage and dependencies * fmt * add codecov config * merge DUI3/Alpha into sdk (#13) * merge DUI3/Alpha into sdk * formatting * Merge Objects dui3/alpha -> dev (#14) * merge DUI3/Alpha into sdk * formatting * Objects changes * Objects tests * Unit test project * update codecov to be less intrusive (#15) * update codecov to be less intrusive * fix codecov yaml * add coverage exclusion * Merge sharp `dui3/alpha` -> sdk `main` (#16) * Merge * csharpier format * Fixed polysharp issues * Integration Tests * Fixes * Some nullability fixes (#17) * add coverage exclusion * fix some tests and fix nullability errors --------- Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com> Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
253 lines
8.6 KiB
C#
253 lines
8.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.NetworkInformation;
|
|
using System.Net.Sockets;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Polly;
|
|
using Polly.Contrib.WaitAndRetry;
|
|
using Polly.Extensions.Http;
|
|
using Polly.Retry;
|
|
using Serilog.Context;
|
|
using Speckle.Core.Common;
|
|
using Speckle.Core.Credentials;
|
|
using Speckle.Core.Logging;
|
|
|
|
namespace Speckle.Core.Helpers;
|
|
|
|
public static class Http
|
|
{
|
|
public static IEnumerable<TimeSpan> DefaultDelay()
|
|
{
|
|
return Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromMilliseconds(100), 5);
|
|
}
|
|
|
|
public static AsyncRetryPolicy<HttpResponseMessage> HttpAsyncPolicy(IEnumerable<TimeSpan>? delay = null)
|
|
{
|
|
return HttpPolicyExtensions
|
|
.HandleTransientHttpError()
|
|
.WaitAndRetryAsync(
|
|
delay ?? DefaultDelay(),
|
|
(ex, timeSpan, retryAttempt, context) => {
|
|
//context.Remove("retryCount");
|
|
//context.Add("retryCount", retryAttempt);
|
|
//Log.Information(
|
|
// ex.Exception,
|
|
// "The http request failed with {exceptionType} exception retrying after {cooldown} milliseconds. This is retry attempt {retryAttempt}",
|
|
// ex.GetType().Name,
|
|
// timeSpan.TotalSeconds * 1000,
|
|
// retryAttempt
|
|
//);
|
|
}
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the user has a valid internet connection by first pinging cloudfare (fast)
|
|
/// and then trying get from the default Speckle server (slower)
|
|
/// </summary>
|
|
/// <returns>True if the user is connected to the internet, false otherwise.</returns>
|
|
public static async Task<bool> UserHasInternet()
|
|
{
|
|
string? defaultServer = null;
|
|
try
|
|
{
|
|
//Perform a quick ping test e.g. to cloudflaire dns, as is quicker than pinging server
|
|
if (await Ping("1.1.1.1").ConfigureAwait(false))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
defaultServer = AccountManager.GetDefaultServerUrl();
|
|
Uri serverUrl = new(defaultServer);
|
|
await HttpPing(serverUrl).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
SpeckleLog.Logger.ForContext("defaultServer", defaultServer).Warning(ex, "Failed to ping internet");
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pings a specific url to verify it's accessible. Retries 3 times.
|
|
/// </summary>
|
|
/// <param name="hostnameOrAddress">The hostname or address to ping.</param>
|
|
/// <returns>True if the the status code is 200, false otherwise.</returns>
|
|
public static async Task<bool> Ping(string hostnameOrAddress)
|
|
{
|
|
SpeckleLog.Logger.Information("Pinging {hostnameOrAddress}", hostnameOrAddress);
|
|
var policy = Policy
|
|
.Handle<PingException>()
|
|
.Or<SocketException>()
|
|
.WaitAndRetryAsync(
|
|
DefaultDelay(),
|
|
(ex, timeSpan, retryAttempt, context) => {
|
|
//Log.Information(
|
|
// ex,
|
|
// "The http request failed with {exceptionType} exception retrying after {cooldown} milliseconds. This is retry attempt {retryAttempt}",
|
|
// ex.GetType().Name,
|
|
// timeSpan.TotalSeconds * 1000,
|
|
// retryAttempt
|
|
//);
|
|
}
|
|
);
|
|
var policyResult = await policy
|
|
.ExecuteAndCaptureAsync(async () =>
|
|
{
|
|
Ping myPing = new();
|
|
var hostname =
|
|
Uri.CheckHostName(hostnameOrAddress) != UriHostNameType.Unknown
|
|
? hostnameOrAddress
|
|
: new Uri(hostnameOrAddress).DnsSafeHost;
|
|
byte[] buffer = new byte[32];
|
|
int timeout = 1000;
|
|
PingOptions pingOptions = new();
|
|
PingReply reply = await myPing.SendPingAsync(hostname, timeout, buffer, pingOptions).ConfigureAwait(false);
|
|
if (reply.Status != IPStatus.Success)
|
|
{
|
|
throw new SpeckleException($"The ping operation failed with status {reply.Status}");
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.ConfigureAwait(false);
|
|
if (policyResult.Outcome == OutcomeType.Successful)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
SpeckleLog.Logger.Warning(
|
|
policyResult.FinalException,
|
|
"Failed to ping {hostnameOrAddress} cause: {exceptionMessage}",
|
|
policyResult.FinalException.Message
|
|
);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a <c>GET</c> request to the provided <paramref name="uri"/>
|
|
/// </summary>
|
|
/// <param name="uri">The URI that should be pinged</param>
|
|
/// <exception cref="HttpRequestException">Request to <paramref name="uri"/> failed</exception>
|
|
public static async Task<HttpResponseMessage> HttpPing(Uri uri)
|
|
{
|
|
try
|
|
{
|
|
using var httpClient = GetHttpProxyClient();
|
|
HttpResponseMessage response = await httpClient.GetAsync(uri).ConfigureAwait(false);
|
|
response.EnsureSuccessStatusCode();
|
|
SpeckleLog.Logger.Information("Successfully pinged {uri}", uri);
|
|
return response;
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
SpeckleLog.Logger.Warning(ex, "Ping to {uri} was unsuccessful: {message}", uri, ex.Message);
|
|
throw new HttpRequestException($"Ping to {uri} was unsuccessful", ex);
|
|
}
|
|
}
|
|
|
|
public static HttpClient GetHttpProxyClient(SpeckleHttpClientHandler? handler = null, TimeSpan? timeout = null)
|
|
{
|
|
IWebProxy proxy = WebRequest.GetSystemWebProxy();
|
|
proxy.Credentials = CredentialCache.DefaultCredentials;
|
|
|
|
handler ??= new SpeckleHttpClientHandler();
|
|
var client = new HttpClient(handler) { Timeout = timeout ?? TimeSpan.FromSeconds(100) };
|
|
return client;
|
|
}
|
|
|
|
public static bool CanAddAuth(string? authToken, out string? bearerHeader)
|
|
{
|
|
if (!string.IsNullOrEmpty(authToken))
|
|
{
|
|
bearerHeader = authToken.NotNull().ToLowerInvariant().Contains("bearer") ? authToken : $"Bearer {authToken}";
|
|
return true;
|
|
}
|
|
|
|
bearerHeader = null;
|
|
return false;
|
|
}
|
|
|
|
public static void AddAuthHeader(HttpClient client, string? authToken)
|
|
{
|
|
if (CanAddAuth(authToken, out string? value))
|
|
{
|
|
client.DefaultRequestHeaders.Add("Authorization", value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class SpeckleHttpClientHandler : HttpClientHandler
|
|
{
|
|
private readonly IEnumerable<TimeSpan> _delay;
|
|
|
|
public SpeckleHttpClientHandler(IEnumerable<TimeSpan>? delay = null)
|
|
{
|
|
_delay = delay ?? Http.DefaultDelay();
|
|
}
|
|
|
|
/// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> requested cancel</exception>
|
|
/// <exception cref="HttpRequestException">Send request failed</exception>
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
// this is a preliminary client server correlation implementation
|
|
// refactor this, when we have a better observability stack
|
|
var context = new Context();
|
|
using (LogContext.PushProperty("correlationId", context.CorrelationId))
|
|
using (LogContext.PushProperty("targetUrl", request.RequestUri))
|
|
using (LogContext.PushProperty("httpMethod", request.Method))
|
|
{
|
|
SpeckleLog.Logger.Debug("Starting execution of http request to {targetUrl}", request.RequestUri);
|
|
var timer = new Stopwatch();
|
|
timer.Start();
|
|
context.Add("retryCount", 0);
|
|
var policyResult = await Http.HttpAsyncPolicy(_delay)
|
|
.ExecuteAndCaptureAsync(
|
|
ctx =>
|
|
{
|
|
request.Headers.Add("x-request-id", ctx.CorrelationId.ToString());
|
|
return base.SendAsync(request, cancellationToken);
|
|
},
|
|
context
|
|
)
|
|
.ConfigureAwait(false);
|
|
timer.Stop();
|
|
var status = policyResult.Outcome == OutcomeType.Successful ? "succeeded" : "failed";
|
|
context.TryGetValue("retryCount", out var retryCount);
|
|
SpeckleLog
|
|
.Logger.ForContext("ExceptionType", policyResult.FinalException?.GetType())
|
|
.Information(
|
|
"Execution of http request to {httpScheme}://{hostUrl}/{relativeUrl} {resultStatus} with {httpStatusCode} after {elapsed} seconds and {retryCount} retries",
|
|
request.RequestUri.Scheme,
|
|
request.RequestUri.Host,
|
|
request.RequestUri.PathAndQuery,
|
|
status,
|
|
policyResult.Result?.StatusCode,
|
|
timer.Elapsed.TotalSeconds,
|
|
retryCount ?? 0
|
|
);
|
|
if (policyResult.Outcome == OutcomeType.Successful)
|
|
{
|
|
return policyResult.Result.NotNull();
|
|
}
|
|
|
|
// if the policy failed due to a cancellation, AND it was our cancellation token, then don't wrap the exception, and rethrow an new cancellation
|
|
if (policyResult.FinalException is OperationCanceledException)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
}
|
|
|
|
throw new HttpRequestException("Policy Failed", policyResult.FinalException);
|
|
}
|
|
}
|
|
}
|