Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,269 @@
|
||||
module websocket
|
||||
|
||||
// MessageEventHandler represents a callback on a new message
|
||||
struct MessageEventHandler {
|
||||
handler SocketMessageFn = unsafe { nil } // callback function
|
||||
handler2 SocketMessageFn2 = unsafe { nil } // callback function with reference
|
||||
is_ref bool // true if has a reference object
|
||||
ref voidptr // referenced object
|
||||
}
|
||||
|
||||
// ErrorEventHandler represents a callback on error
|
||||
struct ErrorEventHandler {
|
||||
handler SocketErrorFn = unsafe { nil } // callback function
|
||||
handler2 SocketErrorFn2 = unsafe { nil } // callback function with reference
|
||||
is_ref bool // true if has a reference object
|
||||
ref voidptr // referenced object
|
||||
}
|
||||
|
||||
// OpenEventHandler represents a callback when connection is opened
|
||||
struct OpenEventHandler {
|
||||
handler SocketOpenFn = unsafe { nil } // callback function
|
||||
handler2 SocketOpenFn2 = unsafe { nil } // callback function with reference
|
||||
is_ref bool // true if has a reference object
|
||||
ref voidptr // referenced object
|
||||
}
|
||||
|
||||
// CloseEventHandler represents a callback on a closing event
|
||||
struct CloseEventHandler {
|
||||
handler SocketCloseFn = unsafe { nil } // callback function
|
||||
handler2 SocketCloseFn2 = unsafe { nil } // callback function with reference
|
||||
is_ref bool // true if has a reference object
|
||||
ref voidptr // referenced object
|
||||
}
|
||||
|
||||
struct AttachedEventHandler {
|
||||
handler ServerAttachedFn = unsafe { nil }
|
||||
handler2 ServerAttachedFn2 = unsafe { nil }
|
||||
is_ref bool
|
||||
ref voidptr
|
||||
}
|
||||
|
||||
pub type AcceptClientFn = fn (mut c ServerClient) !bool
|
||||
|
||||
pub type ServerAttachedFn = fn (mut c ServerClient) !
|
||||
|
||||
pub type ServerAttachedFn2 = fn (mut c ServerClient, v voidptr) !
|
||||
|
||||
pub type SocketMessageFn = fn (mut c Client, msg &Message) !
|
||||
|
||||
pub type SocketMessageFn2 = fn (mut c Client, msg &Message, v voidptr) !
|
||||
|
||||
pub type SocketErrorFn = fn (mut c Client, err string) !
|
||||
|
||||
pub type SocketErrorFn2 = fn (mut c Client, err string, v voidptr) !
|
||||
|
||||
pub type SocketOpenFn = fn (mut c Client) !
|
||||
|
||||
pub type SocketOpenFn2 = fn (mut c Client, v voidptr) !
|
||||
|
||||
pub type SocketCloseFn = fn (mut c Client, code int, reason string) !
|
||||
|
||||
pub type SocketCloseFn2 = fn (mut c Client, code int, reason string, v voidptr) !
|
||||
|
||||
// on_connect registers a callback when client connects to the server
|
||||
pub fn (mut s Server) on_connect(fun AcceptClientFn) ! {
|
||||
if s.accept_client_callbacks.len > 0 {
|
||||
return error('only one callback can be registered for accept client')
|
||||
}
|
||||
s.accept_client_callbacks << fun
|
||||
}
|
||||
|
||||
// on_message registers a callback on new messages
|
||||
pub fn (mut s Server) on_message(fun SocketMessageFn) {
|
||||
s.message_callbacks << MessageEventHandler{
|
||||
handler: fun
|
||||
}
|
||||
}
|
||||
|
||||
// on_message_ref registers a callback on new messages and provides a reference object
|
||||
pub fn (mut s Server) on_message_ref(fun SocketMessageFn2, ref voidptr) {
|
||||
s.message_callbacks << MessageEventHandler{
|
||||
handler2: fun
|
||||
ref: ref
|
||||
is_ref: true
|
||||
}
|
||||
}
|
||||
|
||||
// on_close registers a callback on closed socket
|
||||
pub fn (mut s Server) on_close(fun SocketCloseFn) {
|
||||
s.close_callbacks << CloseEventHandler{
|
||||
handler: fun
|
||||
}
|
||||
}
|
||||
|
||||
// on_close_ref registers a callback on closed socket and provides a reference object
|
||||
pub fn (mut s Server) on_close_ref(fun SocketCloseFn2, ref voidptr) {
|
||||
s.close_callbacks << CloseEventHandler{
|
||||
handler2: fun
|
||||
ref: ref
|
||||
is_ref: true
|
||||
}
|
||||
}
|
||||
|
||||
// on_attached registers a callback after a client has been attached to the
|
||||
// server, the handshake response has been written, and callbacks are set up,
|
||||
// but before the blocking listen loop begins.
|
||||
pub fn (mut s Server) on_attached(fun ServerAttachedFn) {
|
||||
s.attached_callbacks << AttachedEventHandler{
|
||||
handler: fun
|
||||
}
|
||||
}
|
||||
|
||||
// on_attached_ref registers a callback after a client has been attached to the
|
||||
// server and provides a reference object.
|
||||
pub fn (mut s Server) on_attached_ref(fun ServerAttachedFn2, ref voidptr) {
|
||||
s.attached_callbacks << AttachedEventHandler{
|
||||
handler2: fun
|
||||
ref: ref
|
||||
is_ref: true
|
||||
}
|
||||
}
|
||||
|
||||
// on_message registers a callback on new messages
|
||||
pub fn (mut ws Client) on_message(fun SocketMessageFn) {
|
||||
ws.message_callbacks << MessageEventHandler{
|
||||
handler: fun
|
||||
}
|
||||
}
|
||||
|
||||
// on_message_ref registers a callback on new messages and provides a reference object
|
||||
pub fn (mut ws Client) on_message_ref(fun SocketMessageFn2, ref voidptr) {
|
||||
ws.message_callbacks << MessageEventHandler{
|
||||
handler2: fun
|
||||
ref: ref
|
||||
is_ref: true
|
||||
}
|
||||
}
|
||||
|
||||
// on_error registers a callback on errors
|
||||
pub fn (mut ws Client) on_error(fun SocketErrorFn) {
|
||||
ws.error_callbacks << ErrorEventHandler{
|
||||
handler: fun
|
||||
}
|
||||
}
|
||||
|
||||
// on_error_ref registers a callback on errors and provides a reference object
|
||||
pub fn (mut ws Client) on_error_ref(fun SocketErrorFn2, ref voidptr) {
|
||||
ws.error_callbacks << ErrorEventHandler{
|
||||
handler2: fun
|
||||
ref: ref
|
||||
is_ref: true
|
||||
}
|
||||
}
|
||||
|
||||
// on_open registers a callback on successful opening the websocket
|
||||
pub fn (mut ws Client) on_open(fun SocketOpenFn) {
|
||||
ws.open_callbacks << OpenEventHandler{
|
||||
handler: fun
|
||||
}
|
||||
}
|
||||
|
||||
// on_open_ref registers a callback on successful opening the websocket
|
||||
// and provides a reference object
|
||||
pub fn (mut ws Client) on_open_ref(fun SocketOpenFn2, ref voidptr) {
|
||||
ws.open_callbacks << OpenEventHandler{
|
||||
handler2: fun
|
||||
ref: ref
|
||||
is_ref: true
|
||||
}
|
||||
}
|
||||
|
||||
// on_close registers a callback on closed socket
|
||||
pub fn (mut ws Client) on_close(fun SocketCloseFn) {
|
||||
ws.close_callbacks << CloseEventHandler{
|
||||
handler: fun
|
||||
}
|
||||
}
|
||||
|
||||
// on_close_ref registers a callback on closed socket and provides a reference object
|
||||
pub fn (mut ws Client) on_close_ref(fun SocketCloseFn2, ref voidptr) {
|
||||
ws.close_callbacks << CloseEventHandler{
|
||||
handler2: fun
|
||||
ref: ref
|
||||
is_ref: true
|
||||
}
|
||||
}
|
||||
|
||||
// send_connect_event invokes the on_connect callback
|
||||
fn (mut s Server) send_connect_event(mut c ServerClient) !bool {
|
||||
if s.accept_client_callbacks.len == 0 {
|
||||
// If no callback all client will be accepted
|
||||
return true
|
||||
}
|
||||
fun := s.accept_client_callbacks[0]
|
||||
res := fun(mut c)!
|
||||
return res
|
||||
}
|
||||
|
||||
fn (mut s Server) send_attached_event(mut c ServerClient) ! {
|
||||
for ev_handler in s.attached_callbacks {
|
||||
if !ev_handler.is_ref {
|
||||
ev_handler.handler(mut c)!
|
||||
} else {
|
||||
ev_handler.handler2(mut c, ev_handler.ref)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send_message_event invokes the on_message callback
|
||||
fn (mut ws Client) send_message_event(msg &Message) {
|
||||
ws.debug_log('sending on_message event')
|
||||
for ev_handler in ws.message_callbacks {
|
||||
if !ev_handler.is_ref {
|
||||
ev_handler.handler(mut ws, msg) or {
|
||||
ws.logger.error('send_message_event error: ${err}')
|
||||
}
|
||||
} else {
|
||||
ev_handler.handler2(mut ws, msg, ev_handler.ref) or {
|
||||
ws.logger.error('send_message_event error: ${err}')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send_error_event invokes the on_error callback
|
||||
fn (mut ws Client) send_error_event(error string) {
|
||||
ws.debug_log('sending on_error event')
|
||||
for ev_handler in ws.error_callbacks {
|
||||
if !ev_handler.is_ref {
|
||||
ev_handler.handler(mut ws, error) or {
|
||||
ws.logger.error('send_error_event error: ${error}, err: ${err}')
|
||||
}
|
||||
} else {
|
||||
ev_handler.handler2(mut ws, error, ev_handler.ref) or {
|
||||
ws.logger.error('send_error_event error: ${error}, err: ${err}')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send_close_event invokes the on_close callback
|
||||
fn (mut ws Client) send_close_event(code int, reason string) {
|
||||
ws.debug_log('sending on_close event')
|
||||
for ev_handler in ws.close_callbacks {
|
||||
if !ev_handler.is_ref {
|
||||
ev_handler.handler(mut ws, code, reason) or {
|
||||
ws.logger.error('send_close_event error: ${err}')
|
||||
}
|
||||
} else {
|
||||
ev_handler.handler2(mut ws, code, reason, ev_handler.ref) or {
|
||||
ws.logger.error('send_close_event error: ${err}')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send_open_event invokes the on_open callback
|
||||
fn (mut ws Client) send_open_event() {
|
||||
ws.debug_log('sending on_open event')
|
||||
for ev_handler in ws.open_callbacks {
|
||||
if !ev_handler.is_ref {
|
||||
ev_handler.handler(mut ws) or { ws.logger.error('send_open_event error: ${err}') }
|
||||
} else {
|
||||
ev_handler.handler2(mut ws, ev_handler.ref) or {
|
||||
ws.logger.error('send_open_event error: ${err}')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
module websocket
|
||||
|
||||
import encoding.base64
|
||||
import strings
|
||||
|
||||
// handshake manages the websocket handshake process
|
||||
fn (mut ws Client) handshake() ! {
|
||||
nonce := get_nonce(ws.nonce_size)
|
||||
seckey := base64.encode_str(nonce)
|
||||
mut sb := strings.new_builder(1024)
|
||||
defer {
|
||||
unsafe { sb.free() }
|
||||
}
|
||||
sb.write_string('GET ')
|
||||
sb.write_string(ws.uri.resource)
|
||||
sb.write_string(ws.uri.querystring)
|
||||
sb.write_string(' HTTP/1.1\r\nHost: ')
|
||||
sb.write_string(ws.uri.hostname)
|
||||
sb.write_string(':')
|
||||
sb.write_string(ws.uri.port)
|
||||
sb.write_string('\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n')
|
||||
sb.write_string('Sec-WebSocket-Key: ')
|
||||
sb.write_string(seckey)
|
||||
sb.write_string('\r\nSec-WebSocket-Version: 13')
|
||||
for key in ws.header.keys() {
|
||||
val := ws.header.custom_values(key).join(',')
|
||||
sb.write_string('\r\n${key}: ${val}')
|
||||
}
|
||||
sb.write_string('\r\n\r\n')
|
||||
handshake := sb.str()
|
||||
handshake_bytes := handshake.bytes()
|
||||
ws.debug_log('sending handshake: ${handshake}')
|
||||
ws.socket_write(handshake_bytes)!
|
||||
ws.read_handshake(seckey)!
|
||||
}
|
||||
|
||||
// handle_server_handshake manages websocket server handshake process
|
||||
fn (mut s Server) handle_server_handshake(mut c Client) !(string, &ServerClient) {
|
||||
msg := c.read_handshake_str()!
|
||||
handshake_response, client := s.parse_client_handshake(msg, mut c)!
|
||||
return handshake_response, client
|
||||
}
|
||||
|
||||
// parse_client_handshake parses result from handshake process
|
||||
fn (mut s Server) parse_client_handshake(client_handshake string, mut c Client) !(string, &ServerClient) {
|
||||
s.logger.debug('server-> client handshake:\n${client_handshake}')
|
||||
lines := client_handshake.split_into_lines()
|
||||
get_tokens := lines[0].split(' ')
|
||||
if get_tokens.len < 3 {
|
||||
return error_with_code('unexpected get operation, ${get_tokens}', 1)
|
||||
}
|
||||
if get_tokens[0].trim_space() != 'GET' {
|
||||
return error_with_code("unexpected request '${get_tokens[0]}', expected 'GET'", 2)
|
||||
}
|
||||
if get_tokens[2].trim_space() != 'HTTP/1.1' {
|
||||
return error_with_code("unexpected request ${get_tokens}, expected 'HTTP/1.1'", 3)
|
||||
}
|
||||
mut seckey := ''
|
||||
mut flags := []Flag{}
|
||||
mut key := ''
|
||||
for i in 1 .. lines.len {
|
||||
if lines[i].len <= 0 || lines[i] == '\r\n' {
|
||||
continue
|
||||
}
|
||||
keys := lines[i].split(':').map(it.trim_space())
|
||||
match keys[0].to_lower() {
|
||||
'upgrade' {
|
||||
flags << .has_upgrade
|
||||
}
|
||||
'connection' {
|
||||
flags << .has_connection
|
||||
}
|
||||
'sec-websocket-key' {
|
||||
key = keys[1]
|
||||
s.logger.debug('server-> got key: ${key}')
|
||||
seckey = create_key_challenge_response(key)!
|
||||
s.logger.debug('server-> challenge: ${seckey}, response: ${key}')
|
||||
flags << .has_accept
|
||||
}
|
||||
else {
|
||||
// we ignore other headers like protocol for now
|
||||
}
|
||||
}
|
||||
}
|
||||
if flags.len < 3 {
|
||||
return error_with_code('invalid client handshake, ${client_handshake}', 4)
|
||||
}
|
||||
server_handshake := 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${seckey}\r\n\r\n'
|
||||
server_client := &ServerClient{
|
||||
resource_name: get_tokens[1]
|
||||
client_key: key
|
||||
client: unsafe { c }
|
||||
server: unsafe { s }
|
||||
}
|
||||
return server_handshake, server_client
|
||||
}
|
||||
|
||||
// read_handshake_str returns the handshake response
|
||||
fn (mut ws Client) read_handshake_str() !string {
|
||||
mut total_bytes_read := 0
|
||||
mut msg := [1024]u8{}
|
||||
mut buffer := [1]u8{}
|
||||
for total_bytes_read < 1024 {
|
||||
bytes_read := ws.socket_read_ptr(&buffer[0], 1)!
|
||||
if bytes_read == 0 {
|
||||
return error_with_code('unexpected no response from handshake', 5)
|
||||
}
|
||||
msg[total_bytes_read] = buffer[0]
|
||||
total_bytes_read++
|
||||
if total_bytes_read > 5 && msg[total_bytes_read - 1] == `\n`
|
||||
&& msg[total_bytes_read - 2] == `\r` && msg[total_bytes_read - 3] == `\n`
|
||||
&& msg[total_bytes_read - 4] == `\r` {
|
||||
break
|
||||
}
|
||||
}
|
||||
res := msg[..total_bytes_read].bytestr()
|
||||
return res
|
||||
}
|
||||
|
||||
// read_handshake reads the handshake result and check if valid
|
||||
fn (mut ws Client) read_handshake(seckey string) ! {
|
||||
mut msg := ws.read_handshake_str()!
|
||||
ws.check_handshake_response(msg, seckey)!
|
||||
}
|
||||
|
||||
// check_handshake_response checks the response from handshake and returns
|
||||
// the response and secure key provided by the websocket client
|
||||
fn (mut ws Client) check_handshake_response(handshake_response string, seckey string) ! {
|
||||
ws.debug_log('handshake response:\n${handshake_response}')
|
||||
lines := handshake_response.split_into_lines()
|
||||
header := lines[0]
|
||||
if !header.starts_with('HTTP/1.1 101') && !header.starts_with('HTTP/1.0 101') {
|
||||
return error_with_code('handshake_handler: invalid HTTP status response code, ${header}', 6)
|
||||
}
|
||||
for i in 1 .. lines.len {
|
||||
if lines[i].len <= 0 || lines[i] == '\r\n' {
|
||||
continue
|
||||
}
|
||||
keys := lines[i].split(':').map(it.trim_space())
|
||||
match keys[0].to_lower() {
|
||||
'upgrade' {
|
||||
ws.flags << .has_upgrade
|
||||
}
|
||||
'connection' {
|
||||
ws.flags << .has_connection
|
||||
}
|
||||
'sec-websocket-accept' {
|
||||
ws.debug_log('seckey: ${seckey}')
|
||||
challenge := create_key_challenge_response(seckey)!
|
||||
ws.debug_log('challenge: ${challenge}, response: ${keys[1]}')
|
||||
if keys[1].trim_space() != challenge {
|
||||
return error_with_code('handshake_handler: Sec-WebSocket-Accept header does not match computed sha1/base64 response.',
|
||||
7)
|
||||
}
|
||||
ws.flags << .has_accept
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
if ws.flags.len < 3 {
|
||||
ws.close(1002, 'invalid websocket HTTP headers')!
|
||||
return error_with_code('invalid websocket HTTP headers', 8)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
module websocket
|
||||
|
||||
import net
|
||||
import net.http
|
||||
|
||||
// socket_read reads from socket into the provided buffer
|
||||
fn (mut ws Client) socket_read(mut buffer []u8) !int {
|
||||
lock {
|
||||
if ws.get_state() in [.closed, .closing] || ws.conn.sock.handle <= 1 {
|
||||
return error('socket_read: trying to read a closed socket')
|
||||
}
|
||||
if ws.is_ssl {
|
||||
r := ws.ssl_conn.read(mut buffer) or { return error('none') }
|
||||
return r
|
||||
} else {
|
||||
r := ws.conn.read(mut buffer) or { return error('none') }
|
||||
return r
|
||||
}
|
||||
}
|
||||
return error('none')
|
||||
}
|
||||
|
||||
// socket_read reads from socket into the provided byte pointer and length
|
||||
fn (mut ws Client) socket_read_ptr(buf_ptr &u8, len int) !int {
|
||||
lock {
|
||||
if ws.get_state() in [.closed, .closing] || ws.conn.sock.handle <= 1 {
|
||||
return error('socket_read_ptr: trying to read a closed socket')
|
||||
}
|
||||
if ws.is_ssl {
|
||||
r := ws.ssl_conn.socket_read_into_ptr(buf_ptr, len)!
|
||||
return r
|
||||
} else {
|
||||
r := ws.conn.read_ptr(buf_ptr, len)!
|
||||
return r
|
||||
}
|
||||
}
|
||||
return error('none')
|
||||
}
|
||||
|
||||
// socket_write writes the provided byte array to the socket
|
||||
fn (mut ws Client) socket_write(bytes []u8) !int {
|
||||
lock {
|
||||
if ws.get_state() == .closed || ws.conn.sock.handle <= 1 {
|
||||
ws.debug_log('socket_write: Socket already closed')
|
||||
return error('socket_write: trying to write on a closed socket')
|
||||
}
|
||||
if ws.is_ssl {
|
||||
return ws.ssl_conn.write(bytes)
|
||||
} else {
|
||||
for {
|
||||
n := ws.conn.write(bytes) or {
|
||||
if err.code() == net.err_timed_out_code {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return n
|
||||
}
|
||||
panic('reached unreachable code')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown_socket shuts down the socket properly when connection is closed
|
||||
fn (mut ws Client) shutdown_socket() ! {
|
||||
ws.debug_log('shutting down socket')
|
||||
if ws.is_ssl {
|
||||
ws.ssl_conn.shutdown()!
|
||||
} else {
|
||||
ws.conn.close()!
|
||||
}
|
||||
}
|
||||
|
||||
// dial_socket connects tcp socket and initializes default configurations
|
||||
fn (mut ws Client) dial_socket() !&net.TcpConn {
|
||||
tcp_address := '${ws.uri.hostname}:${ws.uri.port}'
|
||||
mut t := if ws.proxy_url == '' {
|
||||
net.dial_tcp(tcp_address)!
|
||||
} else {
|
||||
http.dial_tcp_via_proxy(ws.proxy_url, tcp_address)!
|
||||
}
|
||||
optval := int(1)
|
||||
t.sock.set_option_int(.keep_alive, optval)!
|
||||
t.set_read_timeout(ws.read_timeout)
|
||||
t.set_write_timeout(ws.write_timeout)
|
||||
if ws.is_ssl {
|
||||
ws.ssl_conn.connect(mut t, ws.uri.hostname)!
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
module websocket
|
||||
|
||||
import encoding.utf8
|
||||
|
||||
const header_len_offset = 2 // offset for lengthpart of websocket header
|
||||
|
||||
const buffer_size = 256 // default buffer size
|
||||
|
||||
const extended_payload16_end_byte = 4 // header length with 16-bit extended payload
|
||||
|
||||
const extended_payload64_end_byte = 10
|
||||
|
||||
// Fragment represents a websocket data fragment
|
||||
struct Fragment {
|
||||
data []u8 // included data payload data in a fragment
|
||||
opcode OPCode // interpretation of the payload data
|
||||
}
|
||||
|
||||
// Frame represents a data frame header
|
||||
struct Frame {
|
||||
mut:
|
||||
// length of the websocket header part
|
||||
header_len int = 2
|
||||
// size of total frame
|
||||
frame_size int = 2
|
||||
fin bool // true if final fragment of message
|
||||
rsv1 bool // reserved for future use in websocket RFC
|
||||
rsv2 bool // reserved for future use in websocket RFC
|
||||
rsv3 bool // reserved for future use in websocket RFC
|
||||
opcode OPCode // interpretation of the payload data
|
||||
has_mask bool // true if the payload data is masked
|
||||
payload_len int // payload length
|
||||
masking_key [4]u8 // all frames from client to server is masked with this key
|
||||
}
|
||||
|
||||
const invalid_close_codes = [999, 1004, 1005, 1006, 1014, 1015, 1016, 1100, 2000, 2999, 5000, 65536]
|
||||
|
||||
// validate_client validates client frame rules from RFC6455
|
||||
pub fn (mut ws Client) validate_frame(frame &Frame) ! {
|
||||
if frame.rsv1 || frame.rsv2 || frame.rsv3 {
|
||||
ws.close(1002, 'rsv cannot be other than 0, not negotiated')!
|
||||
return error('rsv cannot be other than 0, not negotiated')
|
||||
}
|
||||
if (int(frame.opcode) >= 3 && int(frame.opcode) <= 7)
|
||||
|| (int(frame.opcode) >= 11 && int(frame.opcode) <= 15) {
|
||||
ws.close(1002, 'use of reserved opcode')!
|
||||
return error('use of reserved opcode')
|
||||
}
|
||||
if frame.has_mask && !ws.is_server {
|
||||
// server should never send masked frames
|
||||
// to client, close connection
|
||||
ws.close(1002, 'client got masked frame')!
|
||||
return error('client sent masked frame')
|
||||
}
|
||||
if is_control_frame(frame.opcode) {
|
||||
if !frame.fin {
|
||||
ws.close(1002, 'control message must not be fragmented')!
|
||||
return error('unexpected control frame with no fin')
|
||||
}
|
||||
if frame.payload_len > 125 {
|
||||
ws.close(1002, 'control frames must not exceed 125 bytes')!
|
||||
return error('unexpected control frame payload length')
|
||||
}
|
||||
}
|
||||
if frame.fin == false && ws.fragments.len == 0 && frame.opcode == .continuation {
|
||||
err_msg := 'unexecpected continuation, there are no frames to continue, ${frame}'
|
||||
ws.close(1002, err_msg)!
|
||||
return error(err_msg)
|
||||
}
|
||||
}
|
||||
|
||||
// is_control_frame returns true if the frame is a control frame
|
||||
fn is_control_frame(opcode OPCode) bool {
|
||||
return opcode !in [.text_frame, .binary_frame, .continuation]
|
||||
}
|
||||
|
||||
// is_data_frame returns true if the frame is a control frame
|
||||
fn is_data_frame(opcode OPCode) bool {
|
||||
return opcode in [.text_frame, .binary_frame]
|
||||
}
|
||||
|
||||
// read_payload reads the message payload from the socket
|
||||
fn (mut ws Client) read_payload(frame &Frame) ![]u8 {
|
||||
if frame.payload_len == 0 {
|
||||
return []u8{}
|
||||
}
|
||||
mut buffer := []u8{cap: frame.payload_len}
|
||||
mut read_buf := [1]u8{}
|
||||
mut bytes_read := 0
|
||||
for bytes_read < frame.payload_len {
|
||||
len := ws.socket_read_ptr(&read_buf[0], 1)!
|
||||
if len != 1 {
|
||||
return error('expected read all message, got zero')
|
||||
}
|
||||
bytes_read += len
|
||||
buffer << read_buf[0]
|
||||
}
|
||||
if bytes_read != frame.payload_len {
|
||||
return error('failed to read payload')
|
||||
}
|
||||
if frame.has_mask {
|
||||
for i in 0 .. frame.payload_len {
|
||||
buffer[i] ^= frame.masking_key[i % 4] & 0xff
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
// validate_utf_8 validates payload for valid utf8 encoding
|
||||
// - Future implementation needs to support fail fast utf errors for strict autobahn conformance
|
||||
fn (mut ws Client) validate_utf_8(opcode OPCode, payload []u8) ! {
|
||||
if opcode in [.text_frame, .close] && !utf8.validate(payload.data, payload.len) {
|
||||
ws.logger.error('malformed utf8 payload, payload len: (${payload.len})')
|
||||
ws.send_error_event('Received malformed utf8.')
|
||||
ws.close(1007, 'malformed utf8 payload')!
|
||||
return error('malformed utf8 payload')
|
||||
}
|
||||
}
|
||||
|
||||
// read_next_message reads 1 to n frames to compose a message
|
||||
pub fn (mut ws Client) read_next_message() !Message {
|
||||
for {
|
||||
frame := ws.parse_frame_header()!
|
||||
ws.validate_frame(&frame)!
|
||||
frame_payload := ws.read_payload(&frame)!
|
||||
if is_control_frame(frame.opcode) {
|
||||
// Control frames can interject other frames
|
||||
// and need to be returned immediately
|
||||
msg := Message{
|
||||
opcode: OPCode(frame.opcode)
|
||||
payload: frame_payload.clone()
|
||||
}
|
||||
unsafe { frame_payload.free() }
|
||||
return msg
|
||||
}
|
||||
// if the message is fragmented we just put it on fragments
|
||||
// a fragment is allowed to have zero size payload
|
||||
if !frame.fin {
|
||||
ws.fragments << &Fragment{
|
||||
data: frame_payload.clone()
|
||||
opcode: frame.opcode
|
||||
}
|
||||
unsafe { frame_payload.free() }
|
||||
continue
|
||||
}
|
||||
if ws.fragments.len == 0 {
|
||||
ws.validate_utf_8(frame.opcode, frame_payload) or {
|
||||
ws.logger.error('UTF8 validation error: ${err}, len of payload(${frame_payload.len})')
|
||||
ws.send_error_event('UTF8 validation error: ${err}, len of payload(${frame_payload.len})')
|
||||
return err
|
||||
}
|
||||
msg := Message{
|
||||
opcode: OPCode(frame.opcode)
|
||||
payload: frame_payload.clone()
|
||||
}
|
||||
unsafe { frame_payload.free() }
|
||||
return msg
|
||||
}
|
||||
defer {
|
||||
ws.fragments = []
|
||||
}
|
||||
if is_data_frame(frame.opcode) {
|
||||
ws.close(0, '')!
|
||||
return error('Unexpected frame opcode')
|
||||
}
|
||||
payload := ws.payload_from_fragments(frame_payload)!
|
||||
opcode := ws.opcode_from_fragments()
|
||||
ws.validate_utf_8(opcode, payload)!
|
||||
msg := Message{
|
||||
opcode: opcode
|
||||
payload: payload.clone()
|
||||
}
|
||||
unsafe {
|
||||
frame_payload.free()
|
||||
payload.free()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
return error('none')
|
||||
}
|
||||
|
||||
// payload_from_fragments returns the whole payload from fragmented message
|
||||
fn (ws &Client) payload_from_fragments(fin_payload []u8) ![]u8 {
|
||||
mut total_size := 0
|
||||
for f in ws.fragments {
|
||||
if f.data.len > 0 {
|
||||
total_size += f.data.len
|
||||
}
|
||||
}
|
||||
total_size += fin_payload.len
|
||||
if total_size == 0 {
|
||||
return []u8{}
|
||||
}
|
||||
mut total_buffer := []u8{cap: total_size}
|
||||
for f in ws.fragments {
|
||||
if f.data.len > 0 {
|
||||
total_buffer << f.data
|
||||
}
|
||||
}
|
||||
total_buffer << fin_payload
|
||||
return total_buffer
|
||||
}
|
||||
|
||||
// opcode_from_fragments returns the opcode for message from the first fragment sent
|
||||
fn (ws &Client) opcode_from_fragments() OPCode {
|
||||
return OPCode(ws.fragments[0].opcode)
|
||||
}
|
||||
|
||||
// parse_frame_header parses next message by decoding the incoming frames
|
||||
pub fn (mut ws Client) parse_frame_header() !Frame {
|
||||
mut buffer := [256]u8{}
|
||||
mut bytes_read := 0
|
||||
mut frame := Frame{}
|
||||
mut rbuff := [1]u8{}
|
||||
mut mask_end_byte := 0
|
||||
for ws.get_state() == .open {
|
||||
read_bytes := ws.socket_read_ptr(&rbuff[0], 1)!
|
||||
if read_bytes == 0 {
|
||||
return error('websocket peer closed connection')
|
||||
}
|
||||
buffer[bytes_read] = rbuff[0]
|
||||
bytes_read++
|
||||
// parses the first two header bytes to get basic frame information
|
||||
if bytes_read == header_len_offset {
|
||||
frame.fin = (buffer[0] & 0x80) == 0x80
|
||||
frame.rsv1 = (buffer[0] & 0x40) == 0x40
|
||||
frame.rsv2 = (buffer[0] & 0x20) == 0x20
|
||||
frame.rsv3 = (buffer[0] & 0x10) == 0x10
|
||||
frame.opcode = unsafe { OPCode(int(buffer[0] & 0x7F)) }
|
||||
frame.has_mask = (buffer[1] & 0x80) == 0x80
|
||||
frame.payload_len = buffer[1] & 0x7F
|
||||
// if the frame has a mask, set the byte position where the mask ends
|
||||
if frame.has_mask {
|
||||
mask_end_byte = if frame.payload_len < 126 {
|
||||
header_len_offset + 4
|
||||
} else if frame.payload_len == 126 {
|
||||
header_len_offset + 6
|
||||
} else if frame.payload_len == 127 {
|
||||
header_len_offset + 12
|
||||
} else {
|
||||
0
|
||||
} // impossible
|
||||
}
|
||||
frame.payload_len = frame.payload_len
|
||||
frame.frame_size = frame.header_len + frame.payload_len
|
||||
if !frame.has_mask && frame.payload_len < 126 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if frame.payload_len == 126 && bytes_read == extended_payload16_end_byte {
|
||||
frame.header_len += 2
|
||||
frame.payload_len = 0
|
||||
frame.payload_len |= int(u32(buffer[2]) << 8)
|
||||
frame.payload_len |= int(buffer[3])
|
||||
frame.frame_size = frame.header_len + frame.payload_len
|
||||
if !frame.has_mask {
|
||||
break
|
||||
}
|
||||
}
|
||||
if frame.payload_len == 127 && bytes_read == extended_payload64_end_byte {
|
||||
frame.header_len += 8
|
||||
// these shift operators needs 64 bit on clang with -prod flag
|
||||
mut payload_len := u64(0)
|
||||
payload_len |= u64(buffer[2]) << 56
|
||||
payload_len |= u64(buffer[3]) << 48
|
||||
payload_len |= u64(buffer[4]) << 40
|
||||
payload_len |= u64(buffer[5]) << 32
|
||||
payload_len |= u64(buffer[6]) << 24
|
||||
payload_len |= u64(buffer[7]) << 16
|
||||
payload_len |= u64(buffer[8]) << 8
|
||||
payload_len |= u64(buffer[9])
|
||||
frame.payload_len = int(payload_len)
|
||||
if !frame.has_mask {
|
||||
break
|
||||
}
|
||||
}
|
||||
if frame.has_mask && bytes_read == mask_end_byte {
|
||||
frame.masking_key[0] = buffer[mask_end_byte - 4]
|
||||
frame.masking_key[1] = buffer[mask_end_byte - 3]
|
||||
frame.masking_key[2] = buffer[mask_end_byte - 2]
|
||||
frame.masking_key[3] = buffer[mask_end_byte - 1]
|
||||
break
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
// unmask_sequence unmask any given sequence
|
||||
fn (f &Frame) unmask_sequence(mut buffer []u8) {
|
||||
for i in 0 .. buffer.len {
|
||||
buffer[i] ^= f.masking_key[i % 4] & 0xff
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Autobahn tests
|
||||
|
||||
This is the autobahn automatic tests on build.
|
||||
The performance tests are skipped due to timeouts in Github actions.
|
||||
|
||||
## Run it locally
|
||||
|
||||
### Test the client
|
||||
|
||||
This is how to test the client:
|
||||
|
||||
1. Run the docker autobahn test suite by running the `docker compose up`
|
||||
2. From the `local_run` folder, compile and run `autobahn_client.v` to test non wss (no TLS) and
|
||||
`autobahn_client_wss.v` to run the TLS tests
|
||||
3. Open `http://localhost:8080` and browse client test results for non TLS and `https://localhost:8081`
|
||||
if you ran the wss tests (it uses local certificate so you will get trust error but just accept use)
|
||||
|
||||
### Test the server
|
||||
|
||||
Todo: add information here
|
||||
@@ -0,0 +1,33 @@
|
||||
// use this test to test the websocket client in the autobahn test
|
||||
module main
|
||||
|
||||
import net.websocket
|
||||
|
||||
fn main() {
|
||||
for i in 1 .. 304 {
|
||||
println('\ncase: ${i}')
|
||||
handle_case(i) or { println('error should be ok: ${err}') }
|
||||
}
|
||||
// update the reports
|
||||
uri := 'ws://autobahn_server:9001/updateReports?agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn handle_case(case_nr int) ! {
|
||||
uri := 'ws://autobahn_server:9001/runCase?case=${case_nr}&agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.on_message(on_message)
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn on_message(mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
// autobahn tests expects to send same message back
|
||||
if msg.opcode == .pong {
|
||||
// We just wanna pass text and binary message back to autobahn
|
||||
return
|
||||
}
|
||||
ws.write(msg.payload, msg.opcode) or { panic(err) }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// use this test to test the websocket client in the autobahn test
|
||||
module main
|
||||
|
||||
import net.websocket
|
||||
|
||||
fn main() {
|
||||
for i in 1 .. 304 {
|
||||
println('\ncase: ${i}')
|
||||
handle_case(i) or { println('error should be ok: ${err}') }
|
||||
}
|
||||
// update the reports
|
||||
// uri := 'wss://localhost:9002/updateReports?agent=v-client'
|
||||
uri := 'wss://autobahn_server_wss:9002/updateReports?agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn handle_case(case_nr int) ! {
|
||||
uri := 'wss://autobahn_server_wss:9002/runCase?case=${case_nr}&agent=v-client'
|
||||
// uri := 'wss://localhost:9002/runCase?case=${case_nr}&agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.on_message(on_message)
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn on_message(mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
// autobahn tests expects to send same message back
|
||||
if msg.opcode == .pong {
|
||||
// We just wanna pass text and binary message back to autobahn
|
||||
return
|
||||
}
|
||||
ws.write(msg.payload, msg.opcode) or { panic(err) }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// use this to test websocket server to the autobahn test
|
||||
module main
|
||||
|
||||
import net.websocket
|
||||
|
||||
fn main() {
|
||||
mut s := websocket.new_server(.ip6, 9002, '/')
|
||||
s.on_message(on_message)
|
||||
s.listen() or { panic(err) }
|
||||
}
|
||||
|
||||
fn handle_case(case_nr int) ! {
|
||||
uri := 'ws://localhost:9002/runCase?case=${case_nr}&agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.on_message(on_message)
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn on_message(mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
// autobahn tests expects to send same message back
|
||||
if msg.opcode == .pong {
|
||||
// We just wanna pass text and binary message back to autobahn
|
||||
return
|
||||
}
|
||||
ws.write(msg.payload, msg.opcode) or { panic(err) }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
version: "3"
|
||||
services:
|
||||
server:
|
||||
container_name: autobahn_server
|
||||
build: fuzzing_server
|
||||
|
||||
ports:
|
||||
- "9001:9001"
|
||||
- "8080:8080"
|
||||
server_wss:
|
||||
container_name: autobahn_server_wss
|
||||
build: fuzzing_server_wss
|
||||
|
||||
ports:
|
||||
- "9002:9002"
|
||||
- "8081:8080"
|
||||
client:
|
||||
container_name: autobahn_client
|
||||
build:
|
||||
dockerfile: vlib/net/websocket/tests/autobahn/ws_test/Dockerfile
|
||||
context: ../../../../../
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM crossbario/autobahn-testsuite
|
||||
COPY check_results.py /check_results.py
|
||||
RUN chmod +x /check_results.py
|
||||
|
||||
COPY config /config
|
||||
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
|
||||
nr_of_client_errs = 0
|
||||
nr_of_client_tests = 0
|
||||
|
||||
nr_of_server_errs = 0
|
||||
nr_of_server_tests = 0
|
||||
|
||||
with open("/reports/clients/index.json") as f:
|
||||
data = json.load(f)
|
||||
|
||||
for i in data["v-client"]:
|
||||
# Count errors
|
||||
if (
|
||||
data["v-client"][i]["behavior"] == "FAILED"
|
||||
or data["v-client"][i]["behaviorClose"] == "FAILED"
|
||||
):
|
||||
nr_of_client_errs = nr_of_client_errs + 1
|
||||
|
||||
nr_of_client_tests = nr_of_client_tests + 1
|
||||
|
||||
with open("/reports/servers/index.json") as f:
|
||||
data = json.load(f)
|
||||
|
||||
for i in data["AutobahnServer"]:
|
||||
if (
|
||||
data["AutobahnServer"][i]["behavior"] == "FAILED"
|
||||
or data["AutobahnServer"][i]["behaviorClose"] == "FAILED"
|
||||
):
|
||||
nr_of_server_errs = nr_of_server_errs + 1
|
||||
|
||||
nr_of_server_tests = nr_of_server_tests + 1
|
||||
|
||||
if nr_of_client_errs > 0 or nr_of_server_errs > 0:
|
||||
print(
|
||||
"FAILED AUTOBAHN TESTS, CLIENT ERRORS {0}(of {1}), SERVER ERRORS {2}(of {3})".format(
|
||||
nr_of_client_errs, nr_of_client_tests, nr_of_server_errs, nr_of_server_tests
|
||||
)
|
||||
)
|
||||
exit(1)
|
||||
|
||||
print(
|
||||
"TEST SUCCESS!, CLIENT TESTS({0}), SERVER TESTS ({1})".format(
|
||||
nr_of_client_tests, nr_of_server_tests
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"options": {
|
||||
"failByDrop": false
|
||||
},
|
||||
"outdir": "./reports/servers",
|
||||
"servers": [
|
||||
{
|
||||
"agent": "AutobahnServer",
|
||||
"url": "ws://autobahn_client:9002"
|
||||
}
|
||||
],
|
||||
"cases": [
|
||||
"*"
|
||||
],
|
||||
"exclude-cases": [
|
||||
"9.*",
|
||||
"11.*",
|
||||
"12.*",
|
||||
"13.*"
|
||||
],
|
||||
"exclude-agent-cases": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"url": "ws://127.0.0.1:9001",
|
||||
"outdir": "./reports/clients",
|
||||
"cases": [
|
||||
"*"
|
||||
],
|
||||
"exclude-cases": [
|
||||
"9.*",
|
||||
"11.*",
|
||||
"12.*",
|
||||
"13.*"
|
||||
],
|
||||
"exclude-agent-cases": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM crossbario/autobahn-testsuite
|
||||
COPY check_results.py /check_results.py
|
||||
RUN chmod +x /check_results.py
|
||||
|
||||
COPY config /config
|
||||
RUN chmod +rx /config/server.crt
|
||||
RUN chmod +rx /config/server.key
|
||||
|
||||
EXPOSE 9002 9002
|
||||
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
|
||||
nr_of_client_errs = 0
|
||||
nr_of_client_tests = 0
|
||||
|
||||
nr_of_server_errs = 0
|
||||
nr_of_server_tests = 0
|
||||
|
||||
with open("/reports/clients/index.json") as f:
|
||||
data = json.load(f)
|
||||
|
||||
for i in data["v-client"]:
|
||||
# Count errors
|
||||
if (
|
||||
data["v-client"][i]["behavior"] == "FAILED"
|
||||
or data["v-client"][i]["behaviorClose"] == "FAILED"
|
||||
):
|
||||
nr_of_client_errs = nr_of_client_errs + 1
|
||||
|
||||
nr_of_client_tests = nr_of_client_tests + 1
|
||||
|
||||
|
||||
if nr_of_client_errs > 0 or nr_of_server_errs > 0:
|
||||
print(
|
||||
"FAILED AUTOBAHN TESTS, CLIENT ERRORS {0}(of {1}), SERVER ERRORS {2}(of {3})".format(
|
||||
nr_of_client_errs, nr_of_client_tests, nr_of_server_errs, nr_of_server_tests
|
||||
)
|
||||
)
|
||||
exit(1)
|
||||
|
||||
print(
|
||||
"TEST SUCCESS!, CLIENT TESTS({0}), SERVER TESTS ({1})".format(
|
||||
nr_of_client_tests, nr_of_server_tests
|
||||
)
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"url": "wss://127.0.0.1:9002",
|
||||
"outdir": "./reports/clients",
|
||||
"key": "/config/server.key",
|
||||
"cert": "/config/server.crt",
|
||||
"cases": [
|
||||
"*"
|
||||
],
|
||||
"exclude-cases": [
|
||||
"9.*",
|
||||
"11.*",
|
||||
"12.*",
|
||||
"13.*"
|
||||
],
|
||||
"exclude-agent-cases": {}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDETCCAfkCFAtFKlcdB3jhD+AXPul81dwmZcs/MA0GCSqGSIb3DQEBCwUAMEUx
|
||||
CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
|
||||
cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjAxMTIxMDgyNjQ5WhcNMzAxMTE5MDgy
|
||||
NjQ5WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
|
||||
CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAnbysLfcIr9+wpoJjb5r728j2e07agedOzh8VLuGnHqmKOUPN
|
||||
f8Ik707kEoBcFY7UM2A9G/1RMIysGp8eleQLMtNdeYc3KlKHBGFrOM3i4gCd7G44
|
||||
lERuKP1PKzRQ6RdVNUXn51XjfxjHWo7kHCEVvZowxvzxLxhwbSwmEmgzcQ1T6vj6
|
||||
Cdop87sdq00F+eOCfTdy+cl+R65sbImVdfY4EQ0QWAVdF3X6njLjpdmteppggbEa
|
||||
ECv3R3qNIV7/rflIPm1efbqp7R1ugvjLPJZ1u12ovtqkgsWbnEyzST8hbEEjsOTJ
|
||||
/cPkH2DaLdh7fMgfcVmqnYXd9T+gpsNGv98DjwIDAQABMA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQBG9GxUOjcrFd1ept9AOTzbxvIUvBiqIEzrL2/+3T1yPPAWQzOmBfZhIVVm
|
||||
EZeeU3xcvd7+AmX+2FPCAD+evjSHjKY048X1YksQS7mYChSgeJiknoJi3mAEAyw6
|
||||
oYGVkETksZLQfXtWTjgljbIQrwTA1s+EW0jvmvaJnWD3/8nFqmfly2/kxVsTcGEa
|
||||
wJGEUS53Cq6y6lLZ+ojjjj1iVCQ94U6L/0xPB9hgXOyL2+iQj+n38ruatnUNF77C
|
||||
UKS7N9BFF42eqVY83Xab0m25s93m8Z7J/63qu0eeA8p5t7+8lbGvOYpwReknLRMf
|
||||
pJfgSEWqWfSaetihbJl2Fmzg2SeJ
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,16 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
|
||||
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBAJ28rC33CK/fsKaCY2+a+9vI9ntO2oHnTs4fFS7h
|
||||
px6pijlDzX/CJO9O5BKAXBWO1DNgPRv9UTCMrBqfHpXkCzLTXXmHNypShwRhazjN
|
||||
4uIAnexuOJREbij9Tys0UOkXVTVF5+dV438Yx1qO5BwhFb2aMMb88S8YcG0sJhJo
|
||||
M3ENU+r4+gnaKfO7HatNBfnjgn03cvnJfkeubGyJlXX2OBENEFgFXRd1+p4y46XZ
|
||||
rXqaYIGxGhAr90d6jSFe/635SD5tXn26qe0dboL4yzyWdbtdqL7apILFm5xMs0k/
|
||||
IWxBI7Dkyf3D5B9g2i3Ye3zIH3FZqp2F3fU/oKbDRr/fA48CAwEAAaAAMA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQARfNhaiioyJPZZ8Hkf9UPbi85djYLDYCC9EqBPHpYpGh15
|
||||
WdRsTModg/X5DeGwtWwRyGSP2ROMWa1NB5RHZ9buIgCIOeszhAvXVaQMlHmpNhSD
|
||||
/hWKGGpAEq12TKHxgi9eTOE2u9MhoJf1G6iGffVsHc8r52THvGqKBp3Bi8G1Pl6L
|
||||
2J1f5qX42K1DEnCx0gGnQkydO6E4UnMbsaDSFSODQwg5LpzSYoYUfpYHstMpqAqL
|
||||
rcEt869YKjemKuTCzHODWxfqlvVr9GctNjKG2WtoqnX+10x3tw/9lsNRKUelCQxb
|
||||
E56eujAoQdMxQ4OjwSnc/gbpWa5gXKYjpgAfx2kY
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@@ -0,0 +1,9 @@
|
||||
# Use this as docker builder with https://github.com/nektos/act
|
||||
# build with: docker build tests/autobahn/. -t myimage
|
||||
# use in act: act -P ubuntu-latest=myimage
|
||||
|
||||
FROM node:12.6-buster-slim
|
||||
|
||||
COPY config/fuzzingserver.json /config/fuzzingserver.json
|
||||
RUN chmod +775 /config/fuzzingserver.json
|
||||
RUN apt update && apt install -y docker
|
||||
@@ -0,0 +1,33 @@
|
||||
// use this test to test the websocket client in the autobahn test
|
||||
module main
|
||||
|
||||
import net.websocket
|
||||
|
||||
fn main() {
|
||||
for i in 1 .. 304 {
|
||||
println('\ncase: ${i}')
|
||||
handle_case(i) or { println('error should be ok: ${err}') }
|
||||
}
|
||||
// update the reports
|
||||
uri := 'ws://localhost:9001/updateReports?agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn handle_case(case_nr int) ! {
|
||||
uri := 'ws://localhost:9001/runCase?case=${case_nr}&agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.on_message(on_message)
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn on_message(mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
// autobahn tests expects to send same message back
|
||||
if msg.opcode == .pong {
|
||||
// We just wanna pass text and binary message back to autobahn
|
||||
return
|
||||
}
|
||||
ws.write(msg.payload, msg.opcode) or { panic(err) }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// use this test to test the websocket client in the autobahn test
|
||||
module main
|
||||
|
||||
import net.websocket
|
||||
|
||||
fn main() {
|
||||
for i in 1 .. 304 {
|
||||
println('\ncase: ${i}')
|
||||
handle_case(i) or { println('error should be ok: ${err}') }
|
||||
}
|
||||
// update the reports
|
||||
// uri := 'wss://localhost:9002/updateReports?agent=v-client'
|
||||
uri := 'wss://autobahn_server_wss:9002/updateReports?agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn handle_case(case_nr int) ! {
|
||||
uri := 'wss://autobahn_server_wss:9002/runCase?case=${case_nr}&agent=v-client'
|
||||
// uri := 'wss://localhost:9002/runCase?case=${case_nr}&agent=v-client'
|
||||
mut ws := websocket.new_client(uri)!
|
||||
ws.on_message(on_message)
|
||||
ws.connect()!
|
||||
ws.listen()!
|
||||
}
|
||||
|
||||
fn on_message(mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
// autobahn tests expects to send same message back
|
||||
if msg.opcode == .pong {
|
||||
// We just wanna pass text and binary message back to autobahn
|
||||
return
|
||||
}
|
||||
ws.write(msg.payload, msg.opcode) or { panic(err) }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM thevlang/vlang:buster-build
|
||||
|
||||
|
||||
COPY ./ /src/
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
RUN make CC=clang
|
||||
|
||||
RUN /src/v /src/vlib/net/websocket/tests/autobahn/autobahn_server.v
|
||||
RUN chmod +x /src/vlib/net/websocket/tests/autobahn/autobahn_server
|
||||
ENTRYPOINT [ "/src/vlib/net/websocket/tests/autobahn/autobahn_server" ]
|
||||
@@ -0,0 +1,16 @@
|
||||
module websocket
|
||||
|
||||
// Uri represents an Uri for websocket connections
|
||||
struct Uri {
|
||||
mut:
|
||||
url string // url to the websocket endpoint
|
||||
hostname string // hostname of the websocket endpoint
|
||||
port string // port of the websocket endpoint
|
||||
resource string // resource of the websocket endpoint
|
||||
querystring string // query string of the websocket endpoint
|
||||
}
|
||||
|
||||
// str returns the string representation of the Uri
|
||||
pub fn (u Uri) str() string {
|
||||
return u.url
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
module websocket
|
||||
|
||||
import rand
|
||||
import crypto.sha1
|
||||
import encoding.base64
|
||||
import encoding.binary
|
||||
import log
|
||||
|
||||
const default_logger = setup_default_logger()
|
||||
|
||||
fn setup_default_logger() &log.Log {
|
||||
mut l := &log.Log{}
|
||||
l.set_level(.info)
|
||||
return l
|
||||
}
|
||||
|
||||
// htonl64 converts payload length to header bits
|
||||
fn htonl64(payload_len u64) []u8 {
|
||||
mut ret := []u8{len: 8}
|
||||
binary.big_endian_put_u64(mut ret, payload_len)
|
||||
return ret
|
||||
}
|
||||
|
||||
// create_masking_key returns a new masking key to use when masking websocket messages
|
||||
fn create_masking_key() []u8 {
|
||||
return rand.bytes(4) or { [u8(0), 0, 0, 0] }
|
||||
}
|
||||
|
||||
// create_key_challenge_response creates a key challenge response from security key
|
||||
fn create_key_challenge_response(seckey string) !string {
|
||||
if seckey.len == 0 {
|
||||
return error('unexpected seckey length zero')
|
||||
}
|
||||
guid := '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
sha1buf := seckey + guid
|
||||
shabytes := sha1buf.bytes()
|
||||
hash := sha1.sum(shabytes)
|
||||
b64 := base64.encode(hash)
|
||||
unsafe {
|
||||
hash.free()
|
||||
shabytes.free()
|
||||
}
|
||||
return b64
|
||||
}
|
||||
|
||||
// get_nonce creates a randomized array used in handshake process
|
||||
fn get_nonce(nonce_size int) string {
|
||||
return rand.string_from_set('0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz',
|
||||
nonce_size)
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
// websocket module implements websocket client and a websocket server
|
||||
// attribution: @thecoderr the author of original websocket client
|
||||
@[manualfree]
|
||||
module websocket
|
||||
|
||||
import net
|
||||
import net.conv
|
||||
import net.http
|
||||
import net.ssl
|
||||
import net.urllib
|
||||
import time
|
||||
import log
|
||||
import rand
|
||||
|
||||
const empty_bytearr = []u8{}
|
||||
|
||||
pub struct ClientState {
|
||||
pub mut:
|
||||
state State = .closed // current state of connection
|
||||
}
|
||||
|
||||
// Client represents websocket client
|
||||
pub struct Client {
|
||||
is_server bool
|
||||
mut:
|
||||
ssl_conn &ssl.SSLConn = unsafe { nil } // secure connection used when wss is used
|
||||
proxy_url string
|
||||
flags []Flag // flags used in handshake
|
||||
fragments []Fragment // current fragments
|
||||
message_callbacks []MessageEventHandler // all callbacks on_message
|
||||
error_callbacks []ErrorEventHandler // all callbacks on_error
|
||||
open_callbacks []OpenEventHandler // all callbacks on_open
|
||||
close_callbacks []CloseEventHandler // all callbacks on_close
|
||||
pub:
|
||||
is_ssl bool // true if secure socket is used
|
||||
uri Uri // uri of current connection
|
||||
id string // unique id of client
|
||||
read_timeout i64
|
||||
write_timeout i64
|
||||
pub mut:
|
||||
header http.Header // headers that will be passed when connecting
|
||||
conn &net.TcpConn = unsafe { nil } // underlying TCP socket connection
|
||||
nonce_size int = 16 // size of nounce used for masking
|
||||
panic_on_callback bool // set to true of callbacks can panic
|
||||
client_state shared ClientState // current state of connection
|
||||
// logger used to log messages
|
||||
logger &log.Logger = default_logger
|
||||
resource_name string // name of current resource
|
||||
last_pong_ut i64 // last time in unix time we got a pong message
|
||||
}
|
||||
|
||||
// Flag represents different types of headers in websocket handshake
|
||||
enum Flag {
|
||||
has_accept // Webs
|
||||
has_connection
|
||||
has_upgrade
|
||||
}
|
||||
|
||||
// State represents the state of the websocket connection.
|
||||
pub enum State {
|
||||
connecting = 0
|
||||
open
|
||||
closing
|
||||
closed
|
||||
}
|
||||
|
||||
// Message represents a whole message combined from 1 to n frames
|
||||
pub struct Message {
|
||||
pub:
|
||||
opcode OPCode // websocket frame type of this message
|
||||
payload []u8 // payload of the message
|
||||
}
|
||||
|
||||
// OPCode represents the supported websocket frame types
|
||||
pub enum OPCode {
|
||||
continuation = 0x00
|
||||
text_frame = 0x01
|
||||
binary_frame = 0x02
|
||||
close = 0x08
|
||||
ping = 0x09
|
||||
pong = 0x0A
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ClientOpt {
|
||||
pub:
|
||||
read_timeout i64 = net.infinite_timeout
|
||||
write_timeout i64 = 30 * time.second
|
||||
logger &log.Logger = default_logger
|
||||
proxy_url string // optional proxy URL used to open the websocket TCP tunnel
|
||||
}
|
||||
|
||||
// new_client instance a new websocket client
|
||||
pub fn new_client(address string, opt ClientOpt) !&Client {
|
||||
uri := parse_uri(address)!
|
||||
return &Client{
|
||||
conn: unsafe { nil }
|
||||
is_server: false
|
||||
ssl_conn: ssl.new_ssl_conn()!
|
||||
is_ssl: address.starts_with('wss')
|
||||
logger: opt.logger
|
||||
uri: uri
|
||||
proxy_url: opt.proxy_url
|
||||
client_state: ClientState{
|
||||
state: .closed
|
||||
}
|
||||
id: rand.uuid_v4()
|
||||
header: http.new_header()
|
||||
read_timeout: opt.read_timeout
|
||||
write_timeout: opt.write_timeout
|
||||
}
|
||||
}
|
||||
|
||||
// connect connects to remote websocket server
|
||||
pub fn (mut ws Client) connect() ! {
|
||||
ws.assert_not_connected()!
|
||||
ws.set_state(.connecting)
|
||||
ws.logger.info('connecting to host ${ws.uri}')
|
||||
ws.conn = ws.dial_socket()!
|
||||
ws.handshake()!
|
||||
ws.set_state(.open)
|
||||
ws.logger.info('successfully connected to host ${ws.uri}')
|
||||
ws.send_open_event()
|
||||
}
|
||||
|
||||
// listen listens and processes incoming messages
|
||||
pub fn (mut ws Client) listen() ! {
|
||||
mut log_msg := 'Starting client listener, server(${ws.is_server})...'
|
||||
ws.logger.info(log_msg)
|
||||
unsafe { log_msg.free() }
|
||||
defer {
|
||||
ws.logger.info('Quit client listener, server(${ws.is_server})...')
|
||||
if ws.get_state() == .open {
|
||||
ws.close(1000, 'closed by client') or {}
|
||||
}
|
||||
}
|
||||
for ws.get_state() == .open {
|
||||
msg := ws.read_next_message() or {
|
||||
if err.code() == net.error_eintr { // Check for EINTR and retry
|
||||
continue
|
||||
} else if ws.get_state() in [.closed, .closing] {
|
||||
return
|
||||
} else {
|
||||
ws.debug_log('failed to read next message: ${err}')
|
||||
ws.send_error_event('failed to read next message: ${err}')
|
||||
return err
|
||||
}
|
||||
}
|
||||
if ws.get_state() in [.closed, .closing] {
|
||||
return
|
||||
}
|
||||
ws.debug_log('got message: ${msg.opcode}')
|
||||
match msg.opcode {
|
||||
.text_frame {
|
||||
ws.debug_log('read: text')
|
||||
ws.send_message_event(msg)
|
||||
unsafe { msg.free() }
|
||||
}
|
||||
.binary_frame {
|
||||
ws.debug_log('read: binary')
|
||||
ws.send_message_event(msg)
|
||||
unsafe { msg.free() }
|
||||
}
|
||||
.ping {
|
||||
ws.debug_log('read: ping, sending pong')
|
||||
ws.send_control_frame(.pong, 'PONG', msg.payload) or {
|
||||
ws.logger.error('error in message callback sending PONG: ${err}')
|
||||
ws.send_error_event('error in message callback sending PONG: ${err}')
|
||||
if ws.panic_on_callback {
|
||||
panic(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if msg.payload.len > 0 {
|
||||
unsafe { msg.free() }
|
||||
}
|
||||
}
|
||||
.pong {
|
||||
ws.debug_log('read: pong')
|
||||
ws.last_pong_ut = time.now().unix()
|
||||
ws.send_message_event(msg)
|
||||
if msg.payload.len > 0 {
|
||||
unsafe { msg.free() }
|
||||
}
|
||||
}
|
||||
.close {
|
||||
ws.debug_log('read: close')
|
||||
defer {
|
||||
ws.manage_clean_close()
|
||||
}
|
||||
if msg.payload.len > 0 {
|
||||
if msg.payload.len == 1 {
|
||||
ws.close(1002, 'close payload cannot be 1 byte')!
|
||||
return error('close payload cannot be 1 byte')
|
||||
}
|
||||
code := u16(msg.payload[0]) << 8 | u16(msg.payload[1])
|
||||
if code in invalid_close_codes {
|
||||
ws.close(1002, 'invalid close code: ${code}')!
|
||||
return error('invalid close code: ${code}')
|
||||
}
|
||||
reason := if msg.payload.len > 2 { msg.payload[2..] } else { []u8{} }
|
||||
if reason.len > 0 {
|
||||
ws.validate_utf_8(.close, reason)!
|
||||
}
|
||||
if ws.get_state() !in [.closing, .closed] {
|
||||
// sending close back according to spec
|
||||
ws.debug_log('close with reason, code: ${code}, reason: ${reason}')
|
||||
r := reason.bytestr()
|
||||
ws.close(code, r)!
|
||||
}
|
||||
unsafe { msg.free() }
|
||||
} else {
|
||||
if ws.get_state() !in [.closing, .closed] {
|
||||
ws.debug_log('close with reason, no code')
|
||||
// sending close back according to spec
|
||||
ws.close(1000, 'normal')!
|
||||
}
|
||||
unsafe { msg.free() }
|
||||
}
|
||||
return
|
||||
}
|
||||
.continuation {
|
||||
ws.logger.error('unexpected opcode continuation, nothing to continue')
|
||||
ws.send_error_event('unexpected opcode continuation, nothing to continue')
|
||||
ws.close(1002, 'nothing to continue')!
|
||||
return error('unexpected opcode continuation, nothing to continue')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// manage_clean_close closes connection in a clean websocket way
|
||||
fn (mut ws Client) manage_clean_close() {
|
||||
ws.send_close_event(1000, 'closed by client')
|
||||
}
|
||||
|
||||
// ping sends ping message to server
|
||||
pub fn (mut ws Client) ping() ! {
|
||||
ws.send_control_frame(.ping, 'PING', [])!
|
||||
}
|
||||
|
||||
// pong sends pong message to server,
|
||||
pub fn (mut ws Client) pong() ! {
|
||||
ws.send_control_frame(.pong, 'PONG', [])!
|
||||
}
|
||||
|
||||
// write_ptr writes len bytes provided a byteptr with a websocket messagetype
|
||||
pub fn (mut ws Client) write_ptr(bytes &u8, payload_len int, code OPCode) !int {
|
||||
// ws.debug_log('write_ptr code: ${code}')
|
||||
if ws.get_state() != .open || ws.conn.sock.handle < 1 {
|
||||
// todo: send error here later
|
||||
return error('trying to write on a closed socket!')
|
||||
}
|
||||
mut header_len := 2 + if payload_len > 125 { 2 } else { 0 } +
|
||||
if payload_len > 0xffff { 6 } else { 0 }
|
||||
if !ws.is_server {
|
||||
header_len += 4
|
||||
}
|
||||
mut header := []u8{len: header_len, init: `0`} // [`0`].repeat(header_len)
|
||||
header[0] = u8(int(code)) | 0x80
|
||||
masking_key := create_masking_key()
|
||||
if ws.is_server {
|
||||
if payload_len <= 125 {
|
||||
header[1] = u8(payload_len)
|
||||
} else if payload_len > 125 && payload_len <= 0xffff {
|
||||
len16 := conv.hton16(u16(payload_len))
|
||||
header[1] = 126
|
||||
unsafe { vmemcpy(&header[2], &len16, 2) }
|
||||
} else if payload_len > 0xffff && payload_len <= 0x7fffffff {
|
||||
len_bytes := htonl64(u64(payload_len))
|
||||
header[1] = 127
|
||||
unsafe { vmemcpy(&header[2], len_bytes.data, 8) }
|
||||
}
|
||||
} else {
|
||||
if payload_len <= 125 {
|
||||
header[1] = u8(payload_len | 0x80)
|
||||
header[2] = masking_key[0]
|
||||
header[3] = masking_key[1]
|
||||
header[4] = masking_key[2]
|
||||
header[5] = masking_key[3]
|
||||
} else if payload_len > 125 && payload_len <= 0xffff {
|
||||
len16 := conv.hton16(u16(payload_len))
|
||||
header[1] = (126 | 0x80)
|
||||
unsafe { vmemcpy(&header[2], &len16, 2) }
|
||||
header[4] = masking_key[0]
|
||||
header[5] = masking_key[1]
|
||||
header[6] = masking_key[2]
|
||||
header[7] = masking_key[3]
|
||||
} else if payload_len > 0xffff && payload_len <= 0x7fffffff {
|
||||
len64 := htonl64(u64(payload_len))
|
||||
header[1] = (127 | 0x80)
|
||||
unsafe { vmemcpy(&header[2], len64.data, 8) }
|
||||
header[10] = masking_key[0]
|
||||
header[11] = masking_key[1]
|
||||
header[12] = masking_key[2]
|
||||
header[13] = masking_key[3]
|
||||
} else {
|
||||
ws.close(1009, 'frame too large')!
|
||||
return error('frame too large')
|
||||
}
|
||||
}
|
||||
len := header.len + payload_len
|
||||
mut frame_buf := []u8{len: len}
|
||||
unsafe {
|
||||
vmemcpy(&frame_buf[0], &u8(header.data), header.len)
|
||||
if payload_len > 0 {
|
||||
vmemcpy(&frame_buf[header.len], bytes, payload_len)
|
||||
}
|
||||
}
|
||||
if !ws.is_server {
|
||||
for i in 0 .. payload_len {
|
||||
frame_buf[header_len + i] ^= masking_key[i % 4] & 0xff
|
||||
}
|
||||
}
|
||||
written_len := ws.socket_write(frame_buf)!
|
||||
unsafe {
|
||||
frame_buf.free()
|
||||
masking_key.free()
|
||||
header.free()
|
||||
}
|
||||
return written_len
|
||||
}
|
||||
|
||||
// write writes a byte array with a websocket messagetype to socket
|
||||
pub fn (mut ws Client) write(bytes []u8, code OPCode) !int {
|
||||
return ws.write_ptr(&u8(bytes.data), bytes.len, code)
|
||||
}
|
||||
|
||||
// write_str, writes a string with a websocket texttype to socket
|
||||
pub fn (mut ws Client) write_string(str string) !int {
|
||||
return ws.write_ptr(str.str, str.len, .text_frame)
|
||||
}
|
||||
|
||||
// close closes the websocket connection
|
||||
pub fn (mut ws Client) close(code int, message string) ! {
|
||||
ws.debug_log('sending close, ${code}, ${message}')
|
||||
ws_state := ws.get_state()
|
||||
if ws_state in [.closed, .closing] || ws.conn.sock.handle <= 1 {
|
||||
ws.debug_log('close: Websocket already closed (${ws_state}), ${message}, ${code} handle(${ws.conn.sock.handle})')
|
||||
err_msg := 'Socket already closed: ${code}'
|
||||
return error(err_msg)
|
||||
}
|
||||
defer {
|
||||
ws.shutdown_socket() or {}
|
||||
ws.reset_state() or {}
|
||||
ws.send_close_event(code, message)
|
||||
}
|
||||
ws.set_state(.closing)
|
||||
// mut code32 := 0
|
||||
if code > 0 {
|
||||
code_ := conv.hton16(u16(code))
|
||||
message_len := message.len + 2
|
||||
mut close_frame := []u8{len: message_len}
|
||||
close_frame[0] = u8(code_ & 0xFF)
|
||||
close_frame[1] = u8(code_ >> 8)
|
||||
// code32 = (close_frame[0] << 8) + close_frame[1]
|
||||
for i in 0 .. message.len {
|
||||
close_frame[i + 2] = message[i]
|
||||
}
|
||||
ws.send_control_frame(.close, 'CLOSE', close_frame)!
|
||||
unsafe { close_frame.free() }
|
||||
} else {
|
||||
ws.send_control_frame(.close, 'CLOSE', [])!
|
||||
}
|
||||
ws.fragments = []
|
||||
}
|
||||
|
||||
// send_control_frame sends a control frame to the server
|
||||
fn (mut ws Client) send_control_frame(code OPCode, frame_typ string, payload []u8) ! {
|
||||
ws.debug_log('send control frame ${code}, frame_type: ${frame_typ}')
|
||||
if ws.get_state() !in [.open, .closing] && ws.conn.sock.handle > 1 {
|
||||
return error('socket is not connected')
|
||||
}
|
||||
header_len := if ws.is_server { 2 } else { 6 }
|
||||
frame_len := header_len + payload.len
|
||||
mut control_frame := []u8{len: frame_len}
|
||||
mut masking_key := if !ws.is_server { create_masking_key() } else { empty_bytearr }
|
||||
defer {
|
||||
unsafe {
|
||||
control_frame.free()
|
||||
if masking_key.len > 0 {
|
||||
masking_key.free()
|
||||
}
|
||||
}
|
||||
}
|
||||
control_frame[0] = u8(int(code) | 0x80)
|
||||
if !ws.is_server {
|
||||
control_frame[1] = u8(payload.len | 0x80)
|
||||
control_frame[2] = masking_key[0]
|
||||
control_frame[3] = masking_key[1]
|
||||
control_frame[4] = masking_key[2]
|
||||
control_frame[5] = masking_key[3]
|
||||
} else {
|
||||
control_frame[1] = u8(payload.len)
|
||||
}
|
||||
if code == .close {
|
||||
if payload.len >= 2 {
|
||||
if !ws.is_server {
|
||||
mut parsed_payload := []u8{len: payload.len + 1}
|
||||
unsafe { vmemcpy(parsed_payload.data, &payload[0], payload.len) }
|
||||
parsed_payload[payload.len] = `\0`
|
||||
for i in 0 .. payload.len {
|
||||
control_frame[6 + i] = (parsed_payload[i] ^ masking_key[i % 4]) & 0xff
|
||||
}
|
||||
unsafe { parsed_payload.free() }
|
||||
} else {
|
||||
unsafe { vmemcpy(&control_frame[2], &payload[0], payload.len) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !ws.is_server {
|
||||
if payload.len > 0 {
|
||||
for i in 0 .. payload.len {
|
||||
control_frame[header_len + i] = (payload[i] ^ masking_key[i % 4]) & 0xff
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if payload.len > 0 {
|
||||
unsafe { vmemcpy(&control_frame[2], &payload[0], payload.len) }
|
||||
}
|
||||
}
|
||||
}
|
||||
ws.socket_write(control_frame) or {
|
||||
return error('send_control_frame: error sending ${frame_typ} control frame.')
|
||||
}
|
||||
}
|
||||
|
||||
// parse_uri parses the url to a Uri
|
||||
fn parse_uri(url string) !&Uri {
|
||||
u := urllib.parse(url)!
|
||||
request_uri := u.request_uri()
|
||||
v := request_uri.split('?')
|
||||
mut port := u.port()
|
||||
uri := u.str()
|
||||
if port == '' {
|
||||
port = if uri.starts_with('ws://') {
|
||||
'80'
|
||||
} else if uri.starts_with('wss://') {
|
||||
'443'
|
||||
} else {
|
||||
u.port()
|
||||
}
|
||||
}
|
||||
querystring := if v.len > 1 { '?' + v[1] } else { '' }
|
||||
return &Uri{
|
||||
url: url
|
||||
hostname: u.hostname()
|
||||
port: port
|
||||
resource: v[0]
|
||||
querystring: querystring
|
||||
}
|
||||
}
|
||||
|
||||
// set_state sets current state of the websocket connection
|
||||
pub fn (mut ws Client) set_state(state State) {
|
||||
lock ws.client_state {
|
||||
ws.client_state.state = state
|
||||
}
|
||||
}
|
||||
|
||||
// get_state return the current state of the websocket connection
|
||||
pub fn (ws &Client) get_state() State {
|
||||
return rlock ws.client_state {
|
||||
ws.client_state.state
|
||||
}
|
||||
}
|
||||
|
||||
// assert_not_connected returns error if the connection is not connected
|
||||
fn (ws &Client) assert_not_connected() ! {
|
||||
match ws.get_state() {
|
||||
.connecting { return error('connect: websocket is connecting') }
|
||||
.open { return error('connect: websocket already open') }
|
||||
.closing { return error('connect: reconnect on closing websocket not supported, please use new client') }
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
// reset_state resets the websocket and initialize default settings
|
||||
pub fn (mut ws Client) reset_state() ! {
|
||||
lock ws.client_state {
|
||||
ws.client_state.state = .closed
|
||||
ws.ssl_conn = ssl.new_ssl_conn()!
|
||||
ws.flags = []
|
||||
ws.fragments = []
|
||||
}
|
||||
}
|
||||
|
||||
// debug_log handles debug logging output for client and server
|
||||
fn (mut ws Client) debug_log(text string) {
|
||||
if ws.is_server {
|
||||
ws.logger.debug('server-> ${text}')
|
||||
} else {
|
||||
ws.logger.debug('client-> ${text}')
|
||||
}
|
||||
}
|
||||
|
||||
// free handles manual free memory of Message struct
|
||||
pub fn (m &Message) free() {
|
||||
unsafe { m.payload.free() }
|
||||
}
|
||||
|
||||
// free handles manual free memory of Client struct
|
||||
pub fn (c &Client) free() {
|
||||
unsafe {
|
||||
c.flags.free()
|
||||
c.fragments.free()
|
||||
c.message_callbacks.free()
|
||||
c.error_callbacks.free()
|
||||
c.open_callbacks.free()
|
||||
c.close_callbacks.free()
|
||||
c.header.free()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module websocket
|
||||
|
||||
// error_code returns the error code
|
||||
fn error_code() int {
|
||||
return C.errno
|
||||
}
|
||||
|
||||
const error_ewouldblock = C.EWOULDBLOCK
|
||||
@@ -0,0 +1,181 @@
|
||||
// vtest build: !windows
|
||||
import encoding.base64
|
||||
import os
|
||||
import net
|
||||
import net.websocket
|
||||
import time
|
||||
|
||||
const proxy_github_job = os.getenv('GITHUB_JOB')
|
||||
|
||||
const proxy_should_skip = proxy_github_job != '' && proxy_github_job != 'websocket_tests'
|
||||
|
||||
const proxy_ws_target_port = 30105
|
||||
const proxy_ws_port = 30106
|
||||
|
||||
@[heap]
|
||||
struct ProxyWebsocketTestResults {
|
||||
pub mut:
|
||||
nr_messages int
|
||||
nr_pong_received int
|
||||
error_message string
|
||||
}
|
||||
|
||||
fn start_proxy_ws_server(listen_port int) ! {
|
||||
mut s := websocket.new_server(.ip, listen_port, '')
|
||||
s.set_ping_interval(1)
|
||||
s.on_connect(fn (mut s websocket.ServerClient) !bool {
|
||||
return s.resource_name == '/'
|
||||
})!
|
||||
s.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
match msg.opcode {
|
||||
.pong { ws.write_string('pong')! }
|
||||
else { ws.write(msg.payload, msg.opcode)! }
|
||||
}
|
||||
})
|
||||
start_proxy_ws_server_in_thread(mut s)
|
||||
}
|
||||
|
||||
fn start_proxy_ws_server_in_thread(mut ws websocket.Server) {
|
||||
spawn fn [mut ws] () {
|
||||
ws.listen() or { panic('websocket server could not listen, err: ${err}') }
|
||||
}()
|
||||
for ws.get_state() != .open {
|
||||
time.sleep(10 * time.millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
fn start_proxy_server(listen_port int, target string, requests chan string) {
|
||||
ready := chan bool{cap: 1}
|
||||
spawn fn [listen_port, target, requests, ready] () {
|
||||
mut listener := net.listen_tcp(.ip, ':${listen_port}') or { panic(err) }
|
||||
ready <- true
|
||||
mut client := listener.accept() or {
|
||||
listener.close() or {}
|
||||
panic(err)
|
||||
}
|
||||
listener.close() or {}
|
||||
request := read_proxy_request(mut client) or {
|
||||
requests <- 'ERROR: ${err}'
|
||||
client.close() or {}
|
||||
return
|
||||
}
|
||||
requests <- request
|
||||
mut upstream := net.dial_tcp(target) or {
|
||||
client.write_string('HTTP/1.1 502 Bad Gateway\r\n\r\n') or {}
|
||||
client.close() or {}
|
||||
return
|
||||
}
|
||||
client.write_string('HTTP/1.1 200 Connection Established\r\n\r\n') or {
|
||||
client.close() or {}
|
||||
upstream.close() or {}
|
||||
return
|
||||
}
|
||||
tunnel_proxy_connection(mut client, mut upstream)
|
||||
}()
|
||||
_ := <-ready
|
||||
}
|
||||
|
||||
fn read_proxy_request(mut conn net.TcpConn) !string {
|
||||
mut total_bytes_read := 0
|
||||
mut msg := [4096]u8{}
|
||||
mut buffer := [1]u8{}
|
||||
for total_bytes_read < msg.len {
|
||||
bytes_read := conn.read_ptr(&buffer[0], 1)!
|
||||
if bytes_read == 0 {
|
||||
return error('unexpected EOF while reading proxy request')
|
||||
}
|
||||
msg[total_bytes_read] = buffer[0]
|
||||
total_bytes_read++
|
||||
if total_bytes_read > 3 && msg[total_bytes_read - 1] == `\n`
|
||||
&& msg[total_bytes_read - 2] == `\r` && msg[total_bytes_read - 3] == `\n`
|
||||
&& msg[total_bytes_read - 4] == `\r` {
|
||||
return msg[..total_bytes_read].bytestr()
|
||||
}
|
||||
}
|
||||
return error('proxy request headers exceeded 4096 bytes')
|
||||
}
|
||||
|
||||
fn tunnel_proxy_connection(mut client net.TcpConn, mut upstream net.TcpConn) {
|
||||
defer {
|
||||
client.close() or {}
|
||||
upstream.close() or {}
|
||||
}
|
||||
client.set_read_timeout(50 * time.millisecond)
|
||||
upstream.set_read_timeout(50 * time.millisecond)
|
||||
deadline := time.now().add(4 * time.second)
|
||||
for time.now() < deadline {
|
||||
relay_proxy_connection(mut client, mut upstream)
|
||||
relay_proxy_connection(mut upstream, mut client)
|
||||
}
|
||||
}
|
||||
|
||||
fn relay_proxy_connection(mut src net.TcpConn, mut dst net.TcpConn) {
|
||||
mut buf := []u8{len: 1024}
|
||||
bytes_read := src.read(mut buf) or {
|
||||
if err.code() == net.err_timed_out_code {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if bytes_read > 0 {
|
||||
dst.write(buf[..bytes_read]) or {}
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_open_cb(mut client websocket.Client) ! {
|
||||
client.pong()!
|
||||
}
|
||||
|
||||
fn proxy_error_cb(mut client websocket.Client, err string, mut res ProxyWebsocketTestResults) ! {
|
||||
res.error_message = err
|
||||
}
|
||||
|
||||
fn proxy_message_cb(mut client websocket.Client, msg &websocket.Message, mut res ProxyWebsocketTestResults) ! {
|
||||
if msg.opcode != .text_frame {
|
||||
return
|
||||
}
|
||||
match msg.payload.bytestr() {
|
||||
'pong' {
|
||||
res.nr_pong_received++
|
||||
}
|
||||
'a' {
|
||||
res.nr_messages++
|
||||
}
|
||||
else {
|
||||
res.error_message = 'unexpected payload: ${msg.payload.bytestr()}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_ws_connection_through_proxy() ! {
|
||||
if proxy_should_skip {
|
||||
return
|
||||
}
|
||||
start_proxy_ws_server(proxy_ws_target_port)!
|
||||
requests := chan string{cap: 1}
|
||||
start_proxy_server(proxy_ws_port, '127.0.0.1:${proxy_ws_target_port}', requests)
|
||||
mut results := ProxyWebsocketTestResults{}
|
||||
mut client := websocket.new_client('ws://127.0.0.1:${proxy_ws_target_port}', websocket.ClientOpt{
|
||||
proxy_url: 'http://user:pass@127.0.0.1:${proxy_ws_port}'
|
||||
})!
|
||||
client.on_open(proxy_open_cb)
|
||||
client.on_error_ref(proxy_error_cb, results)
|
||||
client.on_message_ref(proxy_message_cb, results)
|
||||
client.connect()!
|
||||
spawn client.listen()
|
||||
for msg in ['a', 'a'] {
|
||||
client.write(msg.bytes(), .text_frame)!
|
||||
time.sleep(100 * time.millisecond)
|
||||
}
|
||||
time.sleep(1500 * time.millisecond)
|
||||
request := <-requests
|
||||
auth_token := base64.encode('user:pass'.bytes())
|
||||
assert request.starts_with('CONNECT 127.0.0.1:${proxy_ws_target_port} HTTP/1.1\r\n')
|
||||
assert request.contains('Proxy-Connection: Keep-Alive\r\n')
|
||||
assert request.contains('Proxy-Authorization: Basic ${auth_token}\r\n')
|
||||
client.close(1000, 'done') or {}
|
||||
time.sleep(100 * time.millisecond)
|
||||
assert results.error_message == ''
|
||||
assert results.nr_pong_received >= 2
|
||||
assert results.nr_messages == 2
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
module websocket
|
||||
|
||||
import net
|
||||
import net.ssl
|
||||
import log
|
||||
import time
|
||||
import rand
|
||||
|
||||
pub struct ServerState {
|
||||
mut:
|
||||
ping_interval int = 30 // interval for sending ping to clients (seconds)
|
||||
state State = .closed // current state of connection
|
||||
pub mut:
|
||||
clients map[string]&ServerClient // clients connected to this server
|
||||
}
|
||||
|
||||
// Server represents a websocket server connection
|
||||
pub struct Server {
|
||||
mut:
|
||||
logger &log.Logger = default_logger
|
||||
ls &net.TcpListener = unsafe { nil } // listener used to get incoming connection to socket
|
||||
accept_client_callbacks []AcceptClientFn // accept client callback functions
|
||||
attached_callbacks []AttachedEventHandler
|
||||
message_callbacks []MessageEventHandler // new message callback functions
|
||||
close_callbacks []CloseEventHandler // close message callback functions
|
||||
pub:
|
||||
family net.AddrFamily = .ip
|
||||
port int // port used as listen to incoming connections
|
||||
is_ssl bool // true if secure connection (not supported yet on server)
|
||||
pub mut:
|
||||
server_state shared ServerState
|
||||
}
|
||||
|
||||
// ServerClient represents a connected client
|
||||
pub struct ServerClient {
|
||||
pub:
|
||||
resource_name string // resource that the client access
|
||||
client_key string // unique key of client
|
||||
pub mut:
|
||||
server &Server = unsafe { nil }
|
||||
client &Client = unsafe { nil }
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ServerOpt {
|
||||
pub:
|
||||
logger &log.Logger = default_logger
|
||||
}
|
||||
|
||||
// new_server instance a new websocket server on provided port and route
|
||||
pub fn new_server(family net.AddrFamily, port int, route string, opt ServerOpt) &Server {
|
||||
return &Server{
|
||||
ls: unsafe { nil }
|
||||
family: family
|
||||
port: port
|
||||
logger: opt.logger
|
||||
}
|
||||
}
|
||||
|
||||
// set_ping_interval sets the interval that the server will send ping messages to clients
|
||||
pub fn (mut s Server) set_ping_interval(seconds int) {
|
||||
lock s.server_state {
|
||||
s.server_state.ping_interval = seconds
|
||||
}
|
||||
}
|
||||
|
||||
// get_ping_interval return the interval that the server will send ping messages to clients
|
||||
pub fn (mut s Server) get_ping_interval() int {
|
||||
return rlock s.server_state {
|
||||
s.server_state.ping_interval
|
||||
}
|
||||
}
|
||||
|
||||
// listen start listen and process to incoming connections from websocket clients
|
||||
pub fn (mut s Server) listen() ! {
|
||||
s.logger.info('websocket server: start listen on port ${s.port}')
|
||||
s.ls = net.listen_tcp(s.family, ':${s.port}')!
|
||||
s.set_state(.open)
|
||||
spawn s.handle_ping()
|
||||
for {
|
||||
mut c := s.accept_new_client() or { continue }
|
||||
spawn s.serve_client(mut c)
|
||||
}
|
||||
s.logger.info('websocket server: end listen on port ${s.port}')
|
||||
}
|
||||
|
||||
// Close closes server (not implemented yet)
|
||||
fn (mut s Server) close() {
|
||||
// TODO: implement close when moving to net from x.net
|
||||
}
|
||||
|
||||
// handle_ping sends ping to all clients every set interval
|
||||
fn (mut s Server) handle_ping() {
|
||||
mut clients_to_remove := []string{}
|
||||
for s.get_state() == .open {
|
||||
time.sleep(s.get_ping_interval() * time.second)
|
||||
for i, _ in rlock s.server_state {
|
||||
s.server_state.clients
|
||||
} {
|
||||
mut c := rlock s.server_state {
|
||||
s.server_state.clients[i] or { continue }
|
||||
}
|
||||
if c.client.get_state() == .open {
|
||||
c.client.ping() or {
|
||||
s.logger.debug('server-> error sending ping to client')
|
||||
c.client.close(1002, 'Closing connection: ping send error') or {
|
||||
// we want to continue even if error
|
||||
continue
|
||||
}
|
||||
clients_to_remove << c.client.id
|
||||
}
|
||||
if (time.now().unix() - c.client.last_pong_ut) > s.get_ping_interval() * 2 {
|
||||
clients_to_remove << c.client.id
|
||||
c.client.close(1000, 'no pong received') or { continue }
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: replace for with s.clients.delete_all(clients_to_remove) if (https://github.com/vlang/v/pull/6020) merges
|
||||
for client in clients_to_remove {
|
||||
lock s.server_state {
|
||||
s.server_state.clients.delete(client)
|
||||
}
|
||||
}
|
||||
clients_to_remove.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// serve_client accepts incoming connection and sets up the callbacks
|
||||
fn (mut s Server) serve_client(mut c Client) ! {
|
||||
c.logger.debug('server-> Start serve client (${c.id})')
|
||||
defer {
|
||||
c.logger.debug('server-> End serve client (${c.id})')
|
||||
}
|
||||
mut handshake_response, mut server_client := s.handle_server_handshake(mut c)!
|
||||
s.attach_client(mut server_client, handshake_response)!
|
||||
c.listen() or {
|
||||
s.logger.error(err.msg())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// handle_handshake use an existing connection to respond to the handshake for a given key
|
||||
pub fn (mut s Server) handle_handshake(mut conn net.TcpConn, key string) !&ServerClient {
|
||||
mut logger := &log.Log{}
|
||||
logger.set_level(.debug)
|
||||
mut c := &Client{
|
||||
is_server: true
|
||||
conn: conn
|
||||
is_ssl: false
|
||||
logger: logger
|
||||
client_state: ClientState{
|
||||
state: .open
|
||||
}
|
||||
last_pong_ut: time.now().unix()
|
||||
id: rand.uuid_v4()
|
||||
}
|
||||
mut server_client := &ServerClient{
|
||||
resource_name: 'GET'
|
||||
client_key: key
|
||||
client: unsafe { c }
|
||||
server: unsafe { &s }
|
||||
}
|
||||
digest := create_key_challenge_response(key)!
|
||||
handshake_response := 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${digest}\r\n\r\n'
|
||||
s.attach_client(mut server_client, handshake_response)!
|
||||
spawn s.handle_ping()
|
||||
c.listen() or {
|
||||
s.logger.error(err.msg())
|
||||
return err
|
||||
}
|
||||
return server_client
|
||||
}
|
||||
|
||||
fn (mut s Server) attach_client(mut server_client ServerClient, handshake_response string) ! {
|
||||
accept := s.send_connect_event(mut server_client)!
|
||||
if !accept {
|
||||
s.logger.debug('server-> client not accepted')
|
||||
server_client.client.shutdown_socket()!
|
||||
return
|
||||
}
|
||||
// the client is accepted
|
||||
server_client.client.socket_write(handshake_response.bytes())!
|
||||
lock s.server_state {
|
||||
s.server_state.clients[server_client.client.id] = unsafe { server_client }
|
||||
}
|
||||
s.setup_callbacks(mut server_client)
|
||||
s.send_attached_event(mut server_client) or {
|
||||
lock s.server_state {
|
||||
s.server_state.clients.delete(server_client.client.id)
|
||||
}
|
||||
server_client.client.shutdown_socket() or {}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// setup_callbacks initialize all callback functions
|
||||
fn (mut s Server) setup_callbacks(mut sc ServerClient) {
|
||||
if s.message_callbacks.len > 0 {
|
||||
for cb in s.message_callbacks {
|
||||
if cb.is_ref {
|
||||
sc.client.on_message_ref(cb.handler2, cb.ref)
|
||||
} else {
|
||||
sc.client.on_message(cb.handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.close_callbacks.len > 0 {
|
||||
for cb in s.close_callbacks {
|
||||
if cb.is_ref {
|
||||
sc.client.on_close_ref(cb.handler2, cb.ref)
|
||||
} else {
|
||||
sc.client.on_close(cb.handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
// set standard close so we can remove client if closed
|
||||
sc.client.on_close_ref(delete_client_cb, sc)
|
||||
}
|
||||
|
||||
fn delete_client_cb(mut c Client, _code int, _reason string, mut sc ServerClient) ! {
|
||||
c.logger.debug('server-> Delete client')
|
||||
lock sc.server.server_state {
|
||||
sc.server.server_state.clients.delete(sc.client.id)
|
||||
}
|
||||
}
|
||||
|
||||
// accept_new_client creates a new client instance for client that connects to the socket
|
||||
fn (mut s Server) accept_new_client() !&Client {
|
||||
mut new_conn := s.ls.accept()!
|
||||
c := &Client{
|
||||
is_server: true
|
||||
conn: new_conn
|
||||
ssl_conn: ssl.new_ssl_conn()!
|
||||
logger: s.logger
|
||||
client_state: ClientState{
|
||||
state: .open
|
||||
}
|
||||
last_pong_ut: time.now().unix()
|
||||
id: rand.uuid_v4()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// set_state sets current state in a thread safe way
|
||||
pub fn (mut s Server) set_state(state State) {
|
||||
lock s.server_state {
|
||||
s.server_state.state = state
|
||||
}
|
||||
}
|
||||
|
||||
// get_state return current state in a thread safe way
|
||||
pub fn (s &Server) get_state() State {
|
||||
return rlock s.server_state {
|
||||
s.server_state.state
|
||||
}
|
||||
}
|
||||
|
||||
// free manages manual free of memory for Server instance
|
||||
pub fn (mut s Server) free() {
|
||||
lock s.server_state {
|
||||
unsafe {
|
||||
s.server_state.clients.free()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
s.accept_client_callbacks.free()
|
||||
s.message_callbacks.free()
|
||||
s.close_callbacks.free()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// vtest build: !windows
|
||||
import os
|
||||
import net
|
||||
import net.websocket
|
||||
import time
|
||||
|
||||
@[heap]
|
||||
struct WebsocketTestResults {
|
||||
pub mut:
|
||||
nr_messages int
|
||||
nr_pong_received int
|
||||
nr_closes int
|
||||
}
|
||||
|
||||
// Do not run these tests every time, since they are flaky.
|
||||
// They have their own specialized CI runner.
|
||||
const github_job = os.getenv('GITHUB_JOB')
|
||||
|
||||
const should_skip = get_should_skip()
|
||||
|
||||
fn get_should_skip() bool {
|
||||
return github_job != '' && github_job != 'websocket_tests'
|
||||
}
|
||||
|
||||
// tests with internal ws servers
|
||||
fn test_ws_ipv6() {
|
||||
if should_skip {
|
||||
return
|
||||
}
|
||||
start_server(.ip6, 30001)!
|
||||
|
||||
ws_test(.ip6, 'ws://localhost:30001') or {
|
||||
eprintln('> error while connecting .ip6, err: ${err}')
|
||||
assert false
|
||||
}
|
||||
}
|
||||
|
||||
// tests with internal ws servers
|
||||
fn test_ws_ipv4() {
|
||||
if should_skip {
|
||||
return
|
||||
}
|
||||
start_server(.ip, 30002)!
|
||||
|
||||
ws_test(.ip, 'ws://localhost:30002') or {
|
||||
eprintln('> error while connecting .ip, err: ${err}')
|
||||
assert false
|
||||
}
|
||||
}
|
||||
|
||||
fn start_server(family net.AddrFamily, listen_port int) ! {
|
||||
eprintln('> start_server family:${family} | listen_port: ${listen_port}')
|
||||
mut s := websocket.new_server(family, listen_port, '')
|
||||
// make that in execution test time give time to execute at least one time
|
||||
s.set_ping_interval(1)
|
||||
|
||||
s.on_connect(fn (mut s websocket.ServerClient) !bool {
|
||||
// here you can look att the client info and accept or not accept
|
||||
// just returning a true/false
|
||||
if s.resource_name != '/' {
|
||||
panic('unexpected resource name in test')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})!
|
||||
s.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
match msg.opcode {
|
||||
.pong { ws.write_string('pong')! }
|
||||
else { ws.write(msg.payload, msg.opcode)! }
|
||||
}
|
||||
})
|
||||
|
||||
s.on_close(fn (mut ws websocket.Client, code int, reason string) ! {
|
||||
// not used
|
||||
})
|
||||
start_server_in_thread_and_wait_till_it_is_ready_to_accept_connections(mut s)
|
||||
eprintln('> start_server finished')
|
||||
}
|
||||
|
||||
fn start_server_in_thread_and_wait_till_it_is_ready_to_accept_connections(mut ws websocket.Server) {
|
||||
eprintln('-----------------------------------------------------------------------------')
|
||||
spawn fn [mut ws] () {
|
||||
ws.listen() or { panic('websocket server could not listen, err: ${err}') }
|
||||
}()
|
||||
for ws.get_state() != .open {
|
||||
time.sleep(10 * time.millisecond)
|
||||
}
|
||||
eprintln('-----------------------------------------------------------------------------')
|
||||
}
|
||||
|
||||
fn open_cb(mut client websocket.Client) ! {
|
||||
client.pong()!
|
||||
assert true
|
||||
}
|
||||
|
||||
fn close_cb(mut client websocket.Client, err string) ! {
|
||||
println('error: ${err}')
|
||||
// this can be thrown by internet connection problems
|
||||
assert false
|
||||
}
|
||||
|
||||
fn message_cb(mut client websocket.Client, msg &websocket.Message, mut res WebsocketTestResults) ! {
|
||||
println('client got type: ${msg.opcode} payload:\n${msg.payload}')
|
||||
if msg.opcode == .text_frame {
|
||||
smessage := msg.payload.bytestr()
|
||||
match smessage {
|
||||
'pong' {
|
||||
res.nr_pong_received++
|
||||
}
|
||||
'a' {
|
||||
res.nr_messages++
|
||||
}
|
||||
else {
|
||||
assert false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println('Binary message: ${msg}')
|
||||
}
|
||||
}
|
||||
|
||||
// ws_test tests connect to the websocket server from websocket client
|
||||
fn ws_test(family net.AddrFamily, uri string) ! {
|
||||
eprintln('connecting to ${uri} ...')
|
||||
|
||||
mut test_results := WebsocketTestResults{}
|
||||
mut client := websocket.new_client(uri)!
|
||||
client.on_open(open_cb)
|
||||
client.on_error(close_cb)
|
||||
client.on_message_ref(message_cb, test_results)
|
||||
client.connect()!
|
||||
spawn client.listen()
|
||||
|
||||
text := ['a'].repeat(2)
|
||||
for msg in text {
|
||||
client.write(msg.bytes(), .text_frame) or {
|
||||
panic('fail to write to websocket, err: ${err}')
|
||||
}
|
||||
// sleep to give time to receive response before send a new one
|
||||
time.sleep(100 * time.millisecond)
|
||||
}
|
||||
// sleep to give time to receive response before asserts
|
||||
time.sleep(1500 * time.millisecond)
|
||||
// We expect at least 2 pongs, one sent directly and one indirectly
|
||||
assert test_results.nr_pong_received >= 2
|
||||
assert test_results.nr_messages == 2
|
||||
}
|
||||
|
||||
fn on_message_cb_2(mut cli websocket.Client, msg &websocket.Message) ! {
|
||||
if msg.opcode == .text_frame {
|
||||
cli.close(1000, 'closing connection')!
|
||||
}
|
||||
}
|
||||
|
||||
fn on_close_cb_2(mut cli websocket.Client, code int, reason string, mut res WebsocketTestResults) ! {
|
||||
res.nr_closes++
|
||||
}
|
||||
|
||||
fn test_on_close_when_server_closing_connection() ! {
|
||||
mut ws := websocket.new_server(.ip, 30003, '')
|
||||
ws.on_message(on_message_cb_2)
|
||||
mut test_results := WebsocketTestResults{}
|
||||
ws.on_close_ref(on_close_cb_2, test_results)
|
||||
start_server_in_thread_and_wait_till_it_is_ready_to_accept_connections(mut ws)
|
||||
|
||||
mut client := websocket.new_client('ws://localhost:30003')!
|
||||
client.connect()!
|
||||
spawn client.listen()
|
||||
time.sleep(1000 * time.millisecond)
|
||||
client.write_string('a message')!
|
||||
time.sleep(1000 * time.millisecond)
|
||||
assert test_results.nr_closes == 1
|
||||
}
|
||||
|
||||
fn on_close_cb_3(mut cli websocket.Client, code int, reason string, mut res WebsocketTestResults) ! {
|
||||
res.nr_closes++
|
||||
}
|
||||
|
||||
fn test_on_close_when_client_closing_connection() ! {
|
||||
mut ws := websocket.new_server(.ip, 30004, '')
|
||||
start_server_in_thread_and_wait_till_it_is_ready_to_accept_connections(mut ws)
|
||||
|
||||
mut client := websocket.new_client('ws://localhost:30004')!
|
||||
mut test_results := WebsocketTestResults{}
|
||||
client.on_close_ref(on_close_cb_3, test_results)
|
||||
client.connect()!
|
||||
spawn client.listen()
|
||||
time.sleep(1000 * time.millisecond)
|
||||
client.close(1000, 'closing connection')!
|
||||
time.sleep(1000 * time.millisecond)
|
||||
assert test_results.nr_closes == 1
|
||||
}
|
||||
|
||||
fn attached_fail_cb(mut sc websocket.ServerClient) ! {
|
||||
return error('attached hook failure')
|
||||
}
|
||||
|
||||
fn client_close_ignore_cb(mut cli websocket.Client, code int, reason string) ! {}
|
||||
|
||||
fn test_on_attached_error_does_not_leak_client() ! {
|
||||
mut ws := websocket.new_server(.ip, 30005, '')
|
||||
ws.on_attached(attached_fail_cb)
|
||||
start_server_in_thread_and_wait_till_it_is_ready_to_accept_connections(mut ws)
|
||||
|
||||
mut client := websocket.new_client('ws://localhost:30005')!
|
||||
client.on_close(client_close_ignore_cb)
|
||||
client.connect()!
|
||||
spawn client.listen()
|
||||
time.sleep(500 * time.millisecond)
|
||||
|
||||
client_count := rlock ws.server_state {
|
||||
ws.server_state.clients.len
|
||||
}
|
||||
assert client_count == 0
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module websocket
|
||||
|
||||
import net
|
||||
|
||||
// error_code returns the error code
|
||||
fn error_code() int {
|
||||
return C.WSAGetLastError()
|
||||
}
|
||||
|
||||
const error_ewouldblock = net.WsaError.wsaewouldblock
|
||||
Reference in new issue
Block a user