Unverified Commit cc594f99 authored by lassulus's avatar lassulus Committed by GitHub
Browse files

nixos/h2o: module init (#382527)

parents 51236d99 2bcb6960
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -1608,6 +1608,7 @@
  ./services/web-servers/darkhttpd.nix
  ./services/web-servers/fcgiwrap.nix
  ./services/web-servers/garage.nix
  ./services/web-servers/h2o/default.nix
  ./services/web-servers/hitch/default.nix
  ./services/web-servers/jboss/default.nix
  ./services/web-servers/keter
+263 −0
Original line number Diff line number Diff line
{
  config,
  lib,
  pkgs,
  ...
}:

# TODO: ACME
# TODO: Gems includes for Mruby
# TODO: Recommended options
let
  cfg = config.services.h2o;

  inherit (lib)
    literalExpression
    mkDefault
    mkEnableOption
    mkIf
    mkOption
    types
    ;

  settingsFormat = pkgs.formats.yaml { };

  hostsConfig = lib.concatMapAttrs (
    name: value:
    let
      port = {
        HTTP = lib.attrByPath [ "http" "port" ] cfg.defaultHTTPListenPort value;
        TLS = lib.attrByPath [ "tls" "port" ] cfg.defaultTLSListenPort value;
      };
      serverName = if value.serverName != null then value.serverName else name;
    in
    # HTTP settings
    lib.optionalAttrs (value.tls == null || value.tls.policy == "add") {
      "${serverName}:${builtins.toString port.HTTP}" = value.settings // {
        listen.port = port.HTTP;
      };
    }
    # Redirect settings
    // lib.optionalAttrs (value.tls != null && value.tls.policy == "force") {
      "${serverName}:${builtins.toString port.HTTP}" = {
        listen.port = port.HTTP;
        paths."/" = {
          redirect = {
            status = value.tls.redirectCode;
            url = "https://${serverName}:${builtins.toString port.TLS}";
          };
        };
      };
    }
    # TLS settings
    //
      lib.optionalAttrs
        (
          value.tls != null
          && builtins.elem value.tls.policy [
            "add"
            "only"
            "force"
          ]
        )
        {
          "${serverName}:${builtins.toString port.TLS}" = value.settings // {
            listen =
              let
                identity = value.tls.identity;
              in
              {
                port = port.TLS;
                ssl = value.tls.extraSettings or { } // {
                  inherit identity;
                };
              };
          };
        }
  ) cfg.hosts;

  h2oConfig = settingsFormat.generate "h2o.yaml" (
    lib.recursiveUpdate { hosts = hostsConfig; } cfg.settings
  );
in
{
  options = {
    services.h2o = {
      enable = mkEnableOption "H2O web server";

      user = mkOption {
        type = types.nonEmptyStr;
        default = "h2o";
        description = "User running H2O service";
      };

      group = mkOption {
        type = types.nonEmptyStr;
        default = "h2o";
        description = "Group running H2O services";
      };

      package = lib.mkPackageOption pkgs "h2o" {
        example = ''
          pkgs.h2o.override {
            withMruby = true;
          };
        '';
      };

      defaultHTTPListenPort = mkOption {
        type = types.port;
        default = 80;
        description = ''
          If hosts do not specify listen.port, use these ports for HTTP by default.
        '';
        example = 8080;
      };

      defaultTLSListenPort = mkOption {
        type = types.port;
        default = 443;
        description = ''
          If hosts do not specify listen.port, use these ports for SSL by default.
        '';
        example = 8443;
      };

      mode = mkOption {
        type =
          with types;
          nullOr (enum [
            "daemon"
            "master"
            "worker"
            "test"
          ]);
        default = "master";
        description = "Operating mode of H2O";
      };

      settings = mkOption {
        type = settingsFormat.type;
        description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
      };

      hosts = mkOption {
        type = types.attrsOf (
          types.submodule (
            import ./vhost-options.nix {
              inherit config lib;
            }
          )
        );
        default = { };
        description = ''
          The `hosts` config to be merged with the settings.

          Note that unlike YAML used for H2O, Nix will not support duplicate
          keys to, for instance, have multiple listens in a host block; use the
          virtual host options in like `http` & `tls` or use `$HOST:$PORT`
          keys if manually specifying config.
        '';
        example =
          literalExpression
            # nix
            ''
              {
                "hydra.example.com" = {
                  tls = {
                    policy = "force";
                    indentity = [
                      {
                        key-file = "/path/to/key";
                        certificate-file = "/path/to/cert";
                      };
                    ];
                    extraSettings = {
                      minimum-version = "TLSv1.3";
                    };
                  };
                  settings = {
                    paths."/" = {
                      "file:dir" = "/var/www/default";
                    };
                  };
                };
              }
            '';
      };
    };
  };

  config = mkIf cfg.enable {
    users = {
      users.${cfg.user} =
        {
          group = cfg.group;
        }
        // lib.optionalAttrs (cfg.user == "h2o") {
          isSystemUser = true;
        };
      groups.${cfg.group} = { };
    };

    systemd.services.h2o = {
      description = "H2O web server service";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];

      serviceConfig = {
        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
        ExecStop = "${pkgs.coreutils}/bin/kill -s QUIT $MAINPID";
        User = cfg.user;
        Restart = "always";
        RestartSec = "10s";
        RuntimeDirectory = "h2o";
        RuntimeDirectoryMode = "0750";
        CacheDirectory = "h2o";
        CacheDirectoryMode = "0750";
        LogsDirectory = "h2o";
        LogsDirectoryMode = "0750";
        ProtectSystem = "strict";
        ProtectHome = mkDefault true;
        PrivateTmp = true;
        PrivateDevices = true;
        ProtectHostname = true;
        ProtectClock = true;
        ProtectKernelTunables = true;
        ProtectKernelModules = true;
        ProtectKernelLogs = true;
        ProtectControlGroups = true;
        RestrictAddressFamilies = [
          "AF_UNIX"
          "AF_INET"
          "AF_INET6"
        ];
        RestrictNamespaces = true;
        LockPersonality = true;
        RestrictRealtime = true;
        RestrictSUIDSGID = true;
        RemoveIPC = true;
        PrivateMounts = true;
        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
        CapabilitiesBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
      };

      script =
        let
          args =
            [
              "--conf"
              "${h2oConfig}"
            ]
            ++ lib.optionals (cfg.mode != null) [
              "--mode"
              cfg.mode
            ];
        in
        ''
          ${lib.getExe cfg.package} ${lib.strings.escapeShellArgs args}
        '';
    };
  };

}
+151 −0
Original line number Diff line number Diff line
{ config, lib, ... }:

let
  inherit (lib)
    literalExpression
    mkOption
    types
    ;
in
{
  options = {
    serverName = mkOption {
      type = types.nullOr types.nonEmptyStr;
      default = null;
      description = ''
        Server name to be used for this virtual host. Defaults to attribute
        name in hosts.
      '';
      example = "example.org";
    };

    http = mkOption {
      type = types.nullOr (
        types.submodule {
          options = {
            port = mkOption {
              type = types.port;
              default = config.services.h2o.defaultHTTPListenPort;
              defaultText = literalExpression ''
                config.services.h2o.defaultHTTPListenPort
              '';
              description = ''
                Override the default HTTP port for this virtual host.
              '';
              example = literalExpression "8080";
            };
          };
        }
      );
      default = null;
      description = "HTTP options for virtual host";
    };

    tls = mkOption {
      type = types.nullOr (
        types.submodule {
          options = {
            port = mkOption {
              type = types.port;
              default = config.services.h2o.defaultTLSListenPort;
              defaultText = literalExpression ''
                config.services.h2o.defaultTLSListenPort
              '';
              description = ''
                Override the default TLS port for this virtual host.";
              '';
              example = 8443;
            };
            policy = mkOption {
              type = types.enum [
                "add"
                "only"
                "force"
              ];
              description = ''
                `add` will additionally listen for TLS connections. `only` will
                disable   TLS connections. `force` will redirect non-TLS traffic
                to the TLS connection.
              '';
              example = "force";
            };
            redirectCode = mkOption {
              type = types.ints.between 300 399;
              default = 301;
              example = 308;
              description = ''
                HTTP status used by `globalRedirect` & `forceSSL`. Possible
                usecases include temporary (302, 307) redirects, keeping the
                request method & body (307, 308), or explicitly resetting the
                method to GET (303). See
                <https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections>.
              '';
            };
            identity = mkOption {
              type = types.nonEmptyListOf (
                types.submodule {
                  options = {
                    key-file = mkOption {
                      type = types.path;
                      description = "Path to key file";
                    };
                    certificate-file = mkOption {
                      type = types.path;
                      description = "Path to certificate file";
                    };
                  };
                }
              );
              default = null;
              description = ''
                Key / certificate pairs for the virtual host.
              '';
              example =
                literalExpression
                  # nix
                  ''
                    {
                      indentities = [
                        {
                          key-file = "/path/to/rsa.key";
                          certificate-file = "/path/to/rsa.crt";
                        }
                        {
                          key-file = "/path/to/ecdsa.key";
                          certificate-file = "/path/to/ecdsa.crt";
                        }
                      ];
                    }
                  '';
            };
            extraSettings = mkOption {
              type = types.nullOr types.attrs;
              default = null;
              description = ''
                Additional TLS/SSL-related configuration options.
              '';
              example =
                literalExpression
                  # nix
                  ''
                    {
                      minimum-version = "TLSv1.3";
                    }
                  '';
            };
          };
        }
      );
      default = null;
      description = "TLS options for virtual host";
    };

    settings = mkOption {
      type = types.attrs;
      description = ''
        Attrset to be transformed into YAML for host config. Note that the HTTP
        / TLS configurations will override these config values.
      '';
    };
  };
}
+1 −0
Original line number Diff line number Diff line
@@ -420,6 +420,7 @@ in {
  guacamole-server = handleTest ./guacamole-server.nix {};
  guix = handleTest ./guix {};
  gvisor = handleTest ./gvisor.nix {};
  h2o = discoverTests (import ./web-servers/h2o { inherit handleTestOn; });
  hadoop = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop; };
  hadoop_3_3 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop_3_3; };
  hadoop2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop2; };
+138 −0
Original line number Diff line number Diff line
import ../../make-test-python.nix (
  { lib, pkgs, ... }:

  # Tests basics such as TLS, creating a mime-type & serving Unicode characters.

  let
    domain = {
      HTTP = "h2o.local";
      TLS = "acme.test";
    };

    port = {
      HTTP = 8080;
      TLS = 8443;
    };

    sawatdi_chao_lok = "สวัสดีชาวโลก";

    hello_world_txt = pkgs.writeTextFile {
      name = "/hello_world.txt";
      text = sawatdi_chao_lok;
    };

    hello_world_rst = pkgs.writeTextFile {
      name = "/hello_world.rst";
      text = # rst
        ''
          ====================
          Thaiger Sprint 2025‼
          ====================

          ${sawatdi_chao_lok}
        '';
    };
  in
  {
    name = "h2o-basic";

    meta = {
      maintainers = with lib.maintainers; [ toastal ];
    };

    nodes = {
      server =
        { pkgs, ... }:
        {
          services.h2o = {
            enable = true;
            defaultHTTPListenPort = port.HTTP;
            defaultTLSListenPort = port.TLS;
            hosts = {
              "${domain.HTTP}" = {
                settings = {
                  paths = {
                    "/hello_world.txt" = {
                      "file.file" = "${hello_world_txt}";
                    };
                  };
                };
              };
              "${domain.TLS}" = {
                tls = {
                  policy = "force";
                  identity = [
                    {
                      key-file = ../../common/acme/server/acme.test.key.pem;
                      certificate-file = ../../common/acme/server/acme.test.cert.pem;
                    }
                  ];
                  extraSettings = {
                    minimum-version = "TLSv1.3";
                  };
                };
                settings = {
                  paths = {
                    "/hello_world.rst" = {
                      "file.file" = "${hello_world_rst}";
                    };
                  };
                };
              };
            };
            settings = {
              compress = "ON";
              compress-minimum-size = 32;
              "file.mime.addtypes" = {
                "text/x-rst" = {
                  extensions = [ ".rst" ];
                  is_compressible = "YES";
                };
              };
              ssl-offload = "kernel";
            };
          };

          security.pki.certificates = [
            (builtins.readFile ../../common/acme/server/ca.cert.pem)
          ];

          networking = {
            firewall.allowedTCPPorts = with port; [
              HTTP
              TLS
            ];
            extraHosts = ''
              127.0.0.1 ${domain.HTTP}
              127.0.0.1 ${domain.TLS}
            '';
          };
        };
    };

    testScript = # python
      ''
        server.wait_for_unit("h2o.service")

        http_hello_world_body = server.succeed("curl --fail-with-body 'http://${domain.HTTP}:${builtins.toString port.HTTP}/hello_world.txt'")
        assert "${sawatdi_chao_lok}" in http_hello_world_body

        tls_hello_world_head = server.succeed("curl -v --head --compressed --http2 --tlsv1.3 --fail-with-body 'https://${domain.TLS}:${builtins.toString port.TLS}/hello_world.rst'").lower()
        print(tls_hello_world_head)
        assert "http/2 200" in tls_hello_world_head
        assert "server: h2o" in tls_hello_world_head
        assert "content-type: text/x-rst" in tls_hello_world_head

        tls_hello_world_body = server.succeed("curl -v --http2 --tlsv1.3 --compressed --fail-with-body 'https://${domain.TLS}:${builtins.toString port.TLS}/hello_world.rst'")
        assert "${sawatdi_chao_lok}" in tls_hello_world_body

        tls_hello_world_head_redirected = server.succeed("curl -v --head --fail-with-body 'http://${domain.TLS}:${builtins.toString port.HTTP}/hello_world.rst'").lower()
        assert "redirected" in tls_hello_world_head_redirected

        server.fail("curl --location --max-redirs 0 'http://${domain.TLS}:${builtins.toString port.HTTP}/hello_world.rst'")

        tls_hello_world_body_redirected = server.succeed("curl -v --location --fail-with-body 'http://${domain.TLS}:${builtins.toString port.HTTP}/hello_world.rst'")
        assert "${sawatdi_chao_lok}" in tls_hello_world_body_redirected
      '';
  }
)
Loading