diff --git a/MailKit/MailKit.csproj b/MailKit/MailKit.csproj index b95480bfb0..097b0b5bc0 100644 --- a/MailKit/MailKit.csproj +++ b/MailKit/MailKit.csproj @@ -114,6 +114,7 @@ + diff --git a/MailKit/MailKitLite.csproj b/MailKit/MailKitLite.csproj index ea6192d73a..37dc31252c 100644 --- a/MailKit/MailKitLite.csproj +++ b/MailKit/MailKitLite.csproj @@ -119,6 +119,7 @@ + diff --git a/MailKit/Net/Proxy/ProxyClient.cs b/MailKit/Net/Proxy/ProxyClient.cs index c2d8f26c75..d9b99202c8 100644 --- a/MailKit/Net/Proxy/ProxyClient.cs +++ b/MailKit/Net/Proxy/ProxyClient.cs @@ -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 { /// @@ -45,6 +49,25 @@ namespace MailKit.Net.Proxy /// public abstract class ProxyClient : IProxyClient { +#if NET6_0_OR_GREATER + static IProxyClient systemProxy; + + /// + /// Get a client for the default system proxy. + /// + /// + /// Gets a client for the default system proxy. + /// + /// A client for the default system proxy. + public static IProxyClient SystemProxy { + get { + systemProxy ??= new WebProxyClient (HttpClient.DefaultProxy); + + return systemProxy; + } + } +#endif + /// /// Initializes a new instance of the class. /// diff --git a/MailKit/Net/Proxy/WebProxyClient.cs b/MailKit/Net/Proxy/WebProxyClient.cs new file mode 100644 index 0000000000..7c94a511d5 --- /dev/null +++ b/MailKit/Net/Proxy/WebProxyClient.cs @@ -0,0 +1,194 @@ +// +// WebProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// 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 +{ + /// + /// A proxy client that makes use of a . + /// + /// + /// A proxy client that makes use of a . + /// + internal class WebProxyClient : ProxyClient + { + readonly IWebProxy proxy; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The web proxy. + /// + /// is null. + /// + 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}."); + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + 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); + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected network stream. + /// The host name of the target server. + /// The target server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override async Task 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 diff --git a/UnitTests/Net/Proxy/WebProxyClientTests.cs b/UnitTests/Net/Proxy/WebProxyClientTests.cs new file mode 100644 index 0000000000..936b9365d9 --- /dev/null +++ b/UnitTests/Net/Proxy/WebProxyClientTests.cs @@ -0,0 +1,151 @@ +// +// WebProxyClientTests.cs +// +// Author: Jeffrey Stedfast +// +// 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 (() => 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 (() => proxy.Connect (null, 80)); + Assert.Throws (() => proxy.Connect (null, 80, ConnectTimeout)); + Assert.ThrowsAsync (async () => await proxy.ConnectAsync (null, 80)); + Assert.ThrowsAsync (async () => await proxy.ConnectAsync (null, 80, ConnectTimeout)); + + Assert.Throws (() => proxy.Connect (string.Empty, 80)); + Assert.Throws (() => proxy.Connect (string.Empty, 80, ConnectTimeout)); + Assert.ThrowsAsync (async () => await proxy.ConnectAsync (string.Empty, 80)); + Assert.ThrowsAsync (async () => await proxy.ConnectAsync (string.Empty, 80, ConnectTimeout)); + + Assert.Throws (() => proxy.Connect ("www.google.com", 0)); + Assert.Throws (() => proxy.Connect ("www.google.com", 0, ConnectTimeout)); + Assert.ThrowsAsync (async () => await proxy.ConnectAsync ("www.google.com", 0)); + Assert.ThrowsAsync (async () => await proxy.ConnectAsync ("www.google.com", 0, ConnectTimeout)); + + Assert.Throws (() => proxy.Connect ("www.google.com", 80, -ConnectTimeout)); + Assert.ThrowsAsync (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 (); + } + } + } + } +}