1{
2 lib,
3 config,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.reposilite;
9 format = pkgs.formats.cdn { };
10 configFile = format.generate "reposilite.cdn" cfg.settings;
11
12 useEmbeddedDb = cfg.database.type == "sqlite" || cfg.database.type == "h2";
13 useMySQL = cfg.database.type == "mariadb" || cfg.database.type == "mysql";
14 usePostgres = cfg.database.type == "postgresql";
15
16 # db password is appended at runtime by the service script (if needed)
17 dbString =
18 if useEmbeddedDb then
19 "${cfg.database.type} ${cfg.database.path}"
20 else
21 "${cfg.database.type} ${cfg.database.host}:${builtins.toString cfg.database.port} ${cfg.database.dbname} ${cfg.database.user} $(<${cfg.database.passwordFile})";
22
23 certDir = config.security.acme.certs.${cfg.useACMEHost}.directory;
24
25 databaseModule = {
26 options = {
27 type = lib.mkOption {
28 type = lib.types.enum [
29 "h2"
30 "mariadb"
31 "mysql"
32 "postgresql"
33 "sqlite"
34 ];
35 description = ''
36 Database engine to use.
37 '';
38 default = "sqlite";
39 };
40
41 path = lib.mkOption {
42 type = lib.types.str;
43 description = ''
44 Path to the embedded database file. Set to `--temporary` to use an in-memory database.
45 '';
46 default = "reposilite.db";
47 };
48
49 host = lib.mkOption {
50 type = lib.types.str;
51 description = ''
52 Database host address.
53 '';
54 default = "127.0.0.1";
55 };
56
57 port = lib.mkOption {
58 type = lib.types.port;
59 description = ''
60 Database TCP port.
61 '';
62 defaultText = lib.literalExpression ''
63 if type == "postgresql" then 5432 else 3306
64 '';
65 default = if usePostgres then config.services.postgresql.settings.port else 3306;
66 };
67
68 dbname = lib.mkOption {
69 type = lib.types.str;
70 description = ''
71 Database name.
72 '';
73 default = "reposilite";
74 };
75
76 user = lib.mkOption {
77 type = lib.types.str;
78 description = ''
79 Database user.
80 '';
81 default = "reposilite";
82 };
83
84 passwordFile = lib.mkOption {
85 type = lib.types.nullOr lib.types.path;
86 description = ''
87 Path to the file containing the password for the database connection.
88 This file must be readable by {option}`services.reposilite.user`.
89 '';
90 default = null;
91 };
92 };
93 };
94
95 settingsModule = {
96 freeformType = format.type;
97 options = {
98 hostname = lib.mkOption {
99 type = lib.types.str;
100 description = ''
101 The hostname to bind to. Set to `0.0.0.0` to accept connections from everywhere, or `127.0.0.1` to restrict to localhost."
102 '';
103 default = "0.0.0.0";
104 example = "127.0.0.1";
105 };
106
107 port = lib.mkOption {
108 type = lib.types.port;
109 description = ''
110 The TCP port to bind to.
111 '';
112 default = 3000;
113 };
114
115 database = lib.mkOption {
116 type = lib.types.nullOr lib.types.str;
117 description = ''
118 Database connection string. Please use {option}`services.reposilite.database` instead.
119 See https://reposilite.com/guide/general#local-configuration for valid values.
120 '';
121 default = null;
122 };
123
124 sslEnabled = lib.mkOption {
125 type = lib.types.bool;
126 description = ''
127 Whether to listen for encrypted connections on {option}`settings.sslPort`.
128 '';
129 default = false;
130 };
131
132 sslPort = lib.mkOption {
133 type = lib.types.port; # cant be null
134 description = "SSL port to bind to. SSL needs to be enabled explicitly via {option}`settings.enableSsl`.";
135 default = 443;
136 };
137
138 keyPath = lib.mkOption {
139 type = lib.types.nullOr lib.types.str;
140 description = ''
141 Path to the .jsk KeyStore or paths to the PKCS#8 certificate and private key, separated by a space (see example).
142 You can use `''${WORKING_DIRECTORY}` to refer to paths relative to Reposilite's working directory.
143 If you are using a Java KeyStore, don't forget to specify the password via the {var}`REPOSILITE_LOCAL_KEYPASSWORD` environment variable.
144 See https://reposilite.com/guide/ssl for more information on how to set SSL up.
145 '';
146 default = null;
147 example = "\${WORKING_DIRECTORY}/cert.pem \${WORKING_DIRECTORY}/key.pem";
148 };
149
150 keyPassword = lib.mkOption {
151 type = lib.types.nullOr lib.types.str;
152 description = ''
153 Plaintext password used to unlock the Java KeyStore set in {option}`services.reposilite.settings.keyPath`.
154 WARNING: this option is insecure and should not be used to store the password.
155 Consider using {option}`services.reposilite.keyPasswordFile` instead.
156 '';
157 default = null;
158 };
159
160 enforceSsl = lib.mkOption {
161 type = lib.types.bool;
162 description = ''
163 Whether to redirect all traffic to SSL.
164 '';
165 default = false;
166 };
167
168 webThreadPool = lib.mkOption {
169 type = lib.types.ints.between 5 65535;
170 description = ''
171 Maximum amount of threads used by the core thread pool. (min: 5)
172 The web thread pool handles the first few steps of incoming HTTP connections, tasks are redirected as soon as possible to the IO thread pool.
173 '';
174 default = 16;
175 };
176
177 ioThreadPool = lib.mkOption {
178 type = lib.types.ints.between 2 65535;
179 description = ''
180 The IO thread pool handles all tasks that may benefit from non-blocking IO. (min: 2)
181 Because most tasks are redirected to IO thread pool, it might be a good idea to keep it at least equal to web thread pool.
182 '';
183 default = 8;
184 };
185
186 databaseThreadPool = lib.mkOption {
187 type = lib.types.ints.positive;
188 description = ''
189 Maximum amount of concurrent connections to the database. (one per thread)
190 Embedded databases (sqlite, h2) do not support truly concurrent connections, so the value will always be `1` if they are used.
191 '';
192 default = 1;
193 };
194
195 compressionStrategy = lib.mkOption {
196 type = lib.types.enum [
197 "none"
198 "gzip"
199 ];
200 description = ''
201 Compression algorithm used by this instance of Reposilite.
202 `none` reduces usage of CPU & memory, but requires transfering more data.
203 '';
204 default = "none";
205 };
206
207 idleTimeout = lib.mkOption {
208 type = lib.types.ints.unsigned;
209 description = ''
210 Default idle timeout used by Jetty.
211 '';
212 default = 30000;
213 };
214
215 bypassExternalCache = lib.mkOption {
216 type = lib.types.bool;
217 description = ''
218 Add cache bypass headers to responses from /api/* to avoid issues with proxies such as Cloudflare.
219 '';
220 default = true;
221 };
222
223 cachedLogSize = lib.mkOption {
224 type = lib.types.ints.unsigned;
225 description = ''
226 Amount of messages stored in the cache logger.
227 '';
228 default = 50;
229 };
230
231 defaultFrontend = lib.mkOption {
232 type = lib.types.bool;
233 description = ''
234 Whether to enable the default included frontend with a dashboard.
235 '';
236 default = true;
237 };
238
239 basePath = lib.mkOption {
240 type = lib.types.str;
241 description = ''
242 Custom base path for this Reposilite instance.
243 It is not recommended changing this, you should instead prioritize using a different subdomain.
244 '';
245 default = "/";
246 };
247
248 debugEnabled = lib.mkOption {
249 type = lib.types.bool;
250 description = ''
251 Whether to enable debug mode.
252 '';
253 default = false;
254 };
255 };
256 };
257in
258{
259 options.services.reposilite = {
260 enable = lib.mkEnableOption "Reposilite";
261 package = lib.mkPackageOption pkgs "reposilite" { } // {
262 apply =
263 pkg:
264 pkg.override (old: {
265 plugins = (old.plugins or [ ]) ++ cfg.plugins;
266 });
267 };
268
269 plugins = lib.mkOption {
270 type = lib.types.listOf lib.types.package;
271 description = ''
272 List of plugins to add to Reposilite.
273 '';
274 default = [ ];
275 example = "with reposilitePlugins; [ checksum groovy ]";
276 };
277
278 database = lib.mkOption {
279 description = "Database options.";
280 default = { };
281 type = lib.types.submodule databaseModule;
282 };
283
284 keyPasswordFile = lib.mkOption {
285 type = lib.types.nullOr lib.types.path;
286 description = ''
287 Path the the file containing the password used to unlock the Java KeyStore file specified in {option}`services.reposilite.settings.keyPath`.
288 This file must be readable my {option}`services.reposilite.user`.
289 '';
290 default = null;
291 };
292
293 useACMEHost = lib.mkOption {
294 type = lib.types.nullOr lib.types.str;
295 description = ''
296 Host of an existing Let's Encrypt certificate to use for SSL.
297 Make sure that the certificate directory is readable by the `reposilite` user or group, for example via {option}`security.acme.certs.<cert>.group`.
298 *Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using {option}`security.acme.certs`*
299 '';
300 default = null;
301 };
302
303 settings = lib.mkOption {
304 description = "Configuration written to the reposilite.cdn file";
305 default = { };
306 type = lib.types.submodule settingsModule;
307 };
308
309 workingDirectory = lib.mkOption {
310 type = lib.types.path;
311 description = ''
312 Working directory for Reposilite.
313 '';
314 default = "/var/lib/reposilite";
315 };
316
317 extraArgs = lib.mkOption {
318 type = lib.types.listOf lib.types.str;
319 description = ''
320 Extra arguments/parameters passed to the Reposilite. Can be used for first token generation.
321 '';
322 default = [ ];
323 example = lib.literalExpression ''[ "--token" "name:tempsecrettoken" ]'';
324 };
325
326 user = lib.mkOption {
327 type = lib.types.str;
328 description = ''
329 The user to run Reposilite under.
330 '';
331 default = "reposilite";
332 };
333
334 group = lib.mkOption {
335 type = lib.types.str;
336 description = ''
337 The group to run Reposilite under.
338 '';
339 default = "reposilite";
340 };
341
342 openFirewall = lib.mkOption {
343 type = lib.types.bool;
344 description = ''
345 Whether to open the firewall ports for Reposilite. If SSL is enabled, its port will be opened too.
346 '';
347 default = false;
348 };
349 };
350
351 config = lib.mkIf cfg.enable {
352 assertions = [
353 {
354 assertion = cfg.settings.sslEnabled -> cfg.settings.keyPath != null;
355 message = ''
356 Reposilite was configured to enable SSL, but no valid paths to certificate files were provided via `settings.keyPath`.
357 Read more about SSL certificates here: https://reposilite.com/guide/ssl
358 '';
359 }
360 {
361 assertion = cfg.settings.enforceSsl -> cfg.settings.sslEnabled;
362 message = "You cannot enforce SSL if SSL is not enabled.";
363 }
364 {
365 assertion = !useEmbeddedDb -> cfg.database.passwordFile != null;
366 message = "You need to set `services.reposilite.database.passwordFile` when using MySQL or Postgres.";
367 }
368 ];
369
370 services.reposilite.settings.keyPath = lib.mkIf (
371 cfg.useACMEHost != null
372 ) "${certDir}/fullchain.pem ${certDir}/key.pem";
373
374 environment.systemPackages = [ cfg.package ];
375
376 users = {
377 groups.${cfg.group} = lib.mkIf (cfg.group == "reposilite") { };
378 users.${cfg.user} = lib.mkIf (cfg.user == "reposilite") {
379 isSystemUser = true;
380 group = cfg.group;
381 };
382 };
383
384 networking.firewall = lib.mkIf cfg.openFirewall (
385 lib.mkMerge [
386 {
387 allowedTCPPorts = [ cfg.settings.port ];
388 }
389 (lib.mkIf cfg.settings.sslEnabled {
390 allowedTCPPorts = [ cfg.settings.sslPort ];
391 })
392 ]
393 );
394
395 systemd.services.reposilite = {
396 enable = true;
397 wantedBy = [ "multi-user.target" ];
398 after =
399 [ "network.target" ]
400 ++ (lib.optional useMySQL "mysql.service")
401 ++ (lib.optional usePostgres "postgresql.service");
402
403 script =
404 lib.optionalString (cfg.keyPasswordFile != null && cfg.settings.keyPassword == null) ''
405 export REPOSILITE_LOCAL_KEYPASSWORD="$(<${cfg.keyPasswordFile})"
406 ''
407 + ''
408 export REPOSILITE_LOCAL_DATABASE="${dbString}"
409
410 ${lib.getExe cfg.package} --local-configuration ${configFile} --local-configuration-mode none --working-directory ${cfg.workingDirectory} ${lib.escapeShellArgs cfg.extraArgs}
411 '';
412
413 serviceConfig = lib.mkMerge [
414 (lib.mkIf (builtins.dirOf cfg.workingDirectory == "/var/lib") {
415 StateDirectory = builtins.baseNameOf cfg.workingDirectory;
416 StateDirectoryMode = "700";
417 })
418 {
419 Type = "exec";
420 Restart = "on-failure";
421
422 User = cfg.user;
423 Group = cfg.group;
424 WorkingDirectory = cfg.workingDirectory;
425
426 # TODO better hardening
427 LimitNOFILE = "1048576";
428 PrivateTmp = true;
429 PrivateDevices = true;
430 ProtectHome = true;
431 ProtectSystem = "strict";
432 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
433 }
434 ];
435 };
436 };
437
438 meta.maintainers = [ lib.maintainers.uku3lig ];
439}