Skip to content

Commit

Permalink
feat(websocket): support client
Browse files Browse the repository at this point in the history
Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
  • Loading branch information
zhaojh329 committed Jul 26, 2023
1 parent d0dcbcb commit 53678c6
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 9 deletions.
29 changes: 29 additions & 0 deletions examples/network/websocket_client.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env eco

local websocket = require 'eco.websocket'

local ws, err = websocket.connect('ws://127.0.0.1:8080/ws')
if not ws then
print('failed to connect: ' .. err)
return
end

local bytes, err = ws:send_text('Hello')
if not bytes then
print('failed to send frame: ', err)
return
end

local data, typ, err = ws:recv_frame()
if not data then
print('failed to receive the frame: ', err)
return
end

print('received: ', data, ' (', typ, '): ', err)

local bytes, err = ws:send_close(1000, 'bye')
if not bytes then
print('failed to send frame: ', err)
return
end
File renamed without changes.
10 changes: 5 additions & 5 deletions http.lua
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ local function do_http_request(s, method, path, headers, body, timeout)
return resp
end

local function http_connect_host(host, port, https, opts)
function M.connect(host, port, use_ssl, opts)
local answers, err = dns.query(host)
if not answers then
return nil, 'resolve "' .. host .. '" fail: ' .. err
Expand All @@ -461,18 +461,18 @@ local function http_connect_host(host, port, https, opts)
for _, a in ipairs(answers) do
if a.type == dns.TYPE_A or a.type == dns.TYPE_AAAA then
local connect = socket.connect_tcp
if https then
if use_ssl then
connect = ssl.connect
end

if a.type == dns.TYPE_AAAA then
connect = socket.connect_tcp6
if https then
if use_ssl then
connect = ssl.connect6
end
end

if https then
if use_ssl then
s, err = connect(a.address, port, opts.insecure)
else
s, err = connect(a.address, port)
Expand Down Expand Up @@ -567,7 +567,7 @@ function M.request(req, body, opts)
s, err = socket.connect_tcp(req.proxy.ipaddr, req.proxy.port)
path = req.url
else
s, err = http_connect_host(host, port, scheme == 'https', opts)
s, err = M.connect(host, port, scheme == 'https', opts)
if not s then
return nil, 'connect fail: ' .. err
end
Expand Down
121 changes: 117 additions & 4 deletions websocket.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
local base64 = require 'eco.encoding.base64'
local sha1 = require 'eco.crypto.sha1'
local http = require 'eco.http'
local url = require 'eco.url'
local bit = require 'eco.bit'
local sys = require 'eco.sys'

local tostring = tostring
local concat = table.concat
Expand Down Expand Up @@ -84,8 +86,7 @@ function methods:recv_frame(timeout)
return nil, nil, 'failed to receive the 8 byte payload length: ' .. err
end

if str_byte(data, 1) ~= 0 or str_byte(data, 2) ~= 0 or str_byte(data, 3) ~= 0 or str_byte(data, 4) ~= 0
then
if str_byte(data, 1) ~= 0 or str_byte(data, 2) ~= 0 or str_byte(data, 3) ~= 0 or str_byte(data, 4) ~= 0 then
return nil, nil, 'payload len too large'
end

Expand Down Expand Up @@ -219,7 +220,7 @@ local function build_frame(fin, opcode, payload_len, payload, masking)
if masking then
-- set the mask bit
snd = bor(snd, 0x80)
local key = rand(0xffffffff)
local key = rand(0xffffff)
masking_key = str_char(band(rshift(key, 24), 0xff),
band(rshift(key, 16), 0xff),
band(rshift(key, 8), 0xff),
Expand Down Expand Up @@ -266,7 +267,7 @@ function methods:send_frame(fin, opcode, payload)
end
end

local frame, err = build_frame(fin, opcode, payload_len, payload)
local frame, err = build_frame(fin, opcode, payload_len, payload, mt.masking)
if not frame then
return nil, 'failed to build frame: ' .. err
end
Expand Down Expand Up @@ -380,4 +381,116 @@ function M.upgrade(con, req, opts)
})
end

function M.connect(uri, opts)
local u, err = url.parse(uri)
if not u then
return nil, err
end

local scheme, host, port, path = u.scheme, u.host, u.port, u.raw_path

if scheme ~= 'ws' and scheme ~= 'wss' then
return nil, 'unsupported scheme: ' .. scheme
end

opts = opts or {}

local proto_header, origin_header

local protos = opts.protocols
if protos then
if type(protos) == 'table' then
proto_header = 'Sec-WebSocket-Protocol: ' .. concat(protos, ',') .. '\r\n'

else
proto_header = 'Sec-WebSocket-Protocol: ' .. protos .. '\r\n'
end
end

local origin = opts.origin
if origin then
origin_header = 'Origin: ' .. origin .. '\r\n'
end

local host_header = 'Host: ' .. host

if port ~= 80 and port ~= 443 then
host_header = host_header .. ':' .. port
end

local bytes = str_char(rand(256) - 1, rand(256) - 1, rand(256) - 1,
rand(256) - 1, rand(256) - 1, rand(256) - 1,
rand(256) - 1, rand(256) - 1, rand(256) - 1,
rand(256) - 1, rand(256) - 1, rand(256) - 1,
rand(256) - 1, rand(256) - 1, rand(256) - 1,
rand(256) - 1)

local key = base64.encode(bytes)
local head = {
'GET ' .. path .. ' HTTP/1.1\r\n',
'Upgrade: websocket\r\n',
host_header .. '\r\n',
'Sec-WebSocket-Key: ' .. key .. '\r\n',
proto_header or '',
'Sec-WebSocket-Version: 13\r\n',
origin_header or '',
'Connection: Upgrade\r\n'
}

for k, v in pairs(opts.headers or {}) do
head[#head + 1] = k .. ': ' .. v .. '\r\n'
end

local sock, err = http.connect(host, port, scheme == 'wss', opts)
if not sock then
return nil, 'connect fail: ' .. err
end

local bytes, err = sock:send(concat(head) .. '\r\n')
if not bytes then
sock:close()
return nil, 'failed to send the handshake request: ' .. err
end

local timeout = opts.timeout or 30
local deadtime = sys.uptime() + timeout

local line, err = sock:recv('*l', deadtime - sys.uptime())
if not line then
return nil, err
end

local code, status = line:match('^HTTP/1.1%s*(%d+)%s*(.*)')
if not code or not status then
sock:close()
return nil, 'invalid http status line'
end

if code ~= '101' then
sock:close()
return nil, 'connect fail with status code: ' .. code
end

while true do
line, err = sock:recv('*l', deadtime - sys.uptime())
if not line then
sock:close()
return nil, 'failed to receive response header: ' .. err
end

if line == '' then
break
end
end

opts.max_payload_len = opts.max_payload_len or 65535

return setmetatable({}, {
__index = methods,
masking = true,
sock = sock,
opts = opts
})
end

return M

0 comments on commit 53678c6

Please sign in to comment.