// edtFTPnet // // Copyright (C) 2004 Enterprise Distributed Technologies Ltd // // www.enterprisedt.com // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // Bug fixes, suggestions and comments should posted on // http://www.enterprisedt.com/forums/index.php // // Change Log: // // $Log: FTPControlSocket.cs,v $ // Revision 1.11 2004/11/20 22:33:00 bruceb // removed full classnames // // Revision 1.10 2004/11/15 23:27:03 hans // *** empty log message *** // // Revision 1.9 2004/11/13 19:05:13 bruceb // GetStream removed arg // // Revision 1.8 2004/11/06 11:10:02 bruceb // tidied namespaces, changed IOException to SystemException // // Revision 1.7 2004/11/05 20:00:13 bruceb // events added // // Revision 1.6 2004/11/04 22:32:26 bruceb // made many protected methods internal // // Revision 1.5 2004/11/04 21:18:13 hans // *** empty log message *** // // Revision 1.4 2004/10/29 14:30:31 bruceb // BaseSocket changes // // using System; using System.IO; using System.Collections; using System.Net; using System.Net.Sockets; using EnterpriseDT.Net; using System.Text; using Logger = EnterpriseDT.Util.Debug.Logger; namespace EnterpriseDT.Net.Ftp { /// Supports client-side FTP operations /// /// /// Bruce Blackshaw /// /// $LastChangedRevision$ /// /// public class FTPControlSocket { /// /// Event for notifying start of a transfer /// internal event FTPMessageHandler CommandSent; /// /// Event for notifying start of a transfer /// internal event FTPMessageHandler ReplyReceived; /// /// Get/Set strict checking of FTP return codes. If strict /// checking is on (the default) code must exactly match the expected /// code. If strict checking is off, only the first digit must match. /// virtual internal bool StrictReturnCodes { set { this.strictReturnCodes = value; } get { return strictReturnCodes; } } /// /// Get/Set the TCP timeout on the underlying control socket. /// virtual internal int Timeout { set { timeout = value; if (controlSock == null) throw new System.SystemException("Failed to set timeout - no control socket"); SetSocketTimeout(controlSock, value); } get { return timeout; } } /// Standard FTP end of line sequence internal const string EOL = "\r\n"; /// The default and standard control port number for FTP public const int CONTROL_PORT = 21; /// Used to flag messages private const string DEBUG_ARROW = "---> "; /// Start of password message private static readonly string PASSWORD_MESSAGE = DEBUG_ARROW + "PASS"; /// Logging object private Logger log; /// Use strict return codes if true private bool strictReturnCodes = true; /// Address of the remote host protected IPAddress remoteHost = null; protected int controlPort = -1; /// The underlying socket. protected BaseSocket controlSock = null; /// /// The timeout for the control socket /// protected int timeout = 0; /// The write that writes to the control socket protected StreamWriter writer = null; /// The reader that reads control data from the /// control socket /// protected StreamReader reader = null; /// /// Constructor. Performs TCP connection and /// sets up reader/writer. Allows different control /// port to be used /// /// /// Remote inet address /// /// /// port for control stream /// /// /// the length of the timeout, in milliseconds /// internal FTPControlSocket(IPAddress remoteHost, int controlPort, int timeout) { Initialize( new StandardSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp), remoteHost, controlPort, timeout); } /// /// Default constructor /// internal FTPControlSocket() { } /// /// Performs TCP connection and sets up reader/writer. /// Allows different control port to be used /// /// /// Socket instance /// /// /// address of remote host /// /// /// port for control stream /// /// /// the length of the timeout, in milliseconds /// internal void Initialize(BaseSocket sock, IPAddress remoteHost, int controlPort, int timeout) { this.remoteHost = remoteHost; this.controlPort = controlPort; this.timeout = timeout; log = Logger.GetLogger(typeof(FTPControlSocket)); // establish socket connection & set timeouts controlSock = sock; ConnectSocket(controlSock, remoteHost, controlPort); Timeout = timeout; InitStreams(); ValidateConnection(); } /// /// Establishes the socket connection /// /// /// Socket instance /// /// /// IP address to connect to /// /// /// port to connect to /// internal virtual void ConnectSocket(BaseSocket socket, IPAddress address, int port) { socket.Connect(new IPEndPoint(address, port)); } /// Checks that the standard 220 reply is returned /// following the initiated connection /// internal void ValidateConnection() { FTPReply reply = ReadReply(); ValidateReply(reply, "220"); } /// Obtain the reader/writer streams for this /// connection /// internal void InitStreams() { Stream stream = controlSock.GetStream(); writer = new StreamWriter(stream, Encoding.GetEncoding("US-ASCII")); reader = new StreamReader(stream, Encoding.GetEncoding("US-ASCII")); } /// /// Quit this FTP session and clean up. /// internal virtual void Logout() { SystemException ex = null; try { writer.Close(); } catch (SystemException e) { ex = e; } try { reader.Close(); } catch (SystemException e) { ex = e; } try { controlSock.Close(); } catch (SystemException e) { ex = e; } if (ex != null) throw ex; } /// /// Request a data socket be created on the /// server, connect to it and return our /// connected socket. /// /// /// The mode to connect in /// /// /// connected data socket /// internal virtual FTPDataSocket CreateDataSocket(FTPConnectMode connectMode) { if (connectMode == FTPConnectMode.ACTIVE) { return CreateDataSocketActive(); } else { // PASV return CreateDataSocketPASV(); } } /// /// Request a data socket be created on the Client /// client on any free port, do not connect it to yet. /// /// /// not connected data socket /// internal virtual FTPDataSocket CreateDataSocketActive() { // use any available port return NewActiveDataSocket(0); } /// /// Sets the data port on the server, i.e. sends a PORT /// command /// /// local endpoint /// internal void SetDataPort(IPEndPoint ep) { byte[] hostBytes = BitConverter.GetBytes(ep.Address.Address); // This is a .NET 1.1 API // byte[] hostBytes = ep.Address.GetAddressBytes(); byte[] portBytes = ToByteArray((ushort)ep.Port); // assemble the PORT command string cmd = new StringBuilder("PORT "). Append((short)hostBytes[0]).Append(","). Append((short)hostBytes[1]).Append(","). Append((short)hostBytes[2]).Append(","). Append((short)hostBytes[3]).Append(","). Append((short)portBytes[0]).Append(","). Append((short)portBytes[1]).ToString(); // send command and check reply FTPReply reply = SendCommand(cmd); ValidateReply(reply, "200"); } /// /// Convert a short into a byte array /// /// value to convert /// /// a byte array /// /// internal byte[] ToByteArray(ushort val) { byte[] bytes = new byte[2]; bytes[0] = (byte) (val >> 8); // bits 1- 8 bytes[1] = (byte) (val & 0x00FF); // bits 9-16 return bytes; } /// /// Request a data socket be created on the /// server, connect to it and return our /// connected socket. /// /// connected data socket /// internal virtual FTPDataSocket CreateDataSocketPASV() { // PASSIVE command - tells the server to listen for // a connection attempt rather than initiating it FTPReply replyObj = SendCommand("PASV"); ValidateReply(replyObj, "227"); string reply = replyObj.ReplyText; // The reply to PASV is in the form: // 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). // where h1..h4 are the IP address to connect and // p1,p2 the port number // Example: // 227 Entering Passive Mode (128,3,122,1,15,87). // NOTE: PASV command in IBM/Mainframe returns the string // 227 Entering Passive Mode 128,3,122,1,15,87 (missing // brackets) // extract the IP data string from between the brackets int startIP = reply.IndexOf((System.Char) '('); int endIP = reply.IndexOf((System.Char) ')'); // allow for IBM missing brackets around IP address if (startIP < 0 && endIP < 0) { startIP = reply.ToUpper().LastIndexOf("MODE") + 4; endIP = reply.Length; } string ipData = reply.Substring(startIP + 1, (endIP) - (startIP + 1)); int[] parts = new int[6]; int len = ipData.Length; int partCount = 0; StringBuilder buf = new StringBuilder(); // loop thru and examine each char for (int i = 0; i < len && partCount <= 6; i++) { char ch = ipData[i]; if (System.Char.IsDigit(ch)) buf.Append(ch); else if (ch != ',') { throw new FTPException("Malformed PASV reply: " + reply); } // get the part if (ch == ',' || i + 1 == len) { // at end or at separator try { parts[partCount++] = System.Int32.Parse(buf.ToString()); buf.Length = 0; } catch (FormatException) { throw new FTPException("Malformed PASV reply: " + reply); } } } // assemble the IP address // we try connecting, so we don't bother checking digits etc string ipAddress = parts[0] + "." + parts[1] + "." + parts[2] + "." + parts[3]; // assemble the port number int port = (parts[4] << 8) + parts[5]; // create the socket return NewPassiveDataSocket(ipAddress, port); } /// Constructs a new FTPDataSocket object (client mode) and connect /// to the given remote host and port number. /// /// /// IP Address to connect to. /// /// Remote port to connect to. /// /// A new FTPDataSocket object (client mode) which is /// connected to the given server. /// /// SystemException Thrown if no TCP/IP connection could be made. internal virtual FTPDataSocket NewPassiveDataSocket(string ipAddress, int port) { IPAddress ad = IPAddress.Parse(ipAddress); IPEndPoint ipe = new IPEndPoint(ad, port); BaseSocket sock = new StandardSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); SetSocketTimeout(sock, timeout); sock.Connect(ipe); return new FTPPassiveDataSocket(sock); } /// /// Constructs a new FTPDataSocket object (server mode) which will /// listen on the given port number. /// /// Remote port to listen on. /// /// A new FTPDataSocket object (server mode) which is /// configured to listen on the given port. /// /// SystemException Thrown if an error occurred when creating the socket. internal virtual FTPDataSocket NewActiveDataSocket(int port) { // create listening socket at a system allocated port BaseSocket sock = new StandardSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // choose any port IPEndPoint endPoint = new IPEndPoint(((IPEndPoint)controlSock.LocalEndPoint).Address, 0); sock.Bind(endPoint); // queue up to 5 connections sock.Listen(5); // find out ip & port we are listening on SetDataPort((IPEndPoint)sock.LocalEndPoint); return new FTPActiveDataSocket(sock); } /// Send a command to the FTP server and /// return the server's reply as a structured /// reply object /// /// /// command to send /// /// reply to the supplied command /// public virtual FTPReply SendCommand(string command) { WriteCommand(command); // and read the result return ReadReply(); } /// Send a command to the FTP server. Don't /// read the reply /// /// /// command to send /// internal virtual void WriteCommand(string command) { Log(DEBUG_ARROW + command, true); // send it writer.Write(command + EOL); writer.Flush(); } /// Read the FTP server's reply to a previously /// issued command. RFC 959 states that a reply /// consists of the 3 digit code followed by text. /// The 3 digit code is followed by a hyphen if it /// is a muliline response, and the last line starts /// with the same 3 digit code. /// /// /// structured reply object /// internal virtual FTPReply ReadReply() { string line = reader.ReadLine(); if (line == null || line.Length == 0) throw new SystemException("Unexpected null reply received"); Log(line, false); string replyCode = line.Substring(0, (3) - (0)); StringBuilder reply = new StringBuilder(""); if (line.Length > 3) reply.Append(line.Substring(4)); ArrayList dataLines = null; // check for multiline response and build up // the reply if (line[3] == '-') { dataLines = ArrayList.Synchronized(new ArrayList(10)); bool complete = false; while (!complete) { line = reader.ReadLine(); if (line == null) throw new SystemException("Unexpected null reply received"); Log(line, false); if (line.Length > 3 && line.Substring(0, (3) - (0)).Equals(replyCode) && line[3] == ' ') { reply.Append(line.Substring(3)); complete = true; } else { // not the last line reply.Append(" ").Append(line); dataLines.Add(line); } } // end while } // end if if (dataLines != null) { string[] data = new string[dataLines.Count]; dataLines.CopyTo(data); return new FTPReply(replyCode, reply.ToString(), data); } else { return new FTPReply(replyCode, reply.ToString()); } } /// /// Validate the response the host has supplied against the /// expected reply. If we get an unexpected reply we throw an /// exception, setting the message to that returned by the /// FTP server /// /// the entire reply string we received /// /// the reply we expected to receive /// /// internal virtual FTPReply ValidateReply(string reply, string expectedReplyCode) { FTPReply replyObj = new FTPReply(reply); if (ValidateReplyCode(replyObj, expectedReplyCode)) return replyObj; // if unexpected reply, throw an exception throw new FTPException(replyObj); } /// Validate the response the host has supplied against the /// expected reply. If we get an unexpected reply we throw an /// exception, setting the message to that returned by the /// FTP server /// /// /// the entire reply string we received /// /// array of expected replies /// /// an object encapsulating the server's reply /// /// public virtual FTPReply ValidateReply(string reply, string[] expectedReplyCodes) { FTPReply replyObj = new FTPReply(reply); return ValidateReply(replyObj, expectedReplyCodes); } /// Validate the response the host has supplied against the /// expected reply. If we get an unexpected reply we throw an /// exception, setting the message to that returned by the /// FTP server /// /// /// reply object /// /// array of expected replies /// /// reply object /// /// public virtual FTPReply ValidateReply(FTPReply reply, string[] expectedReplyCodes) { for (int i = 0; i < expectedReplyCodes.Length; i++) if (ValidateReplyCode(reply, expectedReplyCodes[i])) return reply; // got this far, not recognised throw new FTPException(reply); } /// Validate the response the host has supplied against the /// expected reply. If we get an unexpected reply we throw an /// exception, setting the message to that returned by the /// FTP server /// /// /// reply object /// /// expected reply /// /// reply object /// /// public virtual FTPReply ValidateReply(FTPReply reply, string expectedReplyCode) { if (ValidateReplyCode(reply, expectedReplyCode)) return reply; // got this far, not recognised throw new FTPException(reply); } /// /// Validate reply object /// /// reference to reply object /// /// expect reply code /// /// true if valid, false if invalid /// private bool ValidateReplyCode(FTPReply reply, string expectedReplyCode) { string replyCode = reply.ReplyCode; if (strictReturnCodes) { if (replyCode.Equals(expectedReplyCode)) return true; else return false; } else { // non-strict - match first char if (replyCode[0] == expectedReplyCode[0]) return true; else return false; } } /// /// Log a message, checking for passwords /// /// /// message to log /// /// /// true if a response, false otherwise /// internal virtual void Log(string msg, bool command) { if (msg.StartsWith(PASSWORD_MESSAGE)) msg = PASSWORD_MESSAGE + " ********"; log.Debug(msg); if (command) { if (CommandSent != null) CommandSent(this, new FTPMessageEventArgs(msg)); } else { if (ReplyReceived != null) ReplyReceived(this, new FTPMessageEventArgs(msg)); } } /// /// Helper method to set a socket's timeout value /// /// /// socket to set timeout for /// /// /// timeout value to set /// internal void SetSocketTimeout(BaseSocket sock, int timeout) { if (timeout > 0) { sock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, timeout); sock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, timeout); } } } }