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']
45 class TestAPIBasics(APITestsBase
):
47 # paths accessible using the API key only
48 _apiOnlyPaths
= ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics']
49 # paths accessible using an API key or basic auth
50 _statsPaths
= [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost']
51 # paths accessible using basic auth only (list not exhaustive)
52 _basicOnlyPaths
= ['/', '/index.html']
55 def testBasicAuth(self
):
57 API: Basic Authentication
59 for path
in self
._basicOnlyPaths
+ self
._statsPaths
:
60 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
61 r
= requests
.get(url
, auth
=('whatever', "evilsecret"), timeout
=self
._webTimeout
)
62 self
.assertEqual(r
.status_code
, 401)
63 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
65 self
.assertEqual(r
.status_code
, 200)
67 def testXAPIKey(self
):
71 headers
= {'x-api-key': self
._webServerAPIKey
}
72 for path
in self
._apiOnlyPaths
+ self
._statsPaths
:
73 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
74 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
76 self
.assertEqual(r
.status_code
, 200)
78 def testWrongXAPIKey(self
):
82 headers
= {'x-api-key': "evilapikey"}
83 for path
in self
._apiOnlyPaths
+ self
._statsPaths
:
84 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
85 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
86 self
.assertEqual(r
.status_code
, 401)
88 def testBasicAuthOnly(self
):
90 API: Basic Authentication Only
92 headers
= {'x-api-key': self
._webServerAPIKey
}
93 for path
in self
._basicOnlyPaths
:
94 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
95 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
96 self
.assertEqual(r
.status_code
, 401)
98 def testAPIKeyOnly(self
):
102 for path
in self
._apiOnlyPaths
:
103 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
104 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
105 self
.assertEqual(r
.status_code
, 401)
107 def testServersLocalhost(self
):
109 API: /api/v1/servers/localhost
111 headers
= {'x-api-key': self
._webServerAPIKey
}
112 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
113 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
115 self
.assertEqual(r
.status_code
, 200)
116 self
.assertTrue(r
.json())
119 self
.assertEqual(content
['daemon_type'], 'dnsdist')
121 rule_groups
= ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules', 'rules']
122 for key
in ['version', 'acl', 'local', 'servers', 'frontends', 'pools'] + rule_groups
:
123 self
.assertIn(key
, content
)
125 for rule_group
in rule_groups
:
126 for rule
in content
[rule_group
]:
127 for key
in ['id', 'creationOrder', 'matches', 'rule', 'action', 'uuid']:
128 self
.assertIn(key
, rule
)
129 for key
in ['id', 'creationOrder', 'matches']:
130 self
.assertTrue(rule
[key
] >= 0)
132 for server
in content
['servers']:
133 for key
in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit',
134 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors',
135 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse',
136 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections',
137 'tcpNewConnections', 'tcpReusedConnections', 'tlsResumptions', 'tcpAvgQueriesPerConnection',
138 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol']:
139 self
.assertIn(key
, server
)
141 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
142 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']:
143 self
.assertTrue(server
[key
] >= 0)
145 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
147 for frontend
in content
['frontends']:
148 for key
in ['id', 'address', 'udp', 'tcp', 'type', 'queries', 'nonCompliantQueries']:
149 self
.assertIn(key
, frontend
)
151 for key
in ['id', 'queries', 'nonCompliantQueries']:
152 self
.assertTrue(frontend
[key
] >= 0)
154 for pool
in content
['pools']:
155 for key
in ['id', 'name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']:
156 self
.assertIn(key
, pool
)
158 for key
in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']:
159 self
.assertTrue(pool
[key
] >= 0)
161 stats
= content
['statistics']
162 for key
in self
._expectedMetrics
:
163 self
.assertIn(key
, stats
)
164 self
.assertTrue(stats
[key
] >= 0)
166 self
.assertIn(key
, self
._expectedMetrics
)
168 def testServersLocalhostPool(self
):
170 API: /api/v1/servers/localhost/pool?name=mypool
172 headers
= {'x-api-key': self
._webServerAPIKey
}
173 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/pool?name=mypool'
174 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
176 self
.assertEqual(r
.status_code
, 200)
177 self
.assertTrue(r
.json())
180 self
.assertIn('stats', content
)
181 self
.assertIn('servers', content
)
183 for key
in ['name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
184 self
.assertIn(key
, content
['stats'])
186 for key
in ['cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
187 self
.assertTrue(content
['stats'][key
] >= 0)
189 for server
in content
['servers']:
190 for key
in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit',
191 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors',
192 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse',
193 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections',
194 'tcpNewConnections', 'tcpReusedConnections', 'tcpAvgQueriesPerConnection',
195 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol']:
196 self
.assertIn(key
, server
)
198 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
199 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']:
200 self
.assertTrue(server
[key
] >= 0)
202 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
204 def testServersIDontExist(self
):
206 API: /api/v1/servers/idonotexist (should be 404)
208 headers
= {'x-api-key': self
._webServerAPIKey
}
209 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/idonotexist'
210 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
211 self
.assertEqual(r
.status_code
, 404)
213 def testServersLocalhostConfig(self
):
215 API: /api/v1/servers/localhost/config
217 headers
= {'x-api-key': self
._webServerAPIKey
}
218 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config'
219 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
221 self
.assertEqual(r
.status_code
, 200)
222 self
.assertTrue(r
.json())
225 for entry
in content
:
226 for key
in ['type', 'name', 'value']:
227 self
.assertIn(key
, entry
)
229 self
.assertEqual(entry
['type'], 'ConfigSetting')
230 values
[entry
['name']] = entry
['value']
232 for key
in ['acl', 'control-socket', 'ecs-override', 'ecs-source-prefix-v4',
233 'ecs-source-prefix-v6', 'fixup-case', 'max-outstanding', 'server-policy',
234 'stale-cache-entries-ttl', 'tcp-recv-timeout', 'tcp-send-timeout',
235 'truncate-tc', 'verbose', 'verbose-health-checks']:
236 self
.assertIn(key
, values
)
238 for key
in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout',
240 self
.assertTrue(values
[key
] >= 0)
242 self
.assertTrue(values
['ecs-source-prefix-v4'] >= 0 and values
['ecs-source-prefix-v4'] <= 32)
243 self
.assertTrue(values
['ecs-source-prefix-v6'] >= 0 and values
['ecs-source-prefix-v6'] <= 128)
245 def testServersLocalhostConfigAllowFrom(self
):
247 API: /api/v1/servers/localhost/config/allow-from
249 headers
= {'x-api-key': self
._webServerAPIKey
}
250 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
251 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
253 self
.assertEqual(r
.status_code
, 200)
254 self
.assertTrue(r
.json())
256 for key
in ['type', 'name', 'value']:
257 self
.assertIn(key
, content
)
259 self
.assertEqual(content
['name'], 'allow-from')
260 self
.assertEqual(content
['type'], 'ConfigSetting')
261 acl
= content
['value']
262 expectedACL
= ["127.0.0.1/32", "::1/128"]
265 self
.assertEqual(acl
, expectedACL
)
267 def testServersLocalhostConfigAllowFromPut(self
):
269 API: PUT /api/v1/servers/localhost/config/allow-from (should be refused)
271 The API is read-only by default, so this should be refused
273 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
274 payload
= json
.dumps({"name": "allow-from",
275 "type": "ConfigSetting",
277 headers
= {'x-api-key': self
._webServerAPIKey
}
278 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
279 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
281 self
.assertEqual(r
.status_code
, 405)
283 def testServersLocalhostStatistics(self
):
285 API: /api/v1/servers/localhost/statistics
287 headers
= {'x-api-key': self
._webServerAPIKey
}
288 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/statistics'
289 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
291 self
.assertEqual(r
.status_code
, 200)
292 self
.assertTrue(r
.json())
295 for entry
in content
:
296 self
.assertIn('type', entry
)
297 self
.assertIn('name', entry
)
298 self
.assertIn('value', entry
)
299 self
.assertEqual(entry
['type'], 'StatisticItem')
300 values
[entry
['name']] = entry
['value']
302 for key
in self
._expectedMetrics
:
303 self
.assertIn(key
, values
)
304 self
.assertTrue(values
[key
] >= 0)
307 self
.assertIn(key
, self
._expectedMetrics
)
309 def testJsonstatStats(self
):
311 API: /jsonstat?command=stats
313 headers
= {'x-api-key': self
._webServerAPIKey
}
314 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
315 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
317 self
.assertEqual(r
.status_code
, 200)
318 self
.assertTrue(r
.json())
321 for key
in self
._expectedMetrics
:
322 self
.assertIn(key
, content
)
323 self
.assertTrue(content
[key
] >= 0)
325 def testJsonstatDynblocklist(self
):
327 API: /jsonstat?command=dynblocklist
329 headers
= {'x-api-key': self
._webServerAPIKey
}
330 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=dynblocklist'
331 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
333 self
.assertEqual(r
.status_code
, 200)
338 for key
in ['reason', 'seconds', 'blocks', 'action']:
339 self
.assertIn(key
, content
)
341 for key
in ['blocks']:
342 self
.assertTrue(content
[key
] >= 0)
344 class TestAPIServerDown(APITestsBase
):
346 _config_template
= """
347 setACL({"127.0.0.1/32", "::1/128"})
348 newServer{address="127.0.0.1:%s"}
349 getServer(0):setDown()
350 webserver("127.0.0.1:%s")
351 setWebserverConfig({password="%s", apiKey="%s"})
354 def testServerDownNoLatencyLocalhost(self
):
356 API: /api/v1/servers/localhost, no latency for a down server
358 headers
= {'x-api-key': self
._webServerAPIKey
}
359 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
360 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
362 self
.assertEqual(r
.status_code
, 200)
363 self
.assertTrue(r
.json())
366 self
.assertEqual(content
['servers'][0]['latency'], None)
367 self
.assertEqual(content
['servers'][0]['tcpLatency'], None)
369 class TestAPIWritable(APITestsBase
):
371 _APIWriteDir
= '/tmp'
372 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_APIWriteDir']
373 _config_template
= """
374 setACL({"127.0.0.1/32", "::1/128"})
375 newServer{address="127.0.0.1:%s"}
376 webserver("127.0.0.1:%s")
377 setWebserverConfig({password="%s", apiKey="%s"})
378 setAPIWritable(true, "%s")
381 def testSetACL(self
):
385 headers
= {'x-api-key': self
._webServerAPIKey
}
386 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
387 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
389 self
.assertEqual(r
.status_code
, 200)
390 self
.assertTrue(r
.json())
392 acl
= content
['value']
393 expectedACL
= ["127.0.0.1/32", "::1/128"]
396 self
.assertEqual(acl
, expectedACL
)
398 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
399 payload
= json
.dumps({"name": "allow-from",
400 "type": "ConfigSetting",
402 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
404 self
.assertEqual(r
.status_code
, 200)
405 self
.assertTrue(r
.json())
407 acl
= content
['value']
409 self
.assertEqual(acl
, newACL
)
411 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
413 self
.assertEqual(r
.status_code
, 200)
414 self
.assertTrue(r
.json())
416 acl
= content
['value']
418 self
.assertEqual(acl
, newACL
)
420 configFile
= self
._APIWriteDir
+ '/' + 'acl.conf'
421 self
.assertTrue(os
.path
.isfile(configFile
))
423 with
open(configFile
, 'rt') as f
:
424 header
= f
.readline()
427 self
.assertEqual(header
, """-- Generated by the REST API, DO NOT EDIT\n""")
429 self
.assertIn(body
, {
430 """setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})\n""",
431 """setACL({"192.0.2.0/24", "203.0.113.0/24", "198.51.100.0/24"})\n""",
432 """setACL({"198.51.100.0/24", "192.0.2.0/24", "203.0.113.0/24"})\n""",
433 """setACL({"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24"})\n""",
434 """setACL({"203.0.113.0/24", "192.0.2.0/24", "198.51.100.0/24"})\n""",
435 """setACL({"203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"})\n"""
438 class TestAPICustomHeaders(APITestsBase
):
440 # paths accessible using the API key only
441 _apiOnlyPath
= '/api/v1/servers/localhost/config'
442 # paths accessible using basic auth only (list not exhaustive)
444 _consoleKey
= DNSDistTest
.generateConsoleKey()
445 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
446 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
447 _config_template
= """
449 controlSocket("127.0.0.1:%s")
450 setACL({"127.0.0.1/32", "::1/128"})
451 newServer({address="127.0.0.1:%s"})
452 webserver("127.0.0.1:%s")
453 setWebserverConfig({password="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} })
456 def testBasicHeaders(self
):
458 API: Basic custom headers
461 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
463 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
465 self
.assertEqual(r
.status_code
, 200)
466 self
.assertEqual(r
.headers
.get('x-custom'), "custom")
467 self
.assertFalse("x-frame-options" in r
.headers
)
469 def testBasicHeadersUpdate(self
):
471 API: Basic update of custom headers
474 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
475 self
.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})')
476 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
478 self
.assertEqual(r
.status_code
, 200)
479 self
.assertEqual(r
.headers
.get('x-powered-by'), "dnsdist")
480 self
.assertTrue("x-frame-options" in r
.headers
)
482 class TestStatsWithoutAuthentication(APITestsBase
):
484 # paths accessible using the API key only
485 _apiOnlyPath
= '/api/v1/servers/localhost/config'
486 # paths accessible using basic auth only (list not exhaustive)
488 _noAuthenticationPaths
= [ '/metrics', '/jsonstat?command=dynblocklist' ]
489 _consoleKey
= DNSDistTest
.generateConsoleKey()
490 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
491 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
492 _config_template
= """
494 controlSocket("127.0.0.1:%s")
495 setACL({"127.0.0.1/32", "::1/128"})
496 newServer({address="127.0.0.1:%s"})
497 webserver("127.0.0.1:%s")
498 setWebserverConfig({password="%s", apiKey="%s", statsRequireAuthentication=false })
503 API: Stats do not require authentication
506 for path
in self
._noAuthenticationPaths
:
507 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
509 r
= requests
.get(url
, timeout
=self
._webTimeout
)
511 self
.assertEqual(r
.status_code
, 200)
513 # these should still require basic authentication
514 for path
in [self
._basicOnlyPath
]:
515 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
517 r
= requests
.get(url
, timeout
=self
._webTimeout
)
518 self
.assertEqual(r
.status_code
, 401)
520 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
522 self
.assertEqual(r
.status_code
, 200)
524 # these should still require API authentication
525 for path
in [self
._apiOnlyPath
]:
526 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
528 r
= requests
.get(url
, timeout
=self
._webTimeout
)
529 self
.assertEqual(r
.status_code
, 401)
531 headers
= {'x-api-key': self
._webServerAPIKey
}
532 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
534 self
.assertEqual(r
.status_code
, 200)
536 class TestAPIAuth(APITestsBase
):
538 _webServerBasicAuthPasswordNew
= 'password'
539 _webServerBasicAuthPasswordNewHashed
= '$scrypt$ln=10,p=1,r=8$yefz8SAuT3lj3moXqUYvmw==$T98/RYMp76ZYNjd7MpAkcVXZEDqpLtrc3tQ52QflVBA='
540 _webServerAPIKeyNew
= 'apipassword'
541 _webServerAPIKeyNewHashed
= '$scrypt$ln=9,p=1,r=8$y96I9nfkY0LWDQEdSUzWgA==$jiyn9QD36o9d0ADrlqiIBk4AKyQrkD1KYw3CexwtHp4='
542 # paths accessible using the API key only
543 _apiOnlyPath
= '/api/v1/servers/localhost/config'
544 # paths accessible using basic auth only (list not exhaustive)
546 _consoleKey
= DNSDistTest
.generateConsoleKey()
547 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
548 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
549 _config_template
= """
551 controlSocket("127.0.0.1:%s")
552 setACL({"127.0.0.1/32", "::1/128"})
553 newServer{address="127.0.0.1:%s"}
554 webserver("127.0.0.1:%s")
555 setWebserverConfig({password="%s", apiKey="%s"})
558 def testBasicAuthChange(self
):
560 API: Basic Authentication updating credentials
563 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
564 self
.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self
._webServerBasicAuthPasswordNewHashed
))
566 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPasswordNew
), timeout
=self
._webTimeout
)
568 self
.assertEqual(r
.status_code
, 200)
570 # Make sure the old password is not usable any more
571 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
572 self
.assertEqual(r
.status_code
, 401)
574 def testXAPIKeyChange(self
):
576 API: X-Api-Key updating credentials
579 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
580 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
582 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
583 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
585 self
.assertEqual(r
.status_code
, 200)
587 # Make sure the old password is not usable any more
588 headers
= {'x-api-key': self
._webServerAPIKey
}
589 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
590 self
.assertEqual(r
.status_code
, 401)
592 def testBasicAuthOnlyChange(self
):
594 API: X-Api-Key updated to none (disabled)
597 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
598 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
600 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
601 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
603 self
.assertEqual(r
.status_code
, 200)
606 self
.sendConsoleCommand('setWebserverConfig({apiKey=""})')
608 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
609 self
.assertEqual(r
.status_code
, 401)
611 class TestAPIACL(APITestsBase
):
613 _consoleKey
= DNSDistTest
.generateConsoleKey()
614 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
615 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
616 _config_template
= """
618 controlSocket("127.0.0.1:%s")
619 setACL({"127.0.0.1/32", "::1/128"})
620 newServer{address="127.0.0.1:%s"}
621 webserver("127.0.0.1:%s")
622 setWebserverConfig({password="%s", apiKey="%s", acl="192.0.2.1"})
625 def testACLChange(self
):
627 API: Should be denied by ACL then allowed
630 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
632 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
633 self
.assertTrue(False)
634 except requests
.exceptions
.ConnectionError
as exp
:
638 self
.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
640 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
642 self
.assertEqual(r
.status_code
, 200)
644 class TestAPIWithoutAuthentication(APITestsBase
):
646 _apiPath
= '/api/v1/servers/localhost/config'
647 # paths accessible using basic auth only (list not exhaustive)
649 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
650 _config_template
= """
651 setACL({"127.0.0.1/32", "::1/128"})
652 newServer({address="127.0.0.1:%s"})
653 webserver("127.0.0.1:%s")
654 setWebserverConfig({password="%s", apiRequiresAuthentication=false })
659 API: API do not require authentication
662 for path
in [self
._apiPath
]:
663 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
665 r
= requests
.get(url
, timeout
=self
._webTimeout
)
667 self
.assertEqual(r
.status_code
, 200)
669 # these should still require basic authentication
670 for path
in [self
._basicOnlyPath
]:
671 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
673 r
= requests
.get(url
, timeout
=self
._webTimeout
)
674 self
.assertEqual(r
.status_code
, 401)
676 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
678 self
.assertEqual(r
.status_code
, 200)
680 class TestDashboardWithoutAuthentication(APITestsBase
):
683 _config_params
= ['_testServerPort', '_webServerPort']
684 _config_template
= """
685 setACL({"127.0.0.1/32", "::1/128"})
686 newServer({address="127.0.0.1:%d"})
687 webserver("127.0.0.1:%d")
688 setWebserverConfig({ dashboardRequiresAuthentication=false })
692 def testDashboard(self
):
694 API: Dashboard do not require authentication
697 for path
in [self
._basicPath
]:
698 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
700 r
= requests
.get(url
, timeout
=self
._webTimeout
)
702 self
.assertEqual(r
.status_code
, 200)
704 class TestCustomLuaEndpoint(APITestsBase
):
706 _config_template
= """
707 setACL({"127.0.0.1/32", "::1/128"})
708 newServer{address="127.0.0.1:%s"}
709 webserver("127.0.0.1:%s")
710 setWebserverConfig({password="%s"})
712 function customHTTPHandler(req, resp)
713 if req.path ~= '/foo' then
718 if req.version ~= 11 then
723 if req.method ~= 'GET' then
728 local get = req.getvars
729 if get['param'] ~= '42' then
734 local headers = req.headers
735 if headers['customheader'] ~= 'foobar' then
740 resp.body = 'It works!'
742 resp.headers = { ['Foo']='Bar'}
744 registerWebHandler('/foo', customHTTPHandler)
746 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
752 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/foo?param=42'
753 headers
= {'customheader': 'foobar'}
754 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
, headers
=headers
)
756 self
.assertEqual(r
.status_code
, 200)
757 self
.assertEqual(r
.content
, b
'It works!')
758 self
.assertEqual(r
.headers
.get('foo'), "Bar")
760 class TestWebConcurrentConnections(APITestsBase
):
764 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_maxConns']
765 _config_template
= """
766 newServer{address="127.0.0.1:%s"}
767 webserver("127.0.0.1:%s")
768 setWebserverConfig({password="%s", apiKey="%s", maxConcurrentConnections=%d})
771 def testConcurrentConnections(self
):
773 Web: Concurrent connections
777 # open the maximum number of connections
778 for _
in range(self
._maxConns
):
779 conn
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
780 conn
.connect(("127.0.0.1", self
._webServerPort
))
783 # we now hold all the slots, let's try to establish a new connection
784 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
785 self
.assertRaises(requests
.exceptions
.ConnectionError
, requests
.get
, url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
793 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
795 self
.assertEqual(r
.status_code
, 200)
797 class TestAPICustomStatistics(APITestsBase
):
801 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
802 _config_template
= """
803 newServer{address="127.0.0.1:%s"}
804 webserver("127.0.0.1:%s")
805 declareMetric("my-custom-metric", "counter", "Number of statistics")
806 declareMetric("my-other-metric", "counter", "Another number of statistics")
807 declareMetric("my-gauge", "gauge", "Current memory usage")
808 setWebserverConfig({password="%s", apiKey="%s"})
811 def testCustomStats(self
):
813 API: /jsonstat?command=stats
814 Test custom statistics are exposed
816 headers
= {'x-api-key': self
._webServerAPIKey
}
817 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
818 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
820 self
.assertEqual(r
.status_code
, 200)
821 self
.assertTrue(r
.json())
824 expected
= ['my-custom-metric', 'my-other-metric', 'my-gauge']
827 self
.assertIn(key
, content
)
828 self
.assertTrue(content
[key
] >= 0)