Files
skynet/lualib/http/httpc.lua
IAN.Z bed435660e fix: httpc支持ipv6 (#2125)
* fix: httpc支持ipv6

* httpc ip:port解析放在c层

* lua-socket.c listen增加ip:port字符串解析, 相关cluster代码修改 #2124

* socketchannel changehost port可置空

* socketchannel changehost恢复port判空

* httpc解析域名加端口号逻辑优化

* 注释中字符串模式内容格式优化

---------

Co-authored-by: zhuyin.zhu <zhuyin.zhu@bytedance.com>
2025-12-27 17:11:52 +08:00

184 lines
5.0 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 function check_protocol(host)
local protocol = host:match("^[Hh][Tt][Tt][Pp][Ss]?://")
if protocol then
host = string.gsub(host, "^"..protocol, "")
protocol = string.lower(protocol)
if protocol == "https://" then
return "https", host
elseif protocol == "http://" then
return "http", host
else
error(string.format("Invalid protocol: %s", protocol))
end
else
return "http", host
end
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
protocol, host = check_protocol(host)
local hostname, port
if async_dns then
-- hostname string (ends with ":?%d*") must begin with a substring that doesn't contain colon "[^:]"
-- and end with a character that is not a colon or a digit "[^%d%]:]".
-- hostname not end with ".", pattern "%." can avoid splitting "127.0.0.1" into "127.0.0." and "1"
hostname, port = host:match "^([^:]-[^%d%]:%.]):?(%d*)$"
if hostname then
local msg
host, msg = dns.resolve(hostname)
if not host then
error(string.format("%s dns resolve failed msg:%s", hostname, msg))
end
end
end
if port == "" or (port == nil and not host:find ":%d+$") then
port = protocol=="http" and 80 or protocol=="https" and 443
end
local fd = socket.connect(host, port, timeout)
if not fd then
error(string.format("%s connect error host:%s, port:%s, timeout:%s", protocol, host, port, timeout))
end
-- print("protocol hostname port", protocol, hostname, port)
local interface = gen_interface(protocol, fd, hostname)
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(host)
end
return fd, interface, host
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