module websocket import encoding.base64 import strings // handshake manage the handshake part of connecting fn (mut ws Client) handshake() ? { nonce := get_nonce(ws.nonce_size) seckey := base64.encode(nonce) // handshake := 'GET $ws.uri.resource$ws.uri.querystring HTTP/1.1\r\nHost: $ws.uri.hostname:$ws.uri.port\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: $seckey\r\nSec-WebSocket-Version: 13\r\n\r\n' mut sb := strings.new_builder(1024) // todo, remove when autofree defer { sb.free() } sb.write('GET ') sb.write(ws.uri.resource) sb.write(ws.uri.querystring) sb.write(' HTTP/1.1\r\nHost: ') sb.write(ws.uri.hostname) sb.write(':') sb.write(ws.uri.port) sb.write('\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n') sb.write('Sec-WebSocket-Key: ') sb.write(seckey) sb.write('\r\nSec-WebSocket-Version: 13\r\n\r\n') // handshake := 'GET $ws.uri.resource$ws.uri.querystring HTTP/1.1\r\nHost: $ws.uri.hostname:$ws.uri.port\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: $seckey\r\nSec-WebSocket-Version: 13\r\n\r\n' handshake := sb.str() defer { 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()} } // handshake manage the handshake part of connecting 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 } 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) } // path := get_tokens[1].trim_space() 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: c server: s } unsafe { lines.free() flags.free() get_tokens.free() seckey.free() key.free() } return server_handshake, server_client } 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(byteptr(&buffer), 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 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()} } 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) } }