|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.ExceptionServices;
using System.Security.Authentication;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Threading;
namespace System.Net.Mail
{
internal sealed partial class SmtpConnection
{
private static readonly ContextCallback s_AuthenticateCallback = new ContextCallback(AuthenticateCallback);
private readonly BufferBuilder _bufferBuilder = new BufferBuilder();
private bool _isConnected;
private bool _isClosed;
private bool _isStreamOpen;
private readonly EventHandler? _onCloseHandler;
internal SmtpTransport? _parent;
private readonly SmtpClient? _client;
private Stream? _stream;
internal TcpClient? _tcpClient;
private SmtpReplyReaderFactory? _responseReader;
private readonly ICredentialsByHost? _credentials;
private string[]? _extensions;
private bool _enableSsl;
private X509CertificateCollection? _clientCertificates;
internal SmtpConnection(SmtpTransport parent, SmtpClient client, ICredentialsByHost? credentials, ISmtpAuthenticationModule[] authenticationModules)
{
_client = client;
_credentials = credentials;
_authenticationModules = authenticationModules;
_parent = parent;
_tcpClient = new TcpClient();
_onCloseHandler = new EventHandler(OnClose);
}
internal BufferBuilder BufferBuilder => _bufferBuilder;
internal bool IsConnected => _isConnected;
internal bool IsStreamOpen => _isStreamOpen;
internal SmtpReplyReaderFactory? Reader => _responseReader;
internal bool EnableSsl
{
get
{
return _enableSsl;
}
set
{
_enableSsl = value;
}
}
internal X509CertificateCollection? ClientCertificates
{
get
{
return _clientCertificates;
}
set
{
_clientCertificates = value;
}
}
internal void InitializeConnection(string host, int port)
{
_tcpClient!.Connect(host, port);
_stream = _tcpClient.GetStream();
}
internal IAsyncResult BeginInitializeConnection(string host, int port, AsyncCallback? callback, object? state)
{
return _tcpClient!.BeginConnect(host, port, callback, state);
}
internal void EndInitializeConnection(IAsyncResult result)
{
_tcpClient!.EndConnect(result);
_stream = _tcpClient.GetStream();
}
internal IAsyncResult BeginGetConnection(ContextAwareResult outerResult, AsyncCallback? callback, object? state, string host, int port)
{
ConnectAndHandshakeAsyncResult result = new ConnectAndHandshakeAsyncResult(this, host, port, outerResult, callback, state);
result.GetConnection();
return result;
}
internal IAsyncResult BeginFlush(AsyncCallback? callback, object? state)
{
return _stream!.BeginWrite(_bufferBuilder.GetBuffer(), 0, _bufferBuilder.Length, callback, state);
}
internal void EndFlush(IAsyncResult result)
{
_stream!.EndWrite(result);
_bufferBuilder.Reset();
}
internal void Flush()
{
_stream!.Write(_bufferBuilder.GetBuffer(), 0, _bufferBuilder.Length);
_bufferBuilder.Reset();
}
private void ShutdownConnection(bool isAbort)
{
if (!_isClosed)
{
lock (this)
{
if (!_isClosed && _tcpClient != null)
{
try
{
try
{
if (isAbort)
{
// Must destroy manually since sending a QUIT here might not be
// interpreted correctly by the server if it's in the middle of a
// DATA command or some similar situation. This may send a RST
// but this is ok in this situation. Do not reuse this connection
_tcpClient.LingerState = new LingerOption(true, 0);
}
else
{
// Gracefully close the transmission channel
_tcpClient.Client.Blocking = false;
QuitCommand.Send(this);
}
}
finally
{
//free cbt buffer
_stream?.Close();
_tcpClient.Dispose();
}
}
catch (IOException)
{
// Network failure
}
catch (ObjectDisposedException)
{
// See https://github.com/dotnet/runtime/issues/30732, and potentially
// catch additional exception types here if need demonstrates.
}
}
_isClosed = true;
}
}
_isConnected = false;
}
internal void ReleaseConnection()
{
ShutdownConnection(false);
}
internal void Abort()
{
ShutdownConnection(true);
}
internal void GetConnection(string host, int port)
{
if (_isConnected)
{
throw new InvalidOperationException(SR.SmtpAlreadyConnected);
}
InitializeConnection(host, port);
_responseReader = new SmtpReplyReaderFactory(_stream!);
LineInfo info = _responseReader.GetNextReplyReader().ReadLine();
switch (info.StatusCode)
{
case SmtpStatusCode.ServiceReady:
break;
default:
throw new SmtpException(info.StatusCode, info.Line, true);
}
try
{
_extensions = EHelloCommand.Send(this, _client!._clientDomain);
ParseExtensions(_extensions);
}
catch (SmtpException e)
{
if ((e.StatusCode != SmtpStatusCode.CommandUnrecognized)
&& (e.StatusCode != SmtpStatusCode.CommandNotImplemented))
{
throw;
}
HelloCommand.Send(this, _client!._clientDomain);
//if ehello isn't supported, assume basic login
_supportedAuth = SupportedAuth.Login;
}
if (_enableSsl)
{
if (!_serverSupportsStartTls)
{
// Either TLS is already established or server does not support TLS
if (!(_stream is SslStream))
{
throw new SmtpException(SR.MailServerDoesNotSupportStartTls);
}
}
StartTlsCommand.Send(this);
#pragma warning disable SYSLIB0014 // ServicePointManager is obsolete
SslStream sslStream = new SslStream(_stream!, false, ServicePointManager.ServerCertificateValidationCallback);
sslStream.AuthenticateAsClient(
host,
_clientCertificates,
(SslProtocols)ServicePointManager.SecurityProtocol, // enums use same values
ServicePointManager.CheckCertificateRevocationList);
#pragma warning restore SYSLIB0014 // ServicePointManager is obsolete
_stream = sslStream;
_responseReader = new SmtpReplyReaderFactory(_stream);
// According to RFC 3207: The client SHOULD send an EHLO command
// as the first command after a successful TLS negotiation.
_extensions = EHelloCommand.Send(this, _client._clientDomain);
ParseExtensions(_extensions);
}
// if no credentials were supplied, try anonymous
// servers don't appear to anounce that they support anonymous login.
if (_credentials != null)
{
for (int i = 0; i < _authenticationModules.Length; i++)
{
//only authenticate if the auth protocol is supported - chadmu
if (!AuthSupported(_authenticationModules[i]))
{
continue;
}
NetworkCredential? credential = _credentials.GetCredential(host, port, _authenticationModules[i].AuthenticationType);
if (credential == null)
continue;
Authorization? auth = SetContextAndTryAuthenticate(_authenticationModules[i], credential, null);
if (auth != null && auth.Message != null)
{
info = AuthCommand.Send(this, _authenticationModules[i].AuthenticationType, auth.Message);
if (info.StatusCode == SmtpStatusCode.CommandParameterNotImplemented)
{
continue;
}
while ((int)info.StatusCode == 334)
{
auth = _authenticationModules[i].Authenticate(info.Line, null, this, _client.TargetName, null);
if (auth == null)
{
throw new SmtpException(SR.SmtpAuthenticationFailed);
}
info = AuthCommand.Send(this, auth.Message);
if ((int)info.StatusCode == 235)
{
_authenticationModules[i].CloseContext(this);
_isConnected = true;
return;
}
}
}
}
}
_isConnected = true;
}
private Authorization? SetContextAndTryAuthenticate(ISmtpAuthenticationModule module, NetworkCredential? credential, ContextAwareResult? context)
{
// We may need to restore user thread token here
if (ReferenceEquals(credential, CredentialCache.DefaultNetworkCredentials))
{
#if DEBUG
Debug.Assert(context == null || context.IdentityRequested, "Authentication required when it wasn't expected. (Maybe Credentials was changed on another thread?)");
#endif
try
{
ExecutionContext? x = context?.ContextCopy;
if (x != null)
{
AuthenticateCallbackContext authenticationContext =
new AuthenticateCallbackContext(this, module, credential, _client!.TargetName, null);
ExecutionContext.Run(x, s_AuthenticateCallback, authenticationContext);
return authenticationContext._result;
}
else
{
return module.Authenticate(null, credential, this, _client!.TargetName, null);
}
}
catch
{
// Prevent the impersonation from leaking to upstack exception filters.
throw;
}
}
return module.Authenticate(null, credential, this, _client!.TargetName, null);
}
private static void AuthenticateCallback(object? state)
{
AuthenticateCallbackContext context = (AuthenticateCallbackContext)state!;
context._result = context._module.Authenticate(null, context._credential, context._thisPtr, context._spn, context._token);
}
private sealed class AuthenticateCallbackContext
{
internal AuthenticateCallbackContext(SmtpConnection thisPtr, ISmtpAuthenticationModule module, NetworkCredential credential, string? spn, ChannelBinding? Token)
{
_thisPtr = thisPtr;
_module = module;
_credential = credential;
_spn = spn;
_token = Token;
_result = null;
}
internal readonly SmtpConnection _thisPtr;
internal readonly ISmtpAuthenticationModule _module;
internal readonly NetworkCredential _credential;
internal readonly string? _spn;
internal readonly ChannelBinding? _token;
internal Authorization? _result;
}
internal static void EndGetConnection(IAsyncResult result)
{
ConnectAndHandshakeAsyncResult.End(result);
}
internal Stream GetClosableStream()
{
ClosableStream cs = new ClosableStream(_stream!, _onCloseHandler);
_isStreamOpen = true;
return cs;
}
private void OnClose(object? sender, EventArgs args)
{
_isStreamOpen = false;
DataStopCommand.Send(this);
}
private sealed class ConnectAndHandshakeAsyncResult : LazyAsyncResult
{
private string? _authResponse;
private readonly SmtpConnection _connection;
private int _currentModule = -1;
private readonly int _port;
private static readonly AsyncCallback s_handshakeCallback = new AsyncCallback(HandshakeCallback);
private static readonly AsyncCallback s_sendEHelloCallback = new AsyncCallback(SendEHelloCallback);
private static readonly AsyncCallback s_sendHelloCallback = new AsyncCallback(SendHelloCallback);
private static readonly AsyncCallback s_authenticateCallback = new AsyncCallback(AuthenticateCallback);
private static readonly AsyncCallback s_authenticateContinueCallback = new AsyncCallback(AuthenticateContinueCallback);
private readonly string _host;
private readonly ContextAwareResult _outerResult;
internal ConnectAndHandshakeAsyncResult(SmtpConnection connection, string host, int port, ContextAwareResult outerResult, AsyncCallback? callback, object? state) :
base(null, state, callback)
{
_connection = connection;
_host = host;
_port = port;
_outerResult = outerResult;
}
internal static void End(IAsyncResult result)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result;
object? connectResult = thisPtr.InternalWaitForCompletion();
if (connectResult is Exception e)
{
ExceptionDispatchInfo.Throw(e);
}
}
internal void GetConnection()
{
if (_connection._isConnected)
{
throw new InvalidOperationException(SR.SmtpAlreadyConnected);
}
InitializeConnection();
}
private void InitializeConnection()
{
IAsyncResult result = _connection.BeginInitializeConnection(_host, _port, InitializeConnectionCallback, this);
if (result.CompletedSynchronously)
{
try
{
_connection.EndInitializeConnection(result);
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Connect returned");
Handshake();
}
catch (Exception e)
{
InvokeCallback(e);
}
}
}
private static void InitializeConnectionCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
thisPtr._connection.EndInitializeConnection(result);
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"Connect returned {thisPtr}");
thisPtr.Handshake();
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
private void Handshake()
{
_connection._responseReader = new SmtpReplyReaderFactory(_connection._stream!);
SmtpReplyReader reader = _connection.Reader!.GetNextReplyReader();
IAsyncResult result = reader.BeginReadLine(s_handshakeCallback, this);
if (!result.CompletedSynchronously)
{
return;
}
LineInfo info = SmtpReplyReader.EndReadLine(result);
if (info.StatusCode != SmtpStatusCode.ServiceReady)
{
throw new SmtpException(info.StatusCode, info.Line, true);
}
try
{
if (!SendEHello())
{
return;
}
}
catch
{
if (!SendHello())
{
return;
}
}
}
private static void HandshakeCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
try
{
LineInfo info = SmtpReplyReader.EndReadLine(result);
if (info.StatusCode != SmtpStatusCode.ServiceReady)
{
thisPtr.InvokeCallback(new SmtpException(info.StatusCode, info.Line, true));
return;
}
if (!thisPtr.SendEHello())
{
return;
}
}
catch (SmtpException)
{
if (!thisPtr.SendHello())
{
return;
}
}
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
private bool SendEHello()
{
IAsyncResult result = EHelloCommand.BeginSend(_connection, _connection._client!._clientDomain, s_sendEHelloCallback, this);
if (result.CompletedSynchronously)
{
_connection._extensions = EHelloCommand.EndSend(result);
_connection.ParseExtensions(_connection._extensions);
// If we already have a SslStream, this is the second EHLO cmd
// that we sent after TLS handshake compelted. So skip TLS and
// continue with Authenticate.
if (_connection._stream is SslStream)
{
Authenticate();
return true;
}
if (_connection.EnableSsl)
{
if (!_connection._serverSupportsStartTls)
{
// Either TLS is already established or server does not support TLS
if (!(_connection._stream is SslStream))
{
throw new SmtpException(SR.MailServerDoesNotSupportStartTls);
}
}
SendStartTls();
}
else
{
Authenticate();
}
return true;
}
return false;
}
private static void SendEHelloCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
try
{
thisPtr._connection._extensions = EHelloCommand.EndSend(result);
thisPtr._connection.ParseExtensions(thisPtr._connection._extensions);
// If we already have a SSlStream, this is the second EHLO cmd
// that we sent after TLS handshake compelted. So skip TLS and
// continue with Authenticate.
if (thisPtr._connection._stream is SslStream)
{
thisPtr.Authenticate();
return;
}
}
catch (SmtpException e)
{
if ((e.StatusCode != SmtpStatusCode.CommandUnrecognized)
&& (e.StatusCode != SmtpStatusCode.CommandNotImplemented))
{
throw;
}
if (!thisPtr.SendHello())
{
return;
}
}
if (thisPtr._connection.EnableSsl)
{
if (!thisPtr._connection._serverSupportsStartTls)
{
// Either TLS is already established or server does not support TLS
if (!(thisPtr._connection._stream is SslStream))
{
throw new SmtpException(SR.MailServerDoesNotSupportStartTls);
}
}
thisPtr.SendStartTls();
}
else
{
thisPtr.Authenticate();
}
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
private bool SendHello()
{
IAsyncResult result = HelloCommand.BeginSend(_connection, _connection._client!._clientDomain, s_sendHelloCallback, this);
//if ehello isn't supported, assume basic auth
if (result.CompletedSynchronously)
{
_connection._supportedAuth = SupportedAuth.Login;
HelloCommand.EndSend(result);
Authenticate();
return true;
}
return false;
}
private static void SendHelloCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
HelloCommand.EndSend(result);
thisPtr.Authenticate();
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
private bool SendStartTls()
{
IAsyncResult result = StartTlsCommand.BeginSend(_connection, SendStartTlsCallback, this);
if (result.CompletedSynchronously)
{
StartTlsCommand.EndSend(result);
SslStreamAuthenticate();
return true;
}
return false;
}
private static void SendStartTlsCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
StartTlsCommand.EndSend(result);
thisPtr.SslStreamAuthenticate();
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
private bool SslStreamAuthenticate()
{
#pragma warning disable SYSLIB0014 // ServicePointManager is obsolete
_connection._stream = new SslStream(_connection._stream!, false, ServicePointManager.ServerCertificateValidationCallback);
IAsyncResult result = ((SslStream)_connection._stream).BeginAuthenticateAsClient(
_host,
_connection._clientCertificates,
(SslProtocols)ServicePointManager.SecurityProtocol, // enums use same values
ServicePointManager.CheckCertificateRevocationList,
SslStreamAuthenticateCallback,
this);
#pragma warning restore SYSLIB0014 // ServicePointManager is obsolete
if (result.CompletedSynchronously)
{
((SslStream)_connection._stream).EndAuthenticateAsClient(result);
_connection._responseReader = new SmtpReplyReaderFactory(_connection._stream);
SendEHello();
return true;
}
return false;
}
private static void SslStreamAuthenticateCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
(thisPtr._connection._stream as SslStream)!.EndAuthenticateAsClient(result);
thisPtr._connection._responseReader = new SmtpReplyReaderFactory(thisPtr._connection._stream);
thisPtr.SendEHello();
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
private void Authenticate()
{
//if no credentials were supplied, try anonymous
//servers don't appear to anounce that they support anonymous login.
if (_connection._credentials != null)
{
while (++_currentModule < _connection._authenticationModules.Length)
{
//only authenticate if the auth protocol is supported
ISmtpAuthenticationModule module = _connection._authenticationModules[_currentModule];
if (!_connection.AuthSupported(module))
{
continue;
}
NetworkCredential? credential = _connection._credentials.GetCredential(_host, _port, module.AuthenticationType);
if (credential == null)
continue;
Authorization? auth = _connection.SetContextAndTryAuthenticate(module, credential, _outerResult);
if (auth != null && auth.Message != null)
{
IAsyncResult result = AuthCommand.BeginSend(_connection, _connection._authenticationModules[_currentModule].AuthenticationType, auth.Message, s_authenticateCallback, this);
if (!result.CompletedSynchronously)
{
return;
}
LineInfo info = AuthCommand.EndSend(result);
if ((int)info.StatusCode == 334)
{
_authResponse = info.Line;
if (!AuthenticateContinue())
{
return;
}
}
else if ((int)info.StatusCode == 235)
{
module.CloseContext(_connection);
_connection._isConnected = true;
break;
}
}
}
}
_connection._isConnected = true;
InvokeCallback();
}
private static void AuthenticateCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
LineInfo info = AuthCommand.EndSend(result);
if ((int)info.StatusCode == 334)
{
thisPtr._authResponse = info.Line;
if (!thisPtr.AuthenticateContinue())
{
return;
}
}
else if ((int)info.StatusCode == 235)
{
thisPtr._connection._authenticationModules[thisPtr._currentModule].CloseContext(thisPtr._connection);
thisPtr._connection._isConnected = true;
thisPtr.InvokeCallback();
return;
}
thisPtr.Authenticate();
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
private bool AuthenticateContinue()
{
while (true)
{
// We don't need credential on the continued auth assuming they were captured on the first call.
// That should always work, otherwise what if a new credential has been returned?
Authorization? auth = _connection._authenticationModules[_currentModule].Authenticate(_authResponse, null, _connection, _connection._client!.TargetName, null);
if (auth == null)
{
throw new SmtpException(SR.SmtpAuthenticationFailed);
}
IAsyncResult result = AuthCommand.BeginSend(_connection, auth.Message, s_authenticateContinueCallback, this);
if (!result.CompletedSynchronously)
{
return false;
}
LineInfo info = AuthCommand.EndSend(result);
if ((int)info.StatusCode == 235)
{
_connection._authenticationModules[_currentModule].CloseContext(_connection);
_connection._isConnected = true;
InvokeCallback();
return false;
}
else if ((int)info.StatusCode != 334)
{
return true;
}
_authResponse = info.Line;
}
}
private static void AuthenticateContinueCallback(IAsyncResult result)
{
if (!result.CompletedSynchronously)
{
ConnectAndHandshakeAsyncResult thisPtr = (ConnectAndHandshakeAsyncResult)result.AsyncState!;
try
{
LineInfo info = AuthCommand.EndSend(result);
if ((int)info.StatusCode == 235)
{
thisPtr._connection._authenticationModules[thisPtr._currentModule].CloseContext(thisPtr._connection);
thisPtr._connection._isConnected = true;
thisPtr.InvokeCallback();
return;
}
else if ((int)info.StatusCode == 334)
{
thisPtr._authResponse = info.Line;
if (!thisPtr.AuthenticateContinue())
{
return;
}
}
thisPtr.Authenticate();
}
catch (Exception e)
{
thisPtr.InvokeCallback(e);
}
}
}
}
}
}
|