mirror of
https://github.com/cloudwu/skynet.git
synced 2026-07-22 02:53:09 +00:00
* 按照RFC 3986的规范处理ipv6 host的格式化问题,ipv6地址需要用 []括起来 参考: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 * 解决ipv6格式不对的问题 * 只有域名的情况下才 set_ext_host_name
203 lines
5.3 KiB
Lua
203 lines
5.3 KiB
Lua
local skynet = require "skynet"
|
|
local socket = require "http.sockethelper"
|
|
local internal = require "http.internal"
|
|
local dns = require "skynet.dns"
|
|
|
|
local string = string
|
|
local table = table
|
|
local pcall = pcall
|
|
local error = error
|
|
local pairs = pairs
|
|
|
|
local httpc = {}
|
|
|
|
local async_dns
|
|
|
|
function httpc.dns(server,port)
|
|
async_dns = true
|
|
dns.server(server,port)
|
|
end
|
|
|
|
local default_port = {
|
|
http = 80,
|
|
https = 443,
|
|
}
|
|
|
|
local function parse_host(host)
|
|
local colon1 = host:find(":", 1, true)
|
|
if not colon1 then
|
|
-- 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
|
|
return ipv6, port ~= "" and tonumber(port) or nil, "ipv6"
|
|
end
|
|
error(string.format("Invalid host: bare IPv6 address '%s', use '[%s]' instead", host, host))
|
|
end
|
|
|
|
-- single colon: host: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
|
|
return host, nil, "hostname"
|
|
end
|
|
|
|
local function parse_url(host)
|
|
local protocol, hostname = host:match "^(%a+)://(.*)"
|
|
if protocol then
|
|
protocol = string.lower(protocol)
|
|
else
|
|
protocol = "http"
|
|
hostname = host
|
|
end
|
|
local hostheader = hostname
|
|
local htype, port
|
|
hostname, port, htype = parse_host(hostname)
|
|
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
|
|
local function gen_interface(protocol, fd, hostname)
|
|
if protocol == "http" then
|
|
return {
|
|
init = nil,
|
|
close = nil,
|
|
read = socket.readfunc(fd),
|
|
write = socket.writefunc(fd),
|
|
readall = function ()
|
|
return socket.readall(fd)
|
|
end,
|
|
}
|
|
elseif protocol == "https" then
|
|
local tls = require "http.tlshelper"
|
|
SSLCTX_CLIENT = SSLCTX_CLIENT or tls.newctx()
|
|
local tls_ctx = tls.newtls("client", SSLCTX_CLIENT, hostname)
|
|
return {
|
|
init = tls.init_requestfunc(fd, tls_ctx),
|
|
close = tls.closefunc(tls_ctx),
|
|
read = tls.readfunc(fd, tls_ctx),
|
|
write = tls.writefunc(fd, tls_ctx),
|
|
readall = tls.readallfunc(fd, tls_ctx),
|
|
}
|
|
else
|
|
error(string.format("Invalid protocol: %s", protocol))
|
|
end
|
|
end
|
|
|
|
local function connect(host, timeout)
|
|
local protocol, hostname, port, htype, hostheader = parse_url(host)
|
|
local hostaddr = hostname
|
|
if htype == "hostname" then
|
|
if async_dns then
|
|
local msg
|
|
hostaddr, msg = dns.resolve(hostname)
|
|
if not hostaddr then
|
|
error(string.format("%s dns resolve failed msg:%s", hostname, msg))
|
|
end
|
|
end
|
|
end
|
|
|
|
local fd = socket.connect(hostaddr, port, timeout)
|
|
if not fd then
|
|
error(string.format("%s connect error host:%s, port:%s, timeout:%s", protocol, hostname, port, timeout))
|
|
end
|
|
local interface = gen_interface(protocol, fd, htype == "hostname" and hostname or nil)
|
|
if timeout then
|
|
skynet.timeout(timeout, function()
|
|
if not interface.finish then
|
|
socket.shutdown(fd) -- shutdown the socket fd, need close later.
|
|
end
|
|
end)
|
|
end
|
|
if interface.init then
|
|
interface.init(htype == "hostname" and hostname or nil)
|
|
end
|
|
return fd, interface, hostheader
|
|
end
|
|
|
|
local function close_interface(interface, fd)
|
|
interface.finish = true
|
|
socket.close(fd)
|
|
if interface.close then
|
|
interface.close()
|
|
interface.close = nil
|
|
end
|
|
end
|
|
|
|
function httpc.request(method, hostname, url, recvheader, header, content)
|
|
local fd, interface, host = connect(hostname, httpc.timeout)
|
|
local ok , statuscode, body , header = pcall(internal.request, interface, method, host, url, recvheader, header, content)
|
|
if ok then
|
|
ok, body = pcall(internal.response, interface, statuscode, body, header)
|
|
end
|
|
close_interface(interface, fd)
|
|
if ok then
|
|
return statuscode, body
|
|
else
|
|
error(body or statuscode)
|
|
end
|
|
end
|
|
|
|
function httpc.head(hostname, url, recvheader, header, content)
|
|
local fd, interface, host = connect(hostname, httpc.timeout)
|
|
local ok , statuscode = pcall(internal.request, interface, "HEAD", host, url, recvheader, header, content)
|
|
close_interface(interface, fd)
|
|
if ok then
|
|
return statuscode
|
|
else
|
|
error(statuscode)
|
|
end
|
|
end
|
|
|
|
function httpc.request_stream(method, hostname, url, recvheader, header, content)
|
|
local fd, interface, host = connect(hostname, httpc.timeout)
|
|
local ok , statuscode, body , header = pcall(internal.request, interface, method, host, url, recvheader, header, content)
|
|
interface.finish = true -- don't shutdown fd in timeout
|
|
local function close_fd()
|
|
close_interface(interface, fd)
|
|
end
|
|
if not ok then
|
|
close_fd()
|
|
error(statuscode)
|
|
end
|
|
-- todo: stream support timeout
|
|
local stream = internal.response_stream(interface, statuscode, body, header)
|
|
stream._onclose = close_fd
|
|
return stream
|
|
end
|
|
|
|
function httpc.get(...)
|
|
return httpc.request("GET", ...)
|
|
end
|
|
|
|
local function escape(s)
|
|
return (string.gsub(s, "([^A-Za-z0-9_])", function(c)
|
|
return string.format("%%%02X", string.byte(c))
|
|
end))
|
|
end
|
|
|
|
function httpc.post(host, url, form, recvheader)
|
|
local header = {
|
|
["content-type"] = "application/x-www-form-urlencoded"
|
|
}
|
|
local body = {}
|
|
for k,v in pairs(form) do
|
|
table.insert(body, string.format("%s=%s",escape(k),escape(v)))
|
|
end
|
|
|
|
return httpc.request("POST", host, url, recvheader, header, table.concat(body , "&"))
|
|
end
|
|
|
|
return httpc
|