// Telenor Inpli TFTP Server Module // // Copyright 2018 Telenor Inpli AS Norway // Modified 2020 Antmicro namespace libtftp { using System; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Timers; public class TftpServer : IDisposable { /// /// A singleton instance of the TFTP server object /// public static TftpServer Instance { get { if (_instance == null) _instance = new TftpServer(); return _instance; } } private static TftpServer _instance; /// /// An event handler for when a file is received /// public EventHandler FileReceived; /// /// An async event handler for when a file is received /// public event Func FileReceivedAsync; /// /// An event handler for when a file is finished transmitting /// public EventHandler FileTransmitted; /// /// An async event handler for when a file is finished transmitting /// public event Func FileTransmittedAsync; /// /// An event handler for when an error occurs during file receive /// public EventHandler FileReceiveError; /// /// An async event handler for when a file is received /// public event Func FileReceiveErrorAsync; /// /// An event handler for when an error occurs during file transmit /// public EventHandler FileTransmitError; /// /// An async event handler for when an error occurs during file transmit /// public event Func FileTransmitErrorAsync; /// /// An event called to log a message /// public EventHandler Log; /// /// When a read request comes in, this event is a callback to provide the stream to be transferred /// public event Func GetStream; /// /// The maximum period of idle time to retain a session in memory before cleaning it up /// public TimeSpan MaximumIdleSession { get; } = TimeSpan.FromSeconds(5); /// /// The maximum time to wait before retransmitting an unacknowledged packet /// public TimeSpan RetransmissionTimeout { get; } = TimeSpan.FromMilliseconds(1000); /// /// The log level for the server to reach before logging. /// public ETftpLogSeverity LogSeverity { get; set; } = ETftpLogSeverity.Informational; /// /// Response packet is ready. /// public Action DataReady { get; set; } private Timer PeriodicTimer; private ConcurrentDictionary Sessions { get; set; } = new ConcurrentDictionary(); /// /// Constructor /// public TftpServer() { } public void Reset() { Sessions.Clear(); } /// /// Start the server (periodic timer) /// public void Start() { PeriodicTimer = new Timer(500); PeriodicTimer.Elapsed += PeriodicTimer_Elapsed; PeriodicTimer.Start(); } /// /// Handle the incoming UDP packet /// public void OnUdpData(IPEndPoint source, byte[] messageData) { try { if (!Sessions.TryGetValue(source, out TftpSession session)) { LogDebug($"New client detected from {source.Address}:{source.Port}"); session = new TftpSession(this, source); Sessions[source] = session; } Task.Factory.StartNew( async () => { try { await session.OnReceiveAsync(messageData); } catch (Exception e) { LogError("Internal error: " + e.Message); } } ); } catch (Exception e) { LogError("Internal error: " + e.Message); } } private void EmitSessionError(TftpSession session, string reason) { EventHandler handler = (session.Operation == ETftpOperationType.WriteOperation) ? FileReceiveError : FileTransmitError ; var eventArgs = new TftpTransferErrorEventArgs { Id = session.Id, Operation = session.Operation, Filename = session.Filename, RemoteHost = session.RemoteHost, Stream = (session.Operation == ETftpOperationType.WriteOperation) ? (MemoryStream)session.TransferStream : null, TransferInitiated = session.TransferRequestInitiated, TransferCompleted = DateTimeOffset.Now, Transferred = session.Position, FailureReason = reason }; handler?.Invoke( this, eventArgs ); var invocationList = (session.Operation == ETftpOperationType.WriteOperation) ? (FileReceiveErrorAsync?.GetInvocationList()) : (FileTransmitErrorAsync?.GetInvocationList()) ; if (invocationList == null) return; var handlerTasks = new Task[invocationList.Length]; for (int i = 0; i < invocationList.Length; i++) handlerTasks[i] = ((Func)invocationList[i])(this, eventArgs); Task.WhenAll(handlerTasks); } private void ClearIdleSessions() { var now = DateTimeOffset.Now; var idledOutSessions = Sessions.Where(x => now.Subtract(x.Value.IdleSince) > MaximumIdleSession); foreach (var session in idledOutSessions) { UnregisterSession(session.Value, "timeout"); } } private void PeriodicTimer_Elapsed(object sender, ElapsedEventArgs e) { var now = DateTimeOffset.Now; ClearIdleSessions(); var retransmitSessions = Sessions .Where(x => x.Value.Operation == ETftpOperationType.ReadOperation && now.Subtract(x.Value.IdleSince) > RetransmissionTimeout ); var handlerTasks = retransmitSessions.Select(x => Task.Factory.StartNew(() => x.Value.RetransmitAsync())); Task.WhenAll(handlerTasks); } /// /// Called by sessions to request a transfer stream from the host application /// /// Session Id /// The remote host requesting the transfer /// The filename requested by the remote host /// internal async Task GetReadStreamAsync(Guid sessionId, IPEndPoint remoteHost, string filename) { if(GetStream == null) { LogError("No file system available"); return null; } var eventArgs = new TftpGetStreamEventArgs { Id = sessionId, Filename = filename, RemoteHost = remoteHost }; Delegate[] invocationList = GetStream.GetInvocationList(); Task[] handlerTasks = new Task[invocationList.Length]; for (int i = 0; i < invocationList.Length; i++) { handlerTasks[i] = ((Func)invocationList[i])(this, eventArgs); } await Task.WhenAll(handlerTasks); if(eventArgs.Result == null) { LogError("Unknown file"); return null; } return eventArgs.Result; } /// /// Called by a session to remove itself when it's no longer needed /// /// The session to remove internal void UnregisterSession(TftpSession tftpSession) { if (!Sessions.TryRemove(tftpSession.RemoteHost, out TftpSession removedSession)) throw new Exception("Could not remove session " + tftpSession.RemoteHost.ToString() + " from known sessions"); } /// /// Called by a session to remove itself when it's no longer needed /// /// The session to remove internal void UnregisterSession(TftpSession tftpSession, string errorReason) { if (!Sessions.TryRemove(tftpSession.RemoteHost, out TftpSession removedSession)) throw new Exception("Could not remove session " + tftpSession.RemoteHost.ToString() + " from known sessions"); EmitSessionError(tftpSession, errorReason); } /// /// Called by a session to signal that it's complete /// /// The transfer which is complete internal async Task TransferCompleteAsync(TftpSession session) { UnregisterSession(session); EventHandler handler = (session.Operation == ETftpOperationType.WriteOperation) ? FileReceived : FileTransmitted ; var eventArgs = new TftpTransferCompleteEventArgs { Id = session.Id, Operation = session.Operation, Filename = session.Filename, RemoteHost = session.RemoteHost, Stream = (session.Operation == ETftpOperationType.WriteOperation) ? (MemoryStream)session.TransferStream : null, TransferInitiated = session.TransferRequestInitiated, TransferCompleted = DateTimeOffset.Now, Transferred = session.Position }; handler?.Invoke( this, eventArgs ); var invocationList = (session.Operation == ETftpOperationType.WriteOperation) ? (FileReceivedAsync?.GetInvocationList()) : (FileTransmittedAsync?.GetInvocationList()) ; if (invocationList == null) return; var handlerTasks = new Task[invocationList.Length]; for (int i = 0; i < invocationList.Length; i++) { handlerTasks[i] = ((Func)invocationList[i])( this, new TftpTransferCompleteEventArgs { Id = session.Id, Operation = session.Operation, Filename = session.Filename, RemoteHost = session.RemoteHost, Stream = (session.Operation == ETftpOperationType.WriteOperation) ? (MemoryStream)session.TransferStream : null, TransferInitiated = session.TransferRequestInitiated, TransferCompleted = DateTimeOffset.Now, Transferred = session.Position } ); } await Task.WhenAll(handlerTasks); } /// /// Used to send a packet to a host /// /// The endpoint to transfer to /// The data to transmit internal void Transmit(IPEndPoint destination, byte[] buffer) { Transmit(destination, buffer, buffer.Length); } /// /// Transmit a packet to a remote host /// /// The host to transmit to /// The buffer to transmit /// The length of the data to transfer internal void Transmit(IPEndPoint destination, byte[] buffer, int length) { DataReady?.Invoke(destination, buffer, length); } /// /// Implementation of the IDisposable interface /// public void Dispose() { PeriodicTimer.Stop(); } /// /// Syslog an error level message /// /// The message to log internal void LogError(string message) { if (Log != null && LogSeverity >= ETftpLogSeverity.Error) { Log.Invoke( this, new TftpLogEventArgs { Severity = ETftpLogSeverity.Error, TimeStamp = DateTimeOffset.Now, Message = message } ); } } /// /// Syslog an infortmational level message /// /// The message to log internal void LogInfo(string message) { if (Log != null && LogSeverity >= ETftpLogSeverity.Informational) { Log.Invoke( this, new TftpLogEventArgs { Severity = ETftpLogSeverity.Informational, TimeStamp = DateTimeOffset.Now, Message = message } ); } } /// /// Syslog a debug level message /// /// The message to log internal void LogDebug(string message) { if (Log != null && LogSeverity >= ETftpLogSeverity.Debug) { Log.Invoke( this, new TftpLogEventArgs { Severity = ETftpLogSeverity.Debug, TimeStamp = DateTimeOffset.Now, Message = message } ); } } } }