diff --git a/PUP/BSP/BSPChannel.cs b/PUP/BSP/BSPChannel.cs index 67a5195..4e2a88f 100644 --- a/PUP/BSP/BSPChannel.cs +++ b/PUP/BSP/BSPChannel.cs @@ -587,7 +587,7 @@ namespace IFS.BSP // // Pull the next PUP off the output queue and send it. // - PUP nextPup = _outputWindow[_outputWindowIndex++]; + PUP nextPup = _outputWindow[_outputWindowIndex++]; // // If we've sent as many PUPs to the client as it says it can take, @@ -606,13 +606,13 @@ namespace IFS.BSP // TODO: rewrite the underlying PUP code so we don't have to completely recreate the PUPs like this, it makes me hurt. // if (nextPup.Type == PupType.Data || nextPup.Type == PupType.AData) - { - nextPup = new PUP(bAck ? PupType.AData : PupType.Data, _sendPos, nextPup.DestinationPort, nextPup.SourcePort, nextPup.Contents); + { + _outputWindow[_outputWindowIndex - 1] = nextPup = new PUP(bAck ? PupType.AData : PupType.Data, _sendPos, nextPup.DestinationPort, nextPup.SourcePort, nextPup.Contents); } else if (nextPup.Type == PupType.Mark || nextPup.Type == PupType.AMark) { - nextPup = new PUP(bAck ? PupType.AMark : PupType.Mark, _sendPos, nextPup.DestinationPort, nextPup.SourcePort, nextPup.Contents); - } + _outputWindow[_outputWindowIndex - 1] = nextPup = new PUP(bAck ? PupType.AMark : PupType.Mark, _sendPos, nextPup.DestinationPort, nextPup.SourcePort, nextPup.Contents); + } // // Send it! @@ -634,8 +634,9 @@ namespace IFS.BSP { break; } - + // Nope. Request another ACK. + Log.Write(LogType.Verbose, LogComponent.BSP, "Waiting for client to become ready.."); } // @@ -649,15 +650,23 @@ namespace IFS.BSP _lastClientRecvPos, _sendPos); + Log.Write(LogType.Warning, LogComponent.BSP, + "First position in window is {0}.", + _outputWindow[0].ID); + + // Require ACKs for all retried packets. + bAck = true; + // // Move our window index back to the first PUP we missed and start resending from that position. // _outputWindowIndex = 0; while(_outputWindowIndex < _outputWindow.Count) - { + { if (_outputWindow[_outputWindowIndex].ID == _lastClientRecvPos) { _sendPos = _outputWindow[_outputWindowIndex].ID; + Log.Write(LogType.Verbose, LogComponent.BSP, "Resending from position {0}", _sendPos); break; } _outputWindowIndex++; diff --git a/PUP/Boot/BootServer.cs b/PUP/Boot/BootServer.cs index a3e06a5..a3e221d 100644 --- a/PUP/Boot/BootServer.cs +++ b/PUP/Boot/BootServer.cs @@ -1,24 +1,152 @@ -using System; +using IFS.Logging; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace IFS.Boot { - public class BootServerProtocol : PUPProtocolBase + public struct BootFileEntry { - public BootServerProtocol() - { + public ushort BootNumber; + public string Filename; + public DateTime Date; + } + /// + /// BootServer provides encapsulation of boot file mappings and enumerations. + /// + public static class BootServer + { + static BootServer() + { + LoadBootFileTables(); } - /// - /// Called by dispatcher to send incoming data destined for this protocol - /// - /// - public override void RecvData(PUP p) + private static void LoadBootFileTables() { + _numberToNameTable = new Dictionary(); + + using (StreamReader sr = new StreamReader("Conf\\bootdirectory.txt")) + { + int lineNumber = 0; + while (!sr.EndOfStream) + { + // + // Read in the next line. This is either: + // - A comment to EOL (starting with "#") + // - empty + // - a mapping, consisting of two tokens: a number and a name. + // + lineNumber++; + string line = sr.ReadLine().Trim(); + + if (string.IsNullOrEmpty(line) || line.StartsWith("#")) + { + continue; + } + + string[] tokens = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + + if (tokens.Length != 2) + { + Log.Write(LogType.Warning, LogComponent.BootServer, + "bootdirectory.txt line {0}: Invalid syntax: expected two tokens, got {1}. Ignoring.", lineNumber, tokens.Length); + continue; + } + else + { + ushort bootNumber = 0; + try + { + bootNumber = Convert.ToUInt16(tokens[0], 8); + } + catch + { + Log.Write(LogType.Warning, LogComponent.BootServer, + "bootdirectory.txt line {0}: Invalid syntax: '{1}' is not a valid integer value.", lineNumber, tokens[0]); + continue; + } + + // + // Validate that the file exists in the boot subdirectory. + // + if (!File.Exists(Path.Combine(Configuration.BootRoot, tokens[1]))) + { + Log.Write(LogType.Warning, LogComponent.BootServer, + "bootdirectory.txt line {0}: Specified boot file '{1}' does not exist.", lineNumber, tokens[1]); + continue; + } + + // + // Looks like this entry is OK syntactically, add it if it isn't a duplicate. + // + if (!_numberToNameTable.ContainsKey(bootNumber)) + { + _numberToNameTable.Add(bootNumber, tokens[1]); + } + else + { + Log.Write(LogType.Warning, LogComponent.BootServer, + "bootdirectory.txt line {0}: Specified boot file '{1}' has already been specified. Ignoring duplicate.", lineNumber, bootNumber); + continue; + } + } + } + } } + + public static string GetFileNameForNumber(ushort number) + { + if (_numberToNameTable.ContainsKey(number)) + { + return _numberToNameTable[number]; + } + else + { + return null; + } + } + + public static FileStream GetStreamForNumber(ushort number) + { + if (_numberToNameTable.ContainsKey(number)) + { + string filePath = Path.Combine(Configuration.BootRoot, _numberToNameTable[number]); + try + { + return new FileStream(filePath, FileMode.Open, FileAccess.Read); + } + catch (Exception e) + { + Log.Write(LogType.Warning, LogComponent.BootServer, "Bootfile {0} could not be opened, error is '{1}'", filePath, e.Message); + return null; + } + } + else + { + return null; + } + } + + public static List EnumerateBootFiles() + { + List bootFiles = new List(); + + foreach(ushort key in _numberToNameTable.Keys) + { + BootFileEntry newEntry = new BootFileEntry(); + newEntry.BootNumber = key; + newEntry.Filename = _numberToNameTable[key]; + newEntry.Date = new DateTime(1978, 2, 15); // todo: get real date + bootFiles.Add(newEntry); + } + + return bootFiles; + } + + private static Dictionary _numberToNameTable; } } diff --git a/PUP/Conf/bootdirectory.txt b/PUP/Conf/bootdirectory.txt new file mode 100644 index 0000000..cf9ff54 --- /dev/null +++ b/PUP/Conf/bootdirectory.txt @@ -0,0 +1,64 @@ +# bootdirectory.txt: +# +# This file contains a simple mapping from boot number to boot file, one entry per line +# in the form: +# +# +# Boot files are expected to reside in the "boot" subdirectory of the IFS root store. +# +# The order of the files is based on the order used on authentic Xerox Gateway servers, for example +# the config listed here: http://xeroxalto.computerhistory.org/Io/Murray/.Brio.txt!1.html +# + +0 dmt.boot +1 newos.boot +2 ftp.boot +3 scavenger.boot +4 copydisk.boot +5 crttest.boot +6 madtest.boot +7 chat.boot +10 netexec.boot +11 puptest.boot +12 etherwatch.boot +13 keytest.boot +14 calculator.boot +15 diex.boot +16 triex.boot +17 edp.boot +20 bfstest.boot +21 gatecontrol.boot +22 etherload.boot +24 neptune.boot +25 what.boot +40 pupwatch.boot + +# past this point are more esoteric things, here because they're cool +100 mesanetexec.boot +200 cedarnetexec.boot +1000 showais.boot +1001 starwars.boot +1002 fly.boot +1003 kal.boot +1004 pinball.boot +1005 pool.boot +1006 mazewar.boot +1007 trek.boot +1010 invaders.boot +1011 astroroids.boot +1012 astroroids.boot +1013 clock.boot +1014 galaxian.boot +1015 ppong.boot +1017 messenger.boot +1020 reversi.boot +1023 missilecommand.boot +40000 boggs.boot +40002 murray.boot +40003 johnsson.boot + +43000 alphamesamesanetexec.boot + + + + diff --git a/PUP/Configuration.cs b/PUP/Configuration.cs index 14b185f..7b8dd7a 100644 --- a/PUP/Configuration.cs +++ b/PUP/Configuration.cs @@ -10,10 +10,10 @@ namespace IFS /// 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 { - /// /// The root directory for the FTP file store. /// @@ -24,5 +24,10 @@ namespace IFS /// public static readonly string CopyDiskRoot = "C:\\ifs\\copydisk"; + /// + /// The root directory for the Boot file store. + /// + public static readonly string BootRoot = "C:\\ifs\\boot"; + } } diff --git a/PUP/EFTP/EFTPChannel.cs b/PUP/EFTP/EFTPChannel.cs new file mode 100644 index 0000000..4af01bf --- /dev/null +++ b/PUP/EFTP/EFTPChannel.cs @@ -0,0 +1,206 @@ +using IFS.Logging; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; + +namespace IFS.EFTP +{ + /// + /// Represents an open EFTP channel and provides methods for sending data to it. + /// (Receiving is not yet implemented, the current implementation exists only to support Boot File requests.) + /// + public class EFTPChannel + { + public EFTPChannel(PUPPort destination, UInt32 socketID) + { + _clientConnectionPort = destination; + + _outputAckEvent = new AutoResetEvent(false); + + // We create our connection port using a unique socket address. + _serverConnectionPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, socketID); + + _outputQueue = new Queue(65536); + _sendPos = 0; + } + + public PUPPort ServerPort + { + get { return _serverConnectionPort; } + } + + public delegate void DestroyDelegate(); + + public DestroyDelegate OnDestroy; + + public void Destroy() + { + if (OnDestroy != null) + { + OnDestroy(); + } + } + + /// + /// Sends data to the channel (i.e. to the client). Will block (waiting for an ACK) if an ACK is requested. + /// + /// The data to be sent + /// Whether to flush data out immediately or to wait for enough for a full PUP first. + public void Send(byte[] data, int length, bool flush) + { + if (length > data.Length) + { + throw new InvalidOperationException("Length must be less than or equal to the size of data."); + } + + // Add output data to output queue. + // Again, this is really inefficient. + for (int i = 0; i < length; i++) + { + _outputQueue.Enqueue(data[i]); + } + + if (flush || _outputQueue.Count >= PUP.MAX_PUP_SIZE) + { + // Send data until all is used (for a flush) or until we have less than a full PUP (non-flush). + while (_outputQueue.Count >= (flush ? 1 : PUP.MAX_PUP_SIZE)) + { + byte[] chunk = new byte[Math.Min(PUP.MAX_PUP_SIZE, _outputQueue.Count)]; + + // Ugh. + for (int i = 0; i < chunk.Length; i++) + { + chunk[i] = _outputQueue.Dequeue(); + } + + while (true) + { + // Send the data. + PUP dataPup = new PUP(PupType.EFTPData, _sendPos, _clientConnectionPort, _serverConnectionPort, chunk); + + PUPProtocolDispatcher.Instance.SendPup(dataPup); + + // Await an ACK. We will retry several times and resend as necessary. + int retry = 0; + for (retry = 0; retry < EFTPRetryCount; retry++) + { + if (_outputAckEvent.WaitOne(EFTPAckTimeoutPeriod)) + { + // done, we got our ACK. + break; + } + else + { + // timeout: resend the PUP and wait for an ACK again. + PUPProtocolDispatcher.Instance.SendPup(dataPup); + } + } + + if (retry >= EFTPRetryCount) + { + Log.Write(LogType.Error, LogComponent.EFTP, "Timeout waiting for ACK, aborting connection."); + SendAbort("Client unresponsive."); + EFTPManager.DestroyChannel(this); + } + + if (_lastRecvPos == _sendPos) + { + // The client is in sync with us, we are done with this packet. + break; + } + else if (_sendPos - _lastRecvPos > 1) + { + // We lost more than one packet, something is very broken. + Log.Write(LogType.Error, LogComponent.EFTP, "Client lost more than one packet, connection is broken. Aborting."); + SendAbort("Client lost too much data."); + EFTPManager.DestroyChannel(this); + } + else + { + // We lost one packet, move back and send it again. + Log.Write(LogType.Warning, LogComponent.EFTP, "Client lost a packet, resending."); + } + + } + + // Move to next packet. + _sendPos++; + } + } + } + + public void SendEnd() + { + PUP endPup = new PUP(PupType.EFTPEnd, _sendPos, _clientConnectionPort, _serverConnectionPort, new byte[0]); + PUPProtocolDispatcher.Instance.SendPup(endPup); + + // Await an ack + _outputAckEvent.WaitOne(EFTPAckTimeoutPeriod); + + _sendPos++; + + // Send another end to close things off. + endPup = new PUP(PupType.EFTPEnd, _sendPos, _clientConnectionPort, _serverConnectionPort, new byte[0]); + PUPProtocolDispatcher.Instance.SendPup(endPup); + } + + public void RecvData(PUP p) + { + // For now, receive is not implemented. + throw new NotImplementedException(); + } + + public void RecvAck(PUP p) + { + // + // Sanity check that the client's position matches ours. + // + _lastRecvPos = p.ID; + if (_lastRecvPos != _sendPos) + { + Log.Write(LogType.Error, LogComponent.EFTP, "Client position does not match server ({0} != {1}", + _lastRecvPos, _sendPos); + } + + // + // Unblock those waiting for an ACK. + // + _outputAckEvent.Set(); + } + + + public void End(PUP p) + { + + } + + private void SendAbort(string message) + { + /* + PUP abortPup = new PUP(PupType.EFTPAbort, _sendPos, _clientConnectionPort, _serverConnectionPort, Helpers.StringToArray(message)); + + // + // Send this directly, do not wait for the client to be ready (since it may be wedged, and we don't expect anyone to actually notice + // this anyway). + // + PUPProtocolDispatcher.Instance.SendPup(abortPup); + */ + } + + private PUPPort _clientConnectionPort; + private PUPPort _serverConnectionPort; + + private uint _sendPos; + private uint _lastRecvPos; + private Queue _outputQueue; + private AutoResetEvent _outputAckEvent; + + + // Timeouts and retries + private const int EFTPRetryCount = 5; + private const int EFTPAckTimeoutPeriod = 1000; // 1 second + } +} diff --git a/PUP/EFTP/EFTPManager.cs b/PUP/EFTP/EFTPManager.cs new file mode 100644 index 0000000..c111219 --- /dev/null +++ b/PUP/EFTP/EFTPManager.cs @@ -0,0 +1,164 @@ +using IFS.BSP; +using IFS.Logging; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.IO; + +namespace IFS.EFTP +{ + /// + /// EFTP: It's like a really limited version of BSP. + /// This is not a standalone server like FTP but provides routines for sending / receiving data + /// via EFTP, so that actual servers (Boot, Printing, etc.) can serve their clients. + /// + public static class EFTPManager + { + static EFTPManager() + { + // + // Initialize the socket ID counter; we start with a + // number beyond the range of well-defined sockets. + // For each new EFTP channel that gets opened, we will + // increment this counter to ensure that each channel gets + // a unique ID. (Well, until we wrap around...) + // + _nextSocketID = _startingSocketID; + + _activeChannels = new Dictionary(); + + } + public static void SendFile(PUPPort destination, Stream data) + { + UInt32 socketID = GetNextSocketID(); + EFTPChannel newChannel = new EFTPChannel(destination, socketID); + _activeChannels.Add(socketID, newChannel); + + EFTPSend.StartSend(newChannel, data); + } + + public static void RecvData(PUP p) + { + EFTPChannel channel = FindChannelForPup(p); + + if (channel == null) + { + Log.Write(LogType.Error, LogComponent.EFTP, "Received EFTP PUP on an unconnected socket, ignoring."); + return; + } + + switch (p.Type) + { + + case PupType.EFTPData: + { + channel.RecvData(p); + } + break; + + case PupType.EFTPAck: + { + channel.RecvAck(p); + } + break; + + case PupType.EFTPEnd: + { + channel.End(p); + } + break; + + case PupType.EFTPAbort: + { + string abortMessage = Helpers.ArrayToString(p.Contents); + Log.Write(LogType.Warning, LogComponent.RTP, String.Format("EFTP aborted, message: '{0}'", abortMessage)); + + DestroyChannel(channel); + } + break; + + default: + throw new NotImplementedException(String.Format("Unhandled EFTP PUP type {0}.", p.Type)); + + } + } + + /// + /// Indicates whether a channel exists for the socket specified by the given PUP. + /// + /// + /// + public static bool ChannelExistsForSocket(PUP p) + { + return FindChannelForPup(p) != null; + } + + /// + /// Destroys and unregisters the specified channel. + /// + /// + public static void DestroyChannel(EFTPChannel channel) + { + channel.Destroy(); + + _activeChannels.Remove(channel.ServerPort.Socket); + } + + /// + /// Finds the appropriate channel for the given PUP. + /// + /// + /// + private static EFTPChannel FindChannelForPup(PUP p) + { + if (_activeChannels.ContainsKey(p.DestinationPort.Socket)) + { + return _activeChannels[p.DestinationPort.Socket]; + } + else + { + return null; + } + } + + /// + /// Generates a unique Socket ID. We count downward from the max value + /// (whereas BSP counts upwards) so as to avoid any duplicates. + /// TODO: this could be done much more sanely via a centralized ID factory... + /// + /// + private static UInt32 GetNextSocketID() + { + UInt32 next = _nextSocketID; + + _nextSocketID--; + + // + // Handle the wrap around case (which we're very unlikely to + // ever hit, but why not do the right thing). + // Start over at the initial ID. This is very unlikely to + // collide with any pending channels. + // + if (_nextSocketID < 0x1000) + { + _nextSocketID = _startingSocketID; + } + + return next; + } + + + /// + /// Map from socket address to BSP channel + /// + private static Dictionary _activeChannels; + + private static UInt32 _nextSocketID; + private static readonly UInt32 _startingSocketID = UInt32.MaxValue; + } + + +} diff --git a/PUP/EFTP/EFTPSend.cs b/PUP/EFTP/EFTPSend.cs new file mode 100644 index 0000000..59dd1a2 --- /dev/null +++ b/PUP/EFTP/EFTPSend.cs @@ -0,0 +1,65 @@ +using IFS.Logging; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Text; +using System.Threading; + +namespace IFS.EFTP +{ + public class EFTPSend + { + + public static void StartSend(EFTPChannel channel, Stream data) + { + EFTPSend newSender = new EFTPSend(channel, data); + + } + + private EFTPSend(EFTPChannel channel, Stream data) + { + _data = data; + _channel = channel; + + _workerThread = new Thread(SendWorker); + _workerThread.Start(); + + channel.OnDestroy += OnChannelDestroyed; + } + + private void SendWorker() + { + byte[] block = new byte[512]; + while(true) + { + // + // Send the file 512 bytes at a time to the channel, and then finish. + // + int read = _data.Read(block, 0, block.Length); + _channel.Send(block, read, true); + + Log.Write(LogType.Verbose, LogComponent.EFTP, "Sent data, position is now {0}", _data.Position); + + if (read != block.Length) + { + _channel.SendEnd(); + break; + } + } + _data.Close(); + } + + private void OnChannelDestroyed() + { + _workerThread.Abort(); + _data.Close(); + } + + private Thread _workerThread; + + private EFTPChannel _channel; + private Stream _data; + } +} diff --git a/PUP/EFTP/EFTPServer.cs b/PUP/EFTP/EFTPServer.cs deleted file mode 100644 index a2bda93..0000000 --- a/PUP/EFTP/EFTPServer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using IFS.BSP; -using IFS.Logging; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.IO; - -namespace IFS.EFTP -{ - /// - /// EFTP: It's like a really limited version of BSP. - /// This is not a standalone server like FTP but provides routines for sending / receiving data - /// via FTP, so that actual servers (Boot, Printing, etc.) can serve their clients. - /// - public class EFTPServer : PUPProtocolBase - { - /// - /// Called by dispatcher to send incoming data destined for this protocol. - /// - /// - public override void RecvData(PUP p) - { - - } - } - - public class EFTPWorker - { - public EFTPWorker(BSPChannel channel) - { - - } - } -} diff --git a/PUP/Entrypoint.cs b/PUP/Entrypoint.cs index f8a7578..e48ec3c 100644 --- a/PUP/Entrypoint.cs +++ b/PUP/Entrypoint.cs @@ -27,8 +27,7 @@ namespace IFS // 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("Boot", 0x10, ConnectionType.Connectionless, new BootServerProtocol())); + 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())); diff --git a/PUP/FTP/FTPServer.cs b/PUP/FTP/FTPServer.cs index 20d2eec..046c077 100644 --- a/PUP/FTP/FTPServer.cs +++ b/PUP/FTP/FTPServer.cs @@ -172,6 +172,7 @@ namespace IFS.FTP break; case FTPCommand.Enumerate: + case FTPCommand.NewEnumerate: { // Argument to Enumerate is a property list (string). // @@ -180,7 +181,7 @@ namespace IFS.FTP PropertyList pl = new PropertyList(fileSpec); - EnumerateFiles(pl); + EnumerateFiles(pl, command == FTPCommand.NewEnumerate); } break; @@ -318,7 +319,7 @@ namespace IFS.FTP /// Enumerates the files matching the requested file specification. /// /// - private void EnumerateFiles(PropertyList fileSpec) + private void EnumerateFiles(PropertyList fileSpec, bool newEnumerate) { string fullPath = BuildAndValidateFilePath(fileSpec); @@ -329,11 +330,29 @@ namespace IFS.FTP List files = EnumerateFiles(fullPath); - // Send each property list to the user - foreach(PropertyList matchingFile in files) - { + + if (newEnumerate) + { + // "New" enumerate: send all property lists concatenated together in a single Here-Is-Property-List + // command. + StringBuilder sb = new StringBuilder(); + foreach (PropertyList matchingFile in files) + { + sb.Append(matchingFile.ToString()); + } + _channel.SendMark((byte)FTPCommand.HereIsPropertyList, false); - _channel.Send(Helpers.StringToArray(matchingFile.ToString())); + _channel.Send(Helpers.StringToArray(sb.ToString())); + } + else + { + // "Old" enumrate: send each property list to the user in separate Here-Is-Property-List + // command. + foreach (PropertyList matchingFile in files) + { + _channel.SendMark((byte)FTPCommand.HereIsPropertyList, false); + _channel.Send(Helpers.StringToArray(matchingFile.ToString())); + } } // End the enumeration. diff --git a/PUP/IFS.csproj b/PUP/IFS.csproj index a8bbbef..b969f0d 100644 --- a/PUP/IFS.csproj +++ b/PUP/IFS.csproj @@ -82,7 +82,9 @@ - + + + @@ -98,6 +100,9 @@ + + PreserveNewest + PreserveNewest diff --git a/PUP/Logging/Log.cs b/PUP/Logging/Log.cs index 824caf5..d01eebc 100644 --- a/PUP/Logging/Log.cs +++ b/PUP/Logging/Log.cs @@ -21,6 +21,8 @@ namespace IFS.Logging PUP = 0x40, FTP = 0x80, BreathOfLife = 0x100, + EFTP = 0x200, + BootServer = 0x400, All = 0x7fffffff } diff --git a/PUP/MiscServicesProtocol.cs b/PUP/MiscServicesProtocol.cs index 20998f0..8dabb91 100644 --- a/PUP/MiscServicesProtocol.cs +++ b/PUP/MiscServicesProtocol.cs @@ -1,6 +1,10 @@ -using IFS.Logging; +using IFS.Boot; +using IFS.EFTP; +using IFS.Logging; + using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -80,7 +84,11 @@ namespace IFS case PupType.SendBootFileRequest: SendBootFile(p); - break; + break; + + case PupType.BootDirectoryRequest: + SendBootDirectory(p); + break; default: Log.Write(LogComponent.MiscServices, String.Format("Unhandled misc. protocol {0}", p.Type)); @@ -182,7 +190,7 @@ namespace IFS // NOTE: This is *not* a BCPL string, just the raw characters. byte[] interNetworkName = Helpers.StringToArray(hostName); - PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP lookupReply = new PUP(PupType.AddressLookupResponse, p.ID, p.SourcePort, localPort, interNetworkName); PUPProtocolDispatcher.Instance.SendPup(lookupReply); @@ -191,7 +199,7 @@ namespace IFS { // Unknown host, send an error reply string errorString = "Unknown host."; - PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, Helpers.StringToArray(errorString)); PUPProtocolDispatcher.Instance.SendPup(errorReply); @@ -222,7 +230,7 @@ namespace IFS { // 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); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP lookupReply = new PUP(PupType.NameLookupResponse, p.ID, p.SourcePort, localPort, lookupPort.ToArray()); PUPProtocolDispatcher.Instance.SendPup(lookupReply); @@ -231,7 +239,7 @@ namespace IFS { // Unknown host, send an error reply string errorString = "Unknown host."; - PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, Helpers.StringToArray(errorString)); PUPProtocolDispatcher.Instance.SendPup(errorReply); @@ -244,11 +252,79 @@ namespace IFS // The request PUP contains the file number in the lower-order 16-bits of the pup ID. // Assuming the number is a valid bootfile, we start sending it to the client's port via EFTP. // - uint fileNumber = p.ID & 0xffff; + ushort fileNumber = (ushort)p.ID; Log.Write(LogType.Verbose, LogComponent.MiscServices, "Boot file request is for file {0}.", fileNumber); + FileStream bootFile = BootServer.GetStreamForNumber(fileNumber); + + if (bootFile == null) + { + Log.Write(LogType.Warning, LogComponent.MiscServices, "Boot file {0} does not exist or could not be opened.", fileNumber); + } + else + { + // Send the file. + EFTPManager.SendFile(p.SourcePort, bootFile); + } + } + + private void SendBootDirectory(PUP p) + { + // + // From etherboot.bravo + // "Pup ID: if it is in reply to a BootDirRequest, the ID should match the request. + // Pup Contents: 1 or more blocks of the following format: A boot file number (the number that goes in the low 16 bits of a + // BootFileRequest Pup), an Alto format date (2 words), a boot file name in BCPL string format." + // + MemoryStream ms = new MemoryStream(PUP.MAX_PUP_SIZE); + + List bootFiles = BootServer.EnumerateBootFiles(); + + foreach(BootFileEntry entry in bootFiles) + { + BootDirectoryBlock block; + block.FileNumber = entry.BootNumber; + block.FileDate = 0; + block.FileName = new BCPLString(entry.Filename); + + byte[] serialized = Serializer.Serialize(block); + + // + // If this block fits into the current PUP, add it to the stream, otherwise send off the current PUP + // and start a new one. + // + if (serialized.Length + ms.Length <= PUP.MAX_PUP_SIZE) + { + ms.Write(serialized, 0, serialized.Length); + } + else + { + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); + PUP bootDirReply = new PUP(PupType.BootDirectoryReply, p.ID, p.SourcePort, localPort, ms.ToArray()); + PUPProtocolDispatcher.Instance.SendPup(bootDirReply); + + ms.Seek(0, SeekOrigin.Begin); + ms.SetLength(0); + } + } + + // Shuffle out any remaining data. + if (ms.Length > 0) + { + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); + PUP bootDirReply = new PUP(PupType.BootDirectoryReply, p.ID, p.SourcePort, localPort, ms.ToArray()); + PUPProtocolDispatcher.Instance.SendPup(bootDirReply); + } } + + private struct BootDirectoryBlock + { + public ushort FileNumber; + public UInt32 FileDate; + public BCPLString FileName; + } + } } diff --git a/PUP/PUP.cs b/PUP/PUP.cs index e0ed88b..e427e3c 100644 --- a/PUP/PUP.cs +++ b/PUP/PUP.cs @@ -63,8 +63,8 @@ namespace IFS // Alto Boot SendBootFileRequest = 164, - BootDirectoryRequest = 165, - BootDirectoryReply = 166, + BootDirectoryRequest = 175, + BootDirectoryReply = 176, // User authentication AuthenticateRequest = 168, diff --git a/PUP/PUPProtocolDispatcher.cs b/PUP/PUPProtocolDispatcher.cs index 5eeacca..98b866e 100644 --- a/PUP/PUPProtocolDispatcher.cs +++ b/PUP/PUPProtocolDispatcher.cs @@ -1,4 +1,5 @@ using IFS.BSP; +using IFS.EFTP; using IFS.Logging; using IFS.Transport; @@ -61,8 +62,15 @@ namespace IFS } public void SendPup(PUP p) - { - _pupPacketInterface.Send(p); + { + // drop every 10th packet for testing + _packet++; + + // if ((_packet % 10) != 5) + { + _pupPacketInterface.Send(p); + } + } public void Send(byte[] data, byte source, byte destination, ushort frameType) @@ -71,7 +79,7 @@ namespace IFS { _rawPacketInterface.Send(data, source, destination, frameType); } - } + } private void OnPupReceived(PUP pup) { @@ -115,10 +123,14 @@ namespace IFS // An established BSP channel, send data to it. BSPManager.RecvData(pup); } + else if (EFTPManager.ChannelExistsForSocket(pup)) + { + EFTPManager.RecvData(pup); + } else { // Not a protocol we handle; log it. - Log.Write(LogType.Normal, LogComponent.PUP, "Unhandled PUP protocol, socket {0}, dropped packet.", pup.DestinationPort.Socket); + Log.Write(LogType.Normal, LogComponent.PUP, "Unhandled PUP protocol, source socket {0}, destination socket {1}, type {2}, dropped packet.", pup.SourcePort.Socket, pup.DestinationPort.Socket, pup.Type); } } @@ -137,6 +149,8 @@ namespace IFS /// private Dictionary _dispatchMap; + private int _packet; + private static PUPProtocolDispatcher _instance = new PUPProtocolDispatcher(); } }