Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-830959: Refactor of custom http handler setup #668

Closed
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,27 @@ public async Task TestIsRetryableHTTPCode(HttpStatusCode statusCode, bool forceR

Assert.AreEqual(expectedIsRetryable, actualIsRetryable);
}

[Test]
public void ShouldCreateHttpClient()
{
// given
var config = new HttpClientConfig(
crlCheckEnabled: TestDataGenarator.NextBool(),
proxyHost: TestDataGenarator.NextAlphaNumeric(),
proxyPort: TestDataGenarator.NextDigitsString(4),
proxyUser: TestDataGenarator.NextAlphaNumeric(),
proxyPassword: TestDataGenarator.NextAlphaNumeric(),
noProxyList: TestDataGenarator.NextAlphaNumeric(),
disableRetry: TestDataGenarator.NextBool(),
forceRetryOn404: TestDataGenarator.NextBool()
);

// when
var client = HttpUtil.Instance.GetHttpClient(config);

// then
Assert.IsNotNull(client);
}
}
}
63 changes: 63 additions & 0 deletions Snowflake.Data.Tests/Http/SFHttpMessageHandlerFactoryTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
*/

using Snowflake.Data.Core;

using NUnit.Framework;

namespace Snowflake.Data.Tests
{
[TestFixture]
public class SFHttpMessageHandlerFactoryTest
{

[Test]
public void ShouldCreateHttpMessageHandlerWithoutProxyTest()
{
// given
var config = new HttpClientConfig(
crlCheckEnabled: true,
proxyHost: null,
proxyPort: null,
proxyUser: null,
proxyPassword: null,
noProxyList: null,
disableRetry: false,
forceRetryOn404: false
);
var handlerFactory = new HttpMessageHandlerFactoryProvider().createHttpMessageHandlerFactory();

// when
var handler = handlerFactory.Create(config);

// then
Assert.NotNull(handler);
Assert.Null(handlerFactory.ExtractWebProxy(handler));
}

[Test]
public void ShouldCreateHttpMessageHandlerWithProxyTest()
{
// given
var config = new HttpClientConfig(
crlCheckEnabled: true,
proxyHost: "proxy.host.com",
proxyPort: "1234",
proxyUser: "user",
proxyPassword: "password",
noProxyList: null,
disableRetry: false,
forceRetryOn404: false
);
var handlerFactory = new HttpMessageHandlerFactoryProvider().createHttpMessageHandlerFactory();

// when
var handler = handlerFactory.Create(config);

// then
Assert.NotNull(handler);
Assert.NotNull(handlerFactory.ExtractWebProxy(handler));
}
}
}
60 changes: 60 additions & 0 deletions Snowflake.Data.Tests/TestDataGenarator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
*/

using System;
using System.Linq;

namespace Snowflake.Data.Tests
{

public class TestDataGenarator
{
private static Random random = new Random();
private static string lowercaseChars = "abcdefghijklmnopqrstuvwxyz";
private static string uppercaseChars = lowercaseChars.ToUpper();
private static string nonZeroDigits = "123456789";
private static string digitChars = "0" + nonZeroDigits;
private static string letterChars = lowercaseChars + uppercaseChars;
private static string alphanumericChars = letterChars + digitChars;

public static bool NextBool()
{
return random.Next(0, 1) == 1;
}

public static string NextAlphaNumeric()
{
return NextLetter() +
Enumerable.Repeat(alphanumericChars, random.Next(5, 12))
.Select(NextChar)
.Aggregate((s1, s2) => s1 + s2);
}

public static string NextDigitsString(int length)
{
if (length == 1)
{
return NextNonZeroDigitString();
}
return NextNonZeroDigitString() + Enumerable.Repeat(digitChars, length - 1)
.Select(NextChar)
.Aggregate((s1, s2) => s1 + s2);
}

public static string NextNonZeroDigitString()
{
return NextChar(nonZeroDigits);
}

private static string NextLetter()
{
return NextChar(letterChars);
}

private static string NextChar(string chars)
{
return chars[random.Next(chars.Length)].ToString();
}
}
}
82 changes: 82 additions & 0 deletions Snowflake.Data/Core/Http/HttpMessageHandlerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
*/

using System;
using System.Net;
using System.Net.Http;

namespace Snowflake.Data.Core
{

internal abstract class HttpMessageHandlerFactory
{

public HttpMessageHandler Create(HttpClientConfig config)
{
var handler = CreateHandlerWithoutProxy(config);
if (ProxyNeedsToBeAdded(config))
{
var proxy = CreateProxy(config);
return AttachProxyToHandler(handler, proxy);
}

return handler;
}

protected abstract HttpMessageHandler CreateHandlerWithoutProxy(HttpClientConfig config);

protected abstract HttpMessageHandler AttachProxyToHandler(HttpMessageHandler httpMessageHandler,
WebProxy proxy);

public abstract IWebProxy ExtractWebProxy(HttpMessageHandler httpMessageHandler);

private WebProxy CreateProxy(HttpClientConfig config)
{
WebProxy proxy = new WebProxy(config.ProxyHost, int.Parse(config.ProxyPort));
AttachCredentials(proxy, config);
AttachBypassList(proxy, config);
return proxy;
}

private bool ProxyNeedsToBeAdded(HttpClientConfig config)
{
return config.ProxyHost != null;
}

private WebProxy AttachCredentials(WebProxy proxy, HttpClientConfig config)
{
if (String.IsNullOrEmpty(config.ProxyUser))
return proxy;
ICredentials credentials = new NetworkCredential(config.ProxyUser, config.ProxyPassword);
proxy.Credentials = credentials;
return proxy;
}

private WebProxy AttachBypassList(WebProxy proxy, HttpClientConfig config)
{
if (String.IsNullOrEmpty(config.NoProxyList))
return proxy;
string[] bypassList = config.NoProxyList.Split(
new char[] { '|' },
StringSplitOptions.RemoveEmptyEntries);
// Convert simplified syntax to standard regular expression syntax
string entry = null;
for (int i = 0; i < bypassList.Length; i++)
{
// Get the original entry
entry = bypassList[i].Trim();
// . -> [.] because . means any char
entry = entry.Replace(".", "[.]");
// * -> .* because * is a quantifier and need a char or group to apply to
entry = entry.Replace("*", ".*");

// Replace with the valid entry syntax
bypassList[i] = entry;
}

proxy.BypassList = bypassList;
return proxy;
}
}
}
27 changes: 27 additions & 0 deletions Snowflake.Data/Core/Http/HttpMessageHandlerFactoryProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
*/

using System.Runtime.InteropServices;

namespace Snowflake.Data.Core
{

public class HttpMessageHandlerFactoryProvider
{
internal HttpMessageHandlerFactory createHttpMessageHandlerFactory()
{
if (IsRunningOnWindowsDotnet())
{
return new HttpMessageHandlerForWindowsDotnetFactory();
}

return new HttpMessageHandlerForOtherFactory();
}

internal bool IsRunningOnWindowsDotnet()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && SFEnvironment.ClientEnv.IsNetFramework;
}
}
}
40 changes: 40 additions & 0 deletions Snowflake.Data/Core/Http/HttpMessageHandlerForOtherFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
*/

using System.Net;
using System.Net.Http;
using System.Security.Authentication;

namespace Snowflake.Data.Core
{

internal class HttpMessageHandlerForOtherFactory : HttpMessageHandlerFactory
{
protected override HttpMessageHandler CreateHandlerWithoutProxy(HttpClientConfig config)
{
return new HttpClientHandler()
{
// Verify no certificates have been revoked
CheckCertificateRevocationList = config.CrlCheckEnabled,
// Enforce tls v1.2
SslProtocols = SslProtocols.Tls12,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false // Disable cookies
};
}

protected override HttpMessageHandler AttachProxyToHandler(HttpMessageHandler httpMessageHandler,
WebProxy proxy)
{
HttpClientHandler httpHandlerWithProxy = (HttpClientHandler)httpMessageHandler;
httpHandlerWithProxy.Proxy = proxy;
return httpHandlerWithProxy;
}

public override IWebProxy ExtractWebProxy(HttpMessageHandler httpMessageHandler)
{
return ((HttpClientHandler)httpMessageHandler).Proxy;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
*/

using System.Net;
using System.Net.Http;
using System.Security.Authentication;

namespace Snowflake.Data.Core
{

internal class HttpMessageHandlerForWindowsDotnetFactory : HttpMessageHandlerFactory
{
protected override HttpMessageHandler CreateHandlerWithoutProxy(HttpClientConfig config)
{
return new WinHttpHandler()
{
// Verify no certificates have been revoked
CheckCertificateRevocationList = config.CrlCheckEnabled,
// Enforce tls v1.2
SslProtocols = SslProtocols.Tls12,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
CookieUsePolicy = CookieUsePolicy.IgnoreCookies
};
}

protected override HttpMessageHandler AttachProxyToHandler(HttpMessageHandler httpMessageHandler,
WebProxy proxy)
{
WinHttpHandler httpHandlerWithProxy = (WinHttpHandler) httpMessageHandler;
httpHandlerWithProxy.WindowsProxyUsePolicy = WindowsProxyUsePolicy.UseCustomProxy;
httpHandlerWithProxy.Proxy = proxy;
return httpHandlerWithProxy;
}

public override IWebProxy ExtractWebProxy(HttpMessageHandler httpMessageHandler)
{
return ((WinHttpHandler) httpMessageHandler).Proxy;
}
}
}
Loading