github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/system/gate/server/wsp/connection.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package wsp 20 21 import ( 22 "bytes" 23 "encoding/json" 24 "fmt" 25 "io" 26 "net/http" 27 "time" 28 29 "github.com/gorilla/websocket" 30 31 "github.com/e154/smart-home/system/gate/common" 32 ) 33 34 // ConnectionStatus is an enumeration type which represents the status of WebSocket connection. 35 type ConnectionStatus int 36 37 var ( 38 upgrader = websocket.Upgrader{} 39 ) 40 41 const ( 42 // Idle state means it is opened but not working now. 43 // The default value for Connection is Idle, so it is ok to use zero-value(int: 0) for Idle status. 44 Idle ConnectionStatus = iota 45 Busy 46 Closed 47 ) 48 49 // Connection manages a single websocket connection from the peer. 50 // wsp supports multiple connections from a single peer at the same time. 51 type Connection struct { 52 pool *Pool 53 ws *websocket.Conn 54 status ConnectionStatus 55 idleSince time.Time 56 queue chan Message 57 } 58 59 // NewConnection returns a new Connection. 60 func NewConnection(pool *Pool, ws *websocket.Conn) *Connection { 61 c := &Connection{ 62 pool: pool, 63 ws: ws, 64 status: Idle, 65 queue: make(chan Message), 66 } 67 68 // Mark that this connection is ready to use for relay 69 c.Release() 70 71 return c 72 } 73 74 func (c *Connection) WritePump() { 75 var data []byte 76 var messageType int 77 var err error 78 for c.status != Closed { 79 messageType, data, err = c.ws.ReadMessage() 80 if messageType == -1 || err != nil { 81 c.status = Closed 82 close(c.queue) 83 return 84 } 85 msg := Message{ 86 Type: messageType, 87 Value: data, 88 } 89 c.queue <- msg 90 } 91 } 92 93 func (c *Connection) proxyWs(w http.ResponseWriter, r *http.Request) (err error) { 94 defer c.Release() 95 96 // Only pass those headers to the upgrader. 97 upgradeHeader := http.Header{} 98 if hdr := r.Header.Get("Sec-Websocket-Protocol"); hdr != "" { 99 upgradeHeader.Set("Sec-Websocket-Protocol", hdr) 100 } 101 if hdr := r.Header.Get("Set-Cookie"); hdr != "" { 102 upgradeHeader.Set("Set-Cookie", hdr) 103 } 104 105 query := r.URL.Query() 106 accessToken := query.Get("access_token") 107 if accessToken == "" { 108 accessToken = "NIL" 109 } 110 if err = c.ws.WriteMessage(websocket.TextMessage, []byte("WS:"+accessToken)); err != nil { 111 return 112 } 113 114 upgrader.CheckOrigin = func(r *http.Request) bool { 115 return true 116 } 117 118 connPub, err := upgrader.Upgrade(w, r, upgradeHeader) 119 if err != nil { 120 log.Errorf("websocketproxy: couldn't upgrade %s", err) 121 return 122 } 123 defer connPub.Close() 124 125 errBackend := make(chan error, 1) 126 replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { 127 for { 128 msgType, msg, err := src.ReadMessage() 129 if err != nil { 130 m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) 131 if e, ok := err.(*websocket.CloseError); ok { 132 if e.Code != websocket.CloseNoStatusReceived { 133 m = websocket.FormatCloseMessage(e.Code, e.Text) 134 } 135 } 136 errc <- err 137 138 dst.WriteMessage(websocket.CloseMessage, m) 139 break 140 } 141 err = dst.WriteMessage(msgType, msg) 142 if err != nil { 143 errc <- err 144 break 145 } 146 } 147 } 148 149 go func() { 150 for c.status != Closed { 151 msg, ok := <-c.queue 152 if !ok { 153 return 154 } 155 err = connPub.WriteMessage(msg.Type, msg.Value) 156 if err != nil { 157 break 158 } 159 } 160 }() 161 162 go replicateWebsocketConn(c.ws, connPub, errBackend) 163 164 var message string 165 select { 166 case err = <-errBackend: 167 message = "websocketproxy: Error when copying from client to backend: %v" 168 } 169 if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { 170 log.Errorf(message, err) 171 } 172 173 return nil 174 } 175 176 // Proxy a HTTP request through the Proxy over the websocket connection 177 func (c *Connection) proxyRequest(w http.ResponseWriter, r *http.Request) (err error) { 178 log.Infof("proxy request to %s", c.pool.id) 179 defer c.Release() 180 181 // [1]: Serialize HTTP request 182 jsonReq, err := json.Marshal(common.SerializeHTTPRequest(r)) 183 if err != nil { 184 return fmt.Errorf("unable to serialize request : %w", err) 185 } 186 // i.e. 187 // { 188 // "Method":"GET", 189 // "URL":"http://localhost:8081/hello", 190 // "Header":{"Accept":["*/*"],"User-Agent":["curl/7.77.0"],"X-Proxy-Destination":["http://localhost:8081/hello"]}, 191 // "ContentLength":0 192 // } 193 194 // [2]: Send the HTTP request to the peer 195 // Send the serialized HTTP request to the the peer 196 if err = c.ws.WriteMessage(websocket.TextMessage, jsonReq); err != nil { 197 return fmt.Errorf("unable to write request : %w", err) 198 } 199 200 // Pipe the HTTP request body to the peer 201 bodyWriter, err := c.ws.NextWriter(websocket.BinaryMessage) 202 if err != nil { 203 return fmt.Errorf("unable to get request body writer : %w", err) 204 } 205 if _, err = io.Copy(bodyWriter, r.Body); err != nil { 206 return fmt.Errorf("unable to pipe request body : %w", err) 207 } 208 if err = bodyWriter.Close(); err != nil { 209 return fmt.Errorf("unable to pipe request body (close) : %w", err) 210 } 211 212 msg, ok := <-c.queue 213 if !ok { 214 return 215 } 216 217 jsonResponse := msg.Value 218 219 // Deserialize the HTTP Response 220 httpResponse := new(common.HTTPResponse) 221 if err = json.Unmarshal(jsonResponse, httpResponse); err != nil { 222 return fmt.Errorf("unable to unserialize http response : %w", err) 223 } 224 225 // Write response headers back to the client 226 for header, values := range httpResponse.Header { 227 for _, value := range values { 228 w.Header().Add(header, value) 229 } 230 } 231 w.WriteHeader(httpResponse.StatusCode) 232 233 msg, ok = <-c.queue 234 if !ok { 235 return 236 } 237 238 responseBody := msg.Value 239 240 responseBodyReader := bytes.NewReader(responseBody) 241 242 if _, err = io.Copy(w, responseBodyReader); err != nil { 243 return fmt.Errorf("unable to pipe response body : %w", err) 244 } 245 246 return 247 } 248 249 // Take notifies that this connection is going to be used 250 func (c *Connection) Take() bool { 251 252 if c.status == Closed || c.status == Busy { 253 return false 254 } 255 256 c.status = Busy 257 return true 258 } 259 260 // Release notifies that this connection is ready to use again 261 func (c *Connection) Release() { 262 263 if c.status == Closed { 264 return 265 } 266 267 c.idleSince = time.Now() 268 c.status = Idle 269 270 go c.pool.Offer(c) 271 } 272 273 // Close the connection 274 func (c *Connection) Close() { 275 if c.status == Closed { 276 return 277 } 278 c.status = Closed 279 280 log.Infof("Closing connection from %s", c.pool.id) 281 282 // Close the underlying TCP connection 283 c.ws.Close() 284 }