]>
Commit | Line | Data |
---|---|---|
02bbf9eb | 1 | #!/usr/bin/env python |
56d68fad | 2 | import os.path |
02bbf9eb | 3 | |
80dbd7d2 | 4 | import base64 |
d0538b84 | 5 | import dns |
56d68fad | 6 | import json |
02bbf9eb | 7 | import requests |
5e40d2a5 RG |
8 | import socket |
9 | import time | |
630eb526 | 10 | from dnsdisttests import DNSDistTest, pickAvailablePort |
02bbf9eb | 11 | |
72bf42cd RG |
12 | class APITestsBase(DNSDistTest): |
13 | __test__ = False | |
57af5b7c | 14 | _webTimeout = 5.0 |
630eb526 | 15 | _webServerPort = pickAvailablePort() |
02bbf9eb | 16 | _webServerBasicAuthPassword = 'secret' |
2c0392a5 | 17 | _webServerBasicAuthPasswordHashed = '$scrypt$ln=10,p=1,r=8$6DKLnvUYEeXWh3JNOd3iwg==$kSrhdHaRbZ7R74q3lGBqO1xetgxRxhmWzYJ2Qvfm7JM=' |
02bbf9eb | 18 | _webServerAPIKey = 'apisecret' |
2c0392a5 | 19 | _webServerAPIKeyHashed = '$scrypt$ln=10,p=1,r=8$9v8JxDfzQVyTpBkTbkUqYg==$bDQzAOHeK1G9UvTPypNhrX48w974ZXbFPtRKS34+aso=' |
412f99ef | 20 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
72bf42cd RG |
21 | _config_template = """ |
22 | setACL({"127.0.0.1/32", "::1/128"}) | |
23 | newServer{address="127.0.0.1:%s", pool={'', 'mypool'}} | |
24 | webserver("127.0.0.1:%s") | |
cfe95ada | 25 | setWebserverConfig({password="%s", apiKey="%s"}) |
72bf42cd | 26 | """ |
14325153 RG |
27 | _expectedMetrics = ['responses', 'servfail-responses', 'queries', 'acl-drops', |
28 | 'frontend-noerror', 'frontend-nxdomain', 'frontend-servfail', | |
29 | 'rule-drop', 'rule-nxdomain', 'rule-refused', 'self-answered', 'downstream-timeouts', | |
30 | 'downstream-send-errors', 'trunc-failures', 'no-policy', 'latency0-1', | |
31 | 'latency1-10', 'latency10-50', 'latency50-100', 'latency100-1000', | |
32 | 'latency-slow', 'latency-sum', 'latency-count', 'latency-avg100', 'latency-avg1000', | |
89b29a4d RG |
33 | 'latency-avg10000', 'latency-avg1000000', 'latency-tcp-avg100', 'latency-tcp-avg1000', |
34 | 'latency-tcp-avg10000', 'latency-tcp-avg1000000', 'latency-dot-avg100', 'latency-dot-avg1000', | |
35 | 'latency-dot-avg10000', 'latency-dot-avg1000000', 'latency-doh-avg100', 'latency-doh-avg1000', | |
5057253a | 36 | 'latency-doh-avg10000', 'latency-doh-avg1000000', 'latency-doq-avg100', 'latency-doq-avg1000', |
d1f77ae6 CHB |
37 | 'latency-doq-avg10000', 'latency-doq-avg1000000', 'latency-doh3-avg100', 'latency-doh3-avg1000', |
38 | 'latency-doh3-avg10000', 'latency-doh3-avg1000000','uptime', 'real-memory-usage', 'noncompliant-queries', | |
14325153 RG |
39 | 'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits', |
40 | 'cache-misses', 'cpu-iowait', 'cpu-steal', 'cpu-sys-msec', 'cpu-user-msec', 'fd-usage', 'dyn-blocked', | |
41 | 'dyn-block-nmg-size', 'rule-servfail', 'rule-truncated', 'security-status', | |
42 | 'udp-in-csum-errors', 'udp-in-errors', 'udp-noport-errors', 'udp-recvbuf-errors', 'udp-sndbuf-errors', | |
43 | 'udp6-in-errors', 'udp6-recvbuf-errors', 'udp6-sndbuf-errors', 'udp6-noport-errors', 'udp6-in-csum-errors', | |
d1f77ae6 | 44 | 'doh-query-pipe-full', 'doh-response-pipe-full', 'doq-response-pipe-full', 'doh3-response-pipe-full', 'proxy-protocol-invalid', 'tcp-listen-overflows', |
14325153 RG |
45 | 'outgoing-doh-query-pipe-full', 'tcp-query-pipe-full', 'tcp-cross-protocol-query-pipe-full', |
46 | 'tcp-cross-protocol-response-pipe-full'] | |
648edcba RG |
47 | _verboseMode = True |
48 | ||
49 | @classmethod | |
50 | def setUpClass(cls): | |
51 | cls.startResponders() | |
52 | cls.startDNSDist() | |
53 | cls.setUpSockets() | |
54 | cls.waitForTCPSocket('127.0.0.1', cls._webServerPort) | |
55 | print("Launching tests..") | |
72bf42cd RG |
56 | |
57 | class TestAPIBasics(APITestsBase): | |
58 | ||
55afa518 RG |
59 | # paths accessible using the API key only |
60 | _apiOnlyPaths = ['/api/v1/servers/localhost/config', '/api/v1/servers/localhost/config/allow-from', '/api/v1/servers/localhost/statistics'] | |
61 | # paths accessible using an API key or basic auth | |
62 | _statsPaths = [ '/jsonstat?command=stats', '/jsonstat?command=dynblocklist', '/api/v1/servers/localhost'] | |
02bbf9eb RG |
63 | # paths accessible using basic auth only (list not exhaustive) |
64 | _basicOnlyPaths = ['/', '/index.html'] | |
72bf42cd | 65 | __test__ = True |
02bbf9eb RG |
66 | |
67 | def testBasicAuth(self): | |
68 | """ | |
69 | API: Basic Authentication | |
70 | """ | |
55afa518 | 71 | for path in self._basicOnlyPaths + self._statsPaths: |
02bbf9eb | 72 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path |
80dbd7d2 | 73 | r = requests.get(url, auth=('whatever', "evilsecret"), timeout=self._webTimeout) |
4bfebc93 | 74 | self.assertEqual(r.status_code, 401) |
02bbf9eb RG |
75 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) |
76 | self.assertTrue(r) | |
4bfebc93 | 77 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
78 | |
79 | def testXAPIKey(self): | |
80 | """ | |
81 | API: X-Api-Key | |
82 | """ | |
83 | headers = {'x-api-key': self._webServerAPIKey} | |
55afa518 | 84 | for path in self._apiOnlyPaths + self._statsPaths: |
02bbf9eb RG |
85 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path |
86 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
87 | self.assertTrue(r) | |
4bfebc93 | 88 | self.assertEqual(r.status_code, 200) |
02bbf9eb | 89 | |
80dbd7d2 CHB |
90 | def testWrongXAPIKey(self): |
91 | """ | |
92 | API: Wrong X-Api-Key | |
93 | """ | |
94 | headers = {'x-api-key': "evilapikey"} | |
95 | for path in self._apiOnlyPaths + self._statsPaths: | |
96 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
97 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
4bfebc93 | 98 | self.assertEqual(r.status_code, 401) |
c563cbe5 | 99 | |
02bbf9eb RG |
100 | def testBasicAuthOnly(self): |
101 | """ | |
102 | API: Basic Authentication Only | |
103 | """ | |
104 | headers = {'x-api-key': self._webServerAPIKey} | |
105 | for path in self._basicOnlyPaths: | |
106 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
107 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
4bfebc93 | 108 | self.assertEqual(r.status_code, 401) |
02bbf9eb | 109 | |
55afa518 RG |
110 | def testAPIKeyOnly(self): |
111 | """ | |
112 | API: API Key Only | |
113 | """ | |
114 | for path in self._apiOnlyPaths: | |
115 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
116 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
4bfebc93 | 117 | self.assertEqual(r.status_code, 401) |
55afa518 | 118 | |
02bbf9eb RG |
119 | def testServersLocalhost(self): |
120 | """ | |
121 | API: /api/v1/servers/localhost | |
122 | """ | |
123 | headers = {'x-api-key': self._webServerAPIKey} | |
124 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost' | |
125 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
126 | self.assertTrue(r) | |
4bfebc93 | 127 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
128 | self.assertTrue(r.json()) |
129 | content = r.json() | |
130 | ||
4bfebc93 | 131 | self.assertEqual(content['daemon_type'], 'dnsdist') |
02bbf9eb | 132 | |
f8a222ac RG |
133 | rule_groups = ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules', 'rules'] |
134 | for key in ['version', 'acl', 'local', 'servers', 'frontends', 'pools'] + rule_groups: | |
02bbf9eb RG |
135 | self.assertIn(key, content) |
136 | ||
d18eab67 CH |
137 | for rule_group in rule_groups: |
138 | for rule in content[rule_group]: | |
f8a222ac | 139 | for key in ['id', 'creationOrder', 'matches', 'rule', 'action', 'uuid']: |
d18eab67 | 140 | self.assertIn(key, rule) |
f8a222ac | 141 | for key in ['id', 'creationOrder', 'matches']: |
d18eab67 | 142 | self.assertTrue(rule[key] >= 0) |
4ace9fe8 | 143 | |
02bbf9eb RG |
144 | for server in content['servers']: |
145 | for key in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit', | |
6a78f305 | 146 | 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors', |
d70f95ac | 147 | 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse', |
0f06daf9 | 148 | 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections', |
2a5cfdfb | 149 | 'tcpNewConnections', 'tcpReusedConnections', 'tlsResumptions', 'tcpAvgQueriesPerConnection', |
da73e6b3 | 150 | 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol', 'healthCheckFailures', 'healthCheckFailuresParsing', 'healthCheckFailuresTimeout', 'healthCheckFailuresNetwork', 'healthCheckFailuresMismatch', 'healthCheckFailuresInvalid']: |
02bbf9eb RG |
151 | self.assertIn(key, server) |
152 | ||
153 | for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds', | |
d70f95ac | 154 | 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']: |
02bbf9eb RG |
155 | self.assertTrue(server[key] >= 0) |
156 | ||
157 | self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN']) | |
158 | ||
159 | for frontend in content['frontends']: | |
fd23f5de | 160 | for key in ['id', 'address', 'udp', 'tcp', 'type', 'queries', 'nonCompliantQueries']: |
02bbf9eb RG |
161 | self.assertIn(key, frontend) |
162 | ||
fd23f5de | 163 | for key in ['id', 'queries', 'nonCompliantQueries']: |
02bbf9eb RG |
164 | self.assertTrue(frontend[key] >= 0) |
165 | ||
4ace9fe8 | 166 | for pool in content['pools']: |
27792330 | 167 | for key in ['id', 'name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']: |
4ace9fe8 RG |
168 | self.assertIn(key, pool) |
169 | ||
27792330 | 170 | for key in ['id', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts', 'cacheCleanupCount']: |
4ace9fe8 RG |
171 | self.assertTrue(pool[key] >= 0) |
172 | ||
14325153 RG |
173 | stats = content['statistics'] |
174 | for key in self._expectedMetrics: | |
175 | self.assertIn(key, stats) | |
176 | self.assertTrue(stats[key] >= 0) | |
177 | for key in stats: | |
178 | self.assertIn(key, self._expectedMetrics) | |
179 | ||
bc6e4c3a RG |
180 | def testServersLocalhostPool(self): |
181 | """ | |
182 | API: /api/v1/servers/localhost/pool?name=mypool | |
183 | """ | |
184 | headers = {'x-api-key': self._webServerAPIKey} | |
185 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/pool?name=mypool' | |
186 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
187 | self.assertTrue(r) | |
188 | self.assertEqual(r.status_code, 200) | |
189 | self.assertTrue(r.json()) | |
190 | content = r.json() | |
191 | ||
192 | self.assertIn('stats', content) | |
193 | self.assertIn('servers', content) | |
194 | ||
195 | for key in ['name', 'cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']: | |
196 | self.assertIn(key, content['stats']) | |
197 | ||
198 | for key in ['cacheSize', 'cacheEntries', 'cacheHits', 'cacheMisses', 'cacheDeferredInserts', 'cacheDeferredLookups', 'cacheLookupCollisions', 'cacheInsertCollisions', 'cacheTTLTooShorts']: | |
199 | self.assertTrue(content['stats'][key] >= 0) | |
200 | ||
201 | for server in content['servers']: | |
202 | for key in ['id', 'latency', 'name', 'weight', 'outstanding', 'qpsLimit', | |
203 | 'reuseds', 'state', 'address', 'pools', 'qps', 'queries', 'order', 'sendErrors', | |
d70f95ac | 204 | 'dropRate', 'responses', 'nonCompliantResponses', 'tcpDiedSendingQuery', 'tcpDiedReadingResponse', |
bc6e4c3a RG |
205 | 'tcpGaveUp', 'tcpReadTimeouts', 'tcpWriteTimeouts', 'tcpCurrentConnections', |
206 | 'tcpNewConnections', 'tcpReusedConnections', 'tcpAvgQueriesPerConnection', | |
7f7167e0 | 207 | 'tcpAvgConnectionDuration', 'tcpLatency', 'protocol']: |
bc6e4c3a RG |
208 | self.assertIn(key, server) |
209 | ||
210 | for key in ['id', 'latency', 'weight', 'outstanding', 'qpsLimit', 'reuseds', | |
d70f95ac | 211 | 'qps', 'queries', 'order', 'tcpLatency', 'responses', 'nonCompliantResponses']: |
bc6e4c3a RG |
212 | self.assertTrue(server[key] >= 0) |
213 | ||
214 | self.assertTrue(server['state'] in ['up', 'down', 'UP', 'DOWN']) | |
215 | ||
00566cbf PL |
216 | def testServersIDontExist(self): |
217 | """ | |
ef2ea4bf | 218 | API: /api/v1/servers/idonotexist (should be 404) |
00566cbf PL |
219 | """ |
220 | headers = {'x-api-key': self._webServerAPIKey} | |
ef2ea4bf | 221 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/idonotexist' |
00566cbf | 222 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
4bfebc93 | 223 | self.assertEqual(r.status_code, 404) |
00566cbf | 224 | |
02bbf9eb RG |
225 | def testServersLocalhostConfig(self): |
226 | """ | |
227 | API: /api/v1/servers/localhost/config | |
228 | """ | |
229 | headers = {'x-api-key': self._webServerAPIKey} | |
230 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config' | |
231 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
232 | self.assertTrue(r) | |
4bfebc93 | 233 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
234 | self.assertTrue(r.json()) |
235 | content = r.json() | |
236 | values = {} | |
237 | for entry in content: | |
238 | for key in ['type', 'name', 'value']: | |
239 | self.assertIn(key, entry) | |
240 | ||
4bfebc93 | 241 | self.assertEqual(entry['type'], 'ConfigSetting') |
02bbf9eb RG |
242 | values[entry['name']] = entry['value'] |
243 | ||
244 | for key in ['acl', 'control-socket', 'ecs-override', 'ecs-source-prefix-v4', | |
245 | 'ecs-source-prefix-v6', 'fixup-case', 'max-outstanding', 'server-policy', | |
246 | 'stale-cache-entries-ttl', 'tcp-recv-timeout', 'tcp-send-timeout', | |
247 | 'truncate-tc', 'verbose', 'verbose-health-checks']: | |
248 | self.assertIn(key, values) | |
249 | ||
250 | for key in ['max-outstanding', 'stale-cache-entries-ttl', 'tcp-recv-timeout', | |
251 | 'tcp-send-timeout']: | |
252 | self.assertTrue(values[key] >= 0) | |
253 | ||
254 | self.assertTrue(values['ecs-source-prefix-v4'] >= 0 and values['ecs-source-prefix-v4'] <= 32) | |
255 | self.assertTrue(values['ecs-source-prefix-v6'] >= 0 and values['ecs-source-prefix-v6'] <= 128) | |
256 | ||
56d68fad RG |
257 | def testServersLocalhostConfigAllowFrom(self): |
258 | """ | |
259 | API: /api/v1/servers/localhost/config/allow-from | |
260 | """ | |
261 | headers = {'x-api-key': self._webServerAPIKey} | |
262 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
263 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
264 | self.assertTrue(r) | |
4bfebc93 | 265 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
266 | self.assertTrue(r.json()) |
267 | content = r.json() | |
268 | for key in ['type', 'name', 'value']: | |
269 | self.assertIn(key, content) | |
270 | ||
4bfebc93 CH |
271 | self.assertEqual(content['name'], 'allow-from') |
272 | self.assertEqual(content['type'], 'ConfigSetting') | |
078efd26 RG |
273 | acl = content['value'] |
274 | expectedACL = ["127.0.0.1/32", "::1/128"] | |
275 | acl.sort() | |
276 | expectedACL.sort() | |
4bfebc93 | 277 | self.assertEqual(acl, expectedACL) |
56d68fad RG |
278 | |
279 | def testServersLocalhostConfigAllowFromPut(self): | |
280 | """ | |
281 | API: PUT /api/v1/servers/localhost/config/allow-from (should be refused) | |
282 | ||
283 | The API is read-only by default, so this should be refused | |
284 | """ | |
285 | newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"] | |
286 | payload = json.dumps({"name": "allow-from", | |
287 | "type": "ConfigSetting", | |
288 | "value": newACL}) | |
289 | headers = {'x-api-key': self._webServerAPIKey} | |
290 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
291 | r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload) | |
292 | self.assertFalse(r) | |
4bfebc93 | 293 | self.assertEqual(r.status_code, 405) |
56d68fad | 294 | |
02bbf9eb RG |
295 | def testServersLocalhostStatistics(self): |
296 | """ | |
297 | API: /api/v1/servers/localhost/statistics | |
298 | """ | |
299 | headers = {'x-api-key': self._webServerAPIKey} | |
300 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/statistics' | |
301 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
302 | self.assertTrue(r) | |
4bfebc93 | 303 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
304 | self.assertTrue(r.json()) |
305 | content = r.json() | |
306 | values = {} | |
307 | for entry in content: | |
308 | self.assertIn('type', entry) | |
309 | self.assertIn('name', entry) | |
310 | self.assertIn('value', entry) | |
4bfebc93 | 311 | self.assertEqual(entry['type'], 'StatisticItem') |
02bbf9eb RG |
312 | values[entry['name']] = entry['value'] |
313 | ||
14325153 | 314 | for key in self._expectedMetrics: |
02bbf9eb RG |
315 | self.assertIn(key, values) |
316 | self.assertTrue(values[key] >= 0) | |
317 | ||
dd46e5e3 | 318 | for key in values: |
14325153 | 319 | self.assertIn(key, self._expectedMetrics) |
dd46e5e3 | 320 | |
02bbf9eb RG |
321 | def testJsonstatStats(self): |
322 | """ | |
323 | API: /jsonstat?command=stats | |
324 | """ | |
325 | headers = {'x-api-key': self._webServerAPIKey} | |
326 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=stats' | |
327 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
328 | self.assertTrue(r) | |
4bfebc93 | 329 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
330 | self.assertTrue(r.json()) |
331 | content = r.json() | |
332 | ||
14325153 | 333 | for key in self._expectedMetrics: |
02bbf9eb RG |
334 | self.assertIn(key, content) |
335 | self.assertTrue(content[key] >= 0) | |
336 | ||
337 | def testJsonstatDynblocklist(self): | |
338 | """ | |
339 | API: /jsonstat?command=dynblocklist | |
340 | """ | |
341 | headers = {'x-api-key': self._webServerAPIKey} | |
342 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=dynblocklist' | |
343 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
344 | self.assertTrue(r) | |
4bfebc93 | 345 | self.assertEqual(r.status_code, 200) |
02bbf9eb RG |
346 | |
347 | content = r.json() | |
348 | ||
349 | if content: | |
477c86a0 | 350 | for key in ['reason', 'seconds', 'blocks', 'action']: |
02bbf9eb RG |
351 | self.assertIn(key, content) |
352 | ||
353 | for key in ['blocks']: | |
354 | self.assertTrue(content[key] >= 0) | |
56d68fad | 355 | |
d0538b84 RG |
356 | def testServersLocalhostRings(self): |
357 | """ | |
358 | API: /api/v1/servers/localhost/rings | |
359 | """ | |
360 | headers = {'x-api-key': self._webServerAPIKey} | |
361 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/rings' | |
362 | expectedValues = ['age', 'id', 'name', 'requestor', 'size', 'qtype', 'protocol', 'rd'] | |
363 | expectedResponseValues = expectedValues + ['latency', 'rcode', 'tc', 'aa', 'answers', 'backend'] | |
364 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
365 | self.assertTrue(r) | |
366 | self.assertEqual(r.status_code, 200) | |
367 | self.assertTrue(r.json()) | |
368 | content = r.json() | |
369 | self.assertIn('queries', content) | |
370 | self.assertIn('responses', content) | |
371 | self.assertEqual(len(content['queries']), 0) | |
372 | self.assertEqual(len(content['responses']), 0) | |
373 | ||
374 | name = 'simple.api.tests.powerdns.com.' | |
375 | query = dns.message.make_query(name, 'A', 'IN') | |
376 | response = dns.message.make_response(query) | |
377 | rrset = dns.rrset.from_text(name, | |
378 | 3600, | |
379 | dns.rdataclass.IN, | |
380 | dns.rdatatype.A, | |
381 | '127.0.0.1') | |
382 | response.answer.append(rrset) | |
383 | ||
384 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
385 | sender = getattr(self, method) | |
386 | (receivedQuery, receivedResponse) = sender(query, response) | |
387 | self.assertTrue(receivedQuery) | |
388 | self.assertTrue(receivedResponse) | |
389 | receivedQuery.id = query.id | |
390 | self.assertEqual(query, receivedQuery) | |
391 | self.assertEqual(response, receivedResponse) | |
392 | ||
393 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
394 | self.assertTrue(r) | |
395 | self.assertEqual(r.status_code, 200) | |
396 | self.assertTrue(r.json()) | |
397 | content = r.json() | |
398 | self.assertIn('queries', content) | |
399 | self.assertIn('responses', content) | |
400 | self.assertEqual(len(content['queries']), 2) | |
401 | self.assertEqual(len(content['responses']), 2) | |
402 | for entry in content['queries']: | |
403 | for value in expectedValues: | |
404 | self.assertIn(value, entry) | |
405 | for entry in content['responses']: | |
406 | for value in expectedResponseValues: | |
407 | self.assertIn(value, entry) | |
408 | ||
72bf42cd RG |
409 | class TestAPIServerDown(APITestsBase): |
410 | __test__ = True | |
36927800 RG |
411 | _config_template = """ |
412 | setACL({"127.0.0.1/32", "::1/128"}) | |
413 | newServer{address="127.0.0.1:%s"} | |
414 | getServer(0):setDown() | |
fa7e8b5d | 415 | webserver("127.0.0.1:%s") |
cfe95ada | 416 | setWebserverConfig({password="%s", apiKey="%s"}) |
36927800 RG |
417 | """ |
418 | ||
419 | def testServerDownNoLatencyLocalhost(self): | |
420 | """ | |
421 | API: /api/v1/servers/localhost, no latency for a down server | |
422 | """ | |
423 | headers = {'x-api-key': self._webServerAPIKey} | |
424 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost' | |
425 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
426 | self.assertTrue(r) | |
4bfebc93 | 427 | self.assertEqual(r.status_code, 200) |
36927800 RG |
428 | self.assertTrue(r.json()) |
429 | content = r.json() | |
430 | ||
4bfebc93 | 431 | self.assertEqual(content['servers'][0]['latency'], None) |
7f7167e0 | 432 | self.assertEqual(content['servers'][0]['tcpLatency'], None) |
36927800 | 433 | |
72bf42cd RG |
434 | class TestAPIWritable(APITestsBase): |
435 | __test__ = True | |
56d68fad | 436 | _APIWriteDir = '/tmp' |
412f99ef | 437 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_APIWriteDir'] |
56d68fad RG |
438 | _config_template = """ |
439 | setACL({"127.0.0.1/32", "::1/128"}) | |
440 | newServer{address="127.0.0.1:%s"} | |
fa7e8b5d | 441 | webserver("127.0.0.1:%s") |
cfe95ada | 442 | setWebserverConfig({password="%s", apiKey="%s"}) |
56d68fad RG |
443 | setAPIWritable(true, "%s") |
444 | """ | |
445 | ||
446 | def testSetACL(self): | |
447 | """ | |
448 | API: Set ACL | |
449 | """ | |
450 | headers = {'x-api-key': self._webServerAPIKey} | |
451 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/config/allow-from' | |
452 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
453 | self.assertTrue(r) | |
4bfebc93 | 454 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
455 | self.assertTrue(r.json()) |
456 | content = r.json() | |
078efd26 RG |
457 | acl = content['value'] |
458 | expectedACL = ["127.0.0.1/32", "::1/128"] | |
459 | acl.sort() | |
460 | expectedACL.sort() | |
4bfebc93 | 461 | self.assertEqual(acl, expectedACL) |
56d68fad RG |
462 | |
463 | newACL = ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"] | |
464 | payload = json.dumps({"name": "allow-from", | |
465 | "type": "ConfigSetting", | |
466 | "value": newACL}) | |
467 | r = requests.put(url, headers=headers, timeout=self._webTimeout, data=payload) | |
468 | self.assertTrue(r) | |
4bfebc93 | 469 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
470 | self.assertTrue(r.json()) |
471 | content = r.json() | |
b4be3cc0 OM |
472 | acl = content['value'] |
473 | acl.sort() | |
4bfebc93 | 474 | self.assertEqual(acl, newACL) |
56d68fad RG |
475 | |
476 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
477 | self.assertTrue(r) | |
4bfebc93 | 478 | self.assertEqual(r.status_code, 200) |
56d68fad RG |
479 | self.assertTrue(r.json()) |
480 | content = r.json() | |
b4be3cc0 OM |
481 | acl = content['value'] |
482 | acl.sort() | |
4bfebc93 | 483 | self.assertEqual(acl, newACL) |
56d68fad RG |
484 | |
485 | configFile = self._APIWriteDir + '/' + 'acl.conf' | |
486 | self.assertTrue(os.path.isfile(configFile)) | |
487 | fileContent = None | |
b4f23783 | 488 | with open(configFile, 'rt') as f: |
b4be3cc0 OM |
489 | header = f.readline() |
490 | body = f.readline() | |
491 | ||
4bfebc93 | 492 | self.assertEqual(header, """-- Generated by the REST API, DO NOT EDIT\n""") |
b4be3cc0 OM |
493 | |
494 | self.assertIn(body, { | |
495 | """setACL({"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"})\n""", | |
496 | """setACL({"192.0.2.0/24", "203.0.113.0/24", "198.51.100.0/24"})\n""", | |
497 | """setACL({"198.51.100.0/24", "192.0.2.0/24", "203.0.113.0/24"})\n""", | |
498 | """setACL({"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24"})\n""", | |
499 | """setACL({"203.0.113.0/24", "192.0.2.0/24", "198.51.100.0/24"})\n""", | |
500 | """setACL({"203.0.113.0/24", "198.51.100.0/24", "192.0.2.0/24"})\n""" | |
501 | }) | |
80dbd7d2 | 502 | |
72bf42cd RG |
503 | class TestAPICustomHeaders(APITestsBase): |
504 | __test__ = True | |
32c97b56 CHB |
505 | # paths accessible using the API key only |
506 | _apiOnlyPath = '/api/v1/servers/localhost/config' | |
507 | # paths accessible using basic auth only (list not exhaustive) | |
508 | _basicOnlyPath = '/' | |
509 | _consoleKey = DNSDistTest.generateConsoleKey() | |
510 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 511 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
32c97b56 CHB |
512 | _config_template = """ |
513 | setKey("%s") | |
514 | controlSocket("127.0.0.1:%s") | |
515 | setACL({"127.0.0.1/32", "::1/128"}) | |
516 | newServer({address="127.0.0.1:%s"}) | |
fa7e8b5d | 517 | webserver("127.0.0.1:%s") |
cfe95ada | 518 | setWebserverConfig({password="%s", apiKey="%s", customHeaders={["X-Frame-Options"]="", ["X-Custom"]="custom"} }) |
32c97b56 CHB |
519 | """ |
520 | ||
521 | def testBasicHeaders(self): | |
522 | """ | |
523 | API: Basic custom headers | |
524 | """ | |
525 | ||
526 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath | |
527 | ||
528 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
529 | self.assertTrue(r) | |
4bfebc93 CH |
530 | self.assertEqual(r.status_code, 200) |
531 | self.assertEqual(r.headers.get('x-custom'), "custom") | |
32c97b56 CHB |
532 | self.assertFalse("x-frame-options" in r.headers) |
533 | ||
534 | def testBasicHeadersUpdate(self): | |
535 | """ | |
536 | API: Basic update of custom headers | |
537 | """ | |
538 | ||
539 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath | |
540 | self.sendConsoleCommand('setWebserverConfig({customHeaders={["x-powered-by"]="dnsdist"}})') | |
541 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
542 | self.assertTrue(r) | |
4bfebc93 CH |
543 | self.assertEqual(r.status_code, 200) |
544 | self.assertEqual(r.headers.get('x-powered-by'), "dnsdist") | |
32c97b56 CHB |
545 | self.assertTrue("x-frame-options" in r.headers) |
546 | ||
72bf42cd RG |
547 | class TestStatsWithoutAuthentication(APITestsBase): |
548 | __test__ = True | |
fa7e8b5d RG |
549 | # paths accessible using the API key only |
550 | _apiOnlyPath = '/api/v1/servers/localhost/config' | |
551 | # paths accessible using basic auth only (list not exhaustive) | |
552 | _basicOnlyPath = '/' | |
553 | _noAuthenticationPaths = [ '/metrics', '/jsonstat?command=dynblocklist' ] | |
554 | _consoleKey = DNSDistTest.generateConsoleKey() | |
555 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 556 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
fa7e8b5d RG |
557 | _config_template = """ |
558 | setKey("%s") | |
559 | controlSocket("127.0.0.1:%s") | |
560 | setACL({"127.0.0.1/32", "::1/128"}) | |
561 | newServer({address="127.0.0.1:%s"}) | |
562 | webserver("127.0.0.1:%s") | |
cfe95ada | 563 | setWebserverConfig({password="%s", apiKey="%s", statsRequireAuthentication=false }) |
fa7e8b5d RG |
564 | """ |
565 | ||
566 | def testAuth(self): | |
567 | """ | |
568 | API: Stats do not require authentication | |
569 | """ | |
570 | ||
571 | for path in self._noAuthenticationPaths: | |
572 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
573 | ||
574 | r = requests.get(url, timeout=self._webTimeout) | |
575 | self.assertTrue(r) | |
4bfebc93 | 576 | self.assertEqual(r.status_code, 200) |
fa7e8b5d RG |
577 | |
578 | # these should still require basic authentication | |
579 | for path in [self._basicOnlyPath]: | |
580 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
581 | ||
582 | r = requests.get(url, timeout=self._webTimeout) | |
4bfebc93 | 583 | self.assertEqual(r.status_code, 401) |
fa7e8b5d RG |
584 | |
585 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
586 | self.assertTrue(r) | |
4bfebc93 | 587 | self.assertEqual(r.status_code, 200) |
fa7e8b5d RG |
588 | |
589 | # these should still require API authentication | |
590 | for path in [self._apiOnlyPath]: | |
591 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
592 | ||
593 | r = requests.get(url, timeout=self._webTimeout) | |
4bfebc93 | 594 | self.assertEqual(r.status_code, 401) |
fa7e8b5d RG |
595 | |
596 | headers = {'x-api-key': self._webServerAPIKey} | |
597 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
598 | self.assertTrue(r) | |
4bfebc93 | 599 | self.assertEqual(r.status_code, 200) |
32c97b56 | 600 | |
72bf42cd RG |
601 | class TestAPIAuth(APITestsBase): |
602 | __test__ = True | |
80dbd7d2 | 603 | _webServerBasicAuthPasswordNew = 'password' |
2c0392a5 | 604 | _webServerBasicAuthPasswordNewHashed = '$scrypt$ln=10,p=1,r=8$yefz8SAuT3lj3moXqUYvmw==$T98/RYMp76ZYNjd7MpAkcVXZEDqpLtrc3tQ52QflVBA=' |
80dbd7d2 | 605 | _webServerAPIKeyNew = 'apipassword' |
2c0392a5 | 606 | _webServerAPIKeyNewHashed = '$scrypt$ln=9,p=1,r=8$y96I9nfkY0LWDQEdSUzWgA==$jiyn9QD36o9d0ADrlqiIBk4AKyQrkD1KYw3CexwtHp4=' |
80dbd7d2 CHB |
607 | # paths accessible using the API key only |
608 | _apiOnlyPath = '/api/v1/servers/localhost/config' | |
609 | # paths accessible using basic auth only (list not exhaustive) | |
610 | _basicOnlyPath = '/' | |
611 | _consoleKey = DNSDistTest.generateConsoleKey() | |
612 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 613 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
80dbd7d2 CHB |
614 | _config_template = """ |
615 | setKey("%s") | |
616 | controlSocket("127.0.0.1:%s") | |
617 | setACL({"127.0.0.1/32", "::1/128"}) | |
618 | newServer{address="127.0.0.1:%s"} | |
fa7e8b5d | 619 | webserver("127.0.0.1:%s") |
cfe95ada | 620 | setWebserverConfig({password="%s", apiKey="%s"}) |
80dbd7d2 CHB |
621 | """ |
622 | ||
623 | def testBasicAuthChange(self): | |
624 | """ | |
625 | API: Basic Authentication updating credentials | |
626 | """ | |
627 | ||
80dbd7d2 | 628 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._basicOnlyPath |
cfe95ada | 629 | self.sendConsoleCommand('setWebserverConfig({{password="{}"}})'.format(self._webServerBasicAuthPasswordNewHashed)) |
32c97b56 | 630 | |
80dbd7d2 CHB |
631 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPasswordNew), timeout=self._webTimeout) |
632 | self.assertTrue(r) | |
4bfebc93 | 633 | self.assertEqual(r.status_code, 200) |
80dbd7d2 CHB |
634 | |
635 | # Make sure the old password is not usable any more | |
80dbd7d2 | 636 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) |
4bfebc93 | 637 | self.assertEqual(r.status_code, 401) |
80dbd7d2 CHB |
638 | |
639 | def testXAPIKeyChange(self): | |
640 | """ | |
641 | API: X-Api-Key updating credentials | |
642 | """ | |
643 | ||
32c97b56 | 644 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath |
412f99ef | 645 | self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed)) |
80dbd7d2 CHB |
646 | |
647 | headers = {'x-api-key': self._webServerAPIKeyNew} | |
80dbd7d2 CHB |
648 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
649 | self.assertTrue(r) | |
4bfebc93 | 650 | self.assertEqual(r.status_code, 200) |
80dbd7d2 CHB |
651 | |
652 | # Make sure the old password is not usable any more | |
653 | headers = {'x-api-key': self._webServerAPIKey} | |
80dbd7d2 | 654 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
4bfebc93 | 655 | self.assertEqual(r.status_code, 401) |
80dbd7d2 CHB |
656 | |
657 | def testBasicAuthOnlyChange(self): | |
658 | """ | |
659 | API: X-Api-Key updated to none (disabled) | |
660 | """ | |
661 | ||
32c97b56 | 662 | url = 'http://127.0.0.1:' + str(self._webServerPort) + self._apiOnlyPath |
412f99ef | 663 | self.sendConsoleCommand('setWebserverConfig({{apiKey="{}"}})'.format(self._webServerAPIKeyNewHashed)) |
80dbd7d2 CHB |
664 | |
665 | headers = {'x-api-key': self._webServerAPIKeyNew} | |
80dbd7d2 CHB |
666 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
667 | self.assertTrue(r) | |
4bfebc93 | 668 | self.assertEqual(r.status_code, 200) |
80dbd7d2 CHB |
669 | |
670 | # now disable apiKey | |
32c97b56 | 671 | self.sendConsoleCommand('setWebserverConfig({apiKey=""})') |
80dbd7d2 | 672 | |
80dbd7d2 | 673 | r = requests.get(url, headers=headers, timeout=self._webTimeout) |
4bfebc93 | 674 | self.assertEqual(r.status_code, 401) |
1c90c6bd | 675 | |
72bf42cd RG |
676 | class TestAPIACL(APITestsBase): |
677 | __test__ = True | |
1c90c6bd RG |
678 | _consoleKey = DNSDistTest.generateConsoleKey() |
679 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
412f99ef | 680 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] |
1c90c6bd RG |
681 | _config_template = """ |
682 | setKey("%s") | |
683 | controlSocket("127.0.0.1:%s") | |
684 | setACL({"127.0.0.1/32", "::1/128"}) | |
685 | newServer{address="127.0.0.1:%s"} | |
fa7e8b5d | 686 | webserver("127.0.0.1:%s") |
cfe95ada | 687 | setWebserverConfig({password="%s", apiKey="%s", acl="192.0.2.1"}) |
1c90c6bd RG |
688 | """ |
689 | ||
690 | def testACLChange(self): | |
691 | """ | |
692 | API: Should be denied by ACL then allowed | |
693 | """ | |
694 | ||
695 | url = 'http://127.0.0.1:' + str(self._webServerPort) + "/" | |
696 | try: | |
697 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
698 | self.assertTrue(False) | |
699 | except requests.exceptions.ConnectionError as exp: | |
700 | pass | |
701 | ||
702 | # reset the ACL | |
703 | self.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})') | |
704 | ||
705 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
706 | self.assertTrue(r) | |
4bfebc93 | 707 | self.assertEqual(r.status_code, 200) |
88d4fe87 | 708 | |
80af53eb RG |
709 | class TestAPIWithoutAuthentication(APITestsBase): |
710 | __test__ = True | |
711 | _apiPath = '/api/v1/servers/localhost/config' | |
712 | # paths accessible using basic auth only (list not exhaustive) | |
713 | _basicOnlyPath = '/' | |
714 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed'] | |
715 | _config_template = """ | |
716 | setACL({"127.0.0.1/32", "::1/128"}) | |
717 | newServer({address="127.0.0.1:%s"}) | |
718 | webserver("127.0.0.1:%s") | |
719 | setWebserverConfig({password="%s", apiRequiresAuthentication=false }) | |
720 | """ | |
721 | ||
722 | def testAuth(self): | |
723 | """ | |
724 | API: API do not require authentication | |
725 | """ | |
726 | ||
727 | for path in [self._apiPath]: | |
728 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
729 | ||
730 | r = requests.get(url, timeout=self._webTimeout) | |
731 | self.assertTrue(r) | |
732 | self.assertEqual(r.status_code, 200) | |
733 | ||
734 | # these should still require basic authentication | |
735 | for path in [self._basicOnlyPath]: | |
736 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
737 | ||
738 | r = requests.get(url, timeout=self._webTimeout) | |
739 | self.assertEqual(r.status_code, 401) | |
740 | ||
741 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
742 | self.assertTrue(r) | |
743 | self.assertEqual(r.status_code, 200) | |
744 | ||
4bbed5cf RG |
745 | class TestDashboardWithoutAuthentication(APITestsBase): |
746 | __test__ = True | |
747 | _basicPath = '/' | |
748 | _config_params = ['_testServerPort', '_webServerPort'] | |
749 | _config_template = """ | |
750 | setACL({"127.0.0.1/32", "::1/128"}) | |
751 | newServer({address="127.0.0.1:%d"}) | |
752 | webserver("127.0.0.1:%d") | |
753 | setWebserverConfig({ dashboardRequiresAuthentication=false }) | |
754 | """ | |
755 | _verboseMode=True | |
756 | ||
757 | def testDashboard(self): | |
758 | """ | |
759 | API: Dashboard do not require authentication | |
760 | """ | |
761 | ||
762 | for path in [self._basicPath]: | |
763 | url = 'http://127.0.0.1:' + str(self._webServerPort) + path | |
764 | ||
765 | r = requests.get(url, timeout=self._webTimeout) | |
766 | self.assertTrue(r) | |
767 | self.assertEqual(r.status_code, 200) | |
768 | ||
72bf42cd RG |
769 | class TestCustomLuaEndpoint(APITestsBase): |
770 | __test__ = True | |
88d4fe87 RG |
771 | _config_template = """ |
772 | setACL({"127.0.0.1/32", "::1/128"}) | |
773 | newServer{address="127.0.0.1:%s"} | |
72bf42cd | 774 | webserver("127.0.0.1:%s") |
cfe95ada | 775 | setWebserverConfig({password="%s"}) |
88d4fe87 RG |
776 | |
777 | function customHTTPHandler(req, resp) | |
778 | if req.path ~= '/foo' then | |
779 | resp.status = 500 | |
780 | return | |
781 | end | |
782 | ||
783 | if req.version ~= 11 then | |
784 | resp.status = 501 | |
785 | return | |
786 | end | |
787 | ||
788 | if req.method ~= 'GET' then | |
789 | resp.status = 502 | |
790 | return | |
791 | end | |
792 | ||
793 | local get = req.getvars | |
794 | if get['param'] ~= '42' then | |
795 | resp.status = 503 | |
796 | return | |
797 | end | |
798 | ||
799 | local headers = req.headers | |
800 | if headers['customheader'] ~= 'foobar' then | |
801 | resp.status = 504 | |
802 | return | |
803 | end | |
804 | ||
805 | resp.body = 'It works!' | |
806 | resp.status = 200 | |
807 | resp.headers = { ['Foo']='Bar'} | |
808 | end | |
809 | registerWebHandler('/foo', customHTTPHandler) | |
810 | """ | |
72bf42cd | 811 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed'] |
88d4fe87 RG |
812 | |
813 | def testBasic(self): | |
814 | """ | |
815 | Custom Web Handler | |
816 | """ | |
817 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/foo?param=42' | |
818 | headers = {'customheader': 'foobar'} | |
819 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout, headers=headers) | |
820 | self.assertTrue(r) | |
4bfebc93 CH |
821 | self.assertEqual(r.status_code, 200) |
822 | self.assertEqual(r.content, b'It works!') | |
823 | self.assertEqual(r.headers.get('foo'), "Bar") | |
5e40d2a5 | 824 | |
72bf42cd RG |
825 | class TestWebConcurrentConnections(APITestsBase): |
826 | __test__ = True | |
5e40d2a5 RG |
827 | _maxConns = 2 |
828 | ||
412f99ef | 829 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed', '_maxConns'] |
5e40d2a5 RG |
830 | _config_template = """ |
831 | newServer{address="127.0.0.1:%s"} | |
832 | webserver("127.0.0.1:%s") | |
cfe95ada | 833 | setWebserverConfig({password="%s", apiKey="%s", maxConcurrentConnections=%d}) |
5e40d2a5 RG |
834 | """ |
835 | ||
13e4e845 RG |
836 | @classmethod |
837 | def setUpClass(cls): | |
838 | cls.startResponders() | |
839 | cls.startDNSDist() | |
840 | cls.setUpSockets() | |
841 | # do no check if the web server socket is up, because this | |
842 | # might mess up the concurrent connections counter | |
843 | ||
5e40d2a5 RG |
844 | def testConcurrentConnections(self): |
845 | """ | |
846 | Web: Concurrent connections | |
847 | """ | |
848 | ||
849 | conns = [] | |
850 | # open the maximum number of connections | |
851 | for _ in range(self._maxConns): | |
852 | conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
853 | conn.connect(("127.0.0.1", self._webServerPort)) | |
854 | conns.append(conn) | |
855 | ||
856 | # we now hold all the slots, let's try to establish a new connection | |
857 | url = 'http://127.0.0.1:' + str(self._webServerPort) + "/" | |
858 | self.assertRaises(requests.exceptions.ConnectionError, requests.get, url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
859 | ||
860 | # free one slot | |
861 | conns[0].close() | |
862 | conns[0] = None | |
863 | time.sleep(1) | |
864 | ||
865 | # this should work | |
866 | r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout) | |
867 | self.assertTrue(r) | |
4bfebc93 | 868 | self.assertEqual(r.status_code, 200) |
6211164a CHB |
869 | |
870 | class TestAPICustomStatistics(APITestsBase): | |
871 | __test__ = True | |
872 | _maxConns = 2 | |
873 | ||
874 | _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed'] | |
875 | _config_template = """ | |
876 | newServer{address="127.0.0.1:%s"} | |
877 | webserver("127.0.0.1:%s") | |
b08586c7 CHB |
878 | declareMetric("my-custom-metric", "counter", "Number of statistics") |
879 | declareMetric("my-other-metric", "counter", "Another number of statistics") | |
880 | declareMetric("my-gauge", "gauge", "Current memory usage") | |
6211164a CHB |
881 | setWebserverConfig({password="%s", apiKey="%s"}) |
882 | """ | |
883 | ||
884 | def testCustomStats(self): | |
885 | """ | |
886 | API: /jsonstat?command=stats | |
887 | Test custom statistics are exposed | |
888 | """ | |
889 | headers = {'x-api-key': self._webServerAPIKey} | |
890 | url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=stats' | |
891 | r = requests.get(url, headers=headers, timeout=self._webTimeout) | |
892 | self.assertTrue(r) | |
893 | self.assertEqual(r.status_code, 200) | |
894 | self.assertTrue(r.json()) | |
895 | content = r.json() | |
896 | ||
897 | expected = ['my-custom-metric', 'my-other-metric', 'my-gauge'] | |
898 | ||
899 | for key in expected: | |
900 | self.assertIn(key, content) | |
901 | self.assertTrue(content[key] >= 0) |