//****************************************************************************************************** // FtpControlChannel.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 05/22/2003 - J. Ritchie Carroll // Generated original version of source code. // 08/06/2009 - Josh L. Patterson // Edited Comments. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 12/17/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** #region [ Contributor License Agreements ] //******************************************************************************************************* // // Code based on the following project: // http://www.codeproject.com/KB/IP/net_ftp_upload.aspx // // Copyright Alex Kwok & Uwe Keim // // The Code Project Open License (CPOL): // http://www.codeproject.com/info/cpol10.aspx // //******************************************************************************************************* #endregion using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading; namespace GSF.Net.Ftp { #region [ Enumerations ] /// /// FTP transfer mode enumeration. /// public enum TransferMode { /// /// Transfer files in ASCII mode. /// Ascii, /// /// Transfer files in binary mode. /// Binary, /// /// File transfer mode is undetermined. /// Unknown } #endregion /// /// FTP control channel. /// public class FtpControlChannel : IDisposable { #region [ Members ] // Fields private readonly FtpClient m_sessionHost; private FtpSessionConnected m_session; private TcpClient m_connection; private string m_server; private int m_port; private int m_timeout; private bool m_passive; private IPAddress m_activeAddress; private Range m_activePortRange; private int m_lastActivePort; private TransferMode m_currentTransferMode; private FtpResponse m_lastResponse; private bool m_disposed; #endregion #region [ Constructors ] internal FtpControlChannel(FtpClient host) { m_connection = new TcpClient(); m_server = "localhost"; m_port = 21; m_timeout = 30000; m_passive = true; m_sessionHost = host; m_currentTransferMode = TransferMode.Unknown; } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~FtpControlChannel() { Dispose(false); } #endregion #region [ Properties ] internal string Server { get { return m_server; } set { if (value.Length == 0) throw new ArgumentNullException(nameof(value), "Server property must not be blank."); m_server = value; } } internal int Port { get { return m_port; } set { m_port = value; } } internal int Timeout { get { return m_timeout; } set { m_timeout = value; } } internal bool Passive { get { return m_passive; } set { m_passive = value; } } internal IPAddress ActiveAddress { get { return m_activeAddress ?? (m_activeAddress = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(addr => addr.AddressFamily == AddressFamily.InterNetwork)); } set { m_activeAddress = value; } } internal Range DataChannelPortRange { get { return m_activePortRange; } set { if (value.Start <= IPEndPoint.MinPort) throw new ArgumentException($"Minimum port in range must be greater than {IPEndPoint.MinPort}.", nameof(value)); if (value.End > IPEndPoint.MaxPort) throw new ArgumentException($"Maximum port in range must be less than or equal to {IPEndPoint.MaxPort}.", nameof(value)); if (value.Start > value.End) throw new ArgumentException("Start port must be less than or equal to end port."); m_activePortRange = value; } } /// /// Last response from control channel. /// public FtpResponse LastResponse => m_lastResponse; internal FtpSessionConnected Session { get { return m_session; } set { m_session = value; } } #endregion #region [ Methods ] /// /// Releases all the resources used by the object. /// public void Close() { Dispose(); } /// /// Releases all the resources used by the object. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!m_disposed) { try { // This will be done regardless of whether the object is finalized or disposed. if (disposing) { m_lastResponse = null; if ((object)m_connection != null) m_connection.Close(); m_connection = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } /// /// Connects the . /// public void Connect() { if ((object)m_connection == null) return; m_connection.Connect(m_server, m_port); try { NetworkStream stream = m_connection.GetStream(); stream.ReadTimeout = m_timeout; stream.WriteTimeout = m_timeout; m_lastResponse = new FtpResponse(stream); if (m_lastResponse.Code != FtpResponse.ServiceReady) throw new FtpServerDownException("FTP service unavailable.", m_lastResponse); } catch { Close(); throw; } } /// /// Send FTP command to control channel. /// /// A representing the command to send. public void Command(string cmd) { if ((object)m_connection == null) return; // If the client receives an exception when establishing a data channel, // it may not have checked for the server's response when handling the exception FlushResponses(); byte[] buff = Encoding.Default.GetBytes(cmd + Environment.NewLine); NetworkStream stream = m_connection.GetStream(); m_sessionHost.OnCommandSent(cmd); stream.Write(buff, 0, buff.Length); RefreshResponse(); } /// /// Refresh response from control channel. /// public void RefreshResponse() { if ((object)m_connection == null) return; lock (this) { m_lastResponse = new FtpResponse(m_connection.GetStream()); } foreach (string s in m_lastResponse.Responses) { m_sessionHost.OnResponseReceived(s); } } /// /// Flushes data from the control channel to get ready for the next response. /// public void FlushResponses() { StringBuilder responses = new StringBuilder(); lock (this) { NetworkStream stream = m_connection.GetStream(); while (stream.DataAvailable) { int b = stream.ReadByte(); if (b < 0) break; char c = Encoding.ASCII.GetChars(new[] { (byte)b })[0]; responses.Append(c); } } List lines = responses .ToString() .Split('\n') .Where(line => !string.IsNullOrWhiteSpace(line)) .Select(line => line + "\n") .ToList(); foreach (string line in lines) m_sessionHost.OnResponseReceived(line); } internal void REST(long offset) { Command("REST " + offset); if (m_lastResponse.Code != FtpResponse.RequestFileActionPending) throw new FtpResumeNotSupportedException(m_lastResponse); } internal void STOR(string name) { Type(TransferMode.Binary); Command("STOR " + name); if (m_lastResponse.Code != FtpResponse.DataChannelOpenedTransferStart && m_lastResponse.Code != FtpResponse.FileOkBeginOpenDataChannel) throw new FtpCommandException("Failed to send file " + name + ".", m_lastResponse); } internal void RETR(string name) { Type(TransferMode.Binary); Command("RETR " + name); if (m_lastResponse.Code != FtpResponse.DataChannelOpenedTransferStart && m_lastResponse.Code != FtpResponse.FileOkBeginOpenDataChannel) throw new FtpCommandException("Failed to retrieve file " + name + ".", m_lastResponse); } internal void DELE(string fileName) { Command("DELE " + fileName); if (m_lastResponse.Code != FtpResponse.RequestFileActionComplete) // 250) throw new FtpCommandException("Failed to delete file " + fileName + ".", m_lastResponse); } internal void RMD(string dirName) { Command("RMD " + dirName); if (m_lastResponse.Code != FtpResponse.RequestFileActionComplete) // 250) throw new FtpCommandException("Failed to remove subdirectory " + dirName + ".", m_lastResponse); } internal string PWD() { Command("PWD"); if (m_lastResponse.Code != 257) throw new FtpCommandException("Cannot get current directory.", m_lastResponse); Match m = s_pwdExpression.Match(m_lastResponse.Message); return m.Groups[2].Value; } internal void CDUP() { Command("CDUP"); if (m_lastResponse.Code != FtpResponse.RequestFileActionComplete) throw new FtpCommandException("Cannot move to parent directory (CDUP).", m_lastResponse); } internal void CWD(string path) { Command("CWD " + path); if (m_lastResponse.Code != FtpResponse.RequestFileActionComplete && m_lastResponse.Code != FtpResponse.ClosingDataChannel) throw new FtpCommandException("Cannot change directory to " + path + ".", m_lastResponse); } internal void QUIT() { Command("QUIT"); } internal void Type(TransferMode mode) { if (mode == TransferMode.Unknown) return; if (mode == TransferMode.Ascii && m_currentTransferMode != TransferMode.Ascii) Command("TYPE A"); else if (mode == TransferMode.Binary && m_currentTransferMode != TransferMode.Binary) Command("TYPE I"); m_currentTransferMode = mode; } internal void Rename(string oldName, string newName) { Command("RNFR " + oldName); if (m_lastResponse.Code != FtpResponse.RequestFileActionPending) throw new FtpCommandException("Failed to rename file from " + oldName + " to " + newName + ".", m_lastResponse); Command("RNTO " + newName); if (m_lastResponse.Code != FtpResponse.RequestFileActionComplete) throw new FtpCommandException("Failed to rename file from " + oldName + " to " + newName + ".", m_lastResponse); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "StreamReader leaves stream open so that outer using can close")] internal Queue List(bool passive) { const string errorMsgListing = "Error when listing server directory."; try { Type(TransferMode.Ascii); Queue lineQueue = new Queue(); using (FtpDataStream dataStream = GetDataStream()) using (StreamReader lineReader = new StreamReader(dataStream, Encoding.Default, true, 1024, true)) { Command("LIST"); if (m_lastResponse.Code != FtpResponse.DataChannelOpenedTransferStart && m_lastResponse.Code != FtpResponse.FileOkBeginOpenDataChannel) { dataStream.Close(true); throw new FtpCommandException(errorMsgListing, m_lastResponse); } string line = lineReader.ReadLine(); while ((object)line != null) { lineQueue.Enqueue(line); line = lineReader.ReadLine(); } } if (m_lastResponse.Code != FtpResponse.ClosingDataChannel) throw new FtpCommandException(errorMsgListing, m_lastResponse); return lineQueue; } catch (IOException ie) { throw new Exception(errorMsgListing, ie); } catch (SocketException se) { throw new Exception(errorMsgListing, se); } } internal FtpDataStream GetDataStream() { return GetDataStream(TransferDirection.Download); } internal FtpDataStream GetDataStream(TransferDirection direction) { return m_passive ? GetPassiveDataStream(direction) : GetActiveDataStream(direction); } internal FtpDataStream GetActiveDataStream() { return GetActiveDataStream(TransferDirection.Download); } internal FtpDataStream GetActiveDataStream(TransferDirection direction) { IPAddress address = ActiveAddress; byte[] addressBytes = address.GetAddressBytes(); int firstPort = GetActivePort(); int port = firstPort; while (true) { try { TcpListener listener = new TcpListener(address, port); TcpClient client; listener.Start(); try { port = (listener.LocalEndpoint as IPEndPoint)?.Port ?? 0; Command($"PORT {addressBytes[0]},{addressBytes[1]},{addressBytes[2]},{addressBytes[3]},{port / 256},{port % 256}"); for (int i = 0; !listener.Pending(); i++) { if (i * 200 >= m_timeout) throw new TimeoutException("Timeout expired while waiting for connection on active FTP data channel."); Thread.Sleep(200); } client = listener.AcceptTcpClient(); if (direction == TransferDirection.Download) return new FtpInputDataStream(this, client); else return new FtpOutputDataStream(this, client); } finally { listener.Stop(); } } catch (IOException ie) { throw new Exception("Failed to open active port (" + port + ") data connection due to IO exception: " + ie.Message + ".", ie); } catch (SocketException se) { if (se.SocketErrorCode != SocketError.AddressAlreadyInUse) throw new Exception("Failed to open active port (" + port + ") data connection due to socket exception: " + se.Message + ".", se); } port = GetActivePort(); if (port == firstPort) throw new Exception($"All ports in active port range ({m_activePortRange.Start}-{m_activePortRange.End}) are already in use."); } } internal FtpDataStream GetPassiveDataStream() { return GetPassiveDataStream(TransferDirection.Download); } internal FtpDataStream GetPassiveDataStream(TransferDirection direction) { TcpClient client = new TcpClient(); int port = 0; try { port = GetPassivePort(); client.Connect(m_server, port); if (direction == TransferDirection.Download) return new FtpInputDataStream(this, client); else return new FtpOutputDataStream(this, client); } catch (IOException ie) { throw new Exception("Failed to open passive port (" + port + ") data connection due to IO exception: " + ie.Message + ".", ie); } catch (SocketException se) { throw new Exception("Failed to open passive port (" + port + ") data connection due to socket exception: " + se.Message + ".", se); } } private int GetActivePort() { if ((object)m_activePortRange == null) return 0; if (m_lastActivePort == 0) m_lastActivePort = GSF.Security.Cryptography.Random.Int32Between(m_activePortRange.Start, m_activePortRange.End); else if (m_lastActivePort == m_activePortRange.End) m_lastActivePort = m_activePortRange.Start; else m_lastActivePort++; return m_lastActivePort; } private int GetPassivePort() { Command("PASV"); if (m_lastResponse.Code == FtpResponse.EnterPassiveMode) { string[] numbers = s_regularExpression.Match(m_lastResponse.Message).Groups[2].Value.Split(','); return int.Parse(numbers[4]) * 256 + int.Parse(numbers[5]); } else { throw new FtpCommandException("Failed to enter passive mode. - " + m_lastResponse.Code + " - " + m_lastResponse.Message, m_lastResponse); } } #endregion #region [ Static ] // Static Fields private static readonly Regex s_regularExpression = new Regex("(\\()(.*)(\\))"); private static readonly Regex s_pwdExpression = new Regex("(\")(.*)(\")"); #endregion } }