]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
doh: checks around POST HTTP method
authorPetr Špaček <petr.spacek@nic.cz>
Mon, 1 Apr 2019 16:01:08 +0000 (18:01 +0200)
committerPetr Špaček <petr.spacek@nic.cz>
Thu, 11 Apr 2019 07:12:47 +0000 (09:12 +0200)
modules/http/http_doh.lua
modules/http/http_doh.test.lua [new file with mode: 0644]
modules/http/meson.build

index 9c7b71b0544f63d7079ce5e632dc2833dbdd8f33..ae1367dc55a3ce8e4df45df4cea5caa7076ef9ba 100644 (file)
@@ -35,8 +35,26 @@ end
 
 -- Trace execution of DNS queries
 local function serve_doh(h, stream)
-       local path = h:get(':path')
-       local input = stream:get_body_as_string(10)  -- FIXME: timeout
+       local method = h:get(':method')
+       if method ~= 'POST' then
+               return 405, 'only HTTP post is supported'
+       end
+       local content_type = h:get('content-type') or 'application/dns-message'
+       if content_type ~= 'application/dns-message' then
+               return 415, 'only Content-Type: application/dns-message is supported'
+       end
+--     RFC 8484 section-4.1 allows us to ignore Accept header
+--     local accept = h:get('accept') or 'application/dns-message'
+--     if accept ~= 'application/dns-message' then
+--             return 406, 'only Accept: application/dns-message is supported'
+--     end
+
+       local input = stream:get_body_chars(65536, 10)  -- FIXME: timeout
+       if #input < 12 then
+               return 400, 'bad request: input too short'
+       elseif #input > 65535 then
+               return 413, 'bad request: input too long'
+       end
        -- Output buffer
        local output = ''
 
@@ -59,27 +77,22 @@ local function serve_doh(h, stream)
                done = true
        end
 
-       -- Resolve query
+       -- convert query to knot_pkt_t
        local wire = ffi.cast("void *", input)
        local pkt = ffi.C.knot_pkt_new(wire, #input, nil);
        if not pkt then
-               output = 'shit happened in knot_pkt_new'
-       else
-               output = 'knot_pkt_new ok'
+               return 500, 'internal server error'
        end
 
        local result = ffi.C.knot_pkt_parse(pkt, 0)
-       if result > 0 then
-               output = output .. '\nshit in knot_pkt_parse'
-       else
-               output = output .. '\nknot_pkt_parse ok'
+       if result ~= 0 then
+               return 400, 'unparseable DNS message'
        end
        print(pkt)
-       print(output)
-       worker.resolve_pkt(pkt, finish_cb)
-       -- worker.resolve("www.nic.cz", 666, KNOT_CLASS_IN, NULL, finish_cb)
 
-       -- Wait for asynchronous query and free callbacks
+       -- resolve query
+       worker.resolve_pkt(pkt, finish_cb)
+       -- Wait for asynchronous query and free callbacks -- FIXME
        if not done then
                waiting = true
                cond:wait()
diff --git a/modules/http/http_doh.test.lua b/modules/http/http_doh.test.lua
new file mode 100644 (file)
index 0000000..f369124
--- /dev/null
@@ -0,0 +1,123 @@
+local ffi = require('ffi')
+
+function parse_pkt(input)
+       local wire = ffi.cast("void *", input)
+       local pkt = ffi.C.knot_pkt_new(wire, #input, nil);
+       assert(output, 'failed to create new packet')
+
+       local result = ffi.C.knot_pkt_parse(pkt, 0)
+       ok(result > 0, 'knot_pkt_parse works on received answer')
+       print(pkt)
+       return pkt
+end
+
+-- check prerequisites
+local has_http = pcall(require, 'kres_modules.http') and pcall(require, 'http.request')
+if not has_http then
+       pass('skipping http module test because its not installed')
+       done()
+else
+       local request = require('http.request')
+       local endpoints = require('kres_modules.http').endpoints
+
+       -- setup resolver
+       modules = {
+               http = {
+                       port = 0, -- Select random port
+                       cert = false,
+                       endpoints = endpoints,
+               }
+       }
+
+       local server = http.servers[1]
+       ok(server ~= nil, 'creates server instance')
+       local _, host, port = server:localname()
+       ok(host and port, 'binds to an interface')
+       local uri_templ = string.format('http://%s:%d/doh', host, port)
+       local req_templ = assert(request.new_from_uri(uri_templ))
+       req_templ.headers:upsert('content-type', 'application/dns-message')
+
+       -- helper for returning useful values to test on
+       local function eval_req(req)
+               local headers, stream = req:go()
+               same(tonumber(headers:get(':status')), 200, 'status 200')
+               same(headers:get('content-type'), 'application/dns-message')
+               local body = assert(stream:get_body_as_string())
+               -- parse packet!
+               local pkt = parse_pkt(body)
+               return pkt
+       end
+
+       local function check_err(req, exp_status, desc)
+               local headers = req:go(5)  -- randomly chosen timeout
+               local got_status = headers:get(':status')
+               same(got_status, exp_status, desc)
+               print(got_status)  -- TODO
+       end
+
+       -- test whether http interface responds and binds
+       local function test_doh_post()
+               local code, body, mime
+
+               -- simple static page
+               local req = req_templ:clone()
+               local headers, stream = req:go()
+               code, body, mime = eval_req(req)
+       end
+
+       local function test_unsupp_method()
+               local req = assert(req_templ:clone())
+               req.headers:upsert(':method', 'PUT')
+               check_err(req, '405', 'unsupported method finishes with 405')
+       end
+
+       local function test_post_short_input()
+               local req = assert(req_templ:clone())
+               req.headers:upsert(':method', 'POST')
+               req:set_body(string.rep('0', 11))  -- 11 bytes < DNS msg header
+               check_err(req, '400', 'too short POST finishes with 400')
+       end
+
+       local function test_post_long_input()
+               local req = assert(req_templ:clone())
+               req.headers:upsert(':method', 'POST')
+               req:set_body(string.rep('s', 65536))  -- > DNS msg over UDP
+               check_err(req, '413', 'too long POST finishes with 413')
+       end
+
+       local function test_post_unparseable_input()
+               local req = assert(req_templ:clone())
+               req.headers:upsert(':method', 'POST')
+               req:set_body(string.rep('\0', 65535))  -- garbage
+               check_err(req, '400', 'unparseable DNS message finishes with 400')
+       end
+
+       local function test_post_unsupp_type()
+               local req = assert(req_templ:clone())
+               req.headers:upsert(':method', 'POST')
+               req.headers:upsert('content-type', 'application/dns+json')
+               req:set_body(string.rep('\0', 12))  -- valid message
+               check_err(req, '415', 'unsupported request content type finishes with 415')
+       end
+
+--     not implemented
+--     local function test_post_unsupp_accept()
+--             local req = assert(req_templ:clone())
+--             req.headers:upsert(':method', 'POST')
+--             req.headers:upsert('accept', 'application/dns+json')
+--             req:set_body(string.rep('\0', 12))  -- valid message
+--             check_err(req, '406', 'unsupported Accept type finishes with 406')
+--     end
+
+       -- plan tests
+       local tests = {
+               test_unsupp_method,
+               -- test_doh_post,
+               test_post_short_input,
+               test_post_long_input,
+               test_post_unparseable_input,
+               test_post_unsupp_type
+       }
+
+       return tests
+end
index 8817e82baef84f5503a42f467ba8161990364f8b..518f96f3050834eeb95324df8e49fdd3796dafa5 100644 (file)
@@ -18,6 +18,7 @@ lua_mod_src += [
 
 config_tests += [
   ['http', files('http.test.lua')],
+  ['http.doh', files('http_doh.test.lua')],
   ['http.tls', files('test_tls/tls.test.lua')],
 ]