decred.org/dcrdex@v1.0.5/server/admin/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 admin provides a password protected https server to send commands to
     5  // a running dex server.
     6  package admin
     7  
     8  import (
     9  	"context"
    10  	"crypto/sha256"
    11  	"crypto/subtle"
    12  	"crypto/tls"
    13  	"encoding/json"
    14  	"errors"
    15  	"fmt"
    16  	"net"
    17  	"net/http"
    18  	"sync"
    19  	"time"
    20  
    21  	"decred.org/dcrdex/dex"
    22  	"decred.org/dcrdex/dex/msgjson"
    23  	"decred.org/dcrdex/dex/order"
    24  	"decred.org/dcrdex/server/account"
    25  	"decred.org/dcrdex/server/asset"
    26  	"decred.org/dcrdex/server/auth"
    27  	"decred.org/dcrdex/server/db"
    28  	dexsrv "decred.org/dcrdex/server/dex"
    29  	"decred.org/dcrdex/server/market"
    30  	"github.com/decred/slog"
    31  	"github.com/go-chi/chi/v5"
    32  	"github.com/go-chi/chi/v5/middleware"
    33  )
    34  
    35  const (
    36  	// rpcTimeoutSeconds is the number of seconds a connection to the
    37  	// server is allowed to stay open without authenticating before it
    38  	// is closed.
    39  	rpcTimeoutSeconds = 10
    40  
    41  	marketNameKey      = "market"
    42  	accountIDKey       = "account"
    43  	yesKey             = "yes"
    44  	matchIDKey         = "match"
    45  	assetSymbol        = "asset"
    46  	ruleKey            = "rule"
    47  	scaleKey           = "scale"
    48  	includeInactiveKey = "includeinactive"
    49  	nKey               = "n"
    50  	daysKey            = "days"
    51  	strengthKey        = "strength"
    52  )
    53  
    54  var (
    55  	log slog.Logger
    56  )
    57  
    58  // SvrCore is satisfied by server/dex.DEX.
    59  type SvrCore interface {
    60  	AccountInfo(acctID account.AccountID) (*db.Account, error)
    61  	UserMatchFails(aid account.AccountID, n int) ([]*auth.MatchFail, error)
    62  	Notify(acctID account.AccountID, msg *msgjson.Message)
    63  	NotifyAll(msg *msgjson.Message)
    64  	ConfigMsg() json.RawMessage
    65  	Asset(id uint32) (*asset.BackedAsset, error)
    66  	SetFeeRateScale(assetID uint32, scale float64)
    67  	ScaleFeeRate(assetID uint32, rate uint64) uint64
    68  	MarketRunning(mktName string) (found, running bool)
    69  	MarketStatus(mktName string) *market.Status
    70  	MarketStatuses() map[string]*market.Status
    71  	SuspendMarket(name string, tSusp time.Time, persistBooks bool) (*market.SuspendEpoch, error)
    72  	ResumeMarket(name string, asSoonAs time.Time) (startEpoch int64, startTime time.Time, err error)
    73  	ForgiveMatchFail(aid account.AccountID, mid order.MatchID) (forgiven, unbanned bool, err error)
    74  	AccountMatchOutcomesN(user account.AccountID, n int) ([]*auth.MatchOutcome, error)
    75  	BookOrders(base, quote uint32) (orders []*order.LimitOrder, err error)
    76  	EpochOrders(base, quote uint32) (orders []order.Order, err error)
    77  	MarketMatchesStreaming(base, quote uint32, includeInactive bool, N int64, f func(*dexsrv.MatchData) error) (int, error)
    78  	EnableDataAPI(yes bool)
    79  	CreatePrepaidBonds(n int, strength uint32, durSecs int64) ([][]byte, error)
    80  }
    81  
    82  // Server is a multi-client https server.
    83  type Server struct {
    84  	core      SvrCore
    85  	addr      string
    86  	tlsConfig *tls.Config
    87  	srv       *http.Server
    88  	authSHA   [32]byte
    89  }
    90  
    91  // SrvConfig holds variables needed to create a new Server.
    92  type SrvConfig struct {
    93  	Core            SvrCore
    94  	Addr, Cert, Key string
    95  	AuthSHA         [32]byte
    96  	NoTLS           bool
    97  }
    98  
    99  // UseLogger sets the logger for the admin package.
   100  func UseLogger(logger slog.Logger) {
   101  	log = logger
   102  }
   103  
   104  // NewServer is the constructor for a new Server.
   105  func NewServer(cfg *SrvConfig) (*Server, error) {
   106  	// Find the key pair.
   107  	if !dex.FileExists(cfg.Key) || !dex.FileExists(cfg.Cert) {
   108  		return nil, fmt.Errorf("missing certificates")
   109  	}
   110  
   111  	var tlsConfig *tls.Config
   112  	if !cfg.NoTLS {
   113  		keypair, err := tls.LoadX509KeyPair(cfg.Cert, cfg.Key)
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  
   118  		// Prepare the TLS configuration.
   119  		tlsConfig = &tls.Config{
   120  			Certificates: []tls.Certificate{keypair},
   121  			MinVersion:   tls.VersionTLS12,
   122  		}
   123  	}
   124  
   125  	// Create an HTTP router.
   126  	mux := chi.NewRouter()
   127  	httpServer := &http.Server{
   128  		Handler:      mux,
   129  		ReadTimeout:  rpcTimeoutSeconds * time.Second, // slow requests should not hold connections opened
   130  		WriteTimeout: rpcTimeoutSeconds * time.Second, // hung responses must die
   131  	}
   132  
   133  	// Make the server.
   134  	s := &Server{
   135  		core:      cfg.Core,
   136  		srv:       httpServer,
   137  		addr:      cfg.Addr,
   138  		tlsConfig: tlsConfig,
   139  		authSHA:   cfg.AuthSHA,
   140  	}
   141  
   142  	// Middleware
   143  	mux.Use(middleware.Recoverer)
   144  	mux.Use(middleware.RealIP)
   145  	mux.Use(oneTimeConnection)
   146  	mux.Use(s.authMiddleware)
   147  
   148  	// api endpoints
   149  	mux.Route("/api", func(r chi.Router) {
   150  		r.Use(middleware.AllowContentType("text/plain"))
   151  		r.Get("/ping", apiPing)
   152  		r.Get("/config", s.apiConfig)
   153  		r.Get("/enabledataapi/{"+yesKey+"}", s.apiEnableDataAPI)
   154  		r.Route("/account/{"+accountIDKey+"}", func(rm chi.Router) {
   155  			rm.Get("/", s.apiAccountInfo)
   156  			rm.Get("/outcomes", s.apiMatchOutcomes)
   157  			rm.Get("/fails", s.apiMatchFails)
   158  			rm.Get("/forgive_match/{"+matchIDKey+"}", s.apiForgiveMatchFail)
   159  			rm.Post("/notify", s.apiNotify)
   160  		})
   161  		r.Route("/asset/{"+assetSymbol+"}", func(rm chi.Router) {
   162  			rm.Get("/", s.apiAsset)
   163  			rm.Get("/setfeescale/{"+scaleKey+"}", s.apiSetFeeScale)
   164  		})
   165  		r.Post("/notifyall", s.apiNotifyAll)
   166  		r.Get("/markets", s.apiMarkets)
   167  		r.Route("/market/{"+marketNameKey+"}", func(rm chi.Router) {
   168  			rm.Get("/", s.apiMarketInfo)
   169  			rm.Get("/orderbook", s.apiMarketOrderBook)
   170  			rm.Get("/epochorders", s.apiMarketEpochOrders)
   171  			rm.Get("/matches", s.apiMarketMatches)
   172  			rm.Get("/suspend", s.apiSuspend)
   173  			rm.Get("/resume", s.apiResume)
   174  		})
   175  		r.Get("/prepaybonds", s.prepayBonds)
   176  	})
   177  
   178  	return s, nil
   179  }
   180  
   181  // Run starts the server.
   182  func (s *Server) Run(ctx context.Context) {
   183  	// Create listener.
   184  	var listener net.Listener
   185  	var err error
   186  	if s.tlsConfig != nil {
   187  		listener, err = tls.Listen("tcp", s.addr, s.tlsConfig)
   188  	} else {
   189  		listener, err = net.Listen("tcp", s.addr)
   190  	}
   191  	if err != nil {
   192  		log.Errorf("can't listen on %s. admin server quitting: %v", s.addr, err)
   193  		return
   194  	}
   195  
   196  	// Close the listener on context cancellation.
   197  	var wg sync.WaitGroup
   198  	wg.Add(1)
   199  	go func() {
   200  		defer wg.Done()
   201  		<-ctx.Done()
   202  
   203  		if err := s.srv.Shutdown(context.Background()); err != nil {
   204  			// Error from closing listeners:
   205  			log.Errorf("HTTP server Shutdown: %v", err)
   206  		}
   207  	}()
   208  	log.Infof("admin server listening on %s", s.addr)
   209  	if err := s.srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
   210  		log.Warnf("unexpected (http.Server).Serve error: %v", err)
   211  	}
   212  
   213  	// Wait for Shutdown.
   214  	wg.Wait()
   215  	log.Infof("admin server off")
   216  }
   217  
   218  // oneTimeConnection sets fields in the header and request that indicate this
   219  // connection should not be reused.
   220  func oneTimeConnection(next http.Handler) http.Handler {
   221  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   222  		w.Header().Set("Connection", "close")
   223  		r.Close = true
   224  		next.ServeHTTP(w, r)
   225  	})
   226  }
   227  
   228  // authMiddleware checks incoming requests for authentication.
   229  func (s *Server) authMiddleware(next http.Handler) http.Handler {
   230  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   231  		// User is ignored.
   232  		_, pass, ok := r.BasicAuth()
   233  		authSHA := sha256.Sum256([]byte(pass))
   234  		if !ok || subtle.ConstantTimeCompare(s.authSHA[:], authSHA[:]) != 1 {
   235  			log.Warnf("server authentication failure from ip: %s", r.RemoteAddr)
   236  			w.Header().Add("WWW-Authenticate", `Basic realm="dex admin"`)
   237  			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
   238  			return
   239  		}
   240  		log.Infof("server authenticated ip: %s", r.RemoteAddr)
   241  		next.ServeHTTP(w, r)
   242  	})
   243  }