1
0
mirror of https://github.com/livingcomputermuseum/IFS.git synced 2026-01-13 15:27:25 +00:00
livingcomputermuseum.IFS/PUP/MiscServicesProtocol.cs
Josh Dersch 4d992b1bd7 Adding built-in support for Ken Shirriff's BeagleBone-based Alto Ethernet Interface.
Adding revised version of MicrocodeBootRequest, to support booting Dolphin and Dorado hardware.
2023-09-30 22:45:05 -07:00

567 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 <http://www.gnu.org/licenses/>.
*/
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;
}
/// <summary>
/// Implements PUP Miscellaneous Services (see miscSvcsProto.pdf)
/// which include:
/// - Date and Time services
/// - Mail check
/// - Network Directory Lookup
/// - Alto Boot protocols
/// - Authenticate/Validate
/// </summary>
public class MiscServicesProtocol : PUPProtocolBase
{
public MiscServicesProtocol()
{
}
/// <summary>
/// Called by dispatcher to send incoming data destined for this protocol
/// </summary>
/// <param name="p"></param>
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<BootFileEntry> 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<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[] { });
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. Id like to remove this restriction if I get a chance, so please dont 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;
}
}
}