http/src/HTTPApp.m3


 Copyright (C) 1995, Digital Equipment Corporation. 
 All rights reserved. 
 Created by steveg 
                                                                           
 Parts Copyright (C) 1997, Columbia University                             
 All rights reserved.                                                      

 * Last Modified By: Blair MacIntyre
 * Last Modified On: Mon Aug  4 14:50:46 1997
 

MODULE HTTPApp;

<* PRAGMA LL *>

IMPORT App, ConnRW, FloatMode, Fmt, HTTP, IP, Lex, Rd, RdUtils, TCP,
       TCPPeer, Text, TextExtras, TextRd, Thread, Wr;
ClientLog logs the message to the client (through wr) and to the application log (through log)
TYPE
  ClientLog = App.Log OBJECT
    appLog: App.Log;
    wr: Wr.T;
    msg: TEXT;
    serverPush: BOOLEAN;
  OVERRIDES
    log := MessageLog;
  END;

PROCEDURE MessageLog (self: ClientLog; msg: TEXT; status: App.LogStatus)
  RAISES {App.Error} =
  BEGIN
    IF status = App.LogStatus.Error THEN
      IF self.serverPush THEN
        self.msg := Fmt.F("%s%s\n", self.msg, msg);
        ServerPushFrame(self.wr, "text/plain", self.msg, self.appLog);
      ELSE
        TRY
          Wr.PutText(self.wr, msg);
          Wr.PutText(self.wr, "\n");
        EXCEPT
        | Wr.Failure, Thread.Alerted =>
            self.appLog.log(
              "Error sending message to client", App.LogStatus.Error);
        END;
      END;
    END;
    self.appLog.log(msg, status);
  END MessageLog;

PROCEDURE WrLog (old: App.Log; wr: Wr.T; serverPush: BOOLEAN := FALSE):
  App.Log =
  BEGIN
    RETURN NEW(ClientLog, appLog := old, wr := wr,
               msg := "", serverPush := serverPush);
  END WrLog;

VAR
  readWriteMu := NEW(MUTEX);
  readWriteCV := NEW(Thread.Condition);
  readingCnt := 0;
  writingCnt := 0;
  (* single writer, multiple reader algorithm.

     if a thread is writing, then writingCnt # 0.
     if a thread is reading, then readingCnt # 0.

     a thread can read if another thread is reading.
     a thread cannot read if another thread is writing.
     a thread can write if no thread is reading or writing.

     readingCnt and writingCnt are protected by readWriteMu.
  *)

PROCEDURE ReadLock() =
  BEGIN
    LOCK readWriteMu DO
      WHILE writingCnt # 0 DO
        Thread.Wait(readWriteMu, readWriteCV);
      END;

      INC(readingCnt);
    END;
  END ReadLock;

PROCEDURE ReadUnlock() =
  BEGIN
    LOCK readWriteMu DO
      DEC(readingCnt);
    END;
  END ReadUnlock;

PROCEDURE WriteLock() =
  BEGIN
    LOCK readWriteMu DO
      WHILE readingCnt # 0 OR writingCnt # 0 DO
        Thread.Wait(readWriteMu, readWriteCV);
      END;

      INC(writingCnt);
    END;
  END WriteLock;

PROCEDURE WriteUnlock() =
  BEGIN
    LOCK readWriteMu DO
      DEC(writingCnt);
    END;
  END WriteUnlock;

REVEAL
  RequestHandler = RequestHandlerPublic BRANDED
  "HTTPApp.RequestHandler" OBJECT
    port: INTEGER;
  OVERRIDES
    accept := DefaultAccept;
    request := DefaultRequest;
  END;

PROCEDURE DefaultAccept (<* UNUSED *>     self       : RequestHandler;
                         <* UNUSED *>     request    : HTTP.Request;
                         <* UNUSED *>     serverData : REFANY;
                         <* UNUSED *> VAR acceptState: REFANY;
                         <* UNUSED *>     log        : App.Log         ):
  BOOLEAN =
  BEGIN
    <* ASSERT FALSE *>
  END DefaultAccept;

PROCEDURE DefaultRequest(<* UNUSED *> self: RequestHandler;
                         <* UNUSED *> request: HTTP.Request;
                         <* UNUSED *> serverData, acceptState: REFANY;
                         <* UNUSED *> rd: Rd.T;
                         <* UNUSED *> wr: Wr.T;
                         <* UNUSED *> log: App.Log) =
  BEGIN
    <* ASSERT FALSE *>
  END DefaultRequest;

REVEAL
  ReplyHandler = ReplyHandlerPublic BRANDED "HTTPApp.ReplyHandler" OBJECT
  OVERRIDES
    reply := DefaultReply;
  END;

PROCEDURE DefaultReply(<* UNUSED *> self: ReplyHandler;
                       <* UNUSED *> reply: HTTP.Reply;
                       <* UNUSED *> rd: Rd.T;
                       <* UNUSED *> wr: Wr.T;
                       <* UNUSED *> log: App.Log) =
  BEGIN
    <* ASSERT FALSE *>
  END DefaultReply;

REVEAL
  Proxy = ProxyPublic BRANDED "HTTPApp.Proxy" OBJECT
          OVERRIDES
            init := InitProxy;
            add  := AddProxy;
          END;

PROCEDURE InitProxy (self: Proxy): Proxy =
  BEGIN
    self.rules := NIL;
    self.tail := NIL;
    RETURN self;
  END InitProxy;

CONST
  NotComma = SET OF CHAR{'\000' .. '\377'} - SET OF CHAR{','};

PROCEDURE AddProxy (self: Proxy; ruleTxt: TEXT; log: App.Log)
  RAISES {App.Error} =
  VAR
    rd               := TextRd.New(ruleTxt);
    rule             := NEW(ProxyRules);
    prev: ServerList := NIL;
    host: TEXT;
  BEGIN
    IF log = NIL THEN log := App.defaultLog; END;
    TRY
      rule.hostPattern := Lex.Scan(rd);
      IF Text.Length(rule.hostPattern) = 0
           OR Text.GetChar(
                rule.hostPattern, Text.Length(rule.hostPattern) - 1) = '\\' THEN
        log.log(
          Fmt.F("Bad pattern (%s) in hostPattern proxy(s) value: \"%s\"",
                rule.hostPattern, ruleTxt), App.LogStatus.Error);
      END;

      TRY
        LOOP
          Lex.Skip(rd);
          host := Lex.Scan(rd, NotComma);
          IF Text.Length(host) > 0 THEN
            WITH server = NEW(ServerList,
                              head := NEW(Server).initParse(host, log),
                              tail := NIL) DO
              IF prev = NIL THEN
                prev := server;
                rule.proxy := prev;
              ELSE
                prev.tail := server;
                prev := prev.tail
              END;
            END;
          END;
          EVAL Rd.GetChar(rd);
        END;
      EXCEPT
      | Rd.EndOfFile =>
      END;
    EXCEPT
    | Rd.Failure, Thread.Alerted =>
        log.log(Fmt.F("Bad urlPattern:proxy value: \"%s\"", ruleTxt),
                App.LogStatus.Error);
    END;
    IF self.tail = NIL THEN
      self.rules := rule;
    ELSE
      self.tail.tail := rule;
    END;
    self.tail := rule;
  END AddProxy;

REVEAL
  Server = ServerPublic BRANDED "HTTPApp.Server" OBJECT
  OVERRIDES
    init := InitServer;
    initParse := InitParseServer;
  END;

PROCEDURE InitServer (self  : Server;
                      server: TEXT;
                      port  : INTEGER;
                      log   : App.Log  ): Server RAISES {App.Error} =
  VAR addr: IP.Address;
  BEGIN
    IF Text.Equal(server, "DIRECT") THEN
      self.server := NIL
    ELSE
      self.server := server;
      self.port := port;
      self.endpoint := IP.NullEndPoint;
      TRY
        IF NOT IP.GetHostByName(server, addr) THEN
          log.log(Fmt.F("Cannot find %s", server), App.LogStatus.Status);
          RETURN self;
        END;
      EXCEPT
      | IP.Error =>
          log.log(Fmt.F("Cannot find %s", server), App.LogStatus.Status);
          RETURN self;
      END;
      self.endpoint := IP.Endpoint{addr := addr, port := port};
    END;
    RETURN self;
  END InitServer;

CONST
  NonColon = SET OF CHAR{'\000'..'\377'} - SET OF CHAR{':'};
  NonSlash = SET OF CHAR{'\000'..'\377'} - SET OF CHAR{'/'};

PROCEDURE InitParseServer (self: Server; serverAndPort: TEXT; log: App.Log):
  Server RAISES {App.Error} =
  VAR
    server: TEXT;
    port         := 0;
  BEGIN
    IF Text.Equal(serverAndPort, "DIRECT") THEN
      server := "DIRECT";
    ELSE
      WITH rd = TextRd.New(serverAndPort) DO
        TRY
          server := Lex.Scan(rd, NonColon);
          IF Text.Equal(server, "http") THEN
            WITH url = NEW(HTTP.URL).init(serverAndPort, log) DO
              server := url.host;
              port := url.port;
            END;
          ELSE
            IF NOT Rd.EOF(rd) THEN
              EVAL Rd.GetChar(rd);
              port := Lex.Int(rd);
            END;
          END;
        EXCEPT
        | Lex.Error, FloatMode.Trap, Rd.EndOfFile, Rd.Failure,
              Thread.Alerted =>
            log.log(
              Fmt.F("Bad server and port given: \"%s\" (need server:port)",
                    serverAndPort), App.LogStatus.Error);
        END;
      END;
    END;
    RETURN self.init(server, port, log);
  END InitParseServer;

TYPE
  RequestHandlerList = REF RECORD
    head: RequestHandler;
    tail: RequestHandlerList;
  END;

VAR
  requestHandlerList: RequestHandlerList := NIL;

PROCEDURE FindRequestHandler (              request      : HTTP.Request;
                                            serverData   : REFANY;
                              VAR (* OUT *) acceptState  : REFANY;
                                            port, service: INTEGER;
                                            log          : App.Log       ):
  RequestHandler RAISES {App.Error} =
  VAR list: RequestHandlerList;
  BEGIN
    ReadLock();
    TRY
      IF requestHandlerList = NIL THEN RETURN NIL END;
      FOR i := FIRST(RequestPriority) TO LAST(RequestPriority) DO
        list := requestHandlerList;
        REPEAT
          IF list.head.priority = i
               AND (list.head.port = service OR list.head.port = port
                      OR list.head.port = AnyPort)
               AND list.head.accept(request, serverData, acceptState, log) THEN
            RETURN list.head
          END;
          list := list.tail;
        UNTIL list = NIL;
      END;
    FINALLY
      ReadUnlock();
    END;
    RETURN NIL;
  END FindRequestHandler;

TYPE
  Closure = Thread.Closure OBJECT
              client: TCP.T;
              log   : App.Log;
              wrLog : BOOLEAN;
              data  : REFANY;
              port, service  : INTEGER;
            OVERRIDES
              apply := ServerHandler;
            END;

PROCEDURE GetNameDontCrash (host: TCP.T): TEXT RAISES {IP.Error} =
  VAR res := TCPPeer.GetName(host);
  BEGIN
    IF res = NIL THEN
      WITH ep = TCPPeer.Get(host) DO
        RETURN
          Fmt.F("%s.%s.%s.%s", Fmt.Int(ep.addr.a[0]), Fmt.Int(ep.addr.a[1]),
                Fmt.Int(ep.addr.a[2]), Fmt.Int(ep.addr.a[3]));
      END;
    END;
    RETURN res
  END GetNameDontCrash;

PROCEDURE ServerHandler (cl: Closure): REFANY =
  VAR
    rd            : Rd.T;
    wr            : Wr.T;
    request       : HTTP.Request;
    requestHandler: RequestHandler;
    acceptState   : REFANY;
  BEGIN
    TRY
      TRY
        rd := ConnRW.NewRd(cl.client);
        wr := ConnRW.NewWr(cl.client);
        IF cl.wrLog THEN cl.log := WrLog(cl.log, wr, FALSE); END;

        request := NEW(HTTP.Request).parse(rd, cl.log);
        IF Text.Length(request.url.host) = 0 THEN
          request.url.host := App.GetHostName();
          request.url.port := cl.port;
        END;

        IF App.Verbose() THEN
          cl.log.log(Fmt.F("INCOMING REQUEST: %s",
                           request.toText(NIL, TRUE, cl.log)),
                     App.LogStatus.Verbose);
        ELSIF App.Debug() THEN
          TRY
            cl.log.log(Fmt.F("%s %s", GetNameDontCrash(cl.client),
                             request.url.toText()), App.LogStatus.Debug);
          EXCEPT
          | IP.Error =>
          END;
        END;

        requestHandler := FindRequestHandler(request, cl.data, acceptState,
                                             cl.port, cl.service, cl.log);
        IF requestHandler # NIL THEN
          requestHandler.request(
            request, cl.data, acceptState, rd, wr, cl.log);
        ELSE
          TRY
            HTTP.WriteSimpleReplyHeader(
              wr, NIL, cl.log, HTTP.StatusCode[HTTP.StatusType.Not_Found],
              HTTP.StatusReason[HTTP.StatusType.Not_Found]);
            Wr.PutText(wr, "Content-type: text/plain\r\n\r\n");
            Wr.PutText(wr, Fmt.F("The requested item: %s was not found\n",
                                 request.url.toText()));
          EXCEPT
          | Wr.Failure, Thread.Alerted =>
          END;
        END;
      FINALLY
        TRY Wr.Close(wr); EXCEPT | Wr.Failure, Thread.Alerted => END;
        TRY Rd.Close(rd); EXCEPT | Rd.Failure, Thread.Alerted => END;
        Thread.Pause(30.0d0);
        TCP.Close(cl.client);
      END;
    EXCEPT
    | App.Error =>
    END;
    RETURN NIL;
  END ServerHandler;

TYPE
  ServerPorts = REF RECORD
    port, service: INTEGER;
    next: ServerPorts;
  END;

VAR
  serverPorts: ServerPorts := NIL; <* LL = readWriteMu *>

PROCEDURE ServerPort (port, service: INTEGER): BOOLEAN =
  VAR sp: ServerPorts;
  BEGIN
    LOCK readWriteMu DO
      sp := serverPorts;
      WHILE sp # NIL DO
        IF sp.port = port
             AND (sp.service = service OR sp.service = AnyService) THEN
          RETURN TRUE;
        END;
        sp := sp.next;
      END;
      RETURN FALSE;
    END;
  END ServerPort;

PROCEDURE Serve (port, serviceValue: INTEGER; log: App.Log; data: REFANY)
  RAISES {App.Error} =
  VAR
    conn     : TCP.T;
    connector: TCP.Connector;
    nilLog                   := log = NIL;
  BEGIN
    LOCK readWriteMu DO
      serverPorts := NEW(ServerPorts, port := port, service := serviceValue,
                                    next := serverPorts);
    END;

    IF log = NIL THEN log := App.defaultLog END;
    TRY
      connector := TCP.NewConnector(
                     IP.Endpoint{addr := IP.NullAddress, port := port});
    EXCEPT
    | IP.Error (cause) =>
        log.log("IP.Error making connector: " & RdUtils.FailureText(cause),
                App.LogStatus.Error);
        RETURN;
    END;
    log.log(
      Fmt.F("Listening on port: %s", Fmt.Int(port)), App.LogStatus.Status);

    LOOP
      TRY
        conn := TCP.Accept(connector);
        EVAL Thread.Fork(
               NEW(Closure, client := conn, log := log, wrLog := nilLog,
                   data := data, port := port, service := serviceValue));
      EXCEPT
      | IP.Error, Thread.Alerted => (* continue *)
      END;
    END;
  END Serve;

PROCEDURE Direct (request: HTTP.Request; log: App.Log): TCP.T
  RAISES {App.Error} =
  VAR server := NEW(Server).init(request.url.host, request.url.port, log);
  BEGIN
    TRY
      RETURN TCP.Connect(server.endpoint);
    EXCEPT
    | IP.Error, Thread.Alerted =>
        log.log(Fmt.F("Unable to connect to server: %s:%s", server.server,
                      Fmt.Int(server.port)), App.LogStatus.Error);
    END;
    <* ASSERT FALSE *>
  END Direct;

PROCEDURE Client (             request : HTTP.Request;
                               proxy   : Proxy;
                               style   : HTTP.Style;
                               rdClient: Rd.T;
                               wrClient: Wr.T;
                               handler : ReplyHandler;
                  <* UNUSED *> service : INTEGER;
                               log     : App.Log       )
  RAISES {App.Error} =
  VAR
    conn        : TCP.T;
    wrServer    : Wr.T;
    rdServer    : Rd.T;
    rules       : ProxyRules;
    proxyRequest: BOOLEAN    := FALSE;
    proxies     : ServerList;
    hp          : TEXT;
  BEGIN
    IF log = NIL THEN log := App.defaultLog END;
    IF style = NIL THEN style := HTTP.DefaultStyle() END;
    IF App.Verbose() THEN
      log.log(
        Fmt.F("OUTGOING request: %s", request.toText(style, TRUE, log)),
        App.LogStatus.Verbose);
    END;

    IF Text.Length(request.url.host) = 0 THEN
      log.log(Fmt.F("No server given in requested URL: %s",
                    request.url.toText()), App.LogStatus.Error);
    END;

    IF proxy = NIL THEN
      conn := Direct(request, log);
    ELSE
      rules := proxy.rules;
      hp := Fmt.F("%s:%s", request.url.host, Fmt.Int(request.url.port));
      WHILE rules # NIL AND conn = NIL DO
        IF TextExtras.PatternMatch(hp, rules.hostPattern) THEN
          IF App.Verbose() THEN
            IF rules.proxy.head.server = NIL THEN
              log.log(Fmt.F("proxy rule matched: %s %s DIRECT",
                            Fmt.F("%s:%s", request.url.host,
                                  Fmt.Int(request.url.port)),
                            rules.hostPattern), App.LogStatus.Verbose);
            ELSE
              log.log(
                Fmt.F("proxy rule matched: %s %s %s",
                      Fmt.F("%s:%s", request.url.host,
                            Fmt.Int(request.url.port)), rules.hostPattern,
                      rules.proxy.head.server), App.LogStatus.Verbose);
            END;
          END;
          proxies := rules.proxy;
          WHILE proxies # NIL DO
            WITH server = proxies.head DO
              IF server.server = NIL THEN
                conn := Direct(request, log);
                EXIT;
              ELSE
                TRY
                  IF server.endpoint = IP.NullEndPoint THEN
                    (* try to resolve another time *)
                    EVAL server.init(server.server, server.port, log);
                  END;
                  conn := TCP.Connect(server.endpoint);
                  proxyRequest := TRUE;
                  EXIT;
                EXCEPT
                | IP.Error, Thread.Alerted =>
                    log.log(
                      Fmt.F("Unable to connect to proxy server: %s:%s",
                            server.server, Fmt.Int(server.port)),
                      App.LogStatus.Status);
                END;
              END;
            END;
            proxies := proxies.tail;
          END;
        END;
        rules := rules.tail;
      END;

      IF conn = NIL THEN
        log.log(Fmt.F("proxy rule matched: %s DIRECT (default)",
                      Fmt.F("%s:%s", request.url.host,
                            Fmt.Int(request.url.port))),
                App.LogStatus.Verbose);
        conn := Direct(request, log);
      END;
    END;

    wrServer := ConnRW.NewWr(conn);
    rdServer := ConnRW.NewRd(conn);
    TRY
      TRY
        TRY
          request.write(wrServer, style, proxyRequest, log);
          IF rdClient # NIL AND request.method = HTTP.Method.Post
               AND request.postData # NIL THEN
            Wr.PutText(wrServer, request.postData);
          END;
        FINALLY
          (* SCG/MSM 2/22/96 A CERN proxy gets confused if we close the wr
             while still using the rd.

             Symptoms:

             Sending large files though the cern proxy fail partway
             through.  The reader blocks on a read and the socket is in
             FIN_WAIT_2. *)
          Wr.Flush(wrServer);
        END;
        WITH reply = NEW(HTTP.Reply).parse(rdServer, log) DO
          IF App.Verbose() THEN
            log.log(reply.toText(NIL, log), App.LogStatus.Verbose);
          END;
          handler.reply(reply, rdServer, wrClient, log);
        END;
      FINALLY
        Wr.Close(wrServer);
        Rd.Close(rdServer);
        TCP.Close(conn);
      END;
    EXCEPT
    | Rd.Failure, Thread.Alerted, Wr.Failure =>
        log.log(Fmt.F("error get url: %s", request.url.toText()),
                App.LogStatus.Error);
    END;
  END Client;

PROCEDURE RegisterRequestHandler(port: INTEGER; handler: RequestHandler) =
  BEGIN
    WriteLock();
    TRY
      handler.port := port;
      requestHandlerList := NEW(RequestHandlerList, head := handler,
                                tail := requestHandlerList);
    FINALLY
      WriteUnlock();
    END;
  END RegisterRequestHandler;

PROCEDURE ServerPushSupported(request: HTTP.Request): BOOLEAN =
  VAR
    field := request.lookupField("User-Agent");
    trd: TextRd.T;
    agent: TEXT;
  BEGIN
    TRY
      IF field # NIL THEN
        trd := TextRd.New(field.value);
        agent := Lex.Scan(trd, NonSlash);
        IF Text.Equal(agent, "Mozilla") THEN RETURN TRUE END;
      END;
    EXCEPT
    | Rd.Failure, Thread.Alerted =>
    END;
    RETURN FALSE;
  END ServerPushSupported;

PROCEDURE ServerPushFrame(wr: Wr.T; contentType, msg: TEXT; log: App.Log) RAISES {App.Error} =
  BEGIN
    TRY
      IF App.Verbose() THEN
        log.log(Fmt.F("Serverpush: %s", msg), App.LogStatus.Verbose);
      END;
      Wr.PutText(wr, ServerPushBoundaryStringStart);
      Wr.PutText(wr, Fmt.F("Content-type: %s\r\nContent-length: %s\r\n\r\n",
                           contentType, Fmt.Int(Text.Length(msg))));
      Wr.PutText(wr, msg);
      Wr.Flush(wr);
    EXCEPT
    | Wr.Failure, Thread.Alerted =>
        IF App.Debug() THEN
           log.log(Fmt.F("Failure writing \"%s\" server push frame", msg),
                   App.LogStatus.Debug);
        END;
    END;
  END ServerPushFrame;

VAR
  proxy: Proxy := NEW(Proxy).init();

PROCEDURE DefaultProxy(<* UNUSED *> log: App.Log): Proxy =
  BEGIN
    RETURN proxy;
  END DefaultProxy;

TYPE
  Arg = {ProxyServer};

  ArgHandler = App.ArgHandler OBJECT
  OVERRIDES
    set := SetArg;
  END;

PROCEDURE SetArg (            self : ArgHandler;
                  <*UNUSED *> src  : App.ArgSource;
                              value: TEXT;
                              log  : App.Log        ) RAISES {App.Error} =
  BEGIN
    CASE VAL(self.id, Arg) OF
    | Arg.ProxyServer =>
        IF Text.Length(value) > 0 THEN proxy.add(value, log); END;
    END;
  END SetArg;

BEGIN
  EVAL NEW(ArgHandler, id := ORD(Arg.ProxyServer),
           paramName := "pattern ProxyServer:port|DIRECT",
           default := "").init(
           switchName := "proxy", envName := "HTTP_PROXY",
           configName := "proxy");
END HTTPApp.