github.com/vmware/transport-go@v1.3.4/bridge/bridge_client.go (about) 1 // Copyright 2019-2020 VMware, Inc. 2 // SPDX-License-Identifier: BSD-2-Clause 3 4 package bridge 5 6 import ( 7 "bufio" 8 "bytes" 9 "errors" 10 "fmt" 11 "github.com/go-stomp/stomp/v3" 12 "github.com/go-stomp/stomp/v3/frame" 13 "github.com/google/uuid" 14 "github.com/gorilla/websocket" 15 "github.com/vmware/transport-go/model" 16 "log" 17 "net/url" 18 "os" 19 "strconv" 20 "sync" 21 ) 22 23 // BridgeClient encapsulates all subscriptions and io to and from brokers. 24 type BridgeClient struct { 25 WSc *websocket.Conn // WebSocket connection 26 TCPc *stomp.Conn // STOMP TCP Connection 27 ConnectedChan chan bool 28 disconnectedChan chan bool 29 connected bool 30 inboundChan chan *frame.Frame 31 stompConnected bool 32 Subscriptions map[string]*BridgeClientSub 33 logger *log.Logger 34 lock sync.Mutex 35 sendLock sync.Mutex 36 } 37 38 // NewBridgeWsClient Create a new WebSocket client. 39 func NewBridgeWsClient(enableLogging bool) *BridgeClient { 40 return newBridgeWsClient(enableLogging) 41 } 42 43 func newBridgeWsClient(enableLogging bool) *BridgeClient { 44 var l *log.Logger = nil 45 if enableLogging { 46 l = log.New(os.Stderr, "WebSocket Client: ", 2) 47 } 48 return &BridgeClient{ 49 WSc: nil, 50 TCPc: nil, 51 stompConnected: false, 52 connected: false, 53 logger: l, 54 lock: sync.Mutex{}, 55 sendLock: sync.Mutex{}, 56 Subscriptions: make(map[string]*BridgeClientSub), 57 ConnectedChan: make(chan bool), 58 disconnectedChan: make(chan bool), 59 inboundChan: make(chan *frame.Frame)} 60 } 61 62 // Connect to broker endpoint. 63 func (ws *BridgeClient) Connect(url *url.URL, config *BrokerConnectorConfig) error { 64 ws.lock.Lock() 65 defer ws.lock.Unlock() 66 if ws.logger != nil { 67 ws.logger.Printf("connecting to fabric endpoint over %s", url.String()) 68 } 69 70 dialer := websocket.DefaultDialer 71 if config.WebSocketConfig.UseTLS { 72 73 // if the cert and key are not set, we're acting as a client, not a server so we have to 74 // allow these values to be empty when connecting vs serving over TLS. 75 if config.WebSocketConfig.CertFile != "" || config.WebSocketConfig.KeyFile != "" { 76 if err := config.WebSocketConfig.LoadX509KeyPairFromFiles( 77 config.WebSocketConfig.CertFile, 78 config.WebSocketConfig.KeyFile); err != nil { 79 return err 80 } 81 } 82 dialer.TLSClientConfig = config.WebSocketConfig.TLSConfig 83 } 84 85 c, _, err := dialer.Dial(url.String(), config.HttpHeader) 86 if err != nil { 87 return err 88 } 89 ws.WSc = c 90 91 // handle incoming STOMP frames. 92 go ws.handleIncomingSTOMPFrames() 93 94 // go listen to the websocket 95 go ws.listenSocket() 96 97 stompHeaders := []string{ 98 frame.AcceptVersion, 99 string(stomp.V12), 100 frame.Login, 101 config.Username, 102 frame.Passcode, 103 config.Password, 104 frame.HeartBeat, 105 fmt.Sprintf("%d,%d", config.HeartBeatOut.Milliseconds(), config.HeartBeatIn.Milliseconds())} 106 for key, value := range config.STOMPHeader { 107 stompHeaders = append(stompHeaders, key, value) 108 } 109 110 // send connect frame. 111 ws.SendFrame(frame.New(frame.CONNECT, stompHeaders...)) 112 113 // wait to be connected 114 <-ws.ConnectedChan 115 return nil 116 } 117 118 // Disconnect from broker endpoint 119 func (ws *BridgeClient) Disconnect() error { 120 if ws.WSc != nil { 121 defer ws.WSc.Close() 122 ws.disconnectedChan <- true 123 } else { 124 return fmt.Errorf("cannot disconnect, no connection defined") 125 } 126 return nil 127 } 128 129 // Subscribe to destination 130 func (ws *BridgeClient) Subscribe(destination string) *BridgeClientSub { 131 ws.lock.Lock() 132 defer ws.lock.Unlock() 133 id := uuid.New() 134 s := &BridgeClientSub{ 135 C: make(chan *model.Message), 136 Id: &id, 137 Client: ws, 138 Destination: destination, 139 subscribed: true} 140 141 ws.Subscriptions[destination] = s 142 143 // create subscription frame. 144 subscribeFrame := frame.New(frame.SUBSCRIBE, 145 frame.Id, id.String(), 146 frame.Destination, destination, 147 frame.Ack, stomp.AckAuto.String()) 148 149 // send subscription frame. 150 ws.SendFrame(subscribeFrame) 151 return s 152 } 153 154 // Send a payload to a destination 155 func (ws *BridgeClient) Send(destination, contentType string, payload []byte, opts ...func(fr *frame.Frame) error) { 156 ws.lock.Lock() 157 defer ws.lock.Unlock() 158 159 // create send frame. 160 sendFrame := frame.New(frame.SEND, 161 frame.Destination, destination, 162 frame.ContentLength, strconv.Itoa(len(payload)), 163 frame.ContentType, contentType) 164 165 // apply extra frame options such as adding extra headers 166 for _, frameOpt := range opts { 167 _ = frameOpt(sendFrame) 168 } 169 // add payload 170 sendFrame.Body = payload 171 172 // send frame 173 go ws.SendFrame(sendFrame) 174 175 } 176 177 // SendFrame fire a STOMP frame down the WebSocket 178 func (ws *BridgeClient) SendFrame(f *frame.Frame) { 179 ws.sendLock.Lock() 180 defer ws.sendLock.Unlock() 181 var b bytes.Buffer 182 br := bufio.NewWriter(&b) 183 sw := frame.NewWriter(br) 184 185 // write frame to buffer 186 sw.Write(f) 187 w, _ := ws.WSc.NextWriter(websocket.TextMessage) 188 defer w.Close() 189 190 w.Write(b.Bytes()) 191 192 } 193 194 func (ws *BridgeClient) listenSocket() { 195 for { 196 // read each incoming message from websocket 197 _, p, err := ws.WSc.ReadMessage() 198 b := bytes.NewReader(p) 199 sr := frame.NewReader(b) 200 f, _ := sr.Read() 201 202 if err != nil { 203 break // socket can't be read anymore, exit. 204 } 205 if f != nil { 206 ws.inboundChan <- f 207 } 208 } 209 } 210 211 func (ws *BridgeClient) handleIncomingSTOMPFrames() { 212 for { 213 select { 214 case <-ws.disconnectedChan: 215 return 216 case f := <-ws.inboundChan: 217 switch f.Command { 218 case frame.CONNECTED: 219 if ws.logger != nil { 220 ws.logger.Printf("STOMP Client connected") 221 } 222 ws.stompConnected = true 223 ws.connected = true 224 ws.ConnectedChan <- true 225 226 case frame.MESSAGE: 227 for _, sub := range ws.Subscriptions { 228 if sub.Destination == f.Header.Get(frame.Destination) { 229 c := &model.MessageConfig{Payload: f.Body, Destination: sub.Destination} 230 sub.lock.RLock() 231 if sub.subscribed { 232 ws.sendResponseSafe(sub.C, model.GenerateResponse(c)) 233 } 234 sub.lock.RUnlock() 235 } 236 } 237 238 case frame.ERROR: 239 if ws.logger != nil { 240 ws.logger.Printf("STOMP ErrorDir received") 241 } 242 243 for _, sub := range ws.Subscriptions { 244 if sub.Destination == f.Header.Get(frame.Destination) { 245 c := &model.MessageConfig{Payload: f.Body, Err: errors.New("STOMP ErrorDir " + string(f.Body))} 246 sub.E <- model.GenerateError(c) 247 } 248 } 249 } 250 } 251 } 252 } 253 254 func (ws *BridgeClient) sendResponseSafe(C chan *model.Message, m *model.Message) { 255 defer func() { 256 if r := recover(); r != nil { 257 if ws.logger != nil { 258 ws.logger.Println("channel is closed, message undeliverable to closed channel.") 259 } 260 } 261 }() 262 C <- m 263 }