diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 7bfb43ae3aa1..d8f5a489908f 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -98,6 +98,8 @@ - [gocron](https://github.com/flohoss/gocron), a task scheduler with web interface. Available as [services.gocron](#opt-services.gocron.enable). +- [gophernicus](https://github.com/gophernicus/gophernicus), a modern gopher daemon. Available at [services.gophernicus](#opt-services.gophernicus.enable). + - [Unpackerr](https://unpackerr.zip), extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch folder. Available as [services.unpackerr](#opt-services.unpackerr.enable). - [ioquake3](https://ioquake3.org), a open-source port of the 3D action shooter Quake 3 Arena. Available as [programs.ioquake3](#opt-programs.ioquake3.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index aa64e6e20686..548c1da4234e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -912,6 +912,7 @@ ./services/misc/gitolite.nix ./services/misc/gitweb.nix ./services/misc/gollum.nix + ./services/misc/gophernicus.nix ./services/misc/gotenberg.nix ./services/misc/gpsd.nix ./services/misc/graphical-desktop.nix diff --git a/nixos/modules/services/misc/gophernicus.nix b/nixos/modules/services/misc/gophernicus.nix new file mode 100644 index 000000000000..c3f83228d641 --- /dev/null +++ b/nixos/modules/services/misc/gophernicus.nix @@ -0,0 +1,229 @@ +{ + config, + options, + pkgs, + lib, + ... +}: +let + cfg = config.services.gophernicus; + opt = options.services.gophernicus; +in +{ + options.services.gophernicus = { + enable = lib.mkEnableOption "gophernicus, a modern gopher daemon"; + package = lib.mkPackageOption pkgs "gophernicus" { }; + + domain = lib.mkOption { + type = lib.types.str; + description = "A DNS-resolvable domain pointing to this machine or a reverse proxy redirecting here."; + example = "gopher.example.com"; + }; + + path = lib.mkOption { + type = lib.types.listOf lib.types.package; + description = '' + Packages with content in {file}`/bin` that should be available to gophernicus. + + This will be provided as {env}`PATH` to both gophermaps, cgi scripts and filters. + ''; + default = with pkgs; [ + coreutils + gnused + whoami + bash + php + perl + ]; + defaultText = lib.literalExpression '' + with pkgs; [ + coreutils + gnused + whoami + bash + php + perl + ] + ''; + }; + + filters = lib.mkOption { + type = lib.types.attrsOf lib.types.pathInStore; + description = "Executables that should be registered as preprocessing filters."; + default = { }; + example = lib.literalExpression '' + { + # Files with the `.txt` extension should be preprocessed with `my-filter` + txt = pkgs.writeShellScript "my-filter" ''' + echo "To whom it may concern..." + # First argument is the path of the file being filtered + cat "$1" + echo "Best regards, server owner" + '''; + # Files with the `.php` extension should automatically be rendered by php + php = "''${pkgs.php}/bin/php"; + } + ''; + }; + + rootDir = lib.mkOption { + type = lib.types.path; + description = '' + The directory which contains the root of your gopher content. + + Note that you might want to disable {option}`services.gophernicus.enableDefaultRootIndex` if you are + modifying the contents of your root directory. + ''; + default = "/var/lib/gophernicus/gopher"; + example = lib.literalExpression '' + pkgs.symlinkJoin { + name = "gophernicus-root-dir"; + paths = [ + (pkgs.writeTextDir "gophermap" ''' + iHello! + + 1Misc\tmisc + ''') + (pkgs.writeTextDir "misc/info.txt" ''' + World + ''') + ]; + } + ''; + }; + + enableDefaultRootIndex = lib.mkOption { + type = lib.types.bool; + description = "Whether to enable the example gophermap shipped with gophernicus as the default root index."; + default = true; + example = false; + }; + + enableUserDirs = lib.mkOption { + type = lib.types.bool; + description = "Whether to host content from {file}`~/public_gopher` in user home directories."; + default = true; + example = false; + }; + + listenStreams = lib.mkOption { + description = '' + Which sockets to bind to. + + See {option}`systemd.sockets.«name».listenStreams`. + ''; + type = lib.types.listOf lib.types.str; + default = [ "127.0.0.1:70" ]; + example = [ "/run/gophernicus/gophernicus.sock" ]; + }; + + extraArgs = lib.mkOption { + description = '' + Extra commandline arguments to pass to {command}`gophernicus`. + + See {manpage}`gophernicus(8)` for available arguments. + ''; + type = lib.types.attrsOf lib.types.anything; + default = { }; + example = { + u = "gopher_stuff"; + w = 80; + np = true; + nx = true; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.gophernicus.path = opt.path.default; + + systemd.packages = [ cfg.package ]; + + systemd.sockets."gophernicus" = { + wantedBy = [ "sockets.target" ]; + listenStreams = [ "" ] ++ cfg.listenStreams; + }; + + systemd.services."gophernicus@" = { + documentation = [ "man:gophernicus(8)" ]; + serviceConfig = { + DynamicUser = true; + User = "gophernicus"; + Group = "gophernicus"; + + StateDirectory = [ "gophernicus/gopher" ]; + + RuntimeDirectory = [ "gophernicus/bin" ]; + BindReadOnlyPaths = + let + binDir = pkgs.symlinkJoin { + name = "gophernicus-bin"; + paths = cfg.path; + stripPrefix = "/bin"; + }; + in + [ + "${binDir}:/run/gophernicus/bin" + ] + ++ lib.optionals (cfg.rootDir != opt.rootDir.default) [ + "${cfg.rootDir}:/var/lib/gophernicus/gopher" + ] + ++ lib.optionals cfg.enableDefaultRootIndex [ + "${cfg.package}/share/gophernicus/gopher/gophermap:/var/lib/gophernicus/gopher/gophermap" + ]; + + ExecStart = + let + args = lib.cli.toCommandLineShellGNU { } ( + { + r = "/var/lib/gophernicus/gopher"; + h = cfg.domain; + f = if cfg.filters != { } then pkgs.linkFarm "gophernicus-filters" cfg.filters else null; + nu = !cfg.enableUserDirs; + } + // cfg.extraArgs + ); + in + [ + "" + "${lib.getExe cfg.package} ${args}" + ]; + + PrivateUsers = !cfg.enableUserDirs; + ProtectHome = if cfg.enableUserDirs then "read-only" else true; + + ProcSubset = "pid"; + ProtectProc = "invisible"; + UMask = "0077"; + CapabilityBoundingSet = ""; + NoNewPrivileges = true; + ProtectSystem = "strict"; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + LockPersonality = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + PrivateMounts = true; + SystemCallArchitectures = "native"; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ + felixalbrigtsen + h7x4 + ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 538af4363980..fea34f717234 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -793,6 +793,7 @@ in gollum = runTest ./gollum.nix; gonic = runTest ./gonic.nix; google-oslogin = runTest ./google-oslogin; + gophernicus = runTest ./gophernicus.nix; gopro-tool = runTest ./gopro-tool.nix; goss = runTest ./goss.nix; gotenberg = runTest ./gotenberg.nix; diff --git a/nixos/tests/gophernicus.nix b/nixos/tests/gophernicus.nix new file mode 100644 index 000000000000..2b2599c44ec6 --- /dev/null +++ b/nixos/tests/gophernicus.nix @@ -0,0 +1,117 @@ +{ pkgs, lib, ... }: +{ + name = "gophernicus"; + meta.maintainers = with lib.maintainers; [ + felixalbrigtsen + h7x4 + ]; + + containers.machine = + { options, ... }: + { + imports = [ ./common/user-account.nix ]; + + environment.systemPackages = with pkgs; [ + phetch + ]; + + users.users = { + alice.homeMode = "755"; + bob.homeMode = "755"; + }; + + services.gophernicus = { + enable = true; + domain = "localhost"; + + path = options.services.gophernicus.path.default ++ [ + (pkgs.writeScriptBin "hello" ''printf "aaaaaaa\n"'') + ]; + + filters = { + txt = pkgs.writeShellScript "gophernicus-test-filter-script" '' + cat "$1" + echo "world" + ''; + php = "${pkgs.php}/bin/php"; + }; + }; + }; + + testScript = + let + alice-hello = pkgs.writeText "gophernicus-test-alice-hello" '' + Hi! + ''; + bob-hello = pkgs.writeText "gophernicus-test-bob-hello" '' + Hello! + ''; + bob-custom-command = pkgs.writeText "gophernicus-test-bob-custom-command" '' + =hello + ''; + bob-php-cgi = pkgs.writeText "gophernicus-test-bob-php-cgi" '' + #!${lib.getExe pkgs.php} + + ''; + bob-filtered = pkgs.writeText "gophernicus-test-bob-filtered" '' + hello + ''; + bob-php-non-cgi = pkgs.writeText "gophernicus-test-bob-php-non-cgi" '' + + ''; + in + '' + machine.wait_for_unit("sockets.target") + + def phetch_or_fail(path: str) -> str: + result = machine.succeed(f"phetch -r gopher://localhost/{path}") + + print("--- RESPONSE ---") + print(result) + print("----------------") + + assert "Error:" not in result, "Found error in fetched content" + assert "command not found" not in result, "Found missing command error in fetched content" + + return result + + with subtest("Fetch root pages"): + content = phetch_or_fail("") + assert "Welcome to Gophernicus!" in content, "Content not rendered properly" + + content = phetch_or_fail("/server-status") + assert "Total Accesses" in content, "Content not rendered properly" + + content = phetch_or_fail("/caps.txt") + assert "This is an automatically generated caps file." in content, "Content not rendered properly" + + machine.succeed("runuser -u alice -- install -Dm755 -d /home/alice/public_gopher") + machine.succeed("runuser -u bob -- install -Dm755 -d /home/bob/public_gopher") + + with subtest("Fetch user pages"): + machine.succeed("runuser -u alice -- install -Dm644 '${alice-hello}' /home/alice/public_gopher/hello") + machine.succeed("runuser -u bob -- install -Dm644 '${bob-hello}' /home/bob/public_gopher/world") + phetch_or_fail("1~alice/hello") + phetch_or_fail("1~bob/world") + + with subtest("Render gophermap with custom command"): + machine.succeed("runuser -u bob -- install -Dm644 '${bob-custom-command}' /home/bob/public_gopher/custom/gophermap") + content = phetch_or_fail("1~bob/custom") + assert "aaaaaaa" in content, "Content not rendered properly" + + with subtest("Render php as cgi"): + machine.succeed("runuser -u bob -- install -Dm755 '${bob-php-cgi}' /home/bob/public_gopher/cgi-bin/php-cgi.php") + content = phetch_or_fail("1~bob/cgi-bin/php-cgi.php") + assert "Result: 2" in content, "Content not rendered properly" + + with subtest("Render filters"): + machine.succeed("runuser -u bob -- install -Dm644 '${bob-filtered}' /home/bob/public_gopher/filtered.txt") + content = phetch_or_fail("1~bob/filtered.txt") + assert "hello" in content, "Content not rendered properly" + assert "world" in content, "Content not rendered properly" + + machine.succeed("runuser -u bob -- install -Dm644 '${bob-php-non-cgi}' /home/bob/public_gopher/filtered.php") + content = phetch_or_fail("1~bob/filtered.php") + assert "Result: 2" in content, "Content not rendered properly" + ''; +} diff --git a/pkgs/by-name/go/gophernicus/package.nix b/pkgs/by-name/go/gophernicus/package.nix new file mode 100644 index 000000000000..f5fc7a53f9ef --- /dev/null +++ b/pkgs/by-name/go/gophernicus/package.nix @@ -0,0 +1,124 @@ +{ + lib, + stdenv, + buildPackages, + fetchFromGitHub, + gnused, + systemdMinimal, + systemdSupport ? lib.meta.availableOn stdenv.hostPlatform systemdMinimal, + xinetd, + xinetdSupport ? lib.meta.availableOn stdenv.hostPlatform xinetd, + versionCheckHook, + nix-update-script, + nixosTests, +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "gophernicus"; + version = "3.1.1"; + + src = fetchFromGitHub { + owner = "gophernicus"; + repo = "gophernicus"; + tag = finalAttrs.version; + hash = "sha256-pweiUiMmLXiyF9NMxvcWfJPH6JiGRlpT4chJiRGh9vg="; + }; + + postPatch = '' + substituteInPlace README.md \ + --replace-warn 'DEVEL' '${finalAttrs.version}' + + substituteInPlace src/gophernicus.h \ + --replace-fail 'SAFE_PATH "/usr/bin:/bin"' 'SAFE_PATH "/usr/bin:/bin:/run/gophernicus/bin"' + + substituteInPlace init/gophernicus.xinetd.in \ + --replace-fail ' -h@HOSTNAME@' ''' + + substituteInPlace init/gophernicus.env.in \ + --replace-fail '-h @HOSTNAME@ ' ''' + + substituteInPlace init/org.gophernicus.server.plist.in \ + --replace-fail '${"\t\t"}-h@HOSTNAME@${"\n"}' ''' \ + --replace-fail '/usr/local/sbin' '${placeholder "out"}/sbin' + + substituteInPlace init/gophernicus@.service.in \ + --replace-fail 'User=nobody' ''' + + substituteInPlace Makefile.in \ + --replace-fail \ + '$(INSTALL) -s' \ + '$(INSTALL) -s --strip-program=${stdenv.cc.targetPrefix}strip' \ + --replace-fail \ + '$(INSTALL) -m 644 init/$(PLIST) $(DESTDIR)$(LAUNCHD)' \ + '$(INSTALL) -Dm 644 -t $(DESTDIR)$(LAUNCHD) init/$(PLIST)' + + sed -E -i '/^\s+(chown|chmod)/d' Makefile.in + ''; + + configurePlatforms = [ ]; + dontAddStaticConfigureFlags = true; + configureFlags = [ + "--prefix=/" + "--gopherroot=/share/gophernicus/gopher" + "--os=${ + if stdenv.hostPlatform.isDarwin then + "mac" + else if stdenv.hostPlatform.isLinux then + "linux" + else if stdenv.hostPlatform.isBSD then + "freebsd" + else + "autodetected" + }" + ] + ++ lib.optionals systemdSupport [ + "--listener=systemd" + ] + ++ lib.optionals xinetdSupport [ + "--listener=xinetd" + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + "--listener=mac" + ]; + + env = lib.optionalAttrs (!stdenv.buildPlatform.canExecute stdenv.hostPlatform) { + HOSTCC = "${buildPackages.stdenv.cc}/bin/cc"; + CC = "${stdenv.cc.targetPrefix}cc"; + }; + + makeFlags = [ + "DESTDIR=${placeholder "out"}" + ]; + + strictDeps = true; + __structuredAttrs = true; + + installTargets = [ + "install" + ] + ++ lib.optionals systemdSupport [ + "install-systemd" + ]; + + doInstallCheck = true; + nativeInstallCheckInputs = [ versionCheckHook ]; + versionCheckProgramArg = "-v"; + + passthru = { + updateScript = nix-update-script { }; + tests.nixos = nixosTests.gophernicus; + }; + + meta = { + description = "Modern full-featured (and hopefully) secure gopher daemon"; + homepage = "https://gophernicus.org/"; + changelog = "https://github.com/gophernicus/gophernicus/blob/${finalAttrs.src.tag}/changelog"; + license = lib.licenses.bsd2; + platforms = lib.platforms.unix; + maintainers = with lib.maintainers; [ + felixalbrigtsen + h7x4 + ]; + mainProgram = "gophernicus"; + }; +})