]>
Commit | Line | Data |
---|---|---|
02bbf9eb | 1 | #!/usr/bin/env python |
56d68fad | 2 | import os.path |
02bbf9eb | 3 | |
80dbd7d2 | 4 | import base64 |
56d68fad | 5 | import json |
02bbf9eb | 6 | import requests |
5e40d2a5 RG |
7 | import socket |
8 | import time | |
630eb526 | 9 | from dnsdisttests import DNSDistTest, pickAvailablePort |
02bbf9eb | 10 | |
72bf42cd RG |
11 | class APITestsBase(DNSDistTest): |
12 | __test__ = False | |
57af5b7c | 13 | _webTimeout = 5.0 |
630eb526 | 14 | _webServerPort = pickAvailablePort() |
02bbf9eb | 15 | _webServerBasicAuthPassword = 'secret' |
2c0392a5 | 16 | _webServerBasicAuthPasswordHashed = '$scrypt$ln=10,p=1,r=8$6DKLnvUYEeXWh3JNOd3iwg==$kSrhdHaRbZ7R74q3lGBqO1xetgxRxhmWzYJ2Qvfm7JM=' |
02bbf9eb | 17 | _webServerAPIKey = 'apisecret' |
2c0392a5 | 18 | _webServerAPIKeyHashed = '$scrypt$ln=10,p=1,r=8$9v8JxDfzQVyTpBkTbkUqYg==$bDQzAOHeK1G9UvTPypNhrX48w974ZXbFPtRKS34+aso=' |
412f99ef | 19 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
72bf42cd RG |
20 | _config_template = """ |
21 | setACL({"127.0.0.1/32", "::1/128"}) | |
22 | newServer{address="127.0.0.1:%s", pool={'', 'mypool'}} | |
23 | webserver("127.0.0.1:%s") | |
cfe95ada | 24 | setWebserverConfig({password="%s", apiKey="%s"}) |
72bf42cd | 25 | """ |
14325153 RG |
26 | _expectedMetrics = ['responses', 'servfail-responses', 'queries', 'acl-drops', |
27 | 'frontend-noerror', 'frontend-nxdomain', 'frontend-servfail', | |
28 | 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts', | |
29 | 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1', | |
30 | 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000', | |
31 | 'latency-slow', 'latency-sum', 'latency-count', 'latency-avg100', 'latency-avg1000', | |
89b29a4d RG |
32 | 'latency-avg10000', 'latency-avg1000000', 'latency-tcp-avg100', 'latency-tcp-avg1000', |
33 | 'latency-tcp-avg10000', 'latency-tcp-avg1000000', 'latency-dot-avg100', 'latency-dot-avg1000', | |
34 | 'latency-dot-avg10000', 'latency-dot-avg1000000', 'latency-doh-avg100', 'latency-doh-avg1000', | |
5057253a RG |
35 | 'latency-doh-avg10000', 'latency-doh-avg1000000', 'latency-doq-avg100', 'latency-doq-avg1000', |
36 | 'latency-doq-avg10000', 'latency-doq-avg1000000','uptime', 'real-memory-usage', 'noncompliant-queries', | |
14325153 RG |
37 | 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits', |
38 | 'cache-misses', 'cpu-iowait', 'cpu-steal', 'cpu-sys-msec', 'cpu-user-msec', 'fd-usage', 'dyn-blocked', | |
39 | 'dyn-block-nmg-size', 'rule-servfail', 'rule-truncated', 'security-status', | |
40 | 'udp-in-csum-errors', 'udp-in-errors', 'udp-noport-errors', 'udp-recvbuf-errors', 'udp-sndbuf-errors', | |
41 | 'udp6-in-errors', 'udp6-recvbuf-errors', 'udp6-sndbuf-errors', 'udp6-noport-errors', 'udp6-in-csum-errors', | |
f089d71c | 42 | 'doh-query-pipe-full', 'doh-response-pipe-full', 'doq-response-pipe-full', 'proxy-protocol-invalid', 'tcp-listen-overflows', |
14325153 RG |
43 | 'outgoing-doh-query-pipe-full', 'tcp-query-pipe-full', 'tcp-cross-protocol-query-pipe-full', |
44 | 'tcp-cross-protocol-response-pipe-full'] | |
648edcba RG |
45 | _verboseMode = True |
46 | ||
47 | @classmethod | |
48 | def setUpClass(cls): | |
49 | cls.startResponders() | |
50 | cls.startDNSDist() | |
51 | cls.setUpSockets() | |
52 | cls.waitForTCPSocket('127.0.0.1', cls._webServerPort) | |
53 | print("Launching tests..") | |
72bf42cd RG |
54 | |
55 | class TestAPIBasics(APITestsBase): | |
56 | ||
55afa518 RG |
57 | # paths accessible using the API key only |
58 | _apiOnlyPaths = ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics'] | |
59 | # paths accessible using an API key or basic auth | |
60 | _statsPaths = [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost'] | |
02bbf9eb RG |
61 | # paths accessible using basic auth only (list not exhaustive) |
62 | _basicOnlyPaths = ['/', '/index.html'] | |
72bf42cd | 63 | __test__ = True |
02bbf9eb RG |
64 | |
65 | def testBasicAuth(self): | |
66 | """ | |
67 | API: Basic Authentication | |
68 | """ | |
55afa518 | 69 | for path in self._basicOnlyPaths + self._statsPaths: |
02bbf9eb | 70 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path |
80dbd7d2 | 71 | r = requests.get(url, auth=('whatever', "evilsecret"), timeout=self._webTimeout) |
4bfebc93 | 72 | self.assertEqual(r.status_code, 401) |
02bbf9eb RG |
73 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) |
74 | self.assertTrue(r) | |
4bfebc93 | 75 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
76 | |
77 | def testXAPIKey(self): | |
78 | """ | |
79 | API: X-Api-Key | |
80 | """ | |
81 | headers = {'x-api-key': self._webServerAPIKey} | |
55afa518 | 82 | for path in self._apiOnlyPaths + self._statsPaths: |
02bbf9eb RG |
83 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path |
84 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
85 | self.assertTrue(r) | |
4bfebc93 | 86 | self.assertEqual(r.status_code, 200) |
02bbf9eb | 87 | |
80dbd7d2 CHB |
88 | def testWrongXAPIKey(self): |
89 | """ | |
90 | API: Wrong X-Api-Key | |
91 | """ | |
92 | headers = {'x-api-key': "evilapikey"} | |
93 | for path in self._apiOnlyPaths + self._statsPaths: | |
94 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
95 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
4bfebc93 | 96 | self.assertEqual(r.status_code, 401) |
c563cbe5 | 97 | |
02bbf9eb RG |
98 | def testBasicAuthOnly(self): |
99 | """ | |
100 | API: Basic Authentication Only | |
101 | """ | |
102 | headers = {'x-api-key': self._webServerAPIKey} | |
103 | for path in self._basicOnlyPaths: | |
104 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
105 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
4bfebc93 | 106 | self.assertEqual(r.status_code, 401) |
02bbf9eb | 107 | |
55afa518 RG |
108 | def testAPIKeyOnly(self): |
109 | """ | |
110 | API: API Key Only | |
111 | """ | |
112 | for path in self._apiOnlyPaths: | |
113 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
114 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
4bfebc93 | 115 | self.assertEqual(r.status_code, 401) |
55afa518 | 116 | |
02bbf9eb RG |
117 | def testServersLocalhost(self): |
118 | """ | |
119 | API: /api/v1/servers/localhost | |
120 | """ | |
121 | headers = {'x-api-key': self._webServerAPIKey} | |
122 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost' | |
123 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
124 | self.assertTrue(r) | |
4bfebc93 | 125 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
126 | self.assertTrue(r.json()) |
127 | content = r.json() | |
128 | ||
4bfebc93 | 129 | self.assertEqual(content['daemon_type'], 'dnsdist') |
02bbf9eb | 130 | |
f8a222ac RG |
131 | rule_groups = ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules', 'rules'] |
132 | for key in ['version', 'acl', 'local', 'servers', 'frontends', 'pools'] + rule_groups: | |
02bbf9eb RG |
133 | self.assertIn(key, content) |
134 | ||
d18eab67 CH |
135 | for rule_group in rule_groups: |
136 | for rule in content[rule_group]: | |
f8a222ac | 137 | for key in ['id', 'creationOrder', 'matches', 'rule', 'action', 'uuid']: |
d18eab67 | 138 | self.assertIn(key, rule) |
f8a222ac | 139 | for key in ['id', 'creationOrder', 'matches']: |
d18eab67 | 140 | self.assertTrue(rule[key] >= 0) |
4ace9fe8 | 141 | |
02bbf9eb RG |
142 | for server in content['servers']: |
143 | for key in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit', | |
6a78f305 | 144 | 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors', |
d70f95ac | 145 | 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse', |
0f06daf9 | 146 | 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections', |
2a5cfdfb | 147 | 'tcpNewConnections', 'tcpReusedConnections', 'tlsResumptions', 'tcpAvgQueriesPerConnection', |
da73e6b3 | 148 | 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol', 'healthCheckFailures', 'healthCheckFailuresParsing', 'healthCheckFailuresTimeout', 'healthCheckFailuresNetwork', 'healthCheckFailuresMismatch', 'healthCheckFailuresInvalid']: |
02bbf9eb RG |
149 | self.assertIn(key, server) |
150 | ||
151 | for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds', | |
d70f95ac | 152 | 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']: |
02bbf9eb RG |
153 | self.assertTrue(server[key] >= 0) |
154 | ||
155 | self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN']) | |
156 | ||
157 | for frontend in content['frontends']: | |
fd23f5de | 158 | for key in ['id', 'address', 'udp', 'tcp', 'type', 'queries', 'nonCompliantQueries']: |
02bbf9eb RG |
159 | self.assertIn(key, frontend) |
160 | ||
fd23f5de | 161 | for key in ['id', 'queries', 'nonCompliantQueries']: |
02bbf9eb RG |
162 | self.assertTrue(frontend[key] >= 0) |
163 | ||
4ace9fe8 | 164 | for pool in content['pools']: |
27792330 | 165 | for key in ['id', 'name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']: |
4ace9fe8 RG |
166 | self.assertIn(key, pool) |
167 | ||
27792330 | 168 | for key in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']: |
4ace9fe8 RG |
169 | self.assertTrue(pool[key] >= 0) |
170 | ||
14325153 RG |
171 | stats = content['statistics'] |
172 | for key in self._expectedMetrics: | |
173 | self.assertIn(key, stats) | |
174 | self.assertTrue(stats[key] >= 0) | |
175 | for key in stats: | |
176 | self.assertIn(key, self._expectedMetrics) | |
177 | ||
bc6e4c3a RG |
178 | def testServersLocalhostPool(self): |
179 | """ | |
180 | API: /api/v1/servers/localhost/pool?name=mypool | |
181 | """ | |
182 | headers = {'x-api-key': self._webServerAPIKey} | |
183 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/pool?name=mypool' | |
184 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
185 | self.assertTrue(r) | |
186 | self.assertEqual(r.status_code, 200) | |
187 | self.assertTrue(r.json()) | |
188 | content = r.json() | |
189 | ||
190 | self.assertIn('stats', content) | |
191 | self.assertIn('servers', content) | |
192 | ||
193 | for key in ['name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']: | |
194 | self.assertIn(key, content['stats']) | |
195 | ||
196 | for key in ['cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']: | |
197 | self.assertTrue(content['stats'][key] >= 0) | |
198 | ||
199 | for server in content['servers']: | |
200 | for key in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit', | |
201 | 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors', | |
d70f95ac | 202 | 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse', |
bc6e4c3a RG |
203 | 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections', |
204 | 'tcpNewConnections', 'tcpReusedConnections', 'tcpAvgQueriesPerConnection', | |
7f7167e0 | 205 | 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol']: |
bc6e4c3a RG |
206 | self.assertIn(key, server) |
207 | ||
208 | for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds', | |
d70f95ac | 209 | 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']: |
bc6e4c3a RG |
210 | self.assertTrue(server[key] >= 0) |
211 | ||
212 | self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN']) | |
213 | ||
00566cbf PL |
214 | def testServersIDontExist(self): |
215 | """ | |
ef2ea4bf | 216 | API: /api/v1/servers/idonotexist (should be 404) |
00566cbf PL |
217 | """ |
218 | headers = {'x-api-key': self._webServerAPIKey} | |
ef2ea4bf | 219 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/idonotexist' |
00566cbf | 220 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
4bfebc93 | 221 | self.assertEqual(r.status_code, 404) |
00566cbf | 222 | |
02bbf9eb RG |
223 | def testServersLocalhostConfig(self): |
224 | """ | |
225 | API: /api/v1/servers/localhost/config | |
226 | """ | |
227 | headers = {'x-api-key': self._webServerAPIKey} | |
228 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config' | |
229 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
230 | self.assertTrue(r) | |
4bfebc93 | 231 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
232 | self.assertTrue(r.json()) |
233 | content = r.json() | |
234 | values = {} | |
235 | for entry in content: | |
236 | for key in ['type', 'name', 'value']: | |
237 | self.assertIn(key, entry) | |
238 | ||
4bfebc93 | 239 | self.assertEqual(entry['type'], 'ConfigSetting') |
02bbf9eb RG |
240 | values[entry['name']] = entry['value'] |
241 | ||
242 | for key in ['acl', 'control-socket', 'ecs-override', 'ecs-source-prefix-v4', | |
243 | 'ecs-source-prefix-v6', 'fixup-case', 'max-outstanding', 'server-policy', | |
244 | 'stale-cache-entries-ttl', 'tcp-recv-timeout', 'tcp-send-timeout', | |
245 | 'truncate-tc', 'verbose', 'verbose-health-checks']: | |
246 | self.assertIn(key, values) | |
247 | ||
248 | for key in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout', | |
249 | 'tcp-send-timeout']: | |
250 | self.assertTrue(values[key] >= 0) | |
251 | ||
252 | self.assertTrue(values['ecs-source-prefix-v4'] >= 0 and values['ecs-source-prefix-v4'] <= 32) | |
253 | self.assertTrue(values['ecs-source-prefix-v6'] >= 0 and values['ecs-source-prefix-v6'] <= 128) | |
254 | ||
56d68fad RG |
255 | def testServersLocalhostConfigAllowFrom(self): |
256 | """ | |
257 | API: /api/v1/servers/localhost/config/allow-from | |
258 | """ | |
259 | headers = {'x-api-key': self._webServerAPIKey} | |
260 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
261 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
262 | self.assertTrue(r) | |
4bfebc93 | 263 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
264 | self.assertTrue(r.json()) |
265 | content = r.json() | |
266 | for key in ['type', 'name', 'value']: | |
267 | self.assertIn(key, content) | |
268 | ||
4bfebc93 CH |
269 | self.assertEqual(content['name'], 'allow-from') |
270 | self.assertEqual(content['type'], 'ConfigSetting') | |
078efd26 RG |
271 | acl = content['value'] |
272 | expectedACL = ["127.0.0.1/32", "::1/128"] | |
273 | acl.sort() | |
274 | expectedACL.sort() | |
4bfebc93 | 275 | self.assertEqual(acl, expectedACL) |
56d68fad RG |
276 | |
277 | def testServersLocalhostConfigAllowFromPut(self): | |
278 | """ | |
279 | API: PUT /api/v1/servers/localhost/config/allow-from (should be refused) | |
280 | ||
281 | The API is read-only by default, so this should be refused | |
282 | """ | |
283 | newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"] | |
284 | payload = json.dumps({"name": "allow-from", | |
285 | "type": "ConfigSetting", | |
286 | "value": newACL}) | |
287 | headers = {'x-api-key': self._webServerAPIKey} | |
288 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
289 | r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload) | |
290 | self.assertFalse(r) | |
4bfebc93 | 291 | self.assertEqual(r.status_code, 405) |
56d68fad | 292 | |
02bbf9eb RG |
293 | def testServersLocalhostStatistics(self): |
294 | """ | |
295 | API: /api/v1/servers/localhost/statistics | |
296 | """ | |
297 | headers = {'x-api-key': self._webServerAPIKey} | |
298 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/statistics' | |
299 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
300 | self.assertTrue(r) | |
4bfebc93 | 301 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
302 | self.assertTrue(r.json()) |
303 | content = r.json() | |
304 | values = {} | |
305 | for entry in content: | |
306 | self.assertIn('type', entry) | |
307 | self.assertIn('name', entry) | |
308 | self.assertIn('value', entry) | |
4bfebc93 | 309 | self.assertEqual(entry['type'], 'StatisticItem') |
02bbf9eb RG |
310 | values[entry['name']] = entry['value'] |
311 | ||
14325153 | 312 | for key in self._expectedMetrics: |
02bbf9eb RG |
313 | self.assertIn(key, values) |
314 | self.assertTrue(values[key] >= 0) | |
315 | ||
dd46e5e3 | 316 | for key in values: |
14325153 | 317 | self.assertIn(key, self._expectedMetrics) |
dd46e5e3 | 318 | |
02bbf9eb RG |
319 | def testJsonstatStats(self): |
320 | """ | |
321 | API: /jsonstat?command=stats | |
322 | """ | |
323 | headers = {'x-api-key': self._webServerAPIKey} | |
324 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=stats' | |
325 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
326 | self.assertTrue(r) | |
4bfebc93 | 327 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
328 | self.assertTrue(r.json()) |
329 | content = r.json() | |
330 | ||
14325153 | 331 | for key in self._expectedMetrics: |
02bbf9eb RG |
332 | self.assertIn(key, content) |
333 | self.assertTrue(content[key] >= 0) | |
334 | ||
335 | def testJsonstatDynblocklist(self): | |
336 | """ | |
337 | API: /jsonstat?command=dynblocklist | |
338 | """ | |
339 | headers = {'x-api-key': self._webServerAPIKey} | |
340 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=dynblocklist' | |
341 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
342 | self.assertTrue(r) | |
4bfebc93 | 343 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
344 | |
345 | content = r.json() | |
346 | ||
347 | if content: | |
477c86a0 | 348 | for key in ['reason', 'seconds', 'blocks', 'action']: |
02bbf9eb RG |
349 | self.assertIn(key, content) |
350 | ||
351 | for key in ['blocks']: | |
352 | self.assertTrue(content[key] >= 0) | |
56d68fad | 353 | |
72bf42cd RG |
354 | class TestAPIServerDown(APITestsBase): |
355 | __test__ = True | |
36927800 RG |
356 | _config_template = """ |
357 | setACL({"127.0.0.1/32", "::1/128"}) | |
358 | newServer{address="127.0.0.1:%s"} | |
359 | getServer(0):setDown() | |
fa7e8b5d | 360 | webserver("127.0.0.1:%s") |
cfe95ada | 361 | setWebserverConfig({password="%s", apiKey="%s"}) |
36927800 RG |
362 | """ |
363 | ||
364 | def testServerDownNoLatencyLocalhost(self): | |
365 | """ | |
366 | API: /api/v1/servers/localhost, no latency for a down server | |
367 | """ | |
368 | headers = {'x-api-key': self._webServerAPIKey} | |
369 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost' | |
370 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
371 | self.assertTrue(r) | |
4bfebc93 | 372 | self.assertEqual(r.status_code, 200) |
36927800 RG |
373 | self.assertTrue(r.json()) |
374 | content = r.json() | |
375 | ||
4bfebc93 | 376 | self.assertEqual(content['servers'][0]['latency'], None) |
7f7167e0 | 377 | self.assertEqual(content['servers'][0]['tcpLatency'], None) |
36927800 | 378 | |
72bf42cd RG |
379 | class TestAPIWritable(APITestsBase): |
380 | __test__ = True | |
56d68fad | 381 | _APIWriteDir = '/tmp' |
412f99ef | 382 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_APIWriteDir'] |
56d68fad RG |
383 | _config_template = """ |
384 | setACL({"127.0.0.1/32", "::1/128"}) | |
385 | newServer{address="127.0.0.1:%s"} | |
fa7e8b5d | 386 | webserver("127.0.0.1:%s") |
cfe95ada | 387 | setWebserverConfig({password="%s", apiKey="%s"}) |
56d68fad RG |
388 | setAPIWritable(true, "%s") |
389 | """ | |
390 | ||
391 | def testSetACL(self): | |
392 | """ | |
393 | API: Set ACL | |
394 | """ | |
395 | headers = {'x-api-key': self._webServerAPIKey} | |
396 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
397 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
398 | self.assertTrue(r) | |
4bfebc93 | 399 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
400 | self.assertTrue(r.json()) |
401 | content = r.json() | |
078efd26 RG |
402 | acl = content['value'] |
403 | expectedACL = ["127.0.0.1/32", "::1/128"] | |
404 | acl.sort() | |
405 | expectedACL.sort() | |
4bfebc93 | 406 | self.assertEqual(acl, expectedACL) |
56d68fad RG |
407 | |
408 | newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"] | |
409 | payload = json.dumps({"name": "allow-from", | |
410 | "type": "ConfigSetting", | |
411 | "value": newACL}) | |
412 | r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload) | |
413 | self.assertTrue(r) | |
4bfebc93 | 414 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
415 | self.assertTrue(r.json()) |
416 | content = r.json() | |
b4be3cc0 OM |
417 | acl = content['value'] |
418 | acl.sort() | |
4bfebc93 | 419 | self.assertEqual(acl, newACL) |
56d68fad RG |
420 | |
421 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
422 | self.assertTrue(r) | |
4bfebc93 | 423 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
424 | self.assertTrue(r.json()) |
425 | content = r.json() | |
b4be3cc0 OM |
426 | acl = content['value'] |
427 | acl.sort() | |
4bfebc93 | 428 | self.assertEqual(acl, newACL) |
56d68fad RG |
429 | |
430 | configFile = self._APIWriteDir + '/' + 'acl.conf' | |
431 | self.assertTrue(os.path.isfile(configFile)) | |
432 | fileContent = None | |
b4f23783 | 433 | with open(configFile, 'rt') as f: |
b4be3cc0 OM |
434 | header = f.readline() |
435 | body = f.readline() | |
436 | ||
4bfebc93 | 437 | self.assertEqual(header, """-- Generated by the REST API, DO NOT EDIT\n""") |
b4be3cc0 OM |
438 | |
439 | self.assertIn(body, { | |
440 | """setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})\n""", | |
441 | """setACL({"192.0.2.0/24", "203.0.113.0/24", "198.51.100.0/24"})\n""", | |
442 | """setACL({"198.51.100.0/24", "192.0.2.0/24", "203.0.113.0/24"})\n""", | |
443 | """setACL({"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24"})\n""", | |
444 | """setACL({"203.0.113.0/24", "192.0.2.0/24", "198.51.100.0/24"})\n""", | |
445 | """setACL({"203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"})\n""" | |
446 | }) | |
80dbd7d2 | 447 | |
72bf42cd RG |
448 | class TestAPICustomHeaders(APITestsBase): |
449 | __test__ = True | |
32c97b56 CHB |
450 | # paths accessible using the API key only |
451 | _apiOnlyPath = '/api/v1/servers/localhost/config' | |
452 | # paths accessible using basic auth only (list not exhaustive) | |
453 | _basicOnlyPath = '/' | |
454 | _consoleKey = DNSDistTest.generateConsoleKey() | |
455 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 456 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
32c97b56 CHB |
457 | _config_template = """ |
458 | setKey("%s") | |
459 | controlSocket("127.0.0.1:%s") | |
460 | setACL({"127.0.0.1/32", "::1/128"}) | |
461 | newServer({address="127.0.0.1:%s"}) | |
fa7e8b5d | 462 | webserver("127.0.0.1:%s") |
cfe95ada | 463 | setWebserverConfig({password="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} }) |
32c97b56 CHB |
464 | """ |
465 | ||
466 | def testBasicHeaders(self): | |
467 | """ | |
468 | API: Basic custom headers | |
469 | """ | |
470 | ||
471 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath | |
472 | ||
473 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
474 | self.assertTrue(r) | |
4bfebc93 CH |
475 | self.assertEqual(r.status_code, 200) |
476 | self.assertEqual(r.headers.get('x-custom'), "custom") | |
32c97b56 CHB |
477 | self.assertFalse("x-frame-options" in r.headers) |
478 | ||
479 | def testBasicHeadersUpdate(self): | |
480 | """ | |
481 | API: Basic update of custom headers | |
482 | """ | |
483 | ||
484 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath | |
485 | self.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})') | |
486 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
487 | self.assertTrue(r) | |
4bfebc93 CH |
488 | self.assertEqual(r.status_code, 200) |
489 | self.assertEqual(r.headers.get('x-powered-by'), "dnsdist") | |
32c97b56 CHB |
490 | self.assertTrue("x-frame-options" in r.headers) |
491 | ||
72bf42cd RG |
492 | class TestStatsWithoutAuthentication(APITestsBase): |
493 | __test__ = True | |
fa7e8b5d RG |
494 | # paths accessible using the API key only |
495 | _apiOnlyPath = '/api/v1/servers/localhost/config' | |
496 | # paths accessible using basic auth only (list not exhaustive) | |
497 | _basicOnlyPath = '/' | |
498 | _noAuthenticationPaths = [ '/metrics', '/jsonstat?command=dynblocklist' ] | |
499 | _consoleKey = DNSDistTest.generateConsoleKey() | |
500 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 501 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
fa7e8b5d RG |
502 | _config_template = """ |
503 | setKey("%s") | |
504 | controlSocket("127.0.0.1:%s") | |
505 | setACL({"127.0.0.1/32", "::1/128"}) | |
506 | newServer({address="127.0.0.1:%s"}) | |
507 | webserver("127.0.0.1:%s") | |
cfe95ada | 508 | setWebserverConfig({password="%s", apiKey="%s", statsRequireAuthentication=false }) |
fa7e8b5d RG |
509 | """ |
510 | ||
511 | def testAuth(self): | |
512 | """ | |
513 | API: Stats do not require authentication | |
514 | """ | |
515 | ||
516 | for path in self._noAuthenticationPaths: | |
517 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
518 | ||
519 | r = requests.get(url, timeout=self._webTimeout) | |
520 | self.assertTrue(r) | |
4bfebc93 | 521 | self.assertEqual(r.status_code, 200) |
fa7e8b5d RG |
522 | |
523 | # these should still require basic authentication | |
524 | for path in [self._basicOnlyPath]: | |
525 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
526 | ||
527 | r = requests.get(url, timeout=self._webTimeout) | |
4bfebc93 | 528 | self.assertEqual(r.status_code, 401) |
fa7e8b5d RG |
529 | |
530 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
531 | self.assertTrue(r) | |
4bfebc93 | 532 | self.assertEqual(r.status_code, 200) |
fa7e8b5d RG |
533 | |
534 | # these should still require API authentication | |
535 | for path in [self._apiOnlyPath]: | |
536 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
537 | ||
538 | r = requests.get(url, timeout=self._webTimeout) | |
4bfebc93 | 539 | self.assertEqual(r.status_code, 401) |
fa7e8b5d RG |
540 | |
541 | headers = {'x-api-key': self._webServerAPIKey} | |
542 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
543 | self.assertTrue(r) | |
4bfebc93 | 544 | self.assertEqual(r.status_code, 200) |
32c97b56 | 545 | |
72bf42cd RG |
546 | class TestAPIAuth(APITestsBase): |
547 | __test__ = True | |
80dbd7d2 | 548 | _webServerBasicAuthPasswordNew = 'password' |
2c0392a5 | 549 | _webServerBasicAuthPasswordNewHashed = '$scrypt$ln=10,p=1,r=8$yefz8SAuT3lj3moXqUYvmw==$T98/RYMp76ZYNjd7MpAkcVXZEDqpLtrc3tQ52QflVBA=' |
80dbd7d2 | 550 | _webServerAPIKeyNew = 'apipassword' |
2c0392a5 | 551 | _webServerAPIKeyNewHashed = '$scrypt$ln=9,p=1,r=8$y96I9nfkY0LWDQEdSUzWgA==$jiyn9QD36o9d0ADrlqiIBk4AKyQrkD1KYw3CexwtHp4=' |
80dbd7d2 CHB |
552 | # paths accessible using the API key only |
553 | _apiOnlyPath = '/api/v1/servers/localhost/config' | |
554 | # paths accessible using basic auth only (list not exhaustive) | |
555 | _basicOnlyPath = '/' | |
556 | _consoleKey = DNSDistTest.generateConsoleKey() | |
557 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 558 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
80dbd7d2 CHB |
559 | _config_template = """ |
560 | setKey("%s") | |
561 | controlSocket("127.0.0.1:%s") | |
562 | setACL({"127.0.0.1/32", "::1/128"}) | |
563 | newServer{address="127.0.0.1:%s"} | |
fa7e8b5d | 564 | webserver("127.0.0.1:%s") |
cfe95ada | 565 | setWebserverConfig({password="%s", apiKey="%s"}) |
80dbd7d2 CHB |
566 | """ |
567 | ||
568 | def testBasicAuthChange(self): | |
569 | """ | |
570 | API: Basic Authentication updating credentials | |
571 | """ | |
572 | ||
80dbd7d2 | 573 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath |
cfe95ada | 574 | self.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self._webServerBasicAuthPasswordNewHashed)) |
32c97b56 | 575 | |
80dbd7d2 CHB |
576 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPasswordNew), timeout=self._webTimeout) |
577 | self.assertTrue(r) | |
4bfebc93 | 578 | self.assertEqual(r.status_code, 200) |
80dbd7d2 CHB |
579 | |
580 | # Make sure the old password is not usable any more | |
80dbd7d2 | 581 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) |
4bfebc93 | 582 | self.assertEqual(r.status_code, 401) |
80dbd7d2 CHB |
583 | |
584 | def testXAPIKeyChange(self): | |
585 | """ | |
586 | API: X-Api-Key updating credentials | |
587 | """ | |
588 | ||
32c97b56 | 589 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath |
412f99ef | 590 | self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed)) |
80dbd7d2 CHB |
591 | |
592 | headers = {'x-api-key': self._webServerAPIKeyNew} | |
80dbd7d2 CHB |
593 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
594 | self.assertTrue(r) | |
4bfebc93 | 595 | self.assertEqual(r.status_code, 200) |
80dbd7d2 CHB |
596 | |
597 | # Make sure the old password is not usable any more | |
598 | headers = {'x-api-key': self._webServerAPIKey} | |
80dbd7d2 | 599 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
4bfebc93 | 600 | self.assertEqual(r.status_code, 401) |
80dbd7d2 CHB |
601 | |
602 | def testBasicAuthOnlyChange(self): | |
603 | """ | |
604 | API: X-Api-Key updated to none (disabled) | |
605 | """ | |
606 | ||
32c97b56 | 607 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath |
412f99ef | 608 | self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed)) |
80dbd7d2 CHB |
609 | |
610 | headers = {'x-api-key': self._webServerAPIKeyNew} | |
80dbd7d2 CHB |
611 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
612 | self.assertTrue(r) | |
4bfebc93 | 613 | self.assertEqual(r.status_code, 200) |
80dbd7d2 CHB |
614 | |
615 | # now disable apiKey | |
32c97b56 | 616 | self.sendConsoleCommand('setWebserverConfig({apiKey=""})') |
80dbd7d2 | 617 | |
80dbd7d2 | 618 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
4bfebc93 | 619 | self.assertEqual(r.status_code, 401) |
1c90c6bd | 620 | |
72bf42cd RG |
621 | class TestAPIACL(APITestsBase): |
622 | __test__ = True | |
1c90c6bd RG |
623 | _consoleKey = DNSDistTest.generateConsoleKey() |
624 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 625 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
1c90c6bd RG |
626 | _config_template = """ |
627 | setKey("%s") | |
628 | controlSocket("127.0.0.1:%s") | |
629 | setACL({"127.0.0.1/32", "::1/128"}) | |
630 | newServer{address="127.0.0.1:%s"} | |
fa7e8b5d | 631 | webserver("127.0.0.1:%s") |
cfe95ada | 632 | setWebserverConfig({password="%s", apiKey="%s", acl="192.0.2.1"}) |
1c90c6bd RG |
633 | """ |
634 | ||
635 | def testACLChange(self): | |
636 | """ | |
637 | API: Should be denied by ACL then allowed | |
638 | """ | |
639 | ||
640 | url = 'http://127.0.0.1:' + str(self._webServerPort) + "/" | |
641 | try: | |
642 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
643 | self.assertTrue(False) | |
644 | except requests.exceptions.ConnectionError as exp: | |
645 | pass | |
646 | ||
647 | # reset the ACL | |
648 | self.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})') | |
649 | ||
650 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
651 | self.assertTrue(r) | |
4bfebc93 | 652 | self.assertEqual(r.status_code, 200) |
88d4fe87 | 653 | |
80af53eb RG |
654 | class TestAPIWithoutAuthentication(APITestsBase): |
655 | __test__ = True | |
656 | _apiPath = '/api/v1/servers/localhost/config' | |
657 | # paths accessible using basic auth only (list not exhaustive) | |
658 | _basicOnlyPath = '/' | |
659 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed'] | |
660 | _config_template = """ | |
661 | setACL({"127.0.0.1/32", "::1/128"}) | |
662 | newServer({address="127.0.0.1:%s"}) | |
663 | webserver("127.0.0.1:%s") | |
664 | setWebserverConfig({password="%s", apiRequiresAuthentication=false }) | |
665 | """ | |
666 | ||
667 | def testAuth(self): | |
668 | """ | |
669 | API: API do not require authentication | |
670 | """ | |
671 | ||
672 | for path in [self._apiPath]: | |
673 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
674 | ||
675 | r = requests.get(url, timeout=self._webTimeout) | |
676 | self.assertTrue(r) | |
677 | self.assertEqual(r.status_code, 200) | |
678 | ||
679 | # these should still require basic authentication | |
680 | for path in [self._basicOnlyPath]: | |
681 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
682 | ||
683 | r = requests.get(url, timeout=self._webTimeout) | |
684 | self.assertEqual(r.status_code, 401) | |
685 | ||
686 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
687 | self.assertTrue(r) | |
688 | self.assertEqual(r.status_code, 200) | |
689 | ||
4bbed5cf RG |
690 | class TestDashboardWithoutAuthentication(APITestsBase): |
691 | __test__ = True | |
692 | _basicPath = '/' | |
693 | _config_params = ['_testServerPort', '_webServerPort'] | |
694 | _config_template = """ | |
695 | setACL({"127.0.0.1/32", "::1/128"}) | |
696 | newServer({address="127.0.0.1:%d"}) | |
697 | webserver("127.0.0.1:%d") | |
698 | setWebserverConfig({ dashboardRequiresAuthentication=false }) | |
699 | """ | |
700 | _verboseMode=True | |
701 | ||
702 | def testDashboard(self): | |
703 | """ | |
704 | API: Dashboard do not require authentication | |
705 | """ | |
706 | ||
707 | for path in [self._basicPath]: | |
708 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
709 | ||
710 | r = requests.get(url, timeout=self._webTimeout) | |
711 | self.assertTrue(r) | |
712 | self.assertEqual(r.status_code, 200) | |
713 | ||
72bf42cd RG |
714 | class TestCustomLuaEndpoint(APITestsBase): |
715 | __test__ = True | |
88d4fe87 RG |
716 | _config_template = """ |
717 | setACL({"127.0.0.1/32", "::1/128"}) | |
718 | newServer{address="127.0.0.1:%s"} | |
72bf42cd | 719 | webserver("127.0.0.1:%s") |
cfe95ada | 720 | setWebserverConfig({password="%s"}) |
88d4fe87 RG |
721 | |
722 | function customHTTPHandler(req, resp) | |
723 | if req.path ~= '/foo' then | |
724 | resp.status = 500 | |
725 | return | |
726 | end | |
727 | ||
728 | if req.version ~= 11 then | |
729 | resp.status = 501 | |
730 | return | |
731 | end | |
732 | ||
733 | if req.method ~= 'GET' then | |
734 | resp.status = 502 | |
735 | return | |
736 | end | |
737 | ||
738 | local get = req.getvars | |
739 | if get['param'] ~= '42' then | |
740 | resp.status = 503 | |
741 | return | |
742 | end | |
743 | ||
744 | local headers = req.headers | |
745 | if headers['customheader'] ~= 'foobar' then | |
746 | resp.status = 504 | |
747 | return | |
748 | end | |
749 | ||
750 | resp.body = 'It works!' | |
751 | resp.status = 200 | |
752 | resp.headers = { ['Foo']='Bar'} | |
753 | end | |
754 | registerWebHandler('/foo', customHTTPHandler) | |
755 | """ | |
72bf42cd | 756 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed'] |
88d4fe87 RG |
757 | |
758 | def testBasic(self): | |
759 | """ | |
760 | Custom Web Handler | |
761 | """ | |
762 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/foo?param=42' | |
763 | headers = {'customheader': 'foobar'} | |
764 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout, headers=headers) | |
765 | self.assertTrue(r) | |
4bfebc93 CH |
766 | self.assertEqual(r.status_code, 200) |
767 | self.assertEqual(r.content, b'It works!') | |
768 | self.assertEqual(r.headers.get('foo'), "Bar") | |
5e40d2a5 | 769 | |
72bf42cd RG |
770 | class TestWebConcurrentConnections(APITestsBase): |
771 | __test__ = True | |
5e40d2a5 RG |
772 | _maxConns = 2 |
773 | ||
412f99ef | 774 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_maxConns'] |
5e40d2a5 RG |
775 | _config_template = """ |
776 | newServer{address="127.0.0.1:%s"} | |
777 | webserver("127.0.0.1:%s") | |
cfe95ada | 778 | setWebserverConfig({password="%s", apiKey="%s", maxConcurrentConnections=%d}) |
5e40d2a5 RG |
779 | """ |
780 | ||
13e4e845 RG |
781 | @classmethod |
782 | def setUpClass(cls): | |
783 | cls.startResponders() | |
784 | cls.startDNSDist() | |
785 | cls.setUpSockets() | |
786 | # do no check if the web server socket is up, because this | |
787 | # might mess up the concurrent connections counter | |
788 | ||
5e40d2a5 RG |
789 | def testConcurrentConnections(self): |
790 | """ | |
791 | Web: Concurrent connections | |
792 | """ | |
793 | ||
794 | conns = [] | |
795 | # open the maximum number of connections | |
796 | for _ in range(self._maxConns): | |
797 | conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
798 | conn.connect(("127.0.0.1", self._webServerPort)) | |
799 | conns.append(conn) | |
800 | ||
801 | # we now hold all the slots, let's try to establish a new connection | |
802 | url = 'http://127.0.0.1:' + str(self._webServerPort) + "/" | |
803 | self.assertRaises(requests.exceptions.ConnectionError, requests.get, url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
804 | ||
805 | # free one slot | |
806 | conns[0].close() | |
807 | conns[0] = None | |
808 | time.sleep(1) | |
809 | ||
810 | # this should work | |
811 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
812 | self.assertTrue(r) | |
4bfebc93 | 813 | self.assertEqual(r.status_code, 200) |
6211164a CHB |
814 | |
815 | class TestAPICustomStatistics(APITestsBase): | |
816 | __test__ = True | |
817 | _maxConns = 2 | |
818 | ||
819 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] | |
820 | _config_template = """ | |
821 | newServer{address="127.0.0.1:%s"} | |
822 | webserver("127.0.0.1:%s") | |
b08586c7 CHB |
823 | declareMetric("my-custom-metric", "counter", "Number of statistics") |
824 | declareMetric("my-other-metric", "counter", "Another number of statistics") | |
825 | declareMetric("my-gauge", "gauge", "Current memory usage") | |
6211164a CHB |
826 | setWebserverConfig({password="%s", apiKey="%s"}) |
827 | """ | |
828 | ||
829 | def testCustomStats(self): | |
830 | """ | |
831 | API: /jsonstat?command=stats | |
832 | Test custom statistics are exposed | |
833 | """ | |
834 | headers = {'x-api-key': self._webServerAPIKey} | |
835 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=stats' | |
836 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
837 | self.assertTrue(r) | |
838 | self.assertEqual(r.status_code, 200) | |
839 | self.assertTrue(r.json()) | |
840 | content = r.json() | |
841 | ||
842 | expected = ['my-custom-metric', 'my-other-metric', 'my-gauge'] | |
843 | ||
844 | for key in expected: | |
845 | self.assertIn(key, content) | |
846 | self.assertTrue(content[key] >= 0) |