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