]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_API.py
Merge pull request #11662 from omoerbeek/rec-sl-dns64
[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', 'protocol']:
136 self.assertIn(key, server)
137
138 for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
139 'qps', 'queries', 'order']:
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', 'protocol']:
193 self.assertIn(key, server)
194
195 for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds',
196 'qps', 'queries', 'order']:
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
365 class TestAPIWritable(APITestsBase):
366 __test__ = True
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")
375 """
376
377 def testSetACL(self):
378 """
379 API: Set ACL
380 """
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)
384 self.assertTrue(r)
385 self.assertEqual(r.status_code, 200)
386 self.assertTrue(r.json())
387 content = r.json()
388 acl = content['value']
389 expectedACL = ["127.0.0.1/32", "::1/128"]
390 acl.sort()
391 expectedACL.sort()
392 self.assertEqual(acl, expectedACL)
393
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",
397 "value": newACL})
398 r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload)
399 self.assertTrue(r)
400 self.assertEqual(r.status_code, 200)
401 self.assertTrue(r.json())
402 content = r.json()
403 acl = content['value']
404 acl.sort()
405 self.assertEqual(acl, newACL)
406
407 r = requests.get(url, headers=headers, timeout=self._webTimeout)
408 self.assertTrue(r)
409 self.assertEqual(r.status_code, 200)
410 self.assertTrue(r.json())
411 content = r.json()
412 acl = content['value']
413 acl.sort()
414 self.assertEqual(acl, newACL)
415
416 configFile = self._APIWriteDir + '/' + 'acl.conf'
417 self.assertTrue(os.path.isfile(configFile))
418 fileContent = None
419 with open(configFile, 'rt') as f:
420 header = f.readline()
421 body = f.readline()
422
423 self.assertEqual(header, """-- Generated by the REST API, DO NOT EDIT\n""")
424
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"""
432 })
433
434 class TestAPICustomHeaders(APITestsBase):
435 __test__ = True
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)
439 _basicOnlyPath = '/'
440 _consoleKey = DNSDistTest.generateConsoleKey()
441 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
442 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
443 _config_template = """
444 setKey("%s")
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"} })
450 """
451
452 def testBasicHeaders(self):
453 """
454 API: Basic custom headers
455 """
456
457 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
458
459 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
460 self.assertTrue(r)
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)
464
465 def testBasicHeadersUpdate(self):
466 """
467 API: Basic update of custom headers
468 """
469
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)
473 self.assertTrue(r)
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)
477
478 class TestStatsWithoutAuthentication(APITestsBase):
479 __test__ = True
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)
483 _basicOnlyPath = '/'
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 = """
489 setKey("%s")
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 })
495 """
496
497 def testAuth(self):
498 """
499 API: Stats do not require authentication
500 """
501
502 for path in self._noAuthenticationPaths:
503 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
504
505 r = requests.get(url, timeout=self._webTimeout)
506 self.assertTrue(r)
507 self.assertEqual(r.status_code, 200)
508
509 # these should still require basic authentication
510 for path in [self._basicOnlyPath]:
511 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
512
513 r = requests.get(url, timeout=self._webTimeout)
514 self.assertEqual(r.status_code, 401)
515
516 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
517 self.assertTrue(r)
518 self.assertEqual(r.status_code, 200)
519
520 # these should still require API authentication
521 for path in [self._apiOnlyPath]:
522 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
523
524 r = requests.get(url, timeout=self._webTimeout)
525 self.assertEqual(r.status_code, 401)
526
527 headers = {'x-api-key': self._webServerAPIKey}
528 r = requests.get(url, headers=headers, timeout=self._webTimeout)
529 self.assertTrue(r)
530 self.assertEqual(r.status_code, 200)
531
532 class TestAPIAuth(APITestsBase):
533 __test__ = True
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)
541 _basicOnlyPath = '/'
542 _consoleKey = DNSDistTest.generateConsoleKey()
543 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
544 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
545 _config_template = """
546 setKey("%s")
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"})
552 """
553
554 def testBasicAuthChange(self):
555 """
556 API: Basic Authentication updating credentials
557 """
558
559 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath
560 self.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self._webServerBasicAuthPasswordNewHashed))
561
562 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPasswordNew), timeout=self._webTimeout)
563 self.assertTrue(r)
564 self.assertEqual(r.status_code, 200)
565
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)
569
570 def testXAPIKeyChange(self):
571 """
572 API: X-Api-Key updating credentials
573 """
574
575 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath
576 self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed))
577
578 headers = {'x-api-key': self._webServerAPIKeyNew}
579 r = requests.get(url, headers=headers, timeout=self._webTimeout)
580 self.assertTrue(r)
581 self.assertEqual(r.status_code, 200)
582
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)
587
588 def testBasicAuthOnlyChange(self):
589 """
590 API: X-Api-Key updated to none (disabled)
591 """
592
593 url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath
594 self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed))
595
596 headers = {'x-api-key': self._webServerAPIKeyNew}
597 r = requests.get(url, headers=headers, timeout=self._webTimeout)
598 self.assertTrue(r)
599 self.assertEqual(r.status_code, 200)
600
601 # now disable apiKey
602 self.sendConsoleCommand('setWebserverConfig({apiKey=""})')
603
604 r = requests.get(url, headers=headers, timeout=self._webTimeout)
605 self.assertEqual(r.status_code, 401)
606
607 class TestAPIACL(APITestsBase):
608 __test__ = True
609 _consoleKey = DNSDistTest.generateConsoleKey()
610 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
611 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
612 _config_template = """
613 setKey("%s")
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"})
619 """
620
621 def testACLChange(self):
622 """
623 API: Should be denied by ACL then allowed
624 """
625
626 url = 'http://127.0.0.1:' + str(self._webServerPort) + "/"
627 try:
628 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
629 self.assertTrue(False)
630 except requests.exceptions.ConnectionError as exp:
631 pass
632
633 # reset the ACL
634 self.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
635
636 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
637 self.assertTrue(r)
638 self.assertEqual(r.status_code, 200)
639
640 class TestAPIWithoutAuthentication(APITestsBase):
641 __test__ = True
642 _apiPath = '/api/v1/servers/localhost/config'
643 # paths accessible using basic auth only (list not exhaustive)
644 _basicOnlyPath = '/'
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 })
651 """
652
653 def testAuth(self):
654 """
655 API: API do not require authentication
656 """
657
658 for path in [self._apiPath]:
659 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
660
661 r = requests.get(url, timeout=self._webTimeout)
662 self.assertTrue(r)
663 self.assertEqual(r.status_code, 200)
664
665 # these should still require basic authentication
666 for path in [self._basicOnlyPath]:
667 url = 'http://127.0.0.1:' + str(self._webServerPort) + path
668
669 r = requests.get(url, timeout=self._webTimeout)
670 self.assertEqual(r.status_code, 401)
671
672 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
673 self.assertTrue(r)
674 self.assertEqual(r.status_code, 200)
675
676 class TestCustomLuaEndpoint(APITestsBase):
677 __test__ = True
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"})
683
684 function customHTTPHandler(req, resp)
685 if req.path ~= '/foo' then
686 resp.status = 500
687 return
688 end
689
690 if req.version ~= 11 then
691 resp.status = 501
692 return
693 end
694
695 if req.method ~= 'GET' then
696 resp.status = 502
697 return
698 end
699
700 local get = req.getvars
701 if get['param'] ~= '42' then
702 resp.status = 503
703 return
704 end
705
706 local headers = req.headers
707 if headers['customheader'] ~= 'foobar' then
708 resp.status = 504
709 return
710 end
711
712 resp.body = 'It works!'
713 resp.status = 200
714 resp.headers = { ['Foo']='Bar'}
715 end
716 registerWebHandler('/foo', customHTTPHandler)
717 """
718 _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed']
719
720 def testBasic(self):
721 """
722 Custom Web Handler
723 """
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)
727 self.assertTrue(r)
728 self.assertEqual(r.status_code, 200)
729 self.assertEqual(r.content, b'It works!')
730 self.assertEqual(r.headers.get('foo'), "Bar")
731
732 class TestWebConcurrentConnections(APITestsBase):
733 __test__ = True
734 _maxConns = 2
735
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})
741 """
742
743 def testConcurrentConnections(self):
744 """
745 Web: Concurrent connections
746 """
747
748 conns = []
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))
753 conns.append(conn)
754
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)
758
759 # free one slot
760 conns[0].close()
761 conns[0] = None
762 time.sleep(1)
763
764 # this should work
765 r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
766 self.assertTrue(r)
767 self.assertEqual(r.status_code, 200)
768
769 class TestAPICustomStatistics(APITestsBase):
770 __test__ = True
771 _maxConns = 2
772
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"})
781 """
782
783 def testCustomStats(self):
784 """
785 API: /jsonstat?command=stats
786 Test custom statistics are exposed
787 """
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)
791 self.assertTrue(r)
792 self.assertEqual(r.status_code, 200)
793 self.assertTrue(r.json())
794 content = r.json()
795
796 expected = ['my-custom-metric', 'my-other-metric', 'my-gauge']
797
798 for key in expected:
799 self.assertIn(key, content)
800 self.assertTrue(content[key] >= 0)