diff --git a/config.go b/config.go index a54c83fc..f0cfcd33 100644 --- a/config.go +++ b/config.go @@ -33,6 +33,7 @@ const ( defaultBtcnet = btcwire.MainNet defaultMaxPeers = 125 defaultBanDuration = time.Hour * 24 + defaultMaxRPCClients = 10 defaultVerifyEnabled = false defaultDbType = "leveldb" ) @@ -71,6 +72,7 @@ type config struct { RPCListeners []string `long:"rpclisten" description:"Add an interface/port to listen for RPC connections (default port: 8334, testnet: 18334)"` RPCCert string `long:"rpccert" description:"File containing the certificate file"` RPCKey string `long:"rpckey" description:"File containing the certificate key"` + RPCMaxClients int `long:"rpcmaxclients" description:"Max number of RPC clients for standard connections"` DisableRPC bool `long:"norpc" description:"Disable built-in RPC server -- NOTE: The RPC server is disabled by default if no rpcuser/rpcpass is specified"` DisableDNSSeed bool `long:"nodnsseed" description:"Disable DNS seeding for peers"` ExternalIPs []string `long:"externalip" description:"Add an ip to the list of local addresses we claim to listen on to peers"` @@ -278,15 +280,16 @@ func newConfigParser(cfg *config, so *serviceOptions, options flags.Options) *fl func loadConfig() (*config, []string, error) { // Default config. cfg := config{ - DebugLevel: defaultLogLevel, - MaxPeers: defaultMaxPeers, - BanDuration: defaultBanDuration, - ConfigFile: defaultConfigFile, - DataDir: defaultDataDir, - LogDir: defaultLogDir, - DbType: defaultDbType, - RPCKey: defaultRPCKeyFile, - RPCCert: defaultRPCCertFile, + ConfigFile: defaultConfigFile, + DebugLevel: defaultLogLevel, + MaxPeers: defaultMaxPeers, + BanDuration: defaultBanDuration, + RPCMaxClients: defaultMaxRPCClients, + DataDir: defaultDataDir, + LogDir: defaultLogDir, + DbType: defaultDbType, + RPCKey: defaultRPCKeyFile, + RPCCert: defaultRPCCertFile, } // Service options which are only added on Windows. diff --git a/doc.go b/doc.go index 8a547bf3..fa7deac3 100644 --- a/doc.go +++ b/doc.go @@ -41,6 +41,7 @@ Application Options: (default port: 8334, testnet: 18334) --rpccert= File containing the certificate file --rpckey= File containing the certificate key + --rpcmaxclients= Max number of RPC clients for standard connections (10) --norpc Disable built-in RPC server -- NOTE: The RPC server is disabled by default if no rpcuser/rpcpass is specified --nodnsseed Disable DNS seeding for peers diff --git a/rpcserver.go b/rpcserver.go index f840ed7b..7e2a3b98 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -140,14 +140,16 @@ var rpcUnimplemented = map[string]bool{ // rpcServer holds the items the rpc server may need to access (config, // shutdown, main server, etc.) type rpcServer struct { - started int32 - shutdown int32 - server *server - authsha [sha256.Size]byte - ws *wsContext - wg sync.WaitGroup - listeners []net.Listener - quit chan int + started int32 + shutdown int32 + server *server + authsha [sha256.Size]byte + ws *wsContext + numClients int + numClientsMutex sync.Mutex + wg sync.WaitGroup + listeners []net.Listener + quit chan int } // Start is used by server.go to start the rpc listener. @@ -167,11 +169,22 @@ func (s *rpcServer) Start() { } rpcServeMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") + r.Close = true + + // Limit the number of connections to max allowed. + if s.limitConnections(w, r.RemoteAddr) { + return + } + + // Keep track of the number of connected clients. + s.incrementClients() + defer s.decrementClients() if _, err := s.checkAuth(r, true); err != nil { jsonAuthFail(w, r, s) return } jsonRPCRead(w, r, s) + }) rpcServeMux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { @@ -199,6 +212,49 @@ func (s *rpcServer) Start() { } } +// limitConnections responds with a 503 service unavailable and returns true if +// adding another client would exceed the maximum allow RPC clients. +// +// This function is safe for concurrent access. +func (s *rpcServer) limitConnections(w http.ResponseWriter, remoteAddr string) bool { + s.numClientsMutex.Lock() + defer s.numClientsMutex.Unlock() + + if s.numClients+1 > cfg.RPCMaxClients { + rpcsLog.Infof("Max RPC clients exceeded [%d] - "+ + "disconnecting client %s", cfg.RPCMaxClients, + remoteAddr) + http.Error(w, "503 Too busy. Try again later.", + http.StatusServiceUnavailable) + return true + } + return false +} + +// incrementClients adds one to the number of connected RPC clients. Note +// this only applies to standard clients. Websocket clients have their own +// limits and are tracked separately. +// +// This function is safe for concurrent access. +func (s *rpcServer) incrementClients() { + s.numClientsMutex.Lock() + defer s.numClientsMutex.Unlock() + + s.numClients++ +} + +// decrementClients subtracts one from the number of connected RPC clients. +// Note this only applies to standard clients. Websocket clients have their own +// limits and are tracked separately. +// +// This function is safe for concurrent access. +func (s *rpcServer) decrementClients() { + s.numClientsMutex.Lock() + defer s.numClientsMutex.Unlock() + + s.numClients-- +} + // checkAuth checks the HTTP Basic authentication supplied by a wallet // or RPC client in the HTTP request r. If the supplied authentication // does not match the username and password expected, a non-nil error is @@ -344,7 +400,6 @@ func jsonAuthFail(w http.ResponseWriter, r *http.Request, s *rpcServer) { // jsonRPCRead is the RPC wrapper around the jsonRead function to handle reading // and responding to RPC messages. func jsonRPCRead(w http.ResponseWriter, r *http.Request, s *rpcServer) { - r.Close = true if atomic.LoadInt32(&s.shutdown) != 0 { return } diff --git a/sample-btcd.conf b/sample-btcd.conf index 73ba3c31..8a8314fd 100644 --- a/sample-btcd.conf +++ b/sample-btcd.conf @@ -143,6 +143,9 @@ ; rpclisten=0.0.0.0:8337 ; all ipv4 interfaces on non-standard port 8337 ; rpclisten=[::]:8337 ; all ipv6 interfaces on non-standard port 8337 +; Specify the maximum number of concurrent RPC clients for standard connections. +; rpcmaxclients=10 + ; Use the following setting to disable the RPC server even if the rpcuser and ; rpcpass are specified above. This allows one to quickly disable the RPC ; server without having to remove credentials from the config file.