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