189 lines
5.7 KiB
Nix
189 lines
5.7 KiB
Nix
{
|
|
pkgs,
|
|
config,
|
|
flake-inputs,
|
|
...
|
|
}:
|
|
let
|
|
domain = "ntfy.${config.services.nginx.domain}";
|
|
in
|
|
{
|
|
imports = [ ./downstream-module.nix ];
|
|
|
|
networking.firewall.allowedTCPPorts = [
|
|
80
|
|
443
|
|
];
|
|
|
|
services.ntfy-sh = {
|
|
enable = true;
|
|
package = flake-inputs.nixpkgs-unstable.legacyPackages.${pkgs.system}.ntfy-sh;
|
|
|
|
environmentFile = config.sops.secrets."ntfy/users".path;
|
|
|
|
settings = {
|
|
base-url = "https://${domain}";
|
|
listen-http = "127.0.0.1:2586";
|
|
behind-proxy = true;
|
|
|
|
# Paths
|
|
attachment-cache-dir = "/var/lib/ntfy-sh/attachments";
|
|
cache-file = "/var/lib/ntfy-sh/cache-file.db";
|
|
auth-file = "/var/lib/ntfy-sh/user.db";
|
|
auth-default-access = "deny-all";
|
|
auth-access = [ "*:local-*:wo" ];
|
|
|
|
# Don't want to host the front-end
|
|
web-root = "disable";
|
|
};
|
|
};
|
|
|
|
services.nginx.virtualHosts."ntfy.${config.services.nginx.domain}" = {
|
|
forceSSL = true;
|
|
useACMEHost = "tlater.net";
|
|
enableHSTS = true;
|
|
|
|
locations."/" = {
|
|
proxyWebsockets = true;
|
|
proxyPass = "http://${config.services.ntfy-sh.settings.listen-http}";
|
|
extraConfig = ''
|
|
client_max_body_size 0; # Stream request body to backend
|
|
'';
|
|
};
|
|
|
|
# Don't allow writing to topics with the local prefix, *including*
|
|
# webhook writes, since they are set to write-only access from
|
|
# anyone.
|
|
locations."/local" = {
|
|
proxyWebsockets = true;
|
|
proxyPass = "http://${config.services.ntfy-sh.settings.listen-http}";
|
|
extraConfig = ''
|
|
client_max_body_size 0; # Stream request body to backend
|
|
|
|
limit_except GET OPTIONS {
|
|
deny all;
|
|
}
|
|
|
|
location ~ /trigger$ {
|
|
deny all;
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
|
|
sops.secrets."ntfy/users" = { };
|
|
|
|
serviceTests = {
|
|
testNtfyConfig = pkgs.testers.runNixOSTest {
|
|
name = "test-ntfy-config";
|
|
|
|
node.specialArgs = { inherit flake-inputs; };
|
|
nodes = {
|
|
testHost =
|
|
{ lib, ... }:
|
|
{
|
|
imports = [
|
|
./.
|
|
../../nginx
|
|
../../../modules/serviceTests/mocks.nix
|
|
];
|
|
|
|
services.nginx.domain = "testHost";
|
|
|
|
# Don't care for testing SSL here
|
|
services.nginx.virtualHosts."ntfy.testHost" = {
|
|
forceSSL = lib.mkForce false;
|
|
enableHSTS = lib.mkForce false;
|
|
};
|
|
};
|
|
|
|
client =
|
|
{ pkgs, ... }:
|
|
{
|
|
environment.systemPackages = [ pkgs.curl ];
|
|
networking.hosts."192.168.1.2" = [ "ntfy.testHost" ];
|
|
};
|
|
};
|
|
|
|
testScript = ''
|
|
import json
|
|
import time
|
|
from contextlib import contextmanager
|
|
|
|
def read_client_messages():
|
|
client.wait_for_unit("messages.service")
|
|
messages = [json.loads(line) for line in client.succeed("cat messages").split()]
|
|
client.succeed("systemctl stop messages.service")
|
|
client.succeed("rm messages")
|
|
|
|
print(messages)
|
|
|
|
return messages
|
|
|
|
@contextmanager
|
|
def client_subscribe(topic: str, timeout: int = 2):
|
|
systemd_invocation = [
|
|
"systemd-run",
|
|
"--unit messages",
|
|
"--property=Type=oneshot",
|
|
"--property=SuccessExitStatus=28",
|
|
"--remain-after-exit",
|
|
"--setenv=PATH=$PATH",
|
|
"--same-dir",
|
|
"--no-block",
|
|
"/bin/sh -c"
|
|
]
|
|
|
|
curl = [
|
|
"curl",
|
|
"--silent",
|
|
"--show-error",
|
|
f"--max-time {timeout}",
|
|
"-u tlater:insecure",
|
|
f"http://ntfy.testHost/{topic}/json",
|
|
"> messages"
|
|
]
|
|
|
|
client.succeed(f'{" ".join(systemd_invocation)} "{" ".join(curl)}"')
|
|
|
|
# Give some slack so the host doesn't send messages before
|
|
# we're listening
|
|
time.sleep(1)
|
|
|
|
yield
|
|
|
|
|
|
start_all()
|
|
testHost.wait_for_unit("ntfy-sh.service")
|
|
client.wait_until_succeeds("curl http://ntfy.testHost")
|
|
|
|
with subtest("subscribing and writing to local topics works"):
|
|
with client_subscribe("local-test"):
|
|
testHost.succeed("curl --fail --silent --show-error --data test http://127.0.0.1:2586/local-test")
|
|
|
|
messages = read_client_messages()
|
|
t.assertEqual(len(messages), 2)
|
|
t.assertEqual(messages[1].get("message"), "test")
|
|
|
|
with subtest("writing to non-local topics without auth fails"):
|
|
testHost.fail("curl --fail --silent --show-error --data test http://127.0.0.1:2586/test")
|
|
|
|
with subtest("writing to *any* topics from outside localhost fails"):
|
|
client.fail("curl --fail --silent --show-error --data test http://ntfy.testHost/test")
|
|
client.fail("curl --fail --silent --show-error --data test http://ntfy.testHost/local-test")
|
|
# GET requests work by default because websocket shenanigans
|
|
client.fail("curl --fail --silent --show-error http://ntfy.testHost/local-test/trigger?message=test")
|
|
|
|
with subtest("authenticated messaging works from outside localhost"):
|
|
with client_subscribe("test", 10):
|
|
client.succeed("curl -u tlater:insecure --fail --silent --show-error --data test http://ntfy.testHost/test")
|
|
client.succeed("curl -u tlater:insecure --fail --silent --show-error http://ntfy.testHost/test/trigger?message=test2")
|
|
|
|
messages = read_client_messages()
|
|
t.assertEqual(len(messages), 3)
|
|
t.assertEqual(messages[1].get("message"), "test")
|
|
t.assertEqual(messages[2].get("message"), "test2")
|
|
'';
|
|
};
|
|
};
|
|
}
|