package ws import ( "context" "crypto/tls" "encoding/json" "log" "reflect" "sync" "time" "github.com/gorilla/websocket" "github.com/leandrofars/oktopus/internal/db" "github.com/leandrofars/oktopus/internal/mtp/handler" usp_msg "github.com/leandrofars/oktopus/internal/usp_message" "github.com/leandrofars/oktopus/internal/usp_record" "google.golang.org/protobuf/proto" ) type Ws struct { Addr string Port string Token string Route string Auth bool TLS bool InsecureSkipVerify bool Ctx context.Context NewDeviceQueue map[string]string NewDevQMutex *sync.Mutex DB db.Database MsgQueue map[string](chan usp_msg.Msg) QMutex *sync.Mutex } const ( WS_CONNECTION_RETRY = 10 * time.Second ) const ( OFFLINE = "0" ONLINE = "1" ) type deviceStatus struct { Eid string Status string } // Global Websocket connection used in this package var wsConn *websocket.Conn func (w *Ws) Connect() { log.Println("Connecting to WS endpoint...") prefix := "ws://" if w.TLS { prefix = "wss://" } wsUrl := prefix + w.Addr + ":" + w.Port + w.Route if w.Auth { log.Println("WS token:", w.Token) // e.g. ws://localhost:8080/ws/controller?token=123456 wsUrl = wsUrl + "?token=" + w.Token } dialer := websocket.Dialer{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: w.InsecureSkipVerify, }, } // Keeps trying to connect to the WS endpoint until it succeeds or receives a stop signal go func(dialer websocket.Dialer) { for { c, _, err := dialer.Dial(wsUrl, nil) if err != nil { log.Printf("Error to connect to %s, err: %s", wsUrl, err) time.Sleep(WS_CONNECTION_RETRY) continue } // instantiate global ws connection wsConn = c log.Println("Connected to WS endpoint--> ", wsUrl) go w.Subscribe() break } }(dialer) } func (w *Ws) Disconnect() { log.Println("Disconnecting from WS endpoint...") if wsConn != nil { err := wsConn.Close() if err != nil { log.Println("Error while disconnecting from WS endpoint:", err.Error()) } log.Println("Succesfully disconnected from WS endpoint") } else { log.Println("No WS connection to close") } } // Websockets doesn't follow pub/sub architecture, but we use these // naming here to implement the Broker interface and abstract the MTP layer. /* -------------------------------------------------------------------------- */ func (w *Ws) Subscribe() { var m sync.Mutex w.NewDevQMutex = &m w.NewDeviceQueue = make(map[string]string) for { msgType, wsMsg, err := wsConn.ReadMessage() if err != nil { if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("websocket error: %v", err) w.Connect() return } log.Println("websocket unexpected error:", err) return } //TODO: deal with message in new go routine if msgType == websocket.TextMessage { var deviceStatus deviceStatus err = json.Unmarshal(wsMsg, &deviceStatus) if err != nil { log.Println("Websockets Text Message is not about devices status") } log.Println("Received device status message") var status db.Status switch deviceStatus.Status { case ONLINE: status = db.Online case OFFLINE: status = db.Offline default: log.Println("Invalid device status") return } w.DB.UpdateStatus(deviceStatus.Eid, status, db.WEBSOCKETS) //TODO: return error 1003 to device //TODO: get status messages continue } //log.Printf("binary data: %s", string(wsMsg)) //TODO: if error at processing message return error 1003 to devicec //TODO: deal with received messages in parallel var record usp_record.Record //var message usp_msg.Msg err = proto.Unmarshal(wsMsg, &record) if err != nil { log.Println(err) } connRecord := &usp_record.Record_WebsocketConnect{ WebsocketConnect: &usp_record.WebSocketConnectRecord{}, } noSessionRecord := &usp_record.Record_NoSessionContext{ NoSessionContext: &usp_record.NoSessionContextRecord{}, } //log.Printf("Record Type: %++v", record.RecordType) deviceId := record.FromId // New Device Handler if reflect.TypeOf(record.RecordType) == reflect.TypeOf(connRecord) { log.Println("Websocket new device:", deviceId) tr369Message := handler.HandleNewDevice(deviceId) w.NewDevQMutex.Lock() w.NewDeviceQueue[deviceId] = "" w.NewDevQMutex.Unlock() w.Publish(tr369Message, "", "", false) continue } //TODO: see what type of message was received if reflect.TypeOf(record.RecordType) == reflect.TypeOf(noSessionRecord) { //log.Printf("Websocket device %s message", record.FromId) // New device answer if _, ok := w.NewDeviceQueue[deviceId]; ok { log.Printf("New device %s response", deviceId) device := handler.HandleNewDevicesResponse(wsMsg, deviceId, db.WEBSOCKETS) w.NewDevQMutex.Lock() delete(w.NewDeviceQueue, deviceId) w.NewDevQMutex.Unlock() w.DB.CreateDevice(device) if err != nil { log.Fatal(err) } continue } log.Println("Handle api request") var msg usp_msg.Msg err = proto.Unmarshal(record.GetNoSessionContext().Payload, &msg) if err != nil { log.Println(err) continue } if _, ok := w.MsgQueue[msg.Header.MsgId]; ok { //m.QMutex.Lock() w.MsgQueue[msg.Header.MsgId] <- msg //m.QMutex.Unlock() } else { log.Printf("Message answer to request %s arrived too late", msg.Header.MsgId) } } //log.Printf("recv: %++v", record) } } func (w *Ws) Publish(msg []byte, topic, respTopic string, retain bool) { err := wsConn.WriteMessage(websocket.BinaryMessage, msg) if err != nil { log.Println("write:", err) return } } /* -------------------------------------------------------------------------- */