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