/* This file is part of IFS. IFS is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. IFS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with IFS. If not, see . */ using IFS.Boot; using IFS.EFTP; using IFS.Gateway; using IFS.Logging; using IFS.Mail; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; namespace IFS { // // From the spec, the AltoTime response is: // "10 bytes in all, organized as 5 16-bit words: // words 0, 1 Present date and time: a 32-bit integer representing number of // seconds since midnight, January 1, 1901, Greenwich Mean Time (GMT). // // word 2 Local time zone information. Bit 0 is zero if west of Greenwich // and one if east. Bits 1-7 are the number of hours east or west of // Greenwich. Bits 8-15 are an additional number of minutes. // // word 3 Day of the year on or before which Daylight Savings Time takes // effect locally, where 1 = January 1 and 366 = Dcember 31. (The // actual day is the next preceding Sunday.) // // word 4 Day of the year on or before which Daylight Savings Time ends. If // Daylight Savings Time is not observed locally, both the start and // end dates should be 366. // // The local time parameters in words 2 and 4 are those in effect at the server's // location. // struct AltoTime { public uint DateTime; public ushort TimeZone; public ushort DSTStart; public ushort DSTEnd; } /// /// Implements PUP Miscellaneous Services (see miscSvcsProto.pdf) /// which include: /// - Date and Time services /// - Mail check /// - Network Directory Lookup /// - Alto Boot protocols /// - Authenticate/Validate /// public class MiscServicesProtocol : PUPProtocolBase { public MiscServicesProtocol() { } /// /// Called by dispatcher to send incoming data destined for this protocol /// /// public override void RecvData(PUP p) { Log.Write(LogType.Verbose, LogComponent.MiscServices, String.Format("Misc. protocol request is for {0}.", p.Type)); switch (p.Type) { case PupType.StringTimeRequest: SendStringTimeReply(p); break; case PupType.AltoTimeRequest: SendAltoTimeReply(p); break; case PupType.AddressLookupRequest: SendAddressLookupReply(p); break; case PupType.NameLookupRequest: SendNameLookupReply(p); break; case PupType.SendBootFileRequest: SendBootFile(p); break; case PupType.BootDirectoryRequest: SendBootDirectory(p); break; case PupType.AuthenticateRequest: SendAuthenticationResponse(p); break; case PupType.MailCheckRequestLaurel: SendMailCheckResponse(p); break; case PupType.MicrocodeRequest: SendMicrocodeResponse(p); break; default: Log.Write(LogComponent.MiscServices, String.Format("Unhandled misc. protocol {0}", p.Type)); break; } } private void SendStringTimeReply(PUP p) { // // From the spec, the response is: // "A string consisting of the current date and time in the form // '11-SEP-75 15:44:25'" // NOTE: This is *not* a BCPL string, just the raw characters. // // It makes no mention of timezone or DST, so I am assuming local time here. // Good enough for government work. // DateTime currentTime = DateTime.Now; byte[] timeString = Helpers.StringToArray(currentTime.ToString("dd-MMM-yy HH:mm:ss")); PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.SourcePort.Socket); PUP response = new PUP(PupType.StringTimeReply, p.ID, p.SourcePort, localPort, timeString); Router.Instance.SendPup(response); } private void SendAltoTimeReply(PUP p) { // So the Alto epoch is 1/1/1901. For the time being to keep things simple we're assuming // GMT and no DST at all. TODO: make this take into account our TZ, etc. // // Additionally: While certain routines seem to be Y2K compliant (the time requests made from // the Alto's "puptest" diagnostic, for example), the Executive is not. To keep things happy, // we move things back 28 years so that the calendar at least matches up. // DateTime currentTime = new DateTime( DateTime.Now.Year - 28, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second); // The epoch for .NET is 1/1/0001 at 12 midnight and is counted in 100-ns intervals. // Some conversion is needed, is what I'm saying. DateTime altoEpoch = new DateTime(1901, 1, 1); TimeSpan timeSinceAltoEpoch = new TimeSpan(currentTime.Ticks - altoEpoch.Ticks); UInt32 altoTime = (UInt32)timeSinceAltoEpoch.TotalSeconds; // Build the response data AltoTime time = new AltoTime(); time.DateTime = altoTime; time.TimeZone = 0; // Hardcoded to GMT time.DSTStart = 366; // DST not specified yet time.DSTEnd = 366; PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); // Response must contain our network number; this is used to tell clients what network they're on if they don't already know. PUPPort remotePort = new PUPPort(DirectoryServices.Instance.LocalNetwork, p.SourcePort.Host, p.SourcePort.Socket); PUP response = new PUP(PupType.AltoTimeResponse, p.ID, remotePort, localPort, Serializer.Serialize(time)); Router.Instance.SendPup(response); } private void SendAddressLookupReply(PUP p) { // // Need to find more... useful documentation, but here's what I have: // For the request PUP: // A port (6 bytes). // // Response: // A string consisting of an inter-network name expression that matches the request Port. // // // I am at this time unsure what exactly an "inter-network name expression" consists of. // Empirically, a simple string name seems to make the Alto happy. // // // The request PUP contains a port address, we will check the host and network (and ignore the socket). // and see if we have a match. // PUPPort lookupAddress = new PUPPort(p.Contents, 0); string hostName = DirectoryServices.Instance.AddressLookup(new HostAddress(lookupAddress.Network, lookupAddress.Host)); if (!String.IsNullOrEmpty(hostName)) { // We have a result, pack the name into the response. // NOTE: This is *not* a BCPL string, just the raw characters. byte[] interNetworkName = Helpers.StringToArray(hostName); PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP lookupReply = new PUP(PupType.AddressLookupResponse, p.ID, p.SourcePort, localPort, interNetworkName); Router.Instance.SendPup(lookupReply); } else { // Unknown host, send an error reply string errorString = "Unknown host."; PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, Helpers.StringToArray(errorString)); Router.Instance.SendPup(errorReply); } } private void SendNameLookupReply(PUP p) { // // For the request PUP: // A string consisting of an inter-network name expression. // NOTE: This is *not* a BCPL string, just the raw characters. // // Response: // One or more 6-byte blocks containing the address(es) corresponding to the // name expression. Each block is a Pup Port structure, with the network and host numbers in // the first two bytes and the socket number in the last four bytes. // // // For now, the assumption is that each name maps to at most one address. // string lookupName = Helpers.ArrayToString(p.Contents); Log.Write(LogType.Verbose, LogComponent.MiscServices, "Name lookup is for '{0}'", lookupName); HostAddress address = DirectoryServices.Instance.NameLookup(lookupName); if (address != null) { // We found an address, pack the port into the response. PUPPort lookupPort = new PUPPort(address, 0); PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP lookupReply = new PUP(PupType.NameLookupResponse, p.ID, p.SourcePort, localPort, lookupPort.ToArray()); Router.Instance.SendPup(lookupReply); Log.Write(LogType.Verbose, LogComponent.MiscServices, "Address is '{0}'", address); } else { // Unknown host, send an error reply string errorString = "Unknown host."; PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP errorReply = new PUP(PupType.DirectoryLookupErrorReply, p.ID, p.SourcePort, localPort, Helpers.StringToArray(errorString)); Router.Instance.SendPup(errorReply); Log.Write(LogType.Verbose, LogComponent.MiscServices, "Host is unknown."); } } private void SendBootFile(PUP p) { // // The request PUP contains the file number in the lower-order 16-bits of the pup ID. // Assuming the number is a valid bootfile, we start sending it to the client's port via EFTP. // ushort fileNumber = (ushort)p.ID; Log.Write(LogType.Verbose, LogComponent.MiscServices, "Boot file request is for file {0}.", fileNumber); FileStream bootFile = BootServer.GetStreamForNumber(fileNumber); if (bootFile == null) { Log.Write(LogType.Warning, LogComponent.MiscServices, "Boot file {0} does not exist or could not be opened.", fileNumber); } else { // Send the file. EFTPManager.SendFile(p.SourcePort, bootFile); } } private void SendBootDirectory(PUP p) { // // From etherboot.bravo // "Pup ID: if it is in reply to a BootDirRequest, the ID should match the request. // Pup Contents: 1 or more blocks of the following format: A boot file number (the number that goes in the low 16 bits of a // BootFileRequest Pup), an Alto format date (2 words), a boot file name in BCPL string format." // MemoryStream ms = new MemoryStream(PUP.MAX_PUP_SIZE); List bootFiles = BootServer.EnumerateBootFiles(); foreach (BootFileEntry entry in bootFiles) { BootDirectoryBlock block; block.FileNumber = entry.BootNumber; block.FileDate = 0; block.FileName = new BCPLString(entry.Filename); byte[] serialized = Serializer.Serialize(block); // // If this block fits into the current PUP, add it to the stream, otherwise send off the current PUP // and start a new one. // if (serialized.Length + ms.Length <= PUP.MAX_PUP_SIZE) { ms.Write(serialized, 0, serialized.Length); } else { PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP bootDirReply = new PUP(PupType.BootDirectoryReply, p.ID, p.SourcePort, localPort, ms.ToArray()); Router.Instance.SendPup(bootDirReply); ms.Seek(0, SeekOrigin.Begin); ms.SetLength(0); } } // Shuffle out any remaining data. if (ms.Length > 0) { PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); PUP bootDirReply = new PUP(PupType.BootDirectoryReply, p.ID, p.SourcePort, localPort, ms.ToArray()); Router.Instance.SendPup(bootDirReply); } } 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)); Router.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[] { }); Router.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... // mailboxName = Authentication.GetUserNameFromFullName(mailboxName); 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[] { }); Router.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!")); Router.Instance.SendPup(mailReply); } } private void SendMicrocodeResponse(PUP p) { // // TODO; validate that this is a request for V1 of the protocol (I don't think there was ever another version...) // // // The request PUP contains the file number in the lower-order 16-bits of the pup ID. // Assuming the number is a valid bootfile, we start sending it to the client's port via EFTP. // ushort fileNumber = (ushort)p.ID; ushort version = (ushort)(p.ID >> 16); Log.Write(LogType.Verbose, LogComponent.MiscServices, "Microcode request (version {0}) is for file {1}.", version, Helpers.ToOctal(fileNumber)); FileStream microcodeFile = BootServer.GetStreamForNumber(fileNumber); if (microcodeFile == null) { Log.Write(LogType.Warning, LogComponent.MiscServices, "Microcode file {0} does not exist or could not be opened.", Helpers.ToOctal(fileNumber)); } else { // Send the file asynchronously. The MicrocodeReply protocol is extremely simple: // Just send a sequence of MicrocodeReply PUPs containing the microcode data, // there are no acks or flow control of any kind. ThreadPool.QueueUserWorkItem((ctx) => { Log.Write(LogType.Warning, LogComponent.MiscServices, "Sending microcode file {0} ('{1}').", Helpers.ToOctal(fileNumber), microcodeFile.Name); SendMicrocodeFile(p.SourcePort, microcodeFile, fileNumber == 0x100 /* test for Initial.eb */); }, null); } } private void SendMicrocodeFile(PUPPort sourcePort, Stream microcodeFile, bool sendEmptyPacket) { // // "For version 1 of the protocol, a server willing to supply the data simply sends a sequence of packets // of type MicrocodeReply as fast as it can. The high half of its pupID contains the version number(1) // and the low half of the pupID contains the packet sequence number. After all the data packets // have been sent, the server sends an empty (0 data bytes) packet for an end marker. There are no // acknowledgments. This protocol is used by Dolphins and Dorados. // Currently, the version 1 servers send packets containing 3 * n words of data. This constraint is imposed by the // Rev L Dolphin EPROM microcode. I’d like to remove this restriction if I get a chance, so please don’t take // advantage of it unless you need to. The Rev L Dolphin EPROM also requires the second word of the source // socket to be 4. / HGM May - 80." // // Skip the first 256 header words in the microcode file. microcodeFile.Seek(512, SeekOrigin.Begin); // // We send 258 words of data per PUP (3 * 86) in an attempt to make the Dolphin happy. // This is what the original Xerox IFS code did. // We space these out a bit to give the D-machine time to keep up, we're much much faster than they are. // PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, SocketIDGenerator.GetNextSocketID() << 16 | 0x4); bool done = false; uint id = 0; // // Send an empty packet to start the transfer. The prom boot microcode will explicitly ignore this. // Note that this is not documented in the (meager) protocol docs, nor does the BCPL IFS code // appear to actually send such a packet, at least not explicitly. // // Further: // D0 Initial's E3Boot doesn't like the empty packet, and assumes it means the end of the microcode reply; it then // tries to load a 0-length microcode file into CS and falls over. // The below hacks around it (it only sends the empty packet when the Initial microcode file is requested). // I'm unsure if there's a subtle bug in our IFS code here or elsewhere or a subtle bug in PARC's IFS code; it does kind of seem // like the microcode is working around a weird issue but those folks were a lot smarter than I. // Addendum 7/28/23: // After reset, The real D0 seems to occasionally complete the first-stage (Initial) boot without the extra empty packet being sent. // I wonder if there's a hardware glitch the boot microcode is working around. // Additionally: the Dorado boot ucode source makes no mention of ignoring an empty packet, nor does the code implement such behavior. // if (sendEmptyPacket) { Router.Instance.SendPup(new PUP(PupType.MicrocodeReply, 0x10000, sourcePort, localPort, new byte[] { })); } uint checksum = 0; while (!done) { byte[] buffer = new byte[258 * 2]; // 258 words, as the original IFS did int read = microcodeFile.Read(buffer, 0, buffer.Length); if (read < buffer.Length) { done = true; } if (read > 0) { // Send ONLY the bytes we read. byte[] packetBuffer = new byte[read]; Array.Copy(buffer, packetBuffer, read); PUP microcodeReply = new PUP(PupType.MicrocodeReply, (id | 0x10000), sourcePort, localPort, buffer); Router.Instance.SendPup(microcodeReply); Log.Write(LogType.Warning, LogComponent.MiscServices, "Sequence {0} Sent {1} bytes of microcode file", id, read); for (int i = 0; i < read; i += 2) { checksum += (uint)(packetBuffer[i + 1] | (packetBuffer[i] << 8)); } } // Pause a bit to give the D0 time to breathe. // TODO: make this configurable? System.Threading.Thread.Sleep(10); id++; } // // Send an empty packet to conclude the transfer. // Router.Instance.SendPup(new PUP(PupType.MicrocodeReply, (id | 0x10000), sourcePort, localPort, new byte[] { })); Log.Write(LogType.Warning, LogComponent.MiscServices, "Microcode file sent. Checksum {0:x4}", (checksum & 0xffff)); } private struct BootDirectoryBlock { public ushort FileNumber; public UInt32 FileDate; public BCPLString FileName; } } }