//****************************************************************************************************** // 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.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading; using Random = GSF.Security.Cryptography.Random; 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 TcpClient m_connection; private string m_server; private IPAddress m_activeAddress; private Range m_activePortRange; private int m_lastActivePort; private TransferMode m_currentTransferMode; private bool m_disposed; #endregion #region [ Constructors ] internal FtpControlChannel(FtpClient host) { m_connection = new TcpClient(); m_server = "localhost"; Port = 21; Timeout = 30000; 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 => 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; set; } internal int Timeout { get; set; } internal bool Passive { get; set; } internal IPAddress ActiveAddress { get => m_activeAddress ?? (m_activeAddress = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(addr => addr.AddressFamily == AddressFamily.InterNetwork)); set => m_activeAddress = value; } internal Range DataChannelPortRange { get => 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 { get; private set; } internal FtpSessionConnected Session { get; set; } #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. [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "m_connection", Justification = "Member is properly disposed")] protected virtual void Dispose(bool disposing) { if (m_disposed) return; try { if (!disposing) return; LastResponse = null; m_connection?.Dispose(); m_connection = null; } finally { m_disposed = true; // Prevent duplicate dispose. } } /// /// Connects the . /// public void Connect() { if (m_connection == null) return; m_connection.Connect(m_server, Port); try { NetworkStream stream = m_connection.GetStream(); stream.ReadTimeout = Timeout; stream.WriteTimeout = Timeout; LastResponse = new FtpResponse(stream); if (LastResponse.Code != FtpResponse.ServiceReady) throw new FtpServerDownException("FTP service unavailable.", LastResponse); } catch { Close(); throw; } } /// /// Send FTP command to control channel. /// /// A representing the command to send. public void Command(string cmd) { if (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 (m_connection == null) return; lock (this) LastResponse = new FtpResponse(m_connection.GetStream()); foreach (string s in 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 (LastResponse.Code != FtpResponse.RequestFileActionPending) throw new FtpResumeNotSupportedException(LastResponse); } internal void STOR(string name) { Type(TransferMode.Binary); Command($"STOR {name}"); if (LastResponse.Code != FtpResponse.DataChannelOpenedTransferStart && LastResponse.Code != FtpResponse.FileOkBeginOpenDataChannel) throw new FtpCommandException($"Failed to send file {name}.", LastResponse); } internal void RETR(string name) { Type(TransferMode.Binary); Command($"RETR {name}"); if (LastResponse.Code != FtpResponse.DataChannelOpenedTransferStart && LastResponse.Code != FtpResponse.FileOkBeginOpenDataChannel) throw new FtpCommandException($"Failed to retrieve file {name}.", LastResponse); } internal void DELE(string fileName) { Command($"DELE {fileName}"); if (LastResponse.Code != FtpResponse.RequestFileActionComplete) // 250) throw new FtpCommandException($"Failed to delete file {fileName}.", LastResponse); } internal void RMD(string dirName) { Command($"RMD {dirName}"); if (LastResponse.Code != FtpResponse.RequestFileActionComplete) // 250) throw new FtpCommandException($"Failed to remove subdirectory {dirName}.", LastResponse); } internal string PWD() { Command("PWD"); if (LastResponse.Code != 257) throw new FtpCommandException("Cannot get current directory.", LastResponse); Match m = s_pwdExpression.Match(LastResponse.Message); return m.Groups[2].Value; } internal void CDUP() { Command("CDUP"); if (LastResponse.Code != FtpResponse.RequestFileActionComplete) throw new FtpCommandException("Cannot move to parent directory (CDUP).", LastResponse); } internal void CWD(string path) { Command($"CWD {path}"); if (LastResponse.Code != FtpResponse.RequestFileActionComplete && LastResponse.Code != FtpResponse.ClosingDataChannel) throw new FtpCommandException($"Cannot change directory to {path}.", LastResponse); } internal void QUIT() { Command("QUIT"); } internal void Type(TransferMode mode) { switch (mode) { case TransferMode.Unknown: return; case TransferMode.Ascii when m_currentTransferMode != TransferMode.Ascii: Command("TYPE A"); break; case TransferMode.Binary when m_currentTransferMode != TransferMode.Binary: Command("TYPE I"); break; } m_currentTransferMode = mode; } internal void Rename(string oldName, string newName) { Command($"RNFR {oldName}"); if (LastResponse.Code != FtpResponse.RequestFileActionPending) throw new FtpCommandException($"Failed to rename file from {oldName} to {newName}.", LastResponse); Command($"RNTO {newName}"); if (LastResponse.Code != FtpResponse.RequestFileActionComplete) throw new FtpCommandException($"Failed to rename file from {oldName} to {newName}.", LastResponse); } [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 (LastResponse.Code != FtpResponse.DataChannelOpenedTransferStart && LastResponse.Code != FtpResponse.FileOkBeginOpenDataChannel) { dataStream.Close(true); throw new FtpCommandException(errorMsgListing, LastResponse); } string line = lineReader.ReadLine(); while (line != null) { lineQueue.Enqueue(line); line = lineReader.ReadLine(); } } if (LastResponse.Code != FtpResponse.ClosingDataChannel) throw new FtpCommandException(errorMsgListing, 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 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); 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 >= Timeout) throw new TimeoutException("Timeout expired while waiting for connection on active FTP data channel."); Thread.Sleep(200); } TcpClient 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); 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 (m_activePortRange == null) return 0; if (m_lastActivePort == 0) m_lastActivePort = 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 (LastResponse.Code != FtpResponse.EnterPassiveMode) throw new FtpCommandException($"Failed to enter passive mode. - {LastResponse.Code} - {LastResponse.Message}", LastResponse); string[] numbers = s_regularExpression.Match(LastResponse.Message).Groups[2].Value.Split(','); return int.Parse(numbers[4]) * 256 + int.Parse(numbers[5]); } #endregion #region [ Static ] // Static Fields private static readonly Regex s_regularExpression = new Regex("(\\()(.*)(\\))"); private static readonly Regex s_pwdExpression = new Regex("(\")(.*)(\")"); #endregion } }