nixos/chhoto-url: init module (#424630)

Sandro d69cbe23 3875bf60

Changed files
+323
nixos
doc
manual
release-notes
modules
services
web-apps
tests
pkgs
by-name
ch
chhoto-url
+2
nixos/doc/manual/release-notes/rl-2511.section.md
···
- [tlsrpt-reporter], an application suite to generate and deliver TLSRPT reports. Available as [services.tlsrpt](#opt-services.tlsrpt.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).
+
- [Broadcast Box](https://github.com/Glimesh/broadcast-box), a WebRTC broadcast server. Available as [services.broadcast-box](options.html#opt-services.broadcast-box.enable).
- Docker now defaults to 28.x, because version 27.x stopped receiving security updates and bug fixes after [May 2, 2025](https://github.com/moby/moby/pull/49910).
+1
nixos/modules/module-list.nix
···
./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
+212
nixos/modules/services/web-apps/chhoto-url.nix
···
+
{
+
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 <https://github.com/SinTan1729/chhoto-url/blob/main/compose.yaml> 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 <https://github.com/SinTan1729/chhoto-url/blob/main/compose.yaml> 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";
+
};
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
cfssl = runTestOn [ "aarch64-linux" "x86_64-linux" ] ./cfssl.nix;
cgit = runTest ./cgit.nix;
charliecloud = runTest ./charliecloud.nix;
+
chhoto-url = runTest ./chhoto-url.nix;
chromadb = runTest ./chromadb.nix;
chromium = (handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./chromium.nix { }).stable or { };
chrony = runTestOn [ "aarch64-linux" "x86_64-linux" ] ./chrony.nix;
+60
nixos/tests/chhoto-url.nix
···
+
{ 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
+
'';
+
}
+47
pkgs/by-name/ch/chhoto-url/package.nix
···
+
{
+
lib,
+
rustPlatform,
+
fetchFromGitHub,
+
nixosTests,
+
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 = {
+
tests = { inherit (nixosTests) chhoto-url; };
+
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";
+
};
+
})