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  }