mirror of
https://github.com/livingcomputermuseum/IFS.git
synced 2026-01-13 15:27:25 +00:00
Fixed BSP issue on flaky nets, implemented EFTP and Boot services.
This commit is contained in:
parent
39a32469d3
commit
1d90b5e8ae
@ -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++;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BootServer provides encapsulation of boot file mappings and enumerations.
|
||||
/// </summary>
|
||||
public static class BootServer
|
||||
{
|
||||
static BootServer()
|
||||
{
|
||||
LoadBootFileTables();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by dispatcher to send incoming data destined for this protocol
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
public override void RecvData(PUP p)
|
||||
private static void LoadBootFileTables()
|
||||
{
|
||||
_numberToNameTable = new Dictionary<ushort, string>();
|
||||
|
||||
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<BootFileEntry> EnumerateBootFiles()
|
||||
{
|
||||
List<BootFileEntry> bootFiles = new List<BootFileEntry>();
|
||||
|
||||
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<ushort, string> _numberToNameTable;
|
||||
}
|
||||
}
|
||||
|
||||
64
PUP/Conf/bootdirectory.txt
Normal file
64
PUP/Conf/bootdirectory.txt
Normal file
@ -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:
|
||||
# <number(8)> <name>
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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).
|
||||
/// </summary>
|
||||
public static class Configuration
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The root directory for the FTP file store.
|
||||
/// </summary>
|
||||
@ -24,5 +24,10 @@ namespace IFS
|
||||
/// </summary>
|
||||
public static readonly string CopyDiskRoot = "C:\\ifs\\copydisk";
|
||||
|
||||
/// <summary>
|
||||
/// The root directory for the Boot file store.
|
||||
/// </summary>
|
||||
public static readonly string BootRoot = "C:\\ifs\\boot";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
206
PUP/EFTP/EFTPChannel.cs
Normal file
206
PUP/EFTP/EFTPChannel.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.)
|
||||
/// </summary>
|
||||
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<byte>(65536);
|
||||
_sendPos = 0;
|
||||
}
|
||||
|
||||
public PUPPort ServerPort
|
||||
{
|
||||
get { return _serverConnectionPort; }
|
||||
}
|
||||
|
||||
public delegate void DestroyDelegate();
|
||||
|
||||
public DestroyDelegate OnDestroy;
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (OnDestroy != null)
|
||||
{
|
||||
OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends data to the channel (i.e. to the client). Will block (waiting for an ACK) if an ACK is requested.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to be sent</param>
|
||||
/// <param name="flush">Whether to flush data out immediately or to wait for enough for a full PUP first.</param>
|
||||
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<byte> _outputQueue;
|
||||
private AutoResetEvent _outputAckEvent;
|
||||
|
||||
|
||||
// Timeouts and retries
|
||||
private const int EFTPRetryCount = 5;
|
||||
private const int EFTPAckTimeoutPeriod = 1000; // 1 second
|
||||
}
|
||||
}
|
||||
164
PUP/EFTP/EFTPManager.cs
Normal file
164
PUP/EFTP/EFTPManager.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<uint, EFTPChannel>();
|
||||
|
||||
}
|
||||
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));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether a channel exists for the socket specified by the given PUP.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
/// <returns></returns>
|
||||
public static bool ChannelExistsForSocket(PUP p)
|
||||
{
|
||||
return FindChannelForPup(p) != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys and unregisters the specified channel.
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
public static void DestroyChannel(EFTPChannel channel)
|
||||
{
|
||||
channel.Destroy();
|
||||
|
||||
_activeChannels.Remove(channel.ServerPort.Socket);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the appropriate channel for the given PUP.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
/// <returns></returns>
|
||||
private static EFTPChannel FindChannelForPup(PUP p)
|
||||
{
|
||||
if (_activeChannels.ContainsKey(p.DestinationPort.Socket))
|
||||
{
|
||||
return _activeChannels[p.DestinationPort.Socket];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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...
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map from socket address to BSP channel
|
||||
/// </summary>
|
||||
private static Dictionary<UInt32, EFTPChannel> _activeChannels;
|
||||
|
||||
private static UInt32 _nextSocketID;
|
||||
private static readonly UInt32 _startingSocketID = UInt32.MaxValue;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
65
PUP/EFTP/EFTPSend.cs
Normal file
65
PUP/EFTP/EFTPSend.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class EFTPServer : PUPProtocolBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by dispatcher to send incoming data destined for this protocol.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
public override void RecvData(PUP p)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class EFTPWorker
|
||||
{
|
||||
public EFTPWorker(BSPChannel channel)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()));
|
||||
|
||||
@ -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.
|
||||
/// </summary>
|
||||
/// <param name="fileSpec"></param>
|
||||
private void EnumerateFiles(PropertyList fileSpec)
|
||||
private void EnumerateFiles(PropertyList fileSpec, bool newEnumerate)
|
||||
{
|
||||
string fullPath = BuildAndValidateFilePath(fileSpec);
|
||||
|
||||
@ -329,11 +330,29 @@ namespace IFS.FTP
|
||||
|
||||
List<PropertyList> 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.
|
||||
|
||||
@ -82,7 +82,9 @@
|
||||
<Compile Include="CopyDisk\DiabloPack.cs" />
|
||||
<Compile Include="DirectoryServices.cs" />
|
||||
<Compile Include="EchoProtocol.cs" />
|
||||
<Compile Include="EFTP\EFTPServer.cs" />
|
||||
<Compile Include="EFTP\EFTPChannel.cs" />
|
||||
<Compile Include="EFTP\EFTPManager.cs" />
|
||||
<Compile Include="EFTP\EFTPSend.cs" />
|
||||
<Compile Include="Entrypoint.cs" />
|
||||
<Compile Include="FTP\FTPServer.cs" />
|
||||
<Compile Include="FTP\PropertyList.cs" />
|
||||
@ -98,6 +100,9 @@
|
||||
<Compile Include="Transport\PacketInterface.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Conf\bootdirectory.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Conf\hosts.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
@ -21,6 +21,8 @@ namespace IFS.Logging
|
||||
PUP = 0x40,
|
||||
FTP = 0x80,
|
||||
BreathOfLife = 0x100,
|
||||
EFTP = 0x200,
|
||||
BootServer = 0x400,
|
||||
|
||||
All = 0x7fffffff
|
||||
}
|
||||
|
||||
@ -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<BootFileEntry> 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,8 +63,8 @@ namespace IFS
|
||||
|
||||
// Alto Boot
|
||||
SendBootFileRequest = 164,
|
||||
BootDirectoryRequest = 165,
|
||||
BootDirectoryReply = 166,
|
||||
BootDirectoryRequest = 175,
|
||||
BootDirectoryReply = 176,
|
||||
|
||||
// User authentication
|
||||
AuthenticateRequest = 168,
|
||||
|
||||
@ -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
|
||||
/// </summary>
|
||||
private Dictionary<UInt32, PUPProtocolEntry> _dispatchMap;
|
||||
|
||||
private int _packet;
|
||||
|
||||
private static PUPProtocolDispatcher _instance = new PUPProtocolDispatcher();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user