]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_API.py
ecad521c9d7bbd05bde2e3099e814471d84658f2
[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 from dnsdisttests import DNSDistTest
8
9 class TestAPIBasics(DNSDistTest):
10
11 _webTimeout = 2.0
12 _webServerPort = 8083
13 _webServerBasicAuthPassword = 'secret'
14 _webServerAPIKey = 'apisecret'
15 # paths accessible using the API key only
16 _apiOnlyPaths = ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics']
17 # paths accessible using an API key or basic auth
18 _statsPaths = [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost']
19 # paths accessible using basic auth only (list not exhaustive)
20 _basicOnlyPaths = ['/', '/index.html']
21 _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
22 _config_template = """
23 setACL({"127.0.0.1/32", "::1/128"})
24 newServer{address="127.0.0.1:%s"}
25 webserver("127.0.0.1:%s", "%s", "%s")
26 """
27
28 def testBasicAuth(self):
29 """
30 API: Basic Authentication
31 """
32 for path in self._basicOnlyPaths + self._statsPaths:
33 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
34 r = requests.get(url, auth=('whatever', "evilsecret"), timeout=self._webTimeout)
35 self.assertEquals(r.status_code, 401)
36 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
37 self.assertTrue(r)
38 self.assertEquals(r.status_code, 200)
39
40 def testXAPIKey(self):
41 """
42 API: X-Api-Key
43 """
44 headers = {'x-api-key': self._webServerAPIKey}
45 for path in self._apiOnlyPaths + self._statsPaths:
46 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
47 r = requests.get(url, headers=headers, timeout=self._webTimeout)
48 self.assertTrue(r)
49 self.assertEquals(r.status_code, 200)
50
51 def testWrongXAPIKey(self):
52 """
53 API: Wrong X-Api-Key
54 """
55 headers = {'x-api-key': "evilapikey"}
56 for path in self._apiOnlyPaths + self._statsPaths:
57 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
58 r = requests.get(url, headers=headers, timeout=self._webTimeout)
59 self.assertEquals(r.status_code, 401)
60 def testBasicAuthOnly(self):
61 """
62 API: Basic Authentication Only
63 """
64 headers = {'x-api-key': self._webServerAPIKey}
65 for path in self._basicOnlyPaths:
66 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
67 r = requests.get(url, headers=headers, timeout=self._webTimeout)
68 self.assertEquals(r.status_code, 401)
69
70 def testAPIKeyOnly(self):
71 """
72 API: API Key Only
73 """
74 for path in self._apiOnlyPaths:
75 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
76 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
77 self.assertEquals(r.status_code, 401)
78
79 def testServersLocalhost(self):
80 """
81 API: /api/v1/servers/localhost
82 """
83 headers = {'x-api-key': self._webServerAPIKey}
84 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost'
85 r = requests.get(url, headers=headers, timeout=self._webTimeout)
86 self.assertTrue(r)
87 self.assertEquals(r.status_code, 200)
88 self.assertTrue(r.json())
89 content = r.json()
90
91 self.assertEquals(content['daemon_type'], 'dnsdist')
92
93 rule_groups = ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules', 'rules']
94 for key in ['version', 'acl', 'local', 'servers', 'frontends', 'pools'] + rule_groups:
95 self.assertIn(key, content)
96
97 for rule_group in rule_groups:
98 for rule in content[rule_group]:
99 for key in ['id', 'creationOrder', 'matches', 'rule', 'action', 'uuid']:
100 self.assertIn(key, rule)
101 for key in ['id', 'creationOrder', 'matches']:
102 self.assertTrue(rule[key] >= 0)
103
104 for server in content['servers']:
105 for key in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit',
106 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors',
107 'dropRate']:
108 self.assertIn(key, server)
109
110 for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
111 'qps', 'queries', 'order']:
112 self.assertTrue(server[key] >= 0)
113
114 self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN'])
115
116 for frontend in content['frontends']:
117 for key in ['id', 'address', 'udp', 'tcp', 'type', 'queries']:
118 self.assertIn(key, frontend)
119
120 for key in ['id', 'queries']:
121 self.assertTrue(frontend[key] >= 0)
122
123 for pool in content['pools']:
124 for key in ['id', 'name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
125 self.assertIn(key, pool)
126
127 for key in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
128 self.assertTrue(pool[key] >= 0)
129
130 def testServersIDontExist(self):
131 """
132 API: /api/v1/servers/idontexist (should be 404)
133 """
134 headers = {'x-api-key': self._webServerAPIKey}
135 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/idontexist'
136 r = requests.get(url, headers=headers, timeout=self._webTimeout)
137 self.assertEquals(r.status_code, 404)
138
139 def testServersLocalhostConfig(self):
140 """
141 API: /api/v1/servers/localhost/config
142 """
143 headers = {'x-api-key': self._webServerAPIKey}
144 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config'
145 r = requests.get(url, headers=headers, timeout=self._webTimeout)
146 self.assertTrue(r)
147 self.assertEquals(r.status_code, 200)
148 self.assertTrue(r.json())
149 content = r.json()
150 values = {}
151 for entry in content:
152 for key in ['type', 'name', 'value']:
153 self.assertIn(key, entry)
154
155 self.assertEquals(entry['type'], 'ConfigSetting')
156 values[entry['name']] = entry['value']
157
158 for key in ['acl', 'control-socket', 'ecs-override', 'ecs-source-prefix-v4',
159 'ecs-source-prefix-v6', 'fixup-case', 'max-outstanding', 'server-policy',
160 'stale-cache-entries-ttl', 'tcp-recv-timeout', 'tcp-send-timeout',
161 'truncate-tc', 'verbose', 'verbose-health-checks']:
162 self.assertIn(key, values)
163
164 for key in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout',
165 'tcp-send-timeout']:
166 self.assertTrue(values[key] >= 0)
167
168 self.assertTrue(values['ecs-source-prefix-v4'] >= 0 and values['ecs-source-prefix-v4'] <= 32)
169 self.assertTrue(values['ecs-source-prefix-v6'] >= 0 and values['ecs-source-prefix-v6'] <= 128)
170
171 def testServersLocalhostConfigAllowFrom(self):
172 """
173 API: /api/v1/servers/localhost/config/allow-from
174 """
175 headers = {'x-api-key': self._webServerAPIKey}
176 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from'
177 r = requests.get(url, headers=headers, timeout=self._webTimeout)
178 self.assertTrue(r)
179 self.assertEquals(r.status_code, 200)
180 self.assertTrue(r.json())
181 content = r.json()
182 for key in ['type', 'name', 'value']:
183 self.assertIn(key, content)
184
185 self.assertEquals(content['name'], 'allow-from')
186 self.assertEquals(content['type'], 'ConfigSetting')
187 acl = content['value']
188 expectedACL = ["127.0.0.1/32", "::1/128"]
189 acl.sort()
190 expectedACL.sort()
191 self.assertEquals(acl, expectedACL)
192
193 def testServersLocalhostConfigAllowFromPut(self):
194 """
195 API: PUT /api/v1/servers/localhost/config/allow-from (should be refused)
196
197 The API is read-only by default, so this should be refused
198 """
199 newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
200 payload = json.dumps({"name": "allow-from",
201 "type": "ConfigSetting",
202 "value": newACL})
203 headers = {'x-api-key': self._webServerAPIKey}
204 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from'
205 r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload)
206 self.assertFalse(r)
207 self.assertEquals(r.status_code, 405)
208
209 def testServersLocalhostStatistics(self):
210 """
211 API: /api/v1/servers/localhost/statistics
212 """
213 headers = {'x-api-key': self._webServerAPIKey}
214 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/statistics'
215 r = requests.get(url, headers=headers, timeout=self._webTimeout)
216 self.assertTrue(r)
217 self.assertEquals(r.status_code, 200)
218 self.assertTrue(r.json())
219 content = r.json()
220 values = {}
221 for entry in content:
222 self.assertIn('type', entry)
223 self.assertIn('name', entry)
224 self.assertIn('value', entry)
225 self.assertEquals(entry['type'], 'StatisticItem')
226 values[entry['name']] = entry['value']
227
228 expected = ['responses', 'servfail-responses', 'queries', 'acl-drops',
229 'frontend-noerror', 'frontend-nxdomain', 'frontend-servfail',
230 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts',
231 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1',
232 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000',
233 'latency-slow', 'latency-sum', 'latency-count', 'latency-avg100', 'latency-avg1000',
234 'latency-avg10000', 'latency-avg1000000', 'uptime', 'real-memory-usage', 'noncompliant-queries',
235 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits',
236 'cache-misses', 'cpu-user-msec', 'cpu-sys-msec', 'fd-usage', 'dyn-blocked',
237 'dyn-block-nmg-size', 'rule-servfail', 'security-status']
238
239 for key in expected:
240 self.assertIn(key, values)
241 self.assertTrue(values[key] >= 0)
242
243 for key in values:
244 self.assertIn(key, expected)
245
246 def testJsonstatStats(self):
247 """
248 API: /jsonstat?command=stats
249 """
250 headers = {'x-api-key': self._webServerAPIKey}
251 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=stats'
252 r = requests.get(url, headers=headers, timeout=self._webTimeout)
253 self.assertTrue(r)
254 self.assertEquals(r.status_code, 200)
255 self.assertTrue(r.json())
256 content = r.json()
257
258 expected = ['responses', 'servfail-responses', 'queries', 'acl-drops',
259 'frontend-noerror', 'frontend-nxdomain', 'frontend-servfail',
260 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts',
261 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1',
262 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000',
263 'latency-slow', 'latency-avg100', 'latency-avg1000', 'latency-avg10000',
264 'latency-avg1000000', 'uptime', 'real-memory-usage', 'noncompliant-queries',
265 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits',
266 'cache-misses', 'cpu-user-msec', 'cpu-sys-msec', 'fd-usage', 'dyn-blocked',
267 'dyn-block-nmg-size', 'packetcache-hits', 'packetcache-misses', 'over-capacity-drops',
268 'too-old-drops']
269
270 for key in expected:
271 self.assertIn(key, content)
272 self.assertTrue(content[key] >= 0)
273
274 def testJsonstatDynblocklist(self):
275 """
276 API: /jsonstat?command=dynblocklist
277 """
278 headers = {'x-api-key': self._webServerAPIKey}
279 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=dynblocklist'
280 r = requests.get(url, headers=headers, timeout=self._webTimeout)
281 self.assertTrue(r)
282 self.assertEquals(r.status_code, 200)
283
284 content = r.json()
285
286 if content:
287 for key in ['reason', 'seconds', 'blocks', 'action']:
288 self.assertIn(key, content)
289
290 for key in ['blocks']:
291 self.assertTrue(content[key] >= 0)
292
293 class TestAPIServerDown(DNSDistTest):
294
295 _webTimeout = 2.0
296 _webServerPort = 8083
297 _webServerBasicAuthPassword = 'secret'
298 _webServerAPIKey = 'apisecret'
299 # paths accessible using the API key
300 _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
301 _config_template = """
302 setACL({"127.0.0.1/32", "::1/128"})
303 newServer{address="127.0.0.1:%s"}
304 getServer(0):setDown()
305 webserver("127.0.0.1:%s", "%s", "%s")
306 """
307
308 def testServerDownNoLatencyLocalhost(self):
309 """
310 API: /api/v1/servers/localhost, no latency for a down server
311 """
312 headers = {'x-api-key': self._webServerAPIKey}
313 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost'
314 r = requests.get(url, headers=headers, timeout=self._webTimeout)
315 self.assertTrue(r)
316 self.assertEquals(r.status_code, 200)
317 self.assertTrue(r.json())
318 content = r.json()
319
320 self.assertEquals(content['servers'][0]['latency'], None)
321
322 class TestAPIWritable(DNSDistTest):
323
324 _webTimeout = 2.0
325 _webServerPort = 8083
326 _webServerBasicAuthPassword = 'secret'
327 _webServerAPIKey = 'apisecret'
328 _APIWriteDir = '/tmp'
329 _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey', '_APIWriteDir']
330 _config_template = """
331 setACL({"127.0.0.1/32", "::1/128"})
332 newServer{address="127.0.0.1:%s"}
333 webserver("127.0.0.1:%s", "%s", "%s")
334 setAPIWritable(true, "%s")
335 """
336
337 def testSetACL(self):
338 """
339 API: Set ACL
340 """
341 headers = {'x-api-key': self._webServerAPIKey}
342 url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from'
343 r = requests.get(url, headers=headers, timeout=self._webTimeout)
344 self.assertTrue(r)
345 self.assertEquals(r.status_code, 200)
346 self.assertTrue(r.json())
347 content = r.json()
348 acl = content['value']
349 expectedACL = ["127.0.0.1/32", "::1/128"]
350 acl.sort()
351 expectedACL.sort()
352 self.assertEquals(acl, expectedACL)
353
354 newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
355 payload = json.dumps({"name": "allow-from",
356 "type": "ConfigSetting",
357 "value": newACL})
358 r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload)
359 self.assertTrue(r)
360 self.assertEquals(r.status_code, 200)
361 self.assertTrue(r.json())
362 content = r.json()
363 acl = content['value']
364 acl.sort()
365 self.assertEquals(acl, newACL)
366
367 r = requests.get(url, headers=headers, timeout=self._webTimeout)
368 self.assertTrue(r)
369 self.assertEquals(r.status_code, 200)
370 self.assertTrue(r.json())
371 content = r.json()
372 acl = content['value']
373 acl.sort()
374 self.assertEquals(acl, newACL)
375
376 configFile = self._APIWriteDir + '/' + 'acl.conf'
377 self.assertTrue(os.path.isfile(configFile))
378 fileContent = None
379 with open(configFile, 'rt') as f:
380 header = f.readline()
381 body = f.readline()
382
383 self.assertEquals(header, """-- Generated by the REST API, DO NOT EDIT\n""")
384
385 self.assertIn(body, {
386 """setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})\n""",
387 """setACL({"192.0.2.0/24", "203.0.113.0/24", "198.51.100.0/24"})\n""",
388 """setACL({"198.51.100.0/24", "192.0.2.0/24", "203.0.113.0/24"})\n""",
389 """setACL({"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24"})\n""",
390 """setACL({"203.0.113.0/24", "192.0.2.0/24", "198.51.100.0/24"})\n""",
391 """setACL({"203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"})\n"""
392 })
393
394 class TestAPICustomHeaders(DNSDistTest):
395
396 _webTimeout = 2.0
397 _webServerPort = 8083
398 _webServerBasicAuthPassword = 'secret'
399 _webServerAPIKey = 'apisecret'
400 # paths accessible using the API key only
401 _apiOnlyPath = '/api/v1/servers/localhost/config'
402 # paths accessible using basic auth only (list not exhaustive)
403 _basicOnlyPath = '/'
404 _consoleKey = DNSDistTest.generateConsoleKey()
405 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
406 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
407 _config_template = """
408 setKey("%s")
409 controlSocket("127.0.0.1:%s")
410 setACL({"127.0.0.1/32", "::1/128"})
411 newServer({address="127.0.0.1:%s"})
412 webserver("127.0.0.1:%s", "%s", "%s", {["X-Frame-Options"]="", ["X-Custom"]="custom"})
413 """
414
415 def testBasicHeaders(self):
416 """
417 API: Basic custom headers
418 """
419
420 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
421
422 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
423 self.assertTrue(r)
424 self.assertEquals(r.status_code, 200)
425 self.assertEquals(r.headers.get('x-custom'), "custom")
426 self.assertFalse("x-frame-options" in r.headers)
427
428 def testBasicHeadersUpdate(self):
429 """
430 API: Basic update of custom headers
431 """
432
433 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
434 self.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})')
435 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
436 self.assertTrue(r)
437 self.assertEquals(r.status_code, 200)
438 self.assertEquals(r.headers.get('x-powered-by'), "dnsdist")
439 self.assertTrue("x-frame-options" in r.headers)
440
441
442 class TestAPIAuth(DNSDistTest):
443
444 _webTimeout = 2.0
445 _webServerPort = 8083
446 _webServerBasicAuthPassword = 'secret'
447 _webServerBasicAuthPasswordNew = 'password'
448 _webServerAPIKey = 'apisecret'
449 _webServerAPIKeyNew = 'apipassword'
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')
456 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
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"}
462 webserver("127.0.0.1:%s", "%s", "%s")
463 """
464
465 def testBasicAuthChange(self):
466 """
467 API: Basic Authentication updating credentials
468 """
469
470 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
471 self.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self._webServerBasicAuthPasswordNew))
472
473 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPasswordNew), timeout=self._webTimeout)
474 self.assertTrue(r)
475 self.assertEquals(r.status_code, 200)
476
477 # Make sure the old password is not usable any more
478 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
479 self.assertEquals(r.status_code, 401)
480
481 def testXAPIKeyChange(self):
482 """
483 API: X-Api-Key updating credentials
484 """
485
486 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath
487 self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNew))
488
489 headers = {'x-api-key': self._webServerAPIKeyNew}
490 r = requests.get(url, headers=headers, timeout=self._webTimeout)
491 self.assertTrue(r)
492 self.assertEquals(r.status_code, 200)
493
494 # Make sure the old password is not usable any more
495 headers = {'x-api-key': self._webServerAPIKey}
496 r = requests.get(url, headers=headers, timeout=self._webTimeout)
497 self.assertEquals(r.status_code, 401)
498
499 def testBasicAuthOnlyChange(self):
500 """
501 API: X-Api-Key updated to none (disabled)
502 """
503
504 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath
505 self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNew))
506
507 headers = {'x-api-key': self._webServerAPIKeyNew}
508 r = requests.get(url, headers=headers, timeout=self._webTimeout)
509 self.assertTrue(r)
510 self.assertEquals(r.status_code, 200)
511
512 # now disable apiKey
513 self.sendConsoleCommand('setWebserverConfig({apiKey=""})')
514
515 r = requests.get(url, headers=headers, timeout=self._webTimeout)
516 self.assertEquals(r.status_code, 401)