diff --git a/PUP/BSP/BSPChannel.cs b/PUP/BSP/BSPChannel.cs index a817a78..175129c 100644 --- a/PUP/BSP/BSPChannel.cs +++ b/PUP/BSP/BSPChannel.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; namespace IFS.BSP { + public delegate void BSPChannelDestroyDelegate(BSPChannel channel); + /// /// Provides functionality for maintaining/terminating BSP connections, and the transfer of data /// across said connection. @@ -19,7 +21,7 @@ namespace IFS.BSP /// public class BSPChannel { - public BSPChannel(PUP rfcPup, UInt32 socketID, BSPProtocol protocolHandler) + public BSPChannel(PUP rfcPup, UInt32 socketID) { _inputLock = new ReaderWriterLockSlim(); _outputLock = new ReaderWriterLockSlim(); @@ -35,7 +37,7 @@ namespace IFS.BSP _outputWindow = new List(16); _outputWindowLock = new ReaderWriterLockSlim(); - _protocolHandler = protocolHandler; + _destroyed = false; // Init IDs, etc. based on RFC PUP _lastClientRecvPos = _startPos = _recvPos = _sendPos = rfcPup.ID; @@ -63,11 +65,9 @@ namespace IFS.BSP // Create our consumer thread for output and kick it off. _consumerThread = new Thread(OutputConsumerThread); _consumerThread.Start(); - } + } - public delegate void DestroyDelegate(); - - public DestroyDelegate OnDestroy; + public BSPChannelDestroyDelegate OnDestroy; /// /// The port we use to talk to the client. @@ -98,13 +98,10 @@ namespace IFS.BSP /// the channel has been destroyed. /// public void Destroy() - { - _consumerThread.Abort(); - - if (OnDestroy != null) - { - OnDestroy(); - } + { + _destroyed = true; + _consumerThread.Abort(); + OnDestroy(this); } /// @@ -146,7 +143,7 @@ namespace IFS.BSP throw new InvalidOperationException("count + offset must be less than or equal to the length of the buffer being read into."); } - if (count == 0) + if (count == 0 || _destroyed) { // Honor requests to read 0 bytes always, since technically 0 bytes are always available. data = new byte[0]; @@ -210,7 +207,9 @@ namespace IFS.BSP Log.Write(LogType.Error, LogComponent.BSP, "Timed out waiting for data on read, aborting connection."); // We timed out waiting for data, abort the connection. SendAbort("Timeout on read."); - BSPManager.DestroyChannel(this); + Destroy(); + read = 0; + break; } } } @@ -301,7 +300,7 @@ namespace IFS.BSP Log.Write(LogType.Error, LogComponent.BSP, "Mark PUP must be 1 byte in length."); SendAbort("Mark PUP must be 1 byte in length."); - BSPManager.DestroyChannel(this); + Destroy(); return; } } @@ -396,6 +395,11 @@ namespace IFS.BSP throw new InvalidOperationException("Length must be less than or equal to the size of data."); } + if (_destroyed) + { + return; + } + // Add output data to output queue. // Again, this is really inefficient for (int i = 0; i < length; i++) @@ -429,6 +433,11 @@ namespace IFS.BSP /// public void SendAbort(string message) { + if (_destroyed) + { + return; + } + PUP abortPup = new PUP(PupType.Abort, _startPos, _clientConnectionPort, _serverConnectionPort, Helpers.StringToArray(message)); // @@ -445,6 +454,11 @@ namespace IFS.BSP /// public void SendMark(byte markType, bool ack) { + if (_destroyed) + { + return; + } + PUP markPup = new PUP(ack ? PupType.AMark : PupType.Mark, _sendPos, _clientConnectionPort, _serverConnectionPort, new byte[] { markType }); // Send it. @@ -689,7 +703,7 @@ namespace IFS.BSP // Something bad has happened and we don't have that PUP anymore... Log.Write(LogType.Error, LogComponent.BSP, "Client lost more than a window of data, BSP connection is broken. Aborting."); SendAbort("Fatal BSP synchronization error."); - BSPManager.DestroyChannel(this); + Destroy(); _outputWindowLock.ExitUpgradeableReadLock(); return; } @@ -769,11 +783,9 @@ namespace IFS.BSP { Log.Write(LogType.Error, LogComponent.BSP, "Timeout waiting for ACK, aborting connection."); SendAbort("Client unresponsive."); - BSPManager.DestroyChannel(this); + Destroy(); } - } - - private BSPProtocol _protocolHandler; + } // The byte positions for the input and output streams private UInt32 _recvPos; @@ -786,6 +798,8 @@ namespace IFS.BSP private BSPAck _clientLimits; // The stats from the last ACK we got from the client. private uint _lastClientRecvPos; // The client's receive position, as indicated by the last ACK pup received. + private bool _destroyed; // Set when the channel has been closed. + private ReaderWriterLockSlim _inputLock; private AutoResetEvent _inputWriteEvent; diff --git a/PUP/BSP/BSPManager.cs b/PUP/BSP/BSPManager.cs index 21d7de4..b2f0243 100644 --- a/PUP/BSP/BSPManager.cs +++ b/PUP/BSP/BSPManager.cs @@ -15,18 +15,34 @@ namespace IFS.BSP public ushort MaxBytes; public ushort MaxPups; public ushort BytesSent; - } + } public abstract class BSPProtocol : PUPProtocolBase { - public abstract void InitializeServerForChannel(BSPChannel channel); + public abstract void InitializeServerForChannel(BSPChannel channel); } public enum BSPState { Unconnected, Connected - } + } + + public delegate void WorkerExitDelegate(BSPWorkerBase destroyed); + + public abstract class BSPWorkerBase + { + public BSPWorkerBase(BSPChannel channel) + { + _channel = channel; + } + + public abstract void Terminate(); + + public WorkerExitDelegate OnExit; + + protected BSPChannel _channel; + } /// /// Manages active BSP channels and creates new ones as necessary, invoking the associated @@ -47,14 +63,16 @@ namespace IFS.BSP _nextSocketID = _startingSocketID; _activeChannels = new Dictionary(); + + _workers = new List(Configuration.MaxWorkers); } /// - /// Called when a PUP comes in on a known socket and establishes a new BSP channel. - /// The associated protocol handler (server) is woken up to service the channel. + /// Called when a PUP comes in on a known socket. Establishes a new BSP channel. + /// A worker of the appropriate type is woken up to service the channel. /// /// - public static void EstablishRendezvous(PUP p, BSPProtocol protocolHandler) + public static void EstablishRendezvous(PUP p, Type workerType) { if (p.Type != PupType.RFC) { @@ -63,12 +81,13 @@ namespace IFS.BSP } UInt32 socketID = GetNextSocketID(); - BSPChannel newChannel = new BSPChannel(p, socketID, protocolHandler); - _activeChannels.Add(socketID, newChannel); + BSPChannel newChannel = new BSPChannel(p, socketID); + newChannel.OnDestroy += OnChannelDestroyed; + _activeChannels.Add(socketID, newChannel); // - // Initialize the server for this protocol. - protocolHandler.InitializeServerForChannel(newChannel); + // Initialize the worker for this channel. + InitializeWorkerForChannel(newChannel, workerType); // Send RFC response to complete the rendezvous. @@ -172,8 +191,14 @@ namespace IFS.BSP /// public static void DestroyChannel(BSPChannel channel) { + // Tell the channel to shut down. It will in turn + // notify us when that is complete and we will remove + // our references to it. (See OnChannelDestroyed.) channel.Destroy(); + } + public static void OnChannelDestroyed(BSPChannel channel) + { _activeChannels.Remove(channel.ServerPort.Socket); } @@ -218,6 +243,40 @@ namespace IFS.BSP return next; } + private static void InitializeWorkerForChannel(BSPChannel channel, Type workerType) + { + if (_workers.Count < Configuration.MaxWorkers) + { + // Spawn new worker, which starts it running. + // It must be a subclass of BSPWorkerBase or this will throw. + BSPWorkerBase worker = (BSPWorkerBase)Activator.CreateInstance(workerType, new object[] { channel }); + + worker.OnExit += OnWorkerExit; + _workers.Add(worker); + } + else + { + // TODO: send back "server full" repsonse of some sort. + } + } + + public static int WorkerCount + { + get + { + return _workers.Count(); + } + } + + private static void OnWorkerExit(BSPWorkerBase destroyed) + { + if (_workers.Contains(destroyed)) + { + _workers.Remove(destroyed); + } + } + + private static List _workers; /// /// Map from socket address to BSP channel diff --git a/PUP/Conf/ifs.cfg b/PUP/Conf/ifs.cfg new file mode 100644 index 0000000..f946d25 --- /dev/null +++ b/PUP/Conf/ifs.cfg @@ -0,0 +1,17 @@ +# ifs.cfg: +# +# This file contains configuration parameters for the IFS server. +# All numbers are in decimal. +# + +# Debug settings + +LogTypes = Normal +LogComponents = All + +# Normal configuration +FTPRoot = c:\ifs\ftp +CopyDiskRoot = c:\ifs\copydisk +BootRoot = c:\ifs\boot +InterfaceType = RAW +InterfaceName = Ethernet 2 diff --git a/PUP/Configuration.cs b/PUP/Configuration.cs index 7b8dd7a..f0b940f 100644 --- a/PUP/Configuration.cs +++ b/PUP/Configuration.cs @@ -1,33 +1,203 @@ -using System; +using IFS.Logging; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; namespace IFS { + + public class InvalidConfigurationException : Exception + { + public InvalidConfigurationException(string message) : base(message) + { + + } + } + + /// /// Encapsulates global server configuration information. - /// - /// TODO: read in configuration from a text file. - /// TODO also: make cross-platform compatible (no hardcoding of path delimiters). /// public static class Configuration { + static Configuration() + { + ReadConfiguration(); + + // + // Ensure that required values were read from the config file. If not, + // throw so that startup is aborted. + // + if (string.IsNullOrWhiteSpace(FTPRoot) || !Directory.Exists(FTPRoot)) + { + throw new InvalidConfigurationException("FTP root path is invalid."); + } + + if (string.IsNullOrWhiteSpace(CopyDiskRoot) || !Directory.Exists(CopyDiskRoot)) + { + throw new InvalidConfigurationException("CopyDisk root path is invalid."); + } + + if (string.IsNullOrWhiteSpace(BootRoot) || !Directory.Exists(BootRoot)) + { + throw new InvalidConfigurationException("Boot root path is invalid."); + } + + if (MaxWorkers < 1) + { + throw new InvalidConfigurationException("MaxWorkers must be >= 1."); + } + } + + /// + /// The type of interface (UDP or RAW) to communicate over + /// + public static readonly string InterfaceType; + + /// + /// The name of the network interface to use + /// + public static readonly string InterfaceName; + /// /// The root directory for the FTP file store. /// - public static readonly string FTPRoot = "C:\\ifs\\ftp"; + public static readonly string FTPRoot; /// /// The root directory for the CopyDisk file store. /// - public static readonly string CopyDiskRoot = "C:\\ifs\\copydisk"; + public static readonly string CopyDiskRoot; /// /// The root directory for the Boot file store. /// - public static readonly string BootRoot = "C:\\ifs\\boot"; + public static readonly string BootRoot; + /// + /// The maximum number of worker threads for protocol handling. + /// + public static readonly int MaxWorkers = 256; + + /// + /// The components to display logging messages for. + /// + public static readonly LogComponent LogComponents; + + /// + /// The type (Verbosity) of messages to log. + /// + public static readonly LogType LogTypes; + + + private static void ReadConfiguration() + { + using (StreamReader configStream = new StreamReader(Path.Combine("Conf", "ifs.cfg"))) + { + // + // Config file consists of text lines containing name / value pairs: + // = + // Whitespace is ignored. + // + int lineNumber = 0; + while (!configStream.EndOfStream) + { + lineNumber++; + string line = configStream.ReadLine().Trim(); + + if (string.IsNullOrEmpty(line)) + { + // Empty line, ignore. + continue; + } + + if (line.StartsWith("#")) + { + // Comment to EOL, ignore. + continue; + } + + // Find the '=' separating tokens and ensure there are just two. + string[] tokens = line.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + + if (tokens.Length < 2) + { + Log.Write(LogType.Warning, LogComponent.Configuration, + "ifs.cfg line {0}: Invalid syntax.", lineNumber); + continue; + } + + string parameter = tokens[0].Trim(); + string value = tokens[1].Trim(); + + // Reflect over the public, static properties in this class and see if the parameter matches one of them + // If not, it's an error, if it is then we attempt to coerce the value to the correct type. + System.Reflection.FieldInfo[] info = typeof(Configuration).GetFields(BindingFlags.Public | BindingFlags.Static); + + bool bMatch = false; + foreach (FieldInfo field in info) + { + // Case-insensitive compare. + if (field.Name.ToLowerInvariant() == parameter.ToLowerInvariant()) + { + bMatch = true; + + // + // Switch on the type of the field and attempt to convert the value to the appropriate type. + // At this time we support only strings and integers. + // + try + { + switch (field.FieldType.Name) + { + case "Int32": + { + int v = int.Parse(value); + field.SetValue(null, v); + } + break; + + case "String": + { + field.SetValue(null, value); + } + break; + + case "LogType": + { + field.SetValue(null, Enum.Parse(typeof(LogType), value, true)); + } + break; + + case "LogComponent": + { + field.SetValue(null, Enum.Parse(typeof(LogComponent), value, true)); + } + break; + + } + } + catch + { + Log.Write(LogType.Warning, LogComponent.Configuration, + "ifs.cfg line {0}: Value '{1}' is invalid for parameter '{2}'.", lineNumber, value, parameter); + } + } + + } + + + if (!bMatch) + { + Log.Write(LogType.Warning, LogComponent.Configuration, + "ifs.cfg line {0}: Unknown configuration parameter '{1}'.", lineNumber, parameter); + } + } + } + } } } diff --git a/PUP/CopyDisk/CopyDiskServer.cs b/PUP/CopyDisk/CopyDiskServer.cs index e68db15..11a972c 100644 --- a/PUP/CopyDisk/CopyDiskServer.cs +++ b/PUP/CopyDisk/CopyDiskServer.cs @@ -191,84 +191,60 @@ namespace IFS.CopyDisk public ushort Length; public ushort Command; - } + } - public class CopyDiskServer : BSPProtocol + public class CopyDiskWorker : BSPWorkerBase { - /// - /// Called by dispatcher to send incoming data destined for this protocol. - /// - /// - public override void RecvData(PUP p) - { - throw new NotImplementedException(); - } - - public override void InitializeServerForChannel(BSPChannel channel) - { - // Spawn new worker - // TODO: keep track of workers to allow clean shutdown, management, etc. - CopyDiskWorker worker = new CopyDiskWorker(channel); - } - } - - public class CopyDiskWorker - { - public CopyDiskWorker(BSPChannel channel) + public CopyDiskWorker(BSPChannel channel) : base(channel) { // Register for channel events channel.OnDestroy += OnChannelDestroyed; _running = true; - _workerThread = new Thread(new ParameterizedThreadStart(CopyDiskWorkerThreadInit)); - _workerThread.Start(channel); + _workerThread = new Thread(new ThreadStart(CopyDiskWorkerThreadInit)); + _workerThread.Start(); } - private void OnChannelDestroyed() + public override void Terminate() { - // Tell the thread to exit and give it a short period to do so... - _running = false; - - Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Asking CopyDisk worker thread to exit..."); - _workerThread.Join(1000); - - if (_workerThread.IsAlive) - { - Logging.Log.Write(LogType.Verbose, LogComponent.CopyDisk, "CopyDisk worker thread did not exit, terminating."); - _workerThread.Abort(); - } + ShutdownWorker(); } - private void CopyDiskWorkerThreadInit(object obj) + private void OnChannelDestroyed(BSPChannel channel) { - BSPChannel channel = (BSPChannel)obj; + ShutdownWorker(); + } + private void CopyDiskWorkerThreadInit() + { // // Run the worker thread. // If anything goes wrong, log the exception and tear down the BSP connection. // try { - CopyDiskWorkerThread(channel); + CopyDiskWorkerThread(); } catch(Exception e) { if (!(e is ThreadAbortException)) { Logging.Log.Write(LogType.Error, LogComponent.CopyDisk, "CopyDisk worker thread terminated with exception '{0}'.", e.Message); - channel.SendAbort("Server encountered an error."); + _channel.SendAbort("Server encountered an error."); + + OnExit(this); } } } - private void CopyDiskWorkerThread(BSPChannel channel) + private void CopyDiskWorkerThread() { // TODO: enforce state (i.e. reject out-of-order block types.) while (_running) { // Retrieve length of this block (in bytes): - int length = channel.ReadUShort() * 2; + int length = _channel.ReadUShort() * 2; // Sanity check that length is a reasonable value. if (length > 2048) @@ -278,11 +254,11 @@ namespace IFS.CopyDisk } // Retrieve type - CopyDiskBlock blockType = (CopyDiskBlock)channel.ReadUShort(); + CopyDiskBlock blockType = (CopyDiskBlock)_channel.ReadUShort(); // Read rest of block starting at offset 4 (so deserialization works) byte[] data = new byte[length]; - channel.Read(ref data, data.Length - 4, 4); + _channel.Read(ref data, data.Length - 4, 4); Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Copydisk block type is {0}", blockType); @@ -296,7 +272,7 @@ namespace IFS.CopyDisk // Send the response: VersionYesNoBlock vbOut = new VersionYesNoBlock(CopyDiskBlock.Version, vbIn.Code, "LCM IFS CopyDisk of 26-Jan-2016"); - channel.Send(Serializer.Serialize(vbOut)); + _channel.Send(Serializer.Serialize(vbOut)); } break; @@ -318,7 +294,7 @@ namespace IFS.CopyDisk // Send a "Yes" response back. // VersionYesNoBlock yes = new VersionYesNoBlock(CopyDiskBlock.Yes, 0, "Come on in, the water's fine."); - channel.Send(Serializer.Serialize(yes)); + _channel.Send(Serializer.Serialize(yes)); } break; @@ -342,7 +318,7 @@ namespace IFS.CopyDisk { // Invalid name, return No reponse. VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnitNotReady, "Invalid unit name."); - channel.Send(Serializer.Serialize(no)); + _channel.Send(Serializer.Serialize(no)); } else { @@ -361,14 +337,14 @@ namespace IFS.CopyDisk // Send a "HereAreDiskParams" response indicating success. // HereAreDiskParamsBFSBlock diskParams = new HereAreDiskParamsBFSBlock(_pack.Geometry); - channel.Send(Serializer.Serialize(diskParams)); + _channel.Send(Serializer.Serialize(diskParams)); } catch { // If we fail for any reason, return a "No" response. // TODO: can we be more helpful here? VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnitNotReady, "Image could not be opened."); - channel.Send(Serializer.Serialize(no)); + _channel.Send(Serializer.Serialize(no)); } } } @@ -391,7 +367,7 @@ namespace IFS.CopyDisk { // Invalid name, return No reponse. VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnitNotReady, "Invalid unit name or image already exists."); - channel.Send(Serializer.Serialize(no)); + _channel.Send(Serializer.Serialize(no)); } else { @@ -406,7 +382,7 @@ namespace IFS.CopyDisk // Send a "HereAreDiskParams" response indicating success. // HereAreDiskParamsBFSBlock diskParams = new HereAreDiskParamsBFSBlock(_pack.Geometry); - channel.Send(Serializer.Serialize(diskParams)); + _channel.Send(Serializer.Serialize(diskParams)); } } break; @@ -440,13 +416,13 @@ namespace IFS.CopyDisk _endAddress > _pack.MaxAddress) { VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnknownCommand, "Transfer parameters are invalid."); - channel.Send(Serializer.Serialize(no)); + _channel.Send(Serializer.Serialize(no)); } else { // We're OK. Save the parameters and send a Yes response. VersionYesNoBlock yes = new VersionYesNoBlock(CopyDiskBlock.Yes, 0, "You are cleared for launch."); - channel.Send(Serializer.Serialize(yes)); + _channel.Send(Serializer.Serialize(yes)); // // And send the requested range of pages if this is a Retrieve operation @@ -458,7 +434,7 @@ namespace IFS.CopyDisk { DiabloDiskSector sector = _pack.GetSector(i); HereIsDiskPageBlock block = new HereIsDiskPageBlock(sector.Header, sector.Label, sector.Data); - channel.Send(Serializer.Serialize(block), false /* do not flush */); + _channel.Send(Serializer.Serialize(block), false /* do not flush */); if ((i % 100) == 0) { @@ -468,7 +444,7 @@ namespace IFS.CopyDisk // Send "EndOfTransfer" block to finish the transfer. EndOfTransferBlock endTransfer = new EndOfTransferBlock(0); - channel.Send(Serializer.Serialize(endTransfer)); + _channel.Send(Serializer.Serialize(endTransfer)); Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Send done."); } @@ -484,7 +460,7 @@ namespace IFS.CopyDisk { if (_currentAddress > _endAddress) { - channel.SendAbort("Invalid address for page."); + _channel.SendAbort("Invalid address for page."); _running = false; break; } @@ -552,7 +528,7 @@ namespace IFS.CopyDisk Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Sending error summary..."); // No data in block. Send list of errors we encountered. (There should always be none since we're perfect and have no disk errors.) HereAreErrorsBFSBlock errorBlock = new HereAreErrorsBFSBlock(0, 0); - channel.Send(Serializer.Serialize(errorBlock)); + _channel.Send(Serializer.Serialize(errorBlock)); } break; @@ -560,9 +536,34 @@ namespace IFS.CopyDisk Log.Write(LogType.Warning, LogComponent.CopyDisk, "Unhandled CopyDisk block {0}", blockType); break; } - } + } + + if (OnExit != null) + { + OnExit(this); + } } + private void ShutdownWorker() + { + // Tell the thread to exit and give it a short period to do so... + _running = false; + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Asking CopyDisk worker thread to exit..."); + _workerThread.Join(1000); + + if (_workerThread.IsAlive) + { + Logging.Log.Write(LogType.Verbose, LogComponent.CopyDisk, "CopyDisk worker thread did not exit, terminating."); + _workerThread.Abort(); + + if (OnExit != null) + { + OnExit(this); + } + } + } + /// /// Builds a relative path to the directory that holds the disk images. /// diff --git a/PUP/Entrypoint.cs b/PUP/Entrypoint.cs index 0185912..20bc8b9 100644 --- a/PUP/Entrypoint.cs +++ b/PUP/Entrypoint.cs @@ -2,6 +2,8 @@ using IFS.CopyDisk; using IFS.FTP; using IFS.Transport; +using PcapDotNet.Core; +using PcapDotNet.Core.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -14,40 +16,94 @@ namespace IFS public class Entrypoint { static void Main(string[] args) - { - - List ifaces = EthernetInterface.EnumerateDevices(); + { + PrintHerald(); - Console.WriteLine("available interfaces are:"); - foreach(EthernetInterface i in ifaces) - { - Console.WriteLine(String.Format("{0} - address {1}, desc {2} ", i.Name, i.MacAddress, i.Description)); - } + RegisterProtocols(); - NetworkInterface[] netfaces = NetworkInterface.GetAllNetworkInterfaces(); + RegisterInterface(); + // This runs forever, or until the user tells us to exit. + RunCommandPrompt(); + } + + private static void PrintHerald() + { + Console.WriteLine("LCM IFS v0.1, 4/19/2016."); + Console.WriteLine(); + } + + private static void RegisterProtocols() + { // Set up protocols: // Connectionless PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Gateway Information", 2, ConnectionType.Connectionless, new GatewayInformationProtocol())); PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Misc Services", 0x4, ConnectionType.Connectionless, new MiscServicesProtocol())); - PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Echo", 0x5, ConnectionType.Connectionless, new EchoProtocol())); + PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Echo", 0x5, ConnectionType.Connectionless, new EchoProtocol())); // RTP/BSP based: - PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("CopyDisk", 0x15 /* 25B */, ConnectionType.BSP, new CopyDiskServer())); - PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("FTP", 0x3, ConnectionType.BSP, new FTPServer())); + PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("CopyDisk", 0x15 /* 25B */, ConnectionType.BSP, typeof(CopyDiskWorker))); + PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("FTP", 0x3, ConnectionType.BSP, typeof(FTPWorker))); // Breath Of Life - BreathOfLife breathOfLifeServer = new BreathOfLife(); + _breathOfLifeServer = new BreathOfLife(); + } + private static void RegisterInterface() + { + bool bFound = false; - // TODO: MAKE THIS CONFIGURABLE. - PUPProtocolDispatcher.Instance.RegisterInterface(netfaces[0].Description); - - while (true) + switch (Configuration.InterfaceType.ToLowerInvariant()) { - System.Threading.Thread.Sleep(100); + case "udp": + // Find matching network interface + { + NetworkInterface[] ifaces = NetworkInterface.GetAllNetworkInterfaces(); + foreach(NetworkInterface iface in ifaces) + { + if (iface.Name.ToLowerInvariant() == Configuration.InterfaceName.ToLowerInvariant()) + { + PUPProtocolDispatcher.Instance.RegisterUDPInterface(iface); + bFound = true; + break; + } + } + } + break; + + case "raw": + // Find matching RAW interface + { + foreach (LivePacketDevice device in LivePacketDevice.AllLocalMachine) + { + if (device.GetNetworkInterface().Name.ToLowerInvariant() == Configuration.InterfaceName.ToLowerInvariant()) + { + PUPProtocolDispatcher.Instance.RegisterRAWInterface(device); + bFound = true; + break; + } + } + } + break; + } + + // Not found. + if (!bFound) + { + throw new InvalidConfigurationException("The specified network interface is invalid."); } } + + private static void RunCommandPrompt() + { + while (true) + { + Console.Write(">>>"); + string command = Console.ReadLine(); + } + } + + private static BreathOfLife _breathOfLifeServer; } } diff --git a/PUP/FTP/FTPServer.cs b/PUP/FTP/FTPServer.cs index 2815f2d..e5f0f26 100644 --- a/PUP/FTP/FTPServer.cs +++ b/PUP/FTP/FTPServer.cs @@ -77,57 +77,33 @@ namespace IFS.FTP public byte Code; public string Message; - } + } - public class FTPServer : BSPProtocol + public class FTPWorker : BSPWorkerBase { - /// - /// Called by dispatcher to send incoming data destined for this protocol. - /// - /// - public override void RecvData(PUP p) - { - throw new NotImplementedException(); - } - - public override void InitializeServerForChannel(BSPChannel channel) - { - // Spawn new worker - FTPWorker ftpWorker = new FTPWorker(channel); - } - } - - public class FTPWorker - { - public FTPWorker(BSPChannel channel) + public FTPWorker(BSPChannel channel) : base(channel) { // Register for channel events channel.OnDestroy += OnChannelDestroyed; _running = true; - _workerThread = new Thread(new ParameterizedThreadStart(FTPWorkerThreadInit)); - _workerThread.Start(channel); + _workerThread = new Thread(new ThreadStart(FTPWorkerThreadInit)); + _workerThread.Start(); } - private void OnChannelDestroyed() - { - // Tell the thread to exit and give it a short period to do so... - _running = false; - - Log.Write(LogType.Verbose, LogComponent.FTP, "Asking FTP worker thread to exit..."); - _workerThread.Join(1000); - - if (_workerThread.IsAlive) - { - Logging.Log.Write(LogType.Verbose, LogComponent.FTP, "FTP worker thread did not exit, terminating."); - _workerThread.Abort(); - } + public override void Terminate() + { + ShutdownWorker(); } - private void FTPWorkerThreadInit(object obj) + private void OnChannelDestroyed(BSPChannel channel) { - _channel = (BSPChannel)obj; + ShutdownWorker(); + } + + private void FTPWorkerThreadInit() + { // // Run the worker thread. // If anything goes wrong, log the exception and tear down the BSP connection. @@ -142,6 +118,8 @@ namespace IFS.FTP { Log.Write(LogType.Error, LogComponent.FTP, "FTP worker thread terminated with exception '{0}'.", e.Message); _channel.SendAbort("Server encountered an error."); + + OnExit(this); } } } @@ -244,7 +222,9 @@ namespace IFS.FTP Log.Write(LogType.Warning, LogComponent.FTP, "Unhandled FTP command {0}.", command); break; } - } + } + + OnExit(this); } private FTPCommand ReadNextCommandWithData(out byte[] data) @@ -797,7 +777,22 @@ namespace IFS.FTP _channel.SendMark((byte)FTPCommand.EndOfCommand, true); } - private BSPChannel _channel; + private void ShutdownWorker() + { + // Tell the thread to exit and give it a short period to do so... + _running = false; + + Log.Write(LogType.Verbose, LogComponent.FTP, "Asking FTP worker thread to exit..."); + _workerThread.Join(1000); + + if (_workerThread.IsAlive) + { + Logging.Log.Write(LogType.Verbose, LogComponent.FTP, "FTP worker thread did not exit, terminating."); + _workerThread.Abort(); + + OnExit(this); + } + } private Thread _workerThread; private bool _running; diff --git a/PUP/IFS.csproj b/PUP/IFS.csproj index dd73410..82818ea 100644 --- a/PUP/IFS.csproj +++ b/PUP/IFS.csproj @@ -104,6 +104,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/PUP/Logging/Log.cs b/PUP/Logging/Log.cs index dae89a5..8fe42de 100644 --- a/PUP/Logging/Log.cs +++ b/PUP/Logging/Log.cs @@ -24,7 +24,8 @@ namespace IFS.Logging EFTP = 0x200, BootServer = 0x400, UDP = 0x800, - + + Configuration = 0x1000, All = 0x7fffffff } @@ -48,10 +49,9 @@ namespace IFS.Logging public static class Log { static Log() - { - // TODO: make configurable - _components = LogComponent.All; - _type = LogType.All; + { + _components = Configuration.LogComponents; + _type = Configuration.LogTypes; //_logStream = new StreamWriter("log.txt"); } @@ -62,7 +62,6 @@ namespace IFS.Logging set { _components = value; } } -#if LOGGING_ENABLED /// /// Logs a message without specifying type/severity for terseness; /// will not log if Type has been set to None. @@ -91,18 +90,6 @@ namespace IFS.Logging } } } -#else - public static void Write(LogComponent component, string message, params object[] args) - { - - } - - public static void Write(LogType type, LogComponent component, string message, params object[] args) - { - - } - -#endif private static LogComponent _components; private static LogType _type; diff --git a/PUP/PUPProtocolBase.cs b/PUP/PUPProtocolBase.cs index 0eeebfa..7494491 100644 --- a/PUP/PUPProtocolBase.cs +++ b/PUP/PUPProtocolBase.cs @@ -21,6 +21,16 @@ namespace IFS Socket = socket; ConnectionType = connectionType; ProtocolImplementation = implementation; + WorkerType = null; + } + + public PUPProtocolEntry(string friendlyName, UInt32 socket, ConnectionType connectionType, Type workerType) + { + FriendlyName = friendlyName; + Socket = socket; + ConnectionType = connectionType; + WorkerType = workerType; + ProtocolImplementation = null; } /// @@ -39,6 +49,8 @@ namespace IFS public ConnectionType ConnectionType; public PUPProtocolBase ProtocolImplementation; + + public Type WorkerType; } /// diff --git a/PUP/PUPProtocolDispatcher.cs b/PUP/PUPProtocolDispatcher.cs index bbebb64..5fb69de 100644 --- a/PUP/PUPProtocolDispatcher.cs +++ b/PUP/PUPProtocolDispatcher.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using PcapDotNet.Base; using System.Net.NetworkInformation; +using PcapDotNet.Core; namespace IFS { @@ -35,16 +36,21 @@ namespace IFS get { return _instance; } } - public void RegisterInterface(string description) + public void RegisterRAWInterface(LivePacketDevice iface) { - // TODO: support multiple interfaces (for gateway routing, for example.) - // TODO: support configuration options for backend. - //Ethernet enet = new Ethernet(i.Description); + Ethernet enet = new Ethernet(iface); - UDPEncapsulation udp = new UDPEncapsulation(description); - _pupPacketInterface = udp as IPupPacketInterface; - _rawPacketInterface = udp as IRawPacketInterface; + _pupPacketInterface = enet; + _rawPacketInterface = enet; + _pupPacketInterface.RegisterReceiveCallback(OnPupReceived); + } + public void RegisterUDPInterface(NetworkInterface iface) + { + UDPEncapsulation udp = new UDPEncapsulation(iface); + + _pupPacketInterface = udp; + _rawPacketInterface = udp; _pupPacketInterface.RegisterReceiveCallback(OnPupReceived); } @@ -118,7 +124,7 @@ namespace IFS Log.Write(LogType.Verbose, LogComponent.PUP, "Dispatching PUP (source {0}, dest {1}) to BSP protocol for {0}.", pup.SourcePort, pup.DestinationPort, entry.FriendlyName); //entry.ProtocolImplementation.RecvData(pup); - BSPManager.EstablishRendezvous(pup, (BSPProtocol)entry.ProtocolImplementation); + BSPManager.EstablishRendezvous(pup, entry.WorkerType); } } else if (BSPManager.ChannelExistsForSocket(pup)) diff --git a/PUP/Transport/Ethernet.cs b/PUP/Transport/Ethernet.cs index 51fc4f9..993c88b 100644 --- a/PUP/Transport/Ethernet.cs +++ b/PUP/Transport/Ethernet.cs @@ -11,43 +11,19 @@ using PcapDotNet.Packets; using PcapDotNet.Packets.Ethernet; using IFS.Logging; using System.IO; +using System.Net.NetworkInformation; +using System.Threading; namespace IFS.Transport { - public struct EthernetInterface - { - public EthernetInterface(string name, string description, MacAddress macAddress) - { - Name = name; - Description = description; - MacAddress = macAddress; - } - - public static List EnumerateDevices() - { - List interfaces = new List(); - - foreach (LivePacketDevice device in LivePacketDevice.AllLocalMachine) - { - interfaces.Add(new EthernetInterface(device.Name, device.Description, device.GetMacAddress())); - } - - return interfaces; - } - - public string Name; - public string Description; - public MacAddress MacAddress; - } - /// /// Defines interface "to the metal" (raw ethernet frames) which may wrap the underlying transport (for example, winpcap) /// public class Ethernet : IPupPacketInterface, IRawPacketInterface { - public Ethernet(string ifaceName) + public Ethernet(LivePacketDevice iface) { - AttachInterface(ifaceName); + _interface = iface; // Set up maps _pupToEthernetMap = new Dictionary(256); @@ -60,7 +36,10 @@ namespace IFS.Transport // Now that we have a callback we can start receiving stuff. Open(false /* not promiscuous */, int.MaxValue); - BeginReceive(); + + // Kick off the receiver thread, this will never return or exit. + Thread receiveThread = new Thread(new ThreadStart(BeginReceive)); + receiveThread.Start(); } public void Send(PUP p) @@ -241,27 +220,7 @@ namespace IFS.Transport // Not a PUP, Discard the packet. We will not log this, so as to keep noise down. //Log.Write(LogLevel.DroppedPacket, "Not a PUP. Dropping."); } - } - - private void AttachInterface(string ifaceName) - { - _interface = null; - - // Find the specified device by name - foreach (LivePacketDevice device in LivePacketDevice.AllLocalMachine) - { - if (device.Description == ifaceName) - { - _interface = device; - break; - } - } - - if (_interface == null) - { - throw new InvalidOperationException("Requested interface not found."); - } - } + } private void Open(bool promiscuous, int timeout) { diff --git a/PUP/Transport/UDP.cs b/PUP/Transport/UDP.cs index bbc4501..b2b838a 100644 --- a/PUP/Transport/UDP.cs +++ b/PUP/Transport/UDP.cs @@ -15,7 +15,7 @@ namespace IFS.Transport /// public class UDPEncapsulation : IPupPacketInterface, IRawPacketInterface { - public UDPEncapsulation(string interfaceName) + public UDPEncapsulation(NetworkInterface iface) { // Try to set up UDP client. try @@ -23,28 +23,13 @@ namespace IFS.Transport _udpClient = new UdpClient(_udpPort, AddressFamily.InterNetwork); _udpClient.Client.Blocking = true; _udpClient.EnableBroadcast = true; - _udpClient.MulticastLoopback = false; + _udpClient.MulticastLoopback = false; // // Grab the broadcast address for the interface so that we know what broadcast address to use // for our UDP datagrams. - // - NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces(); - - IPInterfaceProperties props = null; - foreach (NetworkInterface nic in nics) - { - if (nic.Description.ToLowerInvariant() == interfaceName.ToLowerInvariant()) - { - props = nic.GetIPProperties(); - break; - } - } - - if (props == null) - { - throw new InvalidOperationException(String.Format("No interface matching description '{0}' was found.", interfaceName)); - } + // + IPInterfaceProperties props = iface.GetIPProperties(); foreach (UnicastIPAddressInformation unicast in props.UnicastAddresses) { @@ -60,16 +45,16 @@ namespace IFS.Transport if (_broadcastEndpoint == null) { - throw new InvalidOperationException(String.Format("No IPV4 network information was found for interface '{0}'.", interfaceName)); + throw new InvalidOperationException(String.Format("No IPV4 network information was found for interface '{0}'.", iface.Name)); } } catch (Exception e) { Log.Write(LogType.Error, LogComponent.UDP, - "Error configuring UDP socket {0} for use with ContrAlto on interface {1}. Ensure that the selected network interface is valid, configured properly, and that nothing else is using this port.", + "Error configuring UDP socket {0} for use with IFS on interface {1}. Ensure that the selected network interface is valid, configured properly, and that nothing else is using this port.", _udpPort, - interfaceName); + iface.Name); Log.Write(LogType.Error, LogComponent.UDP, "Error was '{0}'.",