9 from dnsdisttests
import DNSDistTest
11 class APITestsBase(DNSDistTest
):
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', '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']:
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)