mirror of
https://github.com/cloudwu/skynet.git
synced 2026-07-22 02:53:09 +00:00
fix: improve hostname detection to support numeric-suffixed domains (#2147)
* fix: improve hostname detection to support numeric-suffixed domains * 1.connect改为支持ipv6 2.check_protocol改为parse_url,语义更明确 3.解析域名时直接返回host_type
This commit is contained in:
@@ -23,24 +23,33 @@ local default_port = {
|
|||||||
https = 443,
|
https = 443,
|
||||||
}
|
}
|
||||||
|
|
||||||
local function hostname_port(host)
|
local function parse_host(host)
|
||||||
if host:find ".*:.*:" then
|
local colon1 = host:find(":", 1, true)
|
||||||
-- If host contains 2 or more ":", it's ipv6 address
|
if not colon1 then
|
||||||
local ipv6, port = host:match "^%[(.-)%]:(%d+)$"
|
-- no colon: bare hostname or bare ipv4
|
||||||
|
local htype = host:find("^%d+%.%d+%.%d+%.%d+$") and "ipv4" or "hostname"
|
||||||
|
return host, nil, htype
|
||||||
|
end
|
||||||
|
|
||||||
|
if host:find(":", colon1 + 1, true) then
|
||||||
|
-- two or more colons: ipv6
|
||||||
|
local ipv6, port = host:match("^%[(.-)%]:(%d+)$")
|
||||||
if ipv6 then
|
if ipv6 then
|
||||||
return ipv6, port
|
return ipv6, tonumber(port), "ipv6"
|
||||||
else
|
|
||||||
return host
|
|
||||||
end
|
end
|
||||||
|
return host, nil, "ipv6"
|
||||||
end
|
end
|
||||||
local hostname, port = host:match "(.-):(%d+)$"
|
|
||||||
if hostname then
|
-- single colon: host:port
|
||||||
return hostname, port
|
local h, port = host:match("^(.-):(%d+)$")
|
||||||
|
if h then
|
||||||
|
local htype = h:find("^%d+%.%d+%.%d+%.%d+$") and "ipv4" or "hostname"
|
||||||
|
return h, tonumber(port), htype
|
||||||
end
|
end
|
||||||
return host
|
return host, nil, "hostname"
|
||||||
end
|
end
|
||||||
|
|
||||||
local function check_protocol(host)
|
local function parse_url(host)
|
||||||
local protocol, hostname = host:match "^(%a+)://(.*)"
|
local protocol, hostname = host:match "^(%a+)://(.*)"
|
||||||
if protocol then
|
if protocol then
|
||||||
protocol = string.lower(protocol)
|
protocol = string.lower(protocol)
|
||||||
@@ -48,8 +57,9 @@ local function check_protocol(host)
|
|||||||
protocol = "http"
|
protocol = "http"
|
||||||
hostname = host
|
hostname = host
|
||||||
end
|
end
|
||||||
hostname, port = hostname_port(hostname)
|
local htype
|
||||||
return protocol, hostname, port or default_port[protocol] or error("Invalid protocol: " .. protocol)
|
hostname, port, htype = parse_host(hostname)
|
||||||
|
return protocol, hostname, port or default_port[protocol] or error("Invalid protocol: " .. protocol), htype
|
||||||
end
|
end
|
||||||
|
|
||||||
local SSLCTX_CLIENT = nil
|
local SSLCTX_CLIENT = nil
|
||||||
@@ -81,28 +91,23 @@ local function gen_interface(protocol, fd, hostname)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function connect(host, timeout)
|
local function connect(host, timeout)
|
||||||
local protocol, host, port = check_protocol(host)
|
local protocol, hostname, port, htype = parse_url(host)
|
||||||
local hostaddr = host
|
local hostaddr = hostname
|
||||||
local hostname
|
if htype == "hostname" then
|
||||||
if host:find "^[^:]-%D$" then
|
|
||||||
-- it's a hostname (not ip address), because
|
|
||||||
-- ipv6 contains colons
|
|
||||||
-- ipv4 end with a digit
|
|
||||||
hostname = host
|
|
||||||
if async_dns then
|
if async_dns then
|
||||||
local msg
|
local msg
|
||||||
hostaddr, msg = dns.resolve(host)
|
hostaddr, msg = dns.resolve(hostname)
|
||||||
if not hostaddr then
|
if not hostaddr then
|
||||||
error(string.format("%s dns resolve failed msg:%s", host, msg))
|
error(string.format("%s dns resolve failed msg:%s", hostname, msg))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local fd = socket.connect(hostaddr, port, timeout)
|
local fd = socket.connect(hostaddr, port, timeout)
|
||||||
if not fd then
|
if not fd then
|
||||||
error(string.format("%s connect error host:%s, port:%s, timeout:%s", protocol, host, port, timeout))
|
error(string.format("%s connect error host:%s, port:%s, timeout:%s", protocol, hostname, port, timeout))
|
||||||
end
|
end
|
||||||
local interface = gen_interface(protocol, fd, hostname)
|
local interface = gen_interface(protocol, fd, htype == "hostname" and hostname or nil)
|
||||||
if timeout then
|
if timeout then
|
||||||
skynet.timeout(timeout, function()
|
skynet.timeout(timeout, function()
|
||||||
if not interface.finish then
|
if not interface.finish then
|
||||||
|
|||||||
@@ -58,9 +58,109 @@ local function http_url_test()
|
|||||||
assert(qret[1] == qret[3])
|
assert(qret[1] == qret[3])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Integration test: connect to real server with various URL formats
|
||||||
|
-- Verifies parse_host / parse_url via httpc public API
|
||||||
|
local function test_connect_formats()
|
||||||
|
print("[test_connect_formats] =====>")
|
||||||
|
httpc.timeout = 100
|
||||||
|
|
||||||
|
local passed, total = 0, 0
|
||||||
|
|
||||||
|
local function try_get(desc, host)
|
||||||
|
local ok, status, body = pcall(httpc.get, host, "/", {})
|
||||||
|
total = total + 1
|
||||||
|
if ok then
|
||||||
|
passed = passed + 1
|
||||||
|
print(string.format(" PASS: %-35s status=%s", desc, status))
|
||||||
|
else
|
||||||
|
print(string.format(" FAIL: %-35s error=%s", desc, status))
|
||||||
|
end
|
||||||
|
return ok
|
||||||
|
end
|
||||||
|
|
||||||
|
-- verify parsing is correct even when connection fails
|
||||||
|
-- "connect error" in the message means parsing succeeded
|
||||||
|
local function try_parse(desc, host)
|
||||||
|
local ok, err = pcall(httpc.get, host, "/", {})
|
||||||
|
total = total + 1
|
||||||
|
if ok then
|
||||||
|
passed = passed + 1
|
||||||
|
print(string.format(" PASS: %-35s connected", desc))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
err = tostring(err)
|
||||||
|
if err:find("connect error") or err:find("connect failed") or err:find("Socket Error") then
|
||||||
|
passed = passed + 1
|
||||||
|
print(string.format(" PASS: %-35s parsed ok (connect failed as expected)", desc))
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
print(string.format(" FAIL: %-35s error=%s", desc, err))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- hostname variants
|
||||||
|
try_get("bare hostname", "baidu.com")
|
||||||
|
try_get("hostname:port", "baidu.com:80")
|
||||||
|
try_get("http://hostname", "http://baidu.com")
|
||||||
|
try_get("http://hostname:port", "http://baidu.com:80")
|
||||||
|
try_get("HTTP:// uppercase", "HTTP://baidu.com")
|
||||||
|
|
||||||
|
-- ipv4 variants (resolve first to get a real IP)
|
||||||
|
local ip = dns.resolve("baidu.com")
|
||||||
|
if ip then
|
||||||
|
print(string.format(" resolved baidu.com -> %s", ip))
|
||||||
|
try_get("bare ipv4", ip)
|
||||||
|
try_get("ipv4:port", ip .. ":80")
|
||||||
|
try_get("http://ipv4", "http://" .. ip)
|
||||||
|
try_get("http://ipv4:port", "http://" .. ip .. ":80")
|
||||||
|
else
|
||||||
|
print(" SKIP: ipv4 tests (dns resolve failed)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- https variants (skip if no tls)
|
||||||
|
if pcall(require, "ltls.c") then
|
||||||
|
try_get("https://hostname", "https://baidu.com")
|
||||||
|
try_get("https://hostname:port", "https://baidu.com:443")
|
||||||
|
try_get("HTTPS:// uppercase", "HTTPS://baidu.com")
|
||||||
|
else
|
||||||
|
print(" SKIP: https tests (no ltls module)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ipv6 variants (parsing verification, connection may fail without ipv6 network)
|
||||||
|
try_parse("bare ipv6 ::1", "::1")
|
||||||
|
try_parse("[ipv6]:port", "[::1]:8080")
|
||||||
|
try_parse("bare full ipv6", "2001:db8::1")
|
||||||
|
try_parse("[full ipv6]:port", "[2001:db8::1]:80")
|
||||||
|
try_parse("http://[ipv6]:port", "http://[::1]:80")
|
||||||
|
if pcall(require, "ltls.c") then
|
||||||
|
try_parse("https://[ipv6]:port", "https://[::1]:443")
|
||||||
|
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")
|
||||||
|
try_parse("k8s svc.ns", "my-svc.default:8080")
|
||||||
|
try_parse("k8s svc.ns.svc", "my-svc.default.svc:8080")
|
||||||
|
try_parse("k8s svc fqdn", "my-svc.default.svc.cluster.local:8080")
|
||||||
|
try_parse("k8s svc fqdn no port", "my-svc.default.svc.cluster.local")
|
||||||
|
try_parse("k8s http:// svc fqdn", "http://my-svc.default.svc.cluster.local:8080")
|
||||||
|
try_parse("k8s headless pod", "pod-0.my-svc.default.svc.cluster.local:8080")
|
||||||
|
try_parse("k8s nodeport", "http://node1.cluster.local:30080")
|
||||||
|
try_parse("k8s statefulset pod-0", "web-0.nginx-svc.default:8080")
|
||||||
|
try_parse("k8s numeric ns", "my-svc.ns123:8080")
|
||||||
|
try_parse("k8s all-digit segments", "svc123.ns456:9090")
|
||||||
|
try_parse("k8s pod ip-style dns", "10-244-0-5.default.pod.cluster.local:80")
|
||||||
|
try_parse("k8s 5-segment digits", "10.0.0.1.nip.io:8080")
|
||||||
|
|
||||||
|
print(string.format("[test_connect_formats] done, %d/%d passed", passed, total))
|
||||||
|
end
|
||||||
|
|
||||||
local function main()
|
local function main()
|
||||||
dns.server()
|
dns.server()
|
||||||
|
|
||||||
|
test_connect_formats()
|
||||||
|
|
||||||
http_stream_test()
|
http_stream_test()
|
||||||
http_head_test()
|
http_head_test()
|
||||||
http_url_test()
|
http_url_test()
|
||||||
@@ -77,4 +177,3 @@ skynet.start(function()
|
|||||||
print(pcall(main))
|
print(pcall(main))
|
||||||
skynet.exit()
|
skynet.exit()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Reference in New Issue
Block a user