]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_API.py
Merge pull request #12782 from omoerbeek/dsdist-api-test-timeout
[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
10
11 class APITestsBase(DNSDistTest):
12 __test__ = False
13 _webTimeout = 5.0
14 _webServerPort = 8083
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', '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']:
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)