mirror of
https://github.com/livingcomputermuseum/IFS.git
synced 2026-01-11 23:53:09 +00:00
Basic FTP implementation, cleaned up BSP implementation somewhat.
This commit is contained in:
parent
e83a60167d
commit
602de14881
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using IFS.BSP;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
799
PUP/BSP/BSPChannel.cs
Normal file
799
PUP/BSP/BSPChannel.cs
Normal file
@ -0,0 +1,799 @@
|
||||
using IFS.Logging;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IFS.BSP
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functionality for maintaining/terminating BSP connections, and the transfer of data
|
||||
/// across said connection.
|
||||
///
|
||||
/// Implementation currenty provides (apparently) proper "windows" for sending data to client;
|
||||
/// only one PUP at a time is accepted for input at the moment. This should likely be corrected,
|
||||
/// but is not likely to improve performance altogether that much.
|
||||
/// </summary>
|
||||
public class BSPChannel
|
||||
{
|
||||
public BSPChannel(PUP rfcPup, UInt32 socketID, BSPProtocol protocolHandler)
|
||||
{
|
||||
_inputLock = new ReaderWriterLockSlim();
|
||||
_outputLock = new ReaderWriterLockSlim();
|
||||
|
||||
_inputWriteEvent = new AutoResetEvent(false);
|
||||
_inputQueue = new Queue<ushort>(65536);
|
||||
|
||||
_outputAckEvent = new AutoResetEvent(false);
|
||||
_outputReadyEvent = new AutoResetEvent(false);
|
||||
_dataReadyEvent = new AutoResetEvent(false);
|
||||
_outputQueue = new Queue<byte>(65536);
|
||||
|
||||
_outputWindow = new List<PUP>(16);
|
||||
_outputWindowLock = new ReaderWriterLockSlim();
|
||||
|
||||
_protocolHandler = protocolHandler;
|
||||
|
||||
// Init IDs, etc. based on RFC PUP
|
||||
_lastClientRecvPos = _startPos = _recvPos = _sendPos = rfcPup.ID;
|
||||
|
||||
// Set up socket addresses.
|
||||
// The client sends the connection port it prefers to use
|
||||
// in the RFC pup.
|
||||
_clientConnectionPort = new PUPPort(rfcPup.Contents, 0);
|
||||
|
||||
// If the client doesn't know what network it's on, it's now on ours.
|
||||
if (_clientConnectionPort.Network == 0)
|
||||
{
|
||||
_clientConnectionPort.Network = DirectoryServices.Instance.LocalNetwork;
|
||||
}
|
||||
|
||||
// We create our connection port using a unique socket address.
|
||||
_serverConnectionPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, socketID);
|
||||
|
||||
//
|
||||
// Init MaxPups to indicate that we need to find out what the client actually supports when we first
|
||||
// start sending data.
|
||||
//
|
||||
_clientLimits.MaxPups = 0xffff;
|
||||
|
||||
// Create our consumer thread for output and kick it off.
|
||||
_consumerThread = new Thread(OutputConsumerThread);
|
||||
_consumerThread.Start();
|
||||
}
|
||||
|
||||
public delegate void DestroyDelegate();
|
||||
|
||||
public DestroyDelegate OnDestroy;
|
||||
|
||||
/// <summary>
|
||||
/// The port we use to talk to the client.
|
||||
/// </summary>
|
||||
public PUPPort ClientPort
|
||||
{
|
||||
get { return _clientConnectionPort; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The port the client uses to talk to us.
|
||||
/// </summary>
|
||||
public PUPPort ServerPort
|
||||
{
|
||||
get { return _serverConnectionPort; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last Mark byte received, if any.
|
||||
/// </summary>
|
||||
public byte LastMark
|
||||
{
|
||||
get { return _lastMark; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs cleanup on this channel and notifies anyone who's interested that
|
||||
/// the channel has been destroyed.
|
||||
/// </summary>
|
||||
public void Destroy()
|
||||
{
|
||||
_consumerThread.Abort();
|
||||
|
||||
if (OnDestroy != null)
|
||||
{
|
||||
OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an End request from the client.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
public void End(PUP p)
|
||||
{
|
||||
PUP endReplyPup = new PUP(PupType.EndReply, p.ID, _clientConnectionPort, _serverConnectionPort, new byte[0]);
|
||||
PUPProtocolDispatcher.Instance.SendPup(endReplyPup);
|
||||
|
||||
// "The receiver of the End PUP responds by returning an EndReply Pup with matching ID and then
|
||||
// _dallying_ up to some reasonably long timeout interval (say, 10 seconds) in order to respond to
|
||||
// a retransmitted End Pup should its initial EndReply be lost. If and when the dallying end of the
|
||||
// stream connection receives its EndReply, it may immediately self destruct."
|
||||
// TODO: actually make this happen...
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the channel (i.e. from the client). Will block if not all the requested data is available.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int Read(ref byte[] data, int count)
|
||||
{
|
||||
return Read(ref data, count, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the channel (i.e. from the client). Will block if not all the requested data is available.
|
||||
/// If a Mark byte is encountered, will return a short read.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int Read(ref byte[] data, int count, int offset)
|
||||
{
|
||||
// sanity check
|
||||
if (count + offset > data.Length)
|
||||
{
|
||||
throw new InvalidOperationException("count + offset must be less than or equal to the length of the buffer being read into.");
|
||||
}
|
||||
|
||||
int read = 0;
|
||||
|
||||
//
|
||||
// Loop until either:
|
||||
// - all the data we asked for arrives
|
||||
// - we get a Mark byte
|
||||
// - we time out waiting for data
|
||||
//
|
||||
bool done = false;
|
||||
while (!done)
|
||||
{
|
||||
_inputLock.EnterUpgradeableReadLock();
|
||||
if (_inputQueue.Count > 0)
|
||||
{
|
||||
_inputLock.EnterWriteLock();
|
||||
|
||||
// We have some data right now, read it in.
|
||||
// TODO: this code is ugly and it wants to die.
|
||||
while (_inputQueue.Count > 0 && read < count)
|
||||
{
|
||||
ushort word = _inputQueue.Dequeue();
|
||||
|
||||
// Is this a mark or a data byte?
|
||||
if (word < 0x100)
|
||||
{
|
||||
// Data, place in data stream
|
||||
data[read + offset] = (byte)word;
|
||||
read++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mark. Set last mark and exit.
|
||||
_lastMark = (byte)(word >> 8);
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (read >= count)
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
|
||||
_inputLock.ExitWriteLock();
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
|
||||
// No data in the queue.
|
||||
// Wait until we have received more data, then try again.
|
||||
if (!_inputWriteEvent.WaitOne(BSPReadTimeoutPeriod))
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Timed out waiting for data on read, aborting connection.");
|
||||
// We timed out waiting for data, abort the connection.
|
||||
SendAbort("Timeout on read.");
|
||||
BSPManager.DestroyChannel(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single byte from the channel. Will block if no data is available.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public byte ReadByte()
|
||||
{
|
||||
// TODO: optimize this
|
||||
byte[] data = new byte[1];
|
||||
|
||||
Read(ref data, 1);
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single 16-bit word from the channel. Will block if no data is available.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ushort ReadUShort()
|
||||
{
|
||||
// TODO: optimize this
|
||||
byte[] data = new byte[2];
|
||||
|
||||
Read(ref data, 2);
|
||||
|
||||
return Helpers.ReadUShort(data, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single BCPL string from the channel. Will block as necessary.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public BCPLString ReadBCPLString()
|
||||
{
|
||||
return new BCPLString(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the queue until a Mark byte is received.
|
||||
/// The mark byte is returned (LastMark is also set.) Any data
|
||||
/// between the current position and the Mark read is discarded.
|
||||
///
|
||||
/// This will block until the next Mark is found.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public byte WaitForMark()
|
||||
{
|
||||
byte mark = 0;
|
||||
|
||||
// This data is discarded. The length is arbitrary.
|
||||
byte[] dummyData = new byte[512];
|
||||
|
||||
while (true)
|
||||
{
|
||||
int read = Read(ref dummyData, dummyData.Length);
|
||||
|
||||
// Short read, indicating a Mark.
|
||||
if (read < dummyData.Length)
|
||||
{
|
||||
mark = _lastMark;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return mark;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends incoming client data or Marks into the input queue (called from BSPManager to place new PUP data into the BSP stream.)
|
||||
/// </summary>
|
||||
public void RecvWriteQueue(PUP dataPUP)
|
||||
{
|
||||
//
|
||||
// Sanity check: If this is a Mark PUP, the contents must only be one byte in length.
|
||||
//
|
||||
bool markPup = dataPUP.Type == PupType.AMark || dataPUP.Type == PupType.Mark;
|
||||
if (markPup)
|
||||
{
|
||||
if (dataPUP.Contents.Length != 1)
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Mark PUP must be 1 byte in length.");
|
||||
|
||||
SendAbort("Mark PUP must be 1 byte in length.");
|
||||
BSPManager.DestroyChannel(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are over our high watermark, we will drop the data (and not send an ACK even if requested).
|
||||
// Clients should be honoring the limits we set in the RFC packets.
|
||||
_inputLock.EnterUpgradeableReadLock();
|
||||
|
||||
/*
|
||||
if (_inputQueue.Count + dataPUP.Contents.Length > MaxBytes)
|
||||
{
|
||||
Log.Write(LogLevel.Error, "Queue larger than {0} bytes, dropping.");
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
return;
|
||||
} */
|
||||
|
||||
// Sanity check on expected position from sender vs. received data on our end.
|
||||
// If they don't match then we've lost a packet somewhere.
|
||||
if (dataPUP.ID != _recvPos)
|
||||
{
|
||||
// Current behavior is to simply drop all incoming PUPs (and not ACK them) until they are re-sent to us
|
||||
// (in which case the above sanity check will pass). According to spec, AData requests that are not ACKed
|
||||
// must eventually be resent. This is far simpler than accepting out-of-order data and keeping track
|
||||
// of where it goes in the queue, though less efficient.
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Lost Packet, client ID does not match our receive position ({0} != {1})", dataPUP.ID, _recvPos);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Prepare to add data to the queue
|
||||
_inputLock.EnterWriteLock();
|
||||
|
||||
if (markPup)
|
||||
{
|
||||
//
|
||||
// For mark pups, the data goes in the high byte of the word
|
||||
// so that it can be identified as a mark when it's read back.
|
||||
_inputQueue.Enqueue((ushort)(dataPUP.Contents[0] << 8));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Again, this is really inefficient
|
||||
for (int i = 0; i < dataPUP.Contents.Length; i++)
|
||||
{
|
||||
_inputQueue.Enqueue(dataPUP.Contents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_recvPos += (UInt32)dataPUP.Contents.Length;
|
||||
|
||||
_inputLock.ExitWriteLock();
|
||||
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
|
||||
_inputWriteEvent.Set();
|
||||
|
||||
// If the client wants an ACK, send it now.
|
||||
if (dataPUP.Type == PupType.AData || dataPUP.Type == PupType.AMark)
|
||||
{
|
||||
SendAck();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends data, with immediate flush to the network.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
public void Send(byte[] data)
|
||||
{
|
||||
Send(data, data.Length, true /* flush */);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends data, optionally flushing.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
public void Send(byte[] data, bool flush)
|
||||
{
|
||||
Send(data, data.Length, flush);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
|
||||
// Send the data.
|
||||
PUP dataPup = new PUP(PupType.Data, _sendPos, _clientConnectionPort, _serverConnectionPort, chunk);
|
||||
SendDataPup(dataPup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an Abort PUP to the client, (generally indicating a catastrophic failure of some sort.)
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public void SendAbort(string message)
|
||||
{
|
||||
PUP abortPup = new PUP(PupType.Abort, _startPos, _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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a Mark (or AMark) to the client.
|
||||
/// </summary>
|
||||
/// <param name="markType"></param>
|
||||
/// <param name="ack"></param>
|
||||
public void SendMark(byte markType, bool ack)
|
||||
{
|
||||
PUP markPup = new PUP(ack ? PupType.AMark : PupType.Mark, _sendPos, _clientConnectionPort, _serverConnectionPort, new byte[] { markType });
|
||||
|
||||
// Send it.
|
||||
SendDataPup(markPup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the client sends an ACK.
|
||||
/// Update our record of the client's PUP buffers.
|
||||
/// </summary>
|
||||
/// <param name="ackPUP"></param>
|
||||
public void RecvAck(PUP ackPUP)
|
||||
{
|
||||
//_outputWindowLock.EnterWriteLock();
|
||||
_clientLimits = (BSPAck)Serializer.Deserialize(ackPUP.Contents, typeof(BSPAck));
|
||||
|
||||
Log.Write(LogType.Verbose, LogComponent.BSP,
|
||||
"ACK from client: bytes sent {0}, max bytes {1}, max pups {2}",
|
||||
_clientLimits.BytesSent,
|
||||
_clientLimits.MaxBytes,
|
||||
_clientLimits.MaxPups);
|
||||
|
||||
_lastClientRecvPos = ackPUP.ID;
|
||||
|
||||
//
|
||||
// Unblock those waiting for an ACK.
|
||||
//
|
||||
_outputAckEvent.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an ACK to the client.
|
||||
/// </summary>
|
||||
private void SendAck()
|
||||
{
|
||||
_inputLock.EnterReadLock();
|
||||
BSPAck ack = new BSPAck();
|
||||
ack.MaxBytes = MaxBytes;
|
||||
ack.MaxPups = MaxPups;
|
||||
ack.BytesSent = MaxBytes;
|
||||
_inputLock.ExitReadLock();
|
||||
|
||||
PUP ackPup = new PUP(PupType.Ack, _recvPos, _clientConnectionPort, _serverConnectionPort, Serializer.Serialize(ack));
|
||||
|
||||
PUPProtocolDispatcher.Instance.SendPup(ackPup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a PUP. Will block if client is unable to receive data. If timeouts expire, channel will be shut down.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
private void SendDataPup(PUP p)
|
||||
{
|
||||
//
|
||||
// Sanity check: This should only be called for Data or Mark pups.
|
||||
//
|
||||
if (p.Type != PupType.AData && p.Type != PupType.Data && p.Type != PupType.Mark && p.Type != PupType.AMark)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid PUP type for SendDataPup.");
|
||||
}
|
||||
|
||||
//
|
||||
// Add the pup to the output window. This may block if the window is full.
|
||||
//
|
||||
AddPupToOutputWindow(p);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pings the client with an empty AData PUP, which will cause it to respond with an ACK containing client BSP information.
|
||||
/// </summary>
|
||||
private void RequestClientStats()
|
||||
{
|
||||
//
|
||||
// Send an empty AData PUP to keep the connection alive and to update the client data stats.
|
||||
//
|
||||
PUP aData = new PUP(PupType.AData, _sendPos, _clientConnectionPort, _serverConnectionPort, new byte[0]);
|
||||
PUPProtocolDispatcher.Instance.SendPup(aData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified data/mark PUP to the moving output window, these PUPs will be picked up by the Output Thread
|
||||
/// and sent when the client is ready for them.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
private void AddPupToOutputWindow(PUP p)
|
||||
{
|
||||
// Ensure things are set up
|
||||
EstablishWindow();
|
||||
|
||||
_outputWindowLock.EnterUpgradeableReadLock();
|
||||
|
||||
if (_outputWindow.Count < _clientLimits.MaxPups)
|
||||
{
|
||||
//
|
||||
// There's space in the window, so go for it.
|
||||
//
|
||||
_outputWindowLock.EnterWriteLock();
|
||||
_outputWindow.Add(p);
|
||||
_outputWindowLock.ExitWriteLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// No space right now -- wait until the consumer has made some space.
|
||||
//
|
||||
// Leave the lock so the consumer is unblocked
|
||||
_outputWindowLock.ExitUpgradeableReadLock();
|
||||
|
||||
_outputReadyEvent.WaitOne();
|
||||
|
||||
// Re-enter.
|
||||
_outputWindowLock.EnterUpgradeableReadLock();
|
||||
|
||||
_outputWindowLock.EnterWriteLock();
|
||||
_outputWindow.Add(p);
|
||||
_outputWindowLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
//
|
||||
// Tell the Consumer thread we've added a new PUP to be consumed.
|
||||
//
|
||||
_dataReadyEvent.Set();
|
||||
|
||||
_outputWindowLock.ExitUpgradeableReadLock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OutputConsumerThread consumes data placed into the output window (by sending it to the PUP dispatcher).
|
||||
///
|
||||
/// It is responsible for getting positive handoff (via ACKS) from the client and resending PUPs the client
|
||||
/// missed. While the output window is full, writers to the channel will be blocked.
|
||||
/// </summary>
|
||||
private void OutputConsumerThread()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
//
|
||||
// Wait for data.
|
||||
//
|
||||
_dataReadyEvent.WaitOne();
|
||||
|
||||
_outputWindowLock.EnterUpgradeableReadLock();
|
||||
|
||||
// Keep consuming until we've caught up with production
|
||||
while (_outputWindowIndex < _outputWindow.Count)
|
||||
{
|
||||
//
|
||||
// Pull the next PUP off the output queue and send it.
|
||||
//
|
||||
PUP nextPup = _outputWindow[_outputWindowIndex++];
|
||||
|
||||
//
|
||||
// If we've sent as many PUPs to the client as it says it can take,
|
||||
// we need to change the PUP to an AData PUP so we can acknowledge
|
||||
// acceptance of the entire window we've sent.
|
||||
//
|
||||
bool bAck = false;
|
||||
if (_outputWindowIndex >= _clientLimits.MaxPups)
|
||||
{
|
||||
Log.Write(LogType.Verbose, LogComponent.BSP, "Window full (size {0}), requiring ACK of data.", _clientLimits.MaxPups);
|
||||
bAck = true;
|
||||
}
|
||||
|
||||
//
|
||||
// We need to build a PUP with the proper ID based on the current send position.
|
||||
// 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
//
|
||||
// Send it!
|
||||
//
|
||||
_sendPos += (uint)nextPup.Contents.Length;
|
||||
PUPProtocolDispatcher.Instance.SendPup(nextPup);
|
||||
|
||||
//
|
||||
// If we required an ACK, wait for it to arrive so we can confirm client reception of data.
|
||||
//
|
||||
if (nextPup.Type == PupType.AData || nextPup.Type == PupType.AMark)
|
||||
{
|
||||
// Wait for the client to be able to receive at least one PUP.
|
||||
while (true)
|
||||
{
|
||||
WaitForAck();
|
||||
|
||||
if (_clientLimits.MaxPups > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Nope. Request another ACK.
|
||||
// TODO: should probably error out of this if the client never becomes ready again...
|
||||
RequestClientStats();
|
||||
}
|
||||
|
||||
//
|
||||
// Check that the ACK's position matches ours, if it does not this indicates that the client lost at least one PUP,
|
||||
// so we will need to resend it.
|
||||
if (_lastClientRecvPos != _sendPos)
|
||||
{
|
||||
Log.Write(LogType.Warning, LogComponent.BSP,
|
||||
"Client position != server position for BSP {0} ({1} != {2})",
|
||||
_serverConnectionPort.Socket,
|
||||
_lastClientRecvPos,
|
||||
_sendPos);
|
||||
|
||||
//
|
||||
// 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;
|
||||
break;
|
||||
}
|
||||
_outputWindowIndex++;
|
||||
}
|
||||
|
||||
if (_outputWindowIndex == _outputWindow.Count)
|
||||
{
|
||||
// Something bad has happened and we don't have that PUP anymore...
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Client lost more than a window of data, BSP connection is broken. Aborting.");
|
||||
SendAbort("Fatal BSP synchronization error.");
|
||||
BSPManager.DestroyChannel(this);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// Everything was received OK by the client, remove the PUPs we sent from the output window and let writers continue.
|
||||
//
|
||||
_outputWindowLock.EnterWriteLock();
|
||||
_outputWindow.RemoveRange(0, _outputWindowIndex);
|
||||
_outputWindowIndex = 0;
|
||||
_outputWindowLock.ExitWriteLock();
|
||||
_outputReadyEvent.Set();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_outputWindowLock.ExitUpgradeableReadLock();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used when first actual data is sent over BSP, establishes initial parameters.
|
||||
/// </summary>
|
||||
private void EstablishWindow()
|
||||
{
|
||||
_outputWindowLock.EnterReadLock();
|
||||
int maxPups = _clientLimits.MaxPups;
|
||||
_outputWindowLock.ExitReadLock();
|
||||
|
||||
if (maxPups == 0xffff)
|
||||
{
|
||||
//
|
||||
// Wait for the client to be ready and tell us how many PUPs it can handle to start with.
|
||||
//
|
||||
RequestClientStats();
|
||||
WaitForAck();
|
||||
|
||||
if (_clientLimits.MaxPups == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Client reports MaxPups of 0, this is invalid at start of BSP.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WaitForAck()
|
||||
{
|
||||
//
|
||||
// Wait for the client to ACK.
|
||||
//
|
||||
int retry = 0;
|
||||
for (retry = 0; retry < BSPRetryCount; retry++)
|
||||
{
|
||||
if (_outputAckEvent.WaitOne(BSPAckTimeoutPeriod))
|
||||
{
|
||||
// Done.
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No response within timeout, ask for an update.
|
||||
RequestClientStats();
|
||||
}
|
||||
}
|
||||
|
||||
if (retry >= BSPRetryCount)
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Timeout waiting for ACK, aborting connection.");
|
||||
SendAbort("Client unresponsive.");
|
||||
BSPManager.DestroyChannel(this);
|
||||
}
|
||||
}
|
||||
|
||||
private BSPProtocol _protocolHandler;
|
||||
|
||||
// The byte positions for the input and output streams
|
||||
private UInt32 _recvPos;
|
||||
private UInt32 _sendPos;
|
||||
private UInt32 _startPos;
|
||||
|
||||
private PUPPort _clientConnectionPort; // the client port
|
||||
private PUPPort _serverConnectionPort; // the server port we (the server) have established for communication
|
||||
|
||||
private BSPAck _clientLimits; // The stats from the last ACK we got from the client.
|
||||
private uint _lastClientRecvPos; // The client's receive position, as indicated by the last ACK pup received.
|
||||
|
||||
private ReaderWriterLockSlim _inputLock;
|
||||
private AutoResetEvent _inputWriteEvent;
|
||||
|
||||
private ReaderWriterLockSlim _outputLock;
|
||||
|
||||
private AutoResetEvent _outputAckEvent;
|
||||
|
||||
// NOTE: The input queue consists of ushorts so that
|
||||
// we can encapsulate Mark bytes without using a separate data structure.
|
||||
private Queue<ushort> _inputQueue;
|
||||
private Queue<byte> _outputQueue;
|
||||
|
||||
// The output window (one entry per PUP the client says it's able to handle).
|
||||
private List<PUP> _outputWindow;
|
||||
private int _outputWindowIndex;
|
||||
private ReaderWriterLockSlim _outputWindowLock;
|
||||
private AutoResetEvent _outputReadyEvent;
|
||||
private AutoResetEvent _dataReadyEvent;
|
||||
|
||||
private Thread _consumerThread;
|
||||
|
||||
private byte _lastMark;
|
||||
|
||||
// Constants
|
||||
|
||||
// For now, we work on one PUP at a time.
|
||||
private const int MaxPups = 1;
|
||||
private const int MaxPupSize = 532;
|
||||
private const int MaxBytes = 1 * 532;
|
||||
|
||||
// Timeouts and retries
|
||||
private const int BSPRetryCount = 5;
|
||||
private const int BSPAckTimeoutPeriod = 1000; // 1 second
|
||||
private const int BSPReadTimeoutPeriod = 60000; // 1 minute
|
||||
}
|
||||
}
|
||||
@ -1,693 +0,0 @@
|
||||
using IFS.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IFS
|
||||
{
|
||||
|
||||
public struct BSPAck
|
||||
{
|
||||
public ushort MaxBytes;
|
||||
public ushort MaxPups;
|
||||
public ushort BytesSent;
|
||||
}
|
||||
|
||||
public abstract class BSPProtocol : PUPProtocolBase
|
||||
{
|
||||
public abstract void InitializeServerForChannel(BSPChannel channel);
|
||||
}
|
||||
|
||||
public enum BSPState
|
||||
{
|
||||
Unconnected,
|
||||
Connected
|
||||
}
|
||||
|
||||
public class BSPChannel
|
||||
{
|
||||
public BSPChannel(PUP rfcPup, UInt32 socketID, BSPProtocol protocolHandler)
|
||||
{
|
||||
_inputLock = new ReaderWriterLockSlim();
|
||||
_outputLock = new ReaderWriterLockSlim();
|
||||
|
||||
_inputWriteEvent = new AutoResetEvent(false);
|
||||
_inputQueue = new Queue<ushort>(65536);
|
||||
|
||||
_outputAckEvent = new AutoResetEvent(false);
|
||||
_outputQueue = new Queue<byte>(65536);
|
||||
|
||||
_protocolHandler = protocolHandler;
|
||||
|
||||
// TODO: init IDs, etc. based on RFC PUP
|
||||
_start_pos = _recv_pos = _send_pos = rfcPup.ID;
|
||||
|
||||
// Set up socket addresses.
|
||||
// The client sends the connection port it prefers to use
|
||||
// in the RFC pup.
|
||||
_clientConnectionPort = new PUPPort(rfcPup.Contents, 0);
|
||||
|
||||
//
|
||||
if (_clientConnectionPort.Network == 0)
|
||||
{
|
||||
_clientConnectionPort.Network = DirectoryServices.Instance.LocalNetwork;
|
||||
}
|
||||
|
||||
// We create our connection port using a unique socket address.
|
||||
_serverConnectionPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, socketID);
|
||||
}
|
||||
|
||||
public PUPPort ClientPort
|
||||
{
|
||||
get { return _clientConnectionPort; }
|
||||
}
|
||||
|
||||
public PUPPort ServerPort
|
||||
{
|
||||
get { return _serverConnectionPort; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last Mark byte received, if any.
|
||||
/// </summary>
|
||||
public byte LastMark
|
||||
{
|
||||
get { return _lastMark; }
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (OnDestroy != null)
|
||||
{
|
||||
OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
public void End(PUP p)
|
||||
{
|
||||
PUP endReplyPup = new PUP(PupType.EndReply, p.ID, _clientConnectionPort, _serverConnectionPort, new byte[0]);
|
||||
PUPProtocolDispatcher.Instance.SendPup(endReplyPup);
|
||||
|
||||
// "The receiver of the End PUP responds by returning an EndReply Pup with matching ID and then
|
||||
// _dallying_ up to some reasonably long timeout interval (say, 10 seconds) in order to respond to
|
||||
// a retransmitted End Pup should its initial EndReply be lost. If and when the dallying end of the
|
||||
// stream connection receives its EndReply, it may immediately self destruct."
|
||||
// TODO: actually make this happen...
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the channel (i.e. from the client). Will block if not all the requested data is available.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int Read(ref byte[] data, int count)
|
||||
{
|
||||
return Read(ref data, count, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the channel (i.e. from the client). Will block if not all the requested data is available.
|
||||
/// If a Mark byte is encountered, will return a short read.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int Read(ref byte[] data, int count, int offset)
|
||||
{
|
||||
// sanity check
|
||||
if (count + offset > data.Length)
|
||||
{
|
||||
throw new InvalidOperationException("count + offset must be less than or equal to the length of the buffer being read into.");
|
||||
}
|
||||
|
||||
int read = 0;
|
||||
|
||||
//
|
||||
// Loop until either:
|
||||
// - all the data we asked for arrives
|
||||
// - we get a Mark byte
|
||||
// - we time out waiting for data
|
||||
//
|
||||
bool done = false;
|
||||
while (!done)
|
||||
{
|
||||
_inputLock.EnterUpgradeableReadLock();
|
||||
if (_inputQueue.Count > 0)
|
||||
{
|
||||
_inputLock.EnterWriteLock();
|
||||
|
||||
// We have some data right now, read it in.
|
||||
// TODO: this code is ugly and it wants to die.
|
||||
while (_inputQueue.Count > 0 && read < count)
|
||||
{
|
||||
ushort word = _inputQueue.Dequeue();
|
||||
|
||||
// Is this a mark or a data byte?
|
||||
if (word < 0x100)
|
||||
{
|
||||
// Data, place in data stream
|
||||
data[read + offset] = (byte)word;
|
||||
read++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mark. Set last mark and exit.
|
||||
_lastMark = (byte)(word >> 8);
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (read >= count)
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
|
||||
_inputLock.ExitWriteLock();
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
|
||||
// No data in the queue.
|
||||
// Wait until we have received more data, then try again.
|
||||
if (!_inputWriteEvent.WaitOne(BSPReadTimeoutPeriod))
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Timed out waiting for data on read, aborting connection.");
|
||||
// We timed out waiting for data, abort the connection.
|
||||
SendAbort("Timeout on read.");
|
||||
BSPManager.DestroyChannel(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
public byte ReadByte()
|
||||
{
|
||||
// TODO: optimize this
|
||||
byte[] data = new byte[1];
|
||||
|
||||
Read(ref data, 1);
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
public ushort ReadUShort()
|
||||
{
|
||||
// TODO: optimize this
|
||||
byte[] data = new byte[2];
|
||||
|
||||
Read(ref data, 2);
|
||||
|
||||
return Helpers.ReadUShort(data, 0);
|
||||
}
|
||||
|
||||
public BCPLString ReadBCPLString()
|
||||
{
|
||||
return new BCPLString(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the queue until a Mark byte is received.
|
||||
/// The mark byte is returned (LastMark is also set.)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public byte WaitForMark()
|
||||
{
|
||||
byte mark = 0;
|
||||
|
||||
// This data is discarded. The length is arbitrary.
|
||||
byte[] dummyData = new byte[512];
|
||||
|
||||
while(true)
|
||||
{
|
||||
int read = Read(ref dummyData, dummyData.Length);
|
||||
|
||||
// Short read, indicating a Mark.
|
||||
if (read < dummyData.Length)
|
||||
{
|
||||
mark = _lastMark;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return mark;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends incoming client data or Marks into the input queue (called from BSPManager to place new PUP data into the BSP stream)
|
||||
/// </summary>
|
||||
public void RecvWriteQueue(PUP dataPUP)
|
||||
{
|
||||
//
|
||||
// Sanity check: If this is a Mark PUP, the contents must only be one byte in length.
|
||||
//
|
||||
bool markPup = dataPUP.Type == PupType.AMark || dataPUP.Type == PupType.Mark;
|
||||
if (markPup)
|
||||
{
|
||||
if (dataPUP.Contents.Length != 1)
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Mark PUP must be 1 byte in length.");
|
||||
|
||||
SendAbort("Mark PUP must be 1 byte in length.");
|
||||
BSPManager.DestroyChannel(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are over our high watermark, we will drop the data (and not send an ACK even if requested).
|
||||
// Clients should be honoring the limits we set in the RFC packets.
|
||||
_inputLock.EnterUpgradeableReadLock();
|
||||
|
||||
/*
|
||||
if (_inputQueue.Count + dataPUP.Contents.Length > MaxBytes)
|
||||
{
|
||||
Log.Write(LogLevel.Error, "Queue larger than {0} bytes, dropping.");
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
return;
|
||||
} */
|
||||
|
||||
// Sanity check on expected position from sender vs. received data on our end.
|
||||
// If they don't match then we've lost a packet somewhere.
|
||||
if (dataPUP.ID != _recv_pos)
|
||||
{
|
||||
// Current behavior is to simply drop all incoming PUPs (and not ACK them) until they are re-sent to us
|
||||
// (in which case the above sanity check will pass). According to spec, AData requests that are not ACKed
|
||||
// must eventually be resent. This is far simpler than accepting out-of-order data and keeping track
|
||||
// of where it goes in the queue, though less efficient.
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Lost Packet, client ID does not match our receive position ({0} != {1})", dataPUP.ID, _recv_pos);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Prepare to add data to the queue
|
||||
|
||||
_inputLock.EnterWriteLock();
|
||||
|
||||
if (markPup)
|
||||
{
|
||||
//
|
||||
// For mark pups, the data goes in the high byte of the word
|
||||
// so that it can be identified as a mark when it's read back.
|
||||
_inputQueue.Enqueue((ushort)(dataPUP.Contents[0] << 8));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Again, this is really inefficient
|
||||
for (int i = 0; i < dataPUP.Contents.Length; i++)
|
||||
{
|
||||
_inputQueue.Enqueue(dataPUP.Contents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_recv_pos += (UInt32)dataPUP.Contents.Length;
|
||||
|
||||
_inputLock.ExitWriteLock();
|
||||
|
||||
_inputLock.ExitUpgradeableReadLock();
|
||||
|
||||
_inputWriteEvent.Set();
|
||||
|
||||
// If the client wants an ACK, send it now.
|
||||
if (dataPUP.Type == PupType.AData || dataPUP.Type == PupType.AMark)
|
||||
{
|
||||
SendAck();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends data, with immediate flush.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
public void Send(byte[] data)
|
||||
{
|
||||
Send(data, true /* flush */);
|
||||
}
|
||||
|
||||
/// <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, bool flush)
|
||||
{
|
||||
// Add output data to output queue.
|
||||
// Again, this is really inefficient
|
||||
for (int i = 0; i < data.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();
|
||||
}
|
||||
|
||||
// Send the data, retrying as necessary.
|
||||
PUP dataPup = new PUP(PupType.AData, _send_pos, _clientConnectionPort, _serverConnectionPort, chunk);
|
||||
_send_pos += (uint)chunk.Length;
|
||||
SendPupAwaitAck(dataPup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SendAbort(string message)
|
||||
{
|
||||
PUP abortPup = new PUP(PupType.Abort, _start_pos, _clientConnectionPort, _serverConnectionPort, Helpers.StringToArray(message));
|
||||
PUPProtocolDispatcher.Instance.SendPup(abortPup);
|
||||
}
|
||||
|
||||
public void SendMark(byte markType, bool ack)
|
||||
{
|
||||
PUP markPup = new PUP(ack ? PupType.AMark : PupType.Mark, _send_pos, _clientConnectionPort, _serverConnectionPort, new byte[] { markType });
|
||||
|
||||
// Move pointer one byte for the Mark.
|
||||
_send_pos++;
|
||||
|
||||
if (ack)
|
||||
{
|
||||
SendPupAwaitAck(markPup);
|
||||
}
|
||||
else
|
||||
{
|
||||
PUPProtocolDispatcher.Instance.SendPup(markPup);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the client sends an ACK
|
||||
/// </summary>
|
||||
/// <param name="ackPUP"></param>
|
||||
public void RecvAck(PUP ackPUP)
|
||||
{
|
||||
// Update receiving end stats (max PUPs, etc.)
|
||||
// Ensure client's position matches ours
|
||||
if (ackPUP.ID != _send_pos)
|
||||
{
|
||||
Log.Write(LogType.Warning, LogComponent.BSP,
|
||||
"Client position != server position for BSP {0} ({1} != {2})",
|
||||
_serverConnectionPort.Socket,
|
||||
ackPUP.ID,
|
||||
_send_pos);
|
||||
}
|
||||
|
||||
BSPAck ack = (BSPAck)Serializer.Deserialize(ackPUP.Contents, typeof(BSPAck));
|
||||
|
||||
|
||||
// Let any waiting threads continue
|
||||
_outputAckEvent.Set();
|
||||
}
|
||||
|
||||
/*
|
||||
public void Mark(byte type);
|
||||
public void Interrupt();
|
||||
|
||||
public void Abort(int code, string message);
|
||||
public void Error(int code, string message);
|
||||
|
||||
public void End();
|
||||
*/
|
||||
|
||||
// TODO:
|
||||
// Events for:
|
||||
// Abort, End, Mark, Interrupt (from client)
|
||||
// Repositioning (due to lost packets) (perhaps not necessary)
|
||||
// to allow protocols consuming BSP streams to be alerted when things happen.
|
||||
//
|
||||
|
||||
public delegate void DestroyDelegate();
|
||||
|
||||
public DestroyDelegate OnDestroy;
|
||||
|
||||
private void SendAck()
|
||||
{
|
||||
_inputLock.EnterReadLock();
|
||||
BSPAck ack = new BSPAck();
|
||||
ack.MaxBytes = MaxBytes; //(ushort)(MaxBytes - _inputQueue.Count);
|
||||
ack.MaxPups = MaxPups;
|
||||
ack.BytesSent = MaxBytes; //(ushort)(MaxBytes - _inputQueue.Count);
|
||||
_inputLock.ExitReadLock();
|
||||
|
||||
PUP ackPup = new PUP(PupType.Ack, _recv_pos, _clientConnectionPort, _serverConnectionPort, Serializer.Serialize(ack));
|
||||
|
||||
PUPProtocolDispatcher.Instance.SendPup(ackPup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a PUP and waits for acknowledgement. On timeout, will retry.
|
||||
/// If all retries fails, the channel is closed.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
private void SendPupAwaitAck(PUP p)
|
||||
{
|
||||
// Send the data, retrying as necessary.
|
||||
int retry;
|
||||
for (retry = 0; retry < BSPRetryCount; retry++)
|
||||
{
|
||||
PUPProtocolDispatcher.Instance.SendPup(p);
|
||||
|
||||
// Await an ack for the PUP we just sent. If we timeout, we will retry.
|
||||
//
|
||||
if (_outputAckEvent.WaitOne(BSPAckTimeoutPeriod))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Write(LogType.Warning, LogComponent.BSP, "ACK not received for sent data, retrying.");
|
||||
}
|
||||
|
||||
if (retry >= BSPRetryCount)
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "ACK not received after retries, aborting connection.");
|
||||
SendAbort("ACK not received for sent data.");
|
||||
BSPManager.DestroyChannel(this);
|
||||
}
|
||||
}
|
||||
|
||||
private BSPProtocol _protocolHandler;
|
||||
|
||||
private UInt32 _recv_pos;
|
||||
private UInt32 _send_pos;
|
||||
private UInt32 _start_pos;
|
||||
|
||||
private PUPPort _clientConnectionPort; // the client port
|
||||
private PUPPort _serverConnectionPort; // the server port we (the server) have established for communication
|
||||
|
||||
private ReaderWriterLockSlim _inputLock;
|
||||
private System.Threading.AutoResetEvent _inputWriteEvent;
|
||||
|
||||
private ReaderWriterLockSlim _outputLock;
|
||||
|
||||
private System.Threading.AutoResetEvent _outputAckEvent;
|
||||
|
||||
// NOTE: The input queue consists of ushorts so that
|
||||
// we can encapsulate Mark bytes without using a separate data structure.
|
||||
private Queue<ushort> _inputQueue;
|
||||
private Queue<byte> _outputQueue;
|
||||
|
||||
private byte _lastMark;
|
||||
|
||||
// Constants
|
||||
|
||||
// For now, we work on one PUP at a time.
|
||||
private const int MaxPups = 1;
|
||||
private const int MaxPupSize = 532;
|
||||
private const int MaxBytes = 1 * 532;
|
||||
|
||||
|
||||
private const int BSPRetryCount = 5;
|
||||
private const int BSPAckTimeoutPeriod = 1000; // 1 second
|
||||
private const int BSPReadTimeoutPeriod = 60000; // 1 minute
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class BSPManager
|
||||
{
|
||||
static BSPManager()
|
||||
{
|
||||
//
|
||||
// Initialize the socket ID counter; we start with a
|
||||
// number beyond the range of well-defined sockets.
|
||||
// For each new BSP 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, BSPChannel>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a PUP comes in on a known BSP socket
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
public static void EstablishRendezvous(PUP p, BSPProtocol protocolHandler)
|
||||
{
|
||||
if (p.Type != PupType.RFC)
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.RTP, "Expected RFC pup, got {0}", p.Type);
|
||||
return;
|
||||
}
|
||||
|
||||
UInt32 socketID = GetNextSocketID();
|
||||
BSPChannel newChannel = new BSPChannel(p, socketID, protocolHandler);
|
||||
_activeChannels.Add(socketID, newChannel);
|
||||
|
||||
//
|
||||
// Initialize the server for this protocol.
|
||||
protocolHandler.InitializeServerForChannel(newChannel);
|
||||
|
||||
// Send RFC response to complete the rendezvous.
|
||||
|
||||
// Modify the destination port to specify our network
|
||||
PUPPort sourcePort = p.DestinationPort;
|
||||
sourcePort.Network = DirectoryServices.Instance.LocalNetwork;
|
||||
PUP rfcResponse = new PUP(PupType.RFC, p.ID, newChannel.ClientPort, sourcePort, newChannel.ServerPort.ToArray());
|
||||
|
||||
Log.Write(LogComponent.RTP,
|
||||
"Establishing Rendezvous, ID {0}, Server port {1}, Client port {2}.",
|
||||
p.ID, newChannel.ServerPort, newChannel.ClientPort);
|
||||
|
||||
PUPProtocolDispatcher.Instance.SendPup(rfcResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when BSP-based protocols receive data.
|
||||
/// </summary>
|
||||
/// <param name="p"></param>
|
||||
public static void RecvData(PUP p)
|
||||
{
|
||||
BSPChannel channel = FindChannelForPup(p);
|
||||
|
||||
if (channel == null)
|
||||
{
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Received BSP PUP on an unconnected socket, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (p.Type)
|
||||
{
|
||||
case PupType.RFC:
|
||||
Log.Write(LogType.Error, LogComponent.BSP, "Received RFC on established channel, ignoring.");
|
||||
break;
|
||||
|
||||
case PupType.Data:
|
||||
case PupType.AData:
|
||||
{
|
||||
channel.RecvWriteQueue(p);
|
||||
}
|
||||
break;
|
||||
|
||||
case PupType.Ack:
|
||||
{
|
||||
channel.RecvAck(p);
|
||||
}
|
||||
break;
|
||||
|
||||
case PupType.End:
|
||||
{
|
||||
// Second step of tearing down a connection, the End from the client, to which we will
|
||||
// send an EndReply, expecting a second EndReply.
|
||||
channel.End(p);
|
||||
}
|
||||
break;
|
||||
|
||||
case PupType.EndReply:
|
||||
{
|
||||
// Last step of tearing down a connection, the EndReply from the client.
|
||||
DestroyChannel(channel);
|
||||
}
|
||||
break;
|
||||
|
||||
case PupType.Mark:
|
||||
case PupType.AMark:
|
||||
{
|
||||
channel.RecvWriteQueue(p);
|
||||
}
|
||||
break;
|
||||
|
||||
case PupType.Abort:
|
||||
{
|
||||
string abortMessage = Helpers.ArrayToString(p.Contents);
|
||||
Log.Write(LogType.Warning, LogComponent.RTP, String.Format("BSP aborted, message: '{0}'", abortMessage));
|
||||
|
||||
DestroyChannel(channel);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotImplementedException(String.Format("Unhandled BSP PUP type {0}.", p.Type));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ChannelExistsForSocket(PUP p)
|
||||
{
|
||||
return FindChannelForPup(p) != null;
|
||||
}
|
||||
|
||||
public static void DestroyChannel(BSPChannel channel)
|
||||
{
|
||||
channel.Destroy();
|
||||
|
||||
_activeChannels.Remove(channel.ServerPort.Socket);
|
||||
}
|
||||
|
||||
private static BSPChannel FindChannelForPup(PUP p)
|
||||
{
|
||||
if (_activeChannels.ContainsKey(p.DestinationPort.Socket))
|
||||
{
|
||||
return _activeChannels[p.DestinationPort.Socket];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 < _startingSocketID)
|
||||
{
|
||||
_nextSocketID = _startingSocketID;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map from socket address to BSP channel
|
||||
/// </summary>
|
||||
private static Dictionary<UInt32, BSPChannel> _activeChannels;
|
||||
|
||||
private static UInt32 _nextSocketID;
|
||||
private static readonly UInt32 _startingSocketID = 0x1000;
|
||||
}
|
||||
}
|
||||
28
PUP/Configuration.cs
Normal file
28
PUP/Configuration.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IFS
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates global server configuration information.
|
||||
///
|
||||
/// TODO: read in configuration from a text file.
|
||||
/// </summary>
|
||||
public static class Configuration
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The root directory for the FTP file store.
|
||||
/// </summary>
|
||||
public static readonly string FTPRoot = "C:\\ifs\\ftp";
|
||||
|
||||
/// <summary>
|
||||
/// The root directory for the CopyDisk file store.
|
||||
/// </summary>
|
||||
public static readonly string CopyDiskRoot = "C:\\ifs\\copydisk";
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
using IFS.Logging;
|
||||
using IFS.BSP;
|
||||
using IFS.Logging;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -293,7 +295,7 @@ namespace IFS.CopyDisk
|
||||
Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Copydisk client is version {0}, '{1}'", vbIn.Code, vbIn.Herald.ToString());
|
||||
|
||||
// Send the response:
|
||||
VersionYesNoBlock vbOut = new VersionYesNoBlock(CopyDiskBlock.Version, vbIn.Code, "IFS CopyDisk of 26-Jan-2016!");
|
||||
VersionYesNoBlock vbOut = new VersionYesNoBlock(CopyDiskBlock.Version, vbIn.Code, "LCM IFS CopyDisk of 26-Jan-2016");
|
||||
channel.Send(Serializer.Serialize(vbOut));
|
||||
}
|
||||
break;
|
||||
@ -555,9 +557,7 @@ namespace IFS.CopyDisk
|
||||
/// <returns></returns>
|
||||
private static string GetPathForDiskImage(string packName)
|
||||
{
|
||||
// TODO:
|
||||
// Make this path configurable?
|
||||
return Path.Combine("Disks", packName);
|
||||
return Path.Combine(Configuration.CopyDiskRoot, packName);
|
||||
}
|
||||
|
||||
private Thread _workerThread;
|
||||
|
||||
@ -12,7 +12,7 @@ namespace IFS
|
||||
public class Entrypoint
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
{
|
||||
List<EthernetInterface> ifaces = EthernetInterface.EnumerateDevices();
|
||||
|
||||
Console.WriteLine("available interfaces are:");
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using IFS.BSP;
|
||||
using IFS.Logging;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
using IFS.Logging;
|
||||
using System.IO;
|
||||
|
||||
namespace IFS.FTP
|
||||
{
|
||||
@ -27,17 +29,55 @@ namespace IFS.FTP
|
||||
Rename = 15,
|
||||
}
|
||||
|
||||
struct FTPVersion
|
||||
public enum NoCode
|
||||
{
|
||||
public FTPVersion(byte version, string herald)
|
||||
UnimplmentedCommand = 1,
|
||||
UserNameRequired = 2,
|
||||
IllegalCommand = 3,
|
||||
|
||||
MalformedPropertyList = 8,
|
||||
IllegalServerFilename = 9,
|
||||
IllegalDirectory = 10,
|
||||
IllegalNameBody = 11,
|
||||
IllegalVersion = 12,
|
||||
IllegalType = 13,
|
||||
IllegalByteSize = 14,
|
||||
IllegalEndOfLineConvention = 15,
|
||||
IllegalUserName = 16,
|
||||
IllegalUserPassword = 17,
|
||||
IllegalUserAccount = 18,
|
||||
IllegalConnectName = 19,
|
||||
IllegalConnectPassword = 20,
|
||||
IllegalCreationDate = 21,
|
||||
IllegalWriteDate = 22,
|
||||
IllegalReadDate = 23,
|
||||
IllegalAuthor = 24,
|
||||
IllegalDevice = 25,
|
||||
|
||||
FileNotFound = 64,
|
||||
AccessDenied = 65,
|
||||
TransferParamsInvalid = 66,
|
||||
FileDataError = 67,
|
||||
FileTooLong = 68,
|
||||
DoNotSendFile = 69,
|
||||
StoreNotCompleted = 70,
|
||||
TransientServerFailure = 71,
|
||||
PermamentServerFailure = 72,
|
||||
FileBusy = 73,
|
||||
FileAlreadyExists = 74
|
||||
}
|
||||
|
||||
struct FTPYesNoVersion
|
||||
{
|
||||
public FTPYesNoVersion(byte code, string message)
|
||||
{
|
||||
Version = version;
|
||||
Herald = herald;
|
||||
Code = code;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public byte Version;
|
||||
public string Herald;
|
||||
}
|
||||
public byte Code;
|
||||
public string Message;
|
||||
}
|
||||
|
||||
public class FTPServer : BSPProtocol
|
||||
{
|
||||
@ -54,7 +94,7 @@ namespace IFS.FTP
|
||||
{
|
||||
// Spawn new worker
|
||||
FTPWorker ftpWorker = new FTPWorker(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FTPWorker
|
||||
@ -107,44 +147,13 @@ namespace IFS.FTP
|
||||
}
|
||||
|
||||
private void FTPWorkerThread()
|
||||
{
|
||||
// Buffer used to receive command data.
|
||||
byte[] buffer = new byte[1024];
|
||||
|
||||
{
|
||||
while (_running)
|
||||
{
|
||||
// Discard input until we get a Mark. We should (in general) get a
|
||||
// command, followed by EndOfCommand.
|
||||
FTPCommand command = (FTPCommand)_channel.WaitForMark();
|
||||
|
||||
// Read data until the next Mark, which should be "EndOfCommand"
|
||||
int length = _channel.Read(ref buffer, buffer.Length);
|
||||
|
||||
//
|
||||
// Sanity check: FTP spec doesn't specify max length of a command so the current
|
||||
// length is merely a guess. If we actually filled the buffer then we should note it
|
||||
// so this can be corrected.
|
||||
//
|
||||
if (length == buffer.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Expected short read for FTP command.");
|
||||
}
|
||||
|
||||
//
|
||||
// Ensure that next Mark is "EndOfCommand"
|
||||
//
|
||||
if (_channel.LastMark != (byte)FTPCommand.EndOfCommand)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Expected EndOfCommand, got {0}", (FTPCommand)_channel.LastMark));
|
||||
}
|
||||
|
||||
//
|
||||
// TODO: this is ugly, figure out a clean way to do this. We need to deal with only the
|
||||
// actual data retrieved. Due to the clumsy way we read it in we need to copy it here.
|
||||
//
|
||||
byte[] data = new byte[length];
|
||||
Array.Copy(buffer, data, length);
|
||||
byte[] data = null;
|
||||
|
||||
FTPCommand command = ReadNextCommandWithData(out data);
|
||||
|
||||
//
|
||||
// At this point we should have the entire command, execute it.
|
||||
//
|
||||
@ -152,12 +161,12 @@ namespace IFS.FTP
|
||||
{
|
||||
case FTPCommand.Version:
|
||||
{
|
||||
FTPVersion version = (FTPVersion)Serializer.Deserialize(data, typeof(FTPVersion));
|
||||
Log.Write(LogType.Normal, LogComponent.FTP, "Client FTP version is {0}, herald is '{1}.", version.Version, version.Herald);
|
||||
FTPYesNoVersion version = (FTPYesNoVersion)Serializer.Deserialize(data, typeof(FTPYesNoVersion));
|
||||
Log.Write(LogType.Normal, LogComponent.FTP, "Client FTP version is {0}, herald is '{1}'.", version.Code, version.Message);
|
||||
|
||||
//
|
||||
// Return our Version.
|
||||
FTPVersion serverVersion = new FTPVersion(1, "LCM IFS FTP of 4 Feb 2016.");
|
||||
FTPYesNoVersion serverVersion = new FTPYesNoVersion(1, "LCM IFS FTP of 4 Feb 2016.");
|
||||
SendFTPResponse(FTPCommand.Version, serverVersion);
|
||||
}
|
||||
break;
|
||||
@ -167,7 +176,64 @@ namespace IFS.FTP
|
||||
// Argument to Enumerate is a property list (string).
|
||||
//
|
||||
string fileSpec = Helpers.ArrayToString(data);
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for enumeration is '{0}.", fileSpec);
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for enumeration is '{0}'.", fileSpec);
|
||||
|
||||
PropertyList pl = new PropertyList(fileSpec);
|
||||
|
||||
EnumerateFiles(pl);
|
||||
}
|
||||
break;
|
||||
|
||||
case FTPCommand.Retrieve:
|
||||
{
|
||||
// Argument to Retrieve is a property list (string).
|
||||
//
|
||||
string fileSpec = Helpers.ArrayToString(data);
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for retrieve is '{0}'.", fileSpec);
|
||||
|
||||
PropertyList pl = new PropertyList(fileSpec);
|
||||
|
||||
RetrieveFiles(pl);
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
case FTPCommand.Store:
|
||||
{
|
||||
// Argument to Store is a property list (string).
|
||||
//
|
||||
string fileSpec = Helpers.ArrayToString(data);
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for store is '{0}'.", fileSpec);
|
||||
|
||||
PropertyList pl = new PropertyList(fileSpec);
|
||||
|
||||
StoreFile(pl, false /* old */);
|
||||
}
|
||||
break;
|
||||
|
||||
case FTPCommand.NewStore:
|
||||
{
|
||||
// Argument to New-Store is a property list (string).
|
||||
//
|
||||
string fileSpec = Helpers.ArrayToString(data);
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for new-store is '{0}'.", fileSpec);
|
||||
|
||||
PropertyList pl = new PropertyList(fileSpec);
|
||||
|
||||
StoreFile(pl, true /* new */);
|
||||
}
|
||||
break;
|
||||
|
||||
case FTPCommand.Delete:
|
||||
{
|
||||
// Argument to New-Store is a property list (string).
|
||||
//
|
||||
string fileSpec = Helpers.ArrayToString(data);
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for new-store is '{0}'.", fileSpec);
|
||||
|
||||
PropertyList pl = new PropertyList(fileSpec);
|
||||
|
||||
DeleteFiles(pl);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -178,13 +244,538 @@ namespace IFS.FTP
|
||||
}
|
||||
}
|
||||
|
||||
private FTPCommand ReadNextCommandWithData(out byte[] data)
|
||||
{
|
||||
// Discard input until we get a Mark. We should (in general) get a
|
||||
// command, followed by EndOfCommand.
|
||||
FTPCommand command = (FTPCommand)_channel.WaitForMark();
|
||||
|
||||
data = ReadNextCommandData();
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// As above, but expects the channel read position is such that a Mark has *just* been read.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private byte[] ReadNextCommandData()
|
||||
{
|
||||
// Read data until the next Mark, which should be "EndOfCommand"
|
||||
// TODO: I don't anticipate that any FTP command will contain more than 1k of data, this may need adjustment.
|
||||
byte[] data = null;
|
||||
FTPCommand lastMark = ReadUntilNextMark(out data, 1024);
|
||||
|
||||
//
|
||||
// Ensure that next Mark is "EndOfCommand"
|
||||
//
|
||||
if (lastMark != FTPCommand.EndOfCommand)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Expected EndOfCommand, got {0}", lastMark));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the channel into data until the next Mark is hit, this Mark is returned.
|
||||
/// If the size of the read data exceeds maxSize an exception will be thrown.
|
||||
/// </summary>
|
||||
/// <param name="data">The data read from the channel.</param>
|
||||
/// <param name="maxSize">The maximum size (in bytes) of the data to read.</param>
|
||||
/// <returns>The next Mark encountered</returns>
|
||||
private FTPCommand ReadUntilNextMark(out byte[] data, int maxSize)
|
||||
{
|
||||
MemoryStream ms = new MemoryStream(16384);
|
||||
byte[] buffer = new byte[512];
|
||||
|
||||
while(true)
|
||||
{
|
||||
int length = _channel.Read(ref buffer, buffer.Length);
|
||||
|
||||
ms.Write(buffer, 0, length);
|
||||
|
||||
if (ms.Length > maxSize)
|
||||
{
|
||||
throw new InvalidOperationException("Data size limit exceeded.");
|
||||
}
|
||||
|
||||
//
|
||||
// On a short read, we are done.
|
||||
//
|
||||
if (length < buffer.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
data = ms.ToArray();
|
||||
return (FTPCommand)_channel.LastMark;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the files matching the requested file specification.
|
||||
/// </summary>
|
||||
/// <param name="fileSpec"></param>
|
||||
private void EnumerateFiles(PropertyList fileSpec)
|
||||
{
|
||||
string fullPath = BuildAndValidateFilePath(fileSpec);
|
||||
|
||||
if (fullPath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<PropertyList> files = EnumerateFiles(fullPath);
|
||||
|
||||
// Send each property list to the user
|
||||
foreach(PropertyList matchingFile in files)
|
||||
{
|
||||
_channel.SendMark((byte)FTPCommand.HereIsPropertyList, false);
|
||||
_channel.Send(Helpers.StringToArray(matchingFile.ToString()));
|
||||
}
|
||||
|
||||
// End the enumeration.
|
||||
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the files matching the requested file specification and sends them to the client, one at a time.
|
||||
/// </summary>
|
||||
/// <param name="fileSpec"></param>
|
||||
private void RetrieveFiles(PropertyList fileSpec)
|
||||
{
|
||||
string fullPath = BuildAndValidateFilePath(fileSpec);
|
||||
|
||||
if (fullPath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<PropertyList> files = EnumerateFiles(fullPath);
|
||||
|
||||
// Send each list to the user, followed by the actual file data.
|
||||
//
|
||||
foreach (PropertyList matchingFile in files)
|
||||
{
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Property list for file being sent is '{0}'", matchingFile.ToString());
|
||||
// Tell the client about the file we're about to send
|
||||
SendFTPResponse(FTPCommand.HereIsPropertyList, matchingFile);
|
||||
|
||||
// Await confirmation:
|
||||
byte[] data = null;
|
||||
FTPCommand yesNo = ReadNextCommandWithData(out data);
|
||||
|
||||
if (yesNo == FTPCommand.No)
|
||||
{
|
||||
// Skip this file
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File skipped.");
|
||||
continue;
|
||||
}
|
||||
|
||||
using (FileStream outFile = OpenFile(matchingFile, true))
|
||||
{
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Sending file...");
|
||||
|
||||
// Send the file data.
|
||||
_channel.SendMark((byte)FTPCommand.HereIsFile, true);
|
||||
data = new byte[512];
|
||||
|
||||
while (true)
|
||||
{
|
||||
int read = outFile.Read(data, 0, data.Length);
|
||||
|
||||
if (read == 0)
|
||||
{
|
||||
// Nothing to send, we're done.
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Sending data, current file position {0}.", outFile.Position);
|
||||
_channel.Send(data, read, true);
|
||||
|
||||
if (read < data.Length)
|
||||
{
|
||||
// Short read, end of file.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End the file successfully. Note that we do NOT send an EOC here.
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Sent.");
|
||||
_channel.SendMark((byte)FTPCommand.Yes, false);
|
||||
_channel.Send(Serializer.Serialize(new FTPYesNoVersion(0, "File transferred successfully.")));
|
||||
}
|
||||
|
||||
// End the transfer.
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "All requested files sent.");
|
||||
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
|
||||
}
|
||||
|
||||
|
||||
private void StoreFile(PropertyList fileSpec, bool newStore)
|
||||
{
|
||||
string fullPath = BuildAndValidateFilePath(fileSpec);
|
||||
|
||||
if (fullPath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (newStore)
|
||||
{
|
||||
//
|
||||
// Send the client a Here-Is-Property-List for the file to be stored, indicating that we've accepted the request
|
||||
// for the store.
|
||||
// (If we're unable to write or fail for any reason, we can send a No later).
|
||||
//
|
||||
PropertyList fileProps = new PropertyList();
|
||||
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.ServerFilename, fullPath);
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.Type, "Binary"); // We treat all files as binary for now
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.ByteSize, "8"); // 8-bit bytes, please.
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.Version, "1"); // No real versioning support
|
||||
|
||||
SendFTPResponse(FTPCommand.HereIsPropertyList, fileProps);
|
||||
}
|
||||
|
||||
//
|
||||
// We now expect a "Here-Is-File"...
|
||||
//
|
||||
FTPCommand hereIsFile = (FTPCommand)_channel.WaitForMark();
|
||||
|
||||
if (hereIsFile != FTPCommand.HereIsFile)
|
||||
{
|
||||
throw new InvalidOperationException("Expected Here-Is-File from client.");
|
||||
}
|
||||
|
||||
//
|
||||
// At this point the client should start sending data, so we should start receiving it.
|
||||
//
|
||||
string fullFileName = Path.Combine(Configuration.FTPRoot, fullPath);
|
||||
bool success = true;
|
||||
FTPCommand lastMark;
|
||||
byte[] buffer;
|
||||
|
||||
try
|
||||
{
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Receiving file {0}.", fullFileName);
|
||||
using (FileStream inFile = new FileStream(fullFileName, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
// TODO: move to constant. Possibly make max size configurable.
|
||||
// For now, it seems very unlikely that any Alto is going to have a single file larger than 4mb.
|
||||
lastMark = ReadUntilNextMark(out buffer, 4096 * 1024);
|
||||
|
||||
// Write out to file
|
||||
inFile.Write(buffer, 0, buffer.Length);
|
||||
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Wrote {0} bytes to {1}. Receive completed.", buffer.Length, fullFileName);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// We failed while writing the file, send a No response to the client.
|
||||
// Per the spec, we need to drain the client data first.
|
||||
lastMark = ReadUntilNextMark(out buffer, 4096 * 1024); // TODO: move to constant
|
||||
success = false;
|
||||
|
||||
Log.Write(LogType.Warning, LogComponent.FTP, "Failed to write {0}. Error '{1}'", fullFileName, e.Message);
|
||||
}
|
||||
|
||||
// Read in the last command we got (should be a Yes or No). This is sort of annoying in that it breaks the normal convention of
|
||||
// Command followed by EndOfCommand, so we have to read the remainder of the Yes/No command separately.
|
||||
if (lastMark != FTPCommand.Yes && lastMark != FTPCommand.No)
|
||||
{
|
||||
throw new InvalidOperationException("Expected Yes or No response from client after transfer.");
|
||||
}
|
||||
|
||||
buffer = ReadNextCommandData();
|
||||
FTPYesNoVersion clientYesNo = (FTPYesNoVersion)Serializer.Deserialize(buffer, typeof(FTPYesNoVersion));
|
||||
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Client success code is {0}, {1}, '{2}'", lastMark, clientYesNo.Code, clientYesNo.Code);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
// TODO: provide actual No codes.
|
||||
SendFTPNoResponse(NoCode.FileBusy, "File transfer failed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
SendFTPYesResponse("File transfer completed.");
|
||||
}
|
||||
|
||||
// If we failed to write a complete file, try to see that it gets cleaned up.
|
||||
if (!success || lastMark == FTPCommand.No)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(Path.Combine(Configuration.FTPRoot, fullPath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Just eat the exception, we tried our best...
|
||||
}
|
||||
}
|
||||
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Transfer done.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the files matching the requested file specification and sends them to the client, one at a time.
|
||||
/// </summary>
|
||||
/// <param name="fileSpec"></param>
|
||||
private void DeleteFiles(PropertyList fileSpec)
|
||||
{
|
||||
string fullPath = BuildAndValidateFilePath(fileSpec);
|
||||
|
||||
if (fullPath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<PropertyList> files = EnumerateFiles(fullPath);
|
||||
|
||||
// Send each list to the user, followed by the actual file data.
|
||||
//
|
||||
foreach (PropertyList matchingFile in files)
|
||||
{
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Property list for file being sent is '{0}'", matchingFile.ToString());
|
||||
// Tell the client about the file we're about to send
|
||||
SendFTPResponse(FTPCommand.HereIsPropertyList, matchingFile);
|
||||
|
||||
// Await confirmation:
|
||||
byte[] data = null;
|
||||
FTPCommand yesNo = ReadNextCommandWithData(out data);
|
||||
|
||||
if (yesNo == FTPCommand.No)
|
||||
{
|
||||
// Skip this file
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "File skipped.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Go ahead and delete the file.
|
||||
//
|
||||
try
|
||||
{
|
||||
File.Delete(
|
||||
Path.Combine(
|
||||
Configuration.FTPRoot, matchingFile.GetPropertyValue(KnownPropertyNames.Directory), matchingFile.GetPropertyValue(KnownPropertyNames.ServerFilename)));
|
||||
|
||||
// End the file successfully. Note that we do NOT send an EOC here, only after all files have been deleted.
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "Deleted.");
|
||||
_channel.SendMark((byte)FTPCommand.Yes, false);
|
||||
_channel.Send(Serializer.Serialize(new FTPYesNoVersion(0, "File deleted successfully.")));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
// TODO: calculate real NO codes
|
||||
_channel.SendMark((byte)FTPCommand.No, false);
|
||||
_channel.Send(Serializer.Serialize(new FTPYesNoVersion((byte)NoCode.AccessDenied, e.Message)));
|
||||
}
|
||||
}
|
||||
|
||||
// End the transfer.
|
||||
Log.Write(LogType.Verbose, LogComponent.FTP, "All requested files deleted.");
|
||||
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the file specified by the provided PropertyList
|
||||
/// </summary>
|
||||
/// <param name="fileSpec"></param>
|
||||
/// <returns></returns>
|
||||
private FileStream OpenFile(PropertyList fileSpec, bool readOnly)
|
||||
{
|
||||
string absolutePath = Path.Combine(Configuration.FTPRoot, fileSpec.GetPropertyValue(KnownPropertyNames.Directory), fileSpec.GetPropertyValue(KnownPropertyNames.ServerFilename));
|
||||
|
||||
return new FileStream(absolutePath, FileMode.Open, readOnly ? FileAccess.Read : FileAccess.ReadWrite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all files in the IFS FTP directory matching the specified specification, and returns a full PropertyList for each.
|
||||
/// </summary>
|
||||
/// <param name="fileSpec"></param>
|
||||
/// <returns></returns>
|
||||
private List<PropertyList> EnumerateFiles(string fileSpec)
|
||||
{
|
||||
List<PropertyList> properties = new List<PropertyList>();
|
||||
|
||||
// Build a path rooted in the FTP root.
|
||||
string fullFileSpec = Path.Combine(Configuration.FTPRoot, fileSpec);
|
||||
|
||||
// Split full path into filename and path parts
|
||||
string fileName = Path.GetFileName(fullFileSpec);
|
||||
string path = Path.GetDirectoryName(fullFileSpec);
|
||||
|
||||
// Find all files that match the fileName (which may be a pattern to match or a complete file name for a single file)
|
||||
// These will be absolute paths.
|
||||
string[] matchingFiles = Directory.GetFiles(path, fileName, SearchOption.TopDirectoryOnly);
|
||||
|
||||
// Build a property list containing the required properties.
|
||||
// For now, we ignore any Desired-Property requests (this is legal) and return all properties we know about.
|
||||
foreach (string matchingFile in matchingFiles)
|
||||
{
|
||||
string nameOnly = Path.GetFileName(matchingFile);
|
||||
|
||||
PropertyList fileProps = new PropertyList();
|
||||
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.ServerFilename, nameOnly);
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.Directory, path);
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.NameBody, nameOnly);
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.Type, "Binary"); // We treat all files as binary for now
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.ByteSize, "8"); // 8-bit bytes, please.
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.Version, "1"); // No real versioning support
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.CreationDate, File.GetCreationTime(matchingFile).ToString("dd-MMM-yy HH:mm:ss"));
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.WriteDate, File.GetLastWriteTime(matchingFile).ToString("dd-MMM-yy HH:mm:ss"));
|
||||
fileProps.SetPropertyValue(KnownPropertyNames.ReadDate, File.GetLastAccessTime(matchingFile).ToString("dd-MMM-yy HH:mm:ss"));
|
||||
|
||||
properties.Add(fileProps);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a relative path from the specified file PropertyList and checks for basic validity:
|
||||
/// - that the syntax is correct
|
||||
/// - that it includes no invalid characters.
|
||||
/// - that the directory specified actually exists.
|
||||
/// </summary>
|
||||
/// <param name="fileSpec"></param>
|
||||
/// <returns></returns>
|
||||
private string BuildAndValidateFilePath(PropertyList fileSpec)
|
||||
{
|
||||
//
|
||||
// Pull the file identifying properties from fileSpec and see what we can make of them.
|
||||
//
|
||||
string serverFilename = fileSpec.GetPropertyValue(KnownPropertyNames.ServerFilename);
|
||||
string directory = fileSpec.GetPropertyValue(KnownPropertyNames.Directory);
|
||||
string nameBody = fileSpec.GetPropertyValue(KnownPropertyNames.NameBody);
|
||||
string version = fileSpec.GetPropertyValue(KnownPropertyNames.Version);
|
||||
|
||||
// Sanity checks:
|
||||
|
||||
// At the very least, one of Server-Filename or Name-Body must be specified.
|
||||
if (serverFilename == null && nameBody == null)
|
||||
{
|
||||
SendFTPNoResponse(NoCode.IllegalServerFilename, "Need at least a Server-FileName or Name-Body property.");
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// Attempt to build a full file path from the bits we have.
|
||||
//
|
||||
string relativePath;
|
||||
|
||||
if (directory != null && serverFilename != null)
|
||||
{
|
||||
//
|
||||
// If Directory and Server-Filename are both specified
|
||||
// We will assume Directory specifies the containing directory for Server-Filename, and prepend it.
|
||||
//
|
||||
relativePath = Path.Combine(directory, serverFilename);
|
||||
}
|
||||
else if (serverFilename != null)
|
||||
{
|
||||
// We will just use the Server-Filename as the complete path
|
||||
relativePath = serverFilename;
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// Directory was specified, Server-Filename was not, so we expect at least
|
||||
// Name-Body to be specified.
|
||||
if (nameBody == null)
|
||||
{
|
||||
SendFTPNoResponse(NoCode.IllegalNameBody, "Name-Body must be specified.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
relativePath = Path.Combine(directory, nameBody);
|
||||
|
||||
if (version != null)
|
||||
{
|
||||
relativePath += ("!" + version);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// At this point we should have a path built.
|
||||
// Now let's see if it's valid.
|
||||
//
|
||||
|
||||
//
|
||||
// Path should be relative:
|
||||
//
|
||||
if (Path.IsPathRooted(relativePath))
|
||||
{
|
||||
SendFTPNoResponse(NoCode.IllegalDirectory, "Path must be relative.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build path combined with FTP root directory
|
||||
//
|
||||
string absolutePath = Path.Combine(Configuration.FTPRoot, relativePath);
|
||||
string absoluteDirectory = Path.GetDirectoryName(absolutePath);
|
||||
|
||||
//
|
||||
// Path (including filename) must not contain any trickery like "..\" to try and escape from the directory root
|
||||
// And directory must not include invalid characters.
|
||||
//
|
||||
if (relativePath.Contains("..\\") ||
|
||||
absoluteDirectory.IndexOfAny(Path.GetInvalidPathChars()) != -1)
|
||||
{
|
||||
SendFTPNoResponse(NoCode.IllegalDirectory, "Path is invalid.");
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// Path must exist:
|
||||
//
|
||||
if (!Directory.Exists(absoluteDirectory))
|
||||
{
|
||||
SendFTPNoResponse(NoCode.FileNotFound, "Path does not exist.");
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// Looks like we should be OK.
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
private void SendFTPResponse(FTPCommand responseCommand, object data)
|
||||
{
|
||||
_channel.SendMark((byte)FTPCommand.Version, false);
|
||||
_channel.SendMark((byte)responseCommand, false);
|
||||
_channel.Send(Serializer.Serialize(data));
|
||||
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
|
||||
}
|
||||
|
||||
private void SendFTPResponse(FTPCommand responseCommand, PropertyList data)
|
||||
{
|
||||
_channel.SendMark((byte)responseCommand, false);
|
||||
_channel.Send(Helpers.StringToArray(data.ToString()));
|
||||
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
|
||||
}
|
||||
|
||||
private void SendFTPNoResponse(NoCode code, string message)
|
||||
{
|
||||
_channel.SendMark((byte)FTPCommand.No, false);
|
||||
_channel.Send(Serializer.Serialize(new FTPYesNoVersion((byte)code, message)));
|
||||
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
|
||||
}
|
||||
|
||||
private void SendFTPYesResponse(string message)
|
||||
{
|
||||
_channel.SendMark((byte)FTPCommand.Yes, false);
|
||||
_channel.Send(Serializer.Serialize(new FTPYesNoVersion(1, message)));
|
||||
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
|
||||
}
|
||||
|
||||
private BSPChannel _channel;
|
||||
|
||||
private Thread _workerThread;
|
||||
|
||||
280
PUP/FTP/PropertyList.cs
Normal file
280
PUP/FTP/PropertyList.cs
Normal file
@ -0,0 +1,280 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IFS.FTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the well-known set of FTP property names, both Mandatory and Optional.
|
||||
/// </summary>
|
||||
public static class KnownPropertyNames
|
||||
{
|
||||
// Mandatory
|
||||
public static readonly string ServerFilename = "Server-Filename";
|
||||
public static readonly string Type = "Type";
|
||||
public static readonly string EndOfLineConvention = "End-of-Line-Convention";
|
||||
public static readonly string ByteSize = "Byte-Size";
|
||||
public static readonly string Device = "Device";
|
||||
public static readonly string Directory = "Directory";
|
||||
public static readonly string NameBody = "Name-Body";
|
||||
public static readonly string Version = "Version";
|
||||
|
||||
// Optional
|
||||
public static readonly string Size = "Size";
|
||||
public static readonly string UserName = "User-Name";
|
||||
public static readonly string UserPassword = "User-Password";
|
||||
public static readonly string UserAccount = "User-Account";
|
||||
public static readonly string ConnectName = "Connect-Name";
|
||||
public static readonly string ConnectPassword = "Connect-Password";
|
||||
public static readonly string CreationDate = "Creation-Date";
|
||||
public static readonly string WriteDate = "Write-Date";
|
||||
public static readonly string ReadDate = "Read-Date";
|
||||
public static readonly string Author = "Author";
|
||||
public static readonly string Checksum = "Checksum";
|
||||
public static readonly string DesiredProperty = "Desired-Property";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines an FTP PropertyList and methods to work with the contents of one.
|
||||
/// From the FTP spec:
|
||||
///
|
||||
/// "5.1 Syntax of a file property list
|
||||
///
|
||||
/// A file property list consists of a string of ASCII characters, beginning with a left parenthesis and
|
||||
/// ending with a matching right parenthesis. Within that list, each property is represented similarly
|
||||
/// as a parenthesized list. For example:
|
||||
/// ((Server-Filename TESTFILE.7)(Byte-Size 36))
|
||||
///
|
||||
/// This scheme has the advantage of being human readable, although it will require some form of
|
||||
/// scanner or interpreter. Nevertheless, this is a rigid format, with minimum flexibility ni form; FTP is
|
||||
/// a machine-to-machine protocol, not a programming language.
|
||||
///
|
||||
/// The first item in each property (delimited by a left parenthesis and a space) is the property name,
|
||||
/// taken from a fixed but extensible set. Upper- and lower-case letters are considered equivalent in the
|
||||
/// property name. The text between the first space and the right parenthesis is the property value. All
|
||||
/// characters in the property value are taken literally, except in accordance with the quoting convention
|
||||
/// described below.
|
||||
///
|
||||
/// All spaces are significant, and multiple spaces may not be arbitrarily included. There should be no space
|
||||
/// between the two leading parentheses, for example, and a single space separates a property
|
||||
/// name from the property value. Other spaces in a property value will become part of that value, so
|
||||
/// that the following example will work properly:
|
||||
/// ((Server-Filename xxxxx)(Read-Date 23-Jan-76 11:30:22 PST))
|
||||
///
|
||||
/// A single apostrophe is used as the quote character in a property value, and should be used before a
|
||||
/// parenthesis or a desired apostrophe:
|
||||
/// Don't(!)Goof ==> (PropertyName Don''t'(!')Goof)"
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
public class PropertyList
|
||||
{
|
||||
public PropertyList()
|
||||
{
|
||||
_propertyList = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public PropertyList(string list) : this()
|
||||
{
|
||||
ParseList(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the Property List contains the specified property
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public bool ContainsPropertyValue(string name)
|
||||
{
|
||||
return (_propertyList.ContainsKey(name.ToLowerInvariant()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value for the specified property, if present. Otherwise returns null.
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public string GetPropertyValue(string name)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
|
||||
if (_propertyList.ContainsKey(name))
|
||||
{
|
||||
return _propertyList[name];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPropertyValue(string name, string value)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
|
||||
if (_propertyList.ContainsKey(name))
|
||||
{
|
||||
_propertyList[name] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_propertyList.Add(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the PropertyList back to its string representation.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// Opening paren
|
||||
sb.Append("(");
|
||||
|
||||
foreach(string key in _propertyList.Keys)
|
||||
{
|
||||
sb.AppendFormat("({0} {1})", key, EscapeString(_propertyList[key]));
|
||||
}
|
||||
|
||||
// Closing paren
|
||||
sb.Append(")");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string EscapeString(string value)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(value.Length);
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
if (value[i] == '\'' || value[i] == '(' || value[i] == ')')
|
||||
{
|
||||
// Escape this thing
|
||||
sb.Append('\'');
|
||||
sb.Append(value[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(value[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string representation of a property list into our hash table.
|
||||
/// </summary>
|
||||
/// <param name="list"></param>
|
||||
private void ParseList(string list)
|
||||
{
|
||||
//
|
||||
// First check the basics; the string must start and end with left and right parens, respectively.
|
||||
// We do not trim whitespace as there should not be any per the spec.
|
||||
//
|
||||
if (!list.StartsWith("(") || !list.EndsWith(")"))
|
||||
{
|
||||
throw new InvalidOperationException("Property list must begin and end with parentheses.");
|
||||
}
|
||||
|
||||
//
|
||||
// Looking good so far; parse individual properties now. These also start and end with
|
||||
// left and right parens.
|
||||
//
|
||||
int index = 1;
|
||||
|
||||
//
|
||||
// Loop until we hit the end of the string (minus the closing paren)
|
||||
//
|
||||
while (index < list.Length - 1)
|
||||
{
|
||||
// Start of next property, must begin with a left paren.
|
||||
if (list[index] != '(')
|
||||
{
|
||||
throw new InvalidOperationException("Property must begin with a left parenthesis.");
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
//
|
||||
// Read in the full property name. Property names can't have escaped characters in them
|
||||
// so we don't need to watch out for those, just find the first space.
|
||||
//
|
||||
int endIndex = list.IndexOf(' ', index);
|
||||
|
||||
if (endIndex < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Badly formed property list, no space delimiter found.");
|
||||
}
|
||||
|
||||
string propertyName = list.Substring(index, endIndex - index).ToLowerInvariant();
|
||||
index = endIndex + 1; // Move past space
|
||||
|
||||
//
|
||||
// Read in the property value. This may contain spaces or escaped characters and it ends with an
|
||||
// unescaped right paren.
|
||||
//
|
||||
StringBuilder propertyValue = new StringBuilder();
|
||||
|
||||
while(true)
|
||||
{
|
||||
// End of value?
|
||||
if (list[index] == ')')
|
||||
{
|
||||
// Move past closing paren
|
||||
index++;
|
||||
|
||||
// And we're done with this property.
|
||||
break;
|
||||
}
|
||||
// Quoted value?
|
||||
else if (list[index] == '\'')
|
||||
{
|
||||
// Add quoted character
|
||||
index++;
|
||||
|
||||
// Ensure we don't walk off the end of the string
|
||||
if (index >= list.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid property list syntax.");
|
||||
}
|
||||
|
||||
propertyValue.Append(list[index]);
|
||||
}
|
||||
// Just a normal character
|
||||
else
|
||||
{
|
||||
propertyValue.Append(list[index]);
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
// Ensure we don't walk off the end of the string
|
||||
if (index >= list.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid property list syntax.");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Add name/value pair to the hash table.
|
||||
//
|
||||
if (!_propertyList.ContainsKey(propertyName))
|
||||
{
|
||||
_propertyList.Add(propertyName, propertyValue.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Duplicate property entry for '{0}", propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> _propertyList;
|
||||
}
|
||||
}
|
||||
@ -74,13 +74,16 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="BCPLString.cs" />
|
||||
<Compile Include="BreathOfLife.cs" />
|
||||
<Compile Include="BSPManager.cs" />
|
||||
<Compile Include="BSP\BSPChannel.cs" />
|
||||
<Compile Include="BSP\BSPManager.cs" />
|
||||
<Compile Include="Configuration.cs" />
|
||||
<Compile Include="CopyDisk\CopyDiskServer.cs" />
|
||||
<Compile Include="CopyDisk\DiabloPack.cs" />
|
||||
<Compile Include="DirectoryServices.cs" />
|
||||
<Compile Include="EchoProtocol.cs" />
|
||||
<Compile Include="Entrypoint.cs" />
|
||||
<Compile Include="FTP\FTPServer.cs" />
|
||||
<Compile Include="FTP\PropertyList.cs" />
|
||||
<Compile Include="Logging\Log.cs" />
|
||||
<Compile Include="GatewayInformationProtocol.cs" />
|
||||
<Compile Include="MiscServicesProtocol.cs" />
|
||||
|
||||
@ -186,7 +186,7 @@ namespace IFS
|
||||
|
||||
// Ensure contents are an even number of bytes.
|
||||
int contentLength = (contents.Length % 2) == 0 ? contents.Length : contents.Length + 1;
|
||||
Contents = new byte[contentLength];
|
||||
Contents = new byte[contents.Length];
|
||||
contents.CopyTo(Contents, 0);
|
||||
|
||||
// Length is always the real length of the data (not padded to an even number)
|
||||
@ -209,7 +209,7 @@ namespace IFS
|
||||
Helpers.WriteUInt(ref _rawData, ID, 4);
|
||||
DestinationPort.WriteToArray(ref _rawData, 8);
|
||||
SourcePort.WriteToArray(ref _rawData, 14);
|
||||
Array.Copy(Contents, 0, _rawData, 20, contentLength);
|
||||
Array.Copy(Contents, 0, _rawData, 20, Contents.Length);
|
||||
|
||||
// Calculate the checksum and stow it
|
||||
Checksum = CalculateChecksum();
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using IFS.BSP;
|
||||
using IFS.Logging;
|
||||
using IFS.Transport;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using IFS.Transport;
|
||||
using IFS.Logging;
|
||||
using PcapDotNet.Base;
|
||||
|
||||
namespace IFS
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user