-- Grapevine:  FTP server using VMDefs --

-- [Indigo]<Grapevine>MS>FTPServer.mesa

-- Randy Gobbel		22-May-81 13:50:58 --
-- Andrew Birrell	 5-Mar-82  8:54:25 --
-- Mark Johnson		May 28, 1981  2:09 PM --

DIRECTORY
AltoFile	USING[ CloseDirectory, DEfile, DirHandle, DVPtr, Enumerate,
		       OpenDirectory, sysDirFP ],
BodyDefs	USING[ maxRNameLength, RName ],
FTPDefs,
LogDefs		USING[ WriteLogEntry ],
NameInfoDefs	USING[ Authenticate, AuthenticateInfo, IsMemberClosure,
		       Membership ],
PolicyDefs	USING [CheckOperation, EndOperation],
PupDefs		USING[ AppendMyName, ParsePupAddressConstant, PupAddress ],
Storage		USING[ Free, String ],
String		USING[ AppendChar, AppendNumber,
		       AppendString, EquivalentStrings ],
Time		USING[ Append, Packed, Unpack ],
TimeExtraDefs	USING[ PackedTimeFromString ],
VMDefs		USING[ AbandonFile, CantOpen, CantReadBackingStore,
		       CantWriteBackingStore, CloseFile, DestroyFile,
		       Error, FileHandle, FileTime, GetFileLength, GetFileTimes,
		       MarkStart, OpenFile, Page, pageSize, Position,
		       ReadPage, Release, SetCreationTime, SetFileLength,
		       UsePage, WaitFile ],
VMSpecial	USING[ OpenAltoFileFromFP, QuickAndDirtyAltoRename ];

FTPServer: PROGRAM
   IMPORTS AltoFile, FTPDefs, LogDefs, NameInfoDefs, PolicyDefs, PupDefs,
           Storage, String, Time, TimeExtraDefs, VMDefs, VMSpecial =

BEGIN

WhoIsHe: SIGNAL RETURNS[ net, host: [0..256) ] = CODE;
--communication between LogAction and Backstop --

LogAction: PROC[user: BodyDefs.RName, file: STRING,
                action: { read, write, rename, delete }] =
   BEGIN
   log: STRING = [128];
   String.AppendString[log, "FTP server: "L];
   BEGIN
      net, host: [0..256);
      [net, host] ← SIGNAL WhoIsHe;
      String.AppendNumber[log, net, 8];
      String.AppendChar[log, '#];
      String.AppendNumber[log, host, 8];
   END;
   String.AppendChar[log, ' ];
   String.AppendString[log, user];
   String.AppendString[log, SELECT action FROM
                              read =>   " reading "L,
                              write =>  " writing "L,
                              rename => " renaming "L,
                              delete => " deleting "L
                            ENDCASE => ERROR];
   String.AppendString[log, file];
   LogDefs.WriteLogEntry[log];
   END;

MyCreate: PROC[bufferSize: CARDINAL]
          RETURNS[fileSystem: FTPDefs.FileSystem] =
   BEGIN
   real: BodyDefs.RName = Storage.String[BodyDefs.maxRNameLength];
   RETURN[LOOPHOLE[real]];
   END;

MyDestroy: PROC[ fileSystem: FTPDefs.FileSystem ] =
   { Storage.Free[fileSystem] };

MyDecompose: PROC[ fileSystem: FTPDefs.FileSystem,
                   absoluteFilename: STRING,
                   virtualFilename: FTPDefs.VirtualFilename ] =
   BEGIN
   virtualFilename.device.length ← 0;
   virtualFilename.directory.length ← 0;
   virtualFilename.name.length ← 0;
   String.AppendString[virtualFilename.name, absoluteFilename];
   virtualFilename.version.length ← 0;
   END;

MyCompose: PROC[ fileSystem: FTPDefs.FileSystem,
                 absoluteFilename: STRING,
                 virtualFilename: FTPDefs.VirtualFilename ] =
   BEGIN
   IF virtualFilename.device.length = 0
   AND virtualFilename.directory.length = 0
   AND  virtualFilename.name.length = 0
   AND virtualFilename.version.length = 0
   THEN NULL -- that's what the spec says! --
   ELSE BEGIN
        absoluteFilename.length ← 0;
        String.AppendString[absoluteFilename, virtualFilename.name];
        END;
   END;

DefaultReg: PROC[name: BodyDefs.RName] =
   BEGIN
   default: STRING = ".pa"L;
   FOR i: CARDINAL DECREASING IN [0..name.length)
   DO IF name[i] = '. THEN EXIT;
   REPEAT
   FINISHED =>
      IF name.length + default.length <= name.maxlength
      THEN String.AppendString[name, default];
   ENDLOOP;
   END;

MyInspectCredentials: PROC[ fileSystem: FTPDefs.FileSystem,
                            status: FTPDefs.Status,
                            user, password: STRING ] =
   BEGIN
   real: BodyDefs.RName = LOOPHOLE[fileSystem];
   IF status = primary
   THEN BEGIN
        outcome: NameInfoDefs.AuthenticateInfo;
        IF user = NIL OR user.length > BodyDefs.maxRNameLength
        THEN { real.length ← 0; outcome ← notFound }
        ELSE BEGIN
	     IF password = NIL THEN outcome ← badPwd
	     ELSE BEGIN
                  real.length ← 0; String.AppendString[real, user];
                  DefaultReg[real];
                  outcome ← NameInfoDefs.Authenticate[real, password];
		  END;
             END;
        IF outcome # individual
        THEN BEGIN
             error: FTPDefs.FtpError = SELECT outcome FROM
                 notFound, group => noSuchPrimaryUser,
                 badPwd =>          incorrectPrimaryPassword,
                 allDown =>         unidentifiedTransientError,
               ENDCASE => ERROR;
             string: STRING = SELECT outcome FROM
                 notFound => "Incorrect registry or user name"L,
                 group =>    "Can't login as a group"L,
                 badPwd =>   "Incorrect password"L,
                 allDown =>  "Can't reach any authentication server",
               ENDCASE => ERROR;
             real.length ← 0;
             ERROR FTPDefs.FTPError[error, string];
             END;
        END;
   END;

CheckAccess: PROC[ user: BodyDefs.RName, why: {list, read, write},
                   file: STRING ] =
   BEGIN
   info: NameInfoDefs.Membership;
   IF user = NIL OR user.length = 0
   THEN ERROR FTPDefs.FTPError[credentialsMissing,
                               "No valid user/password given"L];
   IF why # list AND String.EquivalentStrings[file, "Heap.data"L]
   THEN FTPDefs.FTPError[requestedAccessDenied,
                         "'Heap.data' is private!"L];
   IF why # read AND String.EquivalentStrings[file, "GV.log"L]
   THEN info ← NameInfoDefs.IsMemberClosure["LogReaders↑.ms"L, user];
   IF info # yes
   THEN info ← NameInfoDefs.IsMemberClosure["Transport↑.ms"L, user];
   SELECT info FROM
     yes => NULL;
     no, notGroup =>
       ERROR FTPDefs.FTPError[requestedAccessDenied,
                              "You're not allowed access to this server"L];
     allDown =>
       ERROR FTPDefs.FTPError[requestedAccessDenied,
                              "Can't contact access control server"L];
   ENDCASE => ERROR;
   END;

OpenVMFile: PROC[file: STRING,
                 action: {read, write, delete, enumerate}]
            RETURNS[handle: VMDefs.FileHandle] =
   BEGIN
   ENABLE VMDefs.CantOpen => SELECT reason FROM
       notFound =>
         ERROR FTPDefs.FTPError[noSuchFile, "File does not exist"L];
       alreadyExists =>
         ERROR FTPDefs.FTPError[fileAlreadyExists, "File already exists"L];
       accessDenied =>
         ERROR FTPDefs.FTPError[requestedAccessDenied,
                                "File not available at present"L];
       illegalFileName =>
         ERROR FTPDefs.FTPError[illegalFilename,
                                "Illegal file title"L];
     ENDCASE =>
       ERROR FTPDefs.FTPError[unidentifiedPermanentError,
                              "Unidentified ""CantOpen"" from VM"L];
   handle ← VMDefs.OpenFile[
      options: SELECT action FROM
                 read, enumerate => oldReadOnly,
                 write => oldOrNew,
                 delete => old,
               ENDCASE => ERROR,
      name: file, cacheFraction: 2 ];
   END;

GetInfo: PROC[handle: VMDefs.FileHandle, info: FTPDefs.FileInfo] =
   BEGIN
   IF info = NIL THEN RETURN;
   BEGIN
      length: VMDefs.Position = VMDefs.GetFileLength[handle];
      info.byteCount ← LONG[length.page] * 2*VMDefs.pageSize + length.byte;
   END;
   info.fileType ← binary;
   info.byteSize ← 8;
   BEGIN
      Append: PROC[s: STRING, t: Time.Packed] =
         BEGIN
         IF s # NIL
         THEN { s.length ← 0; Time.Append[s, Time.Unpack[t], TRUE] };
         END;
      create, write, read: Time.Packed;
      [create: LOOPHOLE[create, VMDefs.FileTime],
       read: LOOPHOLE[read, VMDefs.FileTime],
       write: LOOPHOLE[write, VMDefs.FileTime]] ← VMDefs.GetFileTimes[handle];
      Append[info.creationDate, create];
      Append[info.writeDate, write];
      Append[info.readDate, read];
   END;
   END;
   

MyEnumerateFiles: PROC[ fileSystem: FTPDefs.FileSystem,
               files: STRING, intent: FTPDefs.EnumerateFilesIntent,
               processFile: PROC[UNSPECIFIED,STRING,FTPDefs.FileInfo],
               processFileData: UNSPECIFIED ] =
   BEGIN
   real: BodyDefs.RName = LOOPHOLE[fileSystem];
   creation: STRING = [22]; -- 31-Oct-83 25:61:61 BST --
   write:    STRING = [22]; -- 1234567890123456789012 --
   read:     STRING = [22];
   fileInfoObject: FTPDefs.FileInfoObject ← [
      fileType: binary, byteSize: 8, byteCount: 0,
      creationDate: creation, writeDate: write, readDate: read,
      author: NIL ];
   SingleFile: PROC[entry: AltoFile.DVPtr, name: STRING] RETURNS[BOOLEAN] =
      BEGIN
      IF entry.type = AltoFile.DEfile
      THEN BEGIN
           handle: VMDefs.FileHandle = VMSpecial.OpenAltoFileFromFP[
                      [entry.fp.serial,entry.fp.leaderDA],FALSE,0];
           GetInfo[handle, @fileInfoObject];
           VMDefs.AbandonFile[handle];
           processFile[processFileData, name, @fileInfoObject];
           END;
      RETURN[FALSE]
      END;
   CheckAccess[real, list, files];
   IF intent = enumeration AND files.length = 1 AND files[0] = '*
   THEN BEGIN
        h: AltoFile.DirHandle = AltoFile.OpenDirectory[AltoFile.sysDirFP];
        [] ← AltoFile.Enumerate[h, SingleFile ! UNWIND =>
                                AltoFile.CloseDirectory[h] ];
        AltoFile.CloseDirectory[h];
        END
   ELSE BEGIN
        IF intent = enumeration
        THEN BEGIN
             handle: VMDefs.FileHandle = OpenVMFile[files, enumerate];
             GetInfo[handle, @fileInfoObject];
             VMDefs.CloseFile[handle];
             END;
        processFile[processFileData, files, @fileInfoObject];
        END;
   END;

MyOpenFile: PROC[ fileSystem: FTPDefs.FileSystem,
               file: STRING, mode: FTPDefs.Mode, fileTypePlease: BOOLEAN,
               info: FTPDefs.FileInfo ]
            RETURNS[ fileHandle: FTPDefs.FileHandle,
                     fileType: FTPDefs.FileType] =
   BEGIN
   real: BodyDefs.RName = LOOPHOLE[fileSystem];
   SELECT mode  FROM
     read =>
        BEGIN
        handle: VMDefs.FileHandle;
        CheckAccess[real, read, file];
        handle ← OpenVMFile[file, read];
        LogAction[real, file, read];
        GetInfo[handle, info];
        fileType ← binary;
        fileHandle ← LOOPHOLE[handle]; 
        END;
     write =>
        BEGIN
        CheckAccess[real, write, file];
        fileHandle ← LOOPHOLE[ OpenVMFile[file, write] ];
        LogAction[real, file, write];
        IF info # NIL AND info.creationDate # NIL
        AND info.creationDate.length > 0
        THEN VMDefs.SetCreationTime[file: LOOPHOLE[fileHandle],
              create: TimeExtraDefs.PackedTimeFromString[info.creationDate]
              ];
        END;
     ENDCASE =>
        ERROR FTPDefs.FTPError[requestedAccessDenied,
                               "Unexpected access mode"L];
   END;

MyReadFile: PROC[fileSystem: FTPDefs.FileSystem,
               fileHandle: FTPDefs.FileHandle,
               sendBlock: PROC[UNSPECIFIED,POINTER,CARDINAL],
               sendBlockData: UNSPECIFIED ] =
   BEGIN
   handle: VMDefs.FileHandle = LOOPHOLE[fileHandle];
   length: VMDefs.Position = VMDefs.GetFileLength[handle];
   FOR p: CARDINAL IN [0..length.page) --complete pages--
   DO page: VMDefs.Page = VMDefs.ReadPage[ [handle,p], 2 ];
      sendBlock[sendBlockData, page, 2*VMDefs.pageSize !
                UNWIND => VMDefs.Release[page] ];
      VMDefs.Release[page];
   ENDLOOP;
   IF length.byte > 0
   THEN --partial page--
        BEGIN
        page: VMDefs.Page = VMDefs.ReadPage[ [handle, length.page], 0 ];
        sendBlock[sendBlockData, page, length.byte !
                  UNWIND => VMDefs.Release[page] ];
        VMDefs.Release[page];
        END;
   END;

MyWriteFile: PROC[fileSystem: FTPDefs.FileSystem,
               fileHandle: FTPDefs.FileHandle,
               receiveBlock: PROC[UNSPECIFIED,POINTER,CARDINAL]
                             RETURNS[CARDINAL],
               receiveBlockData: UNSPECIFIED ] =
   BEGIN
   handle: VMDefs.FileHandle = LOOPHOLE[fileHandle];
   pos: VMDefs.Position ← [0,0];
   page: VMDefs.Page ← VMDefs.UsePage[ [handle, pos.page] ];
   DO ENABLE UNWIND => IF page # NIL THEN VMDefs.Release[page];
      amount: CARDINAL = receiveBlock[receiveBlockData, page+(pos.byte/2),
                                      VMDefs.pageSize-(pos.byte/2) ];
      IF amount = 0 THEN EXIT;
      pos.byte ← pos.byte + amount;
      IF amount MOD 2 # 0
      THEN BEGIN
           temp: UNSPECIFIED;
           IF receiveBlock[receiveBlockData, @temp, SIZE[UNSPECIFIED]] = 0
           THEN EXIT
           ELSE ERROR FTPDefs.FTPError[requestedAccessDenied,
                          "Odd-byte block boundary not implemented"L];
           END;
      IF pos.byte >= 2 * VMDefs.pageSize
      THEN BEGIN
           IF pos.byte > 2 * VMDefs.pageSize THEN ERROR;
           VMDefs.MarkStart[page];
           VMDefs.Release[page]; page ← NIL;
           pos.page ← pos.page+1; pos.byte ← 0;
           page ← VMDefs.UsePage[ [handle, pos.page] ];
           END;
   ENDLOOP;
   IF pos.byte > 0 THEN VMDefs.MarkStart[page];
   VMDefs.Release[page];
   VMDefs.WaitFile[handle];
   VMDefs.SetFileLength[handle, pos];
   END;

MyCloseFile: PROC[fileSystem: FTPDefs.FileSystem,
               fileHandle: FTPDefs.FileHandle,
               aborted: BOOLEAN ] =
   BEGIN
   VMDefs.CloseFile[LOOPHOLE[fileHandle]];
   END;

MyDeleteFile: PROC[fileSystem: FTPDefs.FileSystem, file: STRING] =
   BEGIN
   real: BodyDefs.RName = LOOPHOLE[fileSystem];
   handle: VMDefs.FileHandle;
   CheckAccess[real, write, file];
   handle ← OpenVMFile[file, delete];
   LogAction[real, file, delete];
   VMDefs.DestroyFile[handle];
   END;

MyRenameFile: PROC[fileSystem: FTPDefs.FileSystem,
                   currentFile, newFile: STRING] =
   BEGIN
   real: BodyDefs.RName = LOOPHOLE[fileSystem];
   CheckAccess[real, write, newFile];
   CheckAccess[real, read, currentFile];
   LogAction[real, currentFile, rename];
   IF NOT VMSpecial.QuickAndDirtyAltoRename[old: currentFile, new: newFile]
   THEN ERROR FTPDefs.FTPError[requestedAccessDenied,
          "source of rename doesn't exist or destination already exists"L];
   END;


-- Backstop and Filter for listeners --

Backstop: FTPDefs.BackstopServer ←
   BEGIN
   addr: PupDefs.PupAddress;
   IF NOT PupDefs.ParsePupAddressConstant[ @addr, originOfRequest ]
   THEN BEGIN addr.net ← [0]; addr.host ← [0]; END;
   localInsignia.length ← 0;
   String.AppendString[localInsignia, "Grapevine FTP server on host "L];
   PupDefs.AppendMyName[localInsignia];
   server[ !
      FTPDefs.FTPError => SELECT ftpError FROM
          IN FTPDefs.CommunicationError,
          IN FTPDefs.ProtocolError => CONTINUE;
          IN FTPDefs.UnidentifiedError => CONTINUE;
        ENDCASE => RESUME;
      WhoIsHe => RESUME[addr.net, addr.host];
      VMDefs.Error => SELECT reason FROM
        io =>
          ERROR FTPDefs.FTPError[fileDataError,
                                 "Unrecoverable i/o error"L];
        resources =>
          ERROR FTPDefs.FTPError[noRoomForFile,
                                 "Backing store full"L];
        ENDCASE =>
       ERROR FTPDefs.FTPError[unidentifiedPermanentError,
                              "Unidentified ""VMDefs.Error"""L];
      VMDefs.CantWriteBackingStore =>
          ERROR FTPDefs.FTPError[fileDataError,
                                 "Can't write page to backing store"L];
      VMDefs.CantReadBackingStore =>
          ERROR FTPDefs.FTPError[fileDataError,
                                 "Can't read page from backing store"L];
     ];
   PolicyDefs.EndOperation[FTP];
   END;

Filter: PROC[from: STRING, purpose: FTPDefs.Purpose] =
   BEGIN
   IF NOT PolicyDefs.CheckOperation[FTP]
   THEN BEGIN
        LogDefs.WriteLogEntry["Rejected FTP connection"L];
        ERROR FTPDefs.RejectThisConnection["Server full"L];
        END;
   END;


-- Initialization --

myFilePrimitives: FTPDefs.FilePrimitivesObject ← [
   CreateFileSystem:   MyCreate,
   DestroyFileSystem:  MyDestroy,
   DecomposeFilename:  MyDecompose,
   ComposeFilename:    MyCompose,
   InspectCredentials: MyInspectCredentials,
   EnumerateFiles:     MyEnumerateFiles,
   OpenFile:           MyOpenFile,
   ReadFile:           MyReadFile,
   WriteFile:          MyWriteFile,
   CloseFile:          MyCloseFile,
   DeleteFile:         MyDeleteFile,
   RenameFile:         MyRenameFile
   ];

FTPDefs.FTPInitialize[];
FTPDefs.FTPCatchUnidentifiedErrors[FALSE];

[] ← FTPDefs.FTPCreateListener[
        --purpose--      files,
        --file system--  @myFilePrimitives,
        --mail system--  NIL,
        --comm system--  FTPDefs.PupCommunicationPrimitives[],
        --backstop--     @Backstop,
        --backstopData-- 0,
        --filter--       Filter];

END.