gophernicus: init at 3.1.1 with nixos module (#443747)

This commit is contained in:
h7x4
2026-09-26 21:18:31 +00:00
committed by GitHub
6 changed files with 474 additions and 0 deletions

View File

@@ -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).

View File

@@ -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

View File

@@ -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
];
}

View File

@@ -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;

117
nixos/tests/gophernicus.nix Normal file
View File

@@ -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}
<?php echo "Result: " . (1 + 1); ?>
'';
bob-filtered = pkgs.writeText "gophernicus-test-bob-filtered" ''
hello
'';
bob-php-non-cgi = pkgs.writeText "gophernicus-test-bob-php-non-cgi" ''
<?php echo "Result: " . (1 + 1); ?>
'';
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"
'';
}

View File

@@ -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"}<string>-h@HOSTNAME@</string>${"\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";
};
})