decred.org/dcrdex@v1.0.5/tatanka/tcp/server.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 tcp 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "net/url" 11 "sync" 12 "sync/atomic" 13 "time" 14 15 clientcomms "decred.org/dcrdex/client/comms" 16 "decred.org/dcrdex/dex" 17 "decred.org/dcrdex/dex/msgjson" 18 "decred.org/dcrdex/server/comms" 19 "decred.org/dcrdex/tatanka/mj" 20 "decred.org/dcrdex/tatanka/tanka" 21 ) 22 23 // linkWrapper wraps a comms.Link to create a tanka.Sender. 24 type linkWrapper struct { 25 comms.Link 26 } 27 28 func (l *linkWrapper) Request(msg *msgjson.Message, respHandler func(*msgjson.Message)) error { 29 const requestTimeout = time.Second * 30 30 return l.Link.Request(msg, func(_ comms.Link, respMsg *msgjson.Message) { 31 respHandler(respMsg) 32 }, requestTimeout, func() { // expire 33 errMsg := mj.MustResponse(msg.ID, nil, msgjson.NewError(mj.ErrTimeout, "request timed out")) 34 respHandler(errMsg) 35 }) 36 } 37 38 func (l *linkWrapper) RequestRaw(msgID uint64, rawMsg []byte, respHandler func(*msgjson.Message)) error { 39 const requestTimeout = time.Second * 30 40 return l.Link.RequestRaw(msgID, rawMsg, func(_ comms.Link, respMsg *msgjson.Message) { 41 respHandler(respMsg) 42 }, requestTimeout, func() { // expire 43 errMsg := mj.MustResponse(msgID, nil, msgjson.NewError(mj.ErrTimeout, "request timed out")) 44 respHandler(errMsg) 45 }) 46 } 47 48 func (l *linkWrapper) PeerID() (peerID tanka.PeerID) { 49 copy(peerID[:], []byte(l.CustomID())) 50 return 51 } 52 53 func (l *linkWrapper) SetPeerID(peerID tanka.PeerID) { 54 l.SetCustomID(string(peerID[:])) 55 } 56 57 type wsConnWrapper struct { 58 clientcomms.WsConn 59 cm *dex.ConnectionMaster 60 id atomic.Value 61 } 62 63 func newWsConnWrapper(cl clientcomms.WsConn, cm *dex.ConnectionMaster) *wsConnWrapper { 64 w := &wsConnWrapper{ 65 WsConn: cl, 66 cm: cm, 67 } 68 w.id.Store(tanka.PeerID{}) 69 return w 70 } 71 72 func (cl *wsConnWrapper) Disconnect() { 73 cl.cm.Disconnect() 74 cl.cm.Wait() 75 } 76 77 func (cl *wsConnWrapper) SetPeerID(id tanka.PeerID) { 78 cl.id.Store(id) 79 } 80 81 func (cl *wsConnWrapper) PeerID() tanka.PeerID { 82 return cl.id.Load().(tanka.PeerID) 83 } 84 85 // Server handles HTTP, HTTPS, WebSockets, and Tor communications protocols. 86 type Server struct { 87 t TankaCore 88 wg *sync.WaitGroup 89 srv *comms.Server 90 log dex.Logger 91 } 92 93 type TankaCore interface { 94 // Config() *mj.TatankaConfig 95 Routes() []string 96 HandleMessage(tanka.Sender, *msgjson.Message) *msgjson.Error 97 } 98 99 func NewServer(cfg *comms.RPCConfig, t TankaCore, log dex.Logger) (*Server, error) { 100 srv, err := comms.NewServer(cfg) 101 if err != nil { 102 return nil, fmt.Errorf("NewServer error: %w", err) 103 } 104 105 s := &Server{ 106 t: t, 107 srv: srv, 108 log: log, 109 } 110 111 for _, r := range t.Routes() { 112 srv.Route(r, func(l comms.Link, msg *msgjson.Message) *msgjson.Error { 113 return t.HandleMessage(&linkWrapper{l}, msg) 114 }) 115 } 116 117 return s, nil 118 } 119 120 func (s *Server) Connect(ctx context.Context) (*sync.WaitGroup, error) { 121 var wg sync.WaitGroup 122 s.wg = &wg 123 124 // Start WebSocket server 125 runner := dex.NewStartStopWaiter(s.srv) 126 runner.Start(ctx) // stopped with Stop 127 128 wg.Add(1) 129 go func() { 130 <-ctx.Done() 131 runner.Stop() 132 wg.Done() 133 }() 134 135 return &wg, nil 136 } 137 138 type RemoteNodeConfig struct { 139 URL string `json:"url"` 140 Cert []byte `json:"cert"` 141 } 142 143 func (s *Server) ConnectBootNode( 144 ctx context.Context, 145 rawCfg json.RawMessage, 146 handleMessage func(cl tanka.Sender, msg *msgjson.Message), 147 disconnect func(), 148 ) (tanka.Sender, error) { 149 150 var n RemoteNodeConfig 151 if err := json.Unmarshal(rawCfg, &n); err != nil { 152 return nil, fmt.Errorf("error reading boot node configuration: %w", err) 153 } 154 155 uri, err := url.Parse(n.URL) 156 if err != nil { 157 return nil, fmt.Errorf("url parse error: %w", err) 158 } 159 160 if uri.Path != "/ws" { 161 uri.Path = "/ws" 162 } 163 164 cl, err := clientcomms.NewWsConn(&clientcomms.WsCfg{ 165 URL: uri.String(), 166 PingWait: 20 * time.Second, 167 Cert: n.Cert, 168 ConnectEventFunc: func(status clientcomms.ConnectionStatus) { 169 if status == clientcomms.Disconnected { 170 disconnect() 171 } 172 }, 173 Logger: dex.StdOutLogger(fmt.Sprintf("CL[%s]", n.URL), dex.LevelDebug), 174 DisableAutoReconnect: true, 175 }) 176 if err != nil { 177 return nil, fmt.Errorf("failed to connect to bootnode %q: %v", n.URL, err) 178 } 179 180 cm := dex.NewConnectionMaster(cl) 181 if err := cm.ConnectOnce(ctx); err != nil { 182 return nil, fmt.Errorf("failed to connect to boot node %q. error: %w", n.URL, err) 183 } 184 185 sender := newWsConnWrapper(cl, cm) 186 go s.handleOutboudTatankaConnection(ctx, sender, handleMessage) 187 188 return sender, nil 189 } 190 191 func (s *Server) handleOutboudTatankaConnection(ctx context.Context, cl *wsConnWrapper, handleMessage func(cl tanka.Sender, msg *msgjson.Message)) { 192 msgs := cl.MessageSource() 193 for { 194 if ctx.Err() != nil { 195 return 196 } 197 select { 198 case msg, ok := <-msgs: 199 if !ok { 200 // Connection has been closed. 201 return 202 } 203 handleMessage(cl, msg) 204 case <-ctx.Done(): 205 return 206 } 207 } 208 }