1
0
mirror of https://github.com/livingcomputermuseum/IFS.git synced 2026-03-03 02:07:47 +00:00

Added basic mail support. No routing.

This commit is contained in:
Josh Dersch
2016-06-06 14:57:17 -07:00
parent 0a8b83c315
commit c03d930bec
22 changed files with 806 additions and 198 deletions

View File

@@ -51,38 +51,51 @@ namespace IFS
}
public static UserToken Authenticate(string userName, string password)
{
{
//
// Look up the user
//
UserToken token = null;
if (_accounts.ContainsKey(userName))
//
// Verify that the username's host/registry (if present) matches
// our hostname.
//
if (ValidateUserRegistry(userName))
{
UserToken accountToken = _accounts[userName];
//
// Account exists; compare password hash against the hash of the password provided.
// (If there is no hash then no password is set and we do no check.)
//
if (!string.IsNullOrEmpty(accountToken.PasswordHash))
{
// Convert hash to base64 string and compare with actual password hash
if (ValidatePassword(accountToken, password))
// Strip off any host/registry on the username, lookup based on username only.
//
userName = GetUserNameFromFullName(userName);
if (_accounts.ContainsKey(userName))
{
UserToken accountToken = _accounts[userName];
//
// Account exists; compare password hash against the hash of the password provided.
// (If there is no hash then no password is set and we do no check.)
//
if (!string.IsNullOrEmpty(accountToken.PasswordHash))
{
// Yay!
// Convert hash to base64 string and compare with actual password hash
if (ValidatePassword(accountToken, password))
{
// Yay!
token = accountToken;
}
else
{
// No match, password is incorrect.
token = null;
}
}
else if (string.IsNullOrEmpty(password))
{
// Just ensure both passwords are empty.
token = accountToken;
}
else
{
// No match, password is incorrect.
token = null;
}
}
else if (string.IsNullOrEmpty(password))
{
// Just ensure both passwords are empty.
token = accountToken;
}
}
@@ -164,6 +177,57 @@ namespace IFS
return bSuccess;
}
/// <summary>
/// Verifies whether the specified user account is registered.
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public static bool UserExists(string userName)
{
return _accounts.ContainsKey(userName);
}
/// <summary>
/// Given a full user name (i.e. username.HOST), validates that the
/// HOST (or "registry") portion matches our hostname.
/// </summary>
/// <param name="fullUserName"></param>
/// <returns></returns>
public static bool ValidateUserRegistry(string fullUserName)
{
if (fullUserName.Contains("."))
{
// Strip off the host/registry name and compare to our hostname.
string hostName = fullUserName.Substring(fullUserName.IndexOf(".") + 1);
return hostName.ToLowerInvariant() == DirectoryServices.Instance.LocalHostName.ToLowerInvariant();
}
else
{
// No registry appended, we assume this is destined for us by default.
return true;
}
}
/// <summary>
/// Given a full user name (i.e. username.HOST), returns only the username portion.
/// </summary>
/// <param name="fullUserName"></param>
/// <returns></returns>
public static string GetUserNameFromFullName(string fullUserName)
{
// If user name has a host/registry appended, we will strip it off.
if (fullUserName.Contains("."))
{
return fullUserName.Substring(0, fullUserName.IndexOf("."));
}
else
{
return fullUserName;
}
}
private static bool ValidatePassword(UserToken accountToken, string password)
{
// Convert to UTF-8 byte array

View File

@@ -564,7 +564,7 @@ namespace IFS.BSP
// Ensure things are set up
EstablishWindow();
_outputWindowLock.EnterUpgradeableReadLock();
_outputWindowLock.EnterUpgradeableReadLock();
if (_outputWindow.Count < _clientLimits.MaxPups)
{
@@ -572,7 +572,7 @@ namespace IFS.BSP
// There's space in the window, so go for it.
//
_outputWindowLock.EnterWriteLock();
_outputWindow.Add(p);
_outputWindow.Add(p);
_outputWindowLock.ExitWriteLock();
}
else

View File

@@ -6,14 +6,15 @@
# Debug settings
LogTypes = Error
LogTypes = Verbose
LogComponents = All
# Normal configuration
FTPRoot = c:\ifs\ftp
CopyDiskRoot = c:\ifs\copydisk
BootRoot = c:\ifs\boot
MailRoot = c:\ifs\mail
InterfaceType = RAW
InterfaceName = Ethernet 2
InterfaceName = Ethernet
ServerNetwork = 1
ServerHost = 1

View File

@@ -47,6 +47,11 @@ namespace IFS
throw new InvalidConfigurationException("Boot root path is invalid.");
}
if (string.IsNullOrWhiteSpace(BootRoot) || !Directory.Exists(MailRoot))
{
throw new InvalidConfigurationException("Mail root path is invalid.");
}
if (MaxWorkers < 1)
{
throw new InvalidConfigurationException("MaxWorkers must be >= 1.");
@@ -88,6 +93,11 @@ namespace IFS
/// </summary>
public static readonly string BootRoot;
/// <summary>
/// The root directory for the Mail file store.
/// </summary>
public static readonly string MailRoot;
/// <summary>
/// The maximum number of worker threads for protocol handling.
/// </summary>

View File

@@ -36,12 +36,21 @@ namespace IFS
{
// Get our host address; for now just hardcode it.
// TODO: need to define config files, etc.
_localHost = new HostAddress((byte)Configuration.ServerNetwork, (byte)Configuration.ServerHost);
_localHost = new HostAddress((byte)Configuration.ServerNetwork, (byte)Configuration.ServerHost);
// Load in hosts table from hosts file.
LoadHostTable();
// Look up our hostname in the table.
_localHostName = AddressLookup(_localHost);
if (_localHostName == null)
{
// Our name isn't in the hosts table.
Log.Write(LogType.Error, LogComponent.DirectoryServices, "Warning: local host name not specified in hosts table Defaulting to 'unset'.");
_localHostName = "unset";
}
Log.Write(LogComponent.DirectoryServices, "Directory services initialized.");
}
@@ -103,6 +112,11 @@ namespace IFS
get { return _localHost.Host; }
}
public string LocalHostName
{
get { return _localHostName; }
}
private void LoadHostTable()
{
_hostAddressTable = new Dictionary<byte, Dictionary<byte, string>>();
@@ -259,6 +273,11 @@ namespace IFS
/// </summary>
private HostAddress _localHost;
/// <summary>
/// Our name.
/// </summary>
private string _localHostName;
/// <summary>
/// Hash table for address resolution; outer hash finds the dictionary
/// for a given network, inner hash finds names for hosts.

View File

@@ -46,6 +46,7 @@ namespace IFS
// RTP/BSP based:
PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("CopyDisk", 0x15 /* 25B */, ConnectionType.BSP, typeof(CopyDiskWorker)));
PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("FTP", 0x3, ConnectionType.BSP, typeof(FTPWorker)));
PUPProtocolDispatcher.Instance.RegisterProtocol(new PUPProtocolEntry("Mail", 0x7, ConnectionType.BSP, typeof(FTPWorker)));
// Breath Of Life
_breathOfLifeServer = new BreathOfLife();

View File

@@ -7,11 +7,13 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using IFS.Mail;
namespace IFS.FTP
{
public enum FTPCommand
{
// Standard FTP
Invalid = 0,
Retrieve = 1,
Store = 2,
@@ -27,10 +29,17 @@ namespace IFS.FTP
NewEnumerate = 12,
Delete = 14,
Rename = 15,
// Mail-specific
StoreMail = 16,
RetrieveMail = 17,
FlushMailbox = 18,
MailboxException = 19,
}
public enum NoCode
{
// Standard FTP
UnimplmentedCommand = 1,
UserNameRequired = 2,
IllegalCommand = 3,
@@ -64,7 +73,12 @@ namespace IFS.FTP
TransientServerFailure = 71,
PermamentServerFailure = 72,
FileBusy = 73,
FileAlreadyExists = 74
FileAlreadyExists = 74,
// Mail-specific
NoValidMailbox = 32,
IllegalMailboxSyntax = 33,
IllegalSender = 34,
}
struct FTPYesNoVersion
@@ -210,7 +224,7 @@ namespace IFS.FTP
// 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);
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for delete is '{0}'.", fileSpec);
PropertyList pl = new PropertyList(fileSpec);
@@ -218,6 +232,61 @@ namespace IFS.FTP
}
break;
case FTPCommand.RetrieveMail:
{
// Argument to Retrieve-Mail is a property list (string).
//
string mailSpec = Helpers.ArrayToString(data);
Log.Write(LogType.Verbose, LogComponent.FTP, "Mailbox spec for retrieve-mail is '{0}'.", mailSpec);
PropertyList pl = new PropertyList(mailSpec);
RetrieveMail(pl);
}
break;
case FTPCommand.FlushMailbox:
{
if (_lastRetrievedMailFiles != null)
{
foreach (string mailFileToDelete in _lastRetrievedMailFiles)
{
MailManager.DeleteMail(_lastRetrievedMailbox, mailFileToDelete);
}
}
SendFTPYesResponse("Retreived mail flushed from mailbox.");
}
break;
case FTPCommand.StoreMail:
{
//
// Argument to Retrieve-Mail is one or more property lists.
//
string mailSpec = Helpers.ArrayToString(data);
Log.Write(LogType.Verbose, LogComponent.FTP, "Mailbox spec for store-mail is '{0}'.", mailSpec);
//
// Annoyingly, the argument is numerous property lists with no delimiter, not a single list.
//
List<PropertyList> recipients = new List<PropertyList>();
int currentIndex = 0;
while (currentIndex < mailSpec.Length)
{
int endIndex = 0;
PropertyList pl = new PropertyList(mailSpec, currentIndex, out endIndex);
recipients.Add(pl);
currentIndex = endIndex;
}
StoreMail(recipients);
}
break;
default:
Log.Write(LogType.Warning, LogComponent.FTP, "Unhandled FTP command {0}.", command);
break;
@@ -631,6 +700,243 @@ namespace IFS.FTP
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
}
/// <summary>
/// Retrieves mail files for the specified mailbox.
/// </summary>
/// <param name="fileSpec"></param>
private void RetrieveMail(PropertyList mailSpec)
{
//
// The property list must include a Mailbox property that identifies the mailbox to be opened.
//
if (!mailSpec.ContainsPropertyValue(KnownPropertyNames.Mailbox))
{
SendFTPNoResponse(NoCode.NoValidMailbox, "No mailbox specified.");
return;
}
_lastRetrievedMailbox = mailSpec.GetPropertyValue(KnownPropertyNames.Mailbox);
//
// Validate that the requested mailbox's registry is on this server.
//
if (!Authentication.ValidateUserRegistry(_lastRetrievedMailbox))
{
SendFTPNoResponse(NoCode.NoValidMailbox, "Incorrect registry for this server.");
return;
}
_lastRetrievedMailbox = Authentication.GetUserNameFromFullName(_lastRetrievedMailbox);
//
// Authenticate and see if the user has access to the requested mailbox.
// In our implementation, the username and mailbox name are one and the same, if the two don't match
// (or if authentication failed) then we're done.
//
UserToken user = AuthenticateUser(mailSpec);
if (user == null)
{
SendFTPNoResponse(NoCode.IllegalConnectPassword, "Invalid username or password for mailbox.");
return;
}
if (user.UserName.ToLowerInvariant() != _lastRetrievedMailbox.ToLowerInvariant())
{
SendFTPNoResponse(NoCode.AccessDenied, "You do not have access to the specified mailbox.");
return;
}
//
// All clear at this point. If the user has any mail in his/her mailbox, send it now.
//
_lastRetrievedMailFiles = MailManager.EnumerateMail(_lastRetrievedMailbox);
if (_lastRetrievedMailFiles != null)
{
foreach (string mailFile in _lastRetrievedMailFiles)
{
using (Stream mailStream = MailManager.RetrieveMail(_lastRetrievedMailbox, mailFile))
{
Log.Write(LogType.Verbose, LogComponent.FTP, "Preparing to send mail file {0}.", mailFile);
//
// Build a property list for the mail message.
//
PropertyList mailProps = new PropertyList();
mailProps.SetPropertyValue(KnownPropertyNames.Length, mailStream.Length.ToString());
mailProps.SetPropertyValue(KnownPropertyNames.DateReceived, MailManager.GetReceivedTime(_lastRetrievedMailbox, mailFile));
mailProps.SetPropertyValue(KnownPropertyNames.Opened, "No");
mailProps.SetPropertyValue(KnownPropertyNames.Deleted, "No");
mailProps.SetPropertyValue(KnownPropertyNames.Type, "Text"); // We treat all mail as text
mailProps.SetPropertyValue(KnownPropertyNames.ByteSize, "8"); // 8-bit bytes, please.
//
// Send the property list (without EOC)
//
_channel.SendMark((byte)FTPCommand.HereIsPropertyList, false);
_channel.Send(Helpers.StringToArray(mailProps.ToString()));
//
// Send the mail text.
//
_channel.SendMark((byte)FTPCommand.HereIsFile, true);
byte[] data = new byte[512];
while (true)
{
int read = mailStream.Read(data, 0, data.Length);
if (read == 0)
{
// Nothing to send, we're done.
break;
}
Log.Write(LogType.Verbose, LogComponent.FTP, "Sending mail data, current file position {0}.", mailStream.Position);
_channel.Send(data, read, true);
if (read < data.Length)
{
// Short read, end of file.
break;
}
}
Log.Write(LogType.Verbose, LogComponent.FTP, "Mail file {0} sent.", mailFile);
}
}
}
// All done, send a Yes/EOC to terminate the exchange.
SendFTPYesResponse("Mail retrieved.");
}
/// <summary>
/// Stores mail files to the specified mailboxes.
/// </summary>
/// <param name="fileSpec"></param>
private void StoreMail(List<PropertyList> mailSpecs)
{
//
// There are two defined properties: Mailbox and Sender.
// The Sender field can really be anything and real IFS servers
// did not authenticate or verify this name. We will simply ignore
// it since we can't really do anything useful with it.
// (I do like the total lack of security in this protocol.)
//
List<string> destinationMailboxes = new List<string>();
foreach (PropertyList mailSpec in mailSpecs)
{
if (!mailSpec.ContainsPropertyValue(KnownPropertyNames.Mailbox))
{
Log.Write(LogType.Verbose, LogComponent.Mail, "No mailbox specified, aborting.");
SendFTPNoResponse(NoCode.NoValidMailbox, "No mailbox specified.");
return;
}
string destinationMailbox = mailSpec.GetPropertyValue(KnownPropertyNames.Mailbox);
//
// Validate that the destination mailbox's registry is on this server.
// We do not support forwarding or routing of mail, so mail intended for another server
// will never get there.
//
if (!Authentication.ValidateUserRegistry(destinationMailbox))
{
SendFTPNoResponse(NoCode.NoValidMailbox, "Incorrect registry for this server. Mail forwarding not supported.");
return;
}
destinationMailbox = Authentication.GetUserNameFromFullName(destinationMailbox);
// Verify that the user we're sending this to actually exists...
if (!Authentication.UserExists(destinationMailbox))
{
Log.Write(LogType.Verbose, LogComponent.Mail, "Mailbox {0} does not exist, aborting.", destinationMailbox);
SendFTPNoResponse(NoCode.NoValidMailbox, "The specified mailbox does not exist.");
return;
}
destinationMailboxes.Add(destinationMailbox);
}
// OK so far, send a Yes and wait for a file.
SendFTPYesResponse("Go ahead.");
//
// 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.
//
bool success = true;
FTPCommand lastMark;
byte[] buffer;
try
{
Log.Write(LogType.Verbose, LogComponent.Mail, "Receiving mail file to memory...");
// 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);
Log.Write(LogType.Verbose, LogComponent.Mail, "Received {0} bytes.", buffer.Length);
// Write out to files
foreach (string destination in destinationMailboxes)
{
using (Stream mailFile = MailManager.StoreMail(destination))
{
mailFile.Write(buffer, 0, buffer.Length);
Log.Write(LogType.Verbose, LogComponent.Mail, "Wrote {0} bytes to mail file in mailbox {1}.", buffer.Length, destination);
}
}
}
catch (Exception e)
{
// We failed while writing the mail 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.Mail, "Failed to write mail file. Error '{1}'. Aborting.", 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.Mail, "Client success code is {0}, {1}, '{2}'", lastMark, clientYesNo.Code, clientYesNo.Code);
if (!success)
{
// TODO: provide actual No codes.
SendFTPNoResponse(NoCode.TransientServerFailure, "Mail transfer failed.");
}
else
{
SendFTPYesResponse("Mail transfer completed.");
}
}
/// <summary>
/// Open the file specified by the provided PropertyList
/// </summary>
@@ -818,8 +1124,8 @@ namespace IFS.FTP
{
password = fileSpec.GetPropertyValue(KnownPropertyNames.UserPassword);
}
UserToken user = Authentication.Authenticate(userName, password);
UserToken user = Authentication.Authenticate(userName, password);
if (user == null)
{
@@ -847,7 +1153,7 @@ namespace IFS.FTP
string userDirPath = Path.Combine(Configuration.FTPRoot, userToken.HomeDirectory);
return fullPath.StartsWith(userDirPath, StringComparison.OrdinalIgnoreCase);
}
}
private void SendFTPResponse(FTPCommand responseCommand, object data)
{
@@ -896,5 +1202,13 @@ namespace IFS.FTP
private Thread _workerThread;
private bool _running;
/// <summary>
/// The last set of mail files retrieved via a Retrieve-Mail operation.
/// Saved so that Flush-Mailbox can use it to delete only those mails that
/// were last pulled.
/// </summary>
private IEnumerable<string> _lastRetrievedMailFiles;
private string _lastRetrievedMailbox;
}
}

View File

@@ -34,6 +34,13 @@ namespace IFS.FTP
public static readonly string Author = "Author";
public static readonly string Checksum = "Checksum";
public static readonly string DesiredProperty = "Desired-Property";
// Mail
public static readonly string Mailbox = "Mailbox";
public static readonly string Length = "Length";
public static readonly string DateReceived = "Date-Received";
public static readonly string Opened = "Opened";
public static readonly string Deleted = "Deleted";
}
/// <summary>
@@ -76,9 +83,25 @@ namespace IFS.FTP
_propertyList = new Dictionary<string, string>();
}
/// <summary>
/// Parses a property list from the specified string.
/// </summary>
/// <param name="list"></param>
public PropertyList(string list) : this()
{
ParseList(list);
ParseList(list, 0);
}
/// <summary>
/// Parses a property list from the specified string at the given starting offset.
/// endIndex returns the end of the parsed property list in the string.
/// </summary>
/// <param name="input"></param>
/// <param name="startIndex"></param>
/// <param name="endIndex"></param>
public PropertyList(string input, int startIndex, out int endIndex) : this()
{
endIndex = ParseList(input, startIndex);
}
/// <summary>
@@ -171,8 +194,10 @@ namespace IFS.FTP
/// Parses a string representation of a property list into our hash table.
/// </summary>
/// <param name="list"></param>
private void ParseList(string list)
private int ParseList(string input, int startOffset)
{
string list = input.Substring(startOffset);
//
// 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.
@@ -191,8 +216,14 @@ namespace IFS.FTP
//
// Loop until we hit the end of the string (minus the closing paren)
//
while (index < list.Length - 1)
while (index < list.Length)
{
// If this is a closing paren, this denotes the end of the property list.
if (list[index] == ')')
{
break;
}
// Start of next property, must begin with a left paren.
if (list[index] != '(')
{
@@ -273,6 +304,8 @@ namespace IFS.FTP
throw new InvalidOperationException(String.Format("Duplicate property entry for '{0}", propertyName));
}
}
return index + startOffset + 1;
}
private Dictionary<string, string> _propertyList;

View File

@@ -50,17 +50,41 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="PcapDotNet.Base">
<Reference Include="PcapDotNet.Base, Version=1.0.2.21699, Culture=neutral, PublicKeyToken=4b6f3e583145a652, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>pcap\PcapDotNet.Base.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Core">
<Reference Include="PcapDotNet.Core, Version=1.0.2.21711, Culture=neutral, PublicKeyToken=4b6f3e583145a652, processorArchitecture=x86">
<SpecificVersion>False</SpecificVersion>
<HintPath>pcap\PcapDotNet.Core.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Core.Extensions">
<Reference Include="PcapDotNet.Core.Extensions, Version=1.0.2.21712, Culture=neutral, PublicKeyToken=4b6f3e583145a652, processorArchitecture=x86">
<SpecificVersion>False</SpecificVersion>
<HintPath>pcap\PcapDotNet.Core.Extensions.dll</HintPath>
</Reference>
<Reference Include="PcapDotNet.Packets">
<Reference Include="PcapDotNet.Packets, Version=1.0.2.21701, Culture=neutral, PublicKeyToken=4b6f3e583145a652, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>pcap\PcapDotNet.Packets.dll</HintPath>
</Reference>
<Reference Include="System" />
@@ -95,6 +119,7 @@
<Compile Include="FTP\PropertyList.cs" />
<Compile Include="Logging\Log.cs" />
<Compile Include="GatewayInformationProtocol.cs" />
<Compile Include="Mail\MailManager.cs" />
<Compile Include="MiscServicesProtocol.cs" />
<Compile Include="Serializer.cs" />
<Compile Include="Transport\Ethernet.cs" />

View File

@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.23107.0
VisualStudioVersion = 14.0.25123.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IFS", "IFS.csproj", "{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}"
EndProject
@@ -9,18 +9,23 @@ Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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}.Debug|x64.ActiveCfg = Debug|x64
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Debug|x64.Build.0 = Debug|x64
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Debug|x86.ActiveCfg = Debug|x86
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Debug|x86.Build.0 = Debug|x86
{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
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|x64.ActiveCfg = Release|x64
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|x64.Build.0 = Release|x64
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|x86.ActiveCfg = Release|x86
{5C0BBE4B-76AB-4AC1-8691-F19D8D282DCB}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -24,6 +24,7 @@ namespace IFS.Logging
EFTP = 0x200,
BootServer = 0x400,
UDP = 0x800,
Mail = 0x1000,
Configuration = 0x1000,
All = 0x7fffffff

153
PUP/Mail/MailManager.cs Normal file
View File

@@ -0,0 +1,153 @@
using IFS.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IFS.Mail
{
/// <summary>
/// MailManager implements the filesystem-based portions of the mail system.
/// (The network transport portions are lumped in with the FTP Server).
///
/// It provides methods for retrieving, deleting, and storing mail to/from
/// a specific mailbox.
///
/// Mailboxes in our implementation are provided for each user account. There
/// is one mailbox per user, and the mailbox name is the same as the user's login
/// name. Authentication is handled as one would expect -- each user has read/delete
/// access only to his/her own mailbox, all other mailboxes can only be sent to.
///
/// Mailbox directories are lazy-init -- they're only created when a user receives an
/// e-mail.
///
/// The guest account has its own mailbox, shared by all guest users.
///
/// Each user's mailbox is stored in a subdirectory of the Mail directory.
/// Rather than keeping a single mail file that must be appended and maintained, each
/// mail that is added to a mailbox is an individual text file.
///
/// This class does no authentication, it merely handles the tedious chore of managing
/// users' mailboxes. (See the FTP server, where auth takes place.)
/// </summary>
public static class MailManager
{
static MailManager()
{
}
/// <summary>
/// Retrieves a list of mail files for the specified mailbox.
/// These are not full paths, and are relative to the mailbox in question.
/// Use RetrieveMail to retrieve an individual mail.
/// </summary>
/// <param name="mailbox"></param>
/// <returns></returns>
public static IEnumerable<string> EnumerateMail(string mailbox)
{
if (Directory.Exists(GetMailboxPath(mailbox)))
{
// Get the mail files in this directory
return Directory.EnumerateFiles(GetMailboxPath(mailbox), "*.mail", SearchOption.TopDirectoryOnly);
}
else
{
// No mail by default, the mailbox does not exist at this time.
return null;
}
}
/// <summary>
/// Retrieves a stream for the given mail file in the specified mailbox.
/// </summary>
/// <param name="mailbox"></param>
/// <param name="mailFile"></param>
/// <returns></returns>
public static Stream RetrieveMail(string mailbox, string mailFile)
{
if (File.Exists(GetMailboxPathForFile(mailbox, mailFile)))
{
//
// Open the requested mail file.
//
return new FileStream(GetMailboxPathForFile(mailbox, mailFile), FileMode.Open, FileAccess.Read);
}
else
{
// This shouldn't normally happen, but we handle it gracefully if it does.
Log.Write(LogType.Verbose, LogComponent.Mail, "Specified mail file {0} does not exist in mailbox {1}.", mailFile, mailbox);
return null;
}
}
public static string GetReceivedTime(string mailbox, string mailFile)
{
if (File.Exists(GetMailboxPathForFile(mailbox, mailFile)))
{
//
// Open the requested mail file.
//
return "26-MAY-79 02:33:00";
//return File.GetCreationTime(GetMailboxPathForFile(mailbox, mailFile)).ToString("dd-MMM-yy HH:mm:ss");
}
else
{
// This shouldn't normally happen, but we handle it gracefully if it does.
Log.Write(LogType.Verbose, LogComponent.Mail, "Specified mail file {0} does not exist in mailbox {1}.", mailFile, mailbox);
return String.Empty;
}
}
/// <summary>
/// Deletes the specified mail file from the specified mailbox.
/// </summary>
/// <param name="mailbox"></param>
/// <param name="mailFile"></param>
public static void DeleteMail(string mailbox, string mailFile)
{
if (File.Exists(GetMailboxPathForFile(mailbox, mailFile)))
{
//
// Delete the requested mail file.
//
File.Delete(GetMailboxPathForFile(mailbox, mailFile));
}
else
{
// This shouldn't normally happen, but we handle it gracefully if it does.
Log.Write(LogType.Verbose, LogComponent.Mail, "Specified mail file {0} does not exist in mailbox {1}.", mailFile, mailbox);
}
}
public static Stream StoreMail(string mailbox)
{
string newMailFile = Path.GetRandomFileName() + ".mail";
//
// Create the user's mail directory if it doesn't already exist.
//
if (!Directory.Exists(GetMailboxPath(mailbox)))
{
Directory.CreateDirectory(GetMailboxPath(mailbox));
}
//
// Create the new mail file.
//
return new FileStream(GetMailboxPathForFile(mailbox, newMailFile), FileMode.CreateNew, FileAccess.ReadWrite);
}
private static string GetMailboxPath(string mailbox)
{
return Path.Combine(Configuration.MailRoot, mailbox);
}
private static string GetMailboxPathForFile(string mailbox, string mailFile)
{
return Path.Combine(Configuration.MailRoot, mailbox, mailFile);
}
}
}

View File

@@ -1,7 +1,7 @@
using IFS.Boot;
using IFS.EFTP;
using IFS.Logging;
using IFS.Mail;
using System;
using System.Collections.Generic;
using System.IO;
@@ -88,7 +88,15 @@ namespace IFS
case PupType.BootDirectoryRequest:
SendBootDirectory(p);
break;
break;
case PupType.AuthenticateRequest:
SendAuthenticationResponse(p);
break;
case PupType.MailCheckRequestLaurel:
SendMailCheckResponse(p);
break;
default:
Log.Write(LogComponent.MiscServices, String.Format("Unhandled misc. protocol {0}", p.Type));
@@ -319,6 +327,87 @@ namespace IFS
}
private void SendAuthenticationResponse(PUP p)
{
//
// "Pup Contents: Two Mesa strings (more precisely StringBodys), packed in such a way that
// the maxLength of the first string may be used to locate the second string. The first
// string is a user name and the second a password."
//
// I have chosen not to write a helper class encapsulating Mesa strings since this is the
// first (and so far *only*) instance in which Mesa strings are used in IFS communications.
//
// Empirical analysis shows the format of a Mesa string to be:
// Word 1: Length (bytes)
// Word 2: MaxLength (bytes)
// Byte 4 thru 4 + MaxLength: string data
// data is padded to a word length.
//
string userName = Helpers.MesaArrayToString(p.Contents, 0);
int passwordOffset = (userName.Length % 2) == 0 ? userName.Length : userName.Length + 1;
string password = Helpers.MesaArrayToString(p.Contents, passwordOffset + 4);
UserToken token = Authentication.Authenticate(userName, password);
if (token == null)
{
string errorString = "Invalid username or password.";
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket);
PUP errorReply = new PUP(PupType.AuthenticateNegativeResponse, p.ID, p.SourcePort, localPort, Helpers.StringToArray(errorString));
PUPProtocolDispatcher.Instance.SendPup(errorReply);
}
else
{
// S'ok!
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket);
PUP okReply = new PUP(PupType.AuthenticatePositiveResponse, p.ID, p.SourcePort, localPort, new byte[] { });
PUPProtocolDispatcher.Instance.SendPup(okReply);
}
}
private void SendMailCheckResponse(PUP p)
{
//
// "Pup Contents: A string specifying the mailbox name."
//
//
// See if there is any mail for the specified mailbox.
//
string mailboxName = Helpers.ArrayToString(p.Contents);
//
// If mailbox name has a host/registry appended, we will strip it off.
// TODO: probably should validate host...
//
if (mailboxName.Contains("."))
{
mailboxName = mailboxName.Substring(0, mailboxName.IndexOf("."));
}
IEnumerable<string> mailList = MailManager.EnumerateMail(mailboxName);
if (mailList == null || mailList.Count() == 0)
{
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket);
PUP noMailReply = new PUP(PupType.NoNewMailExistsReply, p.ID, p.SourcePort, localPort, new byte[] { });
PUPProtocolDispatcher.Instance.SendPup(noMailReply);
}
else
{
PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket);
PUP mailReply = new PUP(PupType.NewMailExistsReply, p.ID, p.SourcePort, localPort, Helpers.StringToArray("You've got mail!"));
PUPProtocolDispatcher.Instance.SendPup(mailReply);
}
}
private struct BootDirectoryBlock
{
public ushort FileNumber;

View File

@@ -49,6 +49,13 @@ namespace IFS
AltoTimeRequest = 134,
AltoTimeResponse = 135,
// Mail check
MailCheckRequestMsg = 136,
NewMailExistsReply = 137,
NoNewMailExistsReply = 138,
NoSuchMailboxReply = 139,
MailCheckRequestLaurel = 140,
// Network Lookup
NameLookupRequest = 144,
NameLookupResponse = 145,
@@ -431,5 +438,25 @@ namespace IFS
return sb.ToString();
}
public static string MesaArrayToString(byte[] a, int offset)
{
ushort length = ReadUShort(a, offset);
ushort maxLength = ReadUShort(a, offset + 2);
if (maxLength + offset > a.Length)
{
return String.Empty;
}
StringBuilder sb = new StringBuilder(maxLength);
for (int i = 0; i < maxLength; i++)
{
sb.Append((char)(a[i + offset + 4]));
}
return sb.ToString();
}
}
}

View File

@@ -102,6 +102,7 @@ namespace IFS
pup.DestinationPort.Host != DirectoryServices.Instance.LocalHost) // Not our address.
{
// Do nothing with this PUP.
Log.Write(LogType.Verbose, LogComponent.PUP, "PUP is neither broadcast nor for us. Discarding.");
return;
}

View File

@@ -35,7 +35,7 @@ namespace IFS.Transport
_callback = callback;
// Now that we have a callback we can start receiving stuff.
Open(false /* not promiscuous */, int.MaxValue);
Open(true /* promiscuous */, int.MaxValue);
// Kick off the receiver thread, this will never return or exit.
Thread receiveThread = new Thread(new ThreadStart(BeginReceive));
@@ -175,7 +175,9 @@ namespace IFS.Transport
// Filter out encapsulated 3mbit frames and look for PUPs, forward them on.
//
if ((int)p.Ethernet.EtherType == _3mbitFrameType)
{
{
Log.Write(LogType.Verbose, LogComponent.Ethernet, "3mbit pup received.");
MemoryStream packetStream = p.Ethernet.Payload.ToMemoryStream();
// Read the length prefix (in words), convert to bytes.

Binary file not shown.

Binary file not shown.

View File

@@ -723,7 +723,6 @@ Win32 specific. Number of packets captured, i.e number of packets that are accep
<member name="P:PcapDotNet.Core.PacketTotalStatistics.PacketsDroppedByInterface">
<summary>
Number of packets dropped by the interface.
Not yet supported.
</summary>
</member>
<member name="P:PcapDotNet.Core.PacketTotalStatistics.PacketsDroppedByDriver">

Binary file not shown.

View File

@@ -439,13 +439,6 @@
Obsoleted.
</summary>
</member>
<member name="F:PcapDotNet.Packets.IpV4.IpV4OptionType.MaximumTransmissionUnitReply">
<summary>
MTU Reply.
RFCs 1063, 1191.
Obsoleted.
</summary>
</member>
<member name="F:PcapDotNet.Packets.IpV4.IpV4OptionType.QuickStart">
<summary>
Quick Start (QS). RFC 4782.
@@ -474,12 +467,6 @@
Used to route the internet datagram based on information supplied by the source.
</summary>
</member>
<member name="F:PcapDotNet.Packets.IpV4.IpV4OptionType.CommercialSecurity">
<summary>
http://tools.ietf.org/html/draft-ietf-cipso-ipsecurity
CIPSO - Commercial Security.
</summary>
</member>
<member name="F:PcapDotNet.Packets.IpV4.IpV4OptionType.StrictSourceRouting">
<summary>
Strict Source Routing.
@@ -12448,7 +12435,7 @@
</member>
<member name="F:PcapDotNet.Packets.Icmp.IcmpMessageType.ParameterProblem">
<summary>
RFCs 792, 4884.
RFC 792.
<para>
If the gateway or host processing a datagram finds a problem with the header parameters such that it cannot complete processing the datagram it must discard the datagram.
@@ -12957,18 +12944,9 @@
</member>
<member name="M:PcapDotNet.Packets.Dns.DnsOptionUpdateLease.#ctor(System.Int32)">
<summary>
Builds an instance from a least value.
Builds
</summary>
<param name="lease">
Indicating the lease life, in seconds, desired by the client.
In Update Responses, this field contains the actual lease granted by the server.
Note that the lease granted by the server may be less than, greater than, or equal to the value requested by the client.
To reduce network and server load, a minimum lease of 30 minutes (1800 seconds) is recommended.
Note that leases are expected to be sufficiently long as to make timer discrepancies (due to transmission latency, etc.)
between a client and server negligible.
Clients that expect the updated records to be relatively static may request appropriately longer leases.
Servers may grant relatively longer or shorter leases to reduce network traffic due to refreshes, or reduce stale data, respectively.
</param>
<param name="lease"></param>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsOptionUpdateLease.Lease">
<summary>
@@ -13051,86 +13029,6 @@
The number of bytes the option data takes.
</summary>
</member>
<member name="T:PcapDotNet.Packets.Dns.DnsOptionClientSubnet">
<summary>
https://tools.ietf.org/html/draft-ietf-dnsop-edns-client-subnet
<pre>
+-----+----------------+---------------+
| bit | 0-7 | 8-15 |
+-----+----------------+---------------+
| 0 | FAMILY |
+-----+----------------+---------------+
| 16 | SOURCE NETMASK | SCOPE NETMASK |
+-----+----------------+---------------+
| 32 | ADDRESS |
| ... | |
+-----+--------------------------------+
</pre>
</summary>
</member>
<member name="F:PcapDotNet.Packets.Dns.DnsOptionClientSubnet.MinimumDataLength">
<summary>
The minimum number of bytes this option data can take.
</summary>
</member>
<member name="M:PcapDotNet.Packets.Dns.DnsOptionClientSubnet.#ctor(PcapDotNet.Packets.AddressFamily,System.Byte,System.Byte,PcapDotNet.Packets.DataSegment)">
<summary>
Create a DNS Client Subnet option from family, source netmask, scope netmask and address fields.
</summary>
<param name="family">Indicates the family of the address contained in the option.</param>
<param name="sourceNetmask">
Representing the length of the netmask pertaining to the query.
In replies, it mirrors the same value as in the requests.
It can be set to 0 to disable client-based lookups, in which case the Address field must be absent.
</param>
<param name="scopeNetmask">
Representing the length of the netmask pertaining to the reply.
In requests, it should be set to the longest cacheable length supported by the Intermediate Nameserver.
In requests it may be set to 0 to have the Authoritative Nameserver treat the longest cacheable length as the SourceNetmask length.
In responses, this field is set by the Authoritative Nameserver to indicate the coverage of the response.
It might or might not match SourceNetmask; it could be shorter or longer.
</param>
<param name="address">
Contains either an IPv4 or IPv6 address, depending on Family, truncated in the request to the number of bits indicated by the Source Netmask field,
with bits set to 0 to pad up to the end of the last octet used. (This need not be as many octets as a complete address would take.)
In the reply, if the ScopeNetmask of the request was 0 then Address must contain the same octets as in the request.
Otherwise, the bits for Address will be significant through the maximum of the SouceNetmask or ScopeNetmask, and 0 filled to the end of an octet.
</param>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsOptionClientSubnet.Family">
<summary>
Indicates the family of the address contained in the option.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsOptionClientSubnet.SourceNetmask">
<summary>
Representing the length of the netmask pertaining to the query.
In replies, it mirrors the same value as in the requests.
It can be set to 0 to disable client-based lookups, in which case the Address field must be absent.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsOptionClientSubnet.ScopeNetmask">
<summary>
Representing the length of the netmask pertaining to the reply.
In requests, it should be set to the longest cacheable length supported by the Intermediate Nameserver.
In requests it may be set to 0 to have the Authoritative Nameserver treat the longest cacheable length as the SourceNetmask length.
In responses, this field is set by the Authoritative Nameserver to indicate the coverage of the response.
It might or might not match SourceNetmask; it could be shorter or longer.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsOptionClientSubnet.Address">
<summary>
Contains either an IPv4 or IPv6 address, depending on Family, truncated in the request to the number of bits indicated by the Source Netmask field,
with bits set to 0 to pad up to the end of the last octet used. (This need not be as many octets as a complete address would take.)
In the reply, if the ScopeNetmask of the request was 0 then Address must contain the same octets as in the request.
Otherwise, the bits for Address will be significant through the maximum of the SouceNetmask or ScopeNetmask, and 0 filled to the end of an octet.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsOptionClientSubnet.DataLength">
<summary>
The number of bytes the option data takes.
</summary>
</member>
<member name="T:PcapDotNet.Packets.DataLink">
<summary>
Represents the DataLink type.
@@ -16525,11 +16423,6 @@
NSID.
</summary>
</member>
<member name="F:PcapDotNet.Packets.Dns.DnsOptionCode.ClientSubnet">
<summary>
https://tools.ietf.org/html/draft-ietf-dnsop-edns-client-subnet
</summary>
</member>
<member name="T:PcapDotNet.Packets.Dns.DnsGatewayNone">
<summary>
A gateway representation that represents that no gateway is present.
@@ -23731,23 +23624,12 @@
RFC 792.
</summary>
</member>
<member name="F:PcapDotNet.Packets.Icmp.IcmpParameterProblemLayer.OriginalDatagramLengthMaxValue">
<summary>
The maximum value that OriginalDatagramLength can take.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Icmp.IcmpParameterProblemLayer.Pointer">
<summary>
The pointer identifies the octet of the original datagram's header where the error was detected (it may be in the middle of an option).
For example, 1 indicates something is wrong with the Type of Service, and (if there are options present) 20 indicates something is wrong with the type code of the first option.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Icmp.IcmpParameterProblemLayer.OriginalDatagramLength">
<summary>
Length of the padded "original datagram".
Must divide by 4 and cannot exceed OriginalDatagramLengthMaxValue.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Icmp.IcmpParameterProblemLayer.MessageType">
<summary>
The value of this field determines the format of the remaining data.
@@ -26345,19 +26227,19 @@
</member>
<member name="T:PcapDotNet.Packets.Icmp.IcmpParameterProblemDatagram">
<summary>
RFCs 792, 4884.
RFC 792.
<pre>
+-----+---------+--------+----------+
| Bit | 0-7 | 8-15 | 16-31 |
+-----+---------+--------+----------+
| 0 | Type | Code | Checksum |
+-----+---------+--------+----------+
| 32 | Pointer | Length | unused |
+-----+---------+-------------------+
| 64 | Internet Header |
| | + leading octets of |
| | original datagram |
+-----+-----------------------------+
+-----+---------+------+-----------+
| Bit | 0-7 | 8-15 | 16-31 |
+-----+---------+------+-----------+
| 0 | Type | Code | Checksum |
+-----+---------+------+-----------+
| 32 | Pointer | unused |
+-----+---------+------------------+
| 64 | Internet Header |
| | + 64 bits of |
| | Original Data Datagram |
+-----+----------------------------+
</pre>
</summary>
</member>
@@ -26379,12 +26261,6 @@
For example, 1 indicates something is wrong with the Type of Service, and (if there are options present) 20 indicates something is wrong with the type code of the first option.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Icmp.IcmpParameterProblemDatagram.OriginalDatagramLength">
<summary>
Length of the padded "original datagram".
Must divide by 4 and cannot exceed OriginalDatagramLengthMaxValue.
</summary>
</member>
<member name="T:PcapDotNet.Packets.Http.HttpVersion">
<summary>
Represents an HTTP version.
@@ -27611,12 +27487,6 @@
The public key value.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsResourceDataKey.KeyTag">
<summary>
Used in other records to efficiently select between multiple keys which may be applicable and thus check that a public key about to be used for the
computationally expensive effort to check the signature is possibly valid.
</summary>
</member>
<member name="T:PcapDotNet.Packets.Dns.DnsResourceDataDnsKey">
<summary>
RFCs 3757, 4034, 5011.
@@ -27718,12 +27588,6 @@
The format depends on the algorithm of the key being stored.
</summary>
</member>
<member name="P:PcapDotNet.Packets.Dns.DnsResourceDataDnsKey.KeyTag">
<summary>
Used in other records to efficiently select between multiple keys which may be applicable and thus check that a public key about to be used for the
computationally expensive effort to check the signature is possibly valid.
</summary>
</member>
<member name="T:PcapDotNet.Packets.Arp.ArpOperation">
<summary>
Specifies the operation the ARP sender is performing.