decred.org/dcrdex@v1.0.5/client/rpcserver/rpcserver.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 rpcserver provides a JSON RPC to communicate with the client core. 5 package rpcserver 6 7 import ( 8 "context" 9 "crypto/elliptic" 10 "crypto/sha256" 11 "crypto/subtle" 12 "crypto/tls" 13 "encoding/base64" 14 "encoding/json" 15 "errors" 16 "fmt" 17 "io" 18 "net/http" 19 "os" 20 "sync" 21 "time" 22 23 "decred.org/dcrdex/client/asset" 24 "decred.org/dcrdex/client/core" 25 "decred.org/dcrdex/client/db" 26 "decred.org/dcrdex/client/mm" 27 "decred.org/dcrdex/client/websocket" 28 "decred.org/dcrdex/dex" 29 "decred.org/dcrdex/dex/msgjson" 30 "github.com/decred/dcrd/certgen" 31 "github.com/go-chi/chi/v5" 32 "github.com/go-chi/chi/v5/middleware" 33 ) 34 35 const ( 36 // rpcSemver is the RPC server's semantic API version. Move major up one for 37 // breaking changes. Move minor for backwards compatible features. Move 38 // patch for bug fixes. bwctl requiredRPCSemVer should be kept up to date 39 // with this version. 40 rpcSemverMajor uint32 = 0 41 rpcSemverMinor uint32 = 4 42 rpcSemverPatch uint32 = 0 43 44 // rpcTimeoutSeconds is the number of seconds a connection to the RPC server 45 // is allowed to stay open without authenticating before it is closed. 46 rpcTimeoutSeconds = 10 47 ) 48 49 var ( 50 // Check that core.Core satisfies clientCore. 51 _ clientCore = (*core.Core)(nil) 52 log dex.Logger 53 // errUnknownCmd is wrapped when the command is not known. 54 errUnknownCmd = errors.New("unknown command") 55 ) 56 57 // clientCore is satisfied by core.Core. 58 type clientCore interface { 59 websocket.Core 60 AssetBalance(assetID uint32) (*core.WalletBalance, error) 61 Book(host string, base, quote uint32) (orderBook *core.OrderBook, err error) 62 Cancel(orderID dex.Bytes) error 63 CloseWallet(assetID uint32) error 64 CreateWallet(appPass, walletPass []byte, form *core.WalletForm) error 65 DiscoverAccount(dexAddr string, pass []byte, certI any) (*core.Exchange, bool, error) 66 Exchanges() (exchanges map[string]*core.Exchange) 67 InitializeClient(appPass []byte, seed *string) (string, error) 68 Login(appPass []byte) error 69 Logout() error 70 OpenWallet(assetID uint32, appPass []byte) error 71 ToggleWalletStatus(assetID uint32, disable bool) error 72 GetDEXConfig(dexAddr string, certI any) (*core.Exchange, error) 73 PostBond(form *core.PostBondForm) (*core.PostBondResult, error) 74 UpdateBondOptions(form *core.BondOptionsForm) error 75 Trade(appPass []byte, form *core.TradeForm) (order *core.Order, err error) 76 Wallets() (walletsStates []*core.WalletState) 77 WalletState(assetID uint32) *core.WalletState 78 RescanWallet(assetID uint32, force bool) error 79 Send(appPass []byte, assetID uint32, value uint64, addr string, subtract bool) (asset.Coin, error) 80 ExportSeed(pw []byte) (string, error) 81 DeleteArchivedRecords(olderThan *time.Time, matchesFileStr, ordersFileStr string) (int, error) 82 WalletPeers(assetID uint32) ([]*asset.WalletPeer, error) 83 AddWalletPeer(assetID uint32, host string) error 84 RemoveWalletPeer(assetID uint32, host string) error 85 Notifications(int) (notes, pokes []*db.Notification, _ error) 86 MultiTrade(pw []byte, form *core.MultiTradeForm) []*core.MultiTradeResult 87 TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) 88 WalletTransaction(assetID uint32, txID string) (*asset.WalletTransaction, error) 89 90 // These are core's ticket buying interface. 91 StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) 92 SetVSP(assetID uint32, addr string) error 93 PurchaseTickets(assetID uint32, pw []byte, n int) error 94 SetVotingPreferences(assetID uint32, choices, tSpendPolicy, treasuryPolicy map[string]string) error 95 GenerateBCHRecoveryTransaction(appPW []byte, recipient string) ([]byte, error) 96 } 97 98 // RPCServer is a single-client http and websocket server enabling a JSON 99 // interface to Bison Wallet. 100 type RPCServer struct { 101 core clientCore 102 mm *mm.MarketMaker 103 mux *chi.Mux 104 wsServer *websocket.Server 105 addr string 106 tlsConfig *tls.Config 107 srv *http.Server 108 authSHA [32]byte 109 wg sync.WaitGroup 110 bwVersion *SemVersion 111 ctx context.Context 112 } 113 114 // genCertPair generates a key/cert pair to the paths provided. 115 func genCertPair(certFile, keyFile string, hosts []string) error { 116 log.Infof("Generating TLS certificates...") 117 118 org := "dcrdex autogenerated cert" 119 validUntil := time.Now().Add(10 * 365 * 24 * time.Hour) 120 cert, key, err := certgen.NewTLSCertPair(elliptic.P521(), org, 121 validUntil, hosts) 122 if err != nil { 123 return err 124 } 125 126 // Write cert and key files. 127 if err = os.WriteFile(certFile, cert, 0644); err != nil { 128 return err 129 } 130 if err = os.WriteFile(keyFile, key, 0600); err != nil { 131 os.Remove(certFile) 132 return err 133 } 134 135 log.Infof("Done generating TLS certificates") 136 return nil 137 } 138 139 // writeJSON marshals the provided interface and writes the bytes to the 140 // ResponseWriter. The response code is assumed to be StatusOK. 141 func writeJSON(w http.ResponseWriter, thing any) { 142 writeJSONWithStatus(w, thing, http.StatusOK) 143 } 144 145 // writeJSONWithStatus marshals the provided interface and writes the bytes to 146 // the ResponseWriter with the specified response code. 147 func writeJSONWithStatus(w http.ResponseWriter, thing any, code int) { 148 w.Header().Set("Content-Type", "application/json; charset=utf-8") 149 b, err := json.Marshal(thing) 150 if err != nil { 151 w.WriteHeader(http.StatusInternalServerError) 152 log.Errorf("JSON encode error: %v", err) 153 return 154 } 155 w.WriteHeader(code) 156 _, err = w.Write(b) 157 if err != nil { 158 log.Errorf("Write error: %v", err) 159 } 160 } 161 162 // handleJSON handles all https json requests. 163 func (s *RPCServer) handleJSON(w http.ResponseWriter, r *http.Request) { 164 // All http routes are available over websocket too, so do not support 165 // persistent http connections. Inform the user and close the connection 166 // when response handling is completed. 167 w.Header().Set("Connection", "close") 168 w.Header().Set("Content-Type", "application/json") 169 r.Close = true 170 171 body, err := io.ReadAll(r.Body) 172 r.Body.Close() 173 if err != nil { 174 http.Error(w, "error reading request body", http.StatusBadRequest) 175 return 176 } 177 req := new(msgjson.Message) 178 err = json.Unmarshal(body, req) 179 if err != nil { 180 http.Error(w, "JSON decode error", http.StatusUnprocessableEntity) 181 return 182 } 183 if req.Type != msgjson.Request { 184 http.Error(w, "Responses not accepted", http.StatusMethodNotAllowed) 185 return 186 } 187 s.parseHTTPRequest(w, req) 188 } 189 190 // Config holds variables needed to create a new RPC Server. 191 type Config struct { 192 Core clientCore 193 MarketMaker *mm.MarketMaker 194 Addr, User, Pass, Cert, Key string 195 BWVersion *SemVersion 196 CertHosts []string 197 } 198 199 // SetLogger sets the logger for the RPCServer package. 200 func SetLogger(logger dex.Logger) { 201 log = logger 202 } 203 204 // New is the constructor for an RPCServer. 205 func New(cfg *Config) (*RPCServer, error) { 206 207 if cfg.Pass == "" { 208 return nil, fmt.Errorf("missing RPC password") 209 } 210 211 // Find or create the key pair. 212 keyExists := dex.FileExists(cfg.Key) 213 certExists := dex.FileExists(cfg.Cert) 214 if certExists == !keyExists { 215 return nil, fmt.Errorf("missing cert pair file") 216 } 217 if !keyExists && !certExists { 218 err := genCertPair(cfg.Cert, cfg.Key, cfg.CertHosts) 219 if err != nil { 220 return nil, err 221 } 222 } 223 keypair, err := tls.LoadX509KeyPair(cfg.Cert, cfg.Key) 224 if err != nil { 225 return nil, err 226 } 227 228 // Prepare the TLS configuration. 229 tlsConfig := &tls.Config{ 230 Certificates: []tls.Certificate{keypair}, 231 MinVersion: tls.VersionTLS12, 232 } 233 234 // Create an HTTP router. 235 mux := chi.NewRouter() 236 httpServer := &http.Server{ 237 Handler: mux, 238 ReadTimeout: rpcTimeoutSeconds * time.Second, // slow requests should not hold connections opened 239 WriteTimeout: rpcTimeoutSeconds * time.Second, // hung responses must die 240 } 241 242 // Make the server. 243 s := &RPCServer{ 244 core: cfg.Core, 245 mm: cfg.MarketMaker, 246 mux: mux, 247 srv: httpServer, 248 addr: cfg.Addr, 249 tlsConfig: tlsConfig, 250 bwVersion: cfg.BWVersion, 251 wsServer: websocket.New(cfg.Core, log.SubLogger("WS")), 252 } 253 254 // Create authSHA to verify requests against. 255 login := cfg.User + ":" + cfg.Pass 256 auth := "Basic " + 257 base64.StdEncoding.EncodeToString([]byte(login)) 258 s.authSHA = sha256.Sum256([]byte(auth)) 259 260 // Middleware 261 mux.Use(middleware.Recoverer) 262 mux.Use(middleware.RealIP) 263 mux.Use(s.authMiddleware) 264 265 // The WebSocket handler is mounted on /ws in Connect. 266 267 // HTTPS endpoint 268 mux.Post("/", s.handleJSON) 269 270 return s, nil 271 } 272 273 // Connect starts the RPC server. Satisfies the dex.Connector interface. 274 func (s *RPCServer) Connect(ctx context.Context) (*sync.WaitGroup, error) { 275 // Create listener. 276 listener, err := tls.Listen("tcp", s.addr, s.tlsConfig) 277 if err != nil { 278 return nil, fmt.Errorf("can't listen on %s. rpc server quitting: %w", s.addr, err) 279 } 280 // Update the listening address in case a :0 was provided. 281 s.addr = listener.Addr().String() 282 283 s.ctx = ctx 284 285 // Close the listener on context cancellation. 286 s.wg.Add(1) 287 go func() { 288 defer s.wg.Done() 289 <-ctx.Done() 290 291 if err := s.srv.Shutdown(context.Background()); err != nil { 292 // Error from closing listeners: 293 log.Errorf("HTTP server Shutdown: %v", err) 294 } 295 }() 296 297 // Configure the websocket handler before starting the server. 298 s.mux.Get("/ws", func(w http.ResponseWriter, r *http.Request) { 299 s.wsServer.HandleConnect(ctx, w, r) 300 }) 301 302 s.wg.Add(1) 303 go func() { 304 defer s.wg.Done() 305 if err := s.srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) { 306 log.Warnf("unexpected (http.Server).Serve error: %v", err) 307 } 308 // Disconnect the websocket clients since http.(*Server).Shutdown does 309 // not deal with hijacked websocket connections. 310 s.wsServer.Shutdown() 311 log.Infof("RPC server off") 312 }() 313 log.Infof("RPC server listening on %s", s.addr) 314 return &s.wg, nil 315 } 316 317 // handleRequest sends the request to the correct handler function if able. 318 func (s *RPCServer) handleRequest(req *msgjson.Message) *msgjson.ResponsePayload { 319 payload := new(msgjson.ResponsePayload) 320 if req.Route == "" { 321 log.Debugf("route not specified") 322 payload.Error = msgjson.NewError(msgjson.RPCUnknownRoute, "no route was supplied") 323 return payload 324 } 325 326 // Find the correct handler for this route. 327 h, exists := routes[req.Route] 328 if !exists { 329 log.Debugf("%v: %v", errUnknownCmd, req.Route) 330 payload.Error = msgjson.NewError(msgjson.RPCUnknownRoute, "%v", errUnknownCmd) 331 return payload 332 } 333 334 params := new(RawParams) 335 err := req.Unmarshal(params) // NOT ¶ms to prevent setting it to nil for []byte("null") Payload 336 if err != nil { 337 log.Debugf("cannot unmarshal params for route %s", req.Route) 338 payload.Error = msgjson.NewError(msgjson.RPCParseError, "unable to unmarshal request") 339 return payload 340 } 341 342 return h(s, params) 343 } 344 345 // parseHTTPRequest parses the msgjson message in the request body, creates a 346 // response message, and writes it to the http.ResponseWriter. 347 func (s *RPCServer) parseHTTPRequest(w http.ResponseWriter, req *msgjson.Message) { 348 payload := s.handleRequest(req) 349 resp, err := msgjson.NewResponse(req.ID, payload.Result, payload.Error) 350 if err != nil { 351 msg := fmt.Sprintf("error encoding response: %v", err) 352 http.Error(w, msg, http.StatusInternalServerError) 353 log.Errorf("parseHTTPRequest: NewResponse failed: %s", msg) 354 return 355 } 356 writeJSON(w, resp) 357 } 358 359 // authMiddleware checks incoming requests for authentication. 360 func (s *RPCServer) authMiddleware(next http.Handler) http.Handler { 361 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 362 fail := func() { 363 log.Warnf("authentication failure from ip: %s", r.RemoteAddr) 364 w.Header().Add("WWW-Authenticate", `Basic realm="dex RPC"`) 365 http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 366 } 367 auth := r.Header["Authorization"] 368 if len(auth) == 0 { 369 fail() 370 return 371 } 372 authSHA := sha256.Sum256([]byte(auth[0])) 373 if subtle.ConstantTimeCompare(s.authSHA[:], authSHA[:]) != 1 { 374 fail() 375 return 376 } 377 log.Debugf("authenticated user with ip: %s", r.RemoteAddr) 378 next.ServeHTTP(w, r) 379 }) 380 }