decred.org/dcrdex@v1.0.3/client/webserver/webserver.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 webserver 5 6 import ( 7 "context" 8 "crypto/elliptic" 9 crand "crypto/rand" 10 "crypto/tls" 11 "embed" 12 "encoding/hex" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "io" 17 "io/fs" 18 "mime" 19 "net" 20 "net/http" 21 "os" 22 "path" 23 "path/filepath" 24 "runtime" 25 "strconv" 26 "strings" 27 "sync" 28 "sync/atomic" 29 "time" 30 31 "decred.org/dcrdex/client/asset" 32 "decred.org/dcrdex/client/core" 33 "decred.org/dcrdex/client/db" 34 "decred.org/dcrdex/client/mm" 35 "decred.org/dcrdex/client/mm/libxc" 36 "decred.org/dcrdex/client/webserver/locales" 37 "decred.org/dcrdex/client/websocket" 38 "decred.org/dcrdex/dex" 39 "decred.org/dcrdex/dex/encode" 40 "decred.org/dcrdex/dex/encrypt" 41 "github.com/decred/dcrd/certgen" 42 "github.com/go-chi/chi/v5" 43 "github.com/go-chi/chi/v5/middleware" 44 "golang.org/x/text/language" 45 ) 46 47 // contextKey is the key param type used when saving values to a context using 48 // context.WithValue. A custom type is defined because built-in types are 49 // discouraged. 50 type contextKey string 51 52 const ( 53 // httpConnTimeoutSeconds is the maximum number of seconds allowed for 54 // reading an http request or writing the response, beyond which the http 55 // connection is terminated. 56 httpConnTimeoutSeconds = 10 57 // authCK is the authorization token cookie key. 58 authCK = "dexauth" 59 // pwKeyCK is the cookie used to unencrypt the user's password. 60 pwKeyCK = "sessionkey" 61 // ctxKeyUserInfo is used in the authorization middleware for saving user 62 // info in http request contexts. 63 ctxKeyUserInfo = contextKey("userinfo") 64 // notifyRoute is a route used for general notifications. 65 notifyRoute = "notify" 66 // The basis for content-security-policy. connect-src must be the final 67 // directive so that it can be reliably supplemented on startup. 68 baseCSP = "default-src 'none'; script-src 'self'; img-src 'self' data:; style-src 'self'; font-src 'self'; connect-src 'self'" 69 // site is the common prefix for the site resources with respect to this 70 // webserver package. 71 site = "site" 72 ) 73 74 var ( 75 // errNoCachedPW is returned when attempting to retrieve a cached password, but the 76 // cookie that should contain the cached password is not populated. 77 errNoCachedPW = errors.New("no cached password") 78 ) 79 80 var ( 81 log dex.Logger 82 unbip = dex.BipIDSymbol 83 84 //go:embed site/src/html/*.tmpl 85 htmlTmplRes embed.FS 86 htmlTmplSub, _ = fs.Sub(htmlTmplRes, "site/src/html") // unrooted slash separated path as per io/fs.ValidPath 87 88 //go:embed site/dist site/src/img site/src/font 89 staticSiteRes embed.FS 90 ) 91 92 // clientCore is satisfied by core.Core. 93 type clientCore interface { 94 websocket.Core 95 Network() dex.Network 96 Exchanges() map[string]*core.Exchange 97 Exchange(host string) (*core.Exchange, error) 98 PostBond(form *core.PostBondForm) (*core.PostBondResult, error) 99 RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) 100 UpdateBondOptions(form *core.BondOptionsForm) error 101 Login(pw []byte) error 102 InitializeClient(pw []byte, seed *string) (string, error) 103 AssetBalance(assetID uint32) (*core.WalletBalance, error) 104 CreateWallet(appPW, walletPW []byte, form *core.WalletForm) error 105 OpenWallet(assetID uint32, pw []byte) error 106 RescanWallet(assetID uint32, force bool) error 107 RecoverWallet(assetID uint32, appPW []byte, force bool) error 108 CloseWallet(assetID uint32) error 109 ConnectWallet(assetID uint32) error 110 Wallets() []*core.WalletState 111 WalletState(assetID uint32) *core.WalletState 112 WalletSettings(uint32) (map[string]string, error) 113 ReconfigureWallet([]byte, []byte, *core.WalletForm) error 114 ToggleWalletStatus(assetID uint32, disable bool) error 115 ChangeAppPass([]byte, []byte) error 116 ResetAppPass(newPass []byte, seed string) error 117 NewDepositAddress(assetID uint32) (string, error) 118 AutoWalletConfig(assetID uint32, walletType string) (map[string]string, error) 119 User() *core.User 120 GetDEXConfig(dexAddr string, certI any) (*core.Exchange, error) 121 AddDEX(appPW []byte, dexAddr string, certI any) error 122 DiscoverAccount(dexAddr string, pass []byte, certI any) (*core.Exchange, bool, error) 123 SupportedAssets() map[uint32]*core.SupportedAsset 124 Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) 125 Trade(pw []byte, form *core.TradeForm) (*core.Order, error) 126 TradeAsync(pw []byte, form *core.TradeForm) (*core.InFlightOrder, error) 127 Cancel(oid dex.Bytes) error 128 NotificationFeed() *core.NoteFeed 129 Logout() error 130 Orders(*core.OrderFilter) ([]*core.Order, error) 131 Order(oid dex.Bytes) (*core.Order, error) 132 MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error) 133 MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error) 134 AccountExport(pw []byte, host string) (*core.Account, []*db.Bond, error) 135 AccountImport(pw []byte, account *core.Account, bonds []*db.Bond) error 136 ToggleAccountStatus(pw []byte, host string, disable bool) error 137 IsInitialized() bool 138 ExportSeed(pw []byte) (string, error) 139 PreOrder(*core.TradeForm) (*core.OrderEstimate, error) 140 WalletLogFilePath(assetID uint32) (string, error) 141 BondsFeeBuffer(assetID uint32) (uint64, error) 142 PreAccelerateOrder(oidB dex.Bytes) (*core.PreAccelerate, error) 143 AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) 144 AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) 145 UpdateCert(host string, cert []byte) error 146 UpdateDEXHost(oldHost, newHost string, appPW []byte, certI any) (*core.Exchange, error) 147 WalletRestorationInfo(pw []byte, assetID uint32) ([]*asset.WalletRestoration, error) 148 ToggleRateSourceStatus(src string, disable bool) error 149 FiatRateSources() map[string]bool 150 EstimateSendTxFee(address string, assetID uint32, value uint64, subtract, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) 151 ValidateAddress(address string, assetID uint32) (bool, error) 152 DeleteArchivedRecordsWithBackup(olderThan *time.Time, saveMatchesToFile, saveOrdersToFile bool) (string, int, error) 153 WalletPeers(assetID uint32) ([]*asset.WalletPeer, error) 154 AddWalletPeer(assetID uint32, addr string) error 155 RemoveWalletPeer(assetID uint32, addr string) error 156 Notifications(n int) (notes, pokes []*db.Notification, _ error) 157 ApproveToken(appPW []byte, assetID uint32, dexAddr string, onConrim func()) (string, error) 158 UnapproveToken(appPW []byte, assetID uint32, version uint32) (string, error) 159 ApproveTokenFee(assetID uint32, version uint32, approval bool) (uint64, error) 160 StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) 161 SetVSP(assetID uint32, addr string) error 162 PurchaseTickets(assetID uint32, pw []byte, n int) error 163 SetVotingPreferences(assetID uint32, choices, tSpendPolicy, treasuryPolicy map[string]string) error 164 ListVSPs(assetID uint32) ([]*asset.VotingServiceProvider, error) 165 TicketPage(assetID uint32, scanStart int32, n, skipN int) ([]*asset.Ticket, error) 166 TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) 167 FundsMixingStats(assetID uint32) (*asset.FundsMixingStats, error) 168 ConfigureFundsMixer(appPW []byte, assetID uint32, enabled bool) error 169 SetLanguage(string) error 170 Language() string 171 TakeAction(assetID uint32, actionID string, actionB json.RawMessage) error 172 RedeemGeocode(appPW, code []byte, msg string) (dex.Bytes, uint64, error) 173 ExtensionModeConfig() *core.ExtensionModeConfig 174 } 175 176 type MMCore interface { 177 MarketReport(host string, base, quote uint32) (*mm.MarketReport, error) 178 StartBot(mkt *mm.StartConfig, alternateConfigPath *string, pw []byte) (err error) 179 StopBot(mkt *mm.MarketWithHost) error 180 UpdateCEXConfig(updatedCfg *mm.CEXConfig) error 181 CEXBalance(cexName string, assetID uint32) (*libxc.ExchangeBalance, error) 182 UpdateBotConfig(updatedCfg *mm.BotConfig) error 183 RemoveBotConfig(host string, baseID, quoteID uint32) error 184 Status() *mm.Status 185 ArchivedRuns() ([]*mm.MarketMakingRun, error) 186 RunOverview(startTime int64, mkt *mm.MarketWithHost) (*mm.MarketMakingRunOverview, error) 187 RunLogs(startTime int64, mkt *mm.MarketWithHost, n uint64, refID *uint64, filter *mm.RunLogFilters) (events, updatedEvents []*mm.MarketMakingEvent, overview *mm.MarketMakingRunOverview, err error) 188 CEXBook(host string, baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) 189 } 190 191 // genCertPair generates a key/cert pair to the paths provided. 192 func genCertPair(certFile, keyFile string, altDNSNames []string) error { 193 log.Infof("Generating TLS certificates...") 194 195 org := "dex webserver autogenerated cert" 196 validUntil := time.Now().Add(390 * 24 * time.Hour) // https://blog.mozilla.org/security/2020/07/09/reducing-tls-certificate-lifespans-to-398-days/ 197 cert, key, err := certgen.NewTLSCertPair(elliptic.P384(), org, 198 validUntil, altDNSNames) 199 if err != nil { 200 return err 201 } 202 203 // Write cert and key files. 204 if err = os.WriteFile(certFile, cert, 0644); err != nil { 205 return err 206 } 207 if err = os.WriteFile(keyFile, key, 0600); err != nil { 208 os.Remove(certFile) 209 return err 210 } 211 212 return nil 213 } 214 215 var _ clientCore = (*core.Core)(nil) 216 217 // cachedPassword consists of the serialized crypter and an encrypted password. 218 // A key stored in the cookies is used to deserialize the crypter, then the 219 // crypter is used to decrypt the password. 220 type cachedPassword struct { 221 EncryptedPass []byte 222 SerializedCrypter []byte 223 } 224 225 type Config struct { 226 Core clientCore // *core.Core 227 MarketMaker MMCore // *mm.MarketMaker 228 Addr string 229 CustomSiteDir string 230 Language string 231 Logger dex.Logger 232 UTC bool // for stdout http request logging 233 CertFile string 234 KeyFile string 235 // NoEmbed indicates to serve files from the system disk rather than the 236 // embedded files. Since this is a developer setting, this also implies 237 // reloading of templates on each request. Note that only embedded files 238 // should be used by default since site files from older distributions may 239 // be present on the disk. When NoEmbed is true, this also implies reloading 240 // and execution of html templates on each request. 241 NoEmbed bool 242 HttpProf bool 243 } 244 245 type valStamp struct { 246 val uint64 247 stamp time.Time 248 } 249 250 // WebServer is a single-client http and websocket server enabling a browser 251 // interface to Bison Wallet. 252 type WebServer struct { 253 ctx context.Context 254 wsServer *websocket.Server 255 mux *chi.Mux 256 siteDir string 257 lang atomic.Value // string 258 langs []string 259 core clientCore 260 mm MMCore 261 addr string 262 csp string 263 srv *http.Server 264 html atomic.Value // *templates 265 266 authMtx sync.RWMutex 267 authTokens map[string]bool 268 cachedPasswords map[string]*cachedPassword // cached passwords keyed by auth token 269 270 bondBufMtx sync.Mutex 271 bondBuf map[uint32]valStamp 272 273 useDEXBranding bool 274 } 275 276 // New is the constructor for a new WebServer. CustomSiteDir in the Config can 277 // be left blank, in which case a handful of default locations will be checked. 278 // This will work in most cases. 279 func New(cfg *Config) (*WebServer, error) { 280 log = cfg.Logger 281 282 // Only look for files on disk if NoEmbed is set. This is necessary since 283 // site files from older distributions may be present. 284 var siteDir string // empty signals embedded files only 285 if cfg.NoEmbed { 286 // Look for the "site" folder in the executable's path, the working 287 // directory, or relative to [repo root]/client/cmd/bisonw. 288 execPath, err := os.Executable() // e.g. /usr/bin/bisonw 289 if err != nil { 290 return nil, fmt.Errorf("unable to locate executable path: %w", err) 291 } 292 execPath, err = filepath.EvalSymlinks(execPath) // e.g. /opt/decred/dex/bisonw 293 if err != nil { 294 return nil, fmt.Errorf("unable to locate executable path: %w", err) 295 } 296 execPath = filepath.Dir(execPath) // e.g. /opt/decred/dex 297 298 absDir, _ := filepath.Abs(site) 299 for _, dir := range []string{ 300 cfg.CustomSiteDir, 301 filepath.Join(execPath, site), 302 absDir, 303 filepath.Clean(filepath.Join(execPath, "../../webserver/site")), 304 } { 305 if dir == "" { 306 continue 307 } 308 log.Debugf("Looking for site in %s", dir) 309 if folderExists(dir) { 310 siteDir = dir 311 break 312 } 313 } 314 315 if siteDir == "" { 316 return nil, fmt.Errorf("no HTML template files found. "+ 317 "Place the 'site' folder in the executable's directory %q or the working directory, "+ 318 "or run bisonw from within the client/cmd/bisonw source workspace folder, or specify the"+ 319 "'sitedir' configuration directive to bisonw.", execPath) 320 } 321 322 log.Infof("Located \"site\" folder at %v", siteDir) 323 } else { 324 // Developer should remember to rebuild the Go binary if they modify any 325 // frontend files, otherwise they should run with --no-embed-site. 326 log.Debugf("Using embedded site resources.") 327 } 328 329 // Create an HTTP router. 330 mux := chi.NewRouter() 331 httpServer := &http.Server{ 332 Handler: mux, 333 ReadTimeout: httpConnTimeoutSeconds * time.Second, // slow requests should not hold connections opened 334 WriteTimeout: 2 * time.Minute, // request to response time, must be long enough for slow handlers 335 } 336 337 if cfg.CertFile != "" || cfg.KeyFile != "" { 338 // Find or create the key pair. 339 keyExists := dex.FileExists(cfg.KeyFile) 340 certExists := dex.FileExists(cfg.CertFile) 341 if certExists != keyExists { 342 return nil, fmt.Errorf("missing cert pair file") 343 } 344 if !keyExists { 345 if err := genCertPair(cfg.CertFile, cfg.KeyFile, []string{cfg.Addr}); err != nil { 346 return nil, err 347 } 348 // TODO: generate a separate CA certificate. Browsers don't like 349 // that the site certificate is also a CA. 350 } 351 keyPair, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) 352 if err != nil { 353 return nil, err 354 } 355 log.Infof("Using HTTPS with certificate %v and key %v. "+ 356 "You may import the certificate as an authority (CA) in your browser, "+ 357 "or override the warning about a self-signed certificate. "+ 358 "Delete both files to regenerate them on next startup.", 359 cfg.CertFile, cfg.KeyFile) 360 httpServer.TLSConfig = &tls.Config{ 361 ServerName: cfg.Addr, 362 Certificates: []tls.Certificate{keyPair}, 363 MinVersion: tls.VersionTLS12, 364 } 365 // Uncomment to disable HTTP/2: 366 // httpServer.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0) 367 } 368 369 lang := cfg.Core.Language() 370 371 langs := make([]string, 0, len(localesMap)) 372 for l := range localesMap { 373 langs = append(langs, l) 374 } 375 376 var useDEXBranding bool 377 if xCfg := cfg.Core.ExtensionModeConfig(); xCfg != nil { 378 useDEXBranding = xCfg.UseDEXBranding 379 } 380 381 // Make the server here so its methods can be registered. 382 s := &WebServer{ 383 langs: langs, 384 core: cfg.Core, 385 mm: cfg.MarketMaker, 386 siteDir: siteDir, 387 mux: mux, 388 srv: httpServer, 389 addr: cfg.Addr, 390 wsServer: websocket.New(cfg.Core, log.SubLogger("WS")), 391 authTokens: make(map[string]bool), 392 cachedPasswords: make(map[string]*cachedPassword), 393 bondBuf: map[uint32]valStamp{}, 394 useDEXBranding: useDEXBranding, 395 } 396 s.lang.Store(lang) 397 398 if err := s.buildTemplates(lang); err != nil { 399 return nil, fmt.Errorf("error loading localized html templates: %v", err) 400 } 401 402 // Middleware 403 mux.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{ 404 Logger: &chiLogger{ // logs with Trace() 405 Logger: dex.StdOutLogger("MUX", log.Level(), cfg.UTC), 406 }, 407 NoColor: runtime.GOOS == "windows", 408 })) 409 mux.Use(s.securityMiddleware) 410 mux.Use(middleware.Recoverer) 411 412 // HTTP profiler 413 if cfg.HttpProf { 414 profPath := "/debug/pprof" 415 log.Infof("Mounting the HTTP profiler on %s", profPath) 416 // Option A: mount each httpprof handler directly. The caveat with this 417 // is that httpprof.Index ONLY works when mounted on /debug/pprof/. 418 // 419 // mux.Mount(profPath, http.HandlerFunc(httppprof.Index)) // also 420 // handles: goroutine, heap, threadcreate, block, allocs, mutex 421 // mux.Mount(profPath+"/cmdline", http.HandlerFunc(httppprof.Cmdline)) 422 // mux.Mount(profPath+"/profile", http.HandlerFunc(httppprof.Profile)) 423 // mux.Mount(profPath+"/symbol", http.HandlerFunc(httppprof.Symbol)) 424 // mux.Mount(profPath+"/trace", http.HandlerFunc(httppprof.Trace)) 425 426 // Option B: http pprof uses http.DefaultServeMux, so mount it: 427 mux.Mount(profPath, http.DefaultServeMux) // profPath MUST be /debug/pprof this way 428 } 429 430 // The WebSocket handler is mounted on /ws in Connect. 431 432 // Webpages 433 mux.Group(func(web chi.Router) { 434 // Inject user info for handlers that use extractUserInfo, which 435 // includes most of the page handlers that use commonArgs to 436 // inject the User object for page template execution. 437 web.Use(s.authMiddleware) 438 web.Get(settingsRoute, s.handleSettings) 439 440 web.Get("/generateqrcode", s.handleGenerateQRCode) 441 442 web.Group(func(notInit chi.Router) { 443 notInit.Use(s.requireNotInit) 444 notInit.Get(initRoute, s.handleInit) 445 }) 446 447 // The rest of the web handlers require initialization. 448 web.Group(func(webInit chi.Router) { 449 webInit.Use(s.requireInit) 450 451 webInit.Route(registerRoute, func(rr chi.Router) { 452 rr.Get("/", s.handleRegister) 453 rr.With(dexHostCtx).Get("/{host}", s.handleRegister) 454 }) 455 456 webInit.Group(func(webNoAuth chi.Router) { 457 // The login handler requires init but not auth since 458 // it performs the auth. 459 webNoAuth.Get(loginRoute, s.handleLogin) 460 461 // The rest of these handlers require both init and auth. 462 webNoAuth.Group(func(webAuth chi.Router) { 463 webAuth.Use(s.requireLogin) 464 webAuth.Get(homeRoute, s.handleHome) 465 webAuth.Get(walletsRoute, s.handleWallets) 466 webAuth.Get(walletLogRoute, s.handleWalletLogFile) 467 }) 468 }) 469 470 // Handlers requiring a DEX connection. 471 webInit.Group(func(webDC chi.Router) { 472 webDC.Use(s.requireDEXConnection, s.requireLogin) 473 webDC.With(orderIDCtx).Get("/order/{oid}", s.handleOrder) 474 webDC.Get(ordersRoute, s.handleOrders) 475 webDC.Get(exportOrderRoute, s.handleExportOrders) 476 webDC.Get(marketsRoute, s.handleMarkets) 477 webDC.Get(mmSettingsRoute, s.handleMMSettings) 478 webDC.Get(mmArchivesRoute, s.handleMMArchives) 479 webDC.Get(mmLogsRoute, s.handleMMLogs) 480 webDC.Get(marketMakerRoute, s.handleMarketMaking) 481 webDC.With(dexHostCtx).Get("/dexsettings/{host}", s.handleDexSettings) 482 }) 483 484 }) 485 }) 486 487 // api endpoints 488 mux.Route("/api", func(r chi.Router) { 489 r.Use(middleware.AllowContentType("application/json")) 490 r.Post("/init", s.apiInit) 491 r.Get("/isinitialized", s.apiIsInitialized) 492 r.Post("/resetapppassword", s.apiResetAppPassword) 493 r.Get("/user", s.apiUser) 494 r.Post("/locale", s.apiLocale) 495 r.Post("/setlocale", s.apiSetLocale) 496 497 r.Group(func(apiInit chi.Router) { 498 apiInit.Use(s.rejectUninited) 499 apiInit.Post("/login", s.apiLogin) 500 apiInit.Post("/getdexinfo", s.apiGetDEXInfo) // TODO: Seems unused. 501 apiInit.Post("/adddex", s.apiAddDEX) 502 apiInit.Post("/discoveracct", s.apiDiscoverAccount) 503 apiInit.Post("/bondsfeebuffer", s.apiBondsFeeBuffer) 504 }) 505 506 r.Group(func(apiAuth chi.Router) { 507 apiAuth.Use(s.rejectUnauthed) 508 apiAuth.Get("/notes", s.apiNotes) 509 apiAuth.Post("/defaultwalletcfg", s.apiDefaultWalletCfg) 510 apiAuth.Post("/postbond", s.apiPostBond) 511 apiAuth.Post("/updatebondoptions", s.apiUpdateBondOptions) 512 apiAuth.Post("/redeemprepaidbond", s.apiRedeemPrepaidBond) 513 apiAuth.Post("/newwallet", s.apiNewWallet) 514 apiAuth.Post("/openwallet", s.apiOpenWallet) 515 apiAuth.Post("/depositaddress", s.apiNewDepositAddress) 516 apiAuth.Post("/closewallet", s.apiCloseWallet) 517 apiAuth.Post("/connectwallet", s.apiConnectWallet) 518 apiAuth.Post("/rescanwallet", s.apiRescanWallet) 519 apiAuth.Post("/recoverwallet", s.apiRecoverWallet) 520 apiAuth.Post("/trade", s.apiTrade) 521 apiAuth.Post("/tradeasync", s.apiTradeAsync) 522 apiAuth.Post("/cancel", s.apiCancel) 523 apiAuth.Post("/logout", s.apiLogout) 524 apiAuth.Post("/balance", s.apiGetBalance) 525 apiAuth.Post("/parseconfig", s.apiParseConfig) 526 apiAuth.Post("/reconfigurewallet", s.apiReconfig) 527 apiAuth.Post("/changeapppass", s.apiChangeAppPass) 528 apiAuth.Post("/walletsettings", s.apiWalletSettings) 529 apiAuth.Post("/togglewalletstatus", s.apiToggleWalletStatus) 530 apiAuth.Post("/orders", s.apiOrders) 531 apiAuth.Post("/order", s.apiOrder) 532 apiAuth.Post("/send", s.apiSend) 533 apiAuth.Post("/maxbuy", s.apiMaxBuy) 534 apiAuth.Post("/maxsell", s.apiMaxSell) 535 apiAuth.Post("/preorder", s.apiPreOrder) 536 apiAuth.Post("/exportaccount", s.apiAccountExport) 537 apiAuth.Post("/exportseed", s.apiExportSeed) 538 apiAuth.Post("/importaccount", s.apiAccountImport) 539 apiAuth.Post("/toggleaccountstatus", s.apiToggleAccountStatus) 540 apiAuth.Post("/accelerateorder", s.apiAccelerateOrder) 541 apiAuth.Post("/preaccelerate", s.apiPreAccelerate) 542 apiAuth.Post("/accelerationestimate", s.apiAccelerationEstimate) 543 apiAuth.Post("/updatecert", s.apiUpdateCert) 544 apiAuth.Post("/updatedexhost", s.apiUpdateDEXHost) 545 apiAuth.Post("/restorewalletinfo", s.apiRestoreWalletInfo) 546 apiAuth.Post("/toggleratesource", s.apiToggleRateSource) 547 apiAuth.Post("/validateaddress", s.apiValidateAddress) 548 apiAuth.Post("/txfee", s.apiEstimateSendTxFee) 549 apiAuth.Post("/deletearchivedrecords", s.apiDeleteArchivedRecords) 550 apiAuth.Post("/getwalletpeers", s.apiGetWalletPeers) 551 apiAuth.Post("/addwalletpeer", s.apiAddWalletPeer) 552 apiAuth.Post("/removewalletpeer", s.apiRemoveWalletPeer) 553 apiAuth.Post("/approvetoken", s.apiApproveToken) 554 apiAuth.Post("/unapprovetoken", s.apiUnapproveToken) 555 apiAuth.Post("/approvetokenfee", s.apiApproveTokenFee) 556 apiAuth.Post("/txhistory", s.apiTxHistory) 557 apiAuth.Post("/takeaction", s.apiTakeAction) 558 apiAuth.Post("/redeemgamecode", s.redeemGameCode) 559 560 apiAuth.Post("/stakestatus", s.apiStakeStatus) 561 apiAuth.Post("/setvsp", s.apiSetVSP) 562 apiAuth.Post("/purchasetickets", s.apiPurchaseTickets) 563 apiAuth.Post("/setvotes", s.apiSetVotingPreferences) 564 apiAuth.Post("/listvsps", s.apiListVSPs) 565 apiAuth.Post("/ticketpage", s.apiTicketPage) 566 567 apiAuth.Post("/mixingstats", s.apiMixingStats) 568 apiAuth.Post("/configuremixer", s.apiConfigureMixer) 569 570 apiAuth.Post("/startmarketmakingbot", s.apiStartMarketMakingBot) 571 apiAuth.Post("/stopmarketmakingbot", s.apiStopMarketMakingBot) 572 apiAuth.Post("/updatebotconfig", s.apiUpdateBotConfig) 573 apiAuth.Post("/updatecexconfig", s.apiUpdateCEXConfig) 574 apiAuth.Post("/removebotconfig", s.apiRemoveBotConfig) 575 apiAuth.Get("/marketmakingstatus", s.apiMarketMakingStatus) 576 apiAuth.Post("/marketreport", s.apiMarketReport) 577 apiAuth.Post("/cexbalance", s.apiCEXBalance) 578 apiAuth.Get("/archivedmmruns", s.apiArchivedRuns) 579 apiAuth.Post("/mmrunlogs", s.apiRunLogs) 580 apiAuth.Post("/cexbook", s.apiCEXBook) 581 }) 582 }) 583 584 // Files 585 fileServer(mux, "/js", siteDir, "dist", "text/javascript") 586 fileServer(mux, "/css", siteDir, "dist", "text/css") 587 fileServer(mux, "/img", siteDir, "src/img", "") 588 fileServer(mux, "/font", siteDir, "src/font", "") 589 590 return s, nil 591 } 592 593 // buildTemplates prepares the HTML templates, which are executed and served in 594 // sendTemplate. An empty siteDir indicates that the embedded templates in the 595 // htmlTmplSub FS should be used. If siteDir is set, the templates will be 596 // loaded from disk. 597 func (s *WebServer) buildTemplates(lang string) error { 598 // Try to identify language. 599 acceptLang, err := language.Parse(lang) 600 if err != nil { 601 return fmt.Errorf("unable to parse requested language: %v", err) 602 } 603 604 // Find acceptable match with available locales. 605 langTags := make([]language.Tag, 0, len(locales.Locales)) 606 localeNames := make([]string, 0, len(locales.Locales)) 607 for localeName := range locales.Locales { 608 lang, _ := language.Parse(localeName) // checked in init() 609 langTags = append(langTags, lang) 610 localeNames = append(localeNames, localeName) 611 } 612 _, idx, conf := language.NewMatcher(langTags).Match(acceptLang) 613 localeName := localeNames[idx] // use index because tag may end up as something hyper specific like zh-Hans-u-rg-cnzzzz 614 switch conf { 615 case language.Exact, language.High, language.Low: 616 log.Infof("Using language %v", localeName) 617 case language.No: 618 return fmt.Errorf("no match for %q in recognized languages %v", lang, localeNames) 619 } 620 621 var htmlDir string 622 if s.siteDir == "" { 623 log.Infof("Using embedded HTML templates") 624 } else { 625 htmlDir = filepath.Join(s.siteDir, "src", "html") 626 log.Infof("Using HTML templates in %s", htmlDir) 627 } 628 629 bb := "bodybuilder" 630 html := newTemplates(htmlDir, localeName). 631 addTemplate("login", bb, "forms"). 632 addTemplate("register", bb, "forms"). 633 addTemplate("markets", bb, "forms"). 634 addTemplate("wallets", bb, "forms"). 635 addTemplate("settings", bb, "forms"). 636 addTemplate("orders", bb). 637 addTemplate("order", bb, "forms"). 638 addTemplate("dexsettings", bb, "forms"). 639 addTemplate("init", bb). 640 addTemplate("mm", bb, "forms"). 641 addTemplate("mmsettings", bb, "forms"). 642 addTemplate("mmarchives", bb). 643 addTemplate("mmlogs", bb) 644 s.html.Store(html) 645 646 return html.buildErr() 647 } 648 649 // Addr gives the address on which WebServer is listening. Use only after 650 // Connect. 651 func (s *WebServer) Addr() string { 652 return s.addr 653 } 654 655 // Connect starts the web server. Satisfies the dex.Connector interface. 656 func (s *WebServer) Connect(ctx context.Context) (*sync.WaitGroup, error) { 657 // Start serving. 658 listener, err := net.Listen("tcp", s.addr) 659 if err != nil { 660 return nil, fmt.Errorf("Can't listen on %s. web server quitting: %w", s.addr, err) 661 } 662 https := s.srv.TLSConfig != nil 663 if https { 664 listener = tls.NewListener(listener, s.srv.TLSConfig) 665 } 666 667 s.ctx = ctx 668 669 addr, allowInCSP := prepareAddr(listener.Addr()) 670 if allowInCSP { 671 // Work around a webkit (safari) bug with the handling of the 672 // connect-src directive of content security policy. See: 673 // https://bugs.webkit.org/show_bug.cgi?id=201591. TODO: Remove this 674 // workaround since the issue has been fixed in newer versions of 675 // Safari. When this is removed, the allowInCSP variable can be removed 676 // but prepareAddr should still return 127.0.0.1 for unspecified 677 // addresses. 678 scheme := "ws" 679 if s.srv.TLSConfig != nil { 680 scheme = "wss" 681 } 682 s.csp = fmt.Sprintf("%s %s://%s", baseCSP, scheme, addr) 683 } 684 s.addr = addr 685 686 // Shutdown the server on context cancellation. 687 var wg sync.WaitGroup 688 wg.Add(1) 689 go func() { 690 defer wg.Done() 691 <-ctx.Done() 692 err := s.srv.Shutdown(context.Background()) 693 if err != nil { 694 log.Errorf("Problem shutting down rpc: %v", err) 695 } 696 }() 697 698 // Configure the websocket handler before starting the server. 699 s.mux.Get("/ws", func(w http.ResponseWriter, r *http.Request) { 700 s.wsServer.HandleConnect(ctx, w, r) 701 }) 702 703 wg.Add(1) 704 go func() { 705 defer wg.Done() 706 err = s.srv.Serve(listener) // will modify srv.TLSConfig for http/2 even when !https 707 if !errors.Is(err, http.ErrServerClosed) { 708 log.Warnf("unexpected (http.Server).Serve error: %v", err) 709 } 710 // Disconnect the websocket clients since http.(*Server).Shutdown does 711 // not deal with hijacked websocket connections. 712 s.wsServer.Shutdown() 713 log.Infof("Web server off") 714 }() 715 716 wg.Add(1) 717 go func() { 718 defer wg.Done() 719 s.readNotifications(ctx) 720 }() 721 722 log.Infof("Web server listening on %s (https = %v)", s.addr, https) 723 scheme := "http" 724 if https { 725 scheme = "https" 726 } 727 fmt.Printf("\n\t**** OPEN IN YOUR BROWSER TO LOGIN AND TRADE ---> %s://%s ****\n\n", 728 scheme, s.addr) 729 return &wg, nil 730 } 731 732 // prepareAddr prepares the listening address in case a :0 was provided. 733 func prepareAddr(addr net.Addr) (string, bool) { 734 // If the IP is unspecified, default to `127.0.0.1`. This is a workaround 735 // for an issue where all ip addresses other than exactly 127.0.0.1 will 736 // always fail to match when used in CSP directives. See: 737 // https://w3c.github.io/webappsec-csp/#match-hosts. 738 defaultIP := net.IP{127, 0, 0, 1} 739 tcpAddr, ok := addr.(*net.TCPAddr) 740 if ok && (tcpAddr.IP.IsUnspecified() || tcpAddr.IP.Equal(defaultIP)) { 741 return net.JoinHostPort(defaultIP.String(), strconv.Itoa(tcpAddr.Port)), true 742 } 743 744 return addr.String(), false 745 } 746 747 // authorize creates, stores, and returns a new auth token to identify the user. 748 // deauth should be used to invalidate tokens on logout. 749 func (s *WebServer) authorize() string { 750 b := make([]byte, 32) 751 crand.Read(b) 752 token := hex.EncodeToString(b) 753 zero(b) 754 s.authMtx.Lock() 755 s.authTokens[token] = true 756 s.authMtx.Unlock() 757 return token 758 } 759 760 // deauth invalidates all current auth tokens. All existing sessions will need 761 // to login again. 762 func (s *WebServer) deauth() { 763 s.authMtx.Lock() 764 s.authTokens = make(map[string]bool) 765 s.cachedPasswords = make(map[string]*cachedPassword) 766 s.authMtx.Unlock() 767 } 768 769 // getAuthToken checks the request for an auth token cookie and returns it. 770 // An empty string is returned if there is no auth token cookie. 771 func getAuthToken(r *http.Request) string { 772 var authToken string 773 cookie, err := r.Cookie(authCK) 774 switch { 775 case err == nil: 776 authToken = cookie.Value 777 case errors.Is(err, http.ErrNoCookie): 778 default: 779 log.Errorf("authToken retrieval error: %v", err) 780 } 781 782 return authToken 783 } 784 785 // getPWKey checks the request for a password key cookie. Returns an error 786 // if it does not exist or it is not valid. 787 func getPWKey(r *http.Request) ([]byte, error) { 788 cookie, err := r.Cookie(pwKeyCK) 789 switch { 790 case err == nil: 791 sessionKey, err := hex.DecodeString(cookie.Value) 792 if err != nil { 793 return nil, err 794 } 795 return sessionKey, nil 796 case errors.Is(err, http.ErrNoCookie): 797 return nil, nil 798 default: 799 return nil, err 800 } 801 } 802 803 // isAuthed checks if the incoming request is from an authorized user/device. 804 // Requires the auth token cookie to be set in the request and for the token 805 // to match `WebServer.validAuthToken`. 806 func (s *WebServer) isAuthed(r *http.Request) bool { 807 authToken := getAuthToken(r) 808 if authToken == "" { 809 return false 810 } 811 s.authMtx.RLock() 812 defer s.authMtx.RUnlock() 813 return s.authTokens[authToken] 814 } 815 816 // getCachedPassword retrieves the cached password for the user identified by authToken and 817 // presenting the specified key in their cookies. 818 func (s *WebServer) getCachedPassword(key []byte, authToken string) ([]byte, error) { 819 s.authMtx.Lock() 820 cachedPassword, ok := s.cachedPasswords[authToken] 821 s.authMtx.Unlock() 822 if !ok { 823 return nil, fmt.Errorf("cached encrypted password not found for"+ 824 " auth token: %v", authToken) 825 } 826 827 crypter, err := encrypt.Deserialize(key, cachedPassword.SerializedCrypter) 828 if err != nil { 829 return nil, fmt.Errorf("error deserializing crypter: %w", err) 830 } 831 832 pw, err := crypter.Decrypt(cachedPassword.EncryptedPass) 833 if err != nil { 834 return nil, fmt.Errorf("error decrypting password: %w", err) 835 } 836 837 return pw, nil 838 } 839 840 // getCachedPasswordUsingRequest retrieves the cached password using the information 841 // in the request. 842 func (s *WebServer) getCachedPasswordUsingRequest(r *http.Request) ([]byte, error) { 843 authToken := getAuthToken(r) 844 if authToken == "" { 845 return nil, errNoCachedPW 846 } 847 pwKeyBlob, err := getPWKey(r) 848 if err != nil { 849 return nil, err 850 } 851 if pwKeyBlob == nil { 852 return nil, errNoCachedPW 853 } 854 return s.getCachedPassword(pwKeyBlob, authToken) 855 } 856 857 // cacheAppPassword encrypts the app password with a random encryption key and returns the key. 858 // The authToken is used to lookup the encrypted password when calling getCachedPassword. 859 func (s *WebServer) cacheAppPassword(appPW []byte, authToken string) ([]byte, error) { 860 key := encode.RandomBytes(16) 861 crypter := encrypt.NewCrypter(key) 862 defer crypter.Close() 863 encryptedPass, err := crypter.Encrypt(appPW) 864 if err != nil { 865 return nil, fmt.Errorf("error encrypting password: %v", err) 866 } 867 868 s.authMtx.Lock() 869 s.cachedPasswords[authToken] = &cachedPassword{ 870 EncryptedPass: encryptedPass, 871 SerializedCrypter: crypter.Serialize(), 872 } 873 s.authMtx.Unlock() 874 return key, nil 875 } 876 877 // isPasswordCached checks if a password can be retrieved from the encrypted 878 // password cache using the information in the request. 879 func (s *WebServer) isPasswordCached(r *http.Request) bool { 880 _, err := s.getCachedPasswordUsingRequest(r) 881 return err == nil 882 } 883 884 // readNotifications reads from the Core notification channel and relays to 885 // websocket clients. 886 func (s *WebServer) readNotifications(ctx context.Context) { 887 ch := s.core.NotificationFeed() 888 defer ch.ReturnFeed() 889 890 for { 891 select { 892 case n := <-ch.C: 893 s.wsServer.Notify(notifyRoute, n) 894 case <-ctx.Done(): 895 return 896 } 897 } 898 } 899 900 // readPost unmarshals the request body into the provided interface. 901 func readPost(w http.ResponseWriter, r *http.Request, thing any) bool { 902 body, err := io.ReadAll(r.Body) 903 r.Body.Close() 904 if err != nil { 905 log.Debugf("Error reading request body: %v", err) 906 http.Error(w, "error reading JSON message", http.StatusBadRequest) 907 return false 908 } 909 err = json.Unmarshal(body, thing) 910 if err != nil { 911 log.Debugf("failed to unmarshal JSON request: %v", err) 912 log.Debugf("raw request: %s", string(body)) 913 http.Error(w, "failed to unmarshal JSON request", http.StatusBadRequest) 914 return false 915 } 916 return true 917 } 918 919 // userInfo is information about the connected user. This type embeds the 920 // core.User type, adding fields specific to the users server authentication 921 // and cookies. 922 type userInfo struct { 923 Authed bool 924 PasswordIsCached bool 925 } 926 927 // Extract the userInfo from the request context. This should be used with 928 // authMiddleware. 929 func extractUserInfo(r *http.Request) *userInfo { 930 user, ok := r.Context().Value(ctxKeyUserInfo).(*userInfo) 931 if !ok { 932 log.Errorf("no auth info retrieved from client") 933 return &userInfo{} 934 } 935 return user 936 } 937 938 func serveFile(w http.ResponseWriter, r *http.Request, fullFilePath string) { 939 // Generate the full file system path and test for existence. 940 fi, err := os.Stat(fullFilePath) 941 if err != nil { 942 http.NotFound(w, r) 943 return 944 } 945 946 // Deny directory listings 947 if fi.IsDir() { 948 http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 949 return 950 } 951 952 http.ServeFile(w, r, fullFilePath) 953 } 954 955 // fileServer is a file server for files in subDir with the parent folder 956 // siteDire. An empty siteDir means embedded files only are served. The 957 // pathPrefix is stripped from the request path when locating the file. 958 func fileServer(r chi.Router, pathPrefix, siteDir, subDir, forceContentType string) { 959 if strings.ContainsAny(pathPrefix, "{}*") { 960 panic("FileServer does not permit URL parameters.") 961 } 962 963 // Define a http.HandlerFunc to serve files but not directory indexes. 964 hf := func(w http.ResponseWriter, r *http.Request) { 965 // Ensure the path begins with "/". 966 upath := r.URL.Path 967 if strings.Contains(upath, "..") { 968 http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 969 return 970 } 971 if !strings.HasPrefix(upath, "/") { 972 upath = "/" + upath 973 r.URL.Path = upath 974 } 975 // Strip the path prefix and clean the path. 976 upath = path.Clean(strings.TrimPrefix(upath, pathPrefix)) 977 978 // Deny directory listings (http.ServeFile recognizes index.html and 979 // attempts to serve the directory contents instead). 980 if strings.HasSuffix(upath, "/index.html") { 981 http.NotFound(w, r) 982 return 983 } 984 985 // On Windows, a common registry misconfiguration leads to 986 // mime.TypeByExtension setting an incorrect type for .js files, causing 987 // the browser to refuse to execute the JavaScript. The following 988 // workaround may be removed when Go 1.19 becomes the minimum required 989 // version: https://go-review.googlesource.com/c/go/+/406894 990 // https://github.com/golang/go/issues/32350 991 if forceContentType != "" { 992 w.Header().Set("Content-Type", forceContentType) 993 } 994 995 // If siteDir is set, use system file system only. 996 if siteDir != "" { 997 fullFilePath := filepath.Join(siteDir, subDir, upath) 998 serveFile(w, r, fullFilePath) 999 return 1000 } 1001 1002 // Use the embedded files only. 1003 fs := http.FS(staticSiteRes) // so f is an http.File instead of fs.File 1004 f, err := fs.Open(path.Join(site, subDir, upath)) 1005 if err != nil { 1006 http.NotFound(w, r) 1007 return 1008 } 1009 defer f.Close() 1010 1011 // return in case it is a directory 1012 stat, err := f.Stat() 1013 if err != nil { 1014 http.Error(w, err.Error(), http.StatusInternalServerError) 1015 return 1016 } 1017 if stat.IsDir() { 1018 http.NotFound(w, r) 1019 return 1020 } 1021 1022 if forceContentType == "" { 1023 // http.ServeFile would do the following type detection. 1024 contentType := mime.TypeByExtension(filepath.Ext(upath)) 1025 if contentType == "" { 1026 // Sniff out the content type. See http.serveContent. 1027 var buf [512]byte 1028 n, _ := io.ReadFull(f, buf[:]) 1029 contentType = http.DetectContentType(buf[:n]) 1030 _, err = f.Seek(0, io.SeekStart) // rewind to output whole file 1031 if err != nil { 1032 http.Error(w, err.Error(), http.StatusInternalServerError) 1033 return 1034 } 1035 } 1036 if contentType != "" { 1037 w.Header().Set("Content-Type", contentType) 1038 } // else don't set it (plain) 1039 } 1040 1041 sendSize := stat.Size() 1042 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10)) 1043 1044 // TODO: Set Last-Modified for the embedded files. 1045 // if modTime != nil { 1046 // w.Header().Set("Last-Modified", modTime.Format(http.TimeFormat)) 1047 // } 1048 1049 _, err = io.CopyN(w, f, sendSize) 1050 if err != nil { 1051 log.Errorf("Writing response for path %q failed: %v", r.URL.Path, err) 1052 // Too late to write to header with error code. 1053 } 1054 } 1055 1056 // For the chi.Mux, make sure a path that ends in "/" and append a "*". 1057 muxRoot := pathPrefix 1058 if pathPrefix != "/" && pathPrefix[len(pathPrefix)-1] != '/' { 1059 r.Get(pathPrefix, http.RedirectHandler(pathPrefix+"/", 301).ServeHTTP) 1060 muxRoot += "/" 1061 } 1062 muxRoot += "*" 1063 1064 // Mount the http.HandlerFunc on the pathPrefix. 1065 r.Get(muxRoot, hf) 1066 } 1067 1068 // writeJSON marshals the provided interface and writes the bytes to the 1069 // ResponseWriter. The response code is assumed to be StatusOK. 1070 func writeJSON(w http.ResponseWriter, thing any) { 1071 writeJSONWithStatus(w, thing, http.StatusOK) 1072 } 1073 1074 // writeJSON writes marshals the provided interface and writes the bytes to the 1075 // ResponseWriter with the specified response code. 1076 func writeJSONWithStatus(w http.ResponseWriter, thing any, code int) { 1077 w.Header().Set("Content-Type", "application/json; charset=utf-8") 1078 b, err := json.Marshal(thing) 1079 if err != nil { 1080 w.WriteHeader(http.StatusInternalServerError) 1081 log.Errorf("JSON encode error: %v", err) 1082 return 1083 } 1084 w.WriteHeader(code) 1085 _, err = w.Write(append(b, byte('\n'))) 1086 if err != nil { 1087 log.Errorf("Write error: %v", err) 1088 } 1089 } 1090 1091 func folderExists(fp string) bool { 1092 stat, err := os.Stat(fp) 1093 return err == nil && stat.IsDir() 1094 } 1095 1096 // chiLogger is an adaptor around dex.Logger that satisfies 1097 // chi/middleware.LoggerInterface for chi's DefaultLogFormatter. 1098 type chiLogger struct { 1099 dex.Logger 1100 } 1101 1102 func (l *chiLogger) Print(v ...any) { 1103 l.Trace(v...) 1104 }