9 from dnsdisttests
import DNSDistTest
, pickAvailablePort
11 class APITestsBase(DNSDistTest
):
14 _webServerPort
= pickAvailablePort()
15 _webServerBasicAuthPassword
= 'secret'
16 _webServerBasicAuthPasswordHashed
= '$scrypt$ln=10,p=1,r=8$6DKLnvUYEeXWh3JNOd3iwg==$kSrhdHaRbZ7R74q3lGBqO1xetgxRxhmWzYJ2Qvfm7JM='
17 _webServerAPIKey
= 'apisecret'
18 _webServerAPIKeyHashed
= '$scrypt$ln=10,p=1,r=8$9v8JxDfzQVyTpBkTbkUqYg==$bDQzAOHeK1G9UvTPypNhrX48w974ZXbFPtRKS34+aso='
19 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
20 _config_template
= """
21 setACL({"127.0.0.1/32", "::1/128"})
22 newServer{address="127.0.0.1:%s", pool={'', 'mypool'}}
23 webserver("127.0.0.1:%s")
24 setWebserverConfig({password="%s", apiKey="%s"})
26 _expectedMetrics
= ['responses', 'servfail-responses', 'queries', 'acl-drops',
27 'frontend-noerror', 'frontend-nxdomain', 'frontend-servfail',
28 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts',
29 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1',
30 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000',
31 'latency-slow', 'latency-sum', 'latency-count', 'latency-avg100', 'latency-avg1000',
32 'latency-avg10000', 'latency-avg1000000', 'latency-tcp-avg100', 'latency-tcp-avg1000',
33 'latency-tcp-avg10000', 'latency-tcp-avg1000000', 'latency-dot-avg100', 'latency-dot-avg1000',
34 'latency-dot-avg10000', 'latency-dot-avg1000000', 'latency-doh-avg100', 'latency-doh-avg1000',
35 'latency-doh-avg10000', 'latency-doh-avg1000000', 'uptime', 'real-memory-usage', 'noncompliant-queries',
36 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits',
37 'cache-misses', 'cpu-iowait', 'cpu-steal', 'cpu-sys-msec', 'cpu-user-msec', 'fd-usage', 'dyn-blocked',
38 'dyn-block-nmg-size', 'rule-servfail', 'rule-truncated', 'security-status',
39 'udp-in-csum-errors', 'udp-in-errors', 'udp-noport-errors', 'udp-recvbuf-errors', 'udp-sndbuf-errors',
40 'udp6-in-errors', 'udp6-recvbuf-errors', 'udp6-sndbuf-errors', 'udp6-noport-errors', 'udp6-in-csum-errors',
41 'doh-query-pipe-full', 'doh-response-pipe-full', 'doq-response-pipe-full', 'proxy-protocol-invalid', 'tcp-listen-overflows',
42 'outgoing-doh-query-pipe-full', 'tcp-query-pipe-full', 'tcp-cross-protocol-query-pipe-full',
43 'tcp-cross-protocol-response-pipe-full']
51 cls
.waitForTCPSocket('127.0.0.1', cls
._webServerPort
)
52 print("Launching tests..")
54 class TestAPIBasics(APITestsBase
):
56 # paths accessible using the API key only
57 _apiOnlyPaths
= ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics']
58 # paths accessible using an API key or basic auth
59 _statsPaths
= [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost']
60 # paths accessible using basic auth only (list not exhaustive)
61 _basicOnlyPaths
= ['/', '/index.html']
64 def testBasicAuth(self
):
66 API: Basic Authentication
68 for path
in self
._basicOnlyPaths
+ self
._statsPaths
:
69 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
70 r
= requests
.get(url
, auth
=('whatever', "evilsecret"), timeout
=self
._webTimeout
)
71 self
.assertEqual(r
.status_code
, 401)
72 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
74 self
.assertEqual(r
.status_code
, 200)
76 def testXAPIKey(self
):
80 headers
= {'x-api-key': self
._webServerAPIKey
}
81 for path
in self
._apiOnlyPaths
+ self
._statsPaths
:
82 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
83 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
85 self
.assertEqual(r
.status_code
, 200)
87 def testWrongXAPIKey(self
):
91 headers
= {'x-api-key': "evilapikey"}
92 for path
in self
._apiOnlyPaths
+ self
._statsPaths
:
93 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
94 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
95 self
.assertEqual(r
.status_code
, 401)
97 def testBasicAuthOnly(self
):
99 API: Basic Authentication Only
101 headers
= {'x-api-key': self
._webServerAPIKey
}
102 for path
in self
._basicOnlyPaths
:
103 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
104 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
105 self
.assertEqual(r
.status_code
, 401)
107 def testAPIKeyOnly(self
):
111 for path
in self
._apiOnlyPaths
:
112 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
113 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
114 self
.assertEqual(r
.status_code
, 401)
116 def testServersLocalhost(self
):
118 API: /api/v1/servers/localhost
120 headers
= {'x-api-key': self
._webServerAPIKey
}
121 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
122 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
124 self
.assertEqual(r
.status_code
, 200)
125 self
.assertTrue(r
.json())
128 self
.assertEqual(content
['daemon_type'], 'dnsdist')
130 rule_groups
= ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules', 'rules']
131 for key
in ['version', 'acl', 'local', 'servers', 'frontends', 'pools'] + rule_groups
:
132 self
.assertIn(key
, content
)
134 for rule_group
in rule_groups
:
135 for rule
in content
[rule_group
]:
136 for key
in ['id', 'creationOrder', 'matches', 'rule', 'action', 'uuid']:
137 self
.assertIn(key
, rule
)
138 for key
in ['id', 'creationOrder', 'matches']:
139 self
.assertTrue(rule
[key
] >= 0)
141 for server
in content
['servers']:
142 for key
in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit',
143 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors',
144 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse',
145 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections',
146 'tcpNewConnections', 'tcpReusedConnections', 'tlsResumptions', 'tcpAvgQueriesPerConnection',
147 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol', 'healthCheckFailures', 'healthCheckFailuresParsing', 'healthCheckFailuresTimeout', 'healthCheckFailuresNetwork', 'healthCheckFailuresMismatch', 'healthCheckFailuresInvalid']:
148 self
.assertIn(key
, server
)
150 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
151 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']:
152 self
.assertTrue(server
[key
] >= 0)
154 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
156 for frontend
in content
['frontends']:
157 for key
in ['id', 'address', 'udp', 'tcp', 'type', 'queries', 'nonCompliantQueries']:
158 self
.assertIn(key
, frontend
)
160 for key
in ['id', 'queries', 'nonCompliantQueries']:
161 self
.assertTrue(frontend
[key
] >= 0)
163 for pool
in content
['pools']:
164 for key
in ['id', 'name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']:
165 self
.assertIn(key
, pool
)
167 for key
in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']:
168 self
.assertTrue(pool
[key
] >= 0)
170 stats
= content
['statistics']
171 for key
in self
._expectedMetrics
:
172 self
.assertIn(key
, stats
)
173 self
.assertTrue(stats
[key
] >= 0)
175 self
.assertIn(key
, self
._expectedMetrics
)
177 def testServersLocalhostPool(self
):
179 API: /api/v1/servers/localhost/pool?name=mypool
181 headers
= {'x-api-key': self
._webServerAPIKey
}
182 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/pool?name=mypool'
183 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
185 self
.assertEqual(r
.status_code
, 200)
186 self
.assertTrue(r
.json())
189 self
.assertIn('stats', content
)
190 self
.assertIn('servers', content
)
192 for key
in ['name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
193 self
.assertIn(key
, content
['stats'])
195 for key
in ['cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
196 self
.assertTrue(content
['stats'][key
] >= 0)
198 for server
in content
['servers']:
199 for key
in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit',
200 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors',
201 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse',
202 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections',
203 'tcpNewConnections', 'tcpReusedConnections', 'tcpAvgQueriesPerConnection',
204 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol']:
205 self
.assertIn(key
, server
)
207 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
208 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']:
209 self
.assertTrue(server
[key
] >= 0)
211 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
213 def testServersIDontExist(self
):
215 API: /api/v1/servers/idonotexist (should be 404)
217 headers
= {'x-api-key': self
._webServerAPIKey
}
218 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/idonotexist'
219 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
220 self
.assertEqual(r
.status_code
, 404)
222 def testServersLocalhostConfig(self
):
224 API: /api/v1/servers/localhost/config
226 headers
= {'x-api-key': self
._webServerAPIKey
}
227 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config'
228 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
230 self
.assertEqual(r
.status_code
, 200)
231 self
.assertTrue(r
.json())
234 for entry
in content
:
235 for key
in ['type', 'name', 'value']:
236 self
.assertIn(key
, entry
)
238 self
.assertEqual(entry
['type'], 'ConfigSetting')
239 values
[entry
['name']] = entry
['value']
241 for key
in ['acl', 'control-socket', 'ecs-override', 'ecs-source-prefix-v4',
242 'ecs-source-prefix-v6', 'fixup-case', 'max-outstanding', 'server-policy',
243 'stale-cache-entries-ttl', 'tcp-recv-timeout', 'tcp-send-timeout',
244 'truncate-tc', 'verbose', 'verbose-health-checks']:
245 self
.assertIn(key
, values
)
247 for key
in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout',
249 self
.assertTrue(values
[key
] >= 0)
251 self
.assertTrue(values
['ecs-source-prefix-v4'] >= 0 and values
['ecs-source-prefix-v4'] <= 32)
252 self
.assertTrue(values
['ecs-source-prefix-v6'] >= 0 and values
['ecs-source-prefix-v6'] <= 128)
254 def testServersLocalhostConfigAllowFrom(self
):
256 API: /api/v1/servers/localhost/config/allow-from
258 headers
= {'x-api-key': self
._webServerAPIKey
}
259 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
260 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
262 self
.assertEqual(r
.status_code
, 200)
263 self
.assertTrue(r
.json())
265 for key
in ['type', 'name', 'value']:
266 self
.assertIn(key
, content
)
268 self
.assertEqual(content
['name'], 'allow-from')
269 self
.assertEqual(content
['type'], 'ConfigSetting')
270 acl
= content
['value']
271 expectedACL
= ["127.0.0.1/32", "::1/128"]
274 self
.assertEqual(acl
, expectedACL
)
276 def testServersLocalhostConfigAllowFromPut(self
):
278 API: PUT /api/v1/servers/localhost/config/allow-from (should be refused)
280 The API is read-only by default, so this should be refused
282 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
283 payload
= json
.dumps({"name": "allow-from",
284 "type": "ConfigSetting",
286 headers
= {'x-api-key': self
._webServerAPIKey
}
287 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
288 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
290 self
.assertEqual(r
.status_code
, 405)
292 def testServersLocalhostStatistics(self
):
294 API: /api/v1/servers/localhost/statistics
296 headers
= {'x-api-key': self
._webServerAPIKey
}
297 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/statistics'
298 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
300 self
.assertEqual(r
.status_code
, 200)
301 self
.assertTrue(r
.json())
304 for entry
in content
:
305 self
.assertIn('type', entry
)
306 self
.assertIn('name', entry
)
307 self
.assertIn('value', entry
)
308 self
.assertEqual(entry
['type'], 'StatisticItem')
309 values
[entry
['name']] = entry
['value']
311 for key
in self
._expectedMetrics
:
312 self
.assertIn(key
, values
)
313 self
.assertTrue(values
[key
] >= 0)
316 self
.assertIn(key
, self
._expectedMetrics
)
318 def testJsonstatStats(self
):
320 API: /jsonstat?command=stats
322 headers
= {'x-api-key': self
._webServerAPIKey
}
323 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
324 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
326 self
.assertEqual(r
.status_code
, 200)
327 self
.assertTrue(r
.json())
330 for key
in self
._expectedMetrics
:
331 self
.assertIn(key
, content
)
332 self
.assertTrue(content
[key
] >= 0)
334 def testJsonstatDynblocklist(self
):
336 API: /jsonstat?command=dynblocklist
338 headers
= {'x-api-key': self
._webServerAPIKey
}
339 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=dynblocklist'
340 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
342 self
.assertEqual(r
.status_code
, 200)
347 for key
in ['reason', 'seconds', 'blocks', 'action']:
348 self
.assertIn(key
, content
)
350 for key
in ['blocks']:
351 self
.assertTrue(content
[key
] >= 0)
353 class TestAPIServerDown(APITestsBase
):
355 _config_template
= """
356 setACL({"127.0.0.1/32", "::1/128"})
357 newServer{address="127.0.0.1:%s"}
358 getServer(0):setDown()
359 webserver("127.0.0.1:%s")
360 setWebserverConfig({password="%s", apiKey="%s"})
363 def testServerDownNoLatencyLocalhost(self
):
365 API: /api/v1/servers/localhost, no latency for a down server
367 headers
= {'x-api-key': self
._webServerAPIKey
}
368 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
369 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
371 self
.assertEqual(r
.status_code
, 200)
372 self
.assertTrue(r
.json())
375 self
.assertEqual(content
['servers'][0]['latency'], None)
376 self
.assertEqual(content
['servers'][0]['tcpLatency'], None)
378 class TestAPIWritable(APITestsBase
):
380 _APIWriteDir
= '/tmp'
381 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_APIWriteDir']
382 _config_template
= """
383 setACL({"127.0.0.1/32", "::1/128"})
384 newServer{address="127.0.0.1:%s"}
385 webserver("127.0.0.1:%s")
386 setWebserverConfig({password="%s", apiKey="%s"})
387 setAPIWritable(true, "%s")
390 def testSetACL(self
):
394 headers
= {'x-api-key': self
._webServerAPIKey
}
395 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
396 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
398 self
.assertEqual(r
.status_code
, 200)
399 self
.assertTrue(r
.json())
401 acl
= content
['value']
402 expectedACL
= ["127.0.0.1/32", "::1/128"]
405 self
.assertEqual(acl
, expectedACL
)
407 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
408 payload
= json
.dumps({"name": "allow-from",
409 "type": "ConfigSetting",
411 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
413 self
.assertEqual(r
.status_code
, 200)
414 self
.assertTrue(r
.json())
416 acl
= content
['value']
418 self
.assertEqual(acl
, newACL
)
420 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
422 self
.assertEqual(r
.status_code
, 200)
423 self
.assertTrue(r
.json())
425 acl
= content
['value']
427 self
.assertEqual(acl
, newACL
)
429 configFile
= self
._APIWriteDir
+ '/' + 'acl.conf'
430 self
.assertTrue(os
.path
.isfile(configFile
))
432 with
open(configFile
, 'rt') as f
:
433 header
= f
.readline()
436 self
.assertEqual(header
, """-- Generated by the REST API, DO NOT EDIT\n""")
438 self
.assertIn(body
, {
439 """setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})\n""",
440 """setACL({"192.0.2.0/24", "203.0.113.0/24", "198.51.100.0/24"})\n""",
441 """setACL({"198.51.100.0/24", "192.0.2.0/24", "203.0.113.0/24"})\n""",
442 """setACL({"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24"})\n""",
443 """setACL({"203.0.113.0/24", "192.0.2.0/24", "198.51.100.0/24"})\n""",
444 """setACL({"203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"})\n"""
447 class TestAPICustomHeaders(APITestsBase
):
449 # paths accessible using the API key only
450 _apiOnlyPath
= '/api/v1/servers/localhost/config'
451 # paths accessible using basic auth only (list not exhaustive)
453 _consoleKey
= DNSDistTest
.generateConsoleKey()
454 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
455 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
456 _config_template
= """
458 controlSocket("127.0.0.1:%s")
459 setACL({"127.0.0.1/32", "::1/128"})
460 newServer({address="127.0.0.1:%s"})
461 webserver("127.0.0.1:%s")
462 setWebserverConfig({password="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} })
465 def testBasicHeaders(self
):
467 API: Basic custom headers
470 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
472 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
474 self
.assertEqual(r
.status_code
, 200)
475 self
.assertEqual(r
.headers
.get('x-custom'), "custom")
476 self
.assertFalse("x-frame-options" in r
.headers
)
478 def testBasicHeadersUpdate(self
):
480 API: Basic update of custom headers
483 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
484 self
.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})')
485 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
487 self
.assertEqual(r
.status_code
, 200)
488 self
.assertEqual(r
.headers
.get('x-powered-by'), "dnsdist")
489 self
.assertTrue("x-frame-options" in r
.headers
)
491 class TestStatsWithoutAuthentication(APITestsBase
):
493 # paths accessible using the API key only
494 _apiOnlyPath
= '/api/v1/servers/localhost/config'
495 # paths accessible using basic auth only (list not exhaustive)
497 _noAuthenticationPaths
= [ '/metrics', '/jsonstat?command=dynblocklist' ]
498 _consoleKey
= DNSDistTest
.generateConsoleKey()
499 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
500 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
501 _config_template
= """
503 controlSocket("127.0.0.1:%s")
504 setACL({"127.0.0.1/32", "::1/128"})
505 newServer({address="127.0.0.1:%s"})
506 webserver("127.0.0.1:%s")
507 setWebserverConfig({password="%s", apiKey="%s", statsRequireAuthentication=false })
512 API: Stats do not require authentication
515 for path
in self
._noAuthenticationPaths
:
516 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
518 r
= requests
.get(url
, timeout
=self
._webTimeout
)
520 self
.assertEqual(r
.status_code
, 200)
522 # these should still require basic authentication
523 for path
in [self
._basicOnlyPath
]:
524 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
526 r
= requests
.get(url
, timeout
=self
._webTimeout
)
527 self
.assertEqual(r
.status_code
, 401)
529 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
531 self
.assertEqual(r
.status_code
, 200)
533 # these should still require API authentication
534 for path
in [self
._apiOnlyPath
]:
535 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
537 r
= requests
.get(url
, timeout
=self
._webTimeout
)
538 self
.assertEqual(r
.status_code
, 401)
540 headers
= {'x-api-key': self
._webServerAPIKey
}
541 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
543 self
.assertEqual(r
.status_code
, 200)
545 class TestAPIAuth(APITestsBase
):
547 _webServerBasicAuthPasswordNew
= 'password'
548 _webServerBasicAuthPasswordNewHashed
= '$scrypt$ln=10,p=1,r=8$yefz8SAuT3lj3moXqUYvmw==$T98/RYMp76ZYNjd7MpAkcVXZEDqpLtrc3tQ52QflVBA='
549 _webServerAPIKeyNew
= 'apipassword'
550 _webServerAPIKeyNewHashed
= '$scrypt$ln=9,p=1,r=8$y96I9nfkY0LWDQEdSUzWgA==$jiyn9QD36o9d0ADrlqiIBk4AKyQrkD1KYw3CexwtHp4='
551 # paths accessible using the API key only
552 _apiOnlyPath
= '/api/v1/servers/localhost/config'
553 # paths accessible using basic auth only (list not exhaustive)
555 _consoleKey
= DNSDistTest
.generateConsoleKey()
556 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
557 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
558 _config_template
= """
560 controlSocket("127.0.0.1:%s")
561 setACL({"127.0.0.1/32", "::1/128"})
562 newServer{address="127.0.0.1:%s"}
563 webserver("127.0.0.1:%s")
564 setWebserverConfig({password="%s", apiKey="%s"})
567 def testBasicAuthChange(self
):
569 API: Basic Authentication updating credentials
572 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
573 self
.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self
._webServerBasicAuthPasswordNewHashed
))
575 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPasswordNew
), timeout
=self
._webTimeout
)
577 self
.assertEqual(r
.status_code
, 200)
579 # Make sure the old password is not usable any more
580 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
581 self
.assertEqual(r
.status_code
, 401)
583 def testXAPIKeyChange(self
):
585 API: X-Api-Key updating credentials
588 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
589 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
591 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
592 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
594 self
.assertEqual(r
.status_code
, 200)
596 # Make sure the old password is not usable any more
597 headers
= {'x-api-key': self
._webServerAPIKey
}
598 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
599 self
.assertEqual(r
.status_code
, 401)
601 def testBasicAuthOnlyChange(self
):
603 API: X-Api-Key updated to none (disabled)
606 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
607 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
609 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
610 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
612 self
.assertEqual(r
.status_code
, 200)
615 self
.sendConsoleCommand('setWebserverConfig({apiKey=""})')
617 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
618 self
.assertEqual(r
.status_code
, 401)
620 class TestAPIACL(APITestsBase
):
622 _consoleKey
= DNSDistTest
.generateConsoleKey()
623 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
624 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
625 _config_template
= """
627 controlSocket("127.0.0.1:%s")
628 setACL({"127.0.0.1/32", "::1/128"})
629 newServer{address="127.0.0.1:%s"}
630 webserver("127.0.0.1:%s")
631 setWebserverConfig({password="%s", apiKey="%s", acl="192.0.2.1"})
634 def testACLChange(self
):
636 API: Should be denied by ACL then allowed
639 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
641 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
642 self
.assertTrue(False)
643 except requests
.exceptions
.ConnectionError
as exp
:
647 self
.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
649 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
651 self
.assertEqual(r
.status_code
, 200)
653 class TestAPIWithoutAuthentication(APITestsBase
):
655 _apiPath
= '/api/v1/servers/localhost/config'
656 # paths accessible using basic auth only (list not exhaustive)
658 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
659 _config_template
= """
660 setACL({"127.0.0.1/32", "::1/128"})
661 newServer({address="127.0.0.1:%s"})
662 webserver("127.0.0.1:%s")
663 setWebserverConfig({password="%s", apiRequiresAuthentication=false })
668 API: API do not require authentication
671 for path
in [self
._apiPath
]:
672 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
674 r
= requests
.get(url
, timeout
=self
._webTimeout
)
676 self
.assertEqual(r
.status_code
, 200)
678 # these should still require basic authentication
679 for path
in [self
._basicOnlyPath
]:
680 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
682 r
= requests
.get(url
, timeout
=self
._webTimeout
)
683 self
.assertEqual(r
.status_code
, 401)
685 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
687 self
.assertEqual(r
.status_code
, 200)
689 class TestDashboardWithoutAuthentication(APITestsBase
):
692 _config_params
= ['_testServerPort', '_webServerPort']
693 _config_template
= """
694 setACL({"127.0.0.1/32", "::1/128"})
695 newServer({address="127.0.0.1:%d"})
696 webserver("127.0.0.1:%d")
697 setWebserverConfig({ dashboardRequiresAuthentication=false })
701 def testDashboard(self
):
703 API: Dashboard do not require authentication
706 for path
in [self
._basicPath
]:
707 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
709 r
= requests
.get(url
, timeout
=self
._webTimeout
)
711 self
.assertEqual(r
.status_code
, 200)
713 class TestCustomLuaEndpoint(APITestsBase
):
715 _config_template
= """
716 setACL({"127.0.0.1/32", "::1/128"})
717 newServer{address="127.0.0.1:%s"}
718 webserver("127.0.0.1:%s")
719 setWebserverConfig({password="%s"})
721 function customHTTPHandler(req, resp)
722 if req.path ~= '/foo' then
727 if req.version ~= 11 then
732 if req.method ~= 'GET' then
737 local get = req.getvars
738 if get['param'] ~= '42' then
743 local headers = req.headers
744 if headers['customheader'] ~= 'foobar' then
749 resp.body = 'It works!'
751 resp.headers = { ['Foo']='Bar'}
753 registerWebHandler('/foo', customHTTPHandler)
755 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
761 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/foo?param=42'
762 headers
= {'customheader': 'foobar'}
763 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
, headers
=headers
)
765 self
.assertEqual(r
.status_code
, 200)
766 self
.assertEqual(r
.content
, b
'It works!')
767 self
.assertEqual(r
.headers
.get('foo'), "Bar")
769 class TestWebConcurrentConnections(APITestsBase
):
773 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_maxConns']
774 _config_template
= """
775 newServer{address="127.0.0.1:%s"}
776 webserver("127.0.0.1:%s")
777 setWebserverConfig({password="%s", apiKey="%s", maxConcurrentConnections=%d})
782 cls
.startResponders()
785 # do no check if the web server socket is up, because this
786 # might mess up the concurrent connections counter
788 def testConcurrentConnections(self
):
790 Web: Concurrent connections
794 # open the maximum number of connections
795 for _
in range(self
._maxConns
):
796 conn
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
797 conn
.connect(("127.0.0.1", self
._webServerPort
))
800 # we now hold all the slots, let's try to establish a new connection
801 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
802 self
.assertRaises(requests
.exceptions
.ConnectionError
, requests
.get
, url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
810 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
812 self
.assertEqual(r
.status_code
, 200)
814 class TestAPICustomStatistics(APITestsBase
):
818 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
819 _config_template
= """
820 newServer{address="127.0.0.1:%s"}
821 webserver("127.0.0.1:%s")
822 declareMetric("my-custom-metric", "counter", "Number of statistics")
823 declareMetric("my-other-metric", "counter", "Another number of statistics")
824 declareMetric("my-gauge", "gauge", "Current memory usage")
825 setWebserverConfig({password="%s", apiKey="%s"})
828 def testCustomStats(self
):
830 API: /jsonstat?command=stats
831 Test custom statistics are exposed
833 headers
= {'x-api-key': self
._webServerAPIKey
}
834 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
835 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
837 self
.assertEqual(r
.status_code
, 200)
838 self
.assertTrue(r
.json())
841 expected
= ['my-custom-metric', 'my-other-metric', 'my-gauge']
844 self
.assertIn(key
, content
)
845 self
.assertTrue(content
[key
] >= 0)