From 4ae59c97161dadf85a82b49b38102d657f31d240 Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Mon, 29 Dec 2025 23:00:53 -0600 Subject: [PATCH] feat: implement WebSocketInterface for handling WebSocket connections in WASM environment --- pkg/interfaces/websocket_wasm.go | 261 +++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 pkg/interfaces/websocket_wasm.go diff --git a/pkg/interfaces/websocket_wasm.go b/pkg/interfaces/websocket_wasm.go new file mode 100644 index 0000000..bc922f0 --- /dev/null +++ b/pkg/interfaces/websocket_wasm.go @@ -0,0 +1,261 @@ +//go:build js && wasm +// +build js,wasm + +package interfaces + +import ( + "encoding/binary" + "fmt" + "net" + "sync" + "syscall/js" + "time" + + "git.quad4.io/Networks/Reticulum-Go/pkg/common" + "git.quad4.io/Networks/Reticulum-Go/pkg/debug" +) + +type WebSocketInterface struct { + BaseInterface + wsURL string + ws js.Value + connected bool + mutex sync.RWMutex + messageQueue [][]byte +} + +func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) { + ws := &WebSocketInterface{ + BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled), + wsURL: wsURL, + messageQueue: make([][]byte, 0), + } + + ws.MTU = 1064 + ws.Bitrate = 10000000 + + return ws, nil +} + +func (wsi *WebSocketInterface) GetName() string { + return wsi.Name +} + +func (wsi *WebSocketInterface) GetType() common.InterfaceType { + return wsi.Type +} + +func (wsi *WebSocketInterface) GetMode() common.InterfaceMode { + return wsi.Mode +} + +func (wsi *WebSocketInterface) IsOnline() bool { + wsi.mutex.RLock() + defer wsi.mutex.RUnlock() + return wsi.Online && wsi.connected +} + +func (wsi *WebSocketInterface) IsDetached() bool { + wsi.mutex.RLock() + defer wsi.mutex.RUnlock() + return wsi.Detached +} + +func (wsi *WebSocketInterface) Detach() { + wsi.mutex.Lock() + defer wsi.mutex.Unlock() + wsi.Detached = true + wsi.Online = false + wsi.closeWebSocket() +} + +func (wsi *WebSocketInterface) Enable() { + wsi.mutex.Lock() + defer wsi.mutex.Unlock() + wsi.Enabled = true +} + +func (wsi *WebSocketInterface) Disable() { + wsi.mutex.Lock() + defer wsi.mutex.Unlock() + wsi.Enabled = false + wsi.closeWebSocket() +} + +func (wsi *WebSocketInterface) Start() error { + wsi.mutex.Lock() + defer wsi.mutex.Unlock() + + if wsi.ws.Truthy() { + return fmt.Errorf("WebSocket already started") + } + + ws := js.Global().Get("WebSocket").New(wsi.wsURL) + ws.Set("binaryType", "arraybuffer") + + ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + wsi.mutex.Lock() + wsi.connected = true + wsi.Online = true + wsi.mutex.Unlock() + + debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL) + + wsi.mutex.Lock() + queue := make([][]byte, len(wsi.messageQueue)) + copy(queue, wsi.messageQueue) + wsi.messageQueue = wsi.messageQueue[:0] + wsi.mutex.Unlock() + + for _, msg := range queue { + wsi.sendWebSocketMessage(msg) + } + + return nil + })) + + ws.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + if len(args) < 1 { + return nil + } + + event := args[0] + data := event.Get("data") + + var packetData []byte + if data.Type() == js.TypeString { + packetData = []byte(data.String()) + } else if data.Type() == js.TypeObject { + array := js.Global().Get("Uint8Array").New(data) + length := array.Get("length").Int() + packetData = make([]byte, length) + js.CopyBytesToGo(packetData, array) + } else { + debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String()) + return nil + } + + if len(packetData) < 4 { + debug.Log(debug.DEBUG_ERROR, "WebSocket message too short", "bytes", len(packetData)) + return nil + } + + packetLen := binary.BigEndian.Uint32(packetData[:4]) + if len(packetData) < int(packetLen)+4 { + debug.Log(debug.DEBUG_ERROR, "WebSocket message incomplete", "expected", packetLen+4, "got", len(packetData)) + return nil + } + + packet := packetData[4 : 4+packetLen] + + wsi.mutex.Lock() + wsi.RxBytes += uint64(len(packet)) + wsi.mutex.Unlock() + + wsi.ProcessIncoming(packet) + + return nil + })) + + ws.Set("onerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + debug.Log(debug.DEBUG_ERROR, "WebSocket error", "name", wsi.Name) + return nil + })) + + ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + wsi.mutex.Lock() + wsi.connected = false + wsi.Online = false + wsi.mutex.Unlock() + + debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name) + + if wsi.Enabled && !wsi.Detached { + time.Sleep(2 * time.Second) + go wsi.Start() + } + + return nil + })) + + wsi.ws = ws + + return nil +} + +func (wsi *WebSocketInterface) Stop() error { + wsi.mutex.Lock() + defer wsi.mutex.Unlock() + wsi.Enabled = false + wsi.closeWebSocket() + return nil +} + +func (wsi *WebSocketInterface) closeWebSocket() { + if wsi.ws.Truthy() { + wsi.ws.Call("close") + wsi.ws = js.Value{} + } + wsi.connected = false + wsi.Online = false +} + +func (wsi *WebSocketInterface) Send(data []byte, addr string) error { + if !wsi.IsEnabled() { + return fmt.Errorf("interface not enabled") + } + + wsi.mutex.Lock() + wsi.TxBytes += uint64(len(data)) + wsi.mutex.Unlock() + + if !wsi.connected { + wsi.mutex.Lock() + wsi.messageQueue = append(wsi.messageQueue, data) + wsi.mutex.Unlock() + return nil + } + + return wsi.sendWebSocketMessage(data) +} + +func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error { + if !wsi.ws.Truthy() { + return fmt.Errorf("WebSocket not initialized") + } + + if wsi.ws.Get("readyState").Int() != 1 { + return fmt.Errorf("WebSocket not open") + } + + packetLen := uint32(len(data)) + packet := make([]byte, 4+len(data)) + binary.BigEndian.PutUint32(packet[:4], packetLen) + copy(packet[4:], data) + + array := js.Global().Get("Uint8Array").New(len(packet)) + js.CopyBytesToJS(array, packet) + + wsi.ws.Call("send", array) + + debug.Log(debug.DEBUG_VERBOSE, "WebSocket sent packet", "name", wsi.Name, "bytes", len(data)) + return nil +} + +func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error { + return wsi.Send(data, "") +} + +func (wsi *WebSocketInterface) GetConn() net.Conn { + return nil +} + +func (wsi *WebSocketInterface) GetMTU() int { + return wsi.MTU +} + +func (wsi *WebSocketInterface) IsEnabled() bool { + wsi.mutex.RLock() + defer wsi.mutex.RUnlock() + return wsi.Enabled && wsi.Online && !wsi.Detached +}