decred.org/dcrdex@v1.0.5/client/websocket/websocket.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package websocket
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"net/http"
    10  	"sync"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"decred.org/dcrdex/client/core"
    15  	"decred.org/dcrdex/client/orderbook"
    16  	"decred.org/dcrdex/dex"
    17  	"decred.org/dcrdex/dex/msgjson"
    18  	"decred.org/dcrdex/dex/ws"
    19  )
    20  
    21  var (
    22  	// Time allowed to read the next pong message from the peer. The
    23  	// default is intended for production, but leaving as a var instead of const
    24  	// to facilitate testing.
    25  	pongWait = 60 * time.Second
    26  	// Send pings to peer with this period. Must be less than pongWait. The
    27  	// default is intended for production, but leaving as a var instead of const
    28  	// to facilitate testing.
    29  	pingPeriod = (pongWait * 9) / 10
    30  	// A client id counter.
    31  	cidCounter int32
    32  )
    33  
    34  type bookFeed struct {
    35  	core.BookFeed
    36  	loop        *dex.StartStopWaiter
    37  	host        string
    38  	base, quote uint32
    39  }
    40  
    41  // wsClient is a persistent websocket connection to a client.
    42  type wsClient struct {
    43  	*ws.WSLink
    44  	cid int32
    45  
    46  	feedMtx sync.RWMutex
    47  	feed    *bookFeed
    48  }
    49  
    50  func newWSClient(addr string, conn ws.Connection, hndlr func(msg *msgjson.Message) *msgjson.Error, logger dex.Logger) *wsClient {
    51  	return &wsClient{
    52  		WSLink: ws.NewWSLink(addr, conn, pingPeriod, hndlr, logger),
    53  		cid:    atomic.AddInt32(&cidCounter, 1),
    54  	}
    55  }
    56  
    57  func (cl *wsClient) shutDownFeed() {
    58  	if cl.feed != nil {
    59  		cl.feed.loop.Stop()
    60  		cl.feed.loop.WaitForShutdown()
    61  		cl.feed = nil
    62  	}
    63  }
    64  
    65  // Core specifies the needed methods for Server to operate. Satisfied by *core.Core.
    66  type Core interface {
    67  	SyncBook(dex string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error)
    68  	AckNotes([]dex.Bytes)
    69  }
    70  
    71  // Server is a websocket hub that tracks all running websocket clients, allows
    72  // sending notifications to all of them, and manages per-client order book
    73  // subscriptions.
    74  type Server struct {
    75  	core Core
    76  	log  dex.Logger
    77  	wg   sync.WaitGroup
    78  
    79  	clientsMtx sync.RWMutex
    80  	clients    map[int32]*wsClient
    81  }
    82  
    83  // New returns a new websocket Server.
    84  func New(core Core, log dex.Logger) *Server {
    85  	return &Server{
    86  		core:    core,
    87  		log:     log,
    88  		clients: make(map[int32]*wsClient),
    89  	}
    90  }
    91  
    92  // Shutdown gracefully shuts down all connected clients, waiting for them to
    93  // disconnect and any running goroutines and message handlers to return.
    94  func (s *Server) Shutdown() {
    95  	s.clientsMtx.Lock()
    96  	for _, cl := range s.clients {
    97  		cl.Disconnect()
    98  	}
    99  	s.clientsMtx.Unlock()
   100  	// Each upgraded connection handler must return. This also waits for running
   101  	// marketSyncers and response handlers as long as dex/ws.(*WSLink) operates
   102  	// as designed and each (*Server).connect goroutine waits for the link's
   103  	// WaitGroup before returning.
   104  	s.wg.Wait()
   105  }
   106  
   107  // HandleConnect handles the websocket connection request, creating a
   108  // ws.Connection and a connect thread. Since the http.Request's Context is
   109  // canceled after ServerHTTP returns, a separate context must be provided to be
   110  // able to cancel the hijacked connection handler at a later time since this
   111  // function is not blocking.
   112  func (s *Server) HandleConnect(ctx context.Context, w http.ResponseWriter, r *http.Request) {
   113  	wsConn, err := ws.NewConnection(w, r, pongWait)
   114  	if err != nil {
   115  		s.log.Errorf("ws connection error: %v", err)
   116  		return
   117  	}
   118  
   119  	// wsConn.SetReadLimit(65536) // if websocket reads need to be larger than ws.defaultReadLimit
   120  
   121  	// Launch the handler for the upgraded connection. Shutdown will wait for
   122  	// these to return.
   123  	s.wg.Add(1)
   124  	go func() {
   125  		defer s.wg.Done()
   126  		s.connect(ctx, wsConn, r.RemoteAddr)
   127  	}()
   128  }
   129  
   130  // connect handles a new websocket client by creating a new wsClient, starting
   131  // it, and blocking until the connection closes. This method should be
   132  // run as a goroutine.
   133  func (s *Server) connect(ctx context.Context, conn ws.Connection, addr string) {
   134  	s.log.Debugf("New websocket client %s", addr)
   135  	// Create a new websocket client to handle the new websocket connection
   136  	// and wait for it to shut down.  Once it has shutdown (and hence
   137  	// disconnected), remove it.
   138  	var cl *wsClient
   139  	cl = newWSClient(addr, conn, func(msg *msgjson.Message) *msgjson.Error {
   140  		return s.handleMessage(cl, msg)
   141  	}, s.log.SubLogger(addr))
   142  
   143  	// Lock the clients map before starting the connection listening so that
   144  	// synchronized map accesses are guaranteed to reflect this connection.
   145  	// Also, ensuring only live connections are in the clients map notify from
   146  	// sending before it is connected.
   147  	s.clientsMtx.Lock()
   148  	cm := dex.NewConnectionMaster(cl)
   149  	err := cm.ConnectOnce(ctx) // we discard the cm anyway, but good practice
   150  	if err != nil {
   151  		s.clientsMtx.Unlock()
   152  		s.log.Errorf("websocketHandler client connect: %v", err)
   153  		return
   154  	}
   155  
   156  	// Add the client to the map only after it is connected so that notify does
   157  	// not attempt to send to non-existent connection.
   158  	s.clients[cl.cid] = cl
   159  	s.clientsMtx.Unlock()
   160  
   161  	defer func() {
   162  		cl.feedMtx.Lock()
   163  		cl.shutDownFeed()
   164  		cl.feedMtx.Unlock()
   165  
   166  		s.clientsMtx.Lock()
   167  		delete(s.clients, cl.cid)
   168  		s.clientsMtx.Unlock()
   169  	}()
   170  
   171  	cm.Wait() // also waits for any handleMessage calls in (*WSLink).inHandler
   172  	s.log.Tracef("Disconnected websocket client %s", addr)
   173  }
   174  
   175  // Notify sends a notification to the websocket client.
   176  func (s *Server) Notify(route string, payload any) {
   177  	msg, err := msgjson.NewNotification(route, payload)
   178  	if err != nil {
   179  		s.log.Errorf("%q notification encoding error: %v", route, err)
   180  		return
   181  	}
   182  	s.clientsMtx.RLock()
   183  	defer s.clientsMtx.RUnlock()
   184  	for _, cl := range s.clients {
   185  		if err = cl.Send(msg); err != nil {
   186  			s.log.Warnf("Failed to send %v notification to client %v at %v: %v",
   187  				msg.Route, cl.cid, cl.Addr(), err)
   188  		}
   189  	}
   190  }
   191  
   192  // handleMessage handles the websocket message, calling the right handler for
   193  // the route.
   194  func (s *Server) handleMessage(conn *wsClient, msg *msgjson.Message) *msgjson.Error {
   195  	s.log.Tracef("message of type %d received for route %s", msg.Type, msg.Route)
   196  	if msg.Type == msgjson.Request {
   197  		handler, found := wsHandlers[msg.Route]
   198  		if !found {
   199  			return msgjson.NewError(msgjson.UnknownMessageType, "unknown route %q", msg.Route)
   200  		}
   201  		return handler(s, conn, msg)
   202  	}
   203  	// Web server doesn't send requests, only responses and notifications, so
   204  	// a response-type message from a client is an error.
   205  	return msgjson.NewError(msgjson.UnknownMessageType, "web server only handles requests")
   206  }
   207  
   208  // All request handlers must be defined with this signature.
   209  type wsHandler func(*Server, *wsClient, *msgjson.Message) *msgjson.Error
   210  
   211  // wsHandlers is the map used by the server to locate the router handler for a
   212  // request.
   213  var wsHandlers = map[string]wsHandler{
   214  	"loadmarket":  wsLoadMarket,
   215  	"loadcandles": wsLoadCandles,
   216  	"unmarket":    wsUnmarket,
   217  	"acknotes":    wsAckNotes,
   218  }
   219  
   220  // marketLoad is sent by websocket clients to subscribe to a market and request
   221  // the order book.
   222  type marketLoad struct {
   223  	Host  string `json:"host"`
   224  	Base  uint32 `json:"base"`
   225  	Quote uint32 `json:"quote"`
   226  }
   227  
   228  type candlesLoad struct {
   229  	marketLoad
   230  	Dur string `json:"dur"`
   231  }
   232  
   233  // marketSyncer is used to synchronize market subscriptions. The marketSyncer
   234  // manages a map of clients who are subscribed to the market, and distributes
   235  // order book updates when received.
   236  type marketSyncer struct {
   237  	log  dex.Logger
   238  	feed core.BookFeed
   239  	cl   *wsClient
   240  }
   241  
   242  // newMarketSyncer is the constructor for a marketSyncer, returned as a running
   243  // *dex.StartStopWaiter.
   244  func newMarketSyncer(cl *wsClient, feed core.BookFeed, log dex.Logger) *dex.StartStopWaiter {
   245  	ssWaiter := dex.NewStartStopWaiter(&marketSyncer{
   246  		feed: feed,
   247  		cl:   cl,
   248  		log:  log,
   249  	})
   250  	ssWaiter.Start(context.Background()) // wrapping Run with a cancel bound to Stop
   251  	return ssWaiter
   252  }
   253  
   254  // Run starts the marketSyncer listening for BookUpdates, which it relays to the
   255  // websocket client as notifications.
   256  func (m *marketSyncer) Run(ctx context.Context) {
   257  out:
   258  	for {
   259  		select {
   260  		case update, ok := <-m.feed.Next():
   261  			if !ok {
   262  				// We are skipping m.feed.Close if the feed were closed (external sig).
   263  				return
   264  			}
   265  			note, err := msgjson.NewNotification(update.Action, update)
   266  			if err != nil {
   267  				m.log.Errorf("error encoding notification message: %v", err)
   268  				break out
   269  			}
   270  			err = m.cl.Send(note)
   271  			if err != nil {
   272  				m.log.Debugf("send error. ending market feed: %v", err)
   273  				break out
   274  			}
   275  		case <-ctx.Done():
   276  			break out
   277  		}
   278  	}
   279  	m.feed.Close()
   280  }
   281  
   282  // wsLoadMarket is the handler for the 'loadmarket' websocket route. Subscribes
   283  // the client to the notification feed and sends the order book.
   284  func wsLoadMarket(s *Server, cl *wsClient, msg *msgjson.Message) *msgjson.Error {
   285  	req := new(marketLoad)
   286  	err := json.Unmarshal(msg.Payload, req)
   287  	if err != nil {
   288  		return msgjson.NewError(msgjson.RPCInternal, "error unmarshalling marketload payload: %v", err)
   289  	}
   290  	_, msgErr := loadMarket(s, cl, req)
   291  	return msgErr
   292  }
   293  
   294  func loadMarket(s *Server, cl *wsClient, req *marketLoad) (*bookFeed, *msgjson.Error) {
   295  	name, err := dex.MarketName(req.Base, req.Quote)
   296  	if err != nil {
   297  		return nil, msgjson.NewError(msgjson.UnknownMarketError, "unknown market: %v", err)
   298  	}
   299  
   300  	_, feed, err := s.core.SyncBook(req.Host, req.Base, req.Quote)
   301  	if err != nil {
   302  		return nil, msgjson.NewError(msgjson.RPCOrderBookError, "error getting order feed: %v", err)
   303  	}
   304  
   305  	cl.feedMtx.Lock()
   306  	defer cl.feedMtx.Unlock()
   307  	cl.shutDownFeed()
   308  	cl.feed = &bookFeed{
   309  		BookFeed: feed,
   310  		loop:     newMarketSyncer(cl, feed, s.log.SubLogger(name)),
   311  		host:     req.Host,
   312  		base:     req.Base,
   313  		quote:    req.Quote,
   314  	}
   315  	return cl.feed, nil
   316  }
   317  
   318  func wsLoadCandles(s *Server, cl *wsClient, msg *msgjson.Message) *msgjson.Error {
   319  	req := new(candlesLoad)
   320  	err := json.Unmarshal(msg.Payload, req)
   321  	if err != nil {
   322  		return msgjson.NewError(msgjson.RPCInternal, "error unmarshalling candlesLoad payload: %v", err)
   323  	}
   324  	cl.feedMtx.RLock()
   325  	feed := cl.feed
   326  	cl.feedMtx.RUnlock()
   327  	// If market hasn't been initialized/chosen yet (client should do it in a separate
   328  	// 'loadmarket' request), or if client wants to change currently chosen market (requesting
   329  	// candles for market that's different from currently chosen implies that) - we can
   330  	// try to load it here.
   331  	if feed == nil ||
   332  		(feed.host != req.Host || feed.base != req.Base || feed.quote != req.Quote) {
   333  		var msgErr *msgjson.Error
   334  		feed, msgErr = loadMarket(s, cl, &req.marketLoad)
   335  		if msgErr != nil {
   336  			return msgErr
   337  		}
   338  	}
   339  	err = feed.Candles(req.Dur)
   340  	if err != nil {
   341  		return msgjson.NewError(msgjson.RPCInternal, "%v", err)
   342  	}
   343  	return nil
   344  }
   345  
   346  // wsUnmarket is the handler for the 'unmarket' websocket route. This empty
   347  // message is sent when the user leaves the markets page. This closes the feed,
   348  // and potentially unsubscribes from orderbook with the server if there are no
   349  // other consumers
   350  func wsUnmarket(_ *Server, cl *wsClient, _ *msgjson.Message) *msgjson.Error {
   351  	cl.feedMtx.Lock()
   352  	cl.shutDownFeed()
   353  	cl.feedMtx.Unlock()
   354  
   355  	return nil
   356  }
   357  
   358  type ackNoteIDs []dex.Bytes
   359  
   360  // wsAckNotes is the handler for the 'acknotes' websocket route. It informs the
   361  // Core that the user has seen the specified notifications.
   362  func wsAckNotes(s *Server, _ *wsClient, msg *msgjson.Message) *msgjson.Error {
   363  	ids := make(ackNoteIDs, 0)
   364  	err := msg.Unmarshal(&ids)
   365  	if err != nil {
   366  		s.log.Errorf("error acking notifications: %v", err)
   367  		return nil
   368  	}
   369  	s.core.AckNotes(ids)
   370  	return nil
   371  }