diff --git a/config.go b/config.go index 2385251b..35b7b043 100644 --- a/config.go +++ b/config.go @@ -35,8 +35,8 @@ const ( defaultLogDirname = "log" defaultLogFilename = "dcrpool.log" defaultDBFilename = "dcrpool.kv" - defaultTLSCertFilename = "dcrpool.cert" - defaultTLSKeyFilename = "dcrpool.key" + defaultGUITLSCertFilename = "dcrpool.cert" + defaultGUITLSKeyFilename = "dcrpool.key" defaultWalletTLSCertFilename = "wallet.cert" defaultWalletTLSKeyFilename = "wallet.key" defaultDcrdRPCHost = "127.0.0.1" @@ -45,10 +45,12 @@ const ( defaultPoolFee = 0.01 defaultLastNPeriod = time.Hour * 24 defaultSoloPool = false - defaultGUIPort = 8080 + defaultGUIPort = "8080" + defaultGUIListen = "0.0.0.0" defaultGUIDir = "gui" defaultUseLEHTTPS = false - defaultMinerPort = 5550 + defaultMinerPort = "5550" + defaultMinerListen = "0.0.0.0" defaultDesignation = "YourPoolNameHere" defaultMaxConnectionsPerHost = 100 // 100 connected clients per host defaultWalletAccount = 0 @@ -61,6 +63,7 @@ const ( defaultPGDBName = "dcrpooldb" defaultMonitorCycle = time.Minute * 2 defaultMaxUpgradeTries = 10 + defaultNoGUITLS = false ) var ( @@ -74,8 +77,8 @@ var ( // This keypair is solely for enabling HTTPS connections to the pool's // web interface. - defaultTLSCertFile = filepath.Join(dcrpoolHomeDir, defaultTLSCertFilename) - defaultTLSKeyFile = filepath.Join(dcrpoolHomeDir, defaultTLSKeyFilename) + defaultGUITLSCertFile = filepath.Join(dcrpoolHomeDir, defaultGUITLSCertFilename) + defaultGUITLSKeyFile = filepath.Join(dcrpoolHomeDir, defaultGUITLSKeyFilename) // This keypair is solely for client authentication to the wallet. defaultWalletTLSCertFile = filepath.Join(dcrpoolHomeDir, defaultWalletTLSCertFilename) @@ -93,7 +96,7 @@ type config struct { ConfigFile string `long:"configfile" ini-name:"configfile" description:"Path to configuration file."` DataDir string `long:"datadir" ini-name:"datadir" description:"The data directory."` ActiveNet string `long:"activenet" ini-name:"activenet" description:"The active network being mined on. {testnet3, mainnet, simnet}"` - GUIPort uint32 `long:"guiport" ini-name:"guiport" description:"The pool GUI port."` + GUIListen string `long:"guilisten" ini-name:"guilisten" description:"The address:port for pool GUI listening."` DebugLevel string `long:"debuglevel" ini-name:"debuglevel" description:"Logging level for all subsystems. {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` LogDir string `long:"logdir" ini-name:"logdir" description:"Directory to log output."` DBFile string `long:"dbfile" ini-name:"dbfile" description:"Path to the database file."` @@ -117,14 +120,14 @@ type config struct { GUIDir string `long:"guidir" ini-name:"guidir" description:"The path to the directory containing the pool's user interface assets (templates, css etc.)"` Domain string `long:"domain" ini-name:"domain" description:"The domain of the mining pool, required for TLS."` UseLEHTTPS bool `long:"uselehttps" ini-name:"uselehttps" description:"This enables HTTPS using a Letsencrypt certificate. By default the pool uses a self-signed certificate for HTTPS."` - TLSCert string `long:"tlscert" ini-name:"tlscert" description:"Path to the TLS cert file."` - TLSKey string `long:"tlskey" ini-name:"tlskey" description:"Path to the TLS key file."` + GUITLSCert string `long:"tlscert" ini-name:"tlscert" description:"Path to the TLS cert file (for running GUI on https)."` + GUITLSKey string `long:"tlskey" ini-name:"tlskey" description:"Path to the TLS key file (for running GUI on https)."` WalletTLSCert string `long:"wallettlscert" ini-name:"wallettlscert" description:"Path to the wallet client TLS cert file."` WalletTLSKey string `long:"wallettlskey" ini-name:"wallettlskey" description:"Path to the wallet client TLS key file."` Designation string `long:"designation" ini-name:"designation" description:"The designated codename for this pool. Customises the logo in the top toolbar."` MaxConnectionsPerHost uint32 `long:"maxconnperhost" ini-name:"maxconnperhost" description:"The maximum number of connections allowed per host."` Profile string `long:"profile" ini-name:"profile" description:"Enable HTTP profiling on given [addr:]port -- NOTE port must be between 1024 and 65536"` - MinerPort uint32 `long:"minerport" ini-name:"minerport" description:"Miner connection port."` + MinerListen string `long:"minerlisten" ini-name:"minerlisten" description:"The address:port for miner connections."` CoinbaseConfTimeout time.Duration `long:"conftimeout" ini-name:"conftimeout" description:"The duration to wait for coinbase confirmations."` GenCertsOnly bool `long:"gencertsonly" ini-name:"gencertsonly" description:"Only generate needed TLS key pairs and terminate."` UsePostgres bool `long:"postgres" ini-name:"postgres" description:"Use postgres database instead of bolt."` @@ -136,6 +139,7 @@ type config struct { PurgeDB bool `long:"purgedb" ini-name:"purgedb" description:"Wipes all existing data on startup for a postgres backend. This intended for simnet testing purposes only."` MonitorCycle time.Duration `long:"monitorcycle" ini-name:"monitorcycle" description:"Time spent monitoring a mining client for possible upgrades."` MaxUpgradeTries uint32 `long:"maxupgradetries" ini-name:"maxupgradetries" description:"Maximum consecuctive miner monitoring and upgrade tries."` + NoGUITLS bool `long:"noguitls" ini-name:"noguitls" description:"Disable TLS on GUI endpoint (eg. for reverse proxy with a dedicated webserver)."` poolFeeAddrs []dcrutil.Address dcrdRPCCerts []byte net *params @@ -353,16 +357,16 @@ func loadConfig() (*config, []string, error) { PaymentMethod: defaultPaymentMethod, LastNPeriod: defaultLastNPeriod, SoloPool: defaultSoloPool, - GUIPort: defaultGUIPort, + GUIListen: defaultGUIListen, GUIDir: defaultGUIDir, UseLEHTTPS: defaultUseLEHTTPS, - TLSCert: defaultTLSCertFile, - TLSKey: defaultTLSKeyFile, + GUITLSCert: defaultGUITLSCertFile, + GUITLSKey: defaultGUITLSKeyFile, WalletTLSCert: defaultWalletTLSCertFile, WalletTLSKey: defaultWalletTLSKeyFile, Designation: defaultDesignation, MaxConnectionsPerHost: defaultMaxConnectionsPerHost, - MinerPort: defaultMinerPort, + MinerListen: defaultMinerListen, WalletAccount: defaultWalletAccount, CoinbaseConfTimeout: defaultCoinbaseConfTimeout, UsePostgres: defaultUsePostgres, @@ -373,6 +377,7 @@ func loadConfig() (*config, []string, error) { PGDBName: defaultPGDBName, MonitorCycle: defaultMonitorCycle, MaxUpgradeTries: defaultMaxUpgradeTries, + NoGUITLS: defaultNoGUITLS, } // Service options which are only added on Windows. @@ -455,15 +460,15 @@ func loadConfig() (*config, []string, error) { } else { cfg.DBFile = preCfg.DBFile } - if preCfg.TLSCert == defaultTLSCertFile { - cfg.TLSCert = filepath.Join(cfg.HomeDir, defaultTLSCertFilename) + if preCfg.GUITLSCert == defaultGUITLSCertFile { + cfg.GUITLSCert = filepath.Join(cfg.HomeDir, defaultGUITLSCertFilename) } else { - cfg.TLSCert = preCfg.TLSCert + cfg.GUITLSCert = preCfg.GUITLSCert } - if preCfg.TLSKey == defaultTLSKeyFile { - cfg.TLSKey = filepath.Join(cfg.HomeDir, defaultTLSKeyFilename) + if preCfg.GUITLSKey == defaultGUITLSKeyFile { + cfg.GUITLSKey = filepath.Join(cfg.HomeDir, defaultGUITLSKeyFilename) } else { - cfg.TLSKey = preCfg.TLSKey + cfg.GUITLSKey = preCfg.GUITLSKey } if preCfg.WalletTLSCert == defaultWalletTLSCertFile { cfg.WalletTLSCert = filepath.Join(cfg.HomeDir, @@ -641,6 +646,9 @@ func loadConfig() (*config, []string, error) { cfg.DcrdRPCHost = normalizeAddress(cfg.DcrdRPCHost, cfg.net.DcrdRPCServerPort) cfg.WalletGRPCHost = normalizeAddress(cfg.WalletGRPCHost, cfg.net.WalletGRPCServerPort) + cfg.MinerListen = normalizeAddress(cfg.MinerListen, defaultMinerPort) + cfg.GUIListen = normalizeAddress(cfg.GUIListen, defaultGUIPort) + if !cfg.SoloPool { // Ensure a valid payment method is set. if cfg.PaymentMethod != pool.PPS && cfg.PaymentMethod != pool.PPLNS { @@ -723,6 +731,13 @@ func loadConfig() (*config, []string, error) { mpLog.Warnf("%v", configFileError) } + if cfg.NoGUITLS && cfg.UseLEHTTPS { + err := fmt.Errorf("only one of uselehttps and noguitls can be specified") + fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, usageMessage) + return nil, nil, err + } + // Ensure a domain is set if HTTPS via letsencrypt is preferred. if cfg.UseLEHTTPS && cfg.Domain == "" { err := fmt.Errorf("a valid domain is required for HTTPS " + @@ -735,8 +750,8 @@ func loadConfig() (*config, []string, error) { // Generate self-signed TLS cert and key if they do not already exist. This // keypair is solely for enabling HTTPS connections to the pool's // web interface. - if !cfg.UseLEHTTPS && (!fileExists(cfg.TLSCert) || !fileExists(cfg.TLSKey)) { - err := genCertPair(cfg.TLSCert, cfg.TLSKey) + if !cfg.UseLEHTTPS && (!fileExists(cfg.GUITLSCert) || !fileExists(cfg.GUITLSKey)) { + err := genCertPair(cfg.GUITLSCert, cfg.GUITLSKey) if err != nil { str := "%s: unable to generate dcrpool's TLS cert/key: %v" err := fmt.Errorf(str, funcName, err) diff --git a/dcrpool.go b/dcrpool.go index ed6a0ac4..cea6abae 100644 --- a/dcrpool.go +++ b/dcrpool.go @@ -59,7 +59,7 @@ func newPool(db pool.Database, cfg *config) (*miningPool, error) { PoolFeeAddrs: cfg.poolFeeAddrs, SoloPool: cfg.SoloPool, NonceIterations: iterations, - MinerPort: cfg.MinerPort, + MinerListen: cfg.MinerListen, MaxConnectionsPerHost: cfg.MaxConnectionsPerHost, WalletAccount: cfg.WalletAccount, CoinbaseConfTimeout: cfg.CoinbaseConfTimeout, @@ -92,17 +92,18 @@ func newPool(db pool.Database, cfg *config) (*miningPool, error) { SoloPool: cfg.SoloPool, GUIDir: cfg.GUIDir, AdminPass: cfg.AdminPass, - GUIPort: cfg.GUIPort, + GUIListen: cfg.GUIListen, UseLEHTTPS: cfg.UseLEHTTPS, + NoGUITLS: cfg.NoGUITLS, Domain: cfg.Domain, - TLSCertFile: cfg.TLSCert, - TLSKeyFile: cfg.TLSKey, + TLSCertFile: cfg.GUITLSCert, + TLSKeyFile: cfg.GUITLSKey, ActiveNet: cfg.net.Params, PaymentMethod: cfg.PaymentMethod, Designation: cfg.Designation, PoolFee: cfg.PoolFee, CSRFSecret: csrfSecret, - MinerPort: cfg.MinerPort, + MinerListen: cfg.MinerListen, WithinLimit: p.hub.WithinLimit, FetchLastWorkHeight: p.hub.FetchLastWorkHeight, FetchLastPaymentInfo: p.hub.FetchLastPaymentInfo, diff --git a/gui/gui.go b/gui/gui.go index 583de7a7..73db663f 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -9,7 +9,6 @@ import ( "context" "crypto/tls" "errors" - "fmt" "html/template" "net/http" "os" @@ -40,14 +39,16 @@ type Config struct { CSRFSecret []byte // AdminPass represents the admin password. AdminPass string - // GUIPort represents the port the frontend is served on. - GUIPort uint32 + // GUIListen represents the listening address the frontend is served on. + GUIListen string // TLSCertFile represents the TLS certificate file path. TLSCertFile string // TLSKeyFile represents the TLS key file path. TLSKeyFile string // UseLEHTTPS represents Letsencrypt HTTPS mode. UseLEHTTPS bool + // NoGUITLS starts the webserver listening for plain HTTP. + NoGUITLS bool // Domain represents the domain name of the pool. Domain string // ActiveNet represents the active network being mined on. @@ -58,8 +59,8 @@ type Config struct { Designation string // PoolFee represents the fee charged to participating accounts of the pool. PoolFee float64 - // MinerPort represents the miner connection port for the pool. - MinerPort uint32 + // MinerListen represents the listening address for miner connections. + MinerListen string // WithinLimit returns if a client is within its request limits. WithinLimit func(string, int) bool // FetchLastWorkHeight returns the last work height of the pool. @@ -247,24 +248,8 @@ func (ui *GUI) loadTemplates() error { // Run starts the user interface. func (ui *GUI) Run(ctx context.Context) { go func() { - if !ui.cfg.UseLEHTTPS { - log.Infof("Starting GUI server on port %d (https)", ui.cfg.GUIPort) - ui.server = &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: fmt.Sprintf("0.0.0.0:%v", ui.cfg.GUIPort), - Handler: ui.router, - } - - if err := ui.server.ListenAndServeTLS(ui.cfg.TLSCertFile, - ui.cfg.TLSKeyFile); err != nil && - !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } - } - - if ui.cfg.UseLEHTTPS { + switch { + case ui.cfg.UseLEHTTPS: certMgr := &autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: autocert.DirCache("certs"), @@ -295,6 +280,35 @@ func (ui *GUI) Run(ctx context.Context) { if err := ui.server.ListenAndServeTLS("", ""); err != nil { log.Error(err) } + case ui.cfg.NoGUITLS: + log.Infof("Starting GUI server on %s (http)", ui.cfg.GUIListen) + ui.server = &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ui.cfg.GUIListen, + Handler: ui.router, + } + + if err := ui.server.ListenAndServe(); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } + default: + log.Infof("Starting GUI server on %s (https)", ui.cfg.GUIListen) + ui.server = &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ui.cfg.GUIListen, + Handler: ui.router, + } + + if err := ui.server.ListenAndServeTLS(ui.cfg.TLSCertFile, + ui.cfg.TLSKeyFile); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } } }() diff --git a/gui/index.go b/gui/index.go index 95c17324..1ce44c9c 100644 --- a/gui/index.go +++ b/gui/index.go @@ -5,6 +5,7 @@ package gui import ( + "net" "net/http" "github.com/gorilla/csrf" @@ -15,7 +16,7 @@ import ( type indexPageData struct { HeaderData headerData PoolStatsData poolStatsData - MinerPort uint32 + MinerPort string MinedWork []*minedWork RewardQuotas []*rewardQuota Address string @@ -43,6 +44,12 @@ func (ui *GUI) renderIndex(w http.ResponseWriter, r *http.Request, modalError st address = ui.cfg.Domain } + _, minerPort, err := net.SplitHostPort(ui.cfg.MinerListen) + if err != nil { + log.Errorf("failed to parse port from miner listening address %q: %v", + ui.cfg.MinerListen, err) + } + data := indexPageData{ HeaderData: headerData{ CSRF: csrf.TemplateField(r), @@ -60,7 +67,7 @@ func (ui *GUI) renderIndex(w http.ResponseWriter, r *http.Request, modalError st }, RewardQuotas: rewardQuotas, MinedWork: confirmedWork, - MinerPort: ui.cfg.MinerPort, + MinerPort: minerPort, ModalError: modalError, Address: address, } diff --git a/pool/endpoint.go b/pool/endpoint.go index 6afef546..61b24ffb 100644 --- a/pool/endpoint.go +++ b/pool/endpoint.go @@ -71,7 +71,7 @@ type connection struct { // Endpoint represents a stratum endpoint. type Endpoint struct { - port uint32 + listenAddr string connCh chan *connection discCh chan struct{} listener net.Listener @@ -82,17 +82,17 @@ type Endpoint struct { } // NewEndpoint creates an new miner endpoint. -func NewEndpoint(eCfg *EndpointConfig, port uint32) (*Endpoint, error) { +func NewEndpoint(eCfg *EndpointConfig, listenAddr string) (*Endpoint, error) { endpoint := &Endpoint{ - port: port, - cfg: eCfg, - clients: make(map[string]*Client), - connCh: make(chan *connection, bufferSize), - discCh: make(chan struct{}, bufferSize), + listenAddr: listenAddr, + cfg: eCfg, + clients: make(map[string]*Client), + connCh: make(chan *connection, bufferSize), + discCh: make(chan struct{}, bufferSize), } - listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", "0.0.0.0", endpoint.port)) + listener, err := net.Listen("tcp", listenAddr) if err != nil { - desc := fmt.Sprintf("unable to create endpoint on port %d", port) + desc := fmt.Sprintf("unable to create endpoint on %s", listenAddr) return nil, errs.PoolError(errs.Listener, desc) } endpoint.listener = listener @@ -110,7 +110,7 @@ func (e *Endpoint) removeClient(c *Client) { // listen accepts incoming client connections on the endpoint. // It must be run as a goroutine. func (e *Endpoint) listen() { - log.Infof("listening on :%d", e.port) + log.Infof("listening on %s", e.listenAddr) for { conn, err := e.listener.Accept() if err != nil { diff --git a/pool/endpoint_test.go b/pool/endpoint_test.go index 7b823ab3..ebf22fde 100644 --- a/pool/endpoint_test.go +++ b/pool/endpoint_test.go @@ -3,7 +3,6 @@ package pool import ( "context" "errors" - "fmt" "math" "math/big" "net" @@ -75,8 +74,7 @@ func testEndpoint(t *testing.T) { MonitorCycle: time.Minute, MaxUpgradeTries: 5, } - port := uint32(3030) - endpoint, err := NewEndpoint(eCfg, port) + endpoint, err := NewEndpoint(eCfg, "0.0.0.0:3030") if err != nil { t.Fatalf("[NewEndpoint] unexpected error: %v", err) } @@ -86,8 +84,7 @@ func testEndpoint(t *testing.T) { go endpoint.run(ctx) time.Sleep(time.Millisecond * 100) - laddr, err := net.ResolveTCPAddr("tcp", - fmt.Sprintf("%s:%d", "127.0.0.1", port+1)) + laddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:3031") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -219,8 +216,7 @@ func testEndpoint(t *testing.T) { } // Ensure the endpoint listener can create connections. - ep, err := net.ResolveTCPAddr("tcp", - fmt.Sprintf("%s:%d", "127.0.0.1", port)) + ep, err := net.ResolveTCPAddr("tcp", "127.0.0.1:3030") if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pool/hub.go b/pool/hub.go index 6c44ca18..870f57fc 100644 --- a/pool/hub.go +++ b/pool/hub.go @@ -152,8 +152,8 @@ type HubConfig struct { AdminPass string // NonceIterations returns the possible header nonce iterations. NonceIterations float64 - // MinerPort represents the miner connection port for the pool. - MinerPort uint32 + // MinerListen represents the listening address for miner connections. + MinerListen string // MaxConnectionsPerHost represents the maximum number of connections // allowed per host. MaxConnectionsPerHost uint32 @@ -329,7 +329,7 @@ func NewHub(cancel context.CancelFunc, hcfg *HubConfig) (*Hub, error) { MaxUpgradeTries: h.cfg.MaxUpgradeTries, } - h.endpoint, err = NewEndpoint(eCfg, h.cfg.MinerPort) + h.endpoint, err = NewEndpoint(eCfg, h.cfg.MinerListen) if err != nil { return nil, err } diff --git a/pool/hub_test.go b/pool/hub_test.go index 0478122a..d607dd4c 100644 --- a/pool/hub_test.go +++ b/pool/hub_test.go @@ -234,7 +234,7 @@ func testHub(t *testing.T) { PoolFeeAddrs: []dcrutil.Address{poolFeeAddrs}, MaxConnectionsPerHost: 10, NonceIterations: iterations, - MinerPort: 5050, + MinerListen: "127.0.0.1:5050", WalletAccount: 69, MonitorCycle: time.Minute, MaxUpgradeTries: 5,