Skip to content
Merged
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
4 changes: 4 additions & 0 deletions Frends.HTTP.DownloadFile/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [1.5.0] - 2026-05-13
### Added
- Added UseProxy, ProxyUrl, ProxyUsername, ProxyPassword, HttpProtocolVersion, and SslProtocolVersion options with proxy handler and protocol configuration support.

## [1.4.0] - 2026-03-02
### Added
- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Frends.HTTP.DownloadFile.Definitions;
using Frends.HTTP.DownloadFile.Definitions;
using Definitions = Frends.HTTP.DownloadFile.Definitions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
Expand Down Expand Up @@ -546,4 +548,51 @@ public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string
StringAssert.Contains(ex.Message,
$"Certificate with thumbprint: 'INVALIDTHUMBPRINT' not found in {storeLocationText} cert store.");
}

[TestMethod]
public void HandlerShouldUseConfiguredProxy()
{
var handler = new HttpClientHandler();
var options = new Options
{
UseProxy = true,
ProxyUrl = "http://proxy.example.com:8080",
ProxyUsername = "proxy-user",
ProxyPassword = "proxy-password"
};

handler.SetHandlerSettingsBasedOnOptions(options);

var proxy = handler.Proxy as WebProxy;
Assert.IsNotNull(proxy);
Assert.IsTrue(handler.UseProxy);
Assert.AreEqual(new Uri("http://proxy.example.com:8080/"), proxy.Address);
var credentials = proxy.Credentials.GetCredential(proxy.Address, string.Empty);
Assert.AreEqual("proxy-user", credentials.UserName);
Assert.AreEqual("proxy-password", credentials.Password);
}

[TestMethod]
public async Task RequestShouldSetTls12And13WhenConfigured()
{
var input = new Input
{
Url = _targetFileAddress,
FilePath = _filePath,
Headers = null
};

var options = new Options
{
ConnectionTimeoutSeconds = 60,
SslProtocolVersion = SslVersion.Tls12And13,
HttpProtocolVersion = Definitions.HttpVersion.Http11,
Overwrite = true
};

var result = await HTTP.DownloadFile(input, options, default);

Assert.IsTrue(result.Success);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,43 @@ public enum Authentication
/// </summary>
ClientCertificate
}


/// <summary>
/// HTTP protocol version used for requests.
/// </summary>
public enum HttpVersion
{
/// <summary>
/// HTTP/1.1 - default, widely supported.
/// </summary>
Http11,
/// <summary>
/// HTTP/2 - multiplexed, requires server support.
/// If the server does not support HTTP/2 via ALPN, the request will fail with an exception instead of falling back to HTTP/1.1.
/// </summary>
Http20
}

/// <summary>
/// SSL/TLS protocol version used for secure connections.
/// </summary>
public enum SslVersion
{
/// <summary>
/// OS decides the protocol version.
/// </summary>
Default,
/// <summary>
/// TLS 1.2 only.
/// </summary>
Tls12,
/// <summary>
/// TLS 1.3 only.
/// </summary>
Tls13,
/// <summary>
/// TLS 1.2 and TLS 1.3 - use when server compatibility is uncertain.
/// </summary>
Tls12And13
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,37 @@ public class Options
[UIHint(nameof(Authentication), "", Authentication.OAuth)]
public string Token { get; set; }

/// <summary>
/// Whether to route requests through a proxy server. When set to true, the ProxyUrl must be provided.
/// </summary>
/// <example>false</example>
[DefaultValue(false)]
public bool UseProxy { get; set; } = false;

/// <summary>
/// Proxy URL used for the request.
/// </summary>
/// <example>http://proxy.example.com:8080</example>
[UIHint(nameof(UseProxy), "", true)]
public string ProxyUrl { get; set; }

/// <summary>
/// Username for proxy authentication. Leave empty if proxy does not require authentication.
/// Both username and password must be provided together or left empty.
/// </summary>
/// <example>Username</example>
[UIHint(nameof(UseProxy), "", true)]
public string ProxyUsername { get; set; }

/// <summary>
/// Password for proxy authentication. Leave empty if proxy does not require authentication.
/// Both username and password must be provided together or left empty.
/// </summary>
/// <example>Password123</example>
[PasswordPropertyText]
[UIHint(nameof(UseProxy), "", true)]
public string ProxyPassword { get; set; }

/// <summary>
/// Specifies where the Client Certificate should be loaded from.
/// </summary>
Expand Down Expand Up @@ -137,6 +168,21 @@ public class Options
[DefaultValue(true)]
public bool AutomaticCookieHandling { get; set; } = true;

/// <summary>
/// HTTP protocol version to use for requests. HTTP/1.1 is the default and most compatible option.
/// </summary>
/// <example>Http11</example>
[DefaultValue(HttpVersion.Http11)]
public HttpVersion HttpProtocolVersion { get; set; } = HttpVersion.Http11;

/// <summary>
/// SSL/TLS protocol version to use for secure connections. Default lets the OS decide, which matches previous behavior.
/// Use Tls12 or Tls12And13 if the server requires a specific version.
/// </summary>
/// <example>Default</example>
[DefaultValue(SslVersion.Default)]
public SslVersion SslProtocolVersion { get; set; } = SslVersion.Default;

/// <summary>
/// If set to true, an existing file at the target path will be overwritten during download.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ private static string GetHttpClientCacheKey(Options options)
+ $":{options.ClientCertificateFilePath}:{options.ClientCertificateInBase64}:{options.ClientCertificateKeyPhrase}"
+ $":{options.CertificateThumbprint}:{options.LoadEntireChainForCertificate}:{options.ConnectionTimeoutSeconds}"
+ $":{options.FollowRedirects}:{options.AllowInvalidCertificate}:{options.AllowInvalidResponseContentTypeCharSet}"
+ $":{options.ThrowExceptionOnErrorResponse}:{options.AutomaticCookieHandling}";
+ $":{options.ThrowExceptionOnErrorResponse}:{options.AutomaticCookieHandling}"
+ $":{options.SslProtocolVersion}:{options.HttpProtocolVersion}"
+ $":{options.UseProxy}:{options.ProxyUrl}:{options.ProxyUsername}:{options.ProxyPassword}";
Comment thread
MatteoDelOmbra marked this conversation as resolved.
}

private static void OnPluginUnloadingRequested(AssemblyLoadContext obj)
Expand Down
40 changes: 40 additions & 0 deletions Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication;
using System.Text.RegularExpressions;

namespace Frends.HTTP.DownloadFile;
Expand Down Expand Up @@ -39,8 +40,35 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han
handler.AllowAutoRedirect = options.FollowRedirects;
handler.UseCookies = options.AutomaticCookieHandling;

if (options.UseProxy && !string.IsNullOrWhiteSpace(options.ProxyUrl))
{
handler.Proxy = new WebProxy(options.ProxyUrl);
handler.UseProxy = true;

var hasUsername = !string.IsNullOrWhiteSpace(options.ProxyUsername);
var hasPassword = !string.IsNullOrWhiteSpace(options.ProxyPassword);

if (hasUsername != hasPassword)
{
throw new ArgumentException("Both ProxyUsername and ProxyPassword must be provided together or left empty.");
}

if (hasUsername)
{
handler.Proxy.Credentials = new NetworkCredential(options.ProxyUsername, options.ProxyPassword);
}
}
Comment thread
MatteoDelOmbra marked this conversation as resolved.

if (options.AllowInvalidCertificate)
handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true;

handler.SslProtocols = options.SslProtocolVersion switch
{
SslVersion.Tls12 => SslProtocols.Tls12,
SslVersion.Tls13 => SslProtocols.Tls13,
SslVersion.Tls12And13 => SslProtocols.Tls12 | SslProtocols.Tls13,
_ => SslProtocols.None
};
}

internal static void SetDefaultRequestHeadersBasedOnOptions(this HttpClient httpClient, Options options)
Expand All @@ -49,6 +77,18 @@ internal static void SetDefaultRequestHeadersBasedOnOptions(this HttpClient http
httpClient.DefaultRequestHeaders.ExpectContinue = false;
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("content-type", "application/json");
httpClient.Timeout = TimeSpan.FromSeconds(Convert.ToDouble(options.ConnectionTimeoutSeconds));

httpClient.DefaultRequestVersion = options.HttpProtocolVersion switch
{
Definitions.HttpVersion.Http20 => System.Net.HttpVersion.Version20,
_ => System.Net.HttpVersion.Version11
};

httpClient.DefaultVersionPolicy = options.HttpProtocolVersion switch
{
Definitions.HttpVersion.Http20 => HttpVersionPolicy.RequestVersionExact,
_ => HttpVersionPolicy.RequestVersionOrLower
};
}

private static X509Certificate[] GetCertificates(Options options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>1.4.0</Version>
<Version>1.5.0</Version>
<Authors>Frends</Authors>
<Copyright>Frends</Copyright>
<Company>Frends</Company>
Expand Down
4 changes: 4 additions & 0 deletions Frends.HTTP.RequestBytes/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [1.5.0] - 2026-05-13
### Added
- Added UseProxy, ProxyUrl, ProxyUsername, ProxyPassword, HttpProtocolVersion, and SslProtocolVersion options with proxy handler and protocol configuration support.

## [1.4.0] - 2026-03-02
### Added
- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using RichardSzalay.MockHttp;
using Assert = NUnit.Framework.Assert;
using Frends.HTTP.RequestBytes.Definitions;
using Definitions = Frends.HTTP.RequestBytes.Definitions;
using System.Collections.Generic;
using System.Text;
using Method = Frends.HTTP.RequestBytes.Definitions.Method;
Expand Down Expand Up @@ -399,6 +400,48 @@ public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase()
_mockHttpMessageHandler.VerifyNoOutstandingExpectation();
ClassicAssert.AreEqual("foo åäö", Encoding.UTF8.GetString(result.Body));
}

[TestMethod]
public void HandlerShouldUseConfiguredProxy()
{
var handler = new HttpClientHandler();
var options = new Options
{
UseProxy = true,
ProxyUrl = "http://proxy.example.com:8080",
ProxyUsername = "proxy-user",
ProxyPassword = "proxy-password"
};

handler.SetHandlerSettingsBasedOnOptions(options);

var proxy = handler.Proxy as WebProxy;
Assert.That(proxy, Is.Not.Null);
ClassicAssert.IsTrue(handler.UseProxy);
ClassicAssert.AreEqual(new Uri("http://proxy.example.com:8080/"), proxy.Address);
var credentials = proxy.Credentials.GetCredential(proxy.Address, string.Empty);
ClassicAssert.AreEqual("proxy-user", credentials.UserName);
ClassicAssert.AreEqual("proxy-password", credentials.Password);
}

[TestMethod]
public async Task RequestShouldSetTls12And13WhenConfigured()
{
HTTP.ClientFactory = new HttpClientFactory();

var input = GetInputParams(url: "https://httpbin.org/anything");
var options = new Options
{
ConnectionTimeoutSeconds = 60,
SslProtocolVersion = SslVersion.Tls12And13,
HttpProtocolVersion = Definitions.HttpVersion.Http20
};

var result = await HTTP.RequestBytes(input, options, CancellationToken.None);

ClassicAssert.AreEqual(200, result.StatusCode);
}

}

public class MockHttpClientFactory : IHttpClientFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Frends.HTTP.RequestBytes.Definitions
{
/// <summary>
/// HTTP protocol version used for requests.
/// </summary>
public enum HttpVersion
{
/// <summary>
/// HTTP/1.1 - default, widely supported.
/// </summary>
Http11,
/// <summary>
/// HTTP/2 - multiplexed, requires server support.
/// If the server does not support HTTP/2 via ALPN, the request will fail with an exception instead of falling back to HTTP/1.1.
/// </summary>
Http20
}
}
Loading
Loading