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