diff --git a/Ethernet/Ethernet/Class1.cs b/BSP/BSP/Class1.cs similarity index 89% rename from Ethernet/Ethernet/Class1.cs rename to BSP/BSP/Class1.cs index e92684d..1baa8d2 100644 --- a/Ethernet/Ethernet/Class1.cs +++ b/BSP/BSP/Class1.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Ethernet +namespace BSP { public class Class1 { diff --git a/BSP/BSP/Properties/AssemblyInfo.cs b/BSP/BSP/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..cdf0176 --- /dev/null +++ b/BSP/BSP/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BSP")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Vulcan Inc.")] +[assembly: AssemblyProduct("BSP")] +[assembly: AssemblyCopyright("Copyright © Vulcan Inc. 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5093c127-6a0e-48ff-8ae3-7db590348b6f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/BreathOfLife/BreathOfLife/App.config b/BreathOfLife/BreathOfLife/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/BreathOfLife/BreathOfLife/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BreathOfLife/BreathOfLife/BreathOfLife.csproj b/BreathOfLife/BreathOfLife/BreathOfLife.csproj new file mode 100644 index 0000000..40b076b --- /dev/null +++ b/BreathOfLife/BreathOfLife/BreathOfLife.csproj @@ -0,0 +1,58 @@ + + + + + Debug + AnyCPU + {BD5D7CF7-52BA-4960-A60F-75EEECCEF775} + Exe + Properties + BreathOfLife + BreathOfLife + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BreathOfLife/BreathOfLife/Program.cs b/BreathOfLife/BreathOfLife/Program.cs new file mode 100644 index 0000000..0bed51f --- /dev/null +++ b/BreathOfLife/BreathOfLife/Program.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BreathOfLife +{ + class Program + { + static void Main(string[] args) + { + } + } +} diff --git a/BreathOfLife/BreathOfLife/Properties/AssemblyInfo.cs b/BreathOfLife/BreathOfLife/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..cc9f7cc --- /dev/null +++ b/BreathOfLife/BreathOfLife/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BreathOfLife")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Vulcan Inc.")] +[assembly: AssemblyProduct("BreathOfLife")] +[assembly: AssemblyCopyright("Copyright © Vulcan Inc. 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("af633818-d867-41ba-a09c-d26de9bc75b2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Ethernet/Ethernet/Ethernet.csproj b/Ethernet/Ethernet/Ethernet.csproj index b7e0fb3..15609e2 100644 --- a/Ethernet/Ethernet/Ethernet.csproj +++ b/Ethernet/Ethernet/Ethernet.csproj @@ -20,6 +20,7 @@ DEBUG;TRACE prompt 4 + x64 pdbonly @@ -30,6 +31,18 @@ 4 + + ..\pcap\PcapDotNet.Base.dll + + + ..\pcap\PcapDotNet.Core.dll + + + ..\pcap\PcapDotNet.Core.Extensions.dll + + + ..\pcap\PcapDotNet.Packets.dll + @@ -39,7 +52,7 @@ - + diff --git a/PUP/BCPLString.cs b/PUP/BCPLString.cs new file mode 100644 index 0000000..0554131 --- /dev/null +++ b/PUP/BCPLString.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + /// + /// A BCPL string is one byte of length N followed by N bytes of ASCII. + /// This class is a (very) simple encapsulation of that over a byte array. + /// + public class BCPLString + { + public BCPLString(string s) + { + if (s.Length > 255) + { + throw new InvalidOperationException("Max length for a BCPL string is 255 characters."); + } + + _string = new byte[_string.Length]; + + // We simply take the low 8-bits of each Unicode character and stuff it into the + // byte array. This works fine for the ASCII subset of Unicode but obviously + // is bad for everything else. This is unlikely to be an issue given the lack of + // any real internationalization support on the IFS end of things, but might be + // something to look at. + for(int i=0;i< _string.Length; i++) + { + _string[i] = (byte)s[i]; + } + } + + /// + /// Build a new BCPL string from the raw representation + /// + /// + public BCPLString(byte[] rawData) + { + if (rawData.Length > 256) + { + throw new InvalidOperationException("Max length for a BCPL string is 255 characters."); + } + + // Sanity check that first byte matches length of data sent to us + if (rawData.Length < 1 || rawData[0] != rawData.Length - 1) + { + throw new InvalidOperationException("BCPL length data is invalid."); + } + + _string = new byte[rawData.Length - 1]; + Array.Copy(rawData, 1, _string, 0, rawData.Length - 1); + } + + /// + /// Returns a native representation of the BCPL string + /// + /// + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + // See notes in constructor re: unicode. + for (int i = 0; i < _string.Length; i++) + { + sb.Append((char)_string[i]); + } + + return sb.ToString(); + } + + /// + /// Returns the raw representation of the BCPL string + /// + /// + public byte[] ToArray() + { + byte[] a = new byte[_string.Length + 1]; + + a[0] = (byte)_string.Length; + _string.CopyTo(a, 1); + + return a; + } + + private byte[] _string; + } +} diff --git a/PUP/BSPManager.cs b/PUP/BSPManager.cs new file mode 100644 index 0000000..753bc8a --- /dev/null +++ b/PUP/BSPManager.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace IFS +{ + + + public enum BSPState + { + Unconnected, + Connected + } + + public class BSPChannel + { + public BSPChannel(PUP rfcPup) + { + _inputLock = new ReaderWriterLockSlim(); + _outputLock = new ReaderWriterLockSlim(); + + _inputWriteEvent = new AutoResetEvent(false); + + _inputQueue = new MemoryStream(65536); + + // TODO: init IDs, etc. based on RFC PUP + _start_pos = _recv_pos = _send_pos = rfcPup.ID; + + } + + public PUPPort ClientPort + { + get { return _clientConnectionPort; } + } + + public PUPPort ServerPort + { + get { return _serverConnectionPort; } + } + + /// + /// Reads data from the channel (i.e. from the client). Will block if not all the requested data is available. + /// + /// + public int Read(ref byte[] data, int count) + { + // sanity check + if (count > data.Length) + { + throw new InvalidOperationException("count must be less than or equal to the length of the buffer being read into."); + } + + int read = 0; + + // Loop until the data we asked for arrives or until we time out waiting. + // TODO: handle partial transfers due to aborted BSPs. + while (true) + { + _inputLock.EnterUpgradeableReadLock(); + if (_inputQueue.Count >= count) + { + _inputLock.EnterWriteLock(); + // We have the data right now, read it and return. + // TODO: this is a really inefficient thing. + for (int i = 0; i < count; i++) + { + data[i] = _inputQueue.Dequeue(); + } + + _inputLock.ExitWriteLock(); + _inputLock.ExitUpgradeableReadLock(); + + break; + } + else + { + _inputLock.ExitUpgradeableReadLock(); + + // Not enough data in the queue. + // Wait until we have received more data, then try again. + _inputWriteEvent.WaitOne(); // TODO: timeout and fail + } + } + + return read; + } + + /// + /// Appends data into the input queue (called from BSPManager to place new PUP data into the BSP stream) + /// + public void WriteQueue(PUP dataPUP) + { + // If we are over our high watermark, we will drop the data (and not send an ACK even if requested). + // Clients should be honoring the limits we set in the RFC packets. + _inputLock.EnterUpgradeableReadLock(); + if (_inputQueue.Count + dataPUP.Contents.Length > MaxBytes) + { + _inputLock.ExitUpgradeableReadLock(); + return; + } + + // Sanity check on expected position from sender vs. received data on our end. + // If they don't match then we've lost a packet somewhere. + if (dataPUP.ID != _recv_pos) + { + // Current behavior is to simply drop all incoming PUPs (and not ACK them) until they are re-sent to us + // (in which case the above sanity check will pass). According to spec, AData requests that are not ACKed + // must eventually be resent. This is far simpler than accepting out-of-order data and keeping track + // of where it goes in the queue. + return; + } + + + // Prepare to add data to the queue + // Again, this is really inefficient + _inputLock.EnterWriteLock(); + + for (int i = 0; i < dataPUP.Contents.Length; i++) + { + _inputQueue.Enqueue(dataPUP.Contents[i]); + } + + _recv_pos += (UInt32)dataPUP.Contents.Length; + + _inputLock.ExitWriteLock(); + + _inputLock.ExitUpgradeableReadLock(); + + _inputWriteEvent.Set(); + + if ((PupType)dataPUP.Type == PupType.AData) + { + SendAck(); + } + + } + + /// + /// Sends data to the channel (i.e. to the client). Will block if an ACK is requested. + /// + /// The data to be sent + public void Send(byte[] data) + { + // Write data to the output stream + + // Await an ack for the PUP we just sent + _outputAckEvent.WaitOne(); // TODO: timeout and fail + + } + + public void Ack(PUP ackPUP) + { + // Update receiving end stats (max PUPs, etc.) + // Ensure client's position matches ours + + // Let any waiting threads continue + _outputAckEvent.Set(); + } + + public void Mark(byte type); + public void Interrupt(); + + public void Abort(int code, string message); + public void Error(int code, string message); + + public void End(); + + // TODO: + // Events for: + // Abort, End, Mark, Interrupt (from client) + // Repositioning (due to lost packets) (perhaps not necessary) + // + + private void SendAck() + { + + } + + + private BSPState _state; + private UInt32 _recv_pos; + private UInt32 _send_pos; + private UInt32 _start_pos; + + private PUPPort _clientConnectionPort; // the client port + private PUPPort _serverConnectionPort; // the server port we (the server) have established for communication + + private ReaderWriterLockSlim _inputLock; + private System.Threading.AutoResetEvent _inputWriteEvent; + + private ReaderWriterLockSlim _outputLock; + + private System.Threading.AutoResetEvent _outputAckEvent; + + // TODO: replace this with a more efficient structure for buffering data + private Queue _inputQueue; + + // Constants + + // For now, we work on one PUP at a time. + private const int MaxPups = 1; + private const int MaxPupSize = 532; + private const int MaxBytes = 1 * 532; + } + + /// + /// + /// + public static class BSPManager + { + static BSPManager() + { + + } + + + /// + /// Called when BSP-based protocols receive data. + /// + /// + /// null if no new channel is created due to the sent PUP (not an RFC) + /// a new BSPChannel if one has been created based on the PUP (new RFC) + /// + /// + public static BSPChannel RecvData(PUP p) + { + PupType type = (PupType)p.Type; + + switch (type) + { + case PupType.RFC: + { + BSPChannel newChannel = new BSPChannel(p); + _activeChannels.Add(newChannel.ClientPort.Socket); + + return newChannel; + } + + case PupType.Data: + case PupType.AData: + { + BSPChannel channel = FindChannelForPup(p); + + if (channel != null) + { + channel.WriteQueue(p); + } + } + break; + + case PupType.Ack: + BSPChannel channel = FindChannelForPup(p); + + if (channel != null) + { + channel.Ack(p); + } + break; + + case PupType.End: + { + BSPChannel channel = FindChannelForPup(p); + + if (channel != null) + { + channel.EndReply(); + } + } + break; + + case PupType.Abort: + { + // TODO: tear down the channel + } + break; + + default: + throw new NotImplementedException(String.Format("Unhandled BSP PUP type {0}.", type)); + + } + + return null; + } + + public static void DestroyChannel(BSPChannel channel) + { + + } + + private static BSPChannel FindChannelForPup(PUP p) + { + return null; + } + + private static Dictionary _activeChannels; + } +} diff --git a/PUP/CopyDiskServer.cs b/PUP/CopyDiskServer.cs new file mode 100644 index 0000000..0d4c810 --- /dev/null +++ b/PUP/CopyDiskServer.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + public class CopyDiskServer : PUPProtocolBase + { + /// + /// Called by dispatcher to send incoming data destined for this protocol. + /// + /// + public override void RecvData(PUP p) + { + BSPChannel newChannel = BSPManager.RecvData(p); + + if (newChannel != null) + { + // spwan new worker thread with new BSP channel + } + } + } +} diff --git a/PUP/DirectoryServices.cs b/PUP/DirectoryServices.cs new file mode 100644 index 0000000..16ab812 --- /dev/null +++ b/PUP/DirectoryServices.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + + public class HostAddress + { + public HostAddress(byte network, byte host) + { + Network = network; + Host = host; + } + + public byte Network; + public byte Host; + } + + /// + /// Provides a basic database used by the Misc. Services to do name lookup and whatnot. + /// + /// + public class DirectoryServices + { + private DirectoryServices() + { + // Get our host address; for now just hardcode it. + // TODO: need to define config files, etc. + + _localHost = new HostAddress(1, 34); + } + + public static DirectoryServices Instance + { + get { return _instance; } + } + + public HostAddress LocalHostAddress + { + get { return _localHost; } + } + + private HostAddress _localHost; + + private static DirectoryServices _instance = new DirectoryServices(); + } +} diff --git a/PUP/Dispatcher.cs b/PUP/Dispatcher.cs new file mode 100644 index 0000000..8065914 --- /dev/null +++ b/PUP/Dispatcher.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using IFS.Transport; +using PcapDotNet.Packets; + +namespace IFS +{ + + + + /// + /// Dispatches incoming PUPs to the right protocol handler; sends outgoing PUPs over the network. + /// + public class Dispatcher + { + /// + /// Private Constructor for this class, enforcing Singleton usage. + /// + private Dispatcher() + { + _dispatchMap = new Dictionary(); + + _packetInterface = new Ethernet(iface, OnPacketReceived); + } + + /// + /// Accessor for singleton instance of this class. + /// + public static Dispatcher Instance + { + get { return _instance; } + } + + /// + /// Registers a new protocol with the dispatcher + /// + /// + /// + public void RegisterProtocol(PUPProtocolEntry entry) + { + if (_dispatchMap.ContainsKey(entry.Socket)) + { + throw new InvalidOperationException( + String.Format("Socket {0} has already been registered for protocol {1}", impl.Socket, _dispatchMap[impl.Socket].FriendlyName)); + } + + _dispatchMap[entry.Socket] = entry; + } + + public void SendPup(PUP p) + { + // TODO: Write PUP to ethernet + + } + + private void OnPacketReceived(Packet p) + { + // filter out PUPs, discard all other packets. Forward PUP on to registered endpoints. + // + if ((int)p.Ethernet.EtherType == 512) // 512 = pup type + { + PUP pup = new PUP(p.Ethernet.Payload.ToMemoryStream()); + + if (_dispatchMap.ContainsKey(pup.DestinationPort.Socket)) + { + PUPProtocolEntry entry = _dispatchMap[pup.DestinationPort.Socket]; + entry.ProtocolImplementation.RecvData(pup); + } + else + { + // Not a protocol we handle; TODO: log it. + } + } + else + { + // Not a PUP, Discard the packet. + } + } + + /// + /// Our interface to some kind of network + /// + private IPacketInterface _packetInterface; + + /// + /// Map from socket to protocol implementation + /// + private Dictionary _dispatchMap; + + private static Dispatcher _instance = new Dispatcher(); + } +} diff --git a/PUP/EchoProtocol.cs b/PUP/EchoProtocol.cs new file mode 100644 index 0000000..9c5b48d --- /dev/null +++ b/PUP/EchoProtocol.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + /// + /// Implements the PUP Echo Protocol. + /// + public class EchoProtocol : PUPProtocolBase + { + public EchoProtocol() + { + + } + + /// + /// Called by dispatcher to send incoming data destined for this protocol + /// + /// + public override void RecvData(PUP p) + { + // If this is an EchoMe packet, we will send back an "ImAnEcho" packet. + if (p.Type == PupType.EchoMe) + { + // Just send it back with the source/destination swapped. + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + + PUP echoPup = new PUP(PupType.ImAnEcho, p.ID, p.SourcePort, localPort); + Dispatcher.Instance.SendPup(p); + } + } + + } +} diff --git a/PUP/Entrypoint.cs b/PUP/Entrypoint.cs new file mode 100644 index 0000000..dea6ab1 --- /dev/null +++ b/PUP/Entrypoint.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + public class Entrypoint + { + static void Main(string[] args) + { + // Set up protocols: + + // Connectionless + Dispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Misc Services", 0x4, ConnectionType.Connectionless, new MiscServicesProtocol())); + Dispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Echo", 0x5, ConnectionType.Connectionless, new EchoProtocol())); + + // RTP/BSP based: + Dispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("CopyDisk", 0x15 /* 25B */, ConnectionType.BSP, new CopyDiskServer())); + + } +} diff --git a/PUP/IFS.csproj b/PUP/IFS.csproj new file mode 100644 index 0000000..8135c78 --- /dev/null +++ b/PUP/IFS.csproj @@ -0,0 +1,80 @@ + + + + + Debug + AnyCPU + {5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB} + Exe + Properties + IFS + IFS + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + ..\Ethernet\pcap\PcapDotNet.Base.dll + + + ..\Ethernet\pcap\PcapDotNet.Core.dll + + + ..\Ethernet\pcap\PcapDotNet.Core.Extensions.dll + + + ..\Ethernet\pcap\PcapDotNet.Packets.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ethernet/Ethernet.sln b/PUP/IFS.sln similarity index 54% rename from Ethernet/Ethernet.sln rename to PUP/IFS.sln index 96797f4..686ba3a 100644 --- a/Ethernet/Ethernet.sln +++ b/PUP/IFS.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ethernet", "Ethernet\Ethernet.csproj", "{BFFB20D8-4684-4E44-9521-44F3593525FB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IFS", "IFS.csproj", "{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +11,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BFFB20D8-4684-4E44-9521-44F3593525FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFFB20D8-4684-4E44-9521-44F3593525FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFFB20D8-4684-4E44-9521-44F3593525FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFFB20D8-4684-4E44-9521-44F3593525FB}.Release|Any CPU.Build.0 = Release|Any CPU + {5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PUP/MiscServicesProtocol.cs b/PUP/MiscServicesProtocol.cs new file mode 100644 index 0000000..b02eb82 --- /dev/null +++ b/PUP/MiscServicesProtocol.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + /// + /// Implements PUP Miscellaneous Services (see miscSvcsProto.pdf) + /// which include: + /// - Date and Time services + /// - Mail check + /// - Network Directory Lookup + /// - Alto Boot protocols + /// - Authenticate/Validate + /// + public class MiscServicesProtocol : PUPProtocolBase + { + public MiscServicesProtocol() + { + // TODO: + // load host tables, etc. + } + + /// + /// Called by dispatcher to send incoming data destined for this protocol + /// + /// + public override void RecvData(PUP p) + { + switch (p.Type) + { + case PupType.StringTimeRequest: + SendStringTimeReply(p); + break; + + case PupType.AltoTimeRequest: + SendAltoTimeReply(p); + break; + + case PupType.AddressLookupRequest: + SendAddressLookupReply(p); + break; + + case PupType.NameLookupRequest: + SendNameLookupReply(p); + break; + + default: + // Unhandled, TODO: log it + break; + } + } + + + private void SendStringTimeReply(PUP p) + { + // + // From the spec, the response is: + // "A string consisting of the current date and time in the form + // '11-SEP-75 15:44:25'" + // + // It makes no mention of timezone or DST, so I am assuming local time here. + // Good enough for government work. + // + DateTime currentTime = DateTime.Now; + + BCPLString bcplDateString = new BCPLString(currentTime.ToString("dd-MMM-yy HH:mm:ss")); + + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUP response = new PUP(PupType.StringTimeReply, p.ID, p.SourcePort, localPort, bcplDateString.ToArray()); + + Dispatcher.Instance.SendPup(response); + } + + private void SendAltoTimeReply(PUP p) + { + // + // From the spec, the response is: + // "10 bytes in all, organized as 5 16-bit words: + // words 0, 1 Present date and time: a 32-bit integer representing number of + // seconds since midnight, January 1, 1901, Greenwich Mean Time (GMT). + // + // word 2 Local time zone information. Bit 0 is zero if west of Greenwich + // and one if east. Bits 1-7 are the number of hours east or west of + // Greenwich. Bits 8-15 are an additional number of minutes. + // + // word 3 Day of the year on or before which Daylight Savings Time takes + // effect locally, where 1 = January 1 and 366 = Dcember 31. (The + // actual day is the next preceding Sunday.) + // + // word 4 Day of the year on or before which Daylight Savings Time ends. If + // Daylight Savings Time is not observed locally, both the start and + // end dates should be 366. + // + // The local time parameters in words 2 and 4 are those in effect at the server's + // location. + // + + // So the Alto epoch is 1/1/1901. For the time being to keep things simple we're assuming + // GMT and no DST at all. TODO: make this take into account our TZ, etc. + // + DateTime currentTime = DateTime.Now; + + // The epoch for .NET is 1/1/0001 at 12 midnight and is counted in 100-ns intervals. + // Some conversion is needed, is what I'm saying. + DateTime altoEpoch = new DateTime(1901, 1, 1); + TimeSpan timeSinceAltoEpoch = new TimeSpan(currentTime.Ticks - altoEpoch.Ticks); + + UInt32 altoTime = (UInt32)timeSinceAltoEpoch.TotalSeconds; + + // Build the response data + byte[] data = new byte[10]; + Helpers.WriteUInt(ref data, altoTime, 0); + Helpers.WriteUShort(ref data, 0, 4); // Timezone, hardcoded to GMT + Helpers.WriteUShort(ref data, 366, 6); // DST info, not used right now. + Helpers.WriteUShort(ref data, 366, 8); + + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUP response = new PUP(PupType.AltoTimeResponse, p.ID, p.SourcePort, localPort, data); + + Dispatcher.Instance.SendPup(response); + } + + private void SendAddressLookupReply(PUP p) + { + // + // Need to find more... useful documentation, but here's what I have: + // For the request PUP: + // A port (6 bytes). + // + // Response: + // A string consisting of an inter-network name expression that matches the request Port. + // + + // + // I am at this time unsure what exactly an "inter-network name expression" consists of. + // Empirically, a simple string name seems to make the Alto happy. + // + + // + // The request PUP contains a port address, we will check the host and network (and ignore the socket). + // and see if we have a match. + // + PUPPort lookupAddress = new PUPPort(p.Contents, 0); + string hostName = DirectoryServices.Instance.AddressLookup(lookupAddress); + + if (!String.IsNullOrEmpty(hostName)) + { + // We have a result, pack the name into the response. + BCPLString interNetworkName = new BCPLString(hostName); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUP lookupReply = new PUP(PupType.AddressLookupResponse, p.ID, p.SourcePort, localPort, interNetworkName.ToArray()); + + Dispatcher.Instance.SendPup(lookupReply); + } + else + { + // Unknown host, send an error reply + BCPLString errorString = new BCPLString("Unknown host for address."); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, errorString.ToArray()); + + Dispatcher.Instance.SendPup(errorReply); + } + } + + private void SendNameLookupReply(PUP p) + { + // + // For the request PUP: + // A string consisting of an inter-network name expression + // + // Response: + // One or more 6-byte blocks containing the address(es) corresponding to the + // name expression. Each block is a Pup Port structure, with the network and host numbers in + // the first two bytes and the socket number in the last four bytes. + // + + // + // I'm not sure what would cause a name to resolve to multiple addresses at this time. + // Also still not sure what an 'inter-network name expression' is. + // As above, assuming this is a simple hostname. + // + BCPLString lookupName = new BCPLString(p.Contents); + + HostAddress address = DirectoryServices.Instance.NameLookup(lookupName.ToString()); + + if (address != null) + { + // We found an address, pack the port into the response. + PUPPort lookupPort = new PUPPort(address, 0); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUP lookupReply = new PUP(PupType.NameLookupResponse, p.ID, p.SourcePort, localPort, lookupPort.ToArray()); + + Dispatcher.Instance.SendPup(lookupReply); + } + else + { + // Unknown host, send an error reply + BCPLString errorString = new BCPLString("Unknown host for name."); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, errorString.ToArray()); + + Dispatcher.Instance.SendPup(errorReply); + } + } + } +} diff --git a/PUP/PUP.cs b/PUP/PUP.cs new file mode 100644 index 0000000..aa185c1 --- /dev/null +++ b/PUP/PUP.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + public enum PupType + { + // Basic types + EchoMe = 1, + ImAnEcho = 2, + ImABadEcho = 3, + Error = 4, + + // BSP/RFC types + RFC = 8, + Abort = 9, + End = 10, + EndReply = 11, + Data = 16, + AData = 17, + Ack = 18, + Mark = 19, + Interrupt = 20, + InterruptReply = 21, + AMark = 22, + + // Misc. Services types + StringTimeRequest = 128, + StringTimeReply = 129, + TenexTimeRequest = 130, + TenexTimeReply = 131, + AltoTimeRequestOld = 132, + AltoTimeResponseOld = 133, + AltoTimeRequest = 134, + AltoTimeResponse = 135, + + // Network Lookup + NameLookupRequest = 144, + NameLookupResponse = 145, + DirectoryLookupErrorReply = 146, + AddressLookupRequest = 147, + AddressLookupResponse = 148, + + // Alto Boot + SendBootFileRequest = 164, + BootDirectoryRequest = 165, + BootDirectoryReply = 166, + } + + public struct PUPPort + { + /// + /// Builds a new port address given network, host and socket parameters + /// + /// + /// + /// + public PUPPort(byte network, byte host, UInt32 socket) + { + Network = network; + Host = host; + Socket = socket; + } + + /// + /// Builds a new port address given a HostAddress and a socket. + /// + /// + /// + public PUPPort(HostAddress address, UInt32 socket) + { + Network = address.Network; + Host = address.Host; + Socket = socket; + } + + /// + /// Builds a new port address from an array containing a raw port representaton + /// + /// + /// + public PUPPort(byte[] rawData, int offset) + { + Network = rawData[offset]; + Host = rawData[offset + 1]; + Socket = Helpers.ReadUInt(rawData, 2); + } + + /// + /// Writes this address back out to a raw byte array at the specified offset + /// + /// + /// + public void WriteToArray(ref byte[] rawData, int offset) + { + rawData[offset] = Network; + rawData[offset + 1] = Host; + Helpers.WriteUInt(ref rawData, Socket, offset + 2); + } + + /// + /// Same as above, but simply returns a new array instead of writing into an existing one. + /// + /// + public byte[] ToArray() + { + byte[] a = new byte[6]; + WriteToArray(ref a, 0); + return a; + } + + public byte Network; + public byte Host; + public UInt32 Socket; + } + + public class PUP + { + /// + /// Construct a new packet from the supplied data. + /// + /// + public PUP(PupType type, UInt32 id, PUPPort destination, PUPPort source, byte[] contents) + { + _rawData = null; + + // Ensure content length is <= 532 bytes. (Technically larger PUPs are allowed, + // but conventionally they are not used and I want to keep things safe.) + if (contents.Length > MAX_PUP_SIZE) + { + throw new InvalidOperationException("PUP size must not exceed 532 bytes."); + } + + TransportControl = 0; + Type = type; + ID = id; + DestinationPort = destination; + SourcePort = source; + Contents = contents; + Length = (ushort)(PUP_HEADER_SIZE + PUP_CHECKSUM_SIZE + contents.Length); + + // Stuff data into raw array + _rawData = new byte[Length]; + Helpers.WriteUShort(ref _rawData, Length, 0); + _rawData[2] = TransportControl; + _rawData[3] = (byte)Type; + Helpers.WriteUInt(ref _rawData, ID, 4); + DestinationPort.WriteToArray(ref _rawData, 8); + SourcePort.WriteToArray(ref _rawData, 14); + Array.Copy(Contents, 0, _rawData, 20, Contents.Length); + + // Calculate the checksum and stow it + Checksum = CalculateChecksum(); + Helpers.WriteUShort(ref _rawData, Checksum, Length - 2); + } + + /// + /// Same as above, but with no content (i.e. a zero-byte payload) + /// + /// + /// + /// + /// + public PUP(PupType type, UInt32 id, PUPPort destination, PUPPort source) : + this(type, id, destination, source, new byte[0]) + { + + } + + /// + /// Load in an existing packet from a stream + /// + /// + public PUP(MemoryStream stream) + { + _rawData = new byte[stream.Length]; + stream.Read(_rawData, 0, (int)stream.Length); + + // Read fields in. TODO: investigate more efficient ways to do this. + Length = Helpers.ReadUShort(_rawData, 0); + + // Sanity check size: + if (Length > stream.Length) + { + throw new InvalidOperationException("Length field in PUP is invalid."); + } + + TransportControl = _rawData[2]; + Type = (PupType)_rawData[3]; + ID = Helpers.ReadUInt(_rawData, 4); + DestinationPort = new PUPPort(_rawData, 8); + SourcePort = new PUPPort(_rawData, 14); + Array.Copy(_rawData, 20, Contents, 0, Length - PUP_HEADER_SIZE - PUP_CHECKSUM_SIZE); + Checksum = Helpers.ReadUShort(_rawData, Length - 2); + + // Validate checksum + ushort cChecksum = CalculateChecksum(); + + if (cChecksum != Checksum) + { + throw new InvalidOperationException(String.Format("PUP checksum is invalid. ({0} vs {1}", Checksum, cChecksum)); + } + + } + + public byte[] RawData + { + get { return _rawData; } + } + + private ushort CalculateChecksum() + { + + int i; + + // + // This code "borrowed" from the Stanford PUP code + // and translated roughly to C# + // + Cksum cksm; + + cksm.lcksm = 0; + cksm.scksm.ccksm = 0; // to make the C# compiler happy since it knows not of unions + cksm.scksm.cksm = 0; + + for (i = 0; i < _rawData.Length - PUP_CHECKSUM_SIZE; i += 2) + { + ushort word = Helpers.ReadUShort(_rawData, i); + + cksm.lcksm += word; + cksm.scksm.cksm += cksm.scksm.ccksm; + cksm.scksm.ccksm = 0; + cksm.lcksm <<= 1; + cksm.scksm.cksm += cksm.scksm.ccksm; + cksm.scksm.ccksm = 0; + } + + if (cksm.scksm.cksm == 0xffff) + { + cksm.scksm.cksm = 0; + } + + return cksm.scksm.cksm; + } + + // Structs used by CalculateChecksum to simulate + // a C union in C# + [StructLayout(LayoutKind.Explicit)] + struct Scksum + { + [FieldOffset(0)] + public ushort ccksm; + [FieldOffset(2)] + public ushort cksm; + } + + [StructLayout(LayoutKind.Explicit)] + struct Cksum + { + [FieldOffset(0)] + public ulong lcksm; + + [FieldOffset(0)] + public Scksum scksm; + } + + public readonly ushort Length; + public readonly byte TransportControl; + public readonly PupType Type; + public readonly UInt32 ID; + public readonly PUPPort DestinationPort; + public readonly PUPPort SourcePort; + public readonly byte[] Contents; + public readonly ushort Checksum; + + private byte[] _rawData; + + private const int MAX_PUP_SIZE = 532; + private const int PUP_HEADER_SIZE = 20; + private const int PUP_CHECKSUM_SIZE = 2; + + + } + + public static class Helpers + { + public static ushort ReadUShort(byte[] data, int offset) + { + return (ushort)((data[offset] << 8) | data[offset + 1]); + } + + public static UInt32 ReadUInt(byte[] data, int offset) + { + return (UInt32)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]); + } + + public static void WriteUShort(ref byte[] data, ushort s, int offset) + { + data[offset] = (byte)(s >> 8); + data[offset + 1] = (byte)s; + } + + public static void WriteUInt(ref byte[] data, UInt32 s, int offset) + { + data[offset] = (byte)(s >> 24); + data[offset + 1] = (byte)(s >> 16); + data[offset + 2] = (byte)(s >> 8); + data[offset + 3] = (byte)s; + } + } +} diff --git a/PUP/PUPProtocolBase.cs b/PUP/PUPProtocolBase.cs new file mode 100644 index 0000000..9cf1dfb --- /dev/null +++ b/PUP/PUPProtocolBase.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS +{ + public enum ConnectionType + { + Connectionless, /* echo, name resolution, etc. */ + BSP, /* FTP, Telnet, CopyDisk, etc. */ + } + + public struct PUPProtocolEntry + { + public PUPProtocolEntry(string friendlyName, UInt32 socket, ConnectionType connectionType, PUPProtocolBase implementation) + { + FriendlyName = friendlyName; + Socket = socket; + ConnectionType = connectionType; + ProtocolImplementation = implementation; + } + + /// + /// Indicates the 'friendly' name for the protocol. + /// + public string FriendlyName; + + /// + /// Indicates the socket used by the protocol + /// + public UInt32 Socket; + + /// + /// Indicates the type of connection (connectionless or BSP-based) + /// + public ConnectionType ConnectionType; + + public PUPProtocolBase ProtocolImplementation; + } + + /// + /// Base class for all PUP-based protocols. + /// + public abstract class PUPProtocolBase + { + public PUPProtocolBase() + { + + } + + /// + /// Called by dispatcher to send incoming data destined for this protocol. + /// + /// + public abstract void RecvData(PUP p); + + } +} diff --git a/PUP/Properties/AssemblyInfo.cs b/PUP/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..df0ab97 --- /dev/null +++ b/PUP/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PUP")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Vulcan Inc.")] +[assembly: AssemblyProduct("PUP")] +[assembly: AssemblyCopyright("Copyright © Vulcan Inc. 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("60ac4040-d43f-4d54-ada8-2ba64163973d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/PUP/Transport/Ethernet.cs b/PUP/Transport/Ethernet.cs new file mode 100644 index 0000000..62d023e --- /dev/null +++ b/PUP/Transport/Ethernet.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using PcapDotNet.Core; +using PcapDotNet.Packets; + +namespace IFS.Transport +{ + public struct EthernetInterface + { + public EthernetInterface(string name, string description) + { + Name = name; + Description = description; + } + + public string Name; + public string Description; + } + + /// + /// Defines interface "to the metal" (raw ethernet frames) which may wrap the underlying transport (for example, winpcap) + /// + public class Ethernet : IPacketInterface + { + public Ethernet(EthernetInterface iface, HandlePacket callback) + { + AttachInterface(iface); + _callback = callback; + } + + public static List EnumerateDevices() + { + List interfaces = new List(); + + foreach(LivePacketDevice device in LivePacketDevice.AllLocalMachine) + { + interfaces.Add(new EthernetInterface(device.Name, device.Description)); + } + + return interfaces; + } + + public void Open(bool promiscuous, int timeout) + { + _communicator = _interface.Open(0xffff, promiscuous ? PacketDeviceOpenAttributes.Promiscuous : PacketDeviceOpenAttributes.None, timeout); + } + + /// + /// Begin receiving packets, forever. + /// + public void BeginReceive() + { + _communicator.ReceivePackets(-1, ReceiveCallback); + } + + private void ReceiveCallback(Packet p) + { + _callback(p); + } + + private void AttachInterface(EthernetInterface iface) + { + _interface = null; + + // Find the specified device by name + foreach (LivePacketDevice device in LivePacketDevice.AllLocalMachine) + { + if (device.Name == iface.Name) + { + _interface = device; + break; + } + } + + if (_interface == null) + { + throw new InvalidOperationException("Requested interface not found."); + } + } + + private LivePacketDevice _interface; + private PacketCommunicator _communicator; + private HandlePacket _callback; + + } +} diff --git a/PUP/Transport/PacketInterface.cs b/PUP/Transport/PacketInterface.cs new file mode 100644 index 0000000..f8a3329 --- /dev/null +++ b/PUP/Transport/PacketInterface.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS.Transport +{ + /// + /// PacketInterface provides an abstraction over a transport (Ethernet, IP, Carrier Pigeon) + /// which can provide raw packets. + /// + interface IPacketInterface + { + } +}