1import ./make-test-python.nix ({ pkgs, lib, ... }:
2
3let
4 port = 1888;
5 tlsPort = 1889;
6 anonPort = 1890;
7 password = "VERY_secret";
8 hashedPassword = "$7$101$/WJc4Mp+I+uYE9sR$o7z9rD1EYXHPwEP5GqQj6A7k4W1yVbePlb8TqNcuOLV9WNCiDgwHOB0JHC1WCtdkssqTBduBNUnUGd6kmZvDSw==";
9 topic = "test/foo";
10
11 snakeOil = pkgs.runCommand "snakeoil-certs" {
12 buildInputs = [ pkgs.gnutls.bin ];
13 caTemplate = pkgs.writeText "snakeoil-ca.template" ''
14 cn = server
15 expiration_days = -1
16 cert_signing_key
17 ca
18 '';
19 certTemplate = pkgs.writeText "snakeoil-cert.template" ''
20 cn = server
21 expiration_days = -1
22 tls_www_server
23 encryption_key
24 signing_key
25 '';
26 userCertTemplate = pkgs.writeText "snakeoil-user-cert.template" ''
27 organization = snakeoil
28 cn = client1
29 expiration_days = -1
30 tls_www_client
31 encryption_key
32 signing_key
33 '';
34 } ''
35 mkdir "$out"
36
37 certtool -p --bits 2048 --outfile "$out/ca.key"
38 certtool -s --template "$caTemplate" --load-privkey "$out/ca.key" \
39 --outfile "$out/ca.crt"
40 certtool -p --bits 2048 --outfile "$out/server.key"
41 certtool -c --template "$certTemplate" \
42 --load-ca-privkey "$out/ca.key" \
43 --load-ca-certificate "$out/ca.crt" \
44 --load-privkey "$out/server.key" \
45 --outfile "$out/server.crt"
46
47 certtool -p --bits 2048 --outfile "$out/client1.key"
48 certtool -c --template "$userCertTemplate" \
49 --load-privkey "$out/client1.key" \
50 --load-ca-privkey "$out/ca.key" \
51 --load-ca-certificate "$out/ca.crt" \
52 --outfile "$out/client1.crt"
53 '';
54
55in {
56 name = "mosquitto";
57 meta = with pkgs.lib; {
58 maintainers = with maintainers; [ pennae peterhoeg ];
59 };
60
61 nodes = let
62 client = { pkgs, ... }: {
63 environment.systemPackages = with pkgs; [ mosquitto ];
64 };
65 in {
66 server = { pkgs, ... }: {
67 networking.firewall.allowedTCPPorts = [ port tlsPort anonPort ];
68 services.mosquitto = {
69 enable = true;
70 settings = {
71 sys_interval = 1;
72 };
73 listeners = [
74 {
75 inherit port;
76 users = {
77 password_store = {
78 inherit password;
79 };
80 password_file = {
81 passwordFile = pkgs.writeText "mqtt-password" password;
82 };
83 hashed_store = {
84 inherit hashedPassword;
85 };
86 hashed_file = {
87 hashedPasswordFile = pkgs.writeText "mqtt-hashed-password" hashedPassword;
88 };
89
90 reader = {
91 inherit password;
92 acl = [
93 "read ${topic}"
94 "read $SYS/#" # so we always have something to read
95 ];
96 };
97 writer = {
98 inherit password;
99 acl = [ "write ${topic}" ];
100 };
101 };
102 }
103 {
104 port = tlsPort;
105 users.client1 = {
106 acl = [ "read $SYS/#" ];
107 };
108 settings = {
109 cafile = "${snakeOil}/ca.crt";
110 certfile = "${snakeOil}/server.crt";
111 keyfile = "${snakeOil}/server.key";
112 require_certificate = true;
113 use_identity_as_username = true;
114 };
115 }
116 {
117 port = anonPort;
118 omitPasswordAuth = true;
119 settings.allow_anonymous = true;
120 acl = [ "pattern read #" ];
121 users = {
122 anonWriter = {
123 password = "<ignored>" + password;
124 acl = [ "write ${topic}" ];
125 };
126 };
127 }
128 ];
129 };
130 };
131
132 client1 = client;
133 client2 = client;
134 };
135
136 testScript = ''
137 def mosquitto_cmd(binary, user, topic, port):
138 return (
139 "mosquitto_{} "
140 "-V mqttv311 "
141 "-h server "
142 "-p {} "
143 "-u {} "
144 "-P '${password}' "
145 "-t '{}'"
146 ).format(binary, port, user, topic)
147
148
149 def publish(args, user, topic="${topic}", port=${toString port}):
150 return "{} {}".format(mosquitto_cmd("pub", user, topic, port), args)
151
152 def subscribe(args, user, topic="${topic}", port=${toString port}):
153 return "{} -W 5 -C 1 {}".format(mosquitto_cmd("sub", user, topic, port), args)
154
155 def parallel(*fns):
156 from threading import Thread
157 threads = [ Thread(target=fn) for fn in fns ]
158 for t in threads: t.start()
159 for t in threads: t.join()
160
161
162 start_all()
163 server.wait_for_unit("mosquitto.service")
164
165 with subtest("check passwords"):
166 client1.succeed(publish("-m test", "password_store"))
167 client1.succeed(publish("-m test", "password_file"))
168 client1.succeed(publish("-m test", "hashed_store"))
169 client1.succeed(publish("-m test", "hashed_file"))
170
171 with subtest("check acl"):
172 client1.succeed(subscribe("", "reader", topic="$SYS/#"))
173 client1.fail(subscribe("", "writer", topic="$SYS/#"))
174
175 parallel(
176 lambda: client1.succeed(subscribe("-i 3688cdd7-aa07-42a4-be22-cb9352917e40", "reader")),
177 lambda: [
178 server.wait_for_console_text("3688cdd7-aa07-42a4-be22-cb9352917e40"),
179 client2.succeed(publish("-m test", "writer"))
180 ])
181
182 parallel(
183 lambda: client1.fail(subscribe("-i 24ff16a2-ae33-4a51-9098-1b417153c712", "reader")),
184 lambda: [
185 server.wait_for_console_text("24ff16a2-ae33-4a51-9098-1b417153c712"),
186 client2.succeed(publish("-m test", "reader"))
187 ])
188
189 with subtest("check tls"):
190 client1.succeed(
191 subscribe(
192 "--cafile ${snakeOil}/ca.crt "
193 "--cert ${snakeOil}/client1.crt "
194 "--key ${snakeOil}/client1.key",
195 topic="$SYS/#",
196 port=${toString tlsPort},
197 user="no_such_user"))
198
199 with subtest("check omitPasswordAuth"):
200 parallel(
201 lambda: client1.succeed(subscribe("-i fd56032c-d9cb-4813-a3b4-6be0e04c8fc3",
202 "anonReader", port=${toString anonPort})),
203 lambda: [
204 server.wait_for_console_text("fd56032c-d9cb-4813-a3b4-6be0e04c8fc3"),
205 client2.succeed(publish("-m test", "anonWriter", port=${toString anonPort}))
206 ])
207 '';
208})