Skip to content

Commit

Permalink
Start working on init headers and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ZealousProgramming committed Apr 9, 2024
1 parent cf0ae20 commit 0bfe295
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 44 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion build.bat
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@ECHO OFF
odin build . -file -out:./bin/odin-websocket.exe -debug
odin build . -file -out:./bin/websocket.exe -debug
20 changes: 20 additions & 0 deletions client.odin
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 3 additions & 3 deletions client/client.odin
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
58 changes: 58 additions & 0 deletions headers.odin
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 11 additions & 37 deletions main.odin
Original file line number Diff line number Diff line change
@@ -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)
}
Expand All @@ -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
}
}

}

29 changes: 28 additions & 1 deletion ols.json
Original file line number Diff line number Diff line change
@@ -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 }}
{
"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
}
}
2 changes: 1 addition & 1 deletion run.bat
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@ECHO OFF
odin run . -file -out:./bin/odin-websocket.exe -debug
odin run . -file -out:./bin/websocket.exe -debug
1 change: 1 addition & 0 deletions server.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package websocket
78 changes: 78 additions & 0 deletions server/server.odin
Original file line number Diff line number Diff line change
@@ -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
}
}

}
2 changes: 2 additions & 0 deletions test.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@ECHO OFF
odin test ./tests
30 changes: 30 additions & 0 deletions tests/client_test.odin
Original file line number Diff line number Diff line change
@@ -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)


}
8 changes: 8 additions & 0 deletions tests/server_test.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package websocket_tests

import "core:testing"

@(test)
test_server :: proc(t: ^testing.T) {
testing.expect(t, true)
}
8 changes: 8 additions & 0 deletions websocket.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package websocket

WebSocketError :: enum {
None = 0,
Allocator_Failure,
Challenge_Key_Nil,
Headers_Nil,
}

0 comments on commit 0bfe295

Please sign in to comment.