/// /// Copyright (c) 2017 Sensus Metering Systems /// using System; using System.Net; using System.Net.Sockets; using System.Threading; using System.Collections.Generic; using System.Text; using System.IO; using System.Diagnostics; using log4net; namespace TBF.BenchControl.Network.Tftp { /// /// Simple TFTP Server /// public class TftpServer { static readonly ILog log = LogManager.GetLogger("TftpServer"); const ushort DefaultTftpPortNr = 69; const int TftpHeaderLength = 4; const int TftpDataBlockLength = 512; readonly char[] Delimiter = new char[] { '\0' }; /// Delimits filename, transfer mode and error message in UDP packets /// /// TFTP server request modes /// enum RequestMode { BinaryRead, BinaryWrite, AsciiRead, AsciiWrite, } /// /// Opcodes identifying TFTP packets /// enum Opcode : short { RRQ = 01, /// Read request packet. WRQ = 02, /// Write request packet. DATA = 03, /// Data packet. ACK = 04, /// Acknowledgement packet. ERROR = 05, /// Wrror packet. OACK = 06 /// Option acknowledgement packet. } /// Strings included in RRQ/WRQ packets const string REQUEST_MODE_NETASCII = "netascii"; const string REQUEST_MODE_BINARY = "octet"; /// /// Error codes included in ERROR packet /// enum ErrorCode : short { NO_ERROR = 0, /// Not defined, see error message (if any). ERROR_FILE_NOT_FOUND = 1, /// File not found. ERROR_ACCESS_VIOLATION = 2, /// Access violation. ERROR_ALLOC_ERROR = 3, /// Disk full or allocation exceeded. ERROR_ILLEGAL_OP = 4, /// Illegal TFTP operation. ERROR_UNKNOWN_TID = 5, /// Unknown transfer ID. ERROR_FILE_EXISTS = 6, /// File already exists. ERROR_INVALID_USER = 7, /// No such user. } /// /// Single file transfer related class /// class FileTransfer { public IPEndPoint Endpoint { get { return endpoint; } } IPEndPoint endpoint; public string Filename { get { return filename; } } string filename; public RequestMode Mode { get { return mode; } } RequestMode mode; public System.IO.Stream Stream; public BinaryReader StreamReader; public BinaryWriter StreamWriter; public int CurrentBlock; public int LastSentDataLength; public FileTransfer(IPEndPoint endpoint, string filename, RequestMode mode) { this.endpoint = endpoint; this.filename = filename; this.mode = mode; Stream = null; StreamReader = null; StreamWriter = null; CurrentBlock = (mode == RequestMode.BinaryRead ? 1 : 0); LastSentDataLength = 0; } /// /// IPEndPoint can be used as a key as there is max. one file transfer for each endpoint. /// /// IP endpoint /// List of FileTransfer-s /// public static FileTransfer Find(IPEndPoint ep, IList list) { if (list == null) return null; foreach (FileTransfer ft in list) { if ((ft.endpoint.Address.Equals(ep.Address)) && (ft.endpoint.Port.Equals(ep.Port))) { return ft; } } return null; } /// /// FileTransfer Close() closes open streams. /// public void Close() { if (StreamReader != null) StreamReader.Close(); if (StreamWriter != null) StreamWriter.Close(); if (Stream != null) Stream.Close(); } /// For debugging. public override string ToString() { return "[" + endpoint.ToString() + "," + filename + "," + mode.ToString() + ",block=" + CurrentBlock.ToString() + ",last=" + LastSentDataLength.ToString() + "]"; } } /// /// TFTP server instance related instance fields /// string tftpDirectory; /// TFTP root directory int listenPort; /// UDP port to listen to UdpClient udpClient; Thread listenThread; bool done; /// Flag to finish listenThread IList fileTransfers; /// Current file transfers (max. one transfer for each endpoint) public TftpServer() { tftpDirectory = null; udpClient = null; /// UDP client listenThread = null; /// Listen thread done = false; /// Flag to finish listenThread fileTransfers = null; /// No file transfer } #region Event handler public class NotificationEventArgs : EventArgs { public string Message; public IPAddress IP; public string FileName; public NotificationEventArgs(string message, IPAddress ip, string filename) { this.Message = message; this.IP = ip; this.FileName = filename; } } public delegate void NotificationEventHandler(object sender, NotificationEventArgs args); public static event NotificationEventHandler NotificationHandler; /// /// Writes a log and invokes notification handlers (updates TFTP tab-page). /// /// Notification text start /// File transfer object reference or null /// Logger log type void OnNotify(string message, FileTransfer fileTransfer, log4net.Core.Level level) { /// Write a log IPAddress ip = null; string filename = null; string logStr = message; if (fileTransfer != null) { ip = fileTransfer.Endpoint.Address; filename = fileTransfer.Filename; logStr = string.Format("{0} IP={1}, filename={2}", message, ip, filename); } if (level == log4net.Core.Level.Debug) log.Debug(logStr); else if (level == log4net.Core.Level.Info) log.Info(logStr); else if (level == log4net.Core.Level.Warn) log.Warn(logStr); else if (level == log4net.Core.Level.Error) log.Error(logStr); else if (level == log4net.Core.Level.Fatal || level == log4net.Core.Level.All) log.Fatal(logStr); /// Notification: invoke handlers if (NotificationHandler != null) { NotificationHandler(null, new NotificationEventArgs(message, ip, filename)); } } #endregion /// /// Start TFTP server /// /// TFTP root directory /// Network adadpter to listen to, null=listen to all net.adapters /// true = TFTP is running, false = TFTP start failed public bool Start(string tftpDirectory, IPAddress ipAddress) { if (!Directory.Exists(tftpDirectory)) return false; /// TFTP root directory does not exist if (!tftpDirectory.EndsWith(Path.DirectorySeparatorChar.ToString())) { tftpDirectory += Path.DirectorySeparatorChar.ToString(); } this.tftpDirectory = tftpDirectory; this.listenPort = DefaultTftpPortNr; if (udpClient != null) return false; /// TFTP server is already running try { if (ipAddress != null) { IPEndPoint listenEndpoint = new IPEndPoint(ipAddress, listenPort); udpClient = new UdpClient(listenEndpoint); } else { udpClient = new UdpClient(listenPort, AddressFamily.InterNetwork); } } catch { return false; /// Cannot open UDP port (is another TFTP running?) } try { fileTransfers = new List(); listenThread = new Thread(new ThreadStart(Listener)); listenThread.Name = "TftpServer"; /// Is used as a log file root name listenThread.Start(); } catch { return false; } return true; } /// /// Stop TFTP server /// public void Stop() { done = true; if (udpClient != null) { udpClient.Close(); udpClient = null; } try { /// Close the open file streams if (fileTransfers != null) { foreach (FileTransfer ft in fileTransfers) ft.Close(); } } catch (Exception ex) { log.FatalFormat("Fatal error in Stop(): {0}", ex.Message); } fileTransfers = null; } #region Listner thread section /// /// Get readable string describing the packet. /// /// TFTP packet /// Number of valid byted in array data /// description string GetDebugMessage(Byte[] data, int actualLength) { if (data == null || data.Length < actualLength || actualLength < TftpHeaderLength) return "[invalid packet]"; Opcode opcode = (Opcode)((((short)data[0]) * 256) + (short)data[1]); if (opcode == Opcode.RRQ || opcode == Opcode.WRQ) { Encoding ASCII = Encoding.ASCII; string[] strData = ASCII.GetString(data, 2, data.Length - 2).Split(Delimiter, 3); string filename = strData[0]; string mode = strData[1].ToLower(); return "[" + opcode.ToString() + " filename=" + filename + ", mode=" + mode + "]"; } int number = data[2] * 256 + data[3]; if (actualLength > TftpHeaderLength) { return "[" + opcode.ToString() + " + " + number.ToString() + " + " + (actualLength - TftpHeaderLength).ToString() + " bytes]"; } else { return "[" + opcode.ToString() + " + " + number.ToString() + "]"; } } /// /// Main listener thread loop /// public void Listener() { OnNotify("----- TFTP server started -----", null, log4net.Core.Level.Fatal); while (!done) { try { IPEndPoint endpoint = null; Byte[] data = udpClient.Receive(ref endpoint); log.InfoFormat("Rcvd packet {0}", GetDebugMessage(data, data.Length)); /// /// Process the packet /// Opcode opcode = (Opcode)((((short)data[0]) * 256) + (short)data[1]); /// /// Find any existing file transfer with this IP endpoint /// FileTransfer fileTransfer = FileTransfer.Find(endpoint, fileTransfers); if (opcode == Opcode.RRQ || opcode == Opcode.WRQ) { if (fileTransfer != null) { /// /// If there is an incomplete file transfer with this endpoint, close the open streams. /// OnNotify("Forcing an incomplete file transfer to close: ", fileTransfer, log4net.Core.Level.Error); fileTransfer.Close(); fileTransfers.Remove(fileTransfer); fileTransfer = null; } } else if (fileTransfer == null) { /// If there is no existing file transfer with this endpoint, ignore packets other then RRQ or WRQ. log.ErrorFormat("No matching file transfer - packet ignored: IP={0}", endpoint.Address); continue; } switch (opcode) { case Opcode.RRQ: ProcessReadRequest(data, endpoint); break; case Opcode.WRQ: ProcessWriteRequest(data, endpoint); break; case Opcode.ERROR: ProcessError(data, fileTransfer); break; case Opcode.ACK: ProcessAck(data, fileTransfer); break; case Opcode.DATA: ProcessData(data, fileTransfer); break; case Opcode.OACK: default: break; } } catch (Exception exc) { log.FatalFormat("Fatal error in Listener(): {0}", exc.Message); } } return; } #region TFTP 'GET' section /// /// Handle 'Read Request' (RRQ) /// /// Data from RRQ packet /// Client IP private void ProcessReadRequest(Byte[] data, IPEndPoint endpoint) { /// Extract a filename and a mode string[] strData = Encoding.ASCII.GetString(data, 2, data.Length - 2).Split(Delimiter, 3); string filename = strData[0]; string mode = strData[1].ToLower(); if (mode == REQUEST_MODE_BINARY) { /// Create FileTransfer object, do not add it to this.fileTransfers yet FileTransfer fileTransfer = new FileTransfer(endpoint, filename, RequestMode.BinaryRead); /// Try to open the source file try { fileTransfer.Stream = System.IO.File.OpenRead(tftpDirectory + filename); fileTransfer.StreamReader = new BinaryReader(fileTransfer.Stream); } catch { OnNotify("Cannot open file to be uploaded: ", fileTransfer, log4net.Core.Level.Warn); SendError(fileTransfer, ErrorCode.ERROR_FILE_NOT_FOUND); return; } /// File open OK, start the transfer, send the 1st data packet OnNotify("Uploading file: ", fileTransfer, log4net.Core.Level.Info); fileTransfers.Add(fileTransfer); SendData(fileTransfer); } else { throw (new Exception("Built-in TFTP server does not support '" + mode + "' file transfer mode.")); } } /// /// Send part of file data /// /// location to send stream to /// 512 byte block to send private void SendData(FileTransfer fileTransfer) { int fileOffset = (fileTransfer.CurrentBlock - 1) * TftpDataBlockLength; fileTransfer.Stream.Seek(fileOffset, SeekOrigin.Begin); /// Prepare data buffer Byte[] buffer = new Byte[TftpDataBlockLength + TftpHeaderLength]; buffer[0] = 0; buffer[1] = (byte)Opcode.DATA; buffer[2] = (byte)((fileTransfer.CurrentBlock & 0x0000FF00) >> 8); buffer[3] = (byte) (fileTransfer.CurrentBlock & 0x000000FF); fileTransfer.LastSentDataLength = fileTransfer.StreamReader.Read(buffer, TftpHeaderLength, TftpDataBlockLength); /// Send the data packet int ecode = udpClient.Send(buffer, fileTransfer.LastSentDataLength + TftpHeaderLength, fileTransfer.Endpoint); log.InfoFormat("Sent packet {0} ecode={1}", GetDebugMessage(buffer, fileTransfer.LastSentDataLength + TftpHeaderLength), ecode); /// Check the return value if (ecode != fileTransfer.LastSentDataLength + TftpHeaderLength) { OnNotify("Error when sending data: ecode = " + ecode.ToString() + ", ", fileTransfer, log4net.Core.Level.Error); fileTransfer.Close(); fileTransfers.Remove(fileTransfer); } } /// /// Handle ACK response and send next block. /// /// data from packet /// client private void ProcessAck(Byte[] data, FileTransfer fileTransfer) { int protocolBlocknum = 256 * (int)data[2] + (int)data[3]; /// Check the received block number, support for file size 2GB if (protocolBlocknum == (fileTransfer.CurrentBlock & 0xFFFF)) { /// OK => Check the last data block length if (fileTransfer.LastSentDataLength < TftpDataBlockLength) { /// Nothing more to send, finish the transfer OnNotify("Upload completed: ", fileTransfer, log4net.Core.Level.Info); fileTransfer.Close(); fileTransfers.Remove(fileTransfer); } else { /// Send the next data block fileTransfer.CurrentBlock++; SendData(fileTransfer); } } else { /// Block number NOK => re-send the last block (without incrementing fileTransfer.CurrentBlock). /// This often happens when transferring large files due to iCOM performance limits. log.ErrorFormat("Received ACK does not match the sent block#: IP={0} ---> Re-sending", fileTransfer.Endpoint.Address); SendData(fileTransfer); } } #endregion #region TFTP 'PUT' section /// /// Handle 'Write Request' (WRQ) /// /// Data from WRQ packet /// Client IP private void ProcessWriteRequest(Byte[] data, IPEndPoint endpoint) { /// Extract a filename and a mode string[] strData = Encoding.ASCII.GetString(data, 2, data.Length - 2).Split(Delimiter, 3); string filename = strData[0]; string mode = strData[1].ToLower(); if (mode == REQUEST_MODE_BINARY) { /// Create FileTransfer object, do not add it to this.fileTransfers yet FileTransfer fileTransfer = new FileTransfer(endpoint, filename, RequestMode.BinaryWrite); /// Try to open the source file try { fileTransfer.Stream = System.IO.File.Create(tftpDirectory + filename); fileTransfer.StreamWriter = new BinaryWriter(fileTransfer.Stream); } catch { OnNotify("Cannot open file to be downloaded for writing: ", fileTransfer, log4net.Core.Level.Warn); SendError(fileTransfer, ErrorCode.ERROR_FILE_EXISTS); return; } /// File open OK, start the transfer, send the 0th acknowledge packet OnNotify("Downloading file: ", fileTransfer, log4net.Core.Level.Info); fileTransfers.Add(fileTransfer); SendAck(fileTransfer); } else { throw (new Exception("Built-in TFTP server does not support '" + mode + "' file transfer mode.")); } } /// /// Send acknowledge packet (ACK) /// /// Current FileTransfer object private void SendAck(FileTransfer fileTransfer) { const int AcknowledgePacketLength = 4; /// Prepare the acknowledgment packet Byte[] buffer = new Byte[AcknowledgePacketLength]; buffer[0] = 0; buffer[1] = (byte)Opcode.ACK; buffer[2] = (byte)((fileTransfer.CurrentBlock & 0x0000FF00) >> 8); buffer[3] = (byte) (fileTransfer.CurrentBlock & 0x000000FF); /// Send the packet int ecode = udpClient.Send(buffer, AcknowledgePacketLength, fileTransfer.Endpoint); log.InfoFormat("Sent packet {0} ecode={1}", GetDebugMessage(buffer, AcknowledgePacketLength), ecode); /// Check the return value if (ecode != AcknowledgePacketLength) { OnNotify("Error when sending ACK packet: ecode = " + ecode.ToString() + ", ", fileTransfer, log4net.Core.Level.Error); fileTransfer.Close(); fileTransfers.Remove(fileTransfer); } } private void ProcessData(Byte[] data, FileTransfer fileTransfer) { int protocolBlocknum = 256 * (int)data[2] + (int)data[3]; /// Check the received data block number, support max. file size 2GB if (protocolBlocknum != ((fileTransfer.CurrentBlock + 1) & 0xFFFF)) { OnNotify("Received data block # is not consecutive --> aborting transfer: ", fileTransfer, log4net.Core.Level.Error); SendError(fileTransfer, ErrorCode.ERROR_UNKNOWN_TID); fileTransfer.Close(); fileTransfers.Remove(fileTransfer); return; } /// Update the current block number, save and acknowledge data fileTransfer.CurrentBlock++; for (int i = TftpHeaderLength; i < data.Length; i++) fileTransfer.StreamWriter.Write(data[i]); SendAck(fileTransfer); /// Close the file transfer if the block was the last one (data size < 512) if (data.Length < TftpDataBlockLength + TftpHeaderLength) { OnNotify("Download completed: ", fileTransfer, log4net.Core.Level.Info); fileTransfer.Close(); fileTransfers.Remove(fileTransfer); } } #endregion /// /// Send part of a stream /// /// location to send stream to /// 512 byte block to send private void SendError(FileTransfer fileTransfer, ErrorCode errorCode) { OnNotify("Sending Error Packet: errorCode = " + errorCode.ToString() + ", ", fileTransfer, log4net.Core.Level.Error); const int ErrorPacketLength = 10; Byte[] buffer = new Byte[ErrorPacketLength]; buffer[0] = 0; buffer[1] = (byte)Opcode.ACK; buffer[2] = (byte)((fileTransfer.CurrentBlock & 0xFF00) / 256); buffer[3] = (byte)(fileTransfer.CurrentBlock & 0x00FF); buffer[4] = (Byte)'E'; buffer[5] = (Byte)'r'; buffer[6] = (Byte)'r'; buffer[7] = (Byte)'o'; buffer[8] = (Byte)'r'; buffer[9] = 0; int ecode = udpClient.Send(buffer, ErrorPacketLength, fileTransfer.Endpoint); log.InfoFormat("Sent packet {0} ecode={1}", GetDebugMessage(buffer, ErrorPacketLength), ecode); if (ecode <= 0) { Debug.WriteLine("Error in send : {0}", ecode); } } /// /// Parse an error response /// /// data from packet /// client private void ProcessError(Byte[] data, FileTransfer fileTransfer) { int errorCode = 256 * (int)data[2] + (int)data[3]; string[] strData = Encoding.ASCII.GetString(data, 2, data.Length - 2).Split(Delimiter, 3); string message = strData[0]; OnNotify("Received Error Packet: errorCode=" + errorCode.ToString() + ", message=" + message + ", ", fileTransfer, log4net.Core.Level.Info); } #endregion } }