1
0
mirror of https://github.com/livingcomputermuseum/IFS.git synced 2026-03-08 03:49:22 +00:00

Fleshed out implementation.

This commit is contained in:
Josh Dersch
2015-10-14 16:17:36 -07:00
parent 6be09b106c
commit 53539d8afd
22 changed files with 1602 additions and 9 deletions

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ethernet
namespace BSP
{
public class Class1
{

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("BSP")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Vulcan Inc.")]
[assembly: AssemblyProduct("BSP")]
[assembly: AssemblyCopyright("Copyright © Vulcan Inc. 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("5093c127-6a0e-48ff-8ae3-7db590348b6f")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{BD5D7CF7-52BA-4960-A60F-75EEECCEF775}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>BreathOfLife</RootNamespace>
<AssemblyName>BreathOfLife</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BreathOfLife
{
class Program
{
static void Main(string[] args)
{
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("BreathOfLife")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Vulcan Inc.")]
[assembly: AssemblyProduct("BreathOfLife")]
[assembly: AssemblyCopyright("Copyright © Vulcan Inc. 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("af633818-d867-41ba-a09c-d26de9bc75b2")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -20,6 +20,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@@ -30,6 +31,18 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="PcapDotNet.Base">
<HintPath>..\pcap\PcapDotNet.Base.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Core">
<HintPath>..\pcap\PcapDotNet.Core.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Core.Extensions">
<HintPath>..\pcap\PcapDotNet.Core.Extensions.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Packets">
<HintPath>..\pcap\PcapDotNet.Packets.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@@ -39,7 +52,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="Ethernet.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

89
PUP/BCPLString.cs Normal file
View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
/// <summary>
/// A BCPL string is one byte of length N followed by N bytes of ASCII.
/// This class is a (very) simple encapsulation of that over a byte array.
/// </summary>
public class BCPLString
{
public BCPLString(string s)
{
if (s.Length > 255)
{
throw new InvalidOperationException("Max length for a BCPL string is 255 characters.");
}
_string = new byte[_string.Length];
// We simply take the low 8-bits of each Unicode character and stuff it into the
// byte array. This works fine for the ASCII subset of Unicode but obviously
// is bad for everything else. This is unlikely to be an issue given the lack of
// any real internationalization support on the IFS end of things, but might be
// something to look at.
for(int i=0;i< _string.Length; i++)
{
_string[i] = (byte)s[i];
}
}
/// <summary>
/// Build a new BCPL string from the raw representation
/// </summary>
/// <param name="rawData"></param>
public BCPLString(byte[] rawData)
{
if (rawData.Length > 256)
{
throw new InvalidOperationException("Max length for a BCPL string is 255 characters.");
}
// Sanity check that first byte matches length of data sent to us
if (rawData.Length < 1 || rawData[0] != rawData.Length - 1)
{
throw new InvalidOperationException("BCPL length data is invalid.");
}
_string = new byte[rawData.Length - 1];
Array.Copy(rawData, 1, _string, 0, rawData.Length - 1);
}
/// <summary>
/// Returns a native representation of the BCPL string
/// </summary>
/// <returns></returns>
public override string ToString()
{
StringBuilder sb = new StringBuilder();
// See notes in constructor re: unicode.
for (int i = 0; i < _string.Length; i++)
{
sb.Append((char)_string[i]);
}
return sb.ToString();
}
/// <summary>
/// Returns the raw representation of the BCPL string
/// </summary>
/// <returns></returns>
public byte[] ToArray()
{
byte[] a = new byte[_string.Length + 1];
a[0] = (byte)_string.Length;
_string.CopyTo(a, 1);
return a;
}
private byte[] _string;
}
}

301
PUP/BSPManager.cs Normal file
View File

@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace IFS
{
public enum BSPState
{
Unconnected,
Connected
}
public class BSPChannel
{
public BSPChannel(PUP rfcPup)
{
_inputLock = new ReaderWriterLockSlim();
_outputLock = new ReaderWriterLockSlim();
_inputWriteEvent = new AutoResetEvent(false);
_inputQueue = new MemoryStream(65536);
// TODO: init IDs, etc. based on RFC PUP
_start_pos = _recv_pos = _send_pos = rfcPup.ID;
}
public PUPPort ClientPort
{
get { return _clientConnectionPort; }
}
public PUPPort ServerPort
{
get { return _serverConnectionPort; }
}
/// <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)
{
// sanity check
if (count > data.Length)
{
throw new InvalidOperationException("count must be less than or equal to the length of the buffer being read into.");
}
int read = 0;
// Loop until the data we asked for arrives or until we time out waiting.
// TODO: handle partial transfers due to aborted BSPs.
while (true)
{
_inputLock.EnterUpgradeableReadLock();
if (_inputQueue.Count >= count)
{
_inputLock.EnterWriteLock();
// We have the data right now, read it and return.
// TODO: this is a really inefficient thing.
for (int i = 0; i < count; i++)
{
data[i] = _inputQueue.Dequeue();
}
_inputLock.ExitWriteLock();
_inputLock.ExitUpgradeableReadLock();
break;
}
else
{
_inputLock.ExitUpgradeableReadLock();
// Not enough data in the queue.
// Wait until we have received more data, then try again.
_inputWriteEvent.WaitOne(); // TODO: timeout and fail
}
}
return read;
}
/// <summary>
/// Appends data into the input queue (called from BSPManager to place new PUP data into the BSP stream)
/// </summary>
public void WriteQueue(PUP dataPUP)
{
// If we are over our high watermark, we will drop the data (and not send an ACK even if requested).
// Clients should be honoring the limits we set in the RFC packets.
_inputLock.EnterUpgradeableReadLock();
if (_inputQueue.Count + dataPUP.Contents.Length > MaxBytes)
{
_inputLock.ExitUpgradeableReadLock();
return;
}
// Sanity check on expected position from sender vs. received data on our end.
// If they don't match then we've lost a packet somewhere.
if (dataPUP.ID != _recv_pos)
{
// Current behavior is to simply drop all incoming PUPs (and not ACK them) until they are re-sent to us
// (in which case the above sanity check will pass). According to spec, AData requests that are not ACKed
// must eventually be resent. This is far simpler than accepting out-of-order data and keeping track
// of where it goes in the queue.
return;
}
// Prepare to add data to the queue
// Again, this is really inefficient
_inputLock.EnterWriteLock();
for (int i = 0; i < dataPUP.Contents.Length; i++)
{
_inputQueue.Enqueue(dataPUP.Contents[i]);
}
_recv_pos += (UInt32)dataPUP.Contents.Length;
_inputLock.ExitWriteLock();
_inputLock.ExitUpgradeableReadLock();
_inputWriteEvent.Set();
if ((PupType)dataPUP.Type == PupType.AData)
{
SendAck();
}
}
/// <summary>
/// Sends data to the channel (i.e. to the client). Will block if an ACK is requested.
/// </summary>
/// <param name="data">The data to be sent</param>
public void Send(byte[] data)
{
// Write data to the output stream
// Await an ack for the PUP we just sent
_outputAckEvent.WaitOne(); // TODO: timeout and fail
}
public void Ack(PUP ackPUP)
{
// Update receiving end stats (max PUPs, etc.)
// Ensure client's position matches ours
// Let any waiting threads continue
_outputAckEvent.Set();
}
public void Mark(byte type);
public void Interrupt();
public void Abort(int code, string message);
public void Error(int code, string message);
public void End();
// TODO:
// Events for:
// Abort, End, Mark, Interrupt (from client)
// Repositioning (due to lost packets) (perhaps not necessary)
//
private void SendAck()
{
}
private BSPState _state;
private UInt32 _recv_pos;
private UInt32 _send_pos;
private UInt32 _start_pos;
private PUPPort _clientConnectionPort; // the client port
private PUPPort _serverConnectionPort; // the server port we (the server) have established for communication
private ReaderWriterLockSlim _inputLock;
private System.Threading.AutoResetEvent _inputWriteEvent;
private ReaderWriterLockSlim _outputLock;
private System.Threading.AutoResetEvent _outputAckEvent;
// TODO: replace this with a more efficient structure for buffering data
private Queue<byte> _inputQueue;
// Constants
// For now, we work on one PUP at a time.
private const int MaxPups = 1;
private const int MaxPupSize = 532;
private const int MaxBytes = 1 * 532;
}
/// <summary>
///
/// </summary>
public static class BSPManager
{
static BSPManager()
{
}
/// <summary>
/// Called when BSP-based protocols receive data.
/// </summary>
/// <returns>
/// null if no new channel is created due to the sent PUP (not an RFC)
/// a new BSPChannel if one has been created based on the PUP (new RFC)
/// </returns>
/// <param name="p"></param>
public static BSPChannel RecvData(PUP p)
{
PupType type = (PupType)p.Type;
switch (type)
{
case PupType.RFC:
{
BSPChannel newChannel = new BSPChannel(p);
_activeChannels.Add(newChannel.ClientPort.Socket);
return newChannel;
}
case PupType.Data:
case PupType.AData:
{
BSPChannel channel = FindChannelForPup(p);
if (channel != null)
{
channel.WriteQueue(p);
}
}
break;
case PupType.Ack:
BSPChannel channel = FindChannelForPup(p);
if (channel != null)
{
channel.Ack(p);
}
break;
case PupType.End:
{
BSPChannel channel = FindChannelForPup(p);
if (channel != null)
{
channel.EndReply();
}
}
break;
case PupType.Abort:
{
// TODO: tear down the channel
}
break;
default:
throw new NotImplementedException(String.Format("Unhandled BSP PUP type {0}.", type));
}
return null;
}
public static void DestroyChannel(BSPChannel channel)
{
}
private static BSPChannel FindChannelForPup(PUP p)
{
return null;
}
private static Dictionary<UInt32, BSPChannel> _activeChannels;
}
}

25
PUP/CopyDiskServer.cs Normal file
View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
public class CopyDiskServer : PUPProtocolBase
{
/// <summary>
/// Called by dispatcher to send incoming data destined for this protocol.
/// </summary>
/// <param name="p"></param>
public override void RecvData(PUP p)
{
BSPChannel newChannel = BSPManager.RecvData(p);
if (newChannel != null)
{
// spwan new worker thread with new BSP channel
}
}
}
}

50
PUP/DirectoryServices.cs Normal file
View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
public class HostAddress
{
public HostAddress(byte network, byte host)
{
Network = network;
Host = host;
}
public byte Network;
public byte Host;
}
/// <summary>
/// Provides a basic database used by the Misc. Services to do name lookup and whatnot.
///
/// </summary>
public class DirectoryServices
{
private DirectoryServices()
{
// Get our host address; for now just hardcode it.
// TODO: need to define config files, etc.
_localHost = new HostAddress(1, 34);
}
public static DirectoryServices Instance
{
get { return _instance; }
}
public HostAddress LocalHostAddress
{
get { return _localHost; }
}
private HostAddress _localHost;
private static DirectoryServices _instance = new DirectoryServices();
}
}

96
PUP/Dispatcher.cs Normal file
View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IFS.Transport;
using PcapDotNet.Packets;
namespace IFS
{
/// <summary>
/// Dispatches incoming PUPs to the right protocol handler; sends outgoing PUPs over the network.
/// </summary>
public class Dispatcher
{
/// <summary>
/// Private Constructor for this class, enforcing Singleton usage.
/// </summary>
private Dispatcher()
{
_dispatchMap = new Dictionary<uint, PUPProtocolEntry>();
_packetInterface = new Ethernet(iface, OnPacketReceived);
}
/// <summary>
/// Accessor for singleton instance of this class.
/// </summary>
public static Dispatcher Instance
{
get { return _instance; }
}
/// <summary>
/// Registers a new protocol with the dispatcher
/// </summary>
/// <param name="reg"></param>
/// <param name="impl"></param>
public void RegisterProtocol(PUPProtocolEntry entry)
{
if (_dispatchMap.ContainsKey(entry.Socket))
{
throw new InvalidOperationException(
String.Format("Socket {0} has already been registered for protocol {1}", impl.Socket, _dispatchMap[impl.Socket].FriendlyName));
}
_dispatchMap[entry.Socket] = entry;
}
public void SendPup(PUP p)
{
// TODO: Write PUP to ethernet
}
private void OnPacketReceived(Packet p)
{
// filter out PUPs, discard all other packets. Forward PUP on to registered endpoints.
//
if ((int)p.Ethernet.EtherType == 512) // 512 = pup type
{
PUP pup = new PUP(p.Ethernet.Payload.ToMemoryStream());
if (_dispatchMap.ContainsKey(pup.DestinationPort.Socket))
{
PUPProtocolEntry entry = _dispatchMap[pup.DestinationPort.Socket];
entry.ProtocolImplementation.RecvData(pup);
}
else
{
// Not a protocol we handle; TODO: log it.
}
}
else
{
// Not a PUP, Discard the packet.
}
}
/// <summary>
/// Our interface to some kind of network
/// </summary>
private IPacketInterface _packetInterface;
/// <summary>
/// Map from socket to protocol implementation
/// </summary>
private Dictionary<UInt32, PUPProtocolEntry> _dispatchMap;
private static Dispatcher _instance = new Dispatcher();
}
}

37
PUP/EchoProtocol.cs Normal file
View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
/// <summary>
/// Implements the PUP Echo Protocol.
/// </summary>
public class EchoProtocol : PUPProtocolBase
{
public EchoProtocol()
{
}
/// <summary>
/// Called by dispatcher to send incoming data destined for this protocol
/// </summary>
/// <param name="p"></param>
public override void RecvData(PUP p)
{
// If this is an EchoMe packet, we will send back an "ImAnEcho" packet.
if (p.Type == PupType.EchoMe)
{
// Just send it back with the source/destination swapped.
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket);
PUP echoPup = new PUP(PupType.ImAnEcho, p.ID, p.SourcePort, localPort);
Dispatcher.Instance.SendPup(p);
}
}
}
}

23
PUP/Entrypoint.cs Normal file
View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
public class Entrypoint
{
static void Main(string[] args)
{
// Set up protocols:
// Connectionless
Dispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Misc Services", 0x4, ConnectionType.Connectionless, new MiscServicesProtocol()));
Dispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Echo", 0x5, ConnectionType.Connectionless, new EchoProtocol()));
// RTP/BSP based:
Dispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("CopyDisk", 0x15 /* 25B */, ConnectionType.BSP, new CopyDiskServer()));
}
}

80
PUP/IFS.csproj Normal file
View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>IFS</RootNamespace>
<AssemblyName>IFS</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="PcapDotNet.Base">
<HintPath>..\Ethernet\pcap\PcapDotNet.Base.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Core">
<HintPath>..\Ethernet\pcap\PcapDotNet.Core.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Core.Extensions">
<HintPath>..\Ethernet\pcap\PcapDotNet.Core.Extensions.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Packets">
<HintPath>..\Ethernet\pcap\PcapDotNet.Packets.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BCPLString.cs" />
<Compile Include="BSPManager.cs" />
<Compile Include="CopyDiskServer.cs" />
<Compile Include="DirectoryServices.cs" />
<Compile Include="EchoProtocol.cs" />
<Compile Include="Entrypoint.cs" />
<Compile Include="MiscServicesProtocol.cs" />
<Compile Include="Transport\Ethernet.cs" />
<Compile Include="PUPProtocolBase.cs" />
<Compile Include="PUP.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Dispatcher.cs" />
<Compile Include="Transport\PacketInterface.cs" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -1,9 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.31101.0
# Visual Studio 14
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ethernet", "Ethernet\Ethernet.csproj", "{BFFB20D8-4684-4E44-9521-44F3593525FB}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IFS", "IFS.csproj", "{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,10 +11,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BFFB20D8-4684-4E44-9521-44F3593525FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFFB20D8-4684-4E44-9521-44F3593525FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFFB20D8-4684-4E44-9521-44F3593525FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFFB20D8-4684-4E44-9521-44F3593525FB}.Release|Any CPU.Build.0 = Release|Any CPU
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

210
PUP/MiscServicesProtocol.cs Normal file
View File

@@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
/// <summary>
/// Implements PUP Miscellaneous Services (see miscSvcsProto.pdf)
/// which include:
/// - Date and Time services
/// - Mail check
/// - Network Directory Lookup
/// - Alto Boot protocols
/// - Authenticate/Validate
/// </summary>
public class MiscServicesProtocol : PUPProtocolBase
{
public MiscServicesProtocol()
{
// TODO:
// load host tables, etc.
}
/// <summary>
/// Called by dispatcher to send incoming data destined for this protocol
/// </summary>
/// <param name="p"></param>
public override void RecvData(PUP p)
{
switch (p.Type)
{
case PupType.StringTimeRequest:
SendStringTimeReply(p);
break;
case PupType.AltoTimeRequest:
SendAltoTimeReply(p);
break;
case PupType.AddressLookupRequest:
SendAddressLookupReply(p);
break;
case PupType.NameLookupRequest:
SendNameLookupReply(p);
break;
default:
// Unhandled, TODO: log it
break;
}
}
private void SendStringTimeReply(PUP p)
{
//
// From the spec, the response is:
// "A string consisting of the current date and time in the form
// '11-SEP-75 15:44:25'"
//
// It makes no mention of timezone or DST, so I am assuming local time here.
// Good enough for government work.
//
DateTime currentTime = DateTime.Now;
BCPLString bcplDateString = new BCPLString(currentTime.ToString("dd-MMM-yy HH:mm:ss"));
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket);
PUP response = new PUP(PupType.StringTimeReply, p.ID, p.SourcePort, localPort, bcplDateString.ToArray());
Dispatcher.Instance.SendPup(response);
}
private void SendAltoTimeReply(PUP p)
{
//
// From the spec, the response is:
// "10 bytes in all, organized as 5 16-bit words:
// words 0, 1 Present date and time: a 32-bit integer representing number of
// seconds since midnight, January 1, 1901, Greenwich Mean Time (GMT).
//
// word 2 Local time zone information. Bit 0 is zero if west of Greenwich
// and one if east. Bits 1-7 are the number of hours east or west of
// Greenwich. Bits 8-15 are an additional number of minutes.
//
// word 3 Day of the year on or before which Daylight Savings Time takes
// effect locally, where 1 = January 1 and 366 = Dcember 31. (The
// actual day is the next preceding Sunday.)
//
// word 4 Day of the year on or before which Daylight Savings Time ends. If
// Daylight Savings Time is not observed locally, both the start and
// end dates should be 366.
//
// The local time parameters in words 2 and 4 are those in effect at the server's
// location.
//
// So the Alto epoch is 1/1/1901. For the time being to keep things simple we're assuming
// GMT and no DST at all. TODO: make this take into account our TZ, etc.
//
DateTime currentTime = DateTime.Now;
// The epoch for .NET is 1/1/0001 at 12 midnight and is counted in 100-ns intervals.
// Some conversion is needed, is what I'm saying.
DateTime altoEpoch = new DateTime(1901, 1, 1);
TimeSpan timeSinceAltoEpoch = new TimeSpan(currentTime.Ticks - altoEpoch.Ticks);
UInt32 altoTime = (UInt32)timeSinceAltoEpoch.TotalSeconds;
// Build the response data
byte[] data = new byte[10];
Helpers.WriteUInt(ref data, altoTime, 0);
Helpers.WriteUShort(ref data, 0, 4); // Timezone, hardcoded to GMT
Helpers.WriteUShort(ref data, 366, 6); // DST info, not used right now.
Helpers.WriteUShort(ref data, 366, 8);
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket);
PUP response = new PUP(PupType.AltoTimeResponse, p.ID, p.SourcePort, localPort, data);
Dispatcher.Instance.SendPup(response);
}
private void SendAddressLookupReply(PUP p)
{
//
// Need to find more... useful documentation, but here's what I have:
// For the request PUP:
// A port (6 bytes).
//
// Response:
// A string consisting of an inter-network name expression that matches the request Port.
//
//
// I am at this time unsure what exactly an "inter-network name expression" consists of.
// Empirically, a simple string name seems to make the Alto happy.
//
//
// The request PUP contains a port address, we will check the host and network (and ignore the socket).
// and see if we have a match.
//
PUPPort lookupAddress = new PUPPort(p.Contents, 0);
string hostName = DirectoryServices.Instance.AddressLookup(lookupAddress);
if (!String.IsNullOrEmpty(hostName))
{
// We have a result, pack the name into the response.
BCPLString interNetworkName = new BCPLString(hostName);
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket);
PUP lookupReply = new PUP(PupType.AddressLookupResponse, p.ID, p.SourcePort, localPort, interNetworkName.ToArray());
Dispatcher.Instance.SendPup(lookupReply);
}
else
{
// Unknown host, send an error reply
BCPLString errorString = new BCPLString("Unknown host for address.");
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket);
PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, errorString.ToArray());
Dispatcher.Instance.SendPup(errorReply);
}
}
private void SendNameLookupReply(PUP p)
{
//
// For the request PUP:
// A string consisting of an inter-network name expression
//
// Response:
// One or more 6-byte blocks containing the address(es) corresponding to the
// name expression. Each block is a Pup Port structure, with the network and host numbers in
// the first two bytes and the socket number in the last four bytes.
//
//
// I'm not sure what would cause a name to resolve to multiple addresses at this time.
// Also still not sure what an 'inter-network name expression' is.
// As above, assuming this is a simple hostname.
//
BCPLString lookupName = new BCPLString(p.Contents);
HostAddress address = DirectoryServices.Instance.NameLookup(lookupName.ToString());
if (address != null)
{
// We found an address, pack the port into the response.
PUPPort lookupPort = new PUPPort(address, 0);
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket);
PUP lookupReply = new PUP(PupType.NameLookupResponse, p.ID, p.SourcePort, localPort, lookupPort.ToArray());
Dispatcher.Instance.SendPup(lookupReply);
}
else
{
// Unknown host, send an error reply
BCPLString errorString = new BCPLString("Unknown host for name.");
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket);
PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, errorString.ToArray());
Dispatcher.Instance.SendPup(errorReply);
}
}
}
}

316
PUP/PUP.cs Normal file
View File

@@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
public enum PupType
{
// Basic types
EchoMe = 1,
ImAnEcho = 2,
ImABadEcho = 3,
Error = 4,
// BSP/RFC types
RFC = 8,
Abort = 9,
End = 10,
EndReply = 11,
Data = 16,
AData = 17,
Ack = 18,
Mark = 19,
Interrupt = 20,
InterruptReply = 21,
AMark = 22,
// Misc. Services types
StringTimeRequest = 128,
StringTimeReply = 129,
TenexTimeRequest = 130,
TenexTimeReply = 131,
AltoTimeRequestOld = 132,
AltoTimeResponseOld = 133,
AltoTimeRequest = 134,
AltoTimeResponse = 135,
// Network Lookup
NameLookupRequest = 144,
NameLookupResponse = 145,
DirectoryLookupErrorReply = 146,
AddressLookupRequest = 147,
AddressLookupResponse = 148,
// Alto Boot
SendBootFileRequest = 164,
BootDirectoryRequest = 165,
BootDirectoryReply = 166,
}
public struct PUPPort
{
/// <summary>
/// Builds a new port address given network, host and socket parameters
/// </summary>
/// <param name="network"></param>
/// <param name="host"></param>
/// <param name="socket"></param>
public PUPPort(byte network, byte host, UInt32 socket)
{
Network = network;
Host = host;
Socket = socket;
}
/// <summary>
/// Builds a new port address given a HostAddress and a socket.
/// </summary>
/// <param name="address"></param>
/// <param name="socket"></param>
public PUPPort(HostAddress address, UInt32 socket)
{
Network = address.Network;
Host = address.Host;
Socket = socket;
}
/// <summary>
/// Builds a new port address from an array containing a raw port representaton
/// </summary>
/// <param name="rawData"></param>
/// <param name="offset"></param>
public PUPPort(byte[] rawData, int offset)
{
Network = rawData[offset];
Host = rawData[offset + 1];
Socket = Helpers.ReadUInt(rawData, 2);
}
/// <summary>
/// Writes this address back out to a raw byte array at the specified offset
/// </summary>
/// <param name="rawData"></param>
/// <param name="offset"></param>
public void WriteToArray(ref byte[] rawData, int offset)
{
rawData[offset] = Network;
rawData[offset + 1] = Host;
Helpers.WriteUInt(ref rawData, Socket, offset + 2);
}
/// <summary>
/// Same as above, but simply returns a new array instead of writing into an existing one.
/// </summary>
/// <returns></returns>
public byte[] ToArray()
{
byte[] a = new byte[6];
WriteToArray(ref a, 0);
return a;
}
public byte Network;
public byte Host;
public UInt32 Socket;
}
public class PUP
{
/// <summary>
/// Construct a new packet from the supplied data.
/// </summary>
/// <param name="rawPacket"></param>
public PUP(PupType type, UInt32 id, PUPPort destination, PUPPort source, byte[] contents)
{
_rawData = null;
// Ensure content length is <= 532 bytes. (Technically larger PUPs are allowed,
// but conventionally they are not used and I want to keep things safe.)
if (contents.Length > MAX_PUP_SIZE)
{
throw new InvalidOperationException("PUP size must not exceed 532 bytes.");
}
TransportControl = 0;
Type = type;
ID = id;
DestinationPort = destination;
SourcePort = source;
Contents = contents;
Length = (ushort)(PUP_HEADER_SIZE + PUP_CHECKSUM_SIZE + contents.Length);
// Stuff data into raw array
_rawData = new byte[Length];
Helpers.WriteUShort(ref _rawData, Length, 0);
_rawData[2] = TransportControl;
_rawData[3] = (byte)Type;
Helpers.WriteUInt(ref _rawData, ID, 4);
DestinationPort.WriteToArray(ref _rawData, 8);
SourcePort.WriteToArray(ref _rawData, 14);
Array.Copy(Contents, 0, _rawData, 20, Contents.Length);
// Calculate the checksum and stow it
Checksum = CalculateChecksum();
Helpers.WriteUShort(ref _rawData, Checksum, Length - 2);
}
/// <summary>
/// Same as above, but with no content (i.e. a zero-byte payload)
/// </summary>
/// <param name="type"></param>
/// <param name="id"></param>
/// <param name="destination"></param>
/// <param name="source"></param>
public PUP(PupType type, UInt32 id, PUPPort destination, PUPPort source) :
this(type, id, destination, source, new byte[0])
{
}
/// <summary>
/// Load in an existing packet from a stream
/// </summary>
/// <param name="stream"></param>
public PUP(MemoryStream stream)
{
_rawData = new byte[stream.Length];
stream.Read(_rawData, 0, (int)stream.Length);
// Read fields in. TODO: investigate more efficient ways to do this.
Length = Helpers.ReadUShort(_rawData, 0);
// Sanity check size:
if (Length > stream.Length)
{
throw new InvalidOperationException("Length field in PUP is invalid.");
}
TransportControl = _rawData[2];
Type = (PupType)_rawData[3];
ID = Helpers.ReadUInt(_rawData, 4);
DestinationPort = new PUPPort(_rawData, 8);
SourcePort = new PUPPort(_rawData, 14);
Array.Copy(_rawData, 20, Contents, 0, Length - PUP_HEADER_SIZE - PUP_CHECKSUM_SIZE);
Checksum = Helpers.ReadUShort(_rawData, Length - 2);
// Validate checksum
ushort cChecksum = CalculateChecksum();
if (cChecksum != Checksum)
{
throw new InvalidOperationException(String.Format("PUP checksum is invalid. ({0} vs {1}", Checksum, cChecksum));
}
}
public byte[] RawData
{
get { return _rawData; }
}
private ushort CalculateChecksum()
{
int i;
//
// This code "borrowed" from the Stanford PUP code
// and translated roughly to C#
//
Cksum cksm;
cksm.lcksm = 0;
cksm.scksm.ccksm = 0; // to make the C# compiler happy since it knows not of unions
cksm.scksm.cksm = 0;
for (i = 0; i < _rawData.Length - PUP_CHECKSUM_SIZE; i += 2)
{
ushort word = Helpers.ReadUShort(_rawData, i);
cksm.lcksm += word;
cksm.scksm.cksm += cksm.scksm.ccksm;
cksm.scksm.ccksm = 0;
cksm.lcksm <<= 1;
cksm.scksm.cksm += cksm.scksm.ccksm;
cksm.scksm.ccksm = 0;
}
if (cksm.scksm.cksm == 0xffff)
{
cksm.scksm.cksm = 0;
}
return cksm.scksm.cksm;
}
// Structs used by CalculateChecksum to simulate
// a C union in C#
[StructLayout(LayoutKind.Explicit)]
struct Scksum
{
[FieldOffset(0)]
public ushort ccksm;
[FieldOffset(2)]
public ushort cksm;
}
[StructLayout(LayoutKind.Explicit)]
struct Cksum
{
[FieldOffset(0)]
public ulong lcksm;
[FieldOffset(0)]
public Scksum scksm;
}
public readonly ushort Length;
public readonly byte TransportControl;
public readonly PupType Type;
public readonly UInt32 ID;
public readonly PUPPort DestinationPort;
public readonly PUPPort SourcePort;
public readonly byte[] Contents;
public readonly ushort Checksum;
private byte[] _rawData;
private const int MAX_PUP_SIZE = 532;
private const int PUP_HEADER_SIZE = 20;
private const int PUP_CHECKSUM_SIZE = 2;
}
public static class Helpers
{
public static ushort ReadUShort(byte[] data, int offset)
{
return (ushort)((data[offset] << 8) | data[offset + 1]);
}
public static UInt32 ReadUInt(byte[] data, int offset)
{
return (UInt32)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]);
}
public static void WriteUShort(ref byte[] data, ushort s, int offset)
{
data[offset] = (byte)(s >> 8);
data[offset + 1] = (byte)s;
}
public static void WriteUInt(ref byte[] data, UInt32 s, int offset)
{
data[offset] = (byte)(s >> 24);
data[offset + 1] = (byte)(s >> 16);
data[offset + 2] = (byte)(s >> 8);
data[offset + 3] = (byte)s;
}
}
}

60
PUP/PUPProtocolBase.cs Normal file
View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS
{
public enum ConnectionType
{
Connectionless, /* echo, name resolution, etc. */
BSP, /* FTP, Telnet, CopyDisk, etc. */
}
public struct PUPProtocolEntry
{
public PUPProtocolEntry(string friendlyName, UInt32 socket, ConnectionType connectionType, PUPProtocolBase implementation)
{
FriendlyName = friendlyName;
Socket = socket;
ConnectionType = connectionType;
ProtocolImplementation = implementation;
}
/// <summary>
/// Indicates the 'friendly' name for the protocol.
/// </summary>
public string FriendlyName;
/// <summary>
/// Indicates the socket used by the protocol
/// </summary>
public UInt32 Socket;
/// <summary>
/// Indicates the type of connection (connectionless or BSP-based)
/// </summary>
public ConnectionType ConnectionType;
public PUPProtocolBase ProtocolImplementation;
}
/// <summary>
/// Base class for all PUP-based protocols.
/// </summary>
public abstract class PUPProtocolBase
{
public PUPProtocolBase()
{
}
/// <summary>
/// Called by dispatcher to send incoming data destined for this protocol.
/// </summary>
/// <param name="p"></param>
public abstract void RecvData(PUP p);
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("PUP")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Vulcan Inc.")]
[assembly: AssemblyProduct("PUP")]
[assembly: AssemblyCopyright("Copyright © Vulcan Inc. 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("60ac4040-d43f-4d54-ada8-2ba64163973d")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

90
PUP/Transport/Ethernet.cs Normal file
View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PcapDotNet.Core;
using PcapDotNet.Packets;
namespace IFS.Transport
{
public struct EthernetInterface
{
public EthernetInterface(string name, string description)
{
Name = name;
Description = description;
}
public string Name;
public string Description;
}
/// <summary>
/// Defines interface "to the metal" (raw ethernet frames) which may wrap the underlying transport (for example, winpcap)
/// </summary>
public class Ethernet : IPacketInterface
{
public Ethernet(EthernetInterface iface, HandlePacket callback)
{
AttachInterface(iface);
_callback = callback;
}
public static List<EthernetInterface> EnumerateDevices()
{
List<EthernetInterface> interfaces = new List<EthernetInterface>();
foreach(LivePacketDevice device in LivePacketDevice.AllLocalMachine)
{
interfaces.Add(new EthernetInterface(device.Name, device.Description));
}
return interfaces;
}
public void Open(bool promiscuous, int timeout)
{
_communicator = _interface.Open(0xffff, promiscuous ? PacketDeviceOpenAttributes.Promiscuous : PacketDeviceOpenAttributes.None, timeout);
}
/// <summary>
/// Begin receiving packets, forever.
/// </summary>
public void BeginReceive()
{
_communicator.ReceivePackets(-1, ReceiveCallback);
}
private void ReceiveCallback(Packet p)
{
_callback(p);
}
private void AttachInterface(EthernetInterface iface)
{
_interface = null;
// Find the specified device by name
foreach (LivePacketDevice device in LivePacketDevice.AllLocalMachine)
{
if (device.Name == iface.Name)
{
_interface = device;
break;
}
}
if (_interface == null)
{
throw new InvalidOperationException("Requested interface not found.");
}
}
private LivePacketDevice _interface;
private PacketCommunicator _communicator;
private HandlePacket _callback;
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS.Transport
{
/// <summary>
/// PacketInterface provides an abstraction over a transport (Ethernet, IP, Carrier Pigeon)
/// which can provide raw packets.
/// </summary>
interface IPacketInterface
{
}
}