diff --git a/PUP/Authentication.cs b/PUP/Authentication.cs index 08ae4d8..b97184d 100644 --- a/PUP/Authentication.cs +++ b/PUP/Authentication.cs @@ -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; } + /// + /// Verifies whether the specified user account is registered. + /// + /// + /// + public static bool UserExists(string userName) + { + return _accounts.ContainsKey(userName); + } + + /// + /// Given a full user name (i.e. username.HOST), validates that the + /// HOST (or "registry") portion matches our hostname. + /// + /// + /// + 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; + } + } + + + /// + /// Given a full user name (i.e. username.HOST), returns only the username portion. + /// + /// + /// + 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 diff --git a/PUP/BSP/BSPChannel.cs b/PUP/BSP/BSPChannel.cs index c8c9c5b..b83d81c 100644 --- a/PUP/BSP/BSPChannel.cs +++ b/PUP/BSP/BSPChannel.cs @@ -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 diff --git a/PUP/Conf/ifs.cfg b/PUP/Conf/ifs.cfg index d2a81b5..aa72bc1 100644 --- a/PUP/Conf/ifs.cfg +++ b/PUP/Conf/ifs.cfg @@ -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 diff --git a/PUP/Configuration.cs b/PUP/Configuration.cs index b117c61..4d5346f 100644 --- a/PUP/Configuration.cs +++ b/PUP/Configuration.cs @@ -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 /// public static readonly string BootRoot; + /// + /// The root directory for the Mail file store. + /// + public static readonly string MailRoot; + /// /// The maximum number of worker threads for protocol handling. /// diff --git a/PUP/DirectoryServices.cs b/PUP/DirectoryServices.cs index ce932dd..70a57e2 100644 --- a/PUP/DirectoryServices.cs +++ b/PUP/DirectoryServices.cs @@ -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>(); @@ -259,6 +273,11 @@ namespace IFS /// private HostAddress _localHost; + /// + /// Our name. + /// + private string _localHostName; + /// /// Hash table for address resolution; outer hash finds the dictionary /// for a given network, inner hash finds names for hosts. diff --git a/PUP/Entrypoint.cs b/PUP/Entrypoint.cs index afe8c0e..c00306c 100644 --- a/PUP/Entrypoint.cs +++ b/PUP/Entrypoint.cs @@ -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(); diff --git a/PUP/FTP/FTPServer.cs b/PUP/FTP/FTPServer.cs index c3892f5..556e3f6 100644 --- a/PUP/FTP/FTPServer.cs +++ b/PUP/FTP/FTPServer.cs @@ -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 recipients = new List(); + 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); } + /// + /// Retrieves mail files for the specified mailbox. + /// + /// + 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."); + + } + + /// + /// Stores mail files to the specified mailboxes. + /// + /// + private void StoreMail(List 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 destinationMailboxes = new List(); + + 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."); + } + } + /// /// Open the file specified by the provided PropertyList /// @@ -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; + + /// + /// 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. + /// + private IEnumerable _lastRetrievedMailFiles; + private string _lastRetrievedMailbox; } } diff --git a/PUP/FTP/PropertyList.cs b/PUP/FTP/PropertyList.cs index 6f4dd77..400e267 100644 --- a/PUP/FTP/PropertyList.cs +++ b/PUP/FTP/PropertyList.cs @@ -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"; } /// @@ -76,9 +83,25 @@ namespace IFS.FTP _propertyList = new Dictionary(); } + /// + /// Parses a property list from the specified string. + /// + /// public PropertyList(string list) : this() { - ParseList(list); + ParseList(list, 0); + } + + /// + /// 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. + /// + /// + /// + /// + public PropertyList(string input, int startIndex, out int endIndex) : this() + { + endIndex = ParseList(input, startIndex); } /// @@ -171,8 +194,10 @@ namespace IFS.FTP /// Parses a string representation of a property list into our hash table. /// /// - 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 _propertyList; diff --git a/PUP/IFS.csproj b/PUP/IFS.csproj index aa6000d..8714937 100644 --- a/PUP/IFS.csproj +++ b/PUP/IFS.csproj @@ -50,17 +50,41 @@ prompt MinimumRecommendedRules.ruleset + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + true + - + + False pcap\PcapDotNet.Base.dll - + + False pcap\PcapDotNet.Core.dll - + + False pcap\PcapDotNet.Core.Extensions.dll - + + False pcap\PcapDotNet.Packets.dll @@ -95,6 +119,7 @@ + diff --git a/PUP/IFS.sln b/PUP/IFS.sln index c39a31e..f3fc9e4 100644 --- a/PUP/IFS.sln +++ b/PUP/IFS.sln @@ -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 diff --git a/PUP/Logging/Log.cs b/PUP/Logging/Log.cs index 8fe42de..ece7be8 100644 --- a/PUP/Logging/Log.cs +++ b/PUP/Logging/Log.cs @@ -24,6 +24,7 @@ namespace IFS.Logging EFTP = 0x200, BootServer = 0x400, UDP = 0x800, + Mail = 0x1000, Configuration = 0x1000, All = 0x7fffffff diff --git a/PUP/Mail/MailManager.cs b/PUP/Mail/MailManager.cs new file mode 100644 index 0000000..f5ca684 --- /dev/null +++ b/PUP/Mail/MailManager.cs @@ -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 +{ + /// + /// 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.) + /// + public static class MailManager + { + static MailManager() + { + + } + + /// + /// 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. + /// + /// + /// + public static IEnumerable 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; + } + } + + /// + /// Retrieves a stream for the given mail file in the specified mailbox. + /// + /// + /// + /// + 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; + } + } + + /// + /// Deletes the specified mail file from the specified mailbox. + /// + /// + /// + 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); + } + } +} diff --git a/PUP/MiscServicesProtocol.cs b/PUP/MiscServicesProtocol.cs index 8dabb91..54537d7 100644 --- a/PUP/MiscServicesProtocol.cs +++ b/PUP/MiscServicesProtocol.cs @@ -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 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; diff --git a/PUP/PUP.cs b/PUP/PUP.cs index e427e3c..a15bab6 100644 --- a/PUP/PUP.cs +++ b/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(); + } } } diff --git a/PUP/PUPProtocolDispatcher.cs b/PUP/PUPProtocolDispatcher.cs index 5fb69de..1993e7e 100644 --- a/PUP/PUPProtocolDispatcher.cs +++ b/PUP/PUPProtocolDispatcher.cs @@ -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; } diff --git a/PUP/Transport/Ethernet.cs b/PUP/Transport/Ethernet.cs index 993c88b..3264f75 100644 --- a/PUP/Transport/Ethernet.cs +++ b/PUP/Transport/Ethernet.cs @@ -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. diff --git a/PUP/pcap/PcapDotNet.Base.dll b/PUP/pcap/PcapDotNet.Base.dll index c71b2f7..f0cac7f 100644 Binary files a/PUP/pcap/PcapDotNet.Base.dll and b/PUP/pcap/PcapDotNet.Base.dll differ diff --git a/PUP/pcap/PcapDotNet.Core.Extensions.dll b/PUP/pcap/PcapDotNet.Core.Extensions.dll index 64164c0..25ea555 100644 Binary files a/PUP/pcap/PcapDotNet.Core.Extensions.dll and b/PUP/pcap/PcapDotNet.Core.Extensions.dll differ diff --git a/PUP/pcap/PcapDotNet.Core.dll b/PUP/pcap/PcapDotNet.Core.dll index acb4f87..62b5b5d 100644 Binary files a/PUP/pcap/PcapDotNet.Core.dll and b/PUP/pcap/PcapDotNet.Core.dll differ diff --git a/PUP/pcap/PcapDotNet.Core.xml b/PUP/pcap/PcapDotNet.Core.xml index a0ddfd1..e622ef7 100644 --- a/PUP/pcap/PcapDotNet.Core.xml +++ b/PUP/pcap/PcapDotNet.Core.xml @@ -723,7 +723,6 @@ Win32 specific. Number of packets captured, i.e number of packets that are accep Number of packets dropped by the interface. -Not yet supported. diff --git a/PUP/pcap/PcapDotNet.Packets.dll b/PUP/pcap/PcapDotNet.Packets.dll index 4ffca8c..dc60815 100644 Binary files a/PUP/pcap/PcapDotNet.Packets.dll and b/PUP/pcap/PcapDotNet.Packets.dll differ diff --git a/PUP/pcap/PcapDotNet.Packets.xml b/PUP/pcap/PcapDotNet.Packets.xml index 7bbba5d..b33cc30 100644 --- a/PUP/pcap/PcapDotNet.Packets.xml +++ b/PUP/pcap/PcapDotNet.Packets.xml @@ -439,13 +439,6 @@ Obsoleted. - - - MTU Reply. - RFCs 1063, 1191. - Obsoleted. - - Quick Start (QS). RFC 4782. @@ -474,12 +467,6 @@ Used to route the internet datagram based on information supplied by the source. - - - http://tools.ietf.org/html/draft-ietf-cipso-ipsecurity - CIPSO - Commercial Security. - - Strict Source Routing. @@ -12448,7 +12435,7 @@ - RFCs 792, 4884. + RFC 792. 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 @@ - Builds an instance from a least value. + Builds - - 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. - + @@ -13051,86 +13029,6 @@ The number of bytes the option data takes. - - - https://tools.ietf.org/html/draft-ietf-dnsop-edns-client-subnet -
-            +-----+----------------+---------------+
-            | bit | 0-7            | 8-15          |
-            +-----+----------------+---------------+
-            | 0   | FAMILY                         |
-            +-----+----------------+---------------+
-            | 16  | SOURCE NETMASK | SCOPE NETMASK |
-            +-----+----------------+---------------+
-            | 32  | ADDRESS                        |
-            | ... |                                |
-            +-----+--------------------------------+
-            
-
-
- - - The minimum number of bytes this option data can take. - - - - - Create a DNS Client Subnet option from family, source netmask, scope netmask and address fields. - - Indicates the family of the address contained in the option. - - 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. - - - 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. - - - 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. - - - - - Indicates the family of the address contained in the option. - - - - - 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. - - - - - 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. - - - - - 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. - - - - - The number of bytes the option data takes. - - Represents the DataLink type. @@ -16525,11 +16423,6 @@ NSID. - - - https://tools.ietf.org/html/draft-ietf-dnsop-edns-client-subnet - - A gateway representation that represents that no gateway is present. @@ -23731,23 +23624,12 @@ RFC 792. - - - The maximum value that OriginalDatagramLength can take. - - 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. - - - Length of the padded "original datagram". - Must divide by 4 and cannot exceed OriginalDatagramLengthMaxValue. - - The value of this field determines the format of the remaining data. @@ -26345,19 +26227,19 @@ - RFCs 792, 4884. + RFC 792.
-            +-----+---------+--------+----------+
-            | 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     |
+            +-----+----------------------------+
             
@@ -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.
- - - Length of the padded "original datagram". - Must divide by 4 and cannot exceed OriginalDatagramLengthMaxValue. - - Represents an HTTP version. @@ -27611,12 +27487,6 @@ The public key value. - - - 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. - - RFCs 3757, 4034, 5011. @@ -27718,12 +27588,6 @@ The format depends on the algorithm of the key being stored. - - - 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. - - Specifies the operation the ARP sender is performing.