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', 'protocol']:
136 self
.assertIn(key
, server
)
138 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
139 'qps', 'queries', 'order']:
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', 'protocol']:
193 self
.assertIn(key
, server
)
195 for key
in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
196 'qps', 'queries', 'order']:
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)
365 class TestAPIWritable(APITestsBase
):
367 _APIWriteDir
= '/tmp'
368 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_APIWriteDir']
369 _config_template
= """
370 setACL({"127.0.0.1/32", "::1/128"})
371 newServer{address="127.0.0.1:%s"}
372 webserver("127.0.0.1:%s")
373 setWebserverConfig({password="%s", apiKey="%s"})
374 setAPIWritable(true, "%s")
377 def testSetACL(self
):
381 headers
= {'x-api-key': self
._webServerAPIKey
}
382 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/api/v1/servers/localhost/config/allow-from'
383 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
385 self
.assertEqual(r
.status_code
, 200)
386 self
.assertTrue(r
.json())
388 acl
= content
['value']
389 expectedACL
= ["127.0.0.1/32", "::1/128"]
392 self
.assertEqual(acl
, expectedACL
)
394 newACL
= ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"]
395 payload
= json
.dumps({"name": "allow-from",
396 "type": "ConfigSetting",
398 r
= requests
.put(url
, headers
=headers
, timeout
=self
._webTimeout
, data
=payload
)
400 self
.assertEqual(r
.status_code
, 200)
401 self
.assertTrue(r
.json())
403 acl
= content
['value']
405 self
.assertEqual(acl
, newACL
)
407 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
409 self
.assertEqual(r
.status_code
, 200)
410 self
.assertTrue(r
.json())
412 acl
= content
['value']
414 self
.assertEqual(acl
, newACL
)
416 configFile
= self
._APIWriteDir
+ '/' + 'acl.conf'
417 self
.assertTrue(os
.path
.isfile(configFile
))
419 with
open(configFile
, 'rt') as f
:
420 header
= f
.readline()
423 self
.assertEqual(header
, """-- Generated by the REST API, DO NOT EDIT\n""")
425 self
.assertIn(body
, {
426 """setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})\n""",
427 """setACL({"192.0.2.0/24", "203.0.113.0/24", "198.51.100.0/24"})\n""",
428 """setACL({"198.51.100.0/24", "192.0.2.0/24", "203.0.113.0/24"})\n""",
429 """setACL({"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24"})\n""",
430 """setACL({"203.0.113.0/24", "192.0.2.0/24", "198.51.100.0/24"})\n""",
431 """setACL({"203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"})\n"""
434 class TestAPICustomHeaders(APITestsBase
):
436 # paths accessible using the API key only
437 _apiOnlyPath
= '/api/v1/servers/localhost/config'
438 # paths accessible using basic auth only (list not exhaustive)
440 _consoleKey
= DNSDistTest
.generateConsoleKey()
441 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
442 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
443 _config_template
= """
445 controlSocket("127.0.0.1:%s")
446 setACL({"127.0.0.1/32", "::1/128"})
447 newServer({address="127.0.0.1:%s"})
448 webserver("127.0.0.1:%s")
449 setWebserverConfig({password="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} })
452 def testBasicHeaders(self
):
454 API: Basic custom headers
457 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
459 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
461 self
.assertEqual(r
.status_code
, 200)
462 self
.assertEqual(r
.headers
.get('x-custom'), "custom")
463 self
.assertFalse("x-frame-options" in r
.headers
)
465 def testBasicHeadersUpdate(self
):
467 API: Basic update of custom headers
470 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
471 self
.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})')
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-powered-by'), "dnsdist")
476 self
.assertTrue("x-frame-options" in r
.headers
)
478 class TestStatsWithoutAuthentication(APITestsBase
):
480 # paths accessible using the API key only
481 _apiOnlyPath
= '/api/v1/servers/localhost/config'
482 # paths accessible using basic auth only (list not exhaustive)
484 _noAuthenticationPaths
= [ '/metrics', '/jsonstat?command=dynblocklist' ]
485 _consoleKey
= DNSDistTest
.generateConsoleKey()
486 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
487 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
488 _config_template
= """
490 controlSocket("127.0.0.1:%s")
491 setACL({"127.0.0.1/32", "::1/128"})
492 newServer({address="127.0.0.1:%s"})
493 webserver("127.0.0.1:%s")
494 setWebserverConfig({password="%s", apiKey="%s", statsRequireAuthentication=false })
499 API: Stats do not require authentication
502 for path
in self
._noAuthenticationPaths
:
503 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
505 r
= requests
.get(url
, timeout
=self
._webTimeout
)
507 self
.assertEqual(r
.status_code
, 200)
509 # these should still require basic authentication
510 for path
in [self
._basicOnlyPath
]:
511 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
513 r
= requests
.get(url
, timeout
=self
._webTimeout
)
514 self
.assertEqual(r
.status_code
, 401)
516 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
518 self
.assertEqual(r
.status_code
, 200)
520 # these should still require API authentication
521 for path
in [self
._apiOnlyPath
]:
522 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
524 r
= requests
.get(url
, timeout
=self
._webTimeout
)
525 self
.assertEqual(r
.status_code
, 401)
527 headers
= {'x-api-key': self
._webServerAPIKey
}
528 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
530 self
.assertEqual(r
.status_code
, 200)
532 class TestAPIAuth(APITestsBase
):
534 _webServerBasicAuthPasswordNew
= 'password'
535 _webServerBasicAuthPasswordNewHashed
= '$scrypt$ln=10,p=1,r=8$yefz8SAuT3lj3moXqUYvmw==$T98/RYMp76ZYNjd7MpAkcVXZEDqpLtrc3tQ52QflVBA='
536 _webServerAPIKeyNew
= 'apipassword'
537 _webServerAPIKeyNewHashed
= '$scrypt$ln=9,p=1,r=8$y96I9nfkY0LWDQEdSUzWgA==$jiyn9QD36o9d0ADrlqiIBk4AKyQrkD1KYw3CexwtHp4='
538 # paths accessible using the API key only
539 _apiOnlyPath
= '/api/v1/servers/localhost/config'
540 # paths accessible using basic auth only (list not exhaustive)
542 _consoleKey
= DNSDistTest
.generateConsoleKey()
543 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
544 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
545 _config_template
= """
547 controlSocket("127.0.0.1:%s")
548 setACL({"127.0.0.1/32", "::1/128"})
549 newServer{address="127.0.0.1:%s"}
550 webserver("127.0.0.1:%s")
551 setWebserverConfig({password="%s", apiKey="%s"})
554 def testBasicAuthChange(self
):
556 API: Basic Authentication updating credentials
559 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._basicOnlyPath
560 self
.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self
._webServerBasicAuthPasswordNewHashed
))
562 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPasswordNew
), timeout
=self
._webTimeout
)
564 self
.assertEqual(r
.status_code
, 200)
566 # Make sure the old password is not usable any more
567 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
568 self
.assertEqual(r
.status_code
, 401)
570 def testXAPIKeyChange(self
):
572 API: X-Api-Key updating credentials
575 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
576 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
578 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
579 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
581 self
.assertEqual(r
.status_code
, 200)
583 # Make sure the old password is not usable any more
584 headers
= {'x-api-key': self
._webServerAPIKey
}
585 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
586 self
.assertEqual(r
.status_code
, 401)
588 def testBasicAuthOnlyChange(self
):
590 API: X-Api-Key updated to none (disabled)
593 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + self
._apiOnlyPath
594 self
.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self
._webServerAPIKeyNewHashed
))
596 headers
= {'x-api-key': self
._webServerAPIKeyNew
}
597 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
599 self
.assertEqual(r
.status_code
, 200)
602 self
.sendConsoleCommand('setWebserverConfig({apiKey=""})')
604 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
605 self
.assertEqual(r
.status_code
, 401)
607 class TestAPIACL(APITestsBase
):
609 _consoleKey
= DNSDistTest
.generateConsoleKey()
610 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
611 _config_params
= ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
612 _config_template
= """
614 controlSocket("127.0.0.1:%s")
615 setACL({"127.0.0.1/32", "::1/128"})
616 newServer{address="127.0.0.1:%s"}
617 webserver("127.0.0.1:%s")
618 setWebserverConfig({password="%s", apiKey="%s", acl="192.0.2.1"})
621 def testACLChange(self
):
623 API: Should be denied by ACL then allowed
626 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
628 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
629 self
.assertTrue(False)
630 except requests
.exceptions
.ConnectionError
as exp
:
634 self
.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
636 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
638 self
.assertEqual(r
.status_code
, 200)
640 class TestAPIWithoutAuthentication(APITestsBase
):
642 _apiPath
= '/api/v1/servers/localhost/config'
643 # paths accessible using basic auth only (list not exhaustive)
645 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
646 _config_template
= """
647 setACL({"127.0.0.1/32", "::1/128"})
648 newServer({address="127.0.0.1:%s"})
649 webserver("127.0.0.1:%s")
650 setWebserverConfig({password="%s", apiRequiresAuthentication=false })
655 API: API do not require authentication
658 for path
in [self
._apiPath
]:
659 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
661 r
= requests
.get(url
, timeout
=self
._webTimeout
)
663 self
.assertEqual(r
.status_code
, 200)
665 # these should still require basic authentication
666 for path
in [self
._basicOnlyPath
]:
667 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + path
669 r
= requests
.get(url
, timeout
=self
._webTimeout
)
670 self
.assertEqual(r
.status_code
, 401)
672 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
674 self
.assertEqual(r
.status_code
, 200)
676 class TestCustomLuaEndpoint(APITestsBase
):
678 _config_template
= """
679 setACL({"127.0.0.1/32", "::1/128"})
680 newServer{address="127.0.0.1:%s"}
681 webserver("127.0.0.1:%s")
682 setWebserverConfig({password="%s"})
684 function customHTTPHandler(req, resp)
685 if req.path ~= '/foo' then
690 if req.version ~= 11 then
695 if req.method ~= 'GET' then
700 local get = req.getvars
701 if get['param'] ~= '42' then
706 local headers = req.headers
707 if headers['customheader'] ~= 'foobar' then
712 resp.body = 'It works!'
714 resp.headers = { ['Foo']='Bar'}
716 registerWebHandler('/foo', customHTTPHandler)
718 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
724 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/foo?param=42'
725 headers
= {'customheader': 'foobar'}
726 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
, headers
=headers
)
728 self
.assertEqual(r
.status_code
, 200)
729 self
.assertEqual(r
.content
, b
'It works!')
730 self
.assertEqual(r
.headers
.get('foo'), "Bar")
732 class TestWebConcurrentConnections(APITestsBase
):
736 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_maxConns']
737 _config_template
= """
738 newServer{address="127.0.0.1:%s"}
739 webserver("127.0.0.1:%s")
740 setWebserverConfig({password="%s", apiKey="%s", maxConcurrentConnections=%d})
743 def testConcurrentConnections(self
):
745 Web: Concurrent connections
749 # open the maximum number of connections
750 for _
in range(self
._maxConns
):
751 conn
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
752 conn
.connect(("127.0.0.1", self
._webServerPort
))
755 # we now hold all the slots, let's try to establish a new connection
756 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + "/"
757 self
.assertRaises(requests
.exceptions
.ConnectionError
, requests
.get
, url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
765 r
= requests
.get(url
, auth
=('whatever', self
._webServerBasicAuthPassword
), timeout
=self
._webTimeout
)
767 self
.assertEqual(r
.status_code
, 200)
769 class TestAPICustomStatistics(APITestsBase
):
773 _config_params
= ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
774 _config_template
= """
775 newServer{address="127.0.0.1:%s"}
776 webserver("127.0.0.1:%s")
777 declareMetric("my-custom-metric", "counter", "Number of statistics")
778 declareMetric("my-other-metric", "counter", "Another number of statistics")
779 declareMetric("my-gauge", "gauge", "Current memory usage")
780 setWebserverConfig({password="%s", apiKey="%s"})
783 def testCustomStats(self
):
785 API: /jsonstat?command=stats
786 Test custom statistics are exposed
788 headers
= {'x-api-key': self
._webServerAPIKey
}
789 url
= 'http://127.0.0.1:' + str(self
._webServerPort
) + '/jsonstat?command=stats'
790 r
= requests
.get(url
, headers
=headers
, timeout
=self
._webTimeout
)
792 self
.assertEqual(r
.status_code
, 200)
793 self
.assertTrue(r
.json())
796 expected
= ['my-custom-metric', 'my-other-metric', 'my-gauge']
799 self
.assertIn(key
, content
)
800 self
.assertTrue(content
[key
] >= 0)