From 6b8e818471e70bb03a43b17d59d0aa4ef9376dac Mon Sep 17 00:00:00 2001 From: Defelo Date: Sat, 12 Jul 2025 15:06:00 +0200 Subject: [PATCH 1/3] chhoto-url: init at 6.2.8 (cherry picked from commit 396614dd55e86fe0dd57507e728c6c313858f233) --- pkgs/by-name/ch/chhoto-url/package.nix | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 pkgs/by-name/ch/chhoto-url/package.nix diff --git a/pkgs/by-name/ch/chhoto-url/package.nix b/pkgs/by-name/ch/chhoto-url/package.nix new file mode 100644 index 000000000000..73de105cf5cc --- /dev/null +++ b/pkgs/by-name/ch/chhoto-url/package.nix @@ -0,0 +1,43 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, + nix-update-script, +}: + +rustPlatform.buildRustPackage (finalAttrs: { + pname = "chhoto-url"; + version = "6.2.8"; + + src = fetchFromGitHub { + owner = "SinTan1729"; + repo = "chhoto-url"; + tag = finalAttrs.version; + hash = "sha256-aWiLfhNbtjsM7fEqoNIKsU12/3b8ORTpZ/4jyqSLmdM="; + }; + + sourceRoot = "${finalAttrs.src.name}/actix"; + + postPatch = '' + substituteInPlace src/{main.rs,services.rs} \ + --replace-fail "./resources/" "${placeholder "out"}/share/chhoto-url/resources/" + ''; + + cargoHash = "sha256-rKNGUl1TI21SOBwTuv/TGl46S8FVjCWunJwP5PLdx6g="; + + postInstall = '' + mkdir -p $out/share/chhoto-url + cp -r ${finalAttrs.src}/resources $out/share/chhoto-url/resources + ''; + + passthru.updateScript = nix-update-script { }; + + meta = { + description = "Simple, blazingly fast, selfhosted URL shortener with no unnecessary features"; + homepage = "https://github.com/SinTan1729/chhoto-url"; + changelog = "https://github.com/SinTan1729/chhoto-url/releases/tag/${finalAttrs.version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ defelo ]; + mainProgram = "chhoto-url"; + }; +}) From 6642acbb21943a4826164eb3a98bf15bd3bb86e7 Mon Sep 17 00:00:00 2001 From: Defelo Date: Sat, 12 Jul 2025 16:43:13 +0200 Subject: [PATCH 2/3] nixos/chhoto-url: init module (cherry picked from commit 55e6f26bff0266a9276eb5f146e1da04d8c15d83) --- .../manual/release-notes/rl-2511.section.md | 1 + nixos/modules/module-list.nix | 1 + .../modules/services/web-apps/chhoto-url.nix | 212 ++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 nixos/modules/services/web-apps/chhoto-url.nix diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index a5c8dbec6474..2b0580a37379 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -12,6 +12,7 @@ - [gtklock](https://github.com/jovanlanik/gtklock), a GTK-based lockscreen for Wayland. Available as [programs.gtklock](#opt-programs.gtklock.enable). - [Chrysalis](https://github.com/keyboardio/Chrysalis), a graphical configurator for Kaleidoscope-powered keyboards. Available as [programs.chrysalis](#opt-programs.chrysalis.enable). +- [Chhoto URL](https://github.com/SinTan1729/chhoto-url), a simple, blazingly fast, selfhosted URL shortener with no unnecessary features, written in Rust. Available as [services.chhoto-url](#opt-services.chhoto-url.enable). ## Backward Incompatibilities {#sec-release-25.11-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7dbea2b3d65c..be174b9e453d 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1519,6 +1519,7 @@ ./services/web-apps/castopod.nix ./services/web-apps/changedetection-io.nix ./services/web-apps/chatgpt-retrieval-plugin.nix + ./services/web-apps/chhoto-url.nix ./services/web-apps/cloudlog.nix ./services/web-apps/code-server.nix ./services/web-apps/coder.nix diff --git a/nixos/modules/services/web-apps/chhoto-url.nix b/nixos/modules/services/web-apps/chhoto-url.nix new file mode 100644 index 000000000000..8b6df98dc53e --- /dev/null +++ b/nixos/modules/services/web-apps/chhoto-url.nix @@ -0,0 +1,212 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.chhoto-url; + + environment = lib.mapAttrs ( + _: value: + if value == true then + "True" + else if value == false then + "False" + else + toString value + ) cfg.settings; +in + +{ + meta.maintainers = with lib.maintainers; [ defelo ]; + + options.services.chhoto-url = { + enable = lib.mkEnableOption "Chhoto URL"; + + package = lib.mkPackageOption pkgs "chhoto-url" { }; + + settings = lib.mkOption { + description = '' + Configuration of Chhoto URL. + See for a list of options. + ''; + example = { + port = 4567; + }; + + type = lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + str + int + bool + ]); + + options = { + db_url = lib.mkOption { + type = lib.types.path; + description = "The path of the sqlite database."; + default = "/var/lib/chhoto-url/urls.sqlite"; + }; + + port = lib.mkOption { + type = lib.types.port; + description = "The port to listen on."; + example = 4567; + }; + + cache_control_header = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "The Cache-Control header to send."; + default = null; + example = "no-cache, private"; + }; + + disable_frontend = lib.mkOption { + type = lib.types.bool; + description = "Whether to disable the frontend."; + default = false; + }; + + public_mode = lib.mkOption { + type = lib.types.bool; + description = "Whether to enable public mode."; + default = false; + apply = x: if x then "Enable" else "Disable"; + }; + + public_mode_expiry_delay = lib.mkOption { + type = lib.types.nullOr lib.types.ints.unsigned; + description = "The maximum expiry delay in seconds to force in public mode."; + default = null; + example = 3600; + }; + + redirect_method = lib.mkOption { + type = lib.types.enum [ + "TEMPORARY" + "PERMANENT" + ]; + description = "The redirect method to use."; + default = "PERMANENT"; + }; + + hash_algorithm = lib.mkOption { + type = lib.types.nullOr (lib.types.enum [ "Argon2" ]); + description = '' + The hash algorithm to use for passwords and API keys. + Set to `null` if you want to provide these secrets as plaintext. + ''; + default = null; + }; + + site_url = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "The URL under which Chhoto URL is externally reachable."; + default = null; + }; + + slug_style = lib.mkOption { + type = lib.types.enum [ + "Pair" + "UID" + ]; + description = "The slug style to use for auto-generated URLs."; + default = "Pair"; + }; + + slug_length = lib.mkOption { + type = lib.types.addCheck lib.types.int (x: x >= 4); + description = "The length of auto-generated slugs."; + default = 8; + }; + + try_longer_slugs = lib.mkOption { + type = lib.types.bool; + description = "Whether to try a longer UID upon collision."; + default = false; + }; + + allow_capital_letters = lib.mkOption { + type = lib.types.bool; + description = "Whether to allow capital letters in slugs."; + default = false; + }; + + custom_landing_directory = lib.mkOption { + type = lib.types.nullOr lib.types.path; + description = "The path of a directory which contains a custom landing page."; + default = null; + }; + }; + }; + }; + + environmentFiles = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = [ ]; + example = [ "/run/secrets/chhoto-url.env" ]; + description = '' + Files to load environment variables from in addition to [](#opt-services.chhoto-url.settings). + This is useful to avoid putting secrets into the nix store. + See for a list of options. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.chhoto-url = { + wantedBy = [ "multi-user.target" ]; + + inherit environment; + + serviceConfig = { + User = "chhoto-url"; + Group = "chhoto-url"; + DynamicUser = true; + StateDirectory = "chhoto-url"; + EnvironmentFile = cfg.environmentFiles; + + ExecStart = lib.getExe cfg.package; + + # hardening + AmbientCapabilities = ""; + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SocketBindAllow = "tcp:${toString cfg.settings.port}"; + SocketBindDeny = "any"; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; + UMask = "0077"; + }; + }; + }; +} From a2aec13e69eca9adfe6e0b74a2538cb445d6d44b Mon Sep 17 00:00:00 2001 From: Defelo Date: Sat, 12 Jul 2025 16:48:06 +0200 Subject: [PATCH 3/3] nixos/tests/chhoto-url: init (cherry picked from commit b6ebd3bac96cc9bb55c3ad337a38de49ddec875b) --- nixos/tests/all-tests.nix | 1 + nixos/tests/chhoto-url.nix | 60 ++++++++++++++++++++++++++ pkgs/by-name/ch/chhoto-url/package.nix | 6 ++- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 nixos/tests/chhoto-url.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 8dbaf86f8d68..9de59578a675 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -297,6 +297,7 @@ in cfssl = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./cfssl.nix { }; cgit = runTest ./cgit.nix; charliecloud = handleTest ./charliecloud.nix { }; + chhoto-url = runTest ./chhoto-url.nix; chromadb = runTest ./chromadb.nix; chromium = (handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./chromium.nix { }).stable or { }; chrony = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./chrony.nix { }; diff --git a/nixos/tests/chhoto-url.nix b/nixos/tests/chhoto-url.nix new file mode 100644 index 000000000000..01572b3c3216 --- /dev/null +++ b/nixos/tests/chhoto-url.nix @@ -0,0 +1,60 @@ +{ config, lib, ... }: + +{ + name = "chhoto-url"; + meta.maintainers = with lib.maintainers; [ defelo ]; + + nodes.machine = { + services.chhoto-url = { + enable = true; + settings.port = 8000; + environmentFiles = [ + (builtins.toFile "chhoto-url.env" '' + api_key=api_key + password=password + '') + ]; + }; + }; + + interactive.nodes.machine = { + services.glitchtip.listenAddress = "0.0.0.0"; + networking.firewall.allowedTCPPorts = [ 8000 ]; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 8000; + guest.port = 8000; + } + ]; + }; + + testScript = '' + import json + import re + + machine.wait_for_unit("chhoto-url.service") + machine.wait_for_open_port(8000) + + resp = json.loads(machine.succeed("curl localhost:8000/api/getconfig")) + assert resp["success"] is False + assert resp["reason"] == "No valid authentication was found" + + resp = json.loads(machine.succeed("curl -H 'X-API-Key: api_key' localhost:8000/api/getconfig")) + expected_version = "${config.nodes.machine.services.chhoto-url.package.version}" + assert resp["version"] == expected_version + + resp = json.loads(machine.succeed("curl -H 'X-API-Key: api_key' localhost:8000/api/new -d '{\"longlink\": \"https://nixos.org/\"}'")) + assert resp["success"] is True + assert (match := re.match(r"^http://localhost:8000/(.+)$", resp["shorturl"])) + slug = match[1] + + resp = machine.succeed(f"curl -i {resp["shorturl"]}") + assert (match := re.search(r"(?m)^location: (.+?)\r?$", resp)) + assert match[1] == "https://nixos.org/" + + resp = json.loads(machine.succeed(f"curl -H 'X-API-Key: api_key' localhost:8000/api/expand -d '{slug}'")) + assert resp["success"] is True + assert resp["hits"] == 1 + ''; +} diff --git a/pkgs/by-name/ch/chhoto-url/package.nix b/pkgs/by-name/ch/chhoto-url/package.nix index 73de105cf5cc..8622efa24fd0 100644 --- a/pkgs/by-name/ch/chhoto-url/package.nix +++ b/pkgs/by-name/ch/chhoto-url/package.nix @@ -2,6 +2,7 @@ lib, rustPlatform, fetchFromGitHub, + nixosTests, nix-update-script, }: @@ -30,7 +31,10 @@ rustPlatform.buildRustPackage (finalAttrs: { cp -r ${finalAttrs.src}/resources $out/share/chhoto-url/resources ''; - passthru.updateScript = nix-update-script { }; + passthru = { + tests = { inherit (nixosTests) chhoto-url; }; + updateScript = nix-update-script { }; + }; meta = { description = "Simple, blazingly fast, selfhosted URL shortener with no unnecessary features";