From 6047878f5b8726984d263a85f7d91af484211efd Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 8 Dec 2024 17:43:32 -0800 Subject: [PATCH 1/3] maintainers: add Jaculabilis --- maintainers/maintainer-list.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index fe22dbee11342..5f1d9063e13ce 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -10110,6 +10110,12 @@ githubId = 45084216; keys = [ { fingerprint = "1BF9 8D10 E0D0 0B41 5723 5836 4C13 3A84 E646 9228"; } ]; }; + jaculabilis = { + name = "Tim Van Baak"; + email = "tim.vanbaak@gmail.com"; + github = "Jaculabilis"; + githubId = 10787844; + }; jaduff = { email = "jdduffpublic@proton.me"; github = "jaduff"; From 6a64387ced615dc99d761f2e728394995eda5046 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 8 Dec 2024 17:44:09 -0800 Subject: [PATCH 2/3] immich-public-proxy: init at 1.5.4 --- .../im/immich-public-proxy/package.nix | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 pkgs/by-name/im/immich-public-proxy/package.nix diff --git a/pkgs/by-name/im/immich-public-proxy/package.nix b/pkgs/by-name/im/immich-public-proxy/package.nix new file mode 100644 index 0000000000000..1ac698179c4b6 --- /dev/null +++ b/pkgs/by-name/im/immich-public-proxy/package.nix @@ -0,0 +1,46 @@ +{ + lib, + buildNpmPackage, + fetchFromGitHub, + nix-update-script, + nodejs, +}: +buildNpmPackage rec { + pname = "immich-public-proxy"; + version = "1.5.4"; + src = fetchFromGitHub { + owner = "alangrainger"; + repo = "immich-public-proxy"; + rev = "v${version}"; + hash = "sha256-GoAUR8s2tRHpXD/yk42u6DDvkI97XAUlF9Zsq8pb/1M="; + }; + + sourceRoot = "${src.name}/app"; + + npmDepsHash = "sha256-BN7g+31ijH8r9rsv5zzjnE8PT7ozAswoyZNJ0XqXGyw="; + + # patch in absolute nix store paths so the process doesn't need to cwd in $out + postPatch = '' + substituteInPlace src/index.ts --replace-fail \ + "const app = express()" \ + "const app = express() + // Set the views path to the nix output + app.set('views', '$out/lib/node_modules/immich-public-proxy/views')" \ + --replace-fail \ + "static('public'" \ + "static('$out/lib/node_modules/immich-public-proxy/public'" + ''; + + passthru = { + updateScript = nix-update-script { }; + }; + + meta = { + description = "Share your Immich photos and albums in a safe way without exposing your Immich instance to the public"; + homepage = "https://github.com/alangrainger/immich-public-proxy"; + license = lib.licenses.agpl3Only; + maintainers = with lib.maintainers; [ jaculabilis ]; + inherit (nodejs.meta) platforms; + mainProgram = "immich-public-proxy"; + }; +} From 62780717634cef75a7435dcf25c4d14afe4e6c1d Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 29 Nov 2024 11:26:07 -0800 Subject: [PATCH 3/3] nixos/immich-public-proxy: init module --- .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + .../services/web-apps/immich-public-proxy.nix | 98 ++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/web-apps/immich-public-proxy.nix | 105 ++++++++++++++++++ .../im/immich-public-proxy/package.nix | 4 + 6 files changed, 211 insertions(+) create mode 100644 nixos/modules/services/web-apps/immich-public-proxy.nix create mode 100644 nixos/tests/web-apps/immich-public-proxy.nix diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index f974d5836b174..20229a34bdebf 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -77,6 +77,8 @@ - [Actual Budget](https://actualbudget.org/), a local-first personal finance app. Available as [services.actual](#opt-services.actual.enable). +- [immich-public-proxy](https://github.com/alangrainger/immich-public-proxy), a proxy for sharing Immich albums without exposing the Immich API. Available as [services.immich-public-proxy](#opt-services.immich-public-proxy.enable). + - [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable). - [nvidia-gpu](https://github.com/utkuozdemir/nvidia_gpu_exporter), a Prometheus exporter that scrapes `nvidia-smi` for GPU metrics. Available as [services.prometheus.exporters.nvidia-gpu](#opt-services.prometheus.exporters.nvidia-gpu.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b91d90994feaa..7de85e13c615c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1484,6 +1484,7 @@ ./services/web-apps/icingaweb2/module-monitoring.nix ./services/web-apps/ifm.nix ./services/web-apps/immich.nix + ./services/web-apps/immich-public-proxy.nix ./services/web-apps/invidious.nix ./services/web-apps/invoiceplane.nix ./services/web-apps/isso.nix diff --git a/nixos/modules/services/web-apps/immich-public-proxy.nix b/nixos/modules/services/web-apps/immich-public-proxy.nix new file mode 100644 index 0000000000000..85238e1cbacf3 --- /dev/null +++ b/nixos/modules/services/web-apps/immich-public-proxy.nix @@ -0,0 +1,98 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.immich-public-proxy; + format = pkgs.formats.json { }; + inherit (lib) + types + mkIf + mkOption + mkEnableOption + ; +in +{ + options.services.immich-public-proxy = { + enable = mkEnableOption "Immich Public Proxy"; + package = lib.mkPackageOption pkgs "immich-public-proxy" { }; + + immichUrl = mkOption { + type = types.str; + description = "URL of the Immich instance"; + }; + + port = mkOption { + type = types.port; + default = 3000; + description = "The port that IPP will listen on."; + }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the IPP port in the firewall"; + }; + + settings = mkOption { + type = types.submodule { + freeformType = format.type; + }; + default = { }; + description = '' + Configuration for IPP. See for options and defaults. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.immich-public-proxy = { + description = "Immich public proxy for sharing albums publicly without exposing your Immich instance"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + IMMICH_URL = cfg.immichUrl; + IPP_PORT = builtins.toString cfg.port; + IPP_CONFIG = "${format.generate "config.json" cfg.settings}"; + }; + serviceConfig = { + ExecStart = lib.getExe cfg.package; + SyslogIdentifier = "ipp"; + User = "ipp"; + Group = "ipp"; + DynamicUser = true; + Type = "simple"; + Restart = "on-failure"; + RestartSec = 3; + + # Hardening + CapabilityBoundingSet = ""; + NoNewPrivileges = true; + PrivateUsers = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateMounts = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + + meta.maintainers = with lib.maintainers; [ jaculabilis ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 2952c67d19c28..c89c8b6b541a8 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -467,6 +467,7 @@ in { ifm = handleTest ./ifm.nix {}; iftop = handleTest ./iftop.nix {}; immich = handleTest ./web-apps/immich.nix {}; + immich-public-proxy = handleTest ./web-apps/immich-public-proxy.nix {}; incron = handleTest ./incron.nix {}; incus = pkgs.recurseIntoAttrs (handleTest ./incus { lts = false; inherit system pkgs; }); incus-lts = pkgs.recurseIntoAttrs (handleTest ./incus { inherit system pkgs; }); diff --git a/nixos/tests/web-apps/immich-public-proxy.nix b/nixos/tests/web-apps/immich-public-proxy.nix new file mode 100644 index 0000000000000..5f2034b29442e --- /dev/null +++ b/nixos/tests/web-apps/immich-public-proxy.nix @@ -0,0 +1,105 @@ +import ../make-test-python.nix ( + { pkgs, lib, ... }: + { + name = "immich-public-proxy"; + + nodes.machine = + { pkgs, ... }@args: + { + environment.systemPackages = [ + pkgs.imagemagick + pkgs.immich-cli + ]; + services.immich = { + enable = true; + port = 2283; + # disable a lot of features that aren't needed for this test + machine-learning.enable = false; + settings = { + backup.database.enabled = false; + machineLearning.enabled = false; + map.enabled = false; + reverseGeocoding.enabled = false; + metadata.faces.import = false; + newVersionCheck.enabled = false; + notifications.smtp.enabled = false; + }; + }; + services.immich-public-proxy = { + enable = true; + immichUrl = "http://localhost:2283"; + port = 8002; + settings.ipp.responseHeaders."X-NixOS" = "Rules"; + }; + }; + + testScript = '' + import json + + machine.wait_for_unit("immich-server.service") + machine.wait_for_unit("immich-public-proxy.service") + machine.wait_for_open_port(2283) + machine.wait_for_open_port(8002) + + # The proxy should be up + machine.succeed("curl -sf http://localhost:8002") + + # Verify the static assets are served + machine.succeed("curl -sf http://localhost:8002/robots.txt") + machine.succeed("curl -sf http://localhost:8002/share/static/style.css") + + # Check that the response header in the settings is sent + res = machine.succeed(""" + curl -sD - http://localhost:8002 -o /dev/null + """) + assert "x-nixos: rules" in res.lower(), res + + # Log in to Immich and create an access key + machine.succeed(""" + curl -sf --json '{ "email": "test@example.com", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up + """) + res = machine.succeed(""" + curl -sf --json '{ "email": "test@example.com", "password": "admin" }' http://localhost:2283/api/auth/login + """) + token = json.loads(res)['accessToken'] + res = machine.succeed(""" + curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys + """ % token) + key = json.loads(res)['secret'] + machine.succeed(f"immich login http://localhost:2283/api {key}") + res = machine.succeed("immich server-info") + print(res) + + # Upload some blank images to a new album + # If there's only one image, the proxy serves the image directly + machine.succeed("magick -size 800x600 canvas:white /tmp/white.png") + machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/white.png") + machine.succeed("magick -size 800x600 canvas:black /tmp/black.png") + machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/black.png") + res = machine.succeed("immich server-info") + print(res) + + # Get the new album id + res = machine.succeed(""" + curl -sf -H 'Cookie: immich_access_token=%s' http://localhost:2283/api/albums + """ % token) + album_id = json.loads(res)[0]['id'] + + # Create a shared link + res = machine.succeed(""" + curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "albumId": "%s", "type": "ALBUM" }' http://localhost:2283/api/shared-links + """ % (token, album_id)) + share_key = json.loads(res)['key'] + + # Access the share + machine.succeed(""" + curl -sf http://localhost:2283/share/%s + """ % share_key) + + # Access the share through the proxy + machine.succeed(""" + curl -sf http://localhost:8002/share/%s + """ % share_key) + ''; + } +) diff --git a/pkgs/by-name/im/immich-public-proxy/package.nix b/pkgs/by-name/im/immich-public-proxy/package.nix index 1ac698179c4b6..36335b3959603 100644 --- a/pkgs/by-name/im/immich-public-proxy/package.nix +++ b/pkgs/by-name/im/immich-public-proxy/package.nix @@ -3,6 +3,7 @@ buildNpmPackage, fetchFromGitHub, nix-update-script, + nixosTests, nodejs, }: buildNpmPackage rec { @@ -32,6 +33,9 @@ buildNpmPackage rec { ''; passthru = { + tests = { + inherit (nixosTests) immich-public-proxy; + }; updateScript = nix-update-script { }; };