]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_API.py
Merge pull request #11689 from rgacogne/ddist-skip-allocations-policies
[thirdparty/pdns.git] / regression-tests.dnsdist / test_API.py
1 #!/usr/bin/env python
2 import os.path
3
4 import base64
5 import json
6 import requests
7 import socket
8 import time
9 from dnsdisttests import DNSDistTest
10
11 class APITestsBase(DNSDistTest):
12 __test__ = False
13 _webTimeout = 2.0
14 _webServerPort = 8083
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"})
25 """
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']
41
42 class TestAPIBasics(APITestsBase):
43
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']
50 __test__ = True
51
52 def testBasicAuth(self):
53 """
54 API: Basic Authentication
55 """
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)
61 self.assertTrue(r)
62 self.assertEqual(r.status_code, 200)
63
64 def testXAPIKey(self):
65 """
66 API: X-Api-Key
67 """
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)
72 self.assertTrue(r)
73 self.assertEqual(r.status_code, 200)
74
75 def testWrongXAPIKey(self):
76 """
77 API: Wrong X-Api-Key
78 """
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)
84
85 def testBasicAuthOnly(self):
86 """
87 API: Basic Authentication Only
88 """
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)
94
95 def testAPIKeyOnly(self):
96 """
97 API: API Key Only
98 """
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)
103
104 def testServersLocalhost(self):
105 """
106 API: /api/v1/servers/localhost
107 """
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)
111 self.assertTrue(r)
112 self.assertEqual(r.status_code, 200)
113 self.assertTrue(r.json())
114 content = r.json()
115
116 self.assertEqual(content['daemon_type'], 'dnsdist')
117
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)
121
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)
128
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)
137
138 for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
139 'qps', 'queries', 'order', 'tcpLatency']:
140 self.assertTrue(server[key] >= 0)
141
142 self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN'])
143
144 for frontend in content['frontends']:
145 for key in ['id', 'address', 'udp', 'tcp', 'type', 'queries']:
146 self.assertIn(key, frontend)
147
148 for key in ['id', 'queries']:
149 self.assertTrue(frontend[key] >= 0)
150
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)
154
155 for key in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']:
156 self.assertTrue(pool[key] >= 0)
157
158 stats = content['statistics']
159 for key in self._expectedMetrics:
160 self.assertIn(key, stats)
161 self.assertTrue(stats[key] >= 0)
162 for key in stats:
163 self.assertIn(key, self._expectedMetrics)
164
165 def testServersLocalhostPool(self):
166 """
167 API: /api/v1/servers/localhost/pool?name=mypool
168 """
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)
172 self.assertTrue(r)
173 self.assertEqual(r.status_code, 200)
174 self.assertTrue(r.json())
175 content = r.json()
176
177 self.assertIn('stats', content)
178 self.assertIn('servers', content)
179
180 for key in ['name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
181 self.assertIn(key, content['stats'])
182
183 for key in ['cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']:
184 self.assertTrue(content['stats'][key] >= 0)
185
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)
194
195 for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
196 'qps', 'queries', 'order', 'tcpLatency']:
197 self.assertTrue(server[key] >= 0)
198
199 self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN'])
200
201 def testServersIDontExist(self):
202 """
203 API: /api/v1/servers/idonotexist (should be 404)
204 """
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)
209
210 def testServersLocalhostConfig(self):
211 """
212 API: /api/v1/servers/localhost/config
213 """
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)
217 self.assertTrue(r)
218 self.assertEqual(r.status_code, 200)
219 self.assertTrue(r.json())
220 content = r.json()
221 values = {}
222 for entry in content:
223 for key in ['type', 'name', 'value']:
224 self.assertIn(key, entry)
225
226 self.assertEqual(entry['type'], 'ConfigSetting')
227 values[entry['name']] = entry['value']
228
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)
234
235 for key in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout',
236 'tcp-send-timeout']:
237 self.assertTrue(values[key] >= 0)
238
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)
241
242 def testServersLocalhostConfigAllowFrom(self):
243 """
244 API: /api/v1/servers/localhost/config/allow-from
245 """
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)
249 self.assertTrue(r)
250 self.assertEqual(r.status_code, 200)
251 self.assertTrue(r.json())
252 content = r.json()
253 for key in ['type', 'name', 'value']:
254 self.assertIn(key, content)
255
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"]
260 acl.sort()
261 expectedACL.sort()
262 self.assertEqual(acl, expectedACL)
263
264 def testServersLocalhostConfigAllowFromPut(self):
265 """
266 API: PUT /api/v1/servers/localhost/config/allow-from (should be refused)
267
268 The API is read-only by default, so this should be refused
269 """
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",
273 "value": newACL})
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)
277 self.assertFalse(r)
278 self.assertEqual(r.status_code, 405)
279
280 def testServersLocalhostStatistics(self):
281 """
282 API: /api/v1/servers/localhost/statistics
283 """
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)
287 self.assertTrue(r)
288 self.assertEqual(r.status_code, 200)
289 self.assertTrue(r.json())
290 content = r.json()
291 values = {}
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']
298
299 for key in self._expectedMetrics:
300 self.assertIn(key, values)
301 self.assertTrue(values[key] >= 0)
302
303 for key in values:
304 self.assertIn(key, self._expectedMetrics)
305
306 def testJsonstatStats(self):
307 """
308 API: /jsonstat?command=stats
309 """
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)
313 self.assertTrue(r)
314 self.assertEqual(r.status_code, 200)
315 self.assertTrue(r.json())
316 content = r.json()
317
318 for key in self._expectedMetrics:
319 self.assertIn(key, content)
320 self.assertTrue(content[key] >= 0)
321
322 def testJsonstatDynblocklist(self):
323 """
324 API: /jsonstat?command=dynblocklist
325 """
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)
329 self.assertTrue(r)
330 self.assertEqual(r.status_code, 200)
331
332 content = r.json()
333
334 if content:
335 for key in ['reason', 'seconds', 'blocks', 'action']:
336 self.assertIn(key, content)
337
338 for key in ['blocks']:
339 self.assertTrue(content[key] >= 0)
340
341 class TestAPIServerDown(APITestsBase):
342 __test__ = True
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"})
349 """
350
351 def testServerDownNoLatencyLocalhost(self):
352 """
353 API: /api/v1/servers/localhost, no latency for a down server
354 """
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)
358 self.assertTrue(r)
359 self.assertEqual(r.status_code, 200)
360 self.assertTrue(r.json())
361 content = r.json()
362
363 self.assertEqual(content['servers'][0]['latency'], None)
364 self.assertEqual(content['servers'][0]['tcpLatency'], None)
365
366 class TestAPIWritable(APITestsBase):
367 __test__ = True
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")
376 """
377
378 def testSetACL(self):
379 """
380 API: Set ACL
381 """
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)
385 self.assertTrue(r)
386 self.assertEqual(r.status_code, 200)
387 self.assertTrue(r.json())
388 content = r.json()
389 acl = content['value']
390 expectedACL = ["127.0.0.1/32", "::1/128"]
391 acl.sort()
392 expectedACL.sort()
393 self.assertEqual(acl, expectedACL)
394
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",
398 "value": newACL})
399 r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload)
400 self.assertTrue(r)
401 self.assertEqual(r.status_code, 200)
402 self.assertTrue(r.json())
403 content = r.json()
404 acl = content['value']
405 acl.sort()
406 self.assertEqual(acl, newACL)
407
408 r = requests.get(url, headers=headers, timeout=self._webTimeout)
409 self.assertTrue(r)
410 self.assertEqual(r.status_code, 200)
411 self.assertTrue(r.json())
412 content = r.json()
413 acl = content['value']
414 acl.sort()
415 self.assertEqual(acl, newACL)
416
417 configFile = self._APIWriteDir + '/' + 'acl.conf'
418 self.assertTrue(os.path.isfile(configFile))
419 fileContent = None
420 with open(configFile, 'rt') as f:
421 header = f.readline()
422 body = f.readline()
423
424 self.assertEqual(header, """-- Generated by the REST API, DO NOT EDIT\n""")
425
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"""
433 })
434
435 class TestAPICustomHeaders(APITestsBase):
436 __test__ = True
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)
440 _basicOnlyPath = '/'
441 _consoleKey = DNSDistTest.generateConsoleKey()
442 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
443 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
444 _config_template = """
445 setKey("%s")
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"} })
451 """
452
453 def testBasicHeaders(self):
454 """
455 API: Basic custom headers
456 """
457
458 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
459
460 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
461 self.assertTrue(r)
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)
465
466 def testBasicHeadersUpdate(self):
467 """
468 API: Basic update of custom headers
469 """
470
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)
474 self.assertTrue(r)
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)
478
479 class TestStatsWithoutAuthentication(APITestsBase):
480 __test__ = True
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)
484 _basicOnlyPath = '/'
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 = """
490 setKey("%s")
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 })
496 """
497
498 def testAuth(self):
499 """
500 API: Stats do not require authentication
501 """
502
503 for path in self._noAuthenticationPaths:
504 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
505
506 r = requests.get(url, timeout=self._webTimeout)
507 self.assertTrue(r)
508 self.assertEqual(r.status_code, 200)
509
510 # these should still require basic authentication
511 for path in [self._basicOnlyPath]:
512 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
513
514 r = requests.get(url, timeout=self._webTimeout)
515 self.assertEqual(r.status_code, 401)
516
517 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
518 self.assertTrue(r)
519 self.assertEqual(r.status_code, 200)
520
521 # these should still require API authentication
522 for path in [self._apiOnlyPath]:
523 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
524
525 r = requests.get(url, timeout=self._webTimeout)
526 self.assertEqual(r.status_code, 401)
527
528 headers = {'x-api-key': self._webServerAPIKey}
529 r = requests.get(url, headers=headers, timeout=self._webTimeout)
530 self.assertTrue(r)
531 self.assertEqual(r.status_code, 200)
532
533 class TestAPIAuth(APITestsBase):
534 __test__ = True
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)
542 _basicOnlyPath = '/'
543 _consoleKey = DNSDistTest.generateConsoleKey()
544 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
545 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
546 _config_template = """
547 setKey("%s")
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"})
553 """
554
555 def testBasicAuthChange(self):
556 """
557 API: Basic Authentication updating credentials
558 """
559
560 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
561 self.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self._webServerBasicAuthPasswordNewHashed))
562
563 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPasswordNew), timeout=self._webTimeout)
564 self.assertTrue(r)
565 self.assertEqual(r.status_code, 200)
566
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)
570
571 def testXAPIKeyChange(self):
572 """
573 API: X-Api-Key updating credentials
574 """
575
576 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath
577 self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed))
578
579 headers = {'x-api-key': self._webServerAPIKeyNew}
580 r = requests.get(url, headers=headers, timeout=self._webTimeout)
581 self.assertTrue(r)
582 self.assertEqual(r.status_code, 200)
583
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)
588
589 def testBasicAuthOnlyChange(self):
590 """
591 API: X-Api-Key updated to none (disabled)
592 """
593
594 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath
595 self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed))
596
597 headers = {'x-api-key': self._webServerAPIKeyNew}
598 r = requests.get(url, headers=headers, timeout=self._webTimeout)
599 self.assertTrue(r)
600 self.assertEqual(r.status_code, 200)
601
602 # now disable apiKey
603 self.sendConsoleCommand('setWebserverConfig({apiKey=""})')
604
605 r = requests.get(url, headers=headers, timeout=self._webTimeout)
606 self.assertEqual(r.status_code, 401)
607
608 class TestAPIACL(APITestsBase):
609 __test__ = True
610 _consoleKey = DNSDistTest.generateConsoleKey()
611 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
612 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
613 _config_template = """
614 setKey("%s")
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"})
620 """
621
622 def testACLChange(self):
623 """
624 API: Should be denied by ACL then allowed
625 """
626
627 url = 'http://127.0.0.1:' + str(self._webServerPort) + "/"
628 try:
629 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
630 self.assertTrue(False)
631 except requests.exceptions.ConnectionError as exp:
632 pass
633
634 # reset the ACL
635 self.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
636
637 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
638 self.assertTrue(r)
639 self.assertEqual(r.status_code, 200)
640
641 class TestAPIWithoutAuthentication(APITestsBase):
642 __test__ = True
643 _apiPath = '/api/v1/servers/localhost/config'
644 # paths accessible using basic auth only (list not exhaustive)
645 _basicOnlyPath = '/'
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 })
652 """
653
654 def testAuth(self):
655 """
656 API: API do not require authentication
657 """
658
659 for path in [self._apiPath]:
660 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
661
662 r = requests.get(url, timeout=self._webTimeout)
663 self.assertTrue(r)
664 self.assertEqual(r.status_code, 200)
665
666 # these should still require basic authentication
667 for path in [self._basicOnlyPath]:
668 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
669
670 r = requests.get(url, timeout=self._webTimeout)
671 self.assertEqual(r.status_code, 401)
672
673 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
674 self.assertTrue(r)
675 self.assertEqual(r.status_code, 200)
676
677 class TestCustomLuaEndpoint(APITestsBase):
678 __test__ = True
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"})
684
685 function customHTTPHandler(req, resp)
686 if req.path ~= '/foo' then
687 resp.status = 500
688 return
689 end
690
691 if req.version ~= 11 then
692 resp.status = 501
693 return
694 end
695
696 if req.method ~= 'GET' then
697 resp.status = 502
698 return
699 end
700
701 local get = req.getvars
702 if get['param'] ~= '42' then
703 resp.status = 503
704 return
705 end
706
707 local headers = req.headers
708 if headers['customheader'] ~= 'foobar' then
709 resp.status = 504
710 return
711 end
712
713 resp.body = 'It works!'
714 resp.status = 200
715 resp.headers = { ['Foo']='Bar'}
716 end
717 registerWebHandler('/foo', customHTTPHandler)
718 """
719 _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
720
721 def testBasic(self):
722 """
723 Custom Web Handler
724 """
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)
728 self.assertTrue(r)
729 self.assertEqual(r.status_code, 200)
730 self.assertEqual(r.content, b'It works!')
731 self.assertEqual(r.headers.get('foo'), "Bar")
732
733 class TestWebConcurrentConnections(APITestsBase):
734 __test__ = True
735 _maxConns = 2
736
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})
742 """
743
744 def testConcurrentConnections(self):
745 """
746 Web: Concurrent connections
747 """
748
749 conns = []
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))
754 conns.append(conn)
755
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)
759
760 # free one slot
761 conns[0].close()
762 conns[0] = None
763 time.sleep(1)
764
765 # this should work
766 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
767 self.assertTrue(r)
768 self.assertEqual(r.status_code, 200)
769
770 class TestAPICustomStatistics(APITestsBase):
771 __test__ = True
772 _maxConns = 2
773
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"})
782 """
783
784 def testCustomStats(self):
785 """
786 API: /jsonstat?command=stats
787 Test custom statistics are exposed
788 """
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)
792 self.assertTrue(r)
793 self.assertEqual(r.status_code, 200)
794 self.assertTrue(r.json())
795 content = r.json()
796
797 expected = ['my-custom-metric', 'my-other-metric', 'my-gauge']
798
799 for key in expected:
800 self.assertIn(key, content)
801 self.assertTrue(content[key] >= 0)