diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6d3f39efe67e..ccbfd4d5c0d7 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1635,6 +1635,7 @@ ./services/web-apps/lasuite-docs.nix ./services/web-apps/lasuite-meet.nix ./services/web-apps/lemmy.nix + ./services/web-apps/librespeed.nix ./services/web-apps/libretranslate.nix ./services/web-apps/limesurvey.nix ./services/web-apps/linkwarden.nix diff --git a/nixos/modules/services/web-apps/librespeed.nix b/nixos/modules/services/web-apps/librespeed.nix new file mode 100644 index 000000000000..1ffc382a3d8e --- /dev/null +++ b/nixos/modules/services/web-apps/librespeed.nix @@ -0,0 +1,452 @@ +{ + config, + lib, + options, + pkgs, + ... +}: +let + cfg = config.services.librespeed; + opt = options.services.librespeed; + + settingsFormat = pkgs.formats.toml { }; + + configFile = settingsFormat.generate "librespeed-rust-config.toml" cfg.settings; + + librespeedAssets = + pkgs.runCommand "librespeed-assets" + { + serversList = '' + function get_servers() { + return ${builtins.toJSON cfg.frontend.servers} + } + function override_settings () { + ${lib.pipe cfg.frontend.settings [ + (lib.mapAttrs (name: val: " s.setParameter(${builtins.toJSON name},${builtins.toJSON val});")) + lib.attrValues + lib.concatLines + ]} + } + ''; + passAsFile = [ "serversList" ]; + } + '' + cp -r ${cfg.package}/assets $out + chmod +w "$out/servers_list.js" + cp "$serversListPath" "$out/servers_list.js" + substitute ${cfg.package}/assets/index.html $out/index.html \ + --replace-fail "s.setParameter(\"telemetry_level\",\"basic\"); //enable telemetry" "override_settings();" \ + --replace-fail "LibreSpeed Example" ${lib.escapeShellArg (lib.escapeXML cfg.frontend.pageTitle)} \ + --replace-fail "PUT@YOUR_EMAIL.HERE" ${lib.escapeShellArg (lib.escapeXML cfg.frontend.contactEmail)} \ + --replace-fail "TO BE FILLED BY DEVELOPER" ${lib.escapeShellArg (lib.escapeXML cfg.frontend.contactEmail)} + ''; +in +{ + options.services.librespeed = { + enable = lib.mkEnableOption "LibreSpeed server"; + package = lib.mkPackageOption pkgs "librespeed-rust" { }; + + domain = lib.mkOption { + description = '' + If not `null`, this will add an entry to `services.librespeed.servers` and + configure librespeed to use TLS. + ''; + default = null; + type = with lib.types; nullOr nonEmptyStr; + }; + + downloadIPDB = lib.mkOption { + description = '' + Whether to download the IP info database before starting librespeed. + Disable this if you want to use the Go implementation. + ''; + default = !(cfg.secrets ? "ipinfo_api_key"); + defaultText = lib.literalExpression ''!(config.${opt.secrets} ? "ipinfo_api_key")''; + type = lib.types.bool; + }; + + secrets = lib.mkOption { + description = '' + Attribute set of filesystem paths. + The contents of the specified paths will be read at service start time and merged with the attributes provided in `settings`. + ''; + default = { }; + type = + with lib.types; + nullOr ( + attrsOf (pathWith { + inStore = false; + absolute = true; + }) + ); + }; + + settings = lib.mkOption { + description = '' + LibreSpeed configuration written as Nix expression. + All values set to `null` will be excluded from the evaluated config. + This is useful if you want to omit certain defaults when using a different LibreSpeed implementation. + + See [github.com/librespeed](https://github.com/librespeed/speedtest-rust) for configuration help. + ''; + default = { + assets_path = + if (cfg.frontend.enable && !cfg.frontend.useNginx) then + librespeedAssets + else + pkgs.writeTextDir "index.html" ""; + + bind_address = "::"; + listen_port = 8989; + base_url = "backend"; + worker_threads = "auto"; + + database_type = "none"; + database_file = "/var/lib/librespeed/speedtest.sqlite"; + + # librespeed-rust will fail to start if the following config parameters are omitted. + ipinfo_api_key = ""; + stats_password = ""; + + redact_ip_addresses = false; + result_image_theme = "light"; + + enable_tls = cfg.tlsCertificate != null && cfg.tlsKey != null; + tls_cert_file = lib.optionalString ( + cfg.tlsCertificate != null + ) "/run/credentials/librespeed.service/cert.pem"; + tls_key_file = lib.optionalString ( + cfg.tlsKey != null + ) "/run/credentials/librespeed.service/key.pem"; + }; + defaultText = lib.literalExpression '' + { + assets_path = if (config.${opt.frontend.enable} && !config.${opt.frontend.useNginx}) then + librespeedAssets + else + pkgs.writeTextDir "index.html" ""; + + bind_address = "::"; + listen_port = 8989; + base_url = "backend"; + worker_threads = "auto"; + + database_type = "none"; + database_file = "/var/lib/librespeed/speedtest.sqlite"; + + # librespeed-rust will fail to start if the following config parameters are omitted. + ipinfo_api_key = ""; + stats_password = ""; + + redact_ip_addresses = false; + result_image_theme = "light"; + + enable_tls = config.${opt.tlsCertificate} != null && config.${opt.tlsKey} != null; + tls_cert_file = lib.optionalString (config.${opt.tlsCertificate} != null) "/run/credentials/librespeed.service/cert.pem"; + tls_key_file = lib.optionalString (config.${opt.tlsKey} != null) "/run/credentials/librespeed.service/key.pem"; + } + ''; + type = + with lib.types; + nullOr ( + attrsOf (oneOf [ + (nullOr bool) + int + str + package + ]) + ); + }; + + useACMEHost = lib.mkOption { + type = with lib.types; nullOr nonEmptyStr; + default = null; + example = "speed.example.com"; + description = '' + Use a certificate generated by the NixOS ACME module for the given host. + Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`. + ''; + }; + + tlsCertificate = lib.mkOption { + type = with lib.types; nullOr nonEmptyStr; + default = + if (cfg.useACMEHost != null) then + "${config.security.acme.certs.${cfg.useACMEHost}.directory}/cert.pem" + else + null; + defaultText = lib.literalExpression "lib.optionalString (config.${opt.useACMEHost} != null) \"\${config.security.acme.certs.\${config.${opt.useACMEHost}}.directory}/cert.pem\""; + description = "TLS certificate to use. Use together with `tlsKey`."; + }; + + tlsKey = lib.mkOption { + type = with lib.types; nullOr nonEmptyStr; + default = + if (cfg.useACMEHost != null) then + "${config.security.acme.certs.${cfg.useACMEHost}.directory}/key.pem" + else + null; + defaultText = lib.literalExpression "lib.optionalString (config.${opt.useACMEHost} != null) \"\${config.security.acme.certs.\${config.${opt.useACMEHost}}.directory}/key.pem\""; + description = "TLS private key to use. Use together with `tlsCertificate`."; + }; + + frontend = { + enable = lib.mkEnableOption "" // { + description = '' + Enables the LibreSpeed frontend and adds a nginx virtual host if + not explicitly disabled and `services.librespeed.domain` is not `null`. + ''; + }; + + contactEmail = lib.mkOption { + description = "Email address listed in the privacy policy."; + type = lib.types.str; + }; + + pageTitle = lib.mkOption { + description = "Title of the webpage."; + default = "LibreSpeed"; + type = lib.types.str; + }; + + useNginx = lib.mkOption { + description = '' + Configure nginx for the LibreSpeed frontend. + This will only create a virtual host for the frontend and won't proxy all requests because + the reported upload and download speeds are inaccurate if proxied. + ''; + default = cfg.domain != null; + defaultText = lib.literalExpression "config.${opt.domain} != null"; + type = lib.types.bool; + }; + + settings = lib.mkOption { + description = '' + Override default settings of the speedtest web client. + See [speedtest_worker.js][link] for a list of possible values. + + [link]: https://github.com/librespeed/speedtest/blob/master/speedtest_worker.js#L39 + ''; + default = { + telemetry_level = "basic"; + }; + type = lib.types.nullOr ( + lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + bool + int + str + float + ]); + } + ); + }; + + servers = lib.mkOption { + description = "LibreSpeed servers that should appear in the server list."; + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + description = "Name shown in the server list."; + type = lib.types.nonEmptyStr; + }; + + server = lib.mkOption { + description = "URL to the server. You may use `//` instead of `http://` or `https://`."; + type = lib.types.nonEmptyStr; + }; + + dlURL = lib.mkOption { + description = '' + URL path to download test on this server. + Append `.php` to the default value if the server uses the php implementation. + ''; + default = "backend/garbage"; + type = lib.types.nonEmptyStr; + }; + + ulURL = lib.mkOption { + description = '' + URL path to upload test on this server. + Append `.php` to the default value if the server uses the php implementation. + ''; + default = "backend/empty"; + type = lib.types.nonEmptyStr; + }; + + pingURL = lib.mkOption { + description = '' + URL path to latency/jitter test on this server. + Append `.php` to the default value if the server uses the php implementation. + ''; + default = "backend/empty"; + type = lib.types.nonEmptyStr; + }; + + getIpURL = lib.mkOption { + description = '' + URL path to IP lookup on this server. + Append `.php` to the default value if the server uses the php implementation. + ''; + default = "backend/getIP"; + type = lib.types.nonEmptyStr; + }; + }; + } + ); + }; + }; + }; + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.frontend.useNginx -> cfg.domain != null; + message = "${opt.frontend.useNginx} requires ${opt.domain} to be set."; + } + ]; + + security.acme.certs = lib.mkIf (cfg.useACMEHost != null) { + ${cfg.useACMEHost}.reloadServices = [ "librespeed.service" ]; + }; + + services = { + librespeed = { + frontend.servers = lib.mkIf (cfg.frontend.enable && (cfg.domain != null)) [ + { + name = cfg.domain; + server = "//${cfg.domain}"; + } + ]; + settings = lib.mapAttrs (n: v: lib.mkDefault v) opt.settings.default; + }; + + nginx.virtualHosts = lib.mkIf (cfg.frontend.enable && cfg.frontend.useNginx) { + ${cfg.domain} = { + forceSSL = true; + locations = { + "/".root = librespeedAssets; + "= /servers.json".return = "200 '${builtins.toJSON cfg.frontend.servers}'"; + "/backend/" = { + proxyPass = "http://127.0.0.1:${toString cfg.settings.listen_port}/backend/"; + extraConfig = # nginx + '' + client_max_body_size 0; + gzip off; + proxy_buffering off; + proxy_request_buffering off; + '' + + lib.optionalString (lib.any (m: m.name == "brotli") config.services.nginx.additionalModules) '' + brotli off; + '' + + lib.optionalString (lib.any (m: m.name == "zstd") config.services.nginx.additionalModules) '' + zstd off; + ''; + }; + }; + }; + }; + }; + + systemd.services = { + librespeed-secrets = lib.mkIf (cfg.secrets != { }) { + description = "LibreSpeed secret helper"; + + ExecStart = lib.getExe ( + pkgs.writeShellApplication { + name = "librespeed-secrets"; + runtimeInputs = [ pkgs.coreutils ]; + text = '' + cp ${configFile} ''${RUNTIME_DIRECTORY%%:*}/config.toml + '' + + lib.pipe cfg.secrets [ + (lib.mapAttrs ( + name: file: '' + cat >>''${RUNTIME_DIRECTORY%%:*}/config.toml < lands. + cargoLock.lockFile = ./Cargo.lock; + + postInstall = '' + cp -r assets $out/ + ''; + + meta = { + description = "Very lightweight speed test implementation in Rust"; + homepage = "https://github.com/librespeed/speedtest-rust"; + license = lib.licenses.lgpl3Plus; + teams = with lib.teams; [ c3d2 ]; + mainProgram = "librespeed-rs"; + }; +}