diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index a89c46152ca..c411ecb6dcb 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -95,6 +95,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [jellyseerr](https://github.com/Fallenbagel/jellyseerr), a web-based requests manager for Jellyfin, forked from Overseerr. Available as [services.jellyseerr](#opt-services.jellyseerr.enable). +- [stargazer](https://sr.ht/~zethra/stargazer/), a fast and easy to use Gemini server. Available as [services.stargazer](#opt-services.stargazer.enable). + - [photoprism](https://photoprism.app/), a AI-Powered Photos App for the Decentralized Web. Available as [services.photoprism](options.html#opt-services.photoprism.enable). - [peroxide](https://github.com/ljanyst/peroxide), a fork of the official [ProtonMail bridge](https://github.com/ProtonMail/proton-bridge) that aims to be similar to [Hydroxide](https://github.com/emersion/hydroxide). Available as [services.peroxide](#opt-services.peroxide.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f1c459f7557..bbbe8682fd0 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1243,6 +1243,7 @@ ./services/web-servers/nginx/gitweb.nix ./services/web-servers/phpfpm/default.nix ./services/web-servers/pomerium.nix + ./services/web-servers/stargazer.nix ./services/web-servers/tomcat.nix ./services/web-servers/traefik.nix ./services/web-servers/trafficserver/default.nix diff --git a/nixos/modules/services/web-servers/stargazer.nix b/nixos/modules/services/web-servers/stargazer.nix new file mode 100644 index 00000000000..85783a500d6 --- /dev/null +++ b/nixos/modules/services/web-servers/stargazer.nix @@ -0,0 +1,198 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.stargazer; + routesFormat = pkgs.formats.ini { }; + globalFile = pkgs.writeText "global.ini" '' + listen = ${concatStringsSep " " cfg.listen} + connection-logging = ${boolToString cfg.connectionLogging} + log-ip = ${boolToString cfg.ipLog} + log-ip-partial = ${boolToString cfg.ipLogPartial} + request-timeout = ${toString cfg.requestTimeout} + response-timeout = ${toString cfg.responseTimeout} + + [:tls] + store = ${toString cfg.store} + organization = ${cfg.certOrg} + gen-certs = ${boolToString cfg.genCerts} + regen-certs = ${boolToString cfg.regenCerts} + ${optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"} + + ''; + routesFile = routesFormat.generate "router.ini" cfg.routes; + configFile = pkgs.runCommand "config.ini" { } '' + cat ${globalFile} ${routesFile} > $out + ''; +in +{ + options.services.stargazer = { + enable = mkEnableOption (lib.mdDoc "Stargazer Gemini server"); + + listen = lib.mkOption { + type = types.listOf types.str; + default = [ "0.0.0.0" ] ++ optional config.networking.enableIPv6 "[::0]"; + defaultText = literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"''; + example = literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]''; + description = lib.mdDoc '' + Address and port to listen on. + ''; + }; + + connectionLogging = lib.mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Whether or not to log connections to stdout."; + }; + + ipLog = lib.mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Log client IP addresses in the connection log."; + }; + + ipLogPartial = lib.mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Log partial client IP addresses in the connection log."; + }; + + requestTimeout = lib.mkOption { + type = types.int; + default = 5; + description = lib.mdDoc '' + Number of seconds to wait for the client to send a complete + request. Set to 0 to disable. + ''; + }; + + responseTimeout = lib.mkOption { + type = types.int; + default = 0; + description = lib.mdDoc '' + Number of seconds to wait for the client to send a complete + request and for stargazer to finish sending the response. + Set to 0 to disable. + ''; + }; + + store = lib.mkOption { + type = types.path; + default = /var/lib/gemini/certs; + description = lib.mdDoc '' + Path to the certificate store on disk. This should be a + persistent directory writable by Stargazer. + ''; + }; + + certOrg = lib.mkOption { + type = types.str; + default = "stargazer"; + description = lib.mdDoc '' + The name of the organization responsible for the X.509 + certificate's /O name. + ''; + }; + + genCerts = lib.mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Set to false to disable automatic certificate generation. + Use if you want to provide your own certs. + ''; + }; + + regenCerts = lib.mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Set to false to turn off automatic regeneration of expired certificates. + Use if you want to provide your own certs. + ''; + }; + + certLifetime = lib.mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + How long certs generated by Stargazer should live for. + Certs live forever by default. + ''; + example = literalExpression "\"1y\""; + }; + + routes = lib.mkOption { + type = routesFormat.type; + default = { }; + description = lib.mdDoc '' + Routes that Stargazer should server. + + [Refer to upstream docs](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt) + ''; + example = literalExpression '' + { + "example.com" = { + root = "/srv/gemini/example.com"; + }; + "example.com:/man" = { + root = "/cgi-bin"; + cgi = true; + }; + "other.org~(.*)" = { + redirect = "gemini://example.com"; + rewrite = "\1"; + }; + } + ''; + }; + + user = mkOption { + type = types.str; + default = "stargazer"; + description = lib.mdDoc "User account under which stargazer runs."; + }; + + group = mkOption { + type = types.str; + default = "stargazer"; + description = lib.mdDoc "Group account under which stargazer runs."; + }; + }; + + config = mkIf cfg.enable { + systemd.services.stargazer = { + description = "Stargazer gemini server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile}"; + Restart = "always"; + # User and group + User = cfg.user; + Group = cfg.group; + }; + }; + + # Create default cert store + system.activationScripts.makeStargazerCertDir = + optionalAttrs (cfg.store == /var/lib/gemini/certs) '' + mkdir -p /var/lib/gemini/certs + chown -R ${cfg.user}:${cfg.group} /var/lib/gemini/certs + ''; + + users.users = optionalAttrs (cfg.user == "stargazer") { + stargazer = { + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = optionalAttrs (cfg.group == "stargazer") { + stargazer = { }; + }; + }; + + meta.maintainers = with lib.maintainers; [ gaykitty ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 715fe7e51e0..dace17d7670 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -645,6 +645,7 @@ in { sslh = handleTest ./sslh.nix {}; sssd = handleTestOn ["x86_64-linux"] ./sssd.nix {}; sssd-ldap = handleTestOn ["x86_64-linux"] ./sssd-ldap.nix {}; + stargazer = runTest ./web-servers/stargazer.nix; starship = handleTest ./starship.nix {}; step-ca = handleTestOn ["x86_64-linux"] ./step-ca.nix {}; stratis = handleTest ./stratis {}; diff --git a/nixos/tests/web-servers/stargazer.nix b/nixos/tests/web-servers/stargazer.nix new file mode 100644 index 00000000000..6e720b120d1 --- /dev/null +++ b/nixos/tests/web-servers/stargazer.nix @@ -0,0 +1,30 @@ +{ pkgs, lib, ... }: +{ + name = "stargazer"; + meta = with lib.maintainers; { maintainers = [ gaykitty ]; }; + + nodes = { + geminiserver = { pkgs, ... }: { + services.stargazer = { + enable = true; + routes = { + "localhost" = { + root = toString (pkgs.writeTextDir "index.gmi" '' + # Hello NixOS! + ''); + }; + }; + }; + }; + }; + + testScript = { nodes, ... }: '' + geminiserver.wait_for_unit("stargazer") + geminiserver.wait_for_open_port(1965) + + with subtest("check is serving over gemini"): + response = geminiserver.succeed("${pkgs.gmni}/bin/gmni -j once -i -N gemini://localhost:1965") + print(response) + assert "Hello NixOS!" in response + ''; +}