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', 'uptime', 'real-memory-usage', 'noncompliant-queries',
33 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits',
34 'cache-misses', 'cpu-iowait', 'cpu-steal', 'cpu-sys-msec', 'cpu-user-msec', 'fd-usage', 'dyn-blocked',
35 'dyn-block-nmg-size', 'rule-servfail', 'rule-truncated', 'security-status',
36 'udp-in-csum-errors', 'udp-in-errors', 'udp-noport-errors', 'udp-recvbuf-errors', 'udp-sndbuf-errors',
37 'udp6-in-errors', 'udp6-recvbuf-errors', 'udp6-sndbuf-errors', 'udp6-noport-errors', 'udp6-in-csum-errors',
38 'doh-query-pipe-full', 'doh-response-pipe-full', 'proxy-protocol-invalid', 'tcp-listen-overflows',
39 'outgoing-doh-query-pipe-full', 'tcp-query-pipe-full', 'tcp-cross-protocol-query-pipe-full',
40 'tcp-cross-protocol-response-pipe-full']
42 class TestAPIBasics(APITestsBase
):
44 # paths accessible using the API key only
45 _apiOnlyPaths
= ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics']
46 # paths accessible using an API key or basic auth
47 _statsPaths
= [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost']
48 # paths accessible using basic auth only (list not exhaustive)
49 _basicOnlyPaths
= ['/', '/index.html']
52 def testBasicAuth(self
):
54 API: Basic Authentication
56 for path
in self
._basicOnlyPaths
+ self
._statsPaths
:
57 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
58 r
= requests
.get(url
, auth
=('whatever', "evilsecret"), timeout
=self
._webTimeout
)
59 self
.assertEqual(r
.status_code
, 401)
60 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
62 self
.assertEqual(r
.status_code
, 200)
64 def testXAPIKey(self
):
68 headers
= {'x-api-key': self
._webServerAPIKey
}
69 for path
in self
._apiOnlyPaths
+ self
._statsPaths
:
70 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
71 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
73 self
.assertEqual(r
.status_code
, 200)
75 def testWrongXAPIKey(self
):
79 headers
= {'x-api-key': "evilapikey"}
80 for path
in self
._apiOnlyPaths
+ self
._statsPaths
:
81 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
82 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
83 self
.assertEqual(r
.status_code
, 401)
85 def testBasicAuthOnly(self
):
87 API: Basic Authentication Only
89 headers
= {'x-api-key': self
._webServerAPIKey
}
90 for path
in self
._basicOnlyPaths
:
91 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
92 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
93 self
.assertEqual(r
.status_code
, 401)
95 def testAPIKeyOnly(self
):
99 for path
in self
._apiOnlyPaths
:
100 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
101 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
102 self
.assertEqual(r
.status_code
, 401)
104 def testServersLocalhost(self
):
106 API: /api/v1/servers/localhost
108 headers
= {'x-api-key': self
._webServerAPIKey
}
109 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
110 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
112 self
.assertEqual(r
.status_code
, 200)
113 self
.assertTrue(r
.json())
116 self
.assertEqual(content
['daemon_type'], 'dnsdist')
118 rule_groups
= ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules', 'rules']
119 for key
in ['version', 'acl', 'local', 'servers', 'frontends', 'pools'] + rule_groups
:
120 self
.assertIn(key
, content
)
122 for rule_group
in rule_groups
:
123 for rule
in content
[rule_group
]:
124 for key
in ['id', 'creationOrder', 'matches', 'rule', 'action', 'uuid']:
125 self
.assertIn(key
, rule
)
126 for key
in ['id', 'creationOrder', 'matches']:
127 self
.assertTrue(rule
[key
] >= 0)
129 for server
in content
['servers']:
130 for key
in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit',
131 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors',
132 'dropRate', 'responses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse',
133 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections',
134 'tcpNewConnections', 'tcpReusedConnections', 'tlsResumptions', 'tcpAvgQueriesPerConnection',
135 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol']:
136 self
.assertIn(key
, server
)
138 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
139 'qps', 'queries', 'order', 'tcpLatency']:
140 self
.assertTrue(server
[key
] >= 0)
142 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
144 for frontend
in content
['frontends']:
145 for key
in ['id', 'address', 'udp', 'tcp', 'type', 'queries']:
146 self
.assertIn(key
, frontend
)
148 for key
in ['id', 'queries']:
149 self
.assertTrue(frontend
[key
] >= 0)
151 for pool
in content
['pools']:
152 for key
in ['id', 'name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']:
153 self
.assertIn(key
, pool
)
155 for key
in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']:
156 self
.assertTrue(pool
[key
] >= 0)
158 stats
= content
['statistics']
159 for key
in self
._expectedMetrics
:
160 self
.assertIn(key
, stats
)
161 self
.assertTrue(stats
[key
] >= 0)
163 self
.assertIn(key
, self
._expectedMetrics
)
165 def testServersLocalhostPool(self
):
167 API: /api/v1/servers/localhost/pool?name=mypool
169 headers
= {'x-api-key': self
._webServerAPIKey
}
170 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/pool?name=mypool'
171 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
173 self
.assertEqual(r
.status_code
, 200)
174 self
.assertTrue(r
.json())
177 self
.assertIn('stats', content
)
178 self
.assertIn('servers', content
)
180 for key
in ['name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
181 self
.assertIn(key
, content
['stats'])
183 for key
in ['cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
184 self
.assertTrue(content
['stats'][key
] >= 0)
186 for server
in content
['servers']:
187 for key
in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit',
188 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors',
189 'dropRate', 'responses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse',
190 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections',
191 'tcpNewConnections', 'tcpReusedConnections', 'tcpAvgQueriesPerConnection',
192 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol']:
193 self
.assertIn(key
, server
)
195 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
196 'qps', 'queries', 'order', 'tcpLatency']:
197 self
.assertTrue(server
[key
] >= 0)
199 self
.assertTrue(server
['state'] in ['up', 'down', 'UP', 'DOWN'])
201 def testServersIDontExist(self
):
203 API: /api/v1/servers/idonotexist (should be 404)
205 headers
= {'x-api-key': self
._webServerAPIKey
}
206 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/idonotexist'
207 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
208 self
.assertEqual(r
.status_code
, 404)
210 def testServersLocalhostConfig(self
):
212 API: /api/v1/servers/localhost/config
214 headers
= {'x-api-key': self
._webServerAPIKey
}
215 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config'
216 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
218 self
.assertEqual(r
.status_code
, 200)
219 self
.assertTrue(r
.json())
222 for entry
in content
:
223 for key
in ['type', 'name', 'value']:
224 self
.assertIn(key
, entry
)
226 self
.assertEqual(entry
['type'], 'ConfigSetting')
227 values
[entry
['name']] = entry
['value']
229 for key
in ['acl', 'control-socket', 'ecs-override', 'ecs-source-prefix-v4',
230 'ecs-source-prefix-v6', 'fixup-case', 'max-outstanding', 'server-policy',
231 'stale-cache-entries-ttl', 'tcp-recv-timeout', 'tcp-send-timeout',
232 'truncate-tc', 'verbose', 'verbose-health-checks']:
233 self
.assertIn(key
, values
)
235 for key
in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout',
237 self
.assertTrue(values
[key
] >= 0)
239 self
.assertTrue(values
['ecs-source-prefix-v4'] >= 0 and values
['ecs-source-prefix-v4'] <= 32)
240 self
.assertTrue(values
['ecs-source-prefix-v6'] >= 0 and values
['ecs-source-prefix-v6'] <= 128)
242 def testServersLocalhostConfigAllowFrom(self
):
244 API: /api/v1/servers/localhost/config/allow-from
246 headers
= {'x-api-key': self
._webServerAPIKey
}
247 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
248 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
250 self
.assertEqual(r
.status_code
, 200)
251 self
.assertTrue(r
.json())
253 for key
in ['type', 'name', 'value']:
254 self
.assertIn(key
, content
)
256 self
.assertEqual(content
['name'], 'allow-from')
257 self
.assertEqual(content
['type'], 'ConfigSetting')
258 acl
= content
['value']
259 expectedACL
= ["127.0.0.1/32", "::1/128"]
262 self
.assertEqual(acl
, expectedACL
)
264 def testServersLocalhostConfigAllowFromPut(self
):
266 API: PUT /api/v1/servers/localhost/config/allow-from (should be refused)
268 The API is read-only by default, so this should be refused
270 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
271 payload
= json
.dumps({"name": "allow-from",
272 "type": "ConfigSetting",
274 headers
= {'x-api-key': self
._webServerAPIKey
}
275 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
276 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
278 self
.assertEqual(r
.status_code
, 405)
280 def testServersLocalhostStatistics(self
):
282 API: /api/v1/servers/localhost/statistics
284 headers
= {'x-api-key': self
._webServerAPIKey
}
285 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/statistics'
286 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
288 self
.assertEqual(r
.status_code
, 200)
289 self
.assertTrue(r
.json())
292 for entry
in content
:
293 self
.assertIn('type', entry
)
294 self
.assertIn('name', entry
)
295 self
.assertIn('value', entry
)
296 self
.assertEqual(entry
['type'], 'StatisticItem')
297 values
[entry
['name']] = entry
['value']
299 for key
in self
._expectedMetrics
:
300 self
.assertIn(key
, values
)
301 self
.assertTrue(values
[key
] >= 0)
304 self
.assertIn(key
, self
._expectedMetrics
)
306 def testJsonstatStats(self
):
308 API: /jsonstat?command=stats
310 headers
= {'x-api-key': self
._webServerAPIKey
}
311 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
312 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
314 self
.assertEqual(r
.status_code
, 200)
315 self
.assertTrue(r
.json())
318 for key
in self
._expectedMetrics
:
319 self
.assertIn(key
, content
)
320 self
.assertTrue(content
[key
] >= 0)
322 def testJsonstatDynblocklist(self
):
324 API: /jsonstat?command=dynblocklist
326 headers
= {'x-api-key': self
._webServerAPIKey
}
327 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=dynblocklist'
328 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
330 self
.assertEqual(r
.status_code
, 200)
335 for key
in ['reason', 'seconds', 'blocks', 'action']:
336 self
.assertIn(key
, content
)
338 for key
in ['blocks']:
339 self
.assertTrue(content
[key
] >= 0)
341 class TestAPIServerDown(APITestsBase
):
343 _config_template
= """
344 setACL({"127.0.0.1/32", "::1/128"})
345 newServer{address="127.0.0.1:%s"}
346 getServer(0):setDown()
347 webserver("127.0.0.1:%s")
348 setWebserverConfig({password="%s", apiKey="%s"})
351 def testServerDownNoLatencyLocalhost(self
):
353 API: /api/v1/servers/localhost, no latency for a down server
355 headers
= {'x-api-key': self
._webServerAPIKey
}
356 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost'
357 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
359 self
.assertEqual(r
.status_code
, 200)
360 self
.assertTrue(r
.json())
363 self
.assertEqual(content
['servers'][0]['latency'], None)
364 self
.assertEqual(content
['servers'][0]['tcpLatency'], None)
366 class TestAPIWritable(APITestsBase
):
368 _APIWriteDir
= '/tmp'
369 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_APIWriteDir']
370 _config_template
= """
371 setACL({"127.0.0.1/32", "::1/128"})
372 newServer{address="127.0.0.1:%s"}
373 webserver("127.0.0.1:%s")
374 setWebserverConfig({password="%s", apiKey="%s"})
375 setAPIWritable(true, "%s")
378 def testSetACL(self
):
382 headers
= {'x-api-key': self
._webServerAPIKey
}
383 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
384 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
386 self
.assertEqual(r
.status_code
, 200)
387 self
.assertTrue(r
.json())
389 acl
= content
['value']
390 expectedACL
= ["127.0.0.1/32", "::1/128"]
393 self
.assertEqual(acl
, expectedACL
)
395 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
396 payload
= json
.dumps({"name": "allow-from",
397 "type": "ConfigSetting",
399 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
401 self
.assertEqual(r
.status_code
, 200)
402 self
.assertTrue(r
.json())
404 acl
= content
['value']
406 self
.assertEqual(acl
, newACL
)
408 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
410 self
.assertEqual(r
.status_code
, 200)
411 self
.assertTrue(r
.json())
413 acl
= content
['value']
415 self
.assertEqual(acl
, newACL
)
417 configFile
= self
._APIWriteDir
+ '/' + 'acl.conf'
418 self
.assertTrue(os
.path
.isfile(configFile
))
420 with
open(configFile
, 'rt') as f
:
421 header
= f
.readline()
424 self
.assertEqual(header
, """-- Generated by the REST API, DO NOT EDIT\n""")
426 self
.assertIn(body
, {
427 """setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})\n""",
428 """setACL({"192.0.2.0/24", "203.0.113.0/24", "198.51.100.0/24"})\n""",
429 """setACL({"198.51.100.0/24", "192.0.2.0/24", "203.0.113.0/24"})\n""",
430 """setACL({"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24"})\n""",
431 """setACL({"203.0.113.0/24", "192.0.2.0/24", "198.51.100.0/24"})\n""",
432 """setACL({"203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"})\n"""
435 class TestAPICustomHeaders(APITestsBase
):
437 # paths accessible using the API key only
438 _apiOnlyPath
= '/api/v1/servers/localhost/config'
439 # paths accessible using basic auth only (list not exhaustive)
441 _consoleKey
= DNSDistTest
.generateConsoleKey()
442 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
443 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
444 _config_template
= """
446 controlSocket("127.0.0.1:%s")
447 setACL({"127.0.0.1/32", "::1/128"})
448 newServer({address="127.0.0.1:%s"})
449 webserver("127.0.0.1:%s")
450 setWebserverConfig({password="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} })
453 def testBasicHeaders(self
):
455 API: Basic custom headers
458 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
460 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
462 self
.assertEqual(r
.status_code
, 200)
463 self
.assertEqual(r
.headers
.get('x-custom'), "custom")
464 self
.assertFalse("x-frame-options" in r
.headers
)
466 def testBasicHeadersUpdate(self
):
468 API: Basic update of custom headers
471 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
472 self
.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})')
473 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
475 self
.assertEqual(r
.status_code
, 200)
476 self
.assertEqual(r
.headers
.get('x-powered-by'), "dnsdist")
477 self
.assertTrue("x-frame-options" in r
.headers
)
479 class TestStatsWithoutAuthentication(APITestsBase
):
481 # paths accessible using the API key only
482 _apiOnlyPath
= '/api/v1/servers/localhost/config'
483 # paths accessible using basic auth only (list not exhaustive)
485 _noAuthenticationPaths
= [ '/metrics', '/jsonstat?command=dynblocklist' ]
486 _consoleKey
= DNSDistTest
.generateConsoleKey()
487 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
488 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
489 _config_template
= """
491 controlSocket("127.0.0.1:%s")
492 setACL({"127.0.0.1/32", "::1/128"})
493 newServer({address="127.0.0.1:%s"})
494 webserver("127.0.0.1:%s")
495 setWebserverConfig({password="%s", apiKey="%s", statsRequireAuthentication=false })
500 API: Stats do not require authentication
503 for path
in self
._noAuthenticationPaths
:
504 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
506 r
= requests
.get(url
, timeout
=self
._webTimeout
)
508 self
.assertEqual(r
.status_code
, 200)
510 # these should still require basic authentication
511 for path
in [self
._basicOnlyPath
]:
512 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
514 r
= requests
.get(url
, timeout
=self
._webTimeout
)
515 self
.assertEqual(r
.status_code
, 401)
517 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
519 self
.assertEqual(r
.status_code
, 200)
521 # these should still require API authentication
522 for path
in [self
._apiOnlyPath
]:
523 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
525 r
= requests
.get(url
, timeout
=self
._webTimeout
)
526 self
.assertEqual(r
.status_code
, 401)
528 headers
= {'x-api-key': self
._webServerAPIKey
}
529 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
531 self
.assertEqual(r
.status_code
, 200)
533 class TestAPIAuth(APITestsBase
):
535 _webServerBasicAuthPasswordNew
= 'password'
536 _webServerBasicAuthPasswordNewHashed
= '$scrypt$ln=10,p=1,r=8$yefz8SAuT3lj3moXqUYvmw==$T98/RYMp76ZYNjd7MpAkcVXZEDqpLtrc3tQ52QflVBA='
537 _webServerAPIKeyNew
= 'apipassword'
538 _webServerAPIKeyNewHashed
= '$scrypt$ln=9,p=1,r=8$y96I9nfkY0LWDQEdSUzWgA==$jiyn9QD36o9d0ADrlqiIBk4AKyQrkD1KYw3CexwtHp4='
539 # paths accessible using the API key only
540 _apiOnlyPath
= '/api/v1/servers/localhost/config'
541 # paths accessible using basic auth only (list not exhaustive)
543 _consoleKey
= DNSDistTest
.generateConsoleKey()
544 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
545 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
546 _config_template
= """
548 controlSocket("127.0.0.1:%s")
549 setACL({"127.0.0.1/32", "::1/128"})
550 newServer{address="127.0.0.1:%s"}
551 webserver("127.0.0.1:%s")
552 setWebserverConfig({password="%s", apiKey="%s"})
555 def testBasicAuthChange(self
):
557 API: Basic Authentication updating credentials
560 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
561 self
.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self
._webServerBasicAuthPasswordNewHashed
))
563 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPasswordNew
), timeout
=self
._webTimeout
)
565 self
.assertEqual(r
.status_code
, 200)
567 # Make sure the old password is not usable any more
568 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
569 self
.assertEqual(r
.status_code
, 401)
571 def testXAPIKeyChange(self
):
573 API: X-Api-Key updating credentials
576 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
577 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
579 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
580 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
582 self
.assertEqual(r
.status_code
, 200)
584 # Make sure the old password is not usable any more
585 headers
= {'x-api-key': self
._webServerAPIKey
}
586 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
587 self
.assertEqual(r
.status_code
, 401)
589 def testBasicAuthOnlyChange(self
):
591 API: X-Api-Key updated to none (disabled)
594 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
595 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
597 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
598 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
600 self
.assertEqual(r
.status_code
, 200)
603 self
.sendConsoleCommand('setWebserverConfig({apiKey=""})')
605 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
606 self
.assertEqual(r
.status_code
, 401)
608 class TestAPIACL(APITestsBase
):
610 _consoleKey
= DNSDistTest
.generateConsoleKey()
611 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
612 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
613 _config_template
= """
615 controlSocket("127.0.0.1:%s")
616 setACL({"127.0.0.1/32", "::1/128"})
617 newServer{address="127.0.0.1:%s"}
618 webserver("127.0.0.1:%s")
619 setWebserverConfig({password="%s", apiKey="%s", acl="192.0.2.1"})
622 def testACLChange(self
):
624 API: Should be denied by ACL then allowed
627 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
629 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
630 self
.assertTrue(False)
631 except requests
.exceptions
.ConnectionError
as exp
:
635 self
.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
637 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
639 self
.assertEqual(r
.status_code
, 200)
641 class TestAPIWithoutAuthentication(APITestsBase
):
643 _apiPath
= '/api/v1/servers/localhost/config'
644 # paths accessible using basic auth only (list not exhaustive)
646 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
647 _config_template
= """
648 setACL({"127.0.0.1/32", "::1/128"})
649 newServer({address="127.0.0.1:%s"})
650 webserver("127.0.0.1:%s")
651 setWebserverConfig({password="%s", apiRequiresAuthentication=false })
656 API: API do not require authentication
659 for path
in [self
._apiPath
]:
660 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
662 r
= requests
.get(url
, timeout
=self
._webTimeout
)
664 self
.assertEqual(r
.status_code
, 200)
666 # these should still require basic authentication
667 for path
in [self
._basicOnlyPath
]:
668 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
670 r
= requests
.get(url
, timeout
=self
._webTimeout
)
671 self
.assertEqual(r
.status_code
, 401)
673 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
675 self
.assertEqual(r
.status_code
, 200)
677 class TestCustomLuaEndpoint(APITestsBase
):
679 _config_template
= """
680 setACL({"127.0.0.1/32", "::1/128"})
681 newServer{address="127.0.0.1:%s"}
682 webserver("127.0.0.1:%s")
683 setWebserverConfig({password="%s"})
685 function customHTTPHandler(req, resp)
686 if req.path ~= '/foo' then
691 if req.version ~= 11 then
696 if req.method ~= 'GET' then
701 local get = req.getvars
702 if get['param'] ~= '42' then
707 local headers = req.headers
708 if headers['customheader'] ~= 'foobar' then
713 resp.body = 'It works!'
715 resp.headers = { ['Foo']='Bar'}
717 registerWebHandler('/foo', customHTTPHandler)
719 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
725 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/foo?param=42'
726 headers
= {'customheader': 'foobar'}
727 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
, headers
=headers
)
729 self
.assertEqual(r
.status_code
, 200)
730 self
.assertEqual(r
.content
, b
'It works!')
731 self
.assertEqual(r
.headers
.get('foo'), "Bar")
733 class TestWebConcurrentConnections(APITestsBase
):
737 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_maxConns']
738 _config_template
= """
739 newServer{address="127.0.0.1:%s"}
740 webserver("127.0.0.1:%s")
741 setWebserverConfig({password="%s", apiKey="%s", maxConcurrentConnections=%d})
744 def testConcurrentConnections(self
):
746 Web: Concurrent connections
750 # open the maximum number of connections
751 for _
in range(self
._maxConns
):
752 conn
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
753 conn
.connect(("127.0.0.1", self
._webServerPort
))
756 # we now hold all the slots, let's try to establish a new connection
757 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
758 self
.assertRaises(requests
.exceptions
.ConnectionError
, requests
.get
, url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
766 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
768 self
.assertEqual(r
.status_code
, 200)
770 class TestAPICustomStatistics(APITestsBase
):
774 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
775 _config_template
= """
776 newServer{address="127.0.0.1:%s"}
777 webserver("127.0.0.1:%s")
778 declareMetric("my-custom-metric", "counter", "Number of statistics")
779 declareMetric("my-other-metric", "counter", "Another number of statistics")
780 declareMetric("my-gauge", "gauge", "Current memory usage")
781 setWebserverConfig({password="%s", apiKey="%s"})
784 def testCustomStats(self
):
786 API: /jsonstat?command=stats
787 Test custom statistics are exposed
789 headers
= {'x-api-key': self
._webServerAPIKey
}
790 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
791 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
793 self
.assertEqual(r
.status_code
, 200)
794 self
.assertTrue(r
.json())
797 expected
= ['my-custom-metric', 'my-other-metric', 'my-gauge']
800 self
.assertIn(key
, content
)
801 self
.assertTrue(content
[key
] >= 0)