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