1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.spacecookie;
12
13 spacecookieConfig = {
14 listen = {
15 inherit (cfg) port;
16 };
17 } // cfg.settings;
18
19 format = pkgs.formats.json { };
20
21 configFile = format.generate "spacecookie.json" spacecookieConfig;
22
23in
24{
25 imports = [
26 (mkRenamedOptionModule
27 [ "services" "spacecookie" "root" ]
28 [ "services" "spacecookie" "settings" "root" ]
29 )
30 (mkRenamedOptionModule
31 [ "services" "spacecookie" "hostname" ]
32 [ "services" "spacecookie" "settings" "hostname" ]
33 )
34 ];
35
36 options = {
37
38 services.spacecookie = {
39
40 enable = mkEnableOption "spacecookie";
41
42 package = mkPackageOption pkgs "spacecookie" {
43 example = "haskellPackages.spacecookie";
44 };
45
46 openFirewall = mkOption {
47 type = types.bool;
48 default = false;
49 description = ''
50 Whether to open the necessary port in the firewall for spacecookie.
51 '';
52 };
53
54 port = mkOption {
55 type = types.port;
56 default = 70;
57 description = ''
58 Port the gopher service should be exposed on.
59 '';
60 };
61
62 address = mkOption {
63 type = types.str;
64 default = "[::]";
65 description = ''
66 Address to listen on. Must be in the
67 `ListenStream=` syntax of
68 {manpage}`systemd.socket(5)`.
69 '';
70 };
71
72 settings = mkOption {
73 type = types.submodule {
74 freeformType = format.type;
75
76 options.hostname = mkOption {
77 type = types.str;
78 default = "localhost";
79 description = ''
80 The hostname the service is reachable via. Clients
81 will use this hostname for further requests after
82 loading the initial gopher menu.
83 '';
84 };
85
86 options.root = mkOption {
87 type = types.path;
88 default = "/srv/gopher";
89 description = ''
90 The directory spacecookie should serve via gopher.
91 Files in there need to be world-readable since
92 the spacecookie service file sets
93 `DynamicUser=true`.
94 '';
95 };
96
97 options.log = {
98 enable = mkEnableOption "logging for spacecookie" // {
99 default = true;
100 example = false;
101 };
102
103 hide-ips = mkOption {
104 type = types.bool;
105 default = true;
106 description = ''
107 If enabled, spacecookie will hide personal
108 information of users like IP addresses from
109 log output.
110 '';
111 };
112
113 hide-time = mkOption {
114 type = types.bool;
115 # since we are starting with systemd anyways
116 # we deviate from the default behavior here:
117 # journald will add timestamps, so no need
118 # to double up.
119 default = true;
120 description = ''
121 If enabled, spacecookie will not print timestamps
122 at the beginning of every log line.
123 '';
124 };
125
126 level = mkOption {
127 type = types.enum [
128 "info"
129 "warn"
130 "error"
131 ];
132 default = "info";
133 description = ''
134 Log level for the spacecookie service.
135 '';
136 };
137 };
138 };
139
140 description = ''
141 Settings for spacecookie. The settings set here are
142 directly translated to the spacecookie JSON config
143 file. See
144 [spacecookie.json(5)](https://sternenseemann.github.io/spacecookie/spacecookie.json.5.html)
145 for explanations of all options.
146 '';
147 };
148 };
149 };
150
151 config = mkIf cfg.enable {
152 assertions = [
153 {
154 assertion = !(cfg.settings ? user);
155 message = ''
156 spacecookie is started as a normal user, so the setuid
157 feature doesn't work. If you want to run spacecookie as
158 a specific user, set:
159 systemd.services.spacecookie.serviceConfig = {
160 DynamicUser = false;
161 User = "youruser";
162 Group = "yourgroup";
163 }
164 '';
165 }
166 {
167 assertion = !(cfg.settings ? listen || cfg.settings ? port);
168 message = ''
169 The NixOS spacecookie module uses socket activation,
170 so the listen options have no effect. Use the port
171 and address options in services.spacecookie instead.
172 '';
173 }
174 ];
175
176 systemd.sockets.spacecookie = {
177 description = "Socket for the Spacecookie Gopher Server";
178 wantedBy = [ "sockets.target" ];
179 listenStreams = [ "${cfg.address}:${toString cfg.port}" ];
180 socketConfig = {
181 BindIPv6Only = "both";
182 };
183 };
184
185 systemd.services.spacecookie = {
186 description = "Spacecookie Gopher Server";
187 wantedBy = [ "multi-user.target" ];
188 requires = [ "spacecookie.socket" ];
189
190 serviceConfig = {
191 Type = "notify";
192 ExecStart = "${lib.getBin cfg.package}/bin/spacecookie ${configFile}";
193 FileDescriptorStoreMax = 1;
194
195 DynamicUser = true;
196
197 ProtectSystem = "strict";
198 ProtectHome = true;
199 PrivateTmp = true;
200 PrivateDevices = true;
201 PrivateMounts = true;
202 PrivateUsers = true;
203
204 ProtectKernelTunables = true;
205 ProtectKernelModules = true;
206 ProtectControlGroups = true;
207
208 CapabilityBoundingSet = "";
209 NoNewPrivileges = true;
210 LockPersonality = true;
211 RestrictRealtime = true;
212
213 # AF_UNIX for communication with systemd
214 # AF_INET replaced by BindIPv6Only=both
215 RestrictAddressFamilies = "AF_UNIX AF_INET6";
216 };
217 };
218
219 networking.firewall = mkIf cfg.openFirewall {
220 allowedTCPPorts = [ cfg.port ];
221 };
222 };
223}