aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/vlib/net/websocket
diff options
context:
space:
mode:
authorIndrajith K L2022-12-03 17:00:20 +0530
committerIndrajith K L2022-12-03 17:00:20 +0530
commitf5c4671bfbad96bf346bd7e9a21fc4317b4959df (patch)
tree2764fc62da58f2ba8da7ed341643fc359873142f /v_windows/v/vlib/net/websocket
downloadcli-tools-windows-f5c4671bfbad96bf346bd7e9a21fc4317b4959df.tar.gz
cli-tools-windows-f5c4671bfbad96bf346bd7e9a21fc4317b4959df.tar.bz2
cli-tools-windows-f5c4671bfbad96bf346bd7e9a21fc4317b4959df.zip
Adds most of the toolsHEADmaster
Diffstat (limited to 'v_windows/v/vlib/net/websocket')
-rw-r--r--v_windows/v/vlib/net/websocket/events.v227
-rw-r--r--v_windows/v/vlib/net/websocket/handshake.v185
-rw-r--r--v_windows/v/vlib/net/websocket/io.v100
-rw-r--r--v_windows/v/vlib/net/websocket/message.v295
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/README.md20
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client.v33
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client_wss.v35
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_server.v27
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/docker-compose.yml21
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/Dockerfile5
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/check_results.py46
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingclient.json22
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingserver.json14
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/Dockerfile9
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/check_results.py35
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/fuzzingserver.json16
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.crt19
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.csr16
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.key27
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.pem19
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/local_run/Dockerfile12
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client.v33
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client_wss.v35
-rw-r--r--v_windows/v/vlib/net/websocket/tests/autobahn/ws_test/Dockerfile12
-rw-r--r--v_windows/v/vlib/net/websocket/uri.v16
-rw-r--r--v_windows/v/vlib/net/websocket/utils.v54
-rw-r--r--v_windows/v/vlib/net/websocket/websocket_client.v488
-rw-r--r--v_windows/v/vlib/net/websocket/websocket_nix.c.v10
-rw-r--r--v_windows/v/vlib/net/websocket/websocket_server.v189
-rw-r--r--v_windows/v/vlib/net/websocket/websocket_test.v122
-rw-r--r--v_windows/v/vlib/net/websocket/websocket_windows.c.v12
31 files changed, 2154 insertions, 0 deletions
diff --git a/v_windows/v/vlib/net/websocket/events.v b/v_windows/v/vlib/net/websocket/events.v
new file mode 100644
index 0000000..a442daf
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/events.v
@@ -0,0 +1,227 @@
+module websocket
+
+// MessageEventHandler represents a callback on a new message
+struct MessageEventHandler {
+ handler SocketMessageFn // callback function
+ handler2 SocketMessageFn2 // 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 // callback function
+ handler2 SocketErrorFn2 // 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 // callback function
+ handler2 SocketOpenFn2 // 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 // callback function
+ handler2 SocketCloseFn2 // callback function with reference
+ is_ref bool // true if has a reference object
+ ref voidptr // referenced object
+}
+
+pub type AcceptClientFn = fn (mut c ServerClient) ?bool
+
+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_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
+}
+
+// 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(ws, msg) or { ws.logger.error('send_message_event error: $err') }
+ } else {
+ ev_handler.handler2(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')
+ }
+ }
+ }
+}
diff --git a/v_windows/v/vlib/net/websocket/handshake.v b/v_windows/v/vlib/net/websocket/handshake.v
new file mode 100644
index 0000000..9f3ab00
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/handshake.v
@@ -0,0 +1,185 @@
+[manualfree]
+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()
+ defer {
+ unsafe { handshake.free() }
+ }
+ handshake_bytes := handshake.bytes()
+ ws.debug_log('sending handshake: $handshake')
+ ws.socket_write(handshake_bytes) ?
+ ws.read_handshake(seckey) ?
+ unsafe { handshake_bytes.free() }
+}
+
+// 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) ?
+ unsafe { msg.free() }
+ 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(':')
+ match keys[0] {
+ 'Upgrade', 'upgrade' {
+ flags << .has_upgrade
+ }
+ 'Connection', 'connection' {
+ flags << .has_connection
+ }
+ 'Sec-WebSocket-Key', 'sec-websocket-key' {
+ key = keys[1].trim_space()
+ s.logger.debug('server-> got key: $key')
+ seckey = create_key_challenge_response(key) ?
+ s.logger.debug('server-> challenge: $seckey, response: ${keys[1]}')
+ flags << .has_accept
+ }
+ else {
+ // we ignore other headers like protocol for now
+ }
+ }
+ unsafe { keys.free() }
+ }
+ 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 }
+ }
+ unsafe {
+ lines.free()
+ flags.free()
+ get_tokens.free()
+ seckey.free()
+ key.free()
+ }
+ 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]byte{}
+ mut buffer := [1]byte{}
+ 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) ?
+ unsafe { msg.free() }
+}
+
+// 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(':')
+ match keys[0] {
+ 'Upgrade', 'upgrade' {
+ ws.flags << .has_upgrade
+ }
+ 'Connection', 'connection' {
+ ws.flags << .has_connection
+ }
+ 'Sec-WebSocket-Accept', '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
+ unsafe { challenge.free() }
+ }
+ else {}
+ }
+ unsafe { keys.free() }
+ }
+ unsafe { lines.free() }
+ if ws.flags.len < 3 {
+ ws.close(1002, 'invalid websocket HTTP headers') ?
+ return error_with_code('invalid websocket HTTP headers', 8)
+ }
+}
diff --git a/v_windows/v/vlib/net/websocket/io.v b/v_windows/v/vlib/net/websocket/io.v
new file mode 100644
index 0000000..5408a4e
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/io.v
@@ -0,0 +1,100 @@
+module websocket
+
+import net
+import time
+
+// socket_read reads from socket into the provided buffer
+fn (mut ws Client) socket_read(mut buffer []byte) ?int {
+ lock {
+ if ws.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_into(mut buffer) ?
+ return r
+ } else {
+ for {
+ r := ws.conn.read(mut buffer) or {
+ if err.code == net.err_timed_out_code {
+ continue
+ }
+ return err
+ }
+ return r
+ }
+ }
+ }
+ return none
+}
+
+// socket_read reads from socket into the provided byte pointer and length
+fn (mut ws Client) socket_read_ptr(buf_ptr &byte, len int) ?int {
+ lock {
+ if ws.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 {
+ for {
+ r := ws.conn.read_ptr(buf_ptr, len) or {
+ if err.code == net.err_timed_out_code {
+ continue
+ }
+ return err
+ }
+ return r
+ }
+ }
+ }
+ return none
+}
+
+// socket_write writes the provided byte array to the socket
+fn (mut ws Client) socket_write(bytes []byte) ?int {
+ lock {
+ if ws.state == .closed || ws.conn.sock.handle <= 1 {
+ ws.debug_log('socket_write: Socket allready 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 := net.dial_tcp(tcp_address) ?
+ optval := int(1)
+ t.sock.set_option_int(.keep_alive, optval) ?
+ t.set_read_timeout(30 * time.second)
+ t.set_write_timeout(30 * time.second)
+ if ws.is_ssl {
+ ws.ssl_conn.connect(mut t, ws.uri.hostname) ?
+ }
+ return t
+}
diff --git a/v_windows/v/vlib/net/websocket/message.v b/v_windows/v/vlib/net/websocket/message.v
new file mode 100644
index 0000000..4c57232
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/message.v
@@ -0,0 +1,295 @@
+module websocket
+
+import encoding.utf8
+
+const (
+ header_len_offset = 2 // offset for lengthpart of websocket header
+ buffer_size = 256 // default buffer size
+ extended_payload16_end_byte = 4 // header length with 16-bit extended payload
+ extended_payload64_end_byte = 10 // header length with 64-bit extended payload
+)
+
+// Fragment represents a websocket data fragment
+struct Fragment {
+ data []byte // 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]byte // 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) ?[]byte {
+ if frame.payload_len == 0 {
+ return []byte{}
+ }
+ mut buffer := []byte{cap: frame.payload_len}
+ mut read_buf := [1]byte{}
+ 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 []byte) ? {
+ 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('Recieved 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 none
+}
+
+// payload_from_fragments returs the whole paylaod from fragmented message
+fn (ws Client) payload_from_fragments(fin_payload []byte) ?[]byte {
+ 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 []byte{}
+ }
+ mut total_buffer := []byte{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]byte{}
+ mut bytes_read := 0
+ mut frame := Frame{}
+ mut rbuff := [1]byte{}
+ mut mask_end_byte := 0
+ for ws.state == .open {
+ read_bytes := ws.socket_read_ptr(&rbuff[0], 1) ?
+ if read_bytes == 0 {
+ // this is probably a timeout or close
+ continue
+ }
+ buffer[bytes_read] = rbuff[0]
+ bytes_read++
+ // parses the first two header bytes to get basic frame information
+ if bytes_read == u64(websocket.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 = OPCode(int(buffer[0] & 0x7F))
+ frame.has_mask = (buffer[1] & 0x80) == 0x80
+ frame.payload_len = buffer[1] & 0x7F
+ // if has mask set the byte postition where mask ends
+ if frame.has_mask {
+ mask_end_byte = if frame.payload_len < 126 {
+ websocket.header_len_offset + 4
+ } else if frame.payload_len == 126 {
+ websocket.header_len_offset + 6
+ } else if frame.payload_len == 127 {
+ websocket.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 == u64(websocket.extended_payload16_end_byte) {
+ frame.header_len += 2
+ frame.payload_len = 0
+ frame.payload_len |= buffer[2] << 8
+ frame.payload_len |= buffer[3]
+ frame.frame_size = frame.header_len + frame.payload_len
+ if !frame.has_mask {
+ break
+ }
+ }
+ if frame.payload_len == 127 && bytes_read == u64(websocket.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 []byte) {
+ for i in 0 .. buffer.len {
+ buffer[i] ^= f.masking_key[i % 4] & 0xff
+ }
+}
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/README.md b/v_windows/v/vlib/net/websocket/tests/autobahn/README.md
new file mode 100644
index 0000000..40724ee
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/README.md
@@ -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 ws (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 certificat so you will get trust error but just accept use)
+
+### Test the server
+
+Todo: add information here \ No newline at end of file
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client.v b/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client.v
new file mode 100644
index 0000000..c65fdab
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client.v
@@ -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) }
+}
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client_wss.v b/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client_wss.v
new file mode 100644
index 0000000..c7a3c25
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_client_wss.v
@@ -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) }
+}
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_server.v b/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_server.v
new file mode 100644
index 0000000..0493ca9
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/autobahn_server.v
@@ -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) }
+}
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/docker-compose.yml b/v_windows/v/vlib/net/websocket/tests/autobahn/docker-compose.yml
new file mode 100644
index 0000000..30b58ec
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/docker-compose.yml
@@ -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: ../../../../../
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/Dockerfile b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/Dockerfile
new file mode 100644
index 0000000..ca5201b
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/Dockerfile
@@ -0,0 +1,5 @@
+FROM crossbario/autobahn-testsuite
+COPY check_results.py /check_results.py
+RUN chmod +x /check_results.py
+
+COPY config /config
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/check_results.py b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/check_results.py
new file mode 100644
index 0000000..9275c3c
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/check_results.py
@@ -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
+ )
+)
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingclient.json b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingclient.json
new file mode 100644
index 0000000..b5efbb8
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingclient.json
@@ -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": {}
+} \ No newline at end of file
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingserver.json b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingserver.json
new file mode 100644
index 0000000..3b044a1
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server/config/fuzzingserver.json
@@ -0,0 +1,14 @@
+{
+ "url": "ws://127.0.0.1:9001",
+ "outdir": "./reports/clients",
+ "cases": [
+ "*"
+ ],
+ "exclude-cases": [
+ "9.*",
+ "11.*",
+ "12.*",
+ "13.*"
+ ],
+ "exclude-agent-cases": {}
+} \ No newline at end of file
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/Dockerfile b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/Dockerfile
new file mode 100644
index 0000000..67114c4
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/Dockerfile
@@ -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 \ No newline at end of file
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/check_results.py b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/check_results.py
new file mode 100644
index 0000000..d75904c
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/check_results.py
@@ -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
+ )
+)
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/fuzzingserver.json b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/fuzzingserver.json
new file mode 100644
index 0000000..494dfff
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/fuzzingserver.json
@@ -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": {}
+} \ No newline at end of file
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.crt b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.crt
new file mode 100644
index 0000000..d4071d1
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.crt
@@ -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-----
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.csr b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.csr
new file mode 100644
index 0000000..6013ea9
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.csr
@@ -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-----
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.key b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.key
new file mode 100644
index 0000000..05c9d77
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAnbysLfcIr9+wpoJjb5r728j2e07agedOzh8VLuGnHqmKOUPN
+f8Ik707kEoBcFY7UM2A9G/1RMIysGp8eleQLMtNdeYc3KlKHBGFrOM3i4gCd7G44
+lERuKP1PKzRQ6RdVNUXn51XjfxjHWo7kHCEVvZowxvzxLxhwbSwmEmgzcQ1T6vj6
+Cdop87sdq00F+eOCfTdy+cl+R65sbImVdfY4EQ0QWAVdF3X6njLjpdmteppggbEa
+ECv3R3qNIV7/rflIPm1efbqp7R1ugvjLPJZ1u12ovtqkgsWbnEyzST8hbEEjsOTJ
+/cPkH2DaLdh7fMgfcVmqnYXd9T+gpsNGv98DjwIDAQABAoIBAE+IFfiHGiYzT0pl
+a+WV62+CAGVj+OCO1Dkxiui8dhsLuNnuyeqk5SKUUILTnZpxDaVp3OYD76/e/dfe
+avmApfTWhccE2lfIjLM0u29EwCTb0sSnPnfjmPep4QUTt8gPL7NQsAEAWVh4Eewj
+J/jW5bNXz0hFuQXZ+LXTEM8vIuDY4M0RX/jhEcCVr3QH8Sp/6JEeRY2Mbn5Z6LZ+
+BVuu8e4sCpamWOOWfoIQq3e3TbATFSNP9vzPLKvxwwAw9g5dAKPn3dvem8ofzaaF
+MeJ6T485mnx0naBrI+1qHLb3QcRpSZp6uEOp/4uvkCFm9S3dBGIwOGwHcybWFfFr
+StPfccECgYEAzN2f1BcvL3rt4970lG/MGNeLMpF7h7aWca0DzUNY5sCh+kvENHrD
+U4nH5EHoqxB1c036LKBhsrrrk5F/eQ8M+QEqpKUfqAYUrfy+HRAAeTYbhLkCysrL
++X/mlqYeyzMHj4Pjy5rqoy2TnJFnfIZYwYOL/OfA9IPwGpW2rxVSk1cCgYEAxRul
+9j0Ii3Te08TprfriDpAFQyLH54vqVwe8mkox3cdOyYvUNHdEmDNh3/7dadxVKsIx
+gIkPdGcizOw4elLKWnNFQN3+dCc3LN/zhsop0a6Ow2IatWQ8qOSqNYtD2DGj0w3j
+cJ/BZfacpr/OkAv0kjanYw4+ZSIH/r3Vjdli5okCgYBXltni4Ba4giJ7rrN7U2E7
+rcxBzpm2KIaiC4r4k7bK0clvLj2xAlvIt7vTB6rmmJ7esZQoyFl9BRX7fdW2eIzf
+WXRV+JNUT2VADjNqUZEiQdP6Ju/erF4RSnHYLyYzUpoE7irSvmVbZv0Zj8FjKD2C
+Xy/W7W8+G7roYuI8cS1g+QKBgQCDoHwK3SU4o9ouB0CZ64FMgkbRV4exi9D5P3Rm
+gIeed/uYQiV6x+7pyN5ijDtl9zp0rGwMTvsgG8O0n0b0AReaoYGs2NKU1J9W+1MQ
+Py8AFJbHyVrWqVKM4u77hL3QwQ2K4qpwym6HXdGs1UfnD+TKQ28yig+Gz9wQ9MqI
+yJPwKQKBgQCmZxhmX1SUe3DVnVulMHDLUldbRbFns0VZLiSDhY+hjOAEmnvEdEHp
+6L8/gvdTqUPF/VZQSQiZlii1oTIapQClI2oLfHcGytSorB+bpL7PxAKABp0pA6BS
+JkXzEiV1h5anbxiwid5ZICt6QGQvGvBF7b1VSb+8p9WglLBWZo36pw==
+-----END RSA PRIVATE KEY-----
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.pem b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.pem
new file mode 100644
index 0000000..d4071d1
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.pem
@@ -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-----
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/Dockerfile b/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/Dockerfile
new file mode 100644
index 0000000..ee39644
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/Dockerfile
@@ -0,0 +1,12 @@
+# 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-get update && \
+ apt-get install -y \
+ docker \
+ docker-compose \ No newline at end of file
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client.v b/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client.v
new file mode 100644
index 0000000..ef5b281
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client.v
@@ -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) }
+}
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client_wss.v b/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client_wss.v
new file mode 100644
index 0000000..c7a3c25
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/local_run/autobahn_client_wss.v
@@ -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) }
+}
diff --git a/v_windows/v/vlib/net/websocket/tests/autobahn/ws_test/Dockerfile b/v_windows/v/vlib/net/websocket/tests/autobahn/ws_test/Dockerfile
new file mode 100644
index 0000000..b57cffd
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/tests/autobahn/ws_test/Dockerfile
@@ -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" ]
diff --git a/v_windows/v/vlib/net/websocket/uri.v b/v_windows/v/vlib/net/websocket/uri.v
new file mode 100644
index 0000000..7d388e1
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/uri.v
@@ -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
+}
diff --git a/v_windows/v/vlib/net/websocket/utils.v b/v_windows/v/vlib/net/websocket/utils.v
new file mode 100644
index 0000000..4e48359
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/utils.v
@@ -0,0 +1,54 @@
+module websocket
+
+import rand
+import crypto.sha1
+import encoding.base64
+
+// htonl64 converts payload length to header bits
+fn htonl64(payload_len u64) []byte {
+ mut ret := []byte{len: 8}
+ ret[0] = byte(((payload_len & (u64(0xff) << 56)) >> 56) & 0xff)
+ ret[1] = byte(((payload_len & (u64(0xff) << 48)) >> 48) & 0xff)
+ ret[2] = byte(((payload_len & (u64(0xff) << 40)) >> 40) & 0xff)
+ ret[3] = byte(((payload_len & (u64(0xff) << 32)) >> 32) & 0xff)
+ ret[4] = byte(((payload_len & (u64(0xff) << 24)) >> 24) & 0xff)
+ ret[5] = byte(((payload_len & (u64(0xff) << 16)) >> 16) & 0xff)
+ ret[6] = byte(((payload_len & (u64(0xff) << 8)) >> 8) & 0xff)
+ ret[7] = byte(((payload_len & (u64(0xff) << 0)) >> 0) & 0xff)
+ return ret
+}
+
+// create_masking_key returs a new masking key to use when masking websocket messages
+fn create_masking_key() []byte {
+ mask_bit := byte(rand.intn(255))
+ buf := []byte{len: 4, init: `0`}
+ unsafe { C.memcpy(buf.data, &mask_bit, 4) }
+ return buf
+}
+
+// create_key_challenge_response creates a key challange response from security key
+fn create_key_challenge_response(seckey string) ?string {
+ if seckey.len == 0 {
+ return error('unexpected seckey lengt 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 {
+ mut nonce := []byte{len: nonce_size, cap: nonce_size}
+ alphanum := '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz'
+ for i in 0 .. nonce_size {
+ nonce[i] = alphanum[rand.intn(alphanum.len)]
+ }
+ return unsafe { tos(nonce.data, nonce.len) }.clone()
+}
diff --git a/v_windows/v/vlib/net/websocket/websocket_client.v b/v_windows/v/vlib/net/websocket/websocket_client.v
new file mode 100644
index 0000000..48f8c5c
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/websocket_client.v
@@ -0,0 +1,488 @@
+// websocket module implements websocket client and a websocket server
+// attribution: @thecoderr the author of original websocket client
+[manualfree]
+module websocket
+
+import net
+import net.http
+import net.openssl
+import net.urllib
+import time
+import log
+import rand
+
+const (
+ empty_bytearr = []byte{} // used as empty response to avoid allocation
+)
+
+// Client represents websocket client
+pub struct Client {
+ is_server bool
+mut:
+ ssl_conn &openssl.SSLConn // secure connection used when wss is used
+ 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
+pub mut:
+ header http.Header // headers that will be passed when connecting
+ conn &net.TcpConn // 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
+ state State // current state of connection
+ logger &log.Log // logger used to log messages
+ 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 []byte // 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
+}
+
+// new_client instance a new websocket client
+pub fn new_client(address string) ?&Client {
+ uri := parse_uri(address) ?
+ return &Client{
+ conn: 0
+ is_server: false
+ ssl_conn: openssl.new_ssl_conn()
+ is_ssl: address.starts_with('wss')
+ logger: &log.Log{
+ level: .info
+ }
+ uri: uri
+ state: .closed
+ id: rand.uuid_v4()
+ header: http.new_header()
+ }
+}
+
+// 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() ?
+ // Todo: make setting configurable
+ ws.conn.set_read_timeout(time.second * 30)
+ ws.conn.set_write_timeout(time.second * 30)
+ 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 := 'Starting client listener, server($ws.is_server)...'
+ ws.logger.info(log)
+ unsafe { log.free() }
+ defer {
+ ws.logger.info('Quit client listener, server($ws.is_server)...')
+ if ws.state == .open {
+ ws.close(1000, 'closed by client') or {}
+ }
+ }
+ for ws.state == .open {
+ msg := ws.read_next_message() or {
+ if ws.state in [.closed, .closing] {
+ return
+ }
+ ws.debug_log('failed to read next message: $err')
+ ws.send_error_event('failed to read next message: $err')
+ return err
+ }
+ if ws.state in [.closed, .closing] {
+ return
+ }
+ ws.debug_log('got message: $msg.opcode')
+ match msg.opcode {
+ .text_frame {
+ log = 'read: text'
+ ws.debug_log(log)
+ unsafe { log.free() }
+ 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 {
+ log = 'read: close'
+ ws.debug_log(log)
+ unsafe { log.free() }
+ 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 := (int(msg.payload[0]) << 8) + int(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 { []byte{} }
+ if reason.len > 0 {
+ ws.validate_utf_8(.close, reason) ?
+ }
+ if ws.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.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 &byte, payload_len int, code OPCode) ?int {
+ // ws.debug_log('write_ptr code: $code')
+ if ws.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 := []byte{len: header_len, init: `0`} // [`0`].repeat(header_len)
+ header[0] = byte(int(code)) | 0x80
+ masking_key := create_masking_key()
+ if ws.is_server {
+ if payload_len <= 125 {
+ header[1] = byte(payload_len)
+ } else if payload_len > 125 && payload_len <= 0xffff {
+ len16 := C.htons(payload_len)
+ header[1] = 126
+ unsafe { C.memcpy(&header[2], &len16, 2) }
+ } else if payload_len > 0xffff && payload_len <= 0x7fffffff {
+ len_bytes := htonl64(u64(payload_len))
+ header[1] = 127
+ unsafe { C.memcpy(&header[2], len_bytes.data, 8) }
+ }
+ } else {
+ if payload_len <= 125 {
+ header[1] = byte(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 := C.htons(payload_len)
+ header[1] = (126 | 0x80)
+ unsafe { C.memcpy(&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 { C.memcpy(&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 := []byte{len: len}
+ unsafe {
+ C.memcpy(&frame_buf[0], &byte(header.data), header.len)
+ if payload_len > 0 {
+ C.memcpy(&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 []byte, code OPCode) ?int {
+ return ws.write_ptr(&byte(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')
+ if ws.state in [.closed, .closing] || ws.conn.sock.handle <= 1 {
+ ws.debug_log('close: Websocket allready closed ($ws.state), $message, $code handle($ws.conn.sock.handle)')
+ err_msg := 'Socket allready closed: $code'
+ return error(err_msg)
+ }
+ defer {
+ ws.shutdown_socket() or {}
+ ws.reset_state()
+ }
+ ws.set_state(.closing)
+ // mut code32 := 0
+ if code > 0 {
+ code_ := C.htons(code)
+ message_len := message.len + 2
+ mut close_frame := []byte{len: message_len}
+ close_frame[0] = byte(code_ & 0xFF)
+ close_frame[1] = byte(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 []byte) ? {
+ ws.debug_log('send control frame $code, frame_type: $frame_typ')
+ if ws.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 := []byte{len: frame_len}
+ mut masking_key := if !ws.is_server { create_masking_key() } else { websocket.empty_bytearr }
+ defer {
+ unsafe {
+ control_frame.free()
+ if masking_key.len > 0 {
+ masking_key.free()
+ }
+ }
+ }
+ control_frame[0] = byte(int(code) | 0x80)
+ if !ws.is_server {
+ control_frame[1] = byte(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] = byte(payload.len)
+ }
+ if code == .close {
+ if payload.len >= 2 {
+ if !ws.is_server {
+ mut parsed_payload := []byte{len: payload.len + 1}
+ unsafe { C.memcpy(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 { C.memcpy(&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 { C.memcpy(&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
+fn (mut ws Client) set_state(state State) {
+ lock {
+ ws.state = state
+ }
+}
+
+// assert_not_connected returns error if the connection is not connected
+fn (ws Client) assert_not_connected() ? {
+ match ws.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
+fn (mut ws Client) reset_state() {
+ lock {
+ ws.state = .closed
+ ws.ssl_conn = openssl.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()
+ }
+}
diff --git a/v_windows/v/vlib/net/websocket/websocket_nix.c.v b/v_windows/v/vlib/net/websocket/websocket_nix.c.v
new file mode 100644
index 0000000..f986b98
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/websocket_nix.c.v
@@ -0,0 +1,10 @@
+module websocket
+
+// error_code returns the error code
+fn error_code() int {
+ return C.errno
+}
+
+const (
+ error_ewouldblock = C.EWOULDBLOCK // blocking error code
+)
diff --git a/v_windows/v/vlib/net/websocket/websocket_server.v b/v_windows/v/vlib/net/websocket/websocket_server.v
new file mode 100644
index 0000000..99af3e0
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/websocket_server.v
@@ -0,0 +1,189 @@
+module websocket
+
+import net
+import net.openssl
+import log
+import time
+import rand
+
+// Server represents a websocket server connection
+pub struct Server {
+mut:
+ logger &log.Log // logger used to log
+ ls &net.TcpListener // listener used to get incoming connection to socket
+ accept_client_callbacks []AcceptClientFn // accept client callback functions
+ 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:
+ clients map[string]&ServerClient // clients connected to this server
+ ping_interval int = 30 // interval for sending ping to clients (seconds)
+ state State // current state of connection
+}
+
+// ServerClient represents a connected client
+struct ServerClient {
+pub:
+ resource_name string // resource that the client access
+ client_key string // unique key of client
+pub mut:
+ server &Server
+ client &Client
+}
+
+// new_server instance a new websocket server on provided port and route
+pub fn new_server(family net.AddrFamily, port int, route string) &Server {
+ return &Server{
+ ls: 0
+ family: family
+ port: port
+ logger: &log.Log{
+ level: .info
+ }
+ state: .closed
+ }
+}
+
+// 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) {
+ s.ping_interval = seconds
+}
+
+// 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)
+ go s.handle_ping()
+ for {
+ mut c := s.accept_new_client() or { continue }
+ go 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.state == .open {
+ time.sleep(s.ping_interval * time.second)
+ for i, _ in s.clients {
+ mut c := s.clients[i]
+ if c.client.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.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.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) ?
+ accept := s.send_connect_event(mut server_client) ?
+ if !accept {
+ s.logger.debug('server-> client not accepted')
+ c.shutdown_socket() ?
+ return
+ }
+ // the client is accepted
+ c.socket_write(handshake_response.bytes()) ?
+ lock {
+ s.clients[server_client.client.id] = server_client
+ }
+ s.setup_callbacks(mut server_client)
+ c.listen() or {
+ s.logger.error(err.msg)
+ 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(fn (mut c Client, code int, reason string, mut sc ServerClient) ? {
+ c.logger.debug('server-> Delete client')
+ lock {
+ sc.server.clients.delete(sc.client.id)
+ }
+ }, sc)
+}
+
+// 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: openssl.new_ssl_conn()
+ logger: s.logger
+ state: .open
+ last_pong_ut: time.now().unix
+ id: rand.uuid_v4()
+ }
+ return c
+}
+
+// set_state sets current state in a thread safe way
+fn (mut s Server) set_state(state State) {
+ lock {
+ s.state = state
+ }
+}
+
+// free manages manual free of memory for Server instance
+pub fn (mut s Server) free() {
+ unsafe {
+ s.clients.free()
+ s.accept_client_callbacks.free()
+ s.message_callbacks.free()
+ s.close_callbacks.free()
+ }
+}
diff --git a/v_windows/v/vlib/net/websocket/websocket_test.v b/v_windows/v/vlib/net/websocket/websocket_test.v
new file mode 100644
index 0000000..35e15d3
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/websocket_test.v
@@ -0,0 +1,122 @@
+import os
+import net
+import net.websocket
+import time
+import rand
+
+// TODO: fix connecting to ipv4 websockets
+// (the server seems to work with .ip, but
+// Client can not connect, it needs to be passed
+// .ip too?)
+
+struct WebsocketTestResults {
+pub mut:
+ nr_messages int
+ nr_pong_received int
+}
+
+// Do not run these tests everytime, since they are flaky.
+// They have their own specialized CI runner.
+const github_job = os.getenv('GITHUB_JOB')
+
+const should_skip = github_job != '' && github_job != 'websocket_tests'
+
+// tests with internal ws servers
+fn test_ws_ipv6() {
+ if should_skip {
+ return
+ }
+ port := 30000 + rand.intn(1024)
+ go start_server(.ip6, port)
+ time.sleep(500 * time.millisecond)
+ ws_test(.ip6, 'ws://localhost:$port') or { assert false }
+}
+
+// tests with internal ws servers
+fn test_ws_ipv4() {
+ // TODO: fix client
+ if true || should_skip {
+ return
+ }
+ port := 30000 + rand.intn(1024)
+ go start_server(.ip, port)
+ time.sleep(500 * time.millisecond)
+ ws_test(.ip, 'ws://localhost:$port') or { assert false }
+}
+
+fn start_server(family net.AddrFamily, listen_port int) ? {
+ mut s := websocket.new_server(family, listen_port, '')
+ // make that in execution test time give time to execute at least one time
+ s.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') or { panic(err) } }
+ else { ws.write(msg.payload, msg.opcode) or { panic(err) } }
+ }
+ })
+
+ s.on_close(fn (mut ws websocket.Client, code int, reason string) ? {
+ // not used
+ })
+ s.listen() or {}
+}
+
+// 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 ws := websocket.new_client(uri) ?
+ ws.on_open(fn (mut ws websocket.Client) ? {
+ ws.pong() ?
+ assert true
+ })
+ ws.on_error(fn (mut ws websocket.Client, err string) ? {
+ println('error: $err')
+ // this can be thrown by internet connection problems
+ assert false
+ })
+
+ ws.on_message_ref(fn (mut ws 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')
+ }
+ }, test_results)
+ ws.connect() or { panic('fail to connect') }
+ go ws.listen()
+ text := ['a'].repeat(2)
+ for msg in text {
+ ws.write(msg.bytes(), .text_frame) or { panic('fail to write to websocket') }
+ // sleep to give time to recieve response before send a new one
+ time.sleep(100 * time.millisecond)
+ }
+ // sleep to give time to recieve 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
+}
diff --git a/v_windows/v/vlib/net/websocket/websocket_windows.c.v b/v_windows/v/vlib/net/websocket/websocket_windows.c.v
new file mode 100644
index 0000000..e9f4fc3
--- /dev/null
+++ b/v_windows/v/vlib/net/websocket/websocket_windows.c.v
@@ -0,0 +1,12 @@
+module websocket
+
+import net
+
+// error_code returns the error code
+fn error_code() int {
+ return C.WSAGetLastError()
+}
+
+const (
+ error_ewouldblock = net.WsaError.wsaewouldblock // blocking error code
+)