diff --git a/lualib/http/httpc.lua b/lualib/http/httpc.lua index f1633eb4..b38ffad5 100644 --- a/lualib/http/httpc.lua +++ b/lualib/http/httpc.lua @@ -33,11 +33,11 @@ local function parse_host(host) if host:find(":", colon1 + 1, true) then -- two or more colons: ipv6 - local ipv6, port = host:match("^%[(.-)%]:(%d+)$") + local ipv6, port = host:match("^%[(.-)%]:?(%d*)$") if ipv6 then - return ipv6, tonumber(port), "ipv6" + return ipv6, port ~= "" and tonumber(port) or nil, "ipv6" end - return host, nil, "ipv6" + error(string.format("Invalid host: bare IPv6 address '%s', use '[%s]' instead", host, host)) end -- single colon: host:port @@ -57,9 +57,14 @@ local function parse_url(host) protocol = "http" hostname = host end - local htype + local hostheader = hostname + local htype, port hostname, port, htype = parse_host(hostname) - return protocol, hostname, port or default_port[protocol] or error("Invalid protocol: " .. protocol), htype + port = port or default_port[protocol] + if not port then + error("Invalid protocol: " .. protocol) + end + return protocol, hostname, port, htype, hostheader end local SSLCTX_CLIENT = nil @@ -91,7 +96,7 @@ local function gen_interface(protocol, fd, hostname) end local function connect(host, timeout) - local protocol, hostname, port, htype = parse_url(host) + local protocol, hostname, port, htype, hostheader = parse_url(host) local hostaddr = hostname if htype == "hostname" then if async_dns then @@ -116,9 +121,9 @@ local function connect(host, timeout) end) end if interface.init then - interface.init(host) + interface.init(htype == "hostname" and hostname or nil) end - return fd, interface, host + return fd, interface, hostheader end local function close_interface(interface, fd) diff --git a/lualib/http/tlshelper.lua b/lualib/http/tlshelper.lua index a81405ca..ec8a083c 100644 --- a/lualib/http/tlshelper.lua +++ b/lualib/http/tlshelper.lua @@ -7,7 +7,9 @@ function tlshelper.init_requestfunc(fd, tls_ctx) local readfunc = socket.readfunc(fd) local writefunc = socket.writefunc(fd) return function (hostname) - tls_ctx:set_ext_host_name(hostname) + if hostname then + tls_ctx:set_ext_host_name(hostname) + end local ds1 = tls_ctx:handshake() writefunc(ds1) while not tls_ctx:finished() do diff --git a/test/testhttp.lua b/test/testhttp.lua index f5ea590b..66be926d 100644 --- a/test/testhttp.lua +++ b/test/testhttp.lua @@ -99,6 +99,25 @@ local function test_connect_formats() end end + -- verify that invalid input is properly rejected + local function try_error(desc, host, pattern) + local ok, err = pcall(httpc.get, host, "/", {}) + total = total + 1 + if ok then + print(string.format(" FAIL: %-35s expected error but succeeded", desc)) + return false + end + err = tostring(err) + if err:find(pattern) then + passed = passed + 1 + print(string.format(" PASS: %-35s rejected as expected", desc)) + return true + else + print(string.format(" FAIL: %-35s wrong error=%s", desc, err)) + return false + end + end + -- hostname variants try_get("bare hostname", "baidu.com") try_get("hostname:port", "baidu.com:80") @@ -127,16 +146,34 @@ local function test_connect_formats() print(" SKIP: https tests (no ltls module)") end - -- ipv6 variants (parsing verification, connection may fail without ipv6 network) - try_parse("bare ipv6 ::1", "::1") + -- bare ipv6 should be rejected (RFC 3986: brackets required) + try_error("bare ipv6 ::1 (reject)", "::1", "bare IPv6") + try_error("bare full ipv6 (reject)", "2001:db8::1", "bare IPv6") + + -- bracketed ipv6 (parsing verification, connection may fail without ipv6 network) + try_parse("[ipv6] no port", "[::1]") try_parse("[ipv6]:port", "[::1]:8080") - try_parse("bare full ipv6", "2001:db8::1") + try_parse("[full ipv6] no port", "[2001:db8::1]") try_parse("[full ipv6]:port", "[2001:db8::1]:80") + try_parse("http://[ipv6]", "http://[::1]") try_parse("http://[ipv6]:port", "http://[::1]:80") if pcall(require, "ltls.c") then try_parse("https://[ipv6]:port", "https://[::1]:443") end + -- ipv6 real connection test via AAAA DNS resolution + local ok_aaaa, ipv6 = pcall(dns.resolve, "ipv6.google.com", true) + if ok_aaaa and ipv6 then + local ipv6_host = string.format("[%s]", ipv6) + print(string.format(" resolved ipv6.google.com AAAA -> %s", ipv6)) + try_parse("[ipv6] from AAAA", ipv6_host) + try_parse("[ipv6]:80 from AAAA", ipv6_host .. ":80") + try_parse("http://[ipv6] from AAAA", "http://" .. ipv6_host) + try_parse("http://[ipv6]:80 from AAAA", "http://" .. ipv6_host .. ":80") + else + print(" SKIP: ipv6 AAAA tests (dns resolve failed for ipv6.google.com)") + end + -- k8s internal domain variants (parsing verification, connection will fail outside k8s) -- domains ending with digits must not be mistaken for ipv4 try_parse("k8s svc short", "my-svc:8080")