using System; using System.IO; using System.Net.Sockets; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Collections.Generic; using System.Diagnostics; using System.Net; #if CORE using System.Threading.Tasks; #endif namespace FluentFTP { /// /// Event fired if a bad SSL certificate is encountered. This even is used internally; if you /// don't have a specific reason for using it you are probably looking for FtpSslValidation. /// /// /// public delegate void FtpSocketStreamSslValidation(FtpSocketStream stream, FtpSslValidationEventArgs e); /// /// Event args for the FtpSslValidationError delegate /// public class FtpSslValidationEventArgs : EventArgs { X509Certificate m_certificate = null; /// /// The certificate to be validated /// public X509Certificate Certificate { get { return m_certificate; } set { m_certificate = value; } } X509Chain m_chain = null; /// /// The certificate chain /// public X509Chain Chain { get { return m_chain; } set { m_chain = value; } } SslPolicyErrors m_policyErrors = SslPolicyErrors.None; /// /// Validation errors, if any. /// public SslPolicyErrors PolicyErrors { get { return m_policyErrors; } set { m_policyErrors = value; } } bool m_accept = false; /// /// Gets or sets a value indicating if this certificate should be accepted. The default /// value is false. If the certificate is not accepted, an AuthenticationException will /// be thrown. /// public bool Accept { get { return m_accept; } set { m_accept = value; } } } /// /// Stream class used for talking. Used by FtpClient, extended by FtpDataStream /// public class FtpSocketStream : Stream, IDisposable { /// /// Used for tacking read/write activity on the socket /// to determine if Poll() should be used to test for /// socket conenctivity. The socket in this class will /// not know it has been disconnected if the remote host /// closes the connection first. Using Poll() avoids /// the exception that would be thrown when trying to /// read or write to the disconnected socket. /// private DateTime m_lastActivity = DateTime.Now; private Socket m_socket = null; /// /// The socket used for talking /// protected Socket Socket { get { return m_socket; } private set { m_socket = value; } } int m_socketPollInterval = 15000; /// /// Gets or sets the length of time in miliseconds /// that must pass since the last socket activity /// before calling Poll() on the socket to test for /// connectivity. Setting this interval too low will /// have a negative impact on perfomance. Setting this /// interval to 0 disables Poll()'ing all together. /// The default value is 15 seconds. /// public int SocketPollInterval { get { return m_socketPollInterval; } set { m_socketPollInterval = value; } } /// /// Gets the number of available bytes on the socket, 0 if the /// socket has not been initalized. This property is used internally /// by FtpClient in an effort to detect disconnections and gracefully /// reconnect the control connection. /// internal int SocketDataAvailable { get { if (m_socket != null) return m_socket.Available; return 0; } } /// /// Gets a value indicating if this socket stream is connected /// public bool IsConnected { get { try { if (m_socket == null) return false; if (!m_socket.Connected) { Close(); return false; } if (!CanRead || !CanWrite) { Close(); return false; } if (m_socketPollInterval > 0 && DateTime.Now.Subtract(m_lastActivity).TotalMilliseconds > m_socketPollInterval) { FtpTrace.WriteLine("Testing connectivity using Socket.Poll()..."); if (m_socket.Poll(500000, SelectMode.SelectRead) && m_socket.Available == 0) { Close(); return false; } } } catch (SocketException sockex) { Close(); FtpTrace.WriteLine("FtpSocketStream.IsConnected: Caught and discarded SocketException while testing for connectivity: {0}", sockex.ToString()); return false; } catch (IOException ioex) { Close(); FtpTrace.WriteLine("FtpSocketStream.IsConnected: Caught and discarded IOException while testing for connectivity: {0}", ioex.ToString()); return false; } return true; } } /// /// Gets a value indicating if encryption is being used /// public bool IsEncrypted { get { #if NO_SSL return false; #else return m_sslStream != null; #endif } } NetworkStream m_netStream = null; /// /// The non-encrypted stream /// private NetworkStream NetworkStream { get { return m_netStream; } set { m_netStream = value; } } #if !NO_SSL SslStream m_sslStream = null; /// /// The encrypted stream /// private SslStream SslStream { get { return m_sslStream; } set { m_sslStream = value; } } #endif /// /// Underlying stream, could be a NetworkStream or SslStream /// protected Stream BaseStream { get { #if NO_SSL if (m_netStream != null) return m_netStream; #else if (m_sslStream != null) return m_sslStream; else if (m_netStream != null) return m_netStream; #endif return null; } } /// /// Gets a value indicating if this stream can be read /// public override bool CanRead { get { if (m_netStream != null) return m_netStream.CanRead; return false; } } /// /// Gets a value indicating if this stream if seekable /// public override bool CanSeek { get { return false; } } /// /// Gets a value indicating if this stream can be written to /// public override bool CanWrite { get { if (m_netStream != null) return m_netStream.CanWrite; return false; } } /// /// Gets the length of the stream /// public override long Length { get { return 0; } } /// /// Gets the current position of the stream. Trying to /// set this property throws an InvalidOperationException() /// public override long Position { get { if (BaseStream != null) return BaseStream.Position; return 0; } set { throw new InvalidOperationException(); } } event FtpSocketStreamSslValidation m_sslvalidate = null; /// /// Event is fired when a SSL certificate needs to be validated /// public event FtpSocketStreamSslValidation ValidateCertificate { add { m_sslvalidate += value; } remove { m_sslvalidate -= value; } } int m_readTimeout = Timeout.Infinite; /// /// Gets or sets the amount of time to wait for a read operation to complete. Default /// value is Timeout.Infinite. /// public override int ReadTimeout { get { return m_readTimeout; } set { m_readTimeout = value; } } int m_connectTimeout = 30000; /// /// Gets or sets the length of time miliseconds to wait /// for a connection succeed before giving up. The default /// is 30000 (30 seconds). /// public int ConnectTimeout { get { return m_connectTimeout; } set { m_connectTimeout = value; } } /// /// Gets the local end point of the socket /// public IPEndPoint LocalEndPoint { get { if (m_socket == null) return null; return (IPEndPoint)m_socket.LocalEndPoint; } } /// /// Gets the remote end point of the socket /// public IPEndPoint RemoteEndPoint { get { if (m_socket == null) return null; return (IPEndPoint)m_socket.RemoteEndPoint; } } /// /// Fires the SSL certificate validation event /// /// Certificate being validated /// Certificate chain /// Policy errors if any /// True if it was accepted, false otherwise protected bool OnValidateCertificate(X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { FtpSocketStreamSslValidation evt = m_sslvalidate; if (evt != null) { FtpSslValidationEventArgs e = new FtpSslValidationEventArgs() { Certificate = certificate, Chain = chain, PolicyErrors = errors, Accept = (errors == SslPolicyErrors.None) }; evt(this, e); return e.Accept; } // if the event was not handled then only accept // the certificate if there were no validation errors return (errors == SslPolicyErrors.None); } /// /// Throws an InvalidOperationException /// /// Ignored /// Ignored /// public override long Seek(long offset, SeekOrigin origin) { throw new InvalidOperationException(); } /// /// Throws an InvalidOperationException /// /// Ignored public override void SetLength(long value) { throw new InvalidOperationException(); } /// /// Flushes the stream /// public override void Flush() { if (!IsConnected) throw new InvalidOperationException("The FtpSocketStream object is not connected."); if (BaseStream == null) throw new InvalidOperationException("The base stream of the FtpSocketStream object is null."); BaseStream.Flush(); } /// /// Bypass the stream and read directly off the socket. /// /// The buffer to read into /// The number of bytes read internal int RawSocketRead(byte[] buffer) { int read = 0; if (m_socket != null && m_socket.Connected) { read = m_socket.Receive(buffer, buffer.Length, 0); } return read; } /// /// Reads data from the stream /// /// Buffer to read into /// Where in the buffer to start /// Number of bytes to be read /// public override int Read(byte[] buffer, int offset, int count) { #if !CORE IAsyncResult ar = null; #endif if (BaseStream == null) return 0; m_lastActivity = DateTime.Now; #if CORE return BaseStream.ReadAsync(buffer, offset, count).Result; #else ar = BaseStream.BeginRead(buffer, offset, count, null, null); if (!ar.AsyncWaitHandle.WaitOne(m_readTimeout, true)) { Close(); throw new TimeoutException("Timed out trying to read data from the socket stream!"); } return BaseStream.EndRead(ar); #endif } /// /// Reads a line from the socket /// /// A line from the stream, null if there is nothing to read public string ReadLine(System.Text.Encoding encoding) { List data = new List(); byte[] buf = new byte[1]; string line = null; while (Read(buf, 0, buf.Length) > 0) { data.Add(buf[0]); if ((char)buf[0] == '\n') { line = encoding.GetString(data.ToArray()).Trim('\r', '\n'); break; } } return line; } /// /// Writes data to the stream /// /// Buffer to write to stream /// Where in the buffer to start /// Number of bytes to be read public override void Write(byte[] buffer, int offset, int count) { if (BaseStream == null) return; BaseStream.Write(buffer, offset, count); m_lastActivity = DateTime.Now; } /// /// Writes a line to the stream using the specified encoding /// /// Encoding used for writing the line /// The data to write public void WriteLine(System.Text.Encoding encoding, string buf) { byte[] data; data = encoding.GetBytes(string.Format("{0}\r\n", buf)); Write(data, 0, data.Length); } /// /// Disposes the stream /// public new void Dispose() { FtpTrace.WriteLine("Disposing FtpSocketStream..."); Close(); } /// /// Disconnects from server /// #if CORE public void Close() { #else public override void Close() { #endif if (m_socket != null) { try { if (m_socket.Connected) { //// // Calling Shutdown() with mono causes an // exception if the remote host closed first //m_socket.Shutdown(SocketShutdown.Both); #if CORE m_socket.Dispose(); #else m_socket.Close(); #endif } #if !NET2 m_socket.Dispose(); #endif } catch (SocketException ex) { FtpTrace.WriteLine("Caught and discarded a SocketException while cleaning up the Socket: {0}", ex.ToString()); } finally { m_socket = null; } } if (m_netStream != null) { try { m_netStream.Dispose(); } catch (IOException ex) { FtpTrace.WriteLine("Caught and discarded an IOException while cleaning up the NetworkStream: {0}", ex.ToString()); } finally { m_netStream = null; } } #if !NO_SSL if (m_sslStream != null) { try { m_sslStream.Dispose(); } catch (IOException ex) { FtpTrace.WriteLine("Caught and discarded an IOException while cleaning up the SslStream: {0}", ex.ToString()); } finally { m_sslStream = null; } } #endif #if CORE base.Dispose(); #endif } /// /// Sets socket options on the underlying socket /// /// SocketOptionLevel /// SocketOptionName /// SocketOptionValue public void SetSocketOption(SocketOptionLevel level, SocketOptionName name, bool value) { if (m_socket == null) throw new InvalidOperationException("The underlying socket is null. Have you established a connection?"); m_socket.SetSocketOption(level, name, value); } /// /// Connect to the specified host /// /// The host to connect to /// The port to connect to /// Internet Protocol versions to support durring the connection phase public void Connect(string host, int port, FtpIpVersion ipVersions) { #if CORE IPAddress[] addresses = Dns.GetHostAddressesAsync(host).Result; #else IAsyncResult ar = null; IPAddress[] addresses = Dns.GetHostAddresses(host); #endif if (ipVersions == 0) throw new ArgumentException("The ipVersions parameter must contain at least 1 flag."); for (int i = 0; i < addresses.Length; i++) { #if DEBUG FtpTrace.WriteLine("{0}: {1}", addresses[i].AddressFamily.ToString(), addresses[i].ToString()); #endif // we don't need to do this check unless // a particular version of IP has been // omitted so we won't. if (ipVersions != FtpIpVersion.ANY) { switch (addresses[i].AddressFamily) { case AddressFamily.InterNetwork: if ((ipVersions & FtpIpVersion.IPv4) != FtpIpVersion.IPv4) { #if DEBUG FtpTrace.WriteLine("SKIPPED!"); #endif continue; } break; case AddressFamily.InterNetworkV6: if ((ipVersions & FtpIpVersion.IPv6) != FtpIpVersion.IPv6) { #if DEBUG FtpTrace.WriteLine("SKIPPED!"); #endif continue; } break; } } m_socket = new Socket(addresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); #if CORE m_socket.ConnectAsync(addresses[i], port).Wait(); #else ar = m_socket.BeginConnect(addresses[i], port, null, null); if (!ar.AsyncWaitHandle.WaitOne(m_connectTimeout, true)) { Close(); // check to see if we're out of addresses // and if we are throw a TimeoutException if (i + 1 == addresses.Length) throw new TimeoutException("Timed out trying to connect!"); } else { m_socket.EndConnect(ar); // we got a connection, break out // of the loop. break; } #endif } // make sure that we actually connected to // one of the addresses returned from GetHostAddresses() if (m_socket == null || !m_socket.Connected) { Close(); throw new IOException("Failed to connect to host."); } m_netStream = new NetworkStream(m_socket); m_lastActivity = DateTime.Now; } #if !NO_SSL /// /// Activates SSL on this stream using default protocols. Fires the ValidateCertificate event. /// If this event is not handled and there are SslPolicyErrors present, the certificate will /// not be accepted. /// /// The host to authenticate the certiciate against public void ActivateEncryption(string targethost) { #if CORE ActivateEncryption(targethost, null, SslProtocols.Tls11 | SslProtocols.Ssl3); #else ActivateEncryption(targethost, null, SslProtocols.Default); #endif } /// /// Activates SSL on this stream using default protocols. Fires the ValidateCertificate event. /// If this event is not handled and there are SslPolicyErrors present, the certificate will /// not be accepted. /// /// The host to authenticate the certiciate against /// A collection of client certificates to use when authenticating the SSL stream public void ActivateEncryption(string targethost, X509CertificateCollection clientCerts) { #if CORE ActivateEncryption(targethost, clientCerts, SslProtocols.Tls11 | SslProtocols.Ssl3); #else ActivateEncryption(targethost, clientCerts, SslProtocols.Default); #endif } /// /// Activates SSL on this stream using the specified protocols. Fires the ValidateCertificate event. /// If this event is not handled and there are SslPolicyErrors present, the certificate will /// not be accepted. /// /// The host to authenticate the certiciate against /// A collection of client certificates to use when authenticating the SSL stream /// A bitwise parameter for supported encryption protocols. public void ActivateEncryption(string targethost, X509CertificateCollection clientCerts, SslProtocols sslProtocols) { if (!IsConnected) throw new InvalidOperationException("The FtpSocketStream object is not connected."); if (m_netStream == null) throw new InvalidOperationException("The base network stream is null."); if (m_sslStream != null) throw new InvalidOperationException("SSL Encryption has already been enabled on this stream."); try { DateTime auth_start; TimeSpan auth_time_total; m_sslStream = new SslStream(NetworkStream, true, new RemoteCertificateValidationCallback( delegate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return OnValidateCertificate(certificate, chain, sslPolicyErrors); })); auth_start = DateTime.Now; #if CORE m_sslStream.AuthenticateAsClientAsync(targethost, clientCerts, sslProtocols, true).Wait(); #else m_sslStream.AuthenticateAsClient(targethost, clientCerts, sslProtocols, true); #endif auth_time_total = DateTime.Now.Subtract(auth_start); FtpTrace.WriteLine("Time to activate encryption: {0}h {1}m {2}s, Total Seconds: {3}.", auth_time_total.Hours, auth_time_total.Minutes, auth_time_total.Seconds, auth_time_total.TotalSeconds); } catch (AuthenticationException ex) { // authentication failed and in addition it left our // ssl stream in an unsuable state so cleanup needs // to be done and the exception can be re-thrown for // handling down the chain. Close(); throw ex; } } #endif /// /// Instructs this stream to listen for connections on the specified address and port /// /// The address to listen on /// The port to listen on public void Listen(IPAddress address, int port) { if (!IsConnected) { if (m_socket == null) m_socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); m_socket.Bind(new IPEndPoint(address, port)); m_socket.Listen(1); } } /// /// Accepts a connection from a listening socket /// public void Accept() { if (m_socket != null) m_socket = m_socket.Accept(); } #if CORE public async Task AcceptAsync() { if (m_socket != null) { m_socket = await m_socket.AcceptAsync(); } } #else /// /// Asynchronously accepts a connection from a listening socket /// /// /// /// public IAsyncResult BeginAccept(AsyncCallback callback, object state) { if (m_socket != null) return m_socket.BeginAccept(callback, state); return null; } /// /// Completes a BeginAccept() operation /// /// IAsyncResult returned from BeginAccept public void EndAccept(IAsyncResult ar) { if (m_socket != null) { m_socket = m_socket.EndAccept(ar); m_netStream = new NetworkStream(m_socket); } } #endif } }