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 &params 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  }