]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Merge pull request #8760 from rgacogne/ddist-doh-exact-match
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 12 Feb 2020 09:09:24 +0000 (10:09 +0100)
committerGitHub <noreply@github.com>
Wed, 12 Feb 2020 09:09:24 +0000 (10:09 +0100)
dnsdist: Don't accept sub-paths of configured DoH URLs

1  2 
pdns/dnsdistdist/doh.cc
regression-tests.dnsdist/test_DOH.py

diff --combined pdns/dnsdistdist/doh.cc
index 6f5d1b8fe6eaea9959326aed125cfe1659914475,f45ae902c985e098dbc7cf79f1e350286a48eee3..fcddb9d92964c3dfabf95b9502c06099041de9ae
@@@ -199,6 -199,7 +199,7 @@@ struct DOHServerConfi
    }
  
    LocalHolders holders;
+   std::unordered_set<std::string> paths;
    h2o_globalconf_t h2o_config;
    h2o_context_t h2o_ctx;
    DOHAcceptContext* accept_ctx{nullptr};
@@@ -307,16 -308,6 +308,16 @@@ static void handleResponse(DOHFrontend
        }
      }
  
 +    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());
    }
@@@ -697,6 -688,12 +698,12 @@@ tr
  
    string path(req->path.base, req->path.len);
  
+   string pathOnly(req->path_normalized.base, req->path_normalized.len);
+   if (dsc->paths.count(pathOnly) == 0) {
+     h2o_send_error_404(req, "Not Found", "there is no endpoint configured for this path", 0);
+     return 0;
+   }
    for (const auto& entry : dsc->df->d_responsesMap) {
      if (entry->matches(path)) {
        const auto& customHeaders = entry->getHeaders();
@@@ -1205,6 -1202,7 +1212,7 @@@ tr
  
    for(const auto& url : df->d_urls) {
      register_handler(hostconf, url.c_str(), doh_handler);
+     dsc->paths.insert(url);
    }
  
    h2o_context_init(&dsc->h2o_ctx, h2o_evloop_create(), &dsc->h2o_config);
index 330382c91727c597939c851d7931087ff7226b21,80cc0a294c519e23d40f950d39fafe3ecea694ef..d934421a02495037fffcc87584f70659e546079b
@@@ -2,8 -2,6 +2,8 @@@
  import base64
  import dns
  import os
 +import re
 +import time
  import unittest
  import clientsubnetoption
  from dnsdisttests import DNSDistTest
@@@ -112,21 -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):
  
@@@ -183,7 -166,7 +183,7 @@@ class TestDOH(DNSDistDOHTest)
      _config_template = """
      newServer{address="127.0.0.1:%s"}
  
-     addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}})
+     addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/", "/coffee", "/PowerDNS", "/PowerDNS2", "/PowerDNS-999" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}})
      dohFE = getDOHFrontend(0)
      dohFE:setResponsesMap({newDOHResponseMapEntry('^/coffee$', 418, 'C0FFEE', {['FoO']='bar'})})
  
          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):
          """
          self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
          self.assertEquals(response, receivedResponse)
  
+         # this path is not in the URLs map and should lead to a 404
+         (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS/something", query, caFile=self._caCert, useQueue=False, rawResponse=True)
+         self.assertTrue(receivedResponse)
+         self.assertEquals(receivedResponse, b'there is no endpoint configured for this path')
+         self.assertEquals(self._rcode, 404)
      def testHTTPPathRegex(self):
          """
          DOH: HTTPPathRegex
@@@ -861,56 -848,7 +867,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)