about summary refs log blame commit diff
path: root/web/bubblegum/default.nix
blob: 81e41cfbdfbdc8c263c70c9318e0eb3af5edaf51 (plain) (tree)




























































































































































































































                                                                                     
{ depot, lib, pkgs, ... }:

let

  inherit (depot.nix.yants)
    defun
    restrict
    struct
    string
    int
    attrs
    enum
    ;

  inherit (depot.nix)
    runExecline
    getBins
    ;

  headers = attrs string;

  statusCodes = {
    # 1xx
    "Continue" = 100;
    "Switching Protocols" = 101;
    "Processing" = 102;
    "Early Hints" = 103;
    # 2xx
    "OK" = 200;
    "Created" = 201;
    "Accepted" = 202;
    "Non-Authoritative Information" = 203;
    "No Content" = 204;
    "Reset Content" = 205;
    "Partial Content" = 206;
    "Multi Status" = 207;
    "Already Reported" = 208;
    "IM Used" = 226;
    # 3xx
    "Multiple Choices" = 300;
    "Moved Permanently" = 301;
    "Found" = 302;
    "See Other" = 303;
    "Not Modified" = 304;
    "Use Proxy" = 305;
    "Switch Proxy" = 306;
    "Temporary Redirect" = 307;
    "Permanent Redirect" = 308;
    # 4xx
    "Bad Request" = 400;
    "Unauthorized" = 401;
    "Payment Required" = 402;
    "Forbidden" = 403;
    "Not Found" = 404;
    "Method Not Allowed" = 405;
    "Not Acceptable" = 406;
    "Proxy Authentication Required" = 407;
    "Request Timeout" = 408;
    "Conflict" = 409;
    "Gone" = 410;
    "Length Required" = 411;
    "Precondition Failed" = 412;
    "Payload Too Large" = 413;
    "URI Too Long" = 414;
    "Unsupported Media Type" = 415;
    "Range Not Satisfiable" = 416;
    "Expectation Failed" = 417;
    "I'm a teapot" = 418;
    "Misdirected Request" = 421;
    "Unprocessable Entity" = 422;
    "Locked" = 423;
    "Failed Dependency" = 424;
    "Too Early" = 425;
    "Upgrade Required" = 426;
    "Precondition Required" = 428;
    "Too Many Requests" = 429;
    "Request Header Fields Too Large" = 431;
    "Unavailable For Legal Reasons" = 451;
    # 5xx
    "Internal Server Error" = 500;
    "Not Implemented" = 501;
    "Bad Gateway" = 502;
    "Service Unavailable" = 503;
    "Gateway Timeout" = 504;
    "HTTP Version Not Supported" = 505;
    "Variant Also Negotiates" = 506;
    "Insufficient Storage" = 507;
    "Loop Detected" = 508;
    "Not Extended" = 510;
    "Network Authentication Required" = 511;
  };

  status = enum "bubblegum.status"
    (builtins.attrNames statusCodes);

  /* Generate a CGI response. Takes three arguments:

     1. Status of the response as a string which is
        the descriptive name in the protocol, e. g.
        `"OK"`, `"Not Found"` etc.
     2. Attribute set describing extra headers to
        send, keys and values should both be strings.
     3. Response content as a string.

     See the [README](./README.md) for an example.

    Type: Status -> Headers -> Body -> string
  */
  respond = defun [ status headers string string ]
    (s: hs: body:
      let
        code = status.match s statusCodes;
        renderedHeaders = lib.concatStrings
          (lib.mapAttrsToList (n: v: "${n}: ${v}\r\n") hs);
      in
        lib.concatStrings [
          "Status: ${toString code} ${s}\r\n"
          renderedHeaders
          "\r\n"
          body
        ]);

  /* Returns the value of the `SCRIPT_NAME` environment
     variable used by CGI.
  */
  scriptName = builtins.getEnv "SCRIPT_NAME";

  /* Returns the value of the `PATH_INFO` environment
     variable used by CGI. All cases that could be
     considered as the CGI script's root (i. e.
     `PATH_INFO` is empty or `/`) is mapped to `"/"`
     for convenience.
  */
  pathInfo =
    let
      p = builtins.getEnv "PATH_INFO";
    in
      if builtins.stringLength p == 0
      then "/"
      else p;

  /* Helper function which converts a path from the
     root of the CGI script (i. e. something which
     could be the content of `PATH_INFO`) to an
     absolute path from the web root by also
     utilizing `scriptName`.

     Type: string -> string
  */
  absolutePath = defun [ string string ]
    (path:
      if builtins.substring 0 1 path == "/"
      then "${scriptName}${path}"
      else "${scriptName}/${path}");

  bins = getBins pkgs.coreutils [ "env" "tee" "cat" "printf" "chmod" ]
      // getBins depot.users.sterni.nint [ "nint" ];

  /* Type: args -> either path derivation string -> derivation
  */
  writeCGI =
    { # if given sets the `PATH` to search for `nix-instantiate`
      # Useful when using for example thttpd which unsets `PATH`
      # in the CGI environment.
      binPath ? ""
      # name of the resulting derivation. Defaults to `baseNameOf`
      # the input path or name of the input derivation.
      # Must be given if the input is a string.
    , name ? null
    }:
    input: let
      drvName =
        if name != null
        then name
        else if builtins.isPath input
        then baseNameOf input
        else if lib.isDerivation input
        then input.name
        else builtins.throw "Need name";
      script =
        if builtins.isPath input || lib.isDerivation input
        then input
        else if builtins.isString input
        then pkgs.writeText "${drvName}-source" input
        else builtins.throw "Unsupported input: ${lib.generators.toPretty {} input}";
      shebang = lib.concatStringsSep " " ([
        "#!${bins.env}"
        # use the slightly cursed /usr/bin/env -S which allows us
        # to pass any number of arguments to our interpreter
        # instead of maximum one using plain shebang which considers
        # everything after the first space as the second argument.
        "-S"
      ] ++ lib.optionals (builtins.stringLength binPath > 0) [
        "PATH=${binPath}"
      ] ++ [
        "${bins.nint}"
        # always pass depot so scripts can use this library
        "--arg depot '(import ${depot.depotPath} {})'"
      ]);
    in runExecline.local drvName {} [
      "importas" "out" "out"
      "pipeline" [
        "foreground" [
          "if" [ bins.printf "%s\n" shebang ]
        ]
        "if" [ bins.cat script ]
      ]
      "if" [ bins.tee "$out" ]
      "if" [ bins.chmod "+x" "$out" ]
      "exit" "0"
    ];

in {
  inherit
    respond
    pathInfo
    scriptName
    absolutePath
    writeCGI
    ;
}