Copyright (C) 1995, Digital Equipment Corporation. 
 All rights reserved. 
 Last modified on Sat Feb 22 15:13:48 PST 1997 by steveg 

This interface covers HTTP version 0.9, 1.0 and 1.1. Preference is given to HTTP 1.1 as the primary version.

Correct behaviour is defined by the HTTP 1.1 and HTTP 1.0 specifications.

  App, Rd, Thread, Time, Wr;

  Version = RECORD major, minor: INTEGER; END;

  Version0_9 = Version{0, 9};
  Version1_0 = Version{1, 0};
  Version1_1 = Version{1, 1};

  CurrentVersion = Version1_1;
  (* The default version of HTTP for this interface *)
This interface uses a canonical format for HTTP URL as follows: the scheme is in lower-case the host name is in lower-case when reading a URL, all escaped character (%xx) are converted to their ASCII equivalent (except for the query field when writing a URL, all reserved and unsafe characters are converted to their escaped (lower-case hex) representation

The first of these URL's is the canonical written representation. All four URL's are equivalent.

  Style = REF RECORD
    version: Version;
    wrappedLines, absoluteURLs: BOOLEAN;
    (* "wrappedLines" = TRUE means that field values can wrap to
         the next line if it begins with white space
       "absoluteURLs" = TRUE means that all generated URLs should
         be absolute (include the host name) and not be relative *)
    viaFieldValue: TEXT;

PROCEDURE SetDefaultViaFieldValue(READONLY version: Version;
                                   port   : INTEGER;
                                   alias  : TEXT      := NIL);
  (* generate and set the default viaFieldValue for the default style.
     This field MUST be set for proxies.  The default value has
     the form:

     <hostname> <version> <programName>

     If "alias" is non-NIL, then it is used for "hostName" otherwise
     it will be host:port.  programName comes from the programInfo and
     so "SetDefaultViaFieldValue" SHOULD be called after

PROCEDURE DefaultStyle(READONLY version: Version := CurrentVersion): Style;
allocates and returns the default style for the given version, or the default style for the closest version if the version is not known

  URLFormat = {Default, Canonical, BodyOnly};

  URL <: URLPublic;
  URLPublic =
      absPath                      : BOOLEAN := TRUE;
      scheme                       : TEXT    := "http";
      host                         : TEXT    := "";
      port                         : INTEGER := 80;
      path, params, query, fragment: TEXT    := "";
      (* URL format is:
         The values of the parsed fields are all in canonical form.
         "absPath" is TRUE if the '/' exists before "path".  All field
         except for "query" have been unescaped - it can't be bacause it
         has a format that depends on escapable characters *)
      url: TEXT;
      (* "url" is the original text source of the URL (if known).  The
         "toText" method returns this field (if it exists) it is important
         to set "url" to NIL if ANY field is changed. *)
      init       (url: TEXT; log: App.Log): URL RAISES {App.Error};
      initFromRd (rd: Rd.T; log: App.Log): URL RAISES {App.Error};
      (* parses a url into its components in canonical format "relative" is
         true if only the body of the URL is defined.

         If the scheme, host, body or fragment is empty, the empty string
         is used.  If the port is not given, then "ParseURL" assigns the
         normal default port for the scheme (-1 if no scheme is given). *)
      toText (format := URLFormat.Default): TEXT;
      (* URL's can be displayed in several formats.  The "Default" format
         is the value of the "url" field or the "Canonical" format if the
         "url" field is NIL.  The "Canonical" format is the assemblage of
         the url's fields in canonical format.  The "BodyOnly" format skips
         the "host" and "port" fields.  (It is used for "old-style" - pre
         1.1 - GETs) *)
      equivalent (url: URL): BOOLEAN;
      (* "equivalent" returns TRUE if "self" and "url" are equal in
         canonical format *)
      local (service: INTEGER): BOOLEAN;
      (* "local" returns TRUE if the URL is local to the server associated
         with the service numberer "service"*)
      derelativize (url: URL): URL;
      (* if "self" is a relative URL, then "derelativize" returns the
         absolute URL produced by making self relative to "url".  If "self"
         is an absolute URL, then "self" is returned. *)

  Field <: FieldPublic;
  FieldPublic = OBJECT
                  name, value: TEXT;
                  init (name, value: TEXT): Field;

  Header <: HeaderPublic;
  (* An HTTP header includes a series of name/value pairs. *)
  HeaderPublic =
      lookupField (name: TEXT; value: TEXT := NIL): Field;
      (* return the value of the field named "name", or NIL if no field
         with "name" exists.  If "value" is NIL, then the first field with
         name is returned, otherwise, the first field with that name and
         value is returned. *)
      addField (field: Field; after: Field := NIL): Field;
      (* add a field with "name" and "value" to the header.  There can be
         multiple fields with the same name.  If "after" is NIL then
         "field" will be added as the first field, otherwise "field" will
         be added after "after".  If "after" is not in the list, then
         "field" will be added as the first field.  The result is the new
         field. *)
      removeField (field: Field): BOOLEAN;
      (* remove a field with name "".  If "field.value" is NIL,
         then the first field with the name is removed.  Otherwise, the
         first field with a matching name and value is removed and TRUE is
         returned.  If no field matches, no change is made and FALSE is
         returned. *)
      copyFields (to: Header);
      (* add all of the field to "to" *)
      iterateFields (): FieldIterator;
      (* returns an iterator for all of the fields in the header *)

  FieldIterator <: FieldIteratorPublic;
  FieldIteratorPublic =
      next (): Field;
      (* returns the next field, returns NIL after last field.  If the
         field list is modified, while it is iterated, then the result of
         "next" is unspecified. *)

  FieldType =
    {Accept, Accept_Charset, Accept_Encoding, Accept_Language,
     Accept_Ranges, Age, Allow, Authorization, Cache_Control, Connection,
     Content_Base, Content_Encoding, Content_Language, Content_Length,
     Content_Location, Content_MD5, Content_Range, Content_Type, Date,
     ETag, Expires, From, Host, If_Modified_Since, If_Match, If_None_Match,
     If_Range, If_Unmodified_Since, Last_Modified, Location, Max_Forwards,
     Pragma, Proxy_Authenticate, Proxy_Authorization, Public, Range,
     Referer, Retry_After, Server, Transfer_Encoding, Upgrade, User_Agent,
     Vary, Via, Warning, WWW_Authenticate};

  FieldName = ARRAY FieldType OF
                     "Accept", "Accept-Charset", "Accept-Encoding",
                     "Accept-Language", "Accept-Ranges", "Age", "Allow",
                     "Authorization", "Cache-Control", "Connection",
                     "Content-Base", "Content-Encoding",
                     "Content-Language", "Content-Length",
                     "Content-Location", "Content-MD5", "Content-Range",
                     "Content-Type", "Date", "ETag", "Expires", "From",
                     "Host", "If-Modified-Since", "If-Match",
                     "If-None-Match", "If-Range", "If-Unmodified-Since",
                     "Last-Modified", "Location", "Max-Forwards", "Pragma",
                     "Proxy-Authenticate", "Proxy-Authorization", "Public",
                     "Range", "Referer", "Retry-After", "Server",
                     "Transfer-Encoding", "Upgrade", "User-Agent", "Vary",
                     "Via", "Warning", "WWW-Authenticate"};

  StatusType =
    {Continue, Switching_Protocols, OK, Created, Accepted,
     Non_Authoritative_Information, No_Content, Reset_Content,
     Partial_Content, Multiple_Choices, Moved_Permanently,
     Moved_Temporarily, See_Other, Not_Modified, Use_Proxy, Bad_Request,
     Unauthorized, Payment_Required, Forbidden, Not_Found,
     Method_Not_Allowed, Not_Acceptable, Proxy_Authentication_Required,
     Request_Time_out, Conflict, Gone, Length_Required,
     Precondition_Failed, Request_Entity_Too_Large, Request_URI_Too_Large,
     Unsupported_Media_Type, Internal_Server_Error, Not_Implemented,
     Bad_Gateway, Service_Unavailable, Gateway_Time_out,

  StatusCode = ARRAY StatusType OF
                   100, 101, 200, 201, 202, 203, 204, 205, 206, 300, 301,
                   302, 303, 304, 305, 400, 401, 402, 403, 404, 405, 406,
                   407, 408, 409, 410, 411, 412, 413, 414, 415, 500, 501,
                   502, 503, 504, 505};
  StatusReason = ARRAY StatusType OF
                 "Continue", "Switching Protocols", "OK", "Created",
                 "Accepted", "Non-Authoritative Information", "No Content",
                 "Reset Content", "Partial Content", "Multiple Choices",
                 "Moved Permanently", "Moved Temporarily", "See Other",
                 "Not Modified", "Use Proxy", "Bad Request",
                 "Unauthorized", "Payment Required", "Forbidden",
                 "Not Found", "Method Not Allowed", "Not Acceptable",
                 "Proxy Authentication Required", "Request Time-out",
                 "Conflict", "Gone", "Length Required",
                 "Precondition Failed", "Request Entity Too Large",
                 "Request-URI Too Large", "Unsupported Media Type",
                 "Internal Server Error", "Not Implemented", "Bad Gateway",
                 "Service Unavailable", "Gateway Time-out",
                 "HTTP Version not supported"};

  Method = {Options, Get, Post, Put, Delete, Head, Trace, Connect};

  MethodText = ARRAY Method OF TEXT{"OPTIONS", "GET", "POST", "PUT",
                                    "DELETE", "HEAD", "TRACE", "CONNECT"};

  Request <: RequestPublic;
  RequestPublic =
    Header OBJECT
      method  : Method;
      url     : URL;
      version : Version  := CurrentVersion;
      postData: TEXT     := NIL;
      parse (rd: Rd.T; log: App.Log): Request RAISES {App.Error};
      (* Parse the HTTP request in "rd", sends any messages to "log", and
         returns the parsed header of the request.  On return, "rd" points
         to the first character after the header. *)
      write (wr: Wr.T; style: Style; proxyRequest: BOOLEAN; log: App.Log)
             RAISES {App.Error};
      (* Write the HTTP request described in "request" to "wr".  If
         "proxyRequest" then the full URL is written, otherwise only the
         body is writen.  If "style" is NIL then DefaultStyle() is used. *)
      toText (style: Style; proxyRequest: BOOLEAN; log: App.Log): TEXT
              RAISES {App.Error};
      (* return a text of the request.  If "proxyRequest" then the full URL
         is written, otherwise only the body is writen.  If "style" is NIL
         then DefaultStyle() is used. *)

  Reply <: ReplyPublic;
  ReplyPublic =
    Header OBJECT
      version: Version := CurrentVersion;
      code   : INTEGER := StatusCode[StatusType.OK];
      reason : TEXT    := StatusReason[StatusType.OK];
      parse (rd: Rd.T; log: App.Log): Reply RAISES {App.Error};
      (* Parse the HTTP reply in "rd", sends any messages to "log", and
         returns the parsed header of the reply.  On return, "rd" points to
         the first character after the header.

         If the reply is an HTTP/0.9 reply, then "self.version" is
         Version0_9 and "self.reason" is the text already read while
         looking for the first HTTP line. *)
      write (wr: Wr.T; style: Style; log: App.Log) RAISES {App.Error};
      (* Write the HTTP Reply headers to "wr".  If "style" is NIL, then
         DefaultStyle() is used. *)

      toText (style: Style; log: App.Log): TEXT RAISES {App.Error};
      (* return a text of the reply.  If "style" is NIL, then
         DefaultStyle() is used. *)

PROCEDURE WriteSimpleReplyHeader (wr     : Wr.T;
                                  style  : Style;
                                  log    : App.Log;
                                  code: INTEGER := StatusCode[
                                  reason: TEXT := StatusReason[
  RAISES {App.Error};
  (* write the first line of a HTTP reply to "wr" based on
     "style", "code" and "reason".  The client can follow
     this with additional headers and must write a blank line
     to end the header.

     If "style" is NIL, then DefaultStyle() is used.


PROCEDURE WriteRedirectReply(wr: Wr.T; url, htmlMsg: TEXT; log: App.Log)
  RAISES {App.Error};
  (* write a redirect to "url" with content of "htmlMsg" reply to "wr".
     IF "htmlMsg" is NIL then a generic resource has moved message is
     given.  *)

PROCEDURE WriteTime(wr: Wr.T; time: Time.T; log: App.Log) RAISES {App.Error};
PROCEDURE ReadTime(rd: Rd.T; log: App.Log): Time.T RAISES {App.Error};
Write the time in RFC 822/RFC 1123 format. Read the time in RFC 822/RFC 1123, RFC 850, asctime, and other formats

  ProgramType = {Client, Proxy, Server, Tunnel};
  ProgramInfo = RECORD
                  type                  : ProgramType;
                  name                  : TEXT;
                  authType                              := AuthType.None;
                  authRealm, authAccount: TEXT          := "";
  (* if "authType" is not "AuthType.None" then "authRealm" and "authAccount"
     are used for authentication *)

PROCEDURE SetProgramInfo(READONLY programInfo: ProgramInfo);
PROCEDURE GetProgramInfo(): ProgramInfo;
Get and set the program information. Depending on the type of the program, the appropriate field(s) is (are) added to the requests and response. If programType is Proxy then the program should call SetDefaultViaFieldValue.


  FormQuery <: FormQueryPublic;
  FormQueryPublic =
    Header OBJECT
      init (query: TEXT): FormQuery RAISES {BadFormQuery};
      initFromRd (rd: Rd.T): FormQuery
                    RAISES {BadFormQuery};
      write  (wr: Wr.T; log: App.Log) RAISES {App.Error};
      toText (): TEXT;
  (* a FormQuery corresponds to a parsed query segment with the format:

     each name=value pair becomes a field of the form object.

     the %xx encoded characters ARE unescaped when the form query is
     initialized, and they ARE escaped when the form query is written.

  AuthType = {None, Server, Proxy};

PROCEDURE BasicAuthField(account: TEXT; auth: AuthType): Field;
  (* Returns the Basic authentication field for "account" (name:password)
     for either a server or proxy.
     Basic authentication is described in:

PROCEDURE AuthorizedRequest (request: Request;
                             auth   : AuthType;
                             account: TEXT;
                             log    : App.Log   ): BOOLEAN
  RAISES {App.Error};
  (* Returns TRUE if "request" has valid authentication for
     "account" (formatted as: "name:password") on either the server or proxy *)

PROCEDURE ReplyUnauthorized (wr        : Wr.T;
                             auth      : AuthType;
                             realm     : TEXT;
                             log       : App.Log;
                             defaultMsg: BOOLEAN    := TRUE)
  RAISES {App.Error};
  (* Write an "unauthorized" reply to "wr" covering "realm" for
     either the server or proxy.  Send "Content-type: text/html".
     If "defaultMsg" then a simple
     default message is given.  Otherwise the client is responsible for
     providing the message. *)

    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, (* +, / *)
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, (* 0..9 *)
    -1, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, (* A..O *)
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, (* P..Z *)
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, (* a..o *)
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, ..};            (* p..z *)

  Base64Encode = ARRAY [0..63] OF CHAR{
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
    'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
    'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
    'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
    'w', 'x', 'y', 'z', '0', '1', '2', '3',
    '4', '5', '6', '7', '8', '9', '+', '/'};
Decode and return the <user>:<password> authorization field in a header, or NIL if not there
PROCEDURE AuthorizationAccount (request: Request;
                                auth   : AuthType;
                                log    : App.Log   ): TEXT
  RAISES {App.Error};


  Dest = OBJECT
      RAISES {Wr.Failure, Thread.Alerted, CopyError};
  (* "copy" processes the characters in "a".  "a" may be of
     any length.  The number of times "copy" is called and the
     total characters in the calls is not known ahead of time. *)

  WrDest <: WrDestPublic;
  WrDestPublic = Dest OBJECT
    init(wr: Wr.T): WrDest;
    (* A "WrDest.copy" puts its data into "wr" *)

  Src = OBJECT
    fill(VAR (* OUT *) a: ARRAY OF CHAR): CARDINAL
        RAISES {Rd.Failure, Thread.Alerted, CopyError};
  (* "fill" puts characters in "a" and returns the number put in "a".
     If "fill" puts fewer than NUMBER(a) characters in "a" then
     it is assumed that there are no more characters and "fill" should
     not be called any more *)

  RdSrc <: RdSrcPublic;
  RdSrcPublic = Src OBJECT
    init(rd: Rd.T): RdSrc;
    (* A "RdSrc.fill" gets its data from "rd" *)

PROCEDURE ReadBody (requestOrReply: Header;
                    rd            : Rd.T;
                    dest          : Dest;
                    log           : App.Log ) RAISES {App.Error};
read the body from rd calling dest.copy as necessary. The body is read using the transfer coding specifed in the requestOrReply header fields, content-length, chunked, closing the connection or whatever.

PROCEDURE WriteBody (requestOrReply: Header;
                     wr            : Wr.T;
                     src           : Src;
                     log           : App.Log ) RAISES {App.Error};
write the body to wr calling src.fill as necessary.

IF there is a Transfer-Encoding: chunked header field in requestOrReply then the body will be written in the chunked format. The end of the body is signified when src.fill runs out of characters (returns fewer than possible on some call).

If there is a Content_Length: <length> header field in the requestOrReply header, then <length> bytes will be written. It is an error if there are not <length> bytes available.

Otherwise, the body is written and the end of the body is signified when src.fill runs out of characters (returns fewer than possible on some call).

PROCEDURE EscapeURLEntry(entry: TEXT): TEXT;
PROCEDURE UnescapeURLEntry(entry: TEXT; log: App.Log): TEXT RAISES {App.Error};
Escape or Unescape the characters in a URL body. Reserved characters are escaped as %xx where xx is the hex code for the character.

PROCEDURE DecodeTextForHTML(text: TEXT; log: App.Log): TEXT RAISES {App.Error};
Encode and Decode special HTML characters (<>&) for display in HTML form fields

  Ctl = SET OF CHAR{'\000'.. '\037', '\177'};
  TSpecial = SET OF
               CHAR{'(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/',
                    '[', ']', '?', '=', '{', '}', ' ', '\t'};
  Token = SET OF CHAR {'\000' .. '\377'} - Ctl - TSpecial;

  UserAgent = {Netscape, InternetExplorer, Other};

  NoVersion = -1;
Get the user agent and its version from the request.
PROCEDURE GetUserAgent (req: Request; VAR (* out *) version: INTEGER):