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 }