]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Implement Cache-Control headers in DoH 8762/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 29 Jan 2020 13:57:06 +0000 (14:57 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 29 Jan 2020 13:57:06 +0000 (14:57 +0100)
pdns/dnsdist-lua.cc
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/doh.cc
pdns/doh.hh
regression-tests.dnsdist/test_DOH.py

index 79557c6f96caa5098f796d8779aad5953e789488..a2979bac0b020f64cceed9f2c3e7434083a85009 100644 (file)
@@ -1858,6 +1858,10 @@ void setupLuaConfig(bool client, bool configCheck)
         }
       }
 
+      if (vars->count("sendCacheControlHeaders")) {
+        frontend->d_sendCacheControlHeaders = boost::get<bool>((*vars)["sendCacheControlHeaders"]);
+      }
+
       parseTLSConfig(frontend->d_tlsConfig, "addDOHLocal", vars);
     }
     g_dohlocals.push_back(frontend);
index 9aa47383f8fecc464b9e16ffa6098a35cba6e4cd..1ea9c843fb50784a14c8188a5e3298b658958394 100644 (file)
@@ -107,6 +107,9 @@ Listen Sockets
 
   .. versionadded:: 1.4.0
 
+  .. versionchanged:: 1.5.0
+    ``sendCacheControlHeaders`` option added.
+
   Listen on the specified address and TCP port for incoming DNS over HTTPS connections, presenting the specified X.509 certificate.
   If no certificate (or key) files are specified, listen for incoming DNS over HTTP connections instead.
 
@@ -137,6 +140,7 @@ Listen Sockets
   * ``numberOfStoredSessions``: int - The maximum number of sessions kept in memory at the same time. Default is 20480. Setting this value to 0 disables stored session entirely.
   * ``preferServerCiphers``: bool - Whether to prefer the order of ciphers set by the server instead of the one set by the client. Default is true, meaning that the order of the server is used.
   * ``keyLogFile``: str - Write the TLS keys in the specified file so that an external program can decrypt TLS exchanges, in the format described in https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format.
+  * ``sendCacheControlHeaders``: bool - Whether to parse the response to find the lowest TTL and set a HTTP Cache-Control header accordingly. Default is true.
 
 .. function:: addTLSLocal(address, certFile(s), keyFile(s) [, options])
 
index 9b29e650b810bc5179e35964bbfa475ea1cdc483..6f5d1b8fe6eaea9959326aed125cfe1659914475 100644 (file)
@@ -307,6 +307,16 @@ static void handleResponse(DOHFrontend& df, st_h2o_req_t* req, uint16_t statusCo
       }
     }
 
+    if (df.d_sendCacheControlHeaders && !response.empty()) {
+      uint32_t minTTL = getDNSPacketMinTTL(response.data(), response.size());
+      if (minTTL != std::numeric_limits<uint32_t>::max()) {
+        std::string cacheControlValue = "max-age=" + std::to_string(minTTL);
+        /* we need to duplicate the header content because h2o keeps a pointer and we will be deleted before the response has been sent */
+        h2o_iovec_t ccv = h2o_strdup(&req->pool, cacheControlValue.c_str(), cacheControlValue.size());
+        h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CACHE_CONTROL, nullptr, ccv.base, ccv.len);
+      }
+    }
+
     req->res.content_length = response.size();
     h2o_send_inline(req, response.c_str(), response.size());
   }
index ba0499f7e048ef08b12b47ef33a1db04481c31d2..dbcfbb31ab55a60bc8f92dada7deea11f471dc34 100644 (file)
@@ -76,6 +76,7 @@ struct DOHFrontend
 
   HTTPVersionStats d_http1Stats;
   HTTPVersionStats d_http2Stats;
+  bool d_sendCacheControlHeaders{true};
 
   time_t getTicketsKeyRotationDelay() const
   {
index bb27fde2861e192435a9b3eecaf2de0f24278c0b..330382c91727c597939c851d7931087ff7226b21 100644 (file)
@@ -2,6 +2,8 @@
 import base64
 import dns
 import os
+import re
+import time
 import unittest
 import clientsubnetoption
 from dnsdisttests import DNSDistTest
@@ -110,6 +112,21 @@ class DNSDistDOHTest(DNSDistTest):
         cls._response_headers = response_headers.getvalue()
         return (receivedQuery, message)
 
+    def getHeaderValue(self, name):
+        for header in self._response_headers.decode().splitlines(False):
+            values = header.split(':')
+            key = values[0]
+            if key.lower() == name.lower():
+                return values[1].strip()
+        return None
+
+    def checkHasHeader(self, name, value):
+        got = self.getHeaderValue(name)
+        self.assertEquals(got, value)
+
+    def checkNoHeader(self, name):
+        self.checkHasHeader(name, None)
+
     @classmethod
     def setUpClass(cls):
 
@@ -226,8 +243,10 @@ class TestDOH(DNSDistDOHTest):
         self.assertTrue((self._customResponseHeader2) in self._response_headers.decode())
         self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode()))
         self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode()))
+        self.assertTrue(('cache-control: max-age=3600' in self._response_headers.decode()))
         self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
         self.assertEquals(response, receivedResponse)
+        self.checkHasHeader('cache-control', 'max-age=3600')
 
     def testDOHSimplePOST(self):
         """
@@ -842,7 +861,56 @@ class TestDOHWithCache(DNSDistDOHTest):
         self.assertEquals(expectedQuery, receivedQuery)
         self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
         self.assertEquals(response, receivedResponse)
+        self.checkHasHeader('cache-control', 'max-age=3600')
 
         for _ in range(numberOfQueries):
             (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
             self.assertEquals(receivedResponse, response)
+            self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl))
+
+        time.sleep(1)
+
+        (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
+        self.assertEquals(receivedResponse, response)
+        self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl))
+
+class TestDOHWithoutCacheControl(DNSDistDOHTest):
+
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _caCert = 'ca.pem'
+    _dohServerPort = 8443
+    _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+
+    addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false})
+    """
+    _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
+
+    def testDOHSimple(self):
+        """
+        DOH without cache-control
+        """
+        name = 'simple.doh.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
+        query.id = 0
+        expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
+        expectedQuery.id = 0
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '127.0.0.1')
+        response.answer.append(rrset)
+
+        (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = expectedQuery.id
+        self.assertEquals(expectedQuery, receivedQuery)
+        self.checkNoHeader('cache-control')
+        self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
+        self.assertEquals(response, receivedResponse)