{ http.a68 - HTTP support

  Copyright (C) 2026 Jose E. Marchesi

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program 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 General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see
  <https://www.gnu.org/licenses/>. }

module Http = access Utils
def
    { Routine to call-back in case of error. }

    pub proc (string)void http_error := skip;

    { Perform a GET request to a web server.

      SOCKET is an open file descriptor that communicates with the web
      server.

      URI is a string that identifies the accessed resource, such as
      for example "/api".

      HEADERS is a multiple of string tuples with a collection of
      headers, some of which characterize the request.

      RESPONSE is a callback procedure that will get called repeteadly
      as the response of the GET request is sent back by the server.
      The arguments taken by the callback are N, E, S, where N is the
      number of bytes available for reading from the socket S (without
      blocking) and N is either the total number of characters that
      still have to be received in future calls, or -1 if this number
      is not known.  The last call to RESPONSE will always pass N
      equal to zero.

      If FRAGMENTED is false, then the request response will be
      notified to the caller via a single call to the callback
      procedure H. }

    pub proc http_get = (int socket, string uri,
                         []struct (string n, v) headers,
                         proc(int,int,int)void response,
                         bool fragmented) void:
    begin { Prepare the header to send. }
          string msg := "GET " + uri + " HTTP/1.1'r'n";
          for i to UPB headers
          do msg +:= (n of headers[i]) + ": " + (v of headers[i]);
             msg +:= "'r'n"
          od;
          msg +:= "'r'n";
          { Send it. }
          fputs(socket, msg);
          { Collect answer from server. }
          http_process_response(socket, response, fragmented)
    end;

    { Perform a POST request to a web server.

      SOCKET is an open file descriptor that communicates with the web
      server.

      URI is a string that identifies the accessed resource, such as
      for example "/api".

      HEADERS is a multiple of string tuples with a collection of
      headers, some of which characterize the request.

      CONTENTS is either a string or a callback.  If it is a string,
      then it gets sent to the server in one go.  If it is a callback
      then it will be invoked repeatedly until the boolean referred by
      its argument gets set to "false".  Each string yielded by the
      callback gets sent, as a chunk, to the server.  A callback is
      the appropriate interface to use when posting large data, like
      for example the contents of files.

      RESPONSE is a callback procedure that will get called repeteadly
      as the response of the GET request is sent back by the server.
      The arguments taken by the callback are N, E, S, where N is the
      number of bytes available for reading from the socket S (without
      blocking) and N is either the total number of characters that
      still have to be received in future calls, or -1 if this number
      is not known.  The last call to RESPONSE will always pass N
      equal to zero.

      If FRAGMENTED is false, then the request response will be
      notified to the caller via a single call to the callback
      procedure H. }

    pub proc http_post = (int socket, string uri,
                          []struct (string n, v) headers,
                          union(string,proc(ref bool)string) contents,
                          proc(int,int,int)void response,
                          bool fragmented) void:
    begin
          { Mount the HTTP header from the user-provided headers. }
          string hdr := "POST " + uri + " HTTP/1.1'r'n";
          for i to UPB headers
          do hdr +:= (n of headers[i]) + ": " + (v of headers[i]);
             hdr +:= "'r'n"
          od;
          { Let server know how contents will be transmitted. }
          hdr +:= ((contents
                    | (string s): "Content-Length: " + itoa(UPB s, 10)
                    | "Transfer-Encoding: chunked")
                   + "'r'n");
          { Send the header and the contents to the server. }
          fputs(socket, hdr);
          fputs(socket, "'r'n");
          case contents
          in (string s): fputs (socket, s),
             (proc(ref bool)string cb):
                begin while bool done;
                            string some_content = cb (done);
                            NOT done
                      do { Send chunk. }
                         fputs(socket, itoa(UPB some_content, 16));
                         fputs(socket, "'r'n");
                         fputs(socket, some_content);
                         fputs(socket, "'r'n")
                      od;
                      { Send last, empty chunk and trailer empty line. }
                      fputs(socket, "0'r'n");
                      fputs(socket, "'r'n")
                end
          esac;
          { Collect answer from server. }
          http_process_response(socket, response, fragmented)
    end;

    { Collect data from an HTTP server in SOCKET once a GET or POST
      request has been sent to the server, parse its header and call
      back to H so it will process the whole response.  FRAGMENTED
      determines whether the response is handled with several calls to
      H, or just one call. }

    pub proc http_process_response = (int socket,
                                      proc(int,int,int)void h,
                                      bool fragmented) void:
    begin
          { First the header. }
          int content_length := 0, bool read_chunks := false;
          while string line := fgets (socket, 0);
                line /= "'r'n"
          do if UPB line >= 17 andth line[1:16] = "Content-Length: "
             then content_length := atoi(line[17:], 10)
             elif UPB line >= 26 andth line[1:26] = "Transfer-Encoding: chunked"
             then read_chunks := true
             fi
          od;
          { Then the data. }
          if read_chunks
          then while string line = fgets(socket, 0);
                     int size = atoi(line, 16);
                     h((size = 0 | 0 | -1), size, socket);
                     fgetc(socket); fgetc(socket); { 'r'n }
                     size /= 0
               do ~ od;
               h(0, 0, socket)
          else if content_length = 0
               then http_error("couldn''t find Content-Length header in response") fi;
               if fragmented
               then { Call the handler to process the data in chunks.
                          Eighty is arbitrary, roughly a line size. }
                    for i to content_length % 80
                    do h(content_length - i * 80, 80, socket) od;
                    h(0, content_length %* 80, socket)
               else { Call the handler to process the data in one go. }
                    h(0, content_length, socket)
               fi
          fi
    end;

    { Return a string with the given string URL encoded in HTTP URL
      encoding. }

    pub proc http_escape_url = (string url) string:
    begin
          string res, char sharp = REPR 35;
          for i to UPB url
          do char c = url[i];
             (c = " " | res +:= "%20"
              |: c = "<" | res +:= "%3c"
              |: c = ">" | res +:= "%3e"
              |: c = sharp | res +:= "%23"
              |: c = "%" | res +:= "%25"
              |: c = "+" | res +:= "%2b"
              |: c = "{" | res +:= "%7b"
              |: c = "}" | res +:= "%7d"
              |: c = "|" | res +:= "%7c"
              |: c = "\" | res +:= "%5c"
              |: c = "^" | res +:= "%5e"
              |: c = "~" | res +:= "%7e"
              |: c = "[" | res +:= "%5b"
              |: c = "]" | res +:= "%5d"
              |: c = "''" | res +:= "%60"
              |: c = ";" | res +:= "%3b"
              |: c = "/" | res +:= "%2f"
              |: c = "?" | res +:= "%3f"
              |: c = ":" | res +:= "%3a"
              |: c = "@" | res +:= "%40"
              |: c = "=" | res +:= "%3d"
              |: c = "&" | res +:= "%26"
              |: c = "$" | res +:= "%24"
              | res +:= c)
          od;
          res
    end;

    skip
fed
