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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
153
PUP/Mail/MailManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
27
PUP/PUP.cs
27
PUP/PUP.cs
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
Binary file not shown.
@@ -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.
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user