7 from dnsdisttests
import DNSDistTest
10 class TestAPIBasics(DNSDistTest
):
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")
29 def testBasicAuth(self
):
31 API: Basic Authentication
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
)
39 self
.assertEquals(r
.status_code
, 200)
41 def testXAPIKey(self
):
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
)
50 self
.assertEquals(r
.status_code
, 200)
52 def testWrongXAPIKey(self
):
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
):
63 API: Basic Authentication Only
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)
71 def testAPIKeyOnly(self
):
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)
80 def testServersLocalhost(self
):
82 API: /api/v1/servers/localhost
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
)
88 self
.assertEquals(r
.status_code
, 200)
89 self
.assertTrue(r
.json())
92 self
.assertEquals(content
['daemon_type'], 'dnsdist')
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
)
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)
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)
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',
115 self
.assertIn(key
, server
)
117 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
118 'qps', 'queries', 'order']:
119 self
.assertTrue(server
[key
] >= 0)
121 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
123 for frontend
in content
['frontends']:
124 for key
in ['id', 'address', 'udp', 'tcp', 'queries']:
125 self
.assertIn(key
, frontend
)
127 for key
in ['id', 'queries']:
128 self
.assertTrue(frontend
[key
] >= 0)
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
)
134 for key
in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
135 self
.assertTrue(pool
[key
] >= 0)
137 def testServersIDontExist(self
):
139 API: /api/v1/servers/idontexist (should be 404)
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)
146 def testServersLocalhostConfig(self
):
148 API: /api/v1/servers/localhost/config
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
)
154 self
.assertEquals(r
.status_code
, 200)
155 self
.assertTrue(r
.json())
158 for entry
in content
:
159 for key
in ['type', 'name', 'value']:
160 self
.assertIn(key
, entry
)
162 self
.assertEquals(entry
['type'], 'ConfigSetting')
163 values
[entry
['name']] = entry
['value']
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
)
171 for key
in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout',
173 self
.assertTrue(values
[key
] >= 0)
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)
178 def testServersLocalhostConfigAllowFrom(self
):
180 API: /api/v1/servers/localhost/config/allow-from
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
)
186 self
.assertEquals(r
.status_code
, 200)
187 self
.assertTrue(r
.json())
189 for key
in ['type', 'name', 'value']:
190 self
.assertIn(key
, content
)
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"]
198 self
.assertEquals(acl
, expectedACL
)
200 def testServersLocalhostConfigAllowFromPut(self
):
202 API: PUT /api/v1/servers/localhost/config/allow-from (should be refused)
204 The API is read-only by default, so this should be refused
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",
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
)
214 self
.assertEquals(r
.status_code
, 405)
216 def testServersLocalhostStatistics(self
):
218 API: /api/v1/servers/localhost/statistics
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
)
224 self
.assertEquals(r
.status_code
, 200)
225 self
.assertTrue(r
.json())
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']
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']
246 self
.assertIn(key
, values
)
247 self
.assertTrue(values
[key
] >= 0)
250 self
.assertIn(key
, expected
)
252 def testJsonstatStats(self
):
254 API: /jsonstat?command=stats
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
)
260 self
.assertEquals(r
.status_code
, 200)
261 self
.assertTrue(r
.json())
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',
276 self
.assertIn(key
, content
)
277 self
.assertTrue(content
[key
] >= 0)
279 def testJsonstatDynblocklist(self
):
281 API: /jsonstat?command=dynblocklist
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
)
287 self
.assertEquals(r
.status_code
, 200)
292 for key
in ['reason', 'seconds', 'blocks', 'action']:
293 self
.assertIn(key
, content
)
295 for key
in ['blocks']:
296 self
.assertTrue(content
[key
] >= 0)
298 class TestAPIServerDown(DNSDistTest
):
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")
313 def testServerDownNoLatencyLocalhost(self
):
315 API: /api/v1/servers/localhost, no latency for a down server
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
)
321 self
.assertEquals(r
.status_code
, 200)
322 self
.assertTrue(r
.json())
325 self
.assertEquals(content
['servers'][0]['latency'], None)
327 class TestAPIWritable(DNSDistTest
):
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")
342 def testSetACL(self
):
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
)
350 self
.assertEquals(r
.status_code
, 200)
351 self
.assertTrue(r
.json())
353 acl
= content
['value']
354 expectedACL
= ["127.0.0.1/32", "::1/128"]
357 self
.assertEquals(acl
, expectedACL
)
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",
363 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
365 self
.assertEquals(r
.status_code
, 200)
366 self
.assertTrue(r
.json())
368 self
.assertEquals(content
['value'], newACL
)
370 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
372 self
.assertEquals(r
.status_code
, 200)
373 self
.assertTrue(r
.json())
375 self
.assertEquals(content
['value'], newACL
)
377 configFile
= self
._APIWriteDir
+ '/' + 'acl.conf'
378 self
.assertTrue(os
.path
.isfile(configFile
))
380 with
open(configFile
, 'rt') as f
:
381 fileContent
= f
.read()
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"})
387 class TestAPIAuth(DNSDistTest
):
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)
399 _consoleKey
= DNSDistTest
.generateConsoleKey()
400 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
401 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
402 _config_template
= """
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")
410 def testBasicAuthChange(self
):
412 API: Basic Authentication updating credentials
415 self
.sendConsoleCommand('setWebserverConfig("{}")'.format(self
._webServerBasicAuthPasswordNew
))
417 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
418 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPasswordNew
), timeout
=self
._webTimeout
)
420 self
.assertEquals(r
.status_code
, 200)
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)
427 def testXAPIKeyChange(self
):
429 API: X-Api-Key updating credentials
432 self
.sendConsoleCommand('setWebserverConfig("{}", "{}")'.format(self
._webServerBasicAuthPasswordNew
, self
._webServerAPIKeyNew
))
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
)
438 self
.assertEquals(r
.status_code
, 200)
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)
446 def testBasicAuthOnlyChange(self
):
448 API: X-Api-Key updated to none (disabled)
451 self
.sendConsoleCommand('setWebserverConfig("{}", "{}")'.format(self
._webServerBasicAuthPasswordNew
, self
._webServerAPIKeyNew
))
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
)
457 self
.assertEquals(r
.status_code
, 200)
460 self
.sendConsoleCommand('setWebserverConfig("{}")'.format(self
._webServerBasicAuthPasswordNew
))
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)