Last active
August 28, 2018 02:12
-
-
Save daurnimator/0c010a9393398df4e67be98b1f983d7f to your computer and use it in GitHub Desktop.
DNS over HTTP with lua-http
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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