Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@daurnimator
Last active August 28, 2018 02:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daurnimator/0c010a9393398df4e67be98b1f983d7f to your computer and use it in GitHub Desktop.
Save daurnimator/0c010a9393398df4e67be98b1f983d7f to your computer and use it in GitHub Desktop.
DNS over HTTP with lua-http
#!/usr/bin/env lua
local basexx = require "basexx"
local cq_auxlib = require "cqueues.auxlib"
local cq_packet = require "cqueues.dns.packet"
local http_request = require "http.request"
local function doh_request(uri, method, name, type, class)
local req = http_request.new_from_uri(uri)
req.headers:upsert("accept", "application/dns-message")
local dns_req do
-- Construct request packet
local request_packet = cq_packet.new()
--[[ draft-ietf-doh-dns-over-https-14 Section 4.1:
In order to maximize HTTP cache friendliness, DoH clients using media
formats that include the ID field from the DNS message header, such
as application/dns-message, SHOULD use a DNS ID of 0 in every DNS
request. ]]
request_packet:setqid(0)
request_packet:setflags{rd=true}
cq_auxlib.assert(request_packet:push("QUESTION", name, type, class))
dns_req = request_packet:dump()
end
if method == "GET" then
-- Append request to query args
local old_path = req.headers:get(":path")
local path
local query_state = old_path:match("%?.-(.?)$")
if not query_state then
path = old_path .. "?dns="..basexx.to_url64(dns_req)
elseif query_state ~= "" and query_state ~= "&" then
path = old_path .. "&dns="..basexx.to_url64(dns_req)
else
path = old_path .. "dns="..basexx.to_url64(dns_req)
end
req.headers:upsert(":path", path)
elseif method == "POST" then
req.headers:upsert(":method", "POST")
req.headers:upsert("content-type", "application/dns-message")
req:set_body(dns_req)
else
error("invalid method")
end
local headers, stream, errno = req:go()
if not headers then
return nil, stream, errno
end
if headers:get ":status":sub(1, 1) ~= "2" then
--[[ draft-ietf-doh-dns-over-https-14 Section 4.2:
A successful HTTP response with a 2xx status code ([RFC7231]
Section 6.3) is used for any valid DNS response, regardless of the
DNS response code.]]
return nil -- TODO
end
if headers:get "content-type" ~= "application/dns-message" then
return nil -- TODO
end
local body = assert(stream:get_body_as_string())
local reply = cq_packet.new(body)
return reply
end
-- local server = "https://dns.google.com/experimental?ct"
-- local server = "https://1.1.1.1/dns-query"
local server = "http://127.0.0.1:10053/"
-- local server = "https://cloudflare-dns.com/dns-query"
-- print(doh_request(server, "GET", "example.com"))
-- print(doh_request(server, "GET", "daniel.haxx.se"))
-- print(doh_request(server, "GET", "daurnimator.com"))
print(doh_request(server, "POST", "daurnimator.com"))
#!/usr/bin/env lua
--[[
A DNS-over-HTTP server
Implements a basic server following draft-ietf-doh-dns-over-https-14
Usage: lua examples/doh_server.lua [<port>]
]]
local port = arg[1] or 0 -- 0 means pick one at random
local basexx = require "basexx"
local cq_auxlib = require "cqueues.auxlib"
local cq_dns = require "cqueues.dns"
local cq_packet = require "cqueues.dns.packet"
local cq_record = require "cqueues.dns.record"
local http_server = require "http.server"
local http_headers = require "http.headers"
local question_grep = {
section = cq_record.QUESTION;
}
local ttl_grep = {
section = cq_record.ANSWER;
}
local function handle_request(stream, req_headers, res_headers)
local raw_request_packet
local req_method = req_headers:get ":method"
if req_method == "POST" then
if req_headers:get("content-type") ~= "application/dns-message" then
res_headers:append(":status", "415")
return
end
-- FIXME: Use timeouts
raw_request_packet = assert(stream:get_body_as_string())
elseif req_method == "GET" or req_method == "HEAD" then
local query_arg = req_headers:get(":path"):match("[%?%&]dns=([^&]*)")
if not query_arg then
res_headers:append(":status", "404")
return
end
-- No need to decodeURIComponent
raw_request_packet = basexx.from_url64(query_arg)
else
res_headers:append(":status", "405")
return
end
local request_packet = cq_packet.new(raw_request_packet)
local question do
local iter, state, first = request_packet:grep(question_grep)
question = iter(state, first)
end
if not question then
res_headers:append(":status", "400")
return
end
local resolver = cq_dns.getpool()
-- TODO: look at request_packet flags for recursion settings
local response_packet = cq_auxlib.assert(resolver:query(
question:name(),
question:type(),
question:class()
-- FIXME: Use timeout
))
if not response_packet then
res_headers:append(":status", "503")
return
end
-- Set qid to match request
response_packet:setqid(request_packet:qid())
res_headers:append(":status", "200")
res_headers:append("content-type", "application/dns-message")
local ttl = math.huge
for rec in response_packet:grep(ttl_grep) do
ttl = math.min(ttl, rec:ttl())
end
if ttl < math.huge then
res_headers:append("cache-control", string.format("max-age=%d", ttl))
end
return response_packet:dump()
end
local function reply(myserver, stream) -- luacheck: ignore 212
-- Read in headers
local req_headers = assert(stream:get_headers())
local req_method = req_headers:get ":method"
-- Log request to stdout
assert(io.stdout:write(string.format('[%s] "%s %s HTTP/%g" "%s" "%s"\n',
os.date("%d/%b/%Y:%H:%M:%S %z"),
req_method or "",
req_headers:get(":path") or "",
stream.connection.version,
req_headers:get("referer") or "-",
req_headers:get("user-agent") or "-"
)))
-- Build response
local res_headers = http_headers.new()
local body = handle_request(stream, req_headers, res_headers)
if body then
res_headers:upsert("content-length", string.format("%d", #body))
if req_method == "HEAD" then
body = nil
end
end
-- Send headers to client; end the stream immediately if this was a HEAD request
assert(stream:write_headers(res_headers, not body))
if body then
-- Send body, ending the stream
assert(stream:write_chunk(body, true))
end
end
local myserver = assert(http_server.listen {
host = "localhost";
port = port;
onstream = reply;
onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212
local msg = op .. " on " .. tostring(context) .. " failed"
if err then
msg = msg .. ": " .. tostring(err)
end
assert(io.stderr:write(msg, "\n"))
end;
})
-- Manually call :listen() so that we are bound before calling :localname()
assert(myserver:listen())
do
local bound_port = select(3, myserver:localname())
assert(io.stderr:write(string.format("Now listening on port %d\n", bound_port)))
end
-- Start the main server loop
assert(myserver:loop())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment