github.com/MetalBlockchain/metalgo@v1.11.9/pubsub/connection.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package pubsub
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	"github.com/gorilla/websocket"
    14  	"go.uber.org/zap"
    15  
    16  	"github.com/MetalBlockchain/metalgo/pubsub/bloom"
    17  )
    18  
    19  var (
    20  	ErrFilterNotInitialized        = errors.New("filter not initialized")
    21  	ErrAddressLimit                = errors.New("address limit exceeded")
    22  	ErrInvalidFilterParam          = errors.New("invalid bloom filter params")
    23  	ErrInvalidCommand              = errors.New("invalid command")
    24  	_                       Filter = (*connection)(nil)
    25  )
    26  
    27  type Filter interface {
    28  	Check(addr []byte) bool
    29  }
    30  
    31  // connection is a representation of the websocket connection.
    32  type connection struct {
    33  	s *Server
    34  
    35  	// The websocket connection.
    36  	conn *websocket.Conn
    37  
    38  	// Buffered channel of outbound messages.
    39  	send chan interface{}
    40  
    41  	fp *FilterParam
    42  
    43  	active uint32
    44  }
    45  
    46  func (c *connection) Check(addr []byte) bool {
    47  	return c.fp.Check(addr)
    48  }
    49  
    50  func (c *connection) isActive() bool {
    51  	active := atomic.LoadUint32(&c.active)
    52  	return active != 0
    53  }
    54  
    55  func (c *connection) deactivate() {
    56  	atomic.StoreUint32(&c.active, 0)
    57  }
    58  
    59  func (c *connection) Send(msg interface{}) bool {
    60  	if !c.isActive() {
    61  		return false
    62  	}
    63  	select {
    64  	case c.send <- msg:
    65  		return true
    66  	default:
    67  	}
    68  	return false
    69  }
    70  
    71  // readPump pumps messages from the websocket connection to the hub.
    72  //
    73  // The application runs readPump in a per-connection goroutine. The application
    74  // ensures that there is at most one reader on a connection by executing all
    75  // reads from this goroutine.
    76  func (c *connection) readPump() {
    77  	defer func() {
    78  		c.deactivate()
    79  		c.s.removeConnection(c)
    80  
    81  		// close is called by both the writePump and the readPump so one of them
    82  		// will always error
    83  		_ = c.conn.Close()
    84  	}()
    85  
    86  	c.conn.SetReadLimit(maxMessageSize)
    87  	// SetReadDeadline returns an error if the connection is corrupted
    88  	if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
    89  		return
    90  	}
    91  	c.conn.SetPongHandler(func(string) error {
    92  		return c.conn.SetReadDeadline(time.Now().Add(pongWait))
    93  	})
    94  
    95  	for {
    96  		err := c.readMessage()
    97  		if err != nil {
    98  			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
    99  				c.s.log.Debug("unexpected close in websockets",
   100  					zap.Error(err),
   101  				)
   102  			}
   103  			break
   104  		}
   105  	}
   106  }
   107  
   108  // writePump pumps messages from the hub to the websocket connection.
   109  //
   110  // A goroutine running writePump is started for each connection. The
   111  // application ensures that there is at most one writer to a connection by
   112  // executing all writes from this goroutine.
   113  func (c *connection) writePump() {
   114  	ticker := time.NewTicker(pingPeriod)
   115  	defer func() {
   116  		c.deactivate()
   117  		ticker.Stop()
   118  		c.s.removeConnection(c)
   119  
   120  		// close is called by both the writePump and the readPump so one of them
   121  		// will always error
   122  		_ = c.conn.Close()
   123  	}()
   124  	for {
   125  		select {
   126  		case message, ok := <-c.send:
   127  			if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
   128  				c.s.log.Debug("closing the connection",
   129  					zap.String("reason", "failed to set the write deadline"),
   130  					zap.Error(err),
   131  				)
   132  				return
   133  			}
   134  			if !ok {
   135  				// The hub closed the channel. Attempt to close the connection
   136  				// gracefully.
   137  				_ = c.conn.WriteMessage(websocket.CloseMessage, []byte{})
   138  				return
   139  			}
   140  
   141  			if err := c.conn.WriteJSON(message); err != nil {
   142  				return
   143  			}
   144  		case <-ticker.C:
   145  			if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
   146  				c.s.log.Debug("closing the connection",
   147  					zap.String("reason", "failed to set the write deadline"),
   148  					zap.Error(err),
   149  				)
   150  				return
   151  			}
   152  			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
   153  				return
   154  			}
   155  		}
   156  	}
   157  }
   158  
   159  func (c *connection) readMessage() error {
   160  	_, r, err := c.conn.NextReader()
   161  	if err != nil {
   162  		return err
   163  	}
   164  	cmd := &Command{}
   165  	err = json.NewDecoder(r).Decode(cmd)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	switch {
   171  	case cmd.NewBloom != nil:
   172  		err = c.handleNewBloom(cmd.NewBloom)
   173  	case cmd.NewSet != nil:
   174  		c.handleNewSet(cmd.NewSet)
   175  	case cmd.AddAddresses != nil:
   176  		err = c.handleAddAddresses(cmd.AddAddresses)
   177  	default:
   178  		err = ErrInvalidCommand
   179  	}
   180  	if err != nil {
   181  		c.Send(&errorMsg{
   182  			Error: err.Error(),
   183  		})
   184  	}
   185  	return err
   186  }
   187  
   188  func (c *connection) handleNewBloom(cmd *NewBloom) error {
   189  	if !cmd.IsParamsValid() {
   190  		return ErrInvalidFilterParam
   191  	}
   192  	filter, err := bloom.New(int(cmd.MaxElements), float64(cmd.CollisionProb), MaxBytes)
   193  	if err != nil {
   194  		return fmt.Errorf("bloom filter creation failed %w", err)
   195  	}
   196  	c.fp.SetFilter(filter)
   197  	return nil
   198  }
   199  
   200  func (c *connection) handleNewSet(_ *NewSet) {
   201  	c.fp.NewSet()
   202  }
   203  
   204  func (c *connection) handleAddAddresses(cmd *AddAddresses) error {
   205  	if err := cmd.parseAddresses(); err != nil {
   206  		return fmt.Errorf("address parse failed %w", err)
   207  	}
   208  	err := c.fp.Add(cmd.addressIds...)
   209  	if err != nil {
   210  		return fmt.Errorf("address append failed %w", err)
   211  	}
   212  	c.s.subscribedConnections.Add(c)
   213  	return nil
   214  }