Skip to content

Commit

Permalink
Added ProxyClient.SystemProxy
Browse files Browse the repository at this point in the history
Based on the beginnings of a patch by @jwonagel.
  • Loading branch information
jstedfast committed Sep 29, 2024
1 parent 09851bd commit c5ee1a3
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 0 deletions.
1 change: 1 addition & 0 deletions MailKit/MailKit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
<Compile Include="Net\Proxy\Socks4Client.cs" />
<Compile Include="Net\Proxy\Socks5Client.cs" />
<Compile Include="Net\Proxy\SocksClient.cs" />
<Compile Include="Net\Proxy\WebProxyClient.cs" />
<Compile Include="Net\Smtp\AsyncSmtpClient.cs" />
<Compile Include="Net\Smtp\ISmtpClient.cs" />
<Compile Include="Net\Smtp\SmtpAuthenticationSecretDetector.cs" />
Expand Down
1 change: 1 addition & 0 deletions MailKit/MailKitLite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<Compile Include="Net\Proxy\Socks4Client.cs" />
<Compile Include="Net\Proxy\Socks5Client.cs" />
<Compile Include="Net\Proxy\SocksClient.cs" />
<Compile Include="Net\Proxy\WebProxyClient.cs" />
<Compile Include="Net\Smtp\AsyncSmtpClient.cs" />
<Compile Include="Net\Smtp\ISmtpClient.cs" />
<Compile Include="Net\Smtp\SmtpAuthenticationSecretDetector.cs" />
Expand Down
23 changes: 23 additions & 0 deletions MailKit/Net/Proxy/ProxyClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
using System.Net.Sockets;
using System.Threading.Tasks;

#if NET6_0_OR_GREATER
using System.Net.Http;
#endif

namespace MailKit.Net.Proxy
{
/// <summary>
Expand All @@ -45,6 +49,25 @@ namespace MailKit.Net.Proxy
/// </example>
public abstract class ProxyClient : IProxyClient
{
#if NET6_0_OR_GREATER
static IProxyClient systemProxy;

/// <summary>
/// Get a client for the default system proxy.
/// </summary>
/// <remarks>
/// Gets a client for the default system proxy.
/// </remarks>
/// <value>A client for the default system proxy.</value>
public static IProxyClient SystemProxy {
get {
systemProxy ??= new WebProxyClient (HttpClient.DefaultProxy);

return systemProxy;
}
}
#endif

/// <summary>
/// Initializes a new instance of the <see cref="T:MailKit.Net.Proxy.ProxyClient"/> class.
/// </summary>
Expand Down
194 changes: 194 additions & 0 deletions MailKit/Net/Proxy/WebProxyClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//
// WebProxyClient.cs
//
// Author: Jeffrey Stedfast <[email protected]>
//
// Copyright (c) 2013-2024 .NET Foundation and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

#if NET6_0_OR_GREATER

using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace MailKit.Net.Proxy
{
/// <summary>
/// A proxy client that makes use of a <see cref="IWebProxy"/>.
/// </summary>
/// <remarks>
/// A proxy client that makes use of a <see cref="IWebProxy"/>.
/// </remarks>
internal class WebProxyClient : ProxyClient
{
readonly IWebProxy proxy;

/// <summary>
/// Initializes a new instance of the <see cref="T:MailKit.Net.Proxy.WebProxyClient"/> class.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="T:MailKit.Net.Proxy.ProxyClient"/> class.
/// </remarks>
/// <param name="proxy">The web proxy.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="proxy"/> is <c>null</c>.
/// </exception>
public WebProxyClient (IWebProxy proxy) : base ("System", 0)
{
if (proxy is null)
throw new ArgumentNullException (nameof (proxy));

this.proxy = proxy;
}

static Uri GetTargetUri (string host, int port)
{
string scheme;

switch (port) {
case 25: case 465: case 587: scheme = "smtp"; break;
case 110: case 995: scheme = "pop"; break;
case 143: case 993: scheme = "imap"; break;
default: scheme = "http"; break;
}

return new Uri ($"{scheme}://{host}:{port}");
}

static NetworkCredential GetNetworkCredential (ICredentials credentials, Uri uri)
{
if (credentials is NetworkCredential network)
return network;

return credentials.GetCredential (uri, "Basic");
}

static ProxyClient GetProxyClient (Uri proxyUri, ICredentials credentials)
{
var credential = GetNetworkCredential (credentials, proxyUri);

if (proxyUri.Scheme.Equals ("https", StringComparison.OrdinalIgnoreCase))
return new HttpsProxyClient (proxyUri.Host, proxyUri.Port, credential);

if (proxyUri.Scheme.Equals ("http", StringComparison.OrdinalIgnoreCase))
return new HttpProxyClient (proxyUri.Host, proxyUri.Port, credential);

throw new NotImplementedException ($"The default system proxy does not support {proxyUri.Scheme}.");
}

/// <summary>
/// Connect to the target host.
/// </summary>
/// <remarks>
/// Connects to the target host and port through the proxy server.
/// </remarks>
/// <returns>The connected network stream.</returns>
/// <param name="host">The host name of the target server.</param>
/// <param name="port">The target server port.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="host"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.ArgumentOutOfRangeException">
/// <paramref name="port"/> is not between <c>0</c> and <c>65535</c>.
/// </exception>
/// <exception cref="System.ArgumentException">
/// The <paramref name="host"/> is a zero-length string.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.Net.Sockets.SocketException">
/// A socket error occurred trying to connect to the remote host.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
public override Stream Connect (string host, int port, CancellationToken cancellationToken = default)
{
ValidateArguments (host, port);

var targetUri = GetTargetUri (host, port);
var proxyUri = proxy.GetProxy (targetUri);

if (proxyUri is null) {
// Note: if the proxy URI is null, then it means that the proxy should be bypassed.
var socket = SocketUtils.Connect (host, port, LocalEndPoint, cancellationToken);
return new NetworkStream (socket, true);
}

var proxyClient = GetProxyClient (proxyUri, proxy.Credentials);

return proxyClient.Connect (host, port, cancellationToken);
}

/// <summary>
/// Asynchronously connect to the target host.
/// </summary>
/// <remarks>
/// Asynchronously connects to the target host and port through the proxy server.
/// </remarks>
/// <returns>The connected network stream.</returns>
/// <param name="host">The host name of the target server.</param>
/// <param name="port">The target server port.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="host"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.ArgumentOutOfRangeException">
/// <paramref name="port"/> is not between <c>0</c> and <c>65535</c>.
/// </exception>
/// <exception cref="System.ArgumentException">
/// The <paramref name="host"/> is a zero-length string.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.Net.Sockets.SocketException">
/// A socket error occurred trying to connect to the remote host.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
public override async Task<Stream> ConnectAsync (string host, int port, CancellationToken cancellationToken = default)
{
ValidateArguments (host, port);

var targetUri = GetTargetUri (host, port);
var proxyUri = proxy.GetProxy (targetUri);

if (proxyUri is null) {
// Note: if the proxy URI is null, then it means that the proxy should be bypassed.
var socket = await SocketUtils.ConnectAsync (host, port, LocalEndPoint, cancellationToken).ConfigureAwait (false);
return new NetworkStream (socket, true);
}

var proxyClient = GetProxyClient (proxyUri, proxy.Credentials);

return await proxyClient.ConnectAsync (host, port, cancellationToken);
}
}
}

#endif
151 changes: 151 additions & 0 deletions UnitTests/Net/Proxy/WebProxyClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// WebProxyClientTests.cs
//
// Author: Jeffrey Stedfast <[email protected]>
//
// Copyright (c) 2013-2024 .NET Foundation and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

using System.Net;

using MailKit.Net.Proxy;

namespace UnitTests.Net.Proxy
{
[TestFixture]
public class WebProxyClientTests
{
const int ConnectTimeout = 5 * 1000; // 5 seconds

[Test]
public void TestArgumentExceptions ()
{
var credentials = new NetworkCredential ("user", "password");
var proxy = ProxyClient.SystemProxy;

Assert.Throws<ArgumentNullException> (() => new WebProxyClient (null));

Assert.That (proxy.ProxyPort, Is.EqualTo (1080));
Assert.That (proxy.ProxyHost, Is.EqualTo ("System"));
Assert.That (proxy.ProxyCredentials, Is.Null);

Assert.Throws<ArgumentNullException> (() => proxy.Connect (null, 80));
Assert.Throws<ArgumentNullException> (() => proxy.Connect (null, 80, ConnectTimeout));
Assert.ThrowsAsync<ArgumentNullException> (async () => await proxy.ConnectAsync (null, 80));
Assert.ThrowsAsync<ArgumentNullException> (async () => await proxy.ConnectAsync (null, 80, ConnectTimeout));

Assert.Throws<ArgumentException> (() => proxy.Connect (string.Empty, 80));
Assert.Throws<ArgumentException> (() => proxy.Connect (string.Empty, 80, ConnectTimeout));
Assert.ThrowsAsync<ArgumentException> (async () => await proxy.ConnectAsync (string.Empty, 80));
Assert.ThrowsAsync<ArgumentException> (async () => await proxy.ConnectAsync (string.Empty, 80, ConnectTimeout));

Assert.Throws<ArgumentOutOfRangeException> (() => proxy.Connect ("www.google.com", 0));
Assert.Throws<ArgumentOutOfRangeException> (() => proxy.Connect ("www.google.com", 0, ConnectTimeout));
Assert.ThrowsAsync<ArgumentOutOfRangeException> (async () => await proxy.ConnectAsync ("www.google.com", 0));
Assert.ThrowsAsync<ArgumentOutOfRangeException> (async () => await proxy.ConnectAsync ("www.google.com", 0, ConnectTimeout));

Assert.Throws<ArgumentOutOfRangeException> (() => proxy.Connect ("www.google.com", 80, -ConnectTimeout));
Assert.ThrowsAsync<ArgumentOutOfRangeException> (async () => await proxy.ConnectAsync ("www.google.com", 80, -ConnectTimeout));
}

[Test]
public void TestConnect ()
{
var proxy = ProxyClient.SystemProxy;
Stream stream = null;

try {
stream = proxy.Connect ("www.google.com", 80);
} catch (TimeoutException) {
Assert.Inconclusive ("Timed out.");
} catch (Exception ex) {
Assert.Fail (ex.Message);
} finally {
stream?.Dispose ();
}
}

[Test]
public async Task TestConnectAsync ()
{
var proxy = ProxyClient.SystemProxy;
Stream stream = null;

try {
stream = await proxy.ConnectAsync ("www.google.com", 80);
} catch (TimeoutException) {
Assert.Inconclusive ("Timed out.");
} catch (Exception ex) {
Assert.Fail (ex.Message);
} finally {
stream?.Dispose ();
}
}

[Test]
public void TestConnectViaWebProxy ()
{
using (var server = new HttpProxyListener ()) {
server.Start (IPAddress.Loopback, 0);

var credentials = new NetworkCredential ("username", "password");
var webProxy = new WebProxy (new Uri ($"http://{server.IPAddress}:{server.Port}"), true, null, credentials);

var proxy = new WebProxyClient (webProxy);
Stream stream = null;

try {
stream = proxy.Connect ("www.google.com", 80, ConnectTimeout);
} catch (TimeoutException) {
Assert.Inconclusive ("Timed out.");
} catch (Exception ex) {
Assert.Fail (ex.Message);
} finally {
stream?.Dispose ();
}
}
}

[Test]
public async Task TestConnectViaWebProxyAsync ()
{
using (var server = new HttpProxyListener ()) {
server.Start (IPAddress.Loopback, 0);

var credentials = new NetworkCredential ("username", "password");
var webProxy = new WebProxy (new Uri ($"http://{server.IPAddress}:{server.Port}"), true, null, credentials);

var proxy = new WebProxyClient (webProxy);
Stream stream = null;

try {
stream = await proxy.ConnectAsync ("www.google.com", 80, ConnectTimeout);
} catch (TimeoutException) {
Assert.Inconclusive ("Timed out.");
} catch (Exception ex) {
Assert.Fail (ex.Message);
} finally {
stream?.Dispose ();
}
}
}
}
}

0 comments on commit c5ee1a3

Please sign in to comment.