1{ config, lib, pkgs, ... }:
2
3let
4 cfg = config.services.stargazer;
5 globalSection = ''
6 listen = ${lib.concatStringsSep " " cfg.listen}
7 connection-logging = ${lib.boolToString cfg.connectionLogging}
8 log-ip = ${lib.boolToString cfg.ipLog}
9 log-ip-partial = ${lib.boolToString cfg.ipLogPartial}
10 request-timeout = ${toString cfg.requestTimeout}
11 response-timeout = ${toString cfg.responseTimeout}
12
13 [:tls]
14 store = ${toString cfg.store}
15 organization = ${cfg.certOrg}
16 gen-certs = ${lib.boolToString cfg.genCerts}
17 regen-certs = ${lib.boolToString cfg.regenCerts}
18 ${lib.optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"}
19
20 '';
21 genINI = lib.generators.toINI { };
22 configFile = pkgs.writeText "config.ini" (lib.strings.concatStrings (
23 [ globalSection ] ++ (lib.lists.forEach cfg.routes (section:
24 let
25 name = section.route;
26 params = builtins.removeAttrs section [ "route" ];
27 in
28 genINI
29 {
30 "${name}" = params;
31 } + "\n"
32 ))
33 ));
34in
35{
36 options.services.stargazer = {
37 enable = lib.mkEnableOption (lib.mdDoc "Stargazer Gemini server");
38
39 listen = lib.mkOption {
40 type = lib.types.listOf lib.types.str;
41 default = [ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]";
42 defaultText = lib.literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
43 example = lib.literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
44 description = lib.mdDoc ''
45 Address and port to listen on.
46 '';
47 };
48
49 connectionLogging = lib.mkOption {
50 type = lib.types.bool;
51 default = true;
52 description = lib.mdDoc "Whether or not to log connections to stdout.";
53 };
54
55 ipLog = lib.mkOption {
56 type = lib.types.bool;
57 default = false;
58 description = lib.mdDoc "Log client IP addresses in the connection log.";
59 };
60
61 ipLogPartial = lib.mkOption {
62 type = lib.types.bool;
63 default = false;
64 description = lib.mdDoc "Log partial client IP addresses in the connection log.";
65 };
66
67 requestTimeout = lib.mkOption {
68 type = lib.types.int;
69 default = 5;
70 description = lib.mdDoc ''
71 Number of seconds to wait for the client to send a complete
72 request. Set to 0 to disable.
73 '';
74 };
75
76 responseTimeout = lib.mkOption {
77 type = lib.types.int;
78 default = 0;
79 description = lib.mdDoc ''
80 Number of seconds to wait for the client to send a complete
81 request and for stargazer to finish sending the response.
82 Set to 0 to disable.
83 '';
84 };
85
86 store = lib.mkOption {
87 type = lib.types.path;
88 default = /var/lib/gemini/certs;
89 description = lib.mdDoc ''
90 Path to the certificate store on disk. This should be a
91 persistent directory writable by Stargazer.
92 '';
93 };
94
95 certOrg = lib.mkOption {
96 type = lib.types.str;
97 default = "stargazer";
98 description = lib.mdDoc ''
99 The name of the organization responsible for the X.509
100 certificate's /O name.
101 '';
102 };
103
104 genCerts = lib.mkOption {
105 type = lib.types.bool;
106 default = true;
107 description = lib.mdDoc ''
108 Set to false to disable automatic certificate generation.
109 Use if you want to provide your own certs.
110 '';
111 };
112
113 regenCerts = lib.mkOption {
114 type = lib.types.bool;
115 default = true;
116 description = lib.mdDoc ''
117 Set to false to turn off automatic regeneration of expired certificates.
118 Use if you want to provide your own certs.
119 '';
120 };
121
122 certLifetime = lib.mkOption {
123 type = lib.types.str;
124 default = "";
125 description = lib.mdDoc ''
126 How long certs generated by Stargazer should live for.
127 Certs live forever by default.
128 '';
129 example = lib.literalExpression "\"1y\"";
130 };
131
132 routes = lib.mkOption {
133 type = lib.types.listOf
134 (lib.types.submodule {
135 freeformType = with lib.types; attrsOf (nullOr
136 (oneOf [
137 bool
138 int
139 float
140 str
141 ]) // {
142 description = "INI atom (null, bool, int, float or string)";
143 });
144 options.route = lib.mkOption {
145 type = lib.types.str;
146 description = lib.mdDoc "Route section name";
147 };
148 });
149 default = [ ];
150 description = lib.mdDoc ''
151 Routes that Stargazer should server.
152
153 Expressed as a list of attribute sets. Each set must have a key `route`
154 that becomes the section name for that route in the stargazer ini cofig.
155 The remaining keys and values become the parameters for that route.
156
157 [Refer to upstream docs for other params](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt)
158 '';
159 example = lib.literalExpression ''
160 [
161 {
162 route = "example.com";
163 root = "/srv/gemini/example.com"
164 }
165 {
166 route = "example.com:/man";
167 root = "/cgi-bin";
168 cgi = true;
169 }
170 {
171 route = "other.org~(.*)";
172 redirect = "gemini://example.com";
173 rewrite = "\1";
174 }
175 ]
176 '';
177 };
178
179 user = lib.mkOption {
180 type = lib.types.str;
181 default = "stargazer";
182 description = lib.mdDoc "User account under which stargazer runs.";
183 };
184
185 group = lib.mkOption {
186 type = lib.types.str;
187 default = "stargazer";
188 description = lib.mdDoc "Group account under which stargazer runs.";
189 };
190 };
191
192 config = lib.mkIf cfg.enable {
193 systemd.services.stargazer = {
194 description = "Stargazer gemini server";
195 after = [ "network.target" ];
196 wantedBy = [ "multi-user.target" ];
197 serviceConfig = {
198 ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile}";
199 Restart = "always";
200 # User and group
201 User = cfg.user;
202 Group = cfg.group;
203 };
204 };
205
206 # Create default cert store
207 system.activationScripts.makeStargazerCertDir =
208 lib.optionalAttrs (cfg.store == /var/lib/gemini/certs) ''
209 mkdir -p /var/lib/gemini/certs
210 chown -R ${cfg.user}:${cfg.group} /var/lib/gemini/certs
211 '';
212
213 users.users = lib.optionalAttrs (cfg.user == "stargazer") {
214 stargazer = {
215 group = cfg.group;
216 isSystemUser = true;
217 };
218 };
219
220 users.groups = lib.optionalAttrs (cfg.group == "stargazer") {
221 stargazer = { };
222 };
223 };
224
225 meta.maintainers = with lib.maintainers; [ gaykitty ];
226}