From 0bfe2955935c9063432edf938b9c2a575a66e402 Mon Sep 17 00:00:00 2001 From: ZealousProgramming Date: Tue, 9 Apr 2024 08:47:15 -0400 Subject: [PATCH] Start working on init headers and tests --- README.md | 15 +++++++- build.bat | 2 +- client.odin | 20 +++++++++++ client/client.odin | 6 ++-- headers.odin | 58 +++++++++++++++++++++++++++++++ main.odin | 48 ++++++-------------------- ols.json | 29 +++++++++++++++- run.bat | 2 +- server.odin | 1 + server/server.odin | 78 ++++++++++++++++++++++++++++++++++++++++++ test.bat | 2 ++ tests/client_test.odin | 30 ++++++++++++++++ tests/server_test.odin | 8 +++++ websocket.odin | 8 +++++ 14 files changed, 263 insertions(+), 44 deletions(-) create mode 100644 client.odin create mode 100644 headers.odin create mode 100644 server.odin create mode 100644 server/server.odin create mode 100644 test.bat create mode 100644 tests/client_test.odin create mode 100644 tests/server_test.odin create mode 100644 websocket.odin diff --git a/README.md b/README.md index 42f6a92..64f8e17 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ # odin-websocket -A basic websocket implementation in Odin +A basic websocket implementation in Odin using Laytan's [`odin-http`](https://github.com/laytan/odin-http). Inching(SLOWLY) closer to compliance with [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455#autoid-4) + + +# TODOs +- SHORT TERM GOALS + - [ ] WebSocket URIs + - [ ] Opening Connection + - [ ] Opening Handshake + - [ ] Closing Connection + - [ ] Closing Handshake + - [ ] Data Framing +- LONG TERM GOALS + - [ ] Extensions + - [ ] HTTP upgrade (LONG TERM; Odin doesn't currently support HTTP out-of-the-box) diff --git a/build.bat b/build.bat index e18c433..98061e5 100644 --- a/build.bat +++ b/build.bat @@ -1,2 +1,2 @@ @ECHO OFF -odin build . -file -out:./bin/odin-websocket.exe -debug \ No newline at end of file +odin build . -file -out:./bin/websocket.exe -debug \ No newline at end of file diff --git a/client.odin b/client.odin new file mode 100644 index 0000000..dcce272 --- /dev/null +++ b/client.odin @@ -0,0 +1,20 @@ +package websocket + +import "core:net" +import http "shared:odin-http" + +WebsocketClient :: struct { + connection: Maybe(net.Socket), +} + +client_init :: proc( + request: ^http.Request, + allocator := context.allocator, +) -> ^WebsocketClient { + client := new(WebsocketClient, allocator) + return client +} + +client_free :: proc(client: ^WebsocketClient, allocator := context.allocator) { + free(client) +} diff --git a/client/client.odin b/client/client.odin index 4c01b5f..5c27ed5 100644 --- a/client/client.odin +++ b/client/client.odin @@ -6,14 +6,14 @@ import "core:fmt" import "core:net" import "core:strings" -ENDPOINT :: "127.0.0.1:3000" +SERVER_ENDPOINT :: "127.0.0.1:80" INITIAL_MESSAGE :: "pong" main :: proc() { - fmt.printf("[odin-websocket] Connecting to %s..\n", ENDPOINT) + fmt.printf("[odin-websocket] Connecting to %s..\n", SERVER_ENDPOINT) - socket, err := net.dial_tcp(ENDPOINT) + socket, err := net.dial_tcp(SERVER_ENDPOINT) fmt.assertf(err == nil, "dial error %v", err) defer net.close(socket) diff --git a/headers.odin b/headers.odin new file mode 100644 index 0000000..9e20c0d --- /dev/null +++ b/headers.odin @@ -0,0 +1,58 @@ +package websocket + +import "core:encoding/base64" + +import "core:/log" +import http "shared:odin-http" + +WebSocketHeaders :: http.Headers +WEBSOCKET_VERSION :: "13" + +apply_websocket_headers :: proc( + headers: ^WebSocketHeaders, + request_headers: ^http.Headers, +) -> WebSocketError { + if headers == nil {return .Headers_Nil} + + challenge, challenge_err := challenge_key() + + if challenge_err != .None { + log.errorf( + "[odin-websocket] Failed to generate Challenge Key: %v\n", + challenge_err, + ) + return .Challenge_Key_Nil + } + + http.headers_set(headers, "Upgrade", "websocket") + http.headers_set(headers, "Connection", "Upgrade") + http.headers_set(headers, "Sec-WebSocket-Key", challenge) + http.headers_set(headers, "Sec-WebSocket-Version", WEBSOCKET_VERSION) + + // TODO(devon): Protocols + // TODO(devon): Extensions + + return nil +} + +/** +See [Secition 4.1, Bullet 7 of RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455#autoid-15) +*/ +challenge_key :: proc( + allocator := context.allocator, +) -> ( + challenge: string, + challenge_err: WebSocketError, +) { + b, alloc_err := make([]u8, 16, allocator) + if alloc_err != nil { + log.errorf( + "[odin-websocket] Allocator error occurred attempting to generate challenge key: %v\n", + alloc_err, + ) + return "", .Allocator_Failure + } + + key := base64.encode(b[:], base64.ENC_TABLE, allocator) + return key, .None +} diff --git a/main.odin b/main.odin index 22c94dd..a57ceda 100644 --- a/main.odin +++ b/main.odin @@ -1,25 +1,23 @@ -package main +package websocket -//import "core:encoding/base64" import "core:fmt" import "core:net" -import "core:strings" ADDRESS: net.IP4_Address : net.IP4_Address{127, 0, 0, 1} PORT :: 8573 ENDPOINT :: "127.0.0.1:3000" -INITIAL_MESSAGE :: "ping" +INITIAL_MESSAGE :: "Hellolove" main :: proc() { - fmt.println("[odin-websocket] Setting up server") + fmt.println("[odin-websocket] Hello from the otherside") endpoint, endpoint_err := net.parse_endpoint(ENDPOINT) - if !endpoint_err { - fmt.eprintln("NETWORK ERROR: Failed to parse endpoint") + if endpoint_err { + fmt.eprintln("NETWORK ERROR: Failed to parse endpoints") } - socket, listen_err := net.listen_tcp(endpoint) + tcp_socket, listen_err := net.listen_tcp(endpoint) if listen_err != nil { fmt.eprintf("NETWORK ERROR: Failed to listen - %s\n", listen_err) } @@ -28,52 +26,28 @@ main :: proc() { copy(buffer[:], INITIAL_MESSAGE) for { - connection, _, accept_err := net.accept_tcp(socket) + connection, _, accept_err := net.accept_tcp(tcp_socket) if accept_err != nil { fmt.eprintf( "NETWORK ERROR: Failed to accept incoming connection - %s\n", accept_err, ) } else { - fmt.printf("Incoming Connection: %v\n", connection) + fmt.println("Incoming Connection: %v", connection) } - bytes_written, send_err := net.send_tcp(connection, buffer[:]) + bytes_written, send_err := net.send_tcp(tcp_socket, buffer[:]) if send_err != nil { fmt.eprintf( "NETWORK ERROR: Failed to send message - %s\n", accept_err, ) + return } else { fmt.println("Incoming Connection: %v", connection) } - - received, rerr := net.recv(connection, buffer[:]) - if rerr != nil { - fmt.printf("recv error %v\n", rerr) - return - } - - if (received > 0) { - fmt.println("Received message of length:", received) - - message, alloc_err := strings.clone_from_bytes( - buffer[:], - context.temp_allocator, - ) - if alloc_err != nil { - fmt.eprintf( - "Failed to convert bytes to string: %v\n", - alloc_err, - ) - return - } else { - fmt.println("Received message with content:", message) - } - - return - } } } + diff --git a/ols.json b/ols.json index 9b8b918..45bd7b4 100644 --- a/ols.json +++ b/ols.json @@ -1 +1,28 @@ -{"collections": [{"name": "core","path": "C:\\programming\\odin\\Odin\\core"},{"name": "vendor","path": "C:\\programming\\odin\\Odin\\vendor"}],"enable_document_symbols": true, "enable_semantic_tokens": true, "enable_inlay_hints": true, "enable_procedure_snippet": true, "enable_hover": true, "enable_snippets": true, "enable_format": true, "formatter": { "tabs": true, "tabs_width": 4, "character_width": 80 }} \ No newline at end of file +{ + "collections": [ + { + "name": "core", + "path": "C:\\programming\\odin\\Odin\\core" + }, + { + "name": "vendor", + "path": "C:\\programming\\odin\\Odin\\vendor" + }, + { + "name": "shared", + "path": "C:\\programming\\odin\\Odin\\shared" + } + ], + "enable_document_symbols": true, + "enable_semantic_tokens": true, + "enable_inlay_hints": true, + "enable_procedure_snippet": true, + "enable_hover": true, + "enable_snippets": true, + "enable_format": true, + "formatter": { + "tabs": true, + "tabs_width": 4, + "character_width": 80 + } +} diff --git a/run.bat b/run.bat index 49e5d9e..cfc1066 100644 --- a/run.bat +++ b/run.bat @@ -1,2 +1,2 @@ @ECHO OFF -odin run . -file -out:./bin/odin-websocket.exe -debug \ No newline at end of file +odin run . -file -out:./bin/websocket.exe -debug \ No newline at end of file diff --git a/server.odin b/server.odin new file mode 100644 index 0000000..708bc8c --- /dev/null +++ b/server.odin @@ -0,0 +1 @@ +package websocket diff --git a/server/server.odin b/server/server.odin new file mode 100644 index 0000000..7f4956f --- /dev/null +++ b/server/server.odin @@ -0,0 +1,78 @@ +package main + +//import "core:encoding/base64" +import "core:fmt" +import "core:net" +import "core:strings" + +LISTENER_ENDPOINT :: "127.0.0.1:80" + +INITIAL_MESSAGE :: "ping" + +main :: proc() { + fmt.printf("[odin-websocket] Listening on %s\n", LISTENER_ENDPOINT) + + endpoint, endpoint_err := net.parse_endpoint(LISTENER_ENDPOINT) + if !endpoint_err { + fmt.eprintln("NETWORK ERROR: Failed to parse endpoint") + } + + socket, listen_err := net.listen_tcp(endpoint) + if listen_err != nil { + fmt.eprintf("NETWORK ERROR: Failed to listen - %s\n", listen_err) + } + + buffer: [len(INITIAL_MESSAGE)]u8 + copy(buffer[:], INITIAL_MESSAGE) + + for { + connection, _, accept_err := net.accept_tcp(socket) + if accept_err != nil { + fmt.eprintf( + "NETWORK ERROR: Failed to accept incoming connection - %s\n", + accept_err, + ) + } else { + fmt.printf("Incoming Connection: %v\n", connection) + } + + bytes_written, send_err := net.send_tcp(connection, buffer[:]) + if send_err != nil { + fmt.eprintf( + "NETWORK ERROR: Failed to send message - %s\n", + accept_err, + ) + return + } else { + fmt.printf("Bytes Sent: %v\n", bytes_written) + fmt.printf("Content Sent: %v\n", INITIAL_MESSAGE) + } + + received, rerr := net.recv(connection, buffer[:]) + if rerr != nil { + fmt.printf("recv error %v\n", rerr) + return + } + + if (received > 0) { + fmt.println("Received message of length:", received) + + message, alloc_err := strings.clone_from_bytes( + buffer[:], + context.temp_allocator, + ) + if alloc_err != nil { + fmt.eprintf( + "Failed to convert bytes to string: %v\n", + alloc_err, + ) + return + } else { + fmt.println("Received message with content:", message) + } + + return + } + } + +} diff --git a/test.bat b/test.bat new file mode 100644 index 0000000..f07a8fc --- /dev/null +++ b/test.bat @@ -0,0 +1,2 @@ +@ECHO OFF +odin test ./tests diff --git a/tests/client_test.odin b/tests/client_test.odin new file mode 100644 index 0000000..1885cda --- /dev/null +++ b/tests/client_test.odin @@ -0,0 +1,30 @@ +package websocket_tests + +import "core:testing" + +import http "shared:odin-http" + +import websocket "../" + + +@(test) +test_challenge_key :: proc(t: ^testing.T) { + challenge_key, err := websocket.challenge_key() + + testing.expect(t, err == .None) + testing.expect(t, challenge_key != "") + + // TODO(devon): Failure case by passing a mocked failing allocator +} + +@(test) +test_client :: proc(t: ^testing.T) { + headers := websocket.WebSocketHeaders{} + http.headers_init(&headers) + http.headers_set(&headers, "", "") + + client := websocket.client_init(nil) + testing.expect(t, client != nil) + + +} diff --git a/tests/server_test.odin b/tests/server_test.odin new file mode 100644 index 0000000..ec71689 --- /dev/null +++ b/tests/server_test.odin @@ -0,0 +1,8 @@ +package websocket_tests + +import "core:testing" + +@(test) +test_server :: proc(t: ^testing.T) { + testing.expect(t, true) +} diff --git a/websocket.odin b/websocket.odin new file mode 100644 index 0000000..09169fa --- /dev/null +++ b/websocket.odin @@ -0,0 +1,8 @@ +package websocket + +WebSocketError :: enum { + None = 0, + Allocator_Failure, + Challenge_Key_Nil, + Headers_Nil, +}