Skip to content

Commit a79a56f

Browse files
committed
http/client: Initial prototype for passing .dns_resolver to client.connect
1 parent a698027 commit a79a56f

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

http/client.lua

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
local monotime = require "cqueues".monotime
12
local ca = require "cqueues.auxlib"
23
local cs = require "cqueues.socket"
4+
local cqueues_dns_record = require "cqueues.dns.record"
35
local http_tls = require "http.tls"
46
local http_util = require "http.util"
57
local connection_common = require "http.connection_common"
@@ -80,6 +82,31 @@ local function negotiate(s, options, timeout)
8082
end
8183
end
8284

85+
-- `type` parameter is what sort of records you want to find could be "A" or
86+
-- "AAAA" or `nil` if you want to filter yourself e.g. to implement
87+
-- https://www.ietf.org/archive/id/draft-vavrusa-dnsop-aaaa-for-free-00.txt
88+
local function each_matching_record(pkt, name, type)
89+
-- First need to do CNAME chasing
90+
local params = {
91+
section = "answer";
92+
class = cqueues_dns_record.IN;
93+
type = cqueues_dns_record.CNAME;
94+
name = name .. ".";
95+
}
96+
for _=1, 8 do -- avoid cname loops
97+
-- Ignores any CNAME record past the first (which should never occur anyway)
98+
local func, state, first = pkt:grep(params)
99+
local record = func(state, first)
100+
if record == nil then
101+
-- Not found
102+
break
103+
end
104+
params.name = record:host()
105+
end
106+
params.type = type
107+
return pkt:grep(params)
108+
end
109+
83110
local function connect(options, timeout)
84111
local bind = options.bind
85112
if bind ~= nil then
@@ -99,11 +126,68 @@ local function connect(options, timeout)
99126
port = bind_port;
100127
}
101128
end
129+
130+
local family = options.family
131+
local path = options.path
132+
local host = options.host
133+
if not path and not http_util.is_ip(host) then
134+
local dns_resolver = options.dns_resolver
135+
if dns_resolver then
136+
local deadline = timeout and monotime()+timeout
137+
local hostv4, hostv6
138+
if family == nil or family == cs.AF_UNSPEC or family == cs.AF_INET6 then
139+
-- Query for AAAA record
140+
local packet = ca.fileresult(dns_resolver:query(host, cqueues_dns_record.AAAA, nil, timeout))
141+
if packet then
142+
-- If IPv6 explicitly requested then filter down to only AAAA records
143+
local type = (family == cs.AF_INET6) and cqueues_dns_record.AAAA or nil
144+
for rec in each_matching_record(packet, host, type) do
145+
local t = rec:type()
146+
if t == cqueues_dns_record.AAAA then
147+
hostv6 = rec:addr()
148+
break
149+
elseif t == cqueues_dns_record.A then
150+
hostv4 = rec:addr()
151+
break
152+
end
153+
end
154+
end
155+
end
156+
if (hostv4 == nil and hostv6 == nil) and (family == nil or family == cs.AF_UNSPEC or family == cs.AF_INET) then
157+
-- Query for A record
158+
local packet = ca.fileresult(dns_resolver:query(host, cqueues_dns_record.A, nil, deadline and deadline-monotime()))
159+
if packet then
160+
-- If IPv4 explicitly requested then filter down to only A records
161+
-- Skip AAAA if we already have hostv6
162+
local type = (family == cs.AF_INET or hostv6) and cqueues_dns_record.A or nil
163+
for rec in each_matching_record(packet, host, type) do
164+
local t = rec:type()
165+
if t == cqueues_dns_record.A then
166+
hostv4 = rec:addr()
167+
break
168+
elseif t == cqueues_dns_record.AAAA then
169+
hostv6 = rec:addr()
170+
break
171+
end
172+
end
173+
end
174+
end
175+
if hostv6 then
176+
host = hostv6
177+
elseif hostv4 then
178+
host = hostv4
179+
else
180+
return nil, "The name does not resolve for the supplied parameters"
181+
end
182+
timeout = deadline and deadline-monotime()
183+
end
184+
end
185+
102186
local s, err, errno = ca.fileresult(cs.connect {
103-
family = options.family;
104-
host = options.host;
187+
family = family;
188+
host = host;
105189
port = options.port;
106-
path = options.path;
190+
path = path;
107191
bind = bind;
108192
sendname = false;
109193
v6only = options.v6only;

spec/client_spec.lua

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ describe("http.client module", function()
44
local http_h1_connection = require "http.h1_connection"
55
local http_h2_connection = require "http.h2_connection"
66
local http_headers = require "http.headers"
7+
local http_server = require "http.server"
78
local http_tls = require "http.tls"
89
local cqueues = require "cqueues"
910
local ca = require "cqueues.auxlib"
1011
local cs = require "cqueues.socket"
12+
local cdh = require "cqueues.dns.hosts"
13+
local cdr = require "cqueues.dns.resolver"
14+
local cdrs = require "cqueues.dns.resolvers"
1115
local openssl_pkey = require "openssl.pkey"
1216
local openssl_ctx = require "openssl.ssl.context"
1317
local openssl_x509 = require "openssl.x509"
@@ -29,6 +33,55 @@ describe("http.client module", function()
2933
local res_headers = assert(stream:get_headers())
3034
assert.same("200", res_headers:get(":status"))
3135
end
36+
local function test(client_cb)
37+
local cq = cqueues.new()
38+
local s = assert(http_server.listen {
39+
host = "localhost";
40+
port = 0;
41+
onstream = function(s, stream)
42+
assert(stream:get_headers())
43+
local resp_headers = http_headers.new()
44+
resp_headers:append(":status", "200")
45+
assert(stream:write_headers(resp_headers, false))
46+
assert(stream:write_chunk("hello world", true))
47+
stream:shutdown()
48+
stream.connection:shutdown()
49+
s:close()
50+
end;
51+
})
52+
assert(s:listen())
53+
local family, host, port = s:localname()
54+
cq:wrap(function()
55+
assert_loop(s)
56+
end)
57+
cq:wrap(client_cb, family, host, port)
58+
assert_loop(cq, TEST_TIMEOUT)
59+
assert.truthy(cq:empty())
60+
end
61+
it("works with a cqueues.dns.resolver object", function()
62+
test(function(family, ip, port)
63+
local hosts = cdh.new()
64+
hosts:insert(ip, "example.com")
65+
send_request(assert(client.connect {
66+
dns_resolver = cdr.new(nil, hosts);
67+
family = family;
68+
host = "example.com";
69+
port = port;
70+
}))
71+
end)
72+
end)
73+
it("works with a cqueues.dns.resolvers object", function()
74+
test(function(family, ip, port)
75+
local hosts = cdh.new()
76+
hosts:insert(ip, "example.com")
77+
send_request(assert(client.connect {
78+
dns_resolver = cdrs.new(nil, hosts);
79+
family = family;
80+
host = "example.com";
81+
port = port;
82+
}))
83+
end)
84+
end)
3285
local function test_pair(client_options, server_func)
3386
local s, c = ca.assert(cs.pair())
3487
local cq = cqueues.new();

0 commit comments

Comments
 (0)