7 from dnsdisttests
import DNSDistTest
9 class TestAPIBasics(DNSDistTest
):
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")
28 def testBasicAuth(self
):
30 API: Basic Authentication
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
)
38 self
.assertEquals(r
.status_code
, 200)
40 def testXAPIKey(self
):
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
)
49 self
.assertEquals(r
.status_code
, 200)
51 def testWrongXAPIKey(self
):
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
):
62 API: Basic Authentication Only
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)
70 def testAPIKeyOnly(self
):
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)
79 def testServersLocalhost(self
):
81 API: /api/v1/servers/localhost
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
)
87 self
.assertEquals(r
.status_code
, 200)
88 self
.assertTrue(r
.json())
91 self
.assertEquals(content
['daemon_type'], 'dnsdist')
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
)
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)
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',
108 self
.assertIn(key
, server
)
110 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
111 'qps', 'queries', 'order']:
112 self
.assertTrue(server
[key
] >= 0)
114 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
116 for frontend
in content
['frontends']:
117 for key
in ['id', 'address', 'udp', 'tcp', 'queries']:
118 self
.assertIn(key
, frontend
)
120 for key
in ['id', 'queries']:
121 self
.assertTrue(frontend
[key
] >= 0)
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
)
127 for key
in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
128 self
.assertTrue(pool
[key
] >= 0)
130 def testServersIDontExist(self
):
132 API: /api/v1/servers/idontexist (should be 404)
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)
139 def testServersLocalhostConfig(self
):
141 API: /api/v1/servers/localhost/config
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
)
147 self
.assertEquals(r
.status_code
, 200)
148 self
.assertTrue(r
.json())
151 for entry
in content
:
152 for key
in ['type', 'name', 'value']:
153 self
.assertIn(key
, entry
)
155 self
.assertEquals(entry
['type'], 'ConfigSetting')
156 values
[entry
['name']] = entry
['value']
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
)
164 for key
in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout',
166 self
.assertTrue(values
[key
] >= 0)
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)
171 def testServersLocalhostConfigAllowFrom(self
):
173 API: /api/v1/servers/localhost/config/allow-from
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
)
179 self
.assertEquals(r
.status_code
, 200)
180 self
.assertTrue(r
.json())
182 for key
in ['type', 'name', 'value']:
183 self
.assertIn(key
, content
)
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"]
191 self
.assertEquals(acl
, expectedACL
)
193 def testServersLocalhostConfigAllowFromPut(self
):
195 API: PUT /api/v1/servers/localhost/config/allow-from (should be refused)
197 The API is read-only by default, so this should be refused
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",
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
)
207 self
.assertEquals(r
.status_code
, 405)
209 def testServersLocalhostStatistics(self
):
211 API: /api/v1/servers/localhost/statistics
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
)
217 self
.assertEquals(r
.status_code
, 200)
218 self
.assertTrue(r
.json())
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']
228 expected
= ['responses', 'servfail-responses', 'queries', 'acl-drops',
229 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts',
230 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1',
231 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000',
232 'latency-slow', 'latency-avg100', 'latency-avg1000', 'latency-avg10000',
233 'latency-avg1000000', 'uptime', 'real-memory-usage', 'noncompliant-queries',
234 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits',
235 'cache-misses', 'cpu-user-msec', 'cpu-sys-msec', 'fd-usage', 'dyn-blocked',
236 'dyn-block-nmg-size', 'rule-servfail', 'security-status']
239 self
.assertIn(key
, values
)
240 self
.assertTrue(values
[key
] >= 0)
243 self
.assertIn(key
, expected
)
245 def testJsonstatStats(self
):
247 API: /jsonstat?command=stats
249 headers
= {'x-api-key': self
._webServerAPIKey
}
250 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
251 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
253 self
.assertEquals(r
.status_code
, 200)
254 self
.assertTrue(r
.json())
257 expected
= ['responses', 'servfail-responses', 'queries', 'acl-drops',
258 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts',
259 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1',
260 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000',
261 'latency-slow', 'latency-avg100', 'latency-avg1000', 'latency-avg10000',
262 'latency-avg1000000', 'uptime', 'real-memory-usage', 'noncompliant-queries',
263 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits',
264 'cache-misses', 'cpu-user-msec', 'cpu-sys-msec', 'fd-usage', 'dyn-blocked',
265 'dyn-block-nmg-size', 'packetcache-hits', 'packetcache-misses', 'over-capacity-drops',
269 self
.assertIn(key
, content
)
270 self
.assertTrue(content
[key
] >= 0)
272 def testJsonstatDynblocklist(self
):
274 API: /jsonstat?command=dynblocklist
276 headers
= {'x-api-key': self
._webServerAPIKey
}
277 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=dynblocklist'
278 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
280 self
.assertEquals(r
.status_code
, 200)
285 for key
in ['reason', 'seconds', 'blocks', 'action']:
286 self
.assertIn(key
, content
)
288 for key
in ['blocks']:
289 self
.assertTrue(content
[key
] >= 0)
291 class TestAPIServerDown(DNSDistTest
):
294 _webServerPort
= 8083
295 _webServerBasicAuthPassword
= 'secret'
296 _webServerAPIKey
= 'apisecret'
297 # paths accessible using the API key
298 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
299 _config_template
= """
300 setACL({"127.0.0.1/32", "::1/128"})
301 newServer{address="127.0.0.1:%s"}
302 getServer(0):setDown()
303 webserver("127.0.0.1:%s", "%s", "%s")
306 def testServerDownNoLatencyLocalhost(self
):
308 API: /api/v1/servers/localhost, no latency for a down server
310 headers
= {'x-api-key': self
._webServerAPIKey
}
311 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
312 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
314 self
.assertEquals(r
.status_code
, 200)
315 self
.assertTrue(r
.json())
318 self
.assertEquals(content
['servers'][0]['latency'], None)
320 class TestAPIWritable(DNSDistTest
):
323 _webServerPort
= 8083
324 _webServerBasicAuthPassword
= 'secret'
325 _webServerAPIKey
= 'apisecret'
326 _APIWriteDir
= '/tmp'
327 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey', '_APIWriteDir']
328 _config_template
= """
329 setACL({"127.0.0.1/32", "::1/128"})
330 newServer{address="127.0.0.1:%s"}
331 webserver("127.0.0.1:%s", "%s", "%s")
332 setAPIWritable(true, "%s")
335 def testSetACL(self
):
339 headers
= {'x-api-key': self
._webServerAPIKey
}
340 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
341 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
343 self
.assertEquals(r
.status_code
, 200)
344 self
.assertTrue(r
.json())
346 acl
= content
['value']
347 expectedACL
= ["127.0.0.1/32", "::1/128"]
350 self
.assertEquals(acl
, expectedACL
)
352 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
353 payload
= json
.dumps({"name": "allow-from",
354 "type": "ConfigSetting",
356 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
358 self
.assertEquals(r
.status_code
, 200)
359 self
.assertTrue(r
.json())
361 self
.assertEquals(content
['value'], newACL
)
363 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
365 self
.assertEquals(r
.status_code
, 200)
366 self
.assertTrue(r
.json())
368 self
.assertEquals(content
['value'], newACL
)
370 configFile
= self
._APIWriteDir
+ '/' + 'acl.conf'
371 self
.assertTrue(os
.path
.isfile(configFile
))
373 with
open(configFile
, 'rt') as f
:
374 fileContent
= f
.read()
376 self
.assertEquals(fileContent
, """-- Generated by the REST API, DO NOT EDIT
377 setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})
380 class TestAPICustomHeaders(DNSDistTest
):
383 _webServerPort
= 8083
384 _webServerBasicAuthPassword
= 'secret'
385 _webServerAPIKey
= 'apisecret'
386 # paths accessible using the API key only
387 _apiOnlyPath
= '/api/v1/servers/localhost/config'
388 # paths accessible using basic auth only (list not exhaustive)
390 _consoleKey
= DNSDistTest
.generateConsoleKey()
391 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
392 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
393 _config_template
= """
395 controlSocket("127.0.0.1:%s")
396 setACL({"127.0.0.1/32", "::1/128"})
397 newServer({address="127.0.0.1:%s"})
398 webserver("127.0.0.1:%s", "%s", "%s", {["X-Frame-Options"]="", ["X-Custom"]="custom"})
401 def testBasicHeaders(self
):
403 API: Basic custom headers
406 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
408 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
410 self
.assertEquals(r
.status_code
, 200)
411 self
.assertEquals(r
.headers
.get('x-custom'), "custom")
412 self
.assertFalse("x-frame-options" in r
.headers
)
414 def testBasicHeadersUpdate(self
):
416 API: Basic update of custom headers
419 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
420 self
.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})')
421 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
423 self
.assertEquals(r
.status_code
, 200)
424 self
.assertEquals(r
.headers
.get('x-powered-by'), "dnsdist")
425 self
.assertTrue("x-frame-options" in r
.headers
)
428 class TestAPIAuth(DNSDistTest
):
431 _webServerPort
= 8083
432 _webServerBasicAuthPassword
= 'secret'
433 _webServerBasicAuthPasswordNew
= 'password'
434 _webServerAPIKey
= 'apisecret'
435 _webServerAPIKeyNew
= 'apipassword'
436 # paths accessible using the API key only
437 _apiOnlyPath
= '/api/v1/servers/localhost/config'
438 # paths accessible using basic auth only (list not exhaustive)
440 _consoleKey
= DNSDistTest
.generateConsoleKey()
441 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
442 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
443 _config_template
= """
445 controlSocket("127.0.0.1:%s")
446 setACL({"127.0.0.1/32", "::1/128"})
447 newServer{address="127.0.0.1:%s"}
448 webserver("127.0.0.1:%s", "%s", "%s")
451 def testBasicAuthChange(self
):
453 API: Basic Authentication updating credentials
456 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
457 self
.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self
._webServerBasicAuthPasswordNew
))
459 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPasswordNew
), timeout
=self
._webTimeout
)
461 self
.assertEquals(r
.status_code
, 200)
463 # Make sure the old password is not usable any more
464 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
465 self
.assertEquals(r
.status_code
, 401)
467 def testXAPIKeyChange(self
):
469 API: X-Api-Key updating credentials
472 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
473 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNew
))
475 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
476 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
478 self
.assertEquals(r
.status_code
, 200)
480 # Make sure the old password is not usable any more
481 headers
= {'x-api-key': self
._webServerAPIKey
}
482 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
483 self
.assertEquals(r
.status_code
, 401)
485 def testBasicAuthOnlyChange(self
):
487 API: X-Api-Key updated to none (disabled)
490 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
491 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNew
))
493 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
494 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
496 self
.assertEquals(r
.status_code
, 200)
499 self
.sendConsoleCommand('setWebserverConfig({apiKey=""})')
501 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
502 self
.assertEquals(r
.status_code
, 401)