// Creates a FtpDataStream object, constructs a TLS stream if needed. // In case SSL and ASYNC we delay sigaling the user stream until the handshake is done. private PipelineInstruction QueueOrCreateFtpDataStream(ref Stream stream) { if (_dataSocket == null) { throw new InternalException(); } // // Re-entered pipeline with completed read on the TlsStream // if (_tlsStream != null) { stream = new FtpDataStream(_tlsStream, (FtpWebRequest)_request, IsFtpDataStreamWriteable()); _tlsStream = null; return(PipelineInstruction.GiveStream); } NetworkStream networkStream = new NetworkStream(_dataSocket, true); if (UsingSecureStream) { FtpWebRequest request = (FtpWebRequest)_request; TlsStream tlsStream = new TlsStream(networkStream, _dataSocket, request.RequestUri.Host, request.ClientCertificates); networkStream = tlsStream; if (_isAsync) { _tlsStream = tlsStream; tlsStream.BeginAuthenticateAsClient(s_SSLHandshakeCallback, this); return(PipelineInstruction.Pause); } else { tlsStream.AuthenticateAsClient(); } } stream = new FtpDataStream(networkStream, (FtpWebRequest)_request, IsFtpDataStreamWriteable()); return(PipelineInstruction.GiveStream); }
// This is called by underlying base class code, each time a new response is received from the wire or a protocol stage is resumed. // This function controls the setting up of a data socket/connection, and of saving off the server responses. protected override PipelineInstruction PipelineCallback(PipelineEntry entry, ResponseDescription response, bool timeout, ref Stream stream) { if (NetEventSource.IsEnabled) { NetEventSource.Info(this, $"Command:{entry?.Command} Description:{response?.StatusDescription}"); } // null response is not expected if (response == null) { return(PipelineInstruction.Abort); } FtpStatusCode status = (FtpStatusCode)response.Status; // // Update global "current status" for FtpWebRequest // if (status != FtpStatusCode.ClosingControl) { // A 221 status won't be reflected on the user FTP response // Anything else will (by design?) StatusCode = status; StatusLine = response.StatusDescription; } // If the status code is outside the range defined in RFC (1xx to 5xx) throw if (response.InvalidStatusCode) { throw new WebException(SR.net_InvalidStatusCode, WebExceptionStatus.ProtocolError); } // Update the banner message if any, this is a little hack because the "entry" param is null if (_index == -1) { if (status == FtpStatusCode.SendUserCommand) { _bannerMessage = new StringBuilder(); _bannerMessage.Append(StatusLine); return(PipelineInstruction.Advance); } else if (status == FtpStatusCode.ServiceTemporarilyNotAvailable) { return(PipelineInstruction.Reread); } else { throw GenerateException(status, response.StatusDescription, null); } } // // Check for the result of our attempt to use UTF8 // if (entry.Command == "OPTS utf8 on\r\n") { if (response.PositiveCompletion) { Encoding = Encoding.UTF8; } else { Encoding = Encoding.Default; } return(PipelineInstruction.Advance); } // If we are already logged in and the server returns 530 then // the server does not support re-issuing a USER command, // tear down the connection and start all over again if (entry.Command.IndexOf("USER") != -1) { // The server may not require a password for this user, so bypass the password command if (status == FtpStatusCode.LoggedInProceed) { _loginState = FtpLoginState.LoggedIn; _index++; } } // // Throw on an error with possible recovery option // if (response.TransientFailure || response.PermanentFailure) { if (status == FtpStatusCode.ServiceNotAvailable) { MarkAsRecoverableFailure(); } throw GenerateException(status, response.StatusDescription, null); } if (_loginState != FtpLoginState.LoggedIn && entry.Command.IndexOf("PASS") != -1) { // Note the fact that we logged in if (status == FtpStatusCode.NeedLoginAccount || status == FtpStatusCode.LoggedInProceed) { _loginState = FtpLoginState.LoggedIn; } else { throw GenerateException(status, response.StatusDescription, null); } } // // Parse special cases // if (entry.HasFlag(PipelineEntryFlags.CreateDataConnection) && (response.PositiveCompletion || response.PositiveIntermediate)) { bool isSocketReady; PipelineInstruction result = QueueOrCreateDataConection(entry, response, timeout, ref stream, out isSocketReady); if (!isSocketReady) { return(result); } // otherwise we have a stream to create } // // This is part of the above case and it's all about giving data stream back // if (status == FtpStatusCode.OpeningData || status == FtpStatusCode.DataAlreadyOpen) { if (_dataSocket == null) { return(PipelineInstruction.Abort); } if (!entry.HasFlag(PipelineEntryFlags.GiveDataStream)) { _abortReason = SR.Format(SR.net_ftp_invalid_status_response, status, entry.Command); return(PipelineInstruction.Abort); } // Parse out the Content length, if we can TryUpdateContentLength(response.StatusDescription); // Parse out the file name, when it is returned and use it for our ResponseUri FtpWebRequest request = (FtpWebRequest)_request; if (request.MethodInfo.ShouldParseForResponseUri) { TryUpdateResponseUri(response.StatusDescription, request); } return(QueueOrCreateFtpDataStream(ref stream)); } // // Parse responses by status code exclusivelly // // Update welcome message if (status == FtpStatusCode.LoggedInProceed) { _welcomeMessage.Append(StatusLine); } // OR set the user response ExitMessage else if (status == FtpStatusCode.ClosingControl) { _exitMessage.Append(response.StatusDescription); // And close the control stream socket on "QUIT" CloseSocket(); } // OR set us up for SSL/TLS, after this we'll be writing securely else if (status == FtpStatusCode.ServerWantsSecureSession) { // If NetworkStream is a TlsStream, then this must be in the async callback // from completing the SSL handshake. // So just let the pipeline continue. if (!(NetworkStream is TlsStream)) { FtpWebRequest request = (FtpWebRequest)_request; TlsStream tlsStream = new TlsStream(NetworkStream, Socket, request.RequestUri.Host, request.ClientCertificates); if (_isAsync) { tlsStream.BeginAuthenticateAsClient(ar => { try { tlsStream.EndAuthenticateAsClient(ar); NetworkStream = tlsStream; this.ContinueCommandPipeline(); } catch (Exception e) { this.CloseSocket(); this.InvokeRequestCallback(e); } }, null); return(PipelineInstruction.Pause); } else { tlsStream.AuthenticateAsClient(); NetworkStream = tlsStream; } } } // OR parse out the file size or file time, usually a result of sending SIZE/MDTM commands else if (status == FtpStatusCode.FileStatus) { FtpWebRequest request = (FtpWebRequest)_request; if (entry.Command.StartsWith("SIZE ")) { _contentLength = GetContentLengthFrom213Response(response.StatusDescription); } else if (entry.Command.StartsWith("MDTM ")) { _lastModified = GetLastModifiedFrom213Response(response.StatusDescription); } } // OR parse out our login directory else if (status == FtpStatusCode.PathnameCreated) { if (entry.Command == "PWD\r\n" && !entry.HasFlag(PipelineEntryFlags.UserCommand)) { _loginDirectory = GetLoginDirectory(response.StatusDescription); } } // Asserting we have some positive response else { // We only use CWD to reset ourselves back to the login directory. if (entry.Command.IndexOf("CWD") != -1) { _establishedServerDirectory = _requestedServerDirectory; } } // Intermediate responses require rereading if (response.PositiveIntermediate || (!UsingSecureStream && entry.Command == "AUTH TLS\r\n")) { return(PipelineInstruction.Reread); } return(PipelineInstruction.Advance); }
// This is called by underlying base class code, each time a new response is received from the wire or a protocol stage is resumed. // This function controls the setting up of a data socket/connection, and of saving off the server responses. protected override PipelineInstruction PipelineCallback(PipelineEntry entry, ResponseDescription response, bool timeout, ref Stream stream) { if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Command:{entry?.Command} Description:{response?.StatusDescription}"); // null response is not expected if (response == null) return PipelineInstruction.Abort; FtpStatusCode status = (FtpStatusCode)response.Status; // // Update global "current status" for FtpWebRequest // if (status != FtpStatusCode.ClosingControl) { // A 221 status won't be reflected on the user FTP response // Anything else will (by design?) StatusCode = status; StatusLine = response.StatusDescription; } // If the status code is outside the range defined in RFC (1xx to 5xx) throw if (response.InvalidStatusCode) throw new WebException(SR.net_InvalidStatusCode, WebExceptionStatus.ProtocolError); // Update the banner message if any, this is a little hack because the "entry" param is null if (_index == -1) { if (status == FtpStatusCode.SendUserCommand) { _bannerMessage = new StringBuilder(); _bannerMessage.Append(StatusLine); return PipelineInstruction.Advance; } else if (status == FtpStatusCode.ServiceTemporarilyNotAvailable) { return PipelineInstruction.Reread; } else throw GenerateException(status, response.StatusDescription, null); } // // Check for the result of our attempt to use UTF8 // if (entry.Command == "OPTS utf8 on\r\n") { if (response.PositiveCompletion) { Encoding = Encoding.UTF8; } else { Encoding = Encoding.Default; } return PipelineInstruction.Advance; } // If we are already logged in and the server returns 530 then // the server does not support re-issuing a USER command, // tear down the connection and start all over again if (entry.Command.IndexOf("USER") != -1) { // The server may not require a password for this user, so bypass the password command if (status == FtpStatusCode.LoggedInProceed) { _loginState = FtpLoginState.LoggedIn; _index++; } } // // Throw on an error with possible recovery option // if (response.TransientFailure || response.PermanentFailure) { if (status == FtpStatusCode.ServiceNotAvailable) { MarkAsRecoverableFailure(); } throw GenerateException(status, response.StatusDescription, null); } if (_loginState != FtpLoginState.LoggedIn && entry.Command.IndexOf("PASS") != -1) { // Note the fact that we logged in if (status == FtpStatusCode.NeedLoginAccount || status == FtpStatusCode.LoggedInProceed) _loginState = FtpLoginState.LoggedIn; else throw GenerateException(status, response.StatusDescription, null); } // // Parse special cases // if (entry.HasFlag(PipelineEntryFlags.CreateDataConnection) && (response.PositiveCompletion || response.PositiveIntermediate)) { bool isSocketReady; PipelineInstruction result = QueueOrCreateDataConection(entry, response, timeout, ref stream, out isSocketReady); if (!isSocketReady) return result; // otheriwse we have a stream to create } // // This is part of the above case and it's all about giving data stream back // if (status == FtpStatusCode.OpeningData || status == FtpStatusCode.DataAlreadyOpen) { if (_dataSocket == null) { return PipelineInstruction.Abort; } if (!entry.HasFlag(PipelineEntryFlags.GiveDataStream)) { _abortReason = SR.Format(SR.net_ftp_invalid_status_response, status, entry.Command); return PipelineInstruction.Abort; } // Parse out the Content length, if we can TryUpdateContentLength(response.StatusDescription); // Parse out the file name, when it is returned and use it for our ResponseUri FtpWebRequest request = (FtpWebRequest)_request; if (request.MethodInfo.ShouldParseForResponseUri) { TryUpdateResponseUri(response.StatusDescription, request); } return QueueOrCreateFtpDataStream(ref stream); } // // Parse responses by status code exclusivelly // // Update welcome message if (status == FtpStatusCode.LoggedInProceed) { _welcomeMessage.Append(StatusLine); } // OR set the user response ExitMessage else if (status == FtpStatusCode.ClosingControl) { _exitMessage.Append(response.StatusDescription); // And close the control stream socket on "QUIT" CloseSocket(); } // OR set us up for SSL/TLS, after this we'll be writing securely else if (status == FtpStatusCode.ServerWantsSecureSession) { // If NetworkStream is a TlsStream, then this must be in the async callback // from completing the SSL handshake. // So just let the pipeline continue. if (!(NetworkStream is TlsStream)) { FtpWebRequest request = (FtpWebRequest)_request; TlsStream tlsStream = new TlsStream(NetworkStream, Socket, request.RequestUri.Host, request.ClientCertificates); if (_isAsync) { tlsStream.BeginAuthenticateAsClient(ar => { try { tlsStream.EndAuthenticateAsClient(ar); NetworkStream = tlsStream; this.ContinueCommandPipeline(); } catch (Exception e) { this.CloseSocket(); this.InvokeRequestCallback(e); } }, null); return PipelineInstruction.Pause; } else { tlsStream.AuthenticateAsClient(); NetworkStream = tlsStream; } } } // OR parse out the file size or file time, usually a result of sending SIZE/MDTM commands else if (status == FtpStatusCode.FileStatus) { FtpWebRequest request = (FtpWebRequest)_request; if (entry.Command.StartsWith("SIZE ")) { _contentLength = GetContentLengthFrom213Response(response.StatusDescription); } else if (entry.Command.StartsWith("MDTM ")) { _lastModified = GetLastModifiedFrom213Response(response.StatusDescription); } } // OR parse out our login directory else if (status == FtpStatusCode.PathnameCreated) { if (entry.Command == "PWD\r\n" && !entry.HasFlag(PipelineEntryFlags.UserCommand)) { _loginDirectory = GetLoginDirectory(response.StatusDescription); } } // Asserting we have some positive response else { // We only use CWD to reset ourselves back to the login directory. if (entry.Command.IndexOf("CWD") != -1) { _establishedServerDirectory = _requestedServerDirectory; } } // Intermediate responses require rereading if (response.PositiveIntermediate || (!UsingSecureStream && entry.Command == "AUTH TLS\r\n")) { return PipelineInstruction.Reread; } return PipelineInstruction.Advance; }
internal void GetConnection(string host, int port) { if (_isConnected) { throw new InvalidOperationException(SR.SmtpAlreadyConnected); } InitializeConnection(host, port); _responseReader = new SmtpReplyReaderFactory(_networkStream); 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 e; } 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 (!(_networkStream is TlsStream)) { throw new SmtpException(SR.MailServerDoesNotSupportStartTls); } } StartTlsCommand.Send(this); TlsStream tlsStream = new TlsStream(_networkStream, _tcpClient.Client, host, _clientCertificates); tlsStream.AuthenticateAsClient(); _networkStream = tlsStream; _responseReader = new SmtpReplyReaderFactory(_networkStream); // 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, _channelBindingToken); 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; }
// Creates a FtpDataStream object, constructs a TLS stream if needed. // In case SSL and ASYNC we delay sigaling the user stream until the handshake is done. private PipelineInstruction QueueOrCreateFtpDataStream(ref Stream stream) { if (_dataSocket == null) throw new InternalException(); // // Re-entered pipeline with completed read on the TlsStream // if (_tlsStream != null) { stream = new FtpDataStream(_tlsStream, (FtpWebRequest)_request, IsFtpDataStreamWriteable()); _tlsStream = null; return PipelineInstruction.GiveStream; } NetworkStream networkStream = new NetworkStream(_dataSocket, true); if (UsingSecureStream) { FtpWebRequest request = (FtpWebRequest)_request; TlsStream tlsStream = new TlsStream(networkStream, _dataSocket, request.RequestUri.Host, request.ClientCertificates); networkStream = tlsStream; if (_isAsync) { _tlsStream = tlsStream; tlsStream.BeginAuthenticateAsClient(s_SSLHandshakeCallback, this); return PipelineInstruction.Pause; } else { tlsStream.AuthenticateAsClient(); } } stream = new FtpDataStream(networkStream, (FtpWebRequest)_request, IsFtpDataStreamWriteable()); return PipelineInstruction.GiveStream; }