]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.dnsdist/test_DOH.py
Merge pull request #8804 from rgacogne/ddist-install-config-file
[thirdparty/pdns.git] / regression-tests.dnsdist / test_DOH.py
CommitLineData
10535d88
RG
1#!/usr/bin/env python
2import base64
3import dns
13291274 4import os
0026abf9
RG
5import re
6import time
13291274 7import unittest
10535d88
RG
8import clientsubnetoption
9from dnsdisttests import DNSDistTest
10
11import pycurl
811872fb 12from io import BytesIO
10535d88
RG
13#from hyper import HTTP20Connection
14#from hyper.ssl_compat import SSLContext, PROTOCOL_TLSv1_2
15
13291274 16@unittest.skipIf('SKIP_DOH_TESTS' in os.environ, 'DNS over HTTPS tests are disabled')
10535d88
RG
17class DNSDistDOHTest(DNSDistTest):
18
19 @classmethod
47225117
RG
20 def getDOHGetURL(cls, baseurl, query, rawQuery=False):
21 if rawQuery:
22 wire = query
23 else:
24 wire = query.to_wire()
10535d88
RG
25 param = base64.urlsafe_b64encode(wire).decode('UTF8').rstrip('=')
26 return baseurl + "?dns=" + param
27
28 @classmethod
29 def openDOHConnection(cls, port, caFile, timeout=2.0):
30 conn = pycurl.Curl()
31 conn.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_2)
32
33 conn.setopt(pycurl.HTTPHEADER, ["Content-type: application/dns-message",
34 "Accept: application/dns-message"])
35 return conn
36
37 @classmethod
44947230 38 def sendDOHQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[], useHTTPS=True):
47225117 39 url = cls.getDOHGetURL(baseurl, query, rawQuery)
10535d88 40 conn = cls.openDOHConnection(port, caFile=caFile, timeout=timeout)
811872fb 41 response_headers = BytesIO()
10535d88
RG
42 #conn.setopt(pycurl.VERBOSE, True)
43 conn.setopt(pycurl.URL, url)
44 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (servername, port)])
44947230
RG
45 if useHTTPS:
46 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
47 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
48 if caFile:
49 conn.setopt(pycurl.CAINFO, caFile)
50
9ba32868 51 conn.setopt(pycurl.HTTPHEADER, customHeaders)
ee01507f 52 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
10535d88
RG
53
54 if response:
55 cls._toResponderQueue.put(response, True, timeout)
56
57 receivedQuery = None
58 message = None
ee01507f 59 cls._response_headers = ''
10535d88 60 data = conn.perform_rb()
9676d2a9
RG
61 cls._rcode = conn.getinfo(pycurl.RESPONSE_CODE)
62 if cls._rcode == 200 and not rawResponse:
10535d88 63 message = dns.message.from_wire(data)
9676d2a9
RG
64 elif rawResponse:
65 message = data
10535d88
RG
66
67 if useQueue and not cls._fromResponderQueue.empty():
68 receivedQuery = cls._fromResponderQueue.get(True, timeout)
69
ee01507f 70 cls._response_headers = response_headers.getvalue()
10535d88
RG
71 return (receivedQuery, message)
72
b1e527ad 73 @classmethod
44947230 74 def sendDOHPostQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[], useHTTPS=True):
b1e527ad
RG
75 url = baseurl
76 conn = cls.openDOHConnection(port, caFile=caFile, timeout=timeout)
9676d2a9 77 response_headers = BytesIO()
b1e527ad
RG
78 #conn.setopt(pycurl.VERBOSE, True)
79 conn.setopt(pycurl.URL, url)
80 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (servername, port)])
44947230
RG
81 if useHTTPS:
82 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
83 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
84 if caFile:
85 conn.setopt(pycurl.CAINFO, caFile)
86
9676d2a9
RG
87 conn.setopt(pycurl.HTTPHEADER, customHeaders)
88 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
b1e527ad
RG
89 conn.setopt(pycurl.POST, True)
90 data = query
91 if not rawQuery:
92 data = data.to_wire()
93
94 conn.setopt(pycurl.POSTFIELDS, data)
95
b1e527ad
RG
96 if response:
97 cls._toResponderQueue.put(response, True, timeout)
98
99 receivedQuery = None
100 message = None
9676d2a9 101 cls._response_headers = ''
b1e527ad 102 data = conn.perform_rb()
9676d2a9
RG
103 cls._rcode = conn.getinfo(pycurl.RESPONSE_CODE)
104 if cls._rcode == 200 and not rawResponse:
b1e527ad 105 message = dns.message.from_wire(data)
9676d2a9
RG
106 elif rawResponse:
107 message = data
b1e527ad
RG
108
109 if useQueue and not cls._fromResponderQueue.empty():
110 receivedQuery = cls._fromResponderQueue.get(True, timeout)
111
9676d2a9 112 cls._response_headers = response_headers.getvalue()
b1e527ad
RG
113 return (receivedQuery, message)
114
0026abf9
RG
115 def getHeaderValue(self, name):
116 for header in self._response_headers.decode().splitlines(False):
117 values = header.split(':')
118 key = values[0]
119 if key.lower() == name.lower():
120 return values[1].strip()
121 return None
122
123 def checkHasHeader(self, name, value):
124 got = self.getHeaderValue(name)
125 self.assertEquals(got, value)
126
127 def checkNoHeader(self, name):
128 self.checkHasHeader(name, None)
129
13291274
RG
130 @classmethod
131 def setUpClass(cls):
132
133 # for some reason, @unittest.skipIf() is not applied to derived classes with some versions of Python
134 if 'SKIP_DOH_TESTS' in os.environ:
135 raise unittest.SkipTest('DNS over HTTPS tests are disabled')
136
137 cls.startResponders()
138 cls.startDNSDist()
139 cls.setUpSockets()
140
141 print("Launching tests..")
142
10535d88
RG
143# @classmethod
144# def openDOHConnection(cls, port, caFile, timeout=2.0):
145# sslctx = SSLContext(PROTOCOL_TLSv1_2)
146# sslctx.load_verify_locations(caFile)
147# return HTTP20Connection('127.0.0.1', port=port, secure=True, timeout=timeout, ssl_context=sslctx, force_proto='h2')
148
149# @classmethod
150# def sendDOHQueryOverConnection(cls, conn, baseurl, query, response=None, timeout=2.0):
151# url = cls.getDOHGetURL(baseurl, query)
152
153# if response:
154# cls._toResponderQueue.put(response, True, timeout)
155
156# conn.request('GET', url)
157
158# @classmethod
159# def recvDOHResponseOverConnection(cls, conn, useQueue=False, timeout=2.0):
160# message = None
161# data = conn.get_response()
162# if data:
163# data = data.read()
164# if data:
165# message = dns.message.from_wire(data)
166
167# if useQueue and not cls._fromResponderQueue.empty():
168# receivedQuery = cls._fromResponderQueue.get(True, timeout)
169# return (receivedQuery, message)
170# else:
171# return message
172
173class TestDOH(DNSDistDOHTest):
174
175 _serverKey = 'server.key'
176 _serverCert = 'server.chain'
177 _serverName = 'tls.tests.dnsdist.org'
178 _caCert = 'ca.pem'
179 _dohServerPort = 8443
ee01507f
CR
180 _customResponseHeader1 = 'access-control-allow-origin: *'
181 _customResponseHeader2 = 'user-agent: derp'
10535d88
RG
182 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
183 _config_template = """
184 newServer{address="127.0.0.1:%s"}
ee01507f 185
cf3e149b 186 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}})
28b56482 187 dohFE = getDOHFrontend(0)
e1b72559 188 dohFE:setResponsesMap({newDOHResponseMapEntry('^/coffee$', 418, 'C0FFEE', {['FoO']='bar'})})
10535d88
RG
189
190 addAction("drop.doh.tests.powerdns.com.", DropAction())
191 addAction("refused.doh.tests.powerdns.com.", RCodeAction(DNSRCode.REFUSED))
192 addAction("spoof.doh.tests.powerdns.com.", SpoofAction("1.2.3.4"))
9ba32868
RG
193 addAction(HTTPHeaderRule("X-PowerDNS", "^[a]{5}$"), SpoofAction("2.3.4.5"))
194 addAction(HTTPPathRule("/PowerDNS"), SpoofAction("3.4.5.6"))
9676d2a9
RG
195 addAction(HTTPPathRegexRule("^/PowerDNS-[0-9]"), SpoofAction("6.7.8.9"))
196 addAction("http-status-action.doh.tests.powerdns.com.", HTTPStatusAction(200, "Plaintext answer", "text/plain"))
197 addAction("http-status-action-redirect.doh.tests.powerdns.com.", HTTPStatusAction(307, "https://doh.powerdns.org"))
198
199 function dohHandler(dq)
200 if dq:getHTTPScheme() == 'https' and dq:getHTTPHost() == '%s:%d' and dq:getHTTPPath() == '/' and dq:getHTTPQueryString() == '' then
201 local foundct = false
202 for key,value in pairs(dq:getHTTPHeaders()) do
203 if key == 'content-type' and value == 'application/dns-message' then
204 foundct = true
205 break
206 end
207 end
208 if foundct then
209 dq:setHTTPResponse(200, 'It works!', 'text/plain')
210 dq.dh:setQR(true)
211 return DNSAction.HeaderModify
212 end
213 end
214 return DNSAction.None
215 end
216 addAction("http-lua.doh.tests.powerdns.com.", LuaAction(dohHandler))
10535d88 217 """
9676d2a9 218 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
10535d88
RG
219
220 def testDOHSimple(self):
221 """
222 DOH: Simple query
223 """
224 name = 'simple.doh.tests.powerdns.com.'
225 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
226 query.id = 0
227 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
228 expectedQuery.id = 0
229 response = dns.message.make_response(query)
230 rrset = dns.rrset.from_text(name,
231 3600,
232 dns.rdataclass.IN,
233 dns.rdatatype.A,
234 '127.0.0.1')
235 response.answer.append(rrset)
236
237 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
238 self.assertTrue(receivedQuery)
239 self.assertTrue(receivedResponse)
240 receivedQuery.id = expectedQuery.id
241 self.assertEquals(expectedQuery, receivedQuery)
811872fb
RG
242 self.assertTrue((self._customResponseHeader1) in self._response_headers.decode())
243 self.assertTrue((self._customResponseHeader2) in self._response_headers.decode())
cf3e149b
RG
244 self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode()))
245 self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode()))
0026abf9 246 self.assertTrue(('cache-control: max-age=3600' in self._response_headers.decode()))
10535d88
RG
247 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
248 self.assertEquals(response, receivedResponse)
0026abf9 249 self.checkHasHeader('cache-control', 'max-age=3600')
10535d88 250
b1e527ad
RG
251 def testDOHSimplePOST(self):
252 """
253 DOH: Simple POST query
254 """
255 name = 'simple-post.doh.tests.powerdns.com.'
256 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
257 query.id = 0
258 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
259 expectedQuery.id = 0
260 response = dns.message.make_response(query)
261 rrset = dns.rrset.from_text(name,
262 3600,
263 dns.rdataclass.IN,
264 dns.rdatatype.A,
265 '127.0.0.1')
266 response.answer.append(rrset)
267
268 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
269 self.assertTrue(receivedQuery)
270 self.assertTrue(receivedResponse)
271 receivedQuery.id = expectedQuery.id
272 self.assertEquals(expectedQuery, receivedQuery)
273 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
274 self.assertEquals(response, receivedResponse)
275
10535d88
RG
276 def testDOHExistingEDNS(self):
277 """
278 DOH: Existing EDNS
279 """
280 name = 'existing-edns.doh.tests.powerdns.com.'
281 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
282 query.id = 0
283 response = dns.message.make_response(query)
284 rrset = dns.rrset.from_text(name,
285 3600,
286 dns.rdataclass.IN,
287 dns.rdatatype.A,
288 '127.0.0.1')
289 response.answer.append(rrset)
290
291 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
292 self.assertTrue(receivedQuery)
293 self.assertTrue(receivedResponse)
294 receivedQuery.id = query.id
295 self.assertEquals(query, receivedQuery)
296 self.assertEquals(response, receivedResponse)
297 self.checkQueryEDNSWithoutECS(query, receivedQuery)
298 self.checkResponseEDNSWithoutECS(response, receivedResponse)
299
300 def testDOHExistingECS(self):
301 """
302 DOH: Existing EDNS Client Subnet
303 """
304 name = 'existing-ecs.doh.tests.powerdns.com.'
305 ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4')
306 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.1', 24)
307 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True)
308 query.id = 0
309 response = dns.message.make_response(query)
310 response.use_edns(edns=True, payload=4096, options=[rewrittenEcso])
311 rrset = dns.rrset.from_text(name,
312 3600,
313 dns.rdataclass.IN,
314 dns.rdatatype.A,
315 '127.0.0.1')
316 response.answer.append(rrset)
317
318 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
319 self.assertTrue(receivedQuery)
320 self.assertTrue(receivedResponse)
321 receivedQuery.id = query.id
322 self.assertEquals(query, receivedQuery)
323 self.assertEquals(response, receivedResponse)
324 self.checkQueryEDNSWithECS(query, receivedQuery)
325 self.checkResponseEDNSWithECS(response, receivedResponse)
326
327 def testDropped(self):
328 """
329 DOH: Dropped query
330 """
331 name = 'drop.doh.tests.powerdns.com.'
332 query = dns.message.make_query(name, 'A', 'IN')
333 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
10535d88
RG
334 self.assertEquals(receivedResponse, None)
335
336 def testRefused(self):
337 """
338 DOH: Refused
339 """
340 name = 'refused.doh.tests.powerdns.com.'
341 query = dns.message.make_query(name, 'A', 'IN')
342 query.id = 0
7af22479 343 query.flags &= ~dns.flags.RD
10535d88
RG
344 expectedResponse = dns.message.make_response(query)
345 expectedResponse.set_rcode(dns.rcode.REFUSED)
346
347 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
348 self.assertEquals(receivedResponse, expectedResponse)
349
350 def testSpoof(self):
351 """
352 DOH: Spoofed
353 """
354 name = 'spoof.doh.tests.powerdns.com.'
355 query = dns.message.make_query(name, 'A', 'IN')
356 query.id = 0
357 query.flags &= ~dns.flags.RD
358 expectedResponse = dns.message.make_response(query)
359 rrset = dns.rrset.from_text(name,
360 3600,
361 dns.rdataclass.IN,
362 dns.rdatatype.A,
363 '1.2.3.4')
364 expectedResponse.answer.append(rrset)
365
366 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
367 self.assertEquals(receivedResponse, expectedResponse)
368
47225117
RG
369 def testDOHInvalid(self):
370 """
371 DOH: Invalid query
372 """
373 name = 'invalid.doh.tests.powerdns.com.'
374 invalidQuery = dns.message.make_query(name, 'A', 'IN', use_edns=False)
375 invalidQuery.id = 0
376 # first an invalid query
377 invalidQuery = invalidQuery.to_wire()
378 invalidQuery = invalidQuery[:-5]
379 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=invalidQuery, response=None, useQueue=False, rawQuery=True)
380 self.assertEquals(receivedResponse, None)
381
382 # and now a valid one
383 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
384 query.id = 0
385 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
386 expectedQuery.id = 0
387 response = dns.message.make_response(query)
388 rrset = dns.rrset.from_text(name,
389 3600,
390 dns.rdataclass.IN,
391 dns.rdatatype.A,
392 '127.0.0.1')
393 response.answer.append(rrset)
394 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
395 self.assertTrue(receivedQuery)
396 self.assertTrue(receivedResponse)
397 receivedQuery.id = expectedQuery.id
398 self.assertEquals(expectedQuery, receivedQuery)
399 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
400 self.assertEquals(response, receivedResponse)
10535d88 401
2cb8efb1
RG
402 def testDOHWithoutQuery(self):
403 """
404 DOH: Empty GET query
405 """
406 name = 'empty-get.doh.tests.powerdns.com.'
407 url = self._dohBaseURL
408 conn = self.openDOHConnection(self._dohServerPort, self._caCert, timeout=2.0)
409 conn.setopt(pycurl.URL, url)
410 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
411 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
412 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
413 conn.setopt(pycurl.CAINFO, self._caCert)
414 data = conn.perform_rb()
415 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
416 self.assertEquals(rcode, 400)
417
b1e527ad
RG
418 def testDOHEmptyPOST(self):
419 """
420 DOH: Empty POST query
421 """
422 name = 'empty-post.doh.tests.powerdns.com.'
423
424 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query="", rawQuery=True, response=None, caFile=self._caCert)
425 self.assertEquals(receivedResponse, None)
426
427 # and now a valid one
428 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
429 query.id = 0
430 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
431 expectedQuery.id = 0
432 response = dns.message.make_response(query)
433 rrset = dns.rrset.from_text(name,
434 3600,
435 dns.rdataclass.IN,
436 dns.rdatatype.A,
437 '127.0.0.1')
438 response.answer.append(rrset)
439 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
440 self.assertTrue(receivedQuery)
441 self.assertTrue(receivedResponse)
442 receivedQuery.id = expectedQuery.id
443 self.assertEquals(expectedQuery, receivedQuery)
444 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
445 self.assertEquals(response, receivedResponse)
446
9ba32868
RG
447 def testHeaderRule(self):
448 """
449 DOH: HeaderRule
450 """
451 name = 'header-rule.doh.tests.powerdns.com.'
452 query = dns.message.make_query(name, 'A', 'IN')
453 query.id = 0
454 query.flags &= ~dns.flags.RD
455 expectedResponse = dns.message.make_response(query)
456 rrset = dns.rrset.from_text(name,
457 3600,
458 dns.rdataclass.IN,
459 dns.rdatatype.A,
460 '2.3.4.5')
461 expectedResponse.answer.append(rrset)
462
463 # this header should match
464 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False, customHeaders=['x-powerdnS: aaaaa'])
465 self.assertEquals(receivedResponse, expectedResponse)
466
467 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
468 expectedQuery.flags &= ~dns.flags.RD
469 expectedQuery.id = 0
470 response = dns.message.make_response(query)
471 rrset = dns.rrset.from_text(name,
472 3600,
473 dns.rdataclass.IN,
474 dns.rdatatype.A,
475 '127.0.0.1')
476 response.answer.append(rrset)
477
478 # this content of the header should NOT match
479 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert, customHeaders=['x-powerdnS: bbbbb'])
480 self.assertTrue(receivedQuery)
481 self.assertTrue(receivedResponse)
482 receivedQuery.id = expectedQuery.id
483 self.assertEquals(expectedQuery, receivedQuery)
484 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
485 self.assertEquals(response, receivedResponse)
486
487 def testHTTPPath(self):
488 """
489 DOH: HTTPPath
490 """
491 name = 'http-path.doh.tests.powerdns.com.'
492 query = dns.message.make_query(name, 'A', 'IN')
493 query.id = 0
494 query.flags &= ~dns.flags.RD
495 expectedResponse = dns.message.make_response(query)
496 rrset = dns.rrset.from_text(name,
497 3600,
498 dns.rdataclass.IN,
499 dns.rdatatype.A,
500 '3.4.5.6')
501 expectedResponse.answer.append(rrset)
502
503 # this path should match
504 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS', caFile=self._caCert, query=query, response=None, useQueue=False)
505 self.assertEquals(receivedResponse, expectedResponse)
506
507 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
508 expectedQuery.id = 0
509 expectedQuery.flags &= ~dns.flags.RD
510 response = dns.message.make_response(query)
511 rrset = dns.rrset.from_text(name,
512 3600,
513 dns.rdataclass.IN,
514 dns.rdatatype.A,
515 '127.0.0.1')
516 response.answer.append(rrset)
517
518 # this path should NOT match
519 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert)
520 self.assertTrue(receivedQuery)
521 self.assertTrue(receivedResponse)
522 receivedQuery.id = expectedQuery.id
523 self.assertEquals(expectedQuery, receivedQuery)
524 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
525 self.assertEquals(response, receivedResponse)
526
9676d2a9
RG
527 def testHTTPPathRegex(self):
528 """
529 DOH: HTTPPathRegex
530 """
531 name = 'http-path-regex.doh.tests.powerdns.com.'
532 query = dns.message.make_query(name, 'A', 'IN')
533 query.id = 0
534 query.flags &= ~dns.flags.RD
535 expectedResponse = dns.message.make_response(query)
536 rrset = dns.rrset.from_text(name,
537 3600,
538 dns.rdataclass.IN,
539 dns.rdatatype.A,
540 '6.7.8.9')
541 expectedResponse.answer.append(rrset)
542
543 # this path should match
544 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS-999', caFile=self._caCert, query=query, response=None, useQueue=False)
545 self.assertEquals(receivedResponse, expectedResponse)
546
547 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
548 expectedQuery.id = 0
549 expectedQuery.flags &= ~dns.flags.RD
550 response = dns.message.make_response(query)
551 rrset = dns.rrset.from_text(name,
552 3600,
553 dns.rdataclass.IN,
554 dns.rdatatype.A,
555 '127.0.0.1')
556 response.answer.append(rrset)
557
558 # this path should NOT match
559 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert)
560 self.assertTrue(receivedQuery)
561 self.assertTrue(receivedResponse)
562 receivedQuery.id = expectedQuery.id
563 self.assertEquals(expectedQuery, receivedQuery)
564 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
565 self.assertEquals(response, receivedResponse)
566
567 def testHTTPStatusAction200(self):
568 """
569 DOH: HTTPStatusAction 200 OK
570 """
571 name = 'http-status-action.doh.tests.powerdns.com.'
572 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
573 query.id = 0
574
575 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
576 self.assertTrue(receivedResponse)
577 self.assertEquals(receivedResponse, b'Plaintext answer')
578 self.assertEquals(self._rcode, 200)
579 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
580
581 def testHTTPStatusAction307(self):
582 """
583 DOH: HTTPStatusAction 307
584 """
585 name = 'http-status-action-redirect.doh.tests.powerdns.com.'
586 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
587 query.id = 0
588
589 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
590 self.assertTrue(receivedResponse)
591 self.assertEquals(self._rcode, 307)
592 self.assertTrue('location: https://doh.powerdns.org' in self._response_headers.decode())
593
594 def testHTTPLuaResponse(self):
595 """
596 DOH: Lua HTTP Response
597 """
598 name = 'http-lua.doh.tests.powerdns.com.'
599 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
600 query.id = 0
601
602 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
603 self.assertTrue(receivedResponse)
604 self.assertEquals(receivedResponse, b'It works!')
605 self.assertEquals(self._rcode, 200)
606 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
607
28b56482
RG
608 def testHTTPEarlyResponse(self):
609 """
610 DOH: HTTP Early Response
611 """
ded6907c 612 response_headers = BytesIO()
28b56482
RG
613 url = self._dohBaseURL + 'coffee'
614 conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
615 conn.setopt(pycurl.URL, url)
616 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
617 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
618 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
619 conn.setopt(pycurl.CAINFO, self._caCert)
ded6907c 620 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
28b56482
RG
621 data = conn.perform_rb()
622 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
9b2ef603 623 headers = response_headers.getvalue().decode()
28b56482
RG
624
625 self.assertEquals(rcode, 418)
626 self.assertEquals(data, b'C0FFEE')
ded6907c
RG
627 self.assertIn('foo: bar', headers)
628 self.assertNotIn(self._customResponseHeader2, headers)
28b56482 629
ded6907c 630 response_headers = BytesIO()
28b56482
RG
631 conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
632 conn.setopt(pycurl.URL, url)
633 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
634 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
635 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
636 conn.setopt(pycurl.CAINFO, self._caCert)
ded6907c 637 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
28b56482
RG
638 conn.setopt(pycurl.POST, True)
639 data = ''
640 conn.setopt(pycurl.POSTFIELDS, data)
641
642 data = conn.perform_rb()
643 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
9b2ef603 644 headers = response_headers.getvalue().decode()
28b56482
RG
645 self.assertEquals(rcode, 418)
646 self.assertEquals(data, b'C0FFEE')
ded6907c
RG
647 self.assertIn('foo: bar', headers)
648 self.assertNotIn(self._customResponseHeader2, headers)
28b56482 649
10535d88
RG
650class TestDOHAddingECS(DNSDistDOHTest):
651
652 _serverKey = 'server.key'
653 _serverCert = 'server.chain'
654 _serverName = 'tls.tests.dnsdist.org'
655 _caCert = 'ca.pem'
656 _dohServerPort = 8443
10535d88
RG
657 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
658 _config_template = """
659 newServer{address="127.0.0.1:%s", useClientSubnet=true}
660 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
661 setECSOverride(true)
662 """
663 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
664
665 def testDOHSimple(self):
666 """
667 DOH with ECS: Simple query
668 """
669 name = 'simple.doh-ecs.tests.powerdns.com.'
670 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
671 query.id = 0
672 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
673 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[rewrittenEcso])
674 response = dns.message.make_response(query)
675 rrset = dns.rrset.from_text(name,
676 3600,
677 dns.rdataclass.IN,
678 dns.rdatatype.A,
679 '127.0.0.1')
680 response.answer.append(rrset)
681
682 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
683 self.assertTrue(receivedQuery)
684 self.assertTrue(receivedResponse)
685 expectedQuery.id = receivedQuery.id
686 self.assertEquals(expectedQuery, receivedQuery)
687 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
688 self.assertEquals(response, receivedResponse)
689 self.checkResponseNoEDNS(response, receivedResponse)
690
691 def testDOHExistingEDNS(self):
692 """
693 DOH with ECS: Existing EDNS
694 """
695 name = 'existing-edns.doh-ecs.tests.powerdns.com.'
696 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
697 query.id = 0
698 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
699 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192, options=[rewrittenEcso])
700 response = dns.message.make_response(query)
701 rrset = dns.rrset.from_text(name,
702 3600,
703 dns.rdataclass.IN,
704 dns.rdatatype.A,
705 '127.0.0.1')
706 response.answer.append(rrset)
707
708 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
709 self.assertTrue(receivedQuery)
710 self.assertTrue(receivedResponse)
711 receivedQuery.id = expectedQuery.id
712 self.assertEquals(expectedQuery, receivedQuery)
713 self.assertEquals(response, receivedResponse)
714 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
715 self.checkResponseEDNSWithoutECS(response, receivedResponse)
716
717 def testDOHExistingECS(self):
718 """
719 DOH with ECS: Existing EDNS Client Subnet
720 """
721 name = 'existing-ecs.doh-ecs.tests.powerdns.com.'
722 ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4')
723 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
724 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True)
725 query.id = 0
726 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[rewrittenEcso])
727 response = dns.message.make_response(query)
728 response.use_edns(edns=True, payload=4096, options=[rewrittenEcso])
729 rrset = dns.rrset.from_text(name,
730 3600,
731 dns.rdataclass.IN,
732 dns.rdatatype.A,
733 '127.0.0.1')
734 response.answer.append(rrset)
735
736 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
737 self.assertTrue(receivedQuery)
738 self.assertTrue(receivedResponse)
739 receivedQuery.id = expectedQuery.id
740 self.assertEquals(expectedQuery, receivedQuery)
741 self.assertEquals(response, receivedResponse)
742 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
743 self.checkResponseEDNSWithECS(response, receivedResponse)
44947230
RG
744
745class TestDOHOverHTTP(DNSDistDOHTest):
746
747 _dohServerPort = 8480
748 _serverName = 'tls.tests.dnsdist.org'
749 _dohBaseURL = ("http://%s:%d/" % (_serverName, _dohServerPort))
750 _config_template = """
751 newServer{address="127.0.0.1:%s"}
752 addDOHLocal("127.0.0.1:%s")
753 """
754 _config_params = ['_testServerPort', '_dohServerPort']
11c22318
RG
755 _checkConfigExpectedOutput = b"""No certificate provided for DoH endpoint 127.0.0.1:8480, running in DNS over HTTP mode instead of DNS over HTTPS
756Configuration 'configs/dnsdist_TestDOHOverHTTP.conf' OK!
757"""
44947230
RG
758
759 def testDOHSimple(self):
760 """
761 DOH over HTTP: Simple query
762 """
763 name = 'simple.doh-over-http.tests.powerdns.com.'
764 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
765 query.id = 0
766 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
767 response = dns.message.make_response(query)
768 rrset = dns.rrset.from_text(name,
769 3600,
770 dns.rdataclass.IN,
771 dns.rdatatype.A,
772 '127.0.0.1')
773 response.answer.append(rrset)
774
775 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False)
776 self.assertTrue(receivedQuery)
777 self.assertTrue(receivedResponse)
778 expectedQuery.id = receivedQuery.id
779 self.assertEquals(expectedQuery, receivedQuery)
780 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
781 self.assertEquals(response, receivedResponse)
782 self.checkResponseNoEDNS(response, receivedResponse)
783
784 def testDOHSimplePOST(self):
785 """
786 DOH over HTTP: Simple POST query
787 """
788 name = 'simple-post.doh-over-http.tests.powerdns.com.'
789 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
790 query.id = 0
791 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
792 expectedQuery.id = 0
793 response = dns.message.make_response(query)
794 rrset = dns.rrset.from_text(name,
795 3600,
796 dns.rdataclass.IN,
797 dns.rdatatype.A,
798 '127.0.0.1')
799 response.answer.append(rrset)
800
801 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False)
802 self.assertTrue(receivedQuery)
803 self.assertTrue(receivedResponse)
804 receivedQuery.id = expectedQuery.id
805 self.assertEquals(expectedQuery, receivedQuery)
806 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
807 self.assertEquals(response, receivedResponse)
808 self.checkResponseNoEDNS(response, receivedResponse)
d27309a9
RG
809
810class TestDOHWithCache(DNSDistDOHTest):
811
812 _serverKey = 'server.key'
813 _serverCert = 'server.chain'
814 _serverName = 'tls.tests.dnsdist.org'
815 _caCert = 'ca.pem'
816 _dohServerPort = 8443
817 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
818 _config_template = """
819 newServer{address="127.0.0.1:%s"}
820
821 addDOHLocal("127.0.0.1:%s", "%s", "%s")
822
823 pc = newPacketCache(100, {maxTTL=86400, minTTL=1})
824 getPool(""):setCache(pc)
825 """
826 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
827
828 def testDOHCacheLargeAnswer(self):
829 """
830 DOH with cache: Check that we can cache (and retrieve) large answers
831 """
832 numberOfQueries = 10
833 name = 'large.doh-with-cache.tests.powerdns.com.'
834 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
835 query.id = 0
836 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
837 expectedQuery.id = 0
838 response = dns.message.make_response(query)
839 # we prepare a large answer
840 content = ""
841 for i in range(44):
842 if len(content) > 0:
843 content = content + ', '
844 content = content + (str(i)*50)
845 # pad up to 4096
846 content = content + 'A'*40
847
848 rrset = dns.rrset.from_text(name,
849 3600,
850 dns.rdataclass.IN,
851 dns.rdatatype.TXT,
852 content)
853 response.answer.append(rrset)
854 self.assertEquals(len(response.to_wire()), 4096)
855
856 # first query to fill the cache
857 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
858 self.assertTrue(receivedQuery)
859 self.assertTrue(receivedResponse)
860 receivedQuery.id = expectedQuery.id
861 self.assertEquals(expectedQuery, receivedQuery)
862 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
863 self.assertEquals(response, receivedResponse)
0026abf9 864 self.checkHasHeader('cache-control', 'max-age=3600')
d27309a9
RG
865
866 for _ in range(numberOfQueries):
867 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
868 self.assertEquals(receivedResponse, response)
0026abf9
RG
869 self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl))
870
871 time.sleep(1)
872
873 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
874 self.assertEquals(receivedResponse, response)
875 self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl))
876
877class TestDOHWithoutCacheControl(DNSDistDOHTest):
878
879 _serverKey = 'server.key'
880 _serverCert = 'server.chain'
881 _serverName = 'tls.tests.dnsdist.org'
882 _caCert = 'ca.pem'
883 _dohServerPort = 8443
884 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
885 _config_template = """
886 newServer{address="127.0.0.1:%s"}
887
888 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false})
889 """
890 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
891
892 def testDOHSimple(self):
893 """
894 DOH without cache-control
895 """
896 name = 'simple.doh.tests.powerdns.com.'
897 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
898 query.id = 0
899 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
900 expectedQuery.id = 0
901 response = dns.message.make_response(query)
902 rrset = dns.rrset.from_text(name,
903 3600,
904 dns.rdataclass.IN,
905 dns.rdatatype.A,
906 '127.0.0.1')
907 response.answer.append(rrset)
908
909 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
910 self.assertTrue(receivedQuery)
911 self.assertTrue(receivedResponse)
912 receivedQuery.id = expectedQuery.id
913 self.assertEquals(expectedQuery, receivedQuery)
914 self.checkNoHeader('cache-control')
915 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
916 self.assertEquals(response, receivedResponse)