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();
}
}