From 061342687d745d839021ff71be7df1eb3e43cba2 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Mon, 25 May 2020 20:15:11 +0000 Subject: [PATCH 1/4] rpcclient: Add cookie auth Based on Hugo Landau's cookie auth implementation for Namecoin's ncdns. Fixes https://github.com/btcsuite/btcd/issues/1054 --- rpcclient/cookiefile.go | 64 +++++++++++++++++++++++++++++++++++++ rpcclient/infrastructure.go | 41 ++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 rpcclient/cookiefile.go diff --git a/rpcclient/cookiefile.go b/rpcclient/cookiefile.go new file mode 100644 index 0000000000..9311fbbfcf --- /dev/null +++ b/rpcclient/cookiefile.go @@ -0,0 +1,64 @@ +// Copyright (c) 2017 The Namecoin developers +// Copyright (c) 2019 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package rpcclient + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "time" +) + +func readCookieFile(path string) (username, password string, err error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return + } + + s := strings.TrimSpace(string(b)) + parts := strings.SplitN(s, ":", 2) + if len(parts) != 2 { + err = fmt.Errorf("malformed cookie file") + return + } + + username, password = parts[0], parts[1] + return +} + +func cookieRetriever(path string) func() (username, password string, err error) { + lastCheckTime := time.Time{} + lastModTime := time.Time{} + + curUsername, curPassword := "", "" + var curError error + + doUpdate := func() { + if !lastCheckTime.IsZero() && time.Now().Before(lastCheckTime.Add(30*time.Second)) { + return + } + + lastCheckTime = time.Now() + + st, err := os.Stat(path) + if err != nil { + curError = err + return + } + + modTime := st.ModTime() + if !modTime.Equal(lastModTime) { + lastModTime = modTime + curUsername, curPassword, curError = readCookieFile(path) + } + } + + return func() (username, password string, err error) { + doUpdate() + return curUsername, curPassword, curError + } +} diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index a2079886d3..fa9e00e232 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -851,7 +851,12 @@ func (c *Client) sendPost(jReq *jsonRequest) { httpReq.Header.Set("Content-Type", "application/json") // Configure basic access authorization. - httpReq.SetBasicAuth(c.config.User, c.config.Pass) + user, pass, err := c.config.getAuth() + if err != nil { + jReq.responseChan <- &response{result: nil, err: err} + return + } + httpReq.SetBasicAuth(user, pass) log.Tracef("Sending command [%s] with id %d", jReq.method, jReq.id) c.sendPostRequest(httpReq, jReq) @@ -1096,6 +1101,15 @@ type ConnConfig struct { // Pass is the passphrase to use to authenticate to the RPC server. Pass string + // CookiePath is the path to a cookie file containing the username and + // passphrase to use to authenticate to the RPC server. It is used + // instead of User and Pass if non-empty. + CookiePath string + + // retrieveCookie is a function that returns the cookie username and + // passphrase. + retrieveCookie func() (username, passphrase string, err error) + // Params is the string representing the network that the server // is running. If there is no parameter set in the config, then // mainnet will be used by default. @@ -1149,6 +1163,25 @@ type ConnConfig struct { EnableBCInfoHacks bool } +// getAuth returns the username and passphrase that will actually be used for +// this connection. This will be the result of checking the cookie if a cookie +// path is configured; if not, it will be the user-configured username and +// passphrase. +func (config *ConnConfig) getAuth() (username, passphrase string, err error) { + // If cookie auth isn't in use, just use the supplied + // username/passphrase. + if config.CookiePath == "" { + return config.User, config.Pass, nil + } + + // Initialize the cookie retriever on first run. + if config.retrieveCookie == nil { + config.retrieveCookie = cookieRetriever(config.CookiePath) + } + + return config.retrieveCookie() +} + // newHTTPClient returns a new http client that is configured according to the // proxy and TLS settings in the associated connection configuration. func newHTTPClient(config *ConnConfig) (*http.Client, error) { @@ -1218,7 +1251,11 @@ func dial(config *ConnConfig) (*websocket.Conn, error) { // The RPC server requires basic authorization, so create a custom // request header with the Authorization header set. - login := config.User + ":" + config.Pass + user, pass, err := config.getAuth() + if err != nil { + return nil, err + } + login := user + ":" + pass auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) requestHeader := make(http.Header) requestHeader.Add("Authorization", auth) From 61949536974182843ac0d00ebbc27cd3b7a8f026 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Wed, 9 Oct 2019 11:23:05 +0000 Subject: [PATCH 2/4] rpcclient: Refactor cookie caching --- rpcclient/cookiefile.go | 35 ----------------------------------- rpcclient/infrastructure.go | 35 ++++++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/rpcclient/cookiefile.go b/rpcclient/cookiefile.go index 9311fbbfcf..b29e29b759 100644 --- a/rpcclient/cookiefile.go +++ b/rpcclient/cookiefile.go @@ -8,9 +8,7 @@ package rpcclient import ( "fmt" "io/ioutil" - "os" "strings" - "time" ) func readCookieFile(path string) (username, password string, err error) { @@ -29,36 +27,3 @@ func readCookieFile(path string) (username, password string, err error) { username, password = parts[0], parts[1] return } - -func cookieRetriever(path string) func() (username, password string, err error) { - lastCheckTime := time.Time{} - lastModTime := time.Time{} - - curUsername, curPassword := "", "" - var curError error - - doUpdate := func() { - if !lastCheckTime.IsZero() && time.Now().Before(lastCheckTime.Add(30*time.Second)) { - return - } - - lastCheckTime = time.Now() - - st, err := os.Stat(path) - if err != nil { - curError = err - return - } - - modTime := st.ModTime() - if !modTime.Equal(lastModTime) { - lastModTime = modTime - curUsername, curPassword, curError = readCookieFile(path) - } - } - - return func() (username, password string, err error) { - doUpdate() - return curUsername, curPassword, curError - } -} diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index fa9e00e232..42aeb49fbd 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -19,6 +19,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "sync" "sync/atomic" @@ -1106,9 +1107,11 @@ type ConnConfig struct { // instead of User and Pass if non-empty. CookiePath string - // retrieveCookie is a function that returns the cookie username and - // passphrase. - retrieveCookie func() (username, passphrase string, err error) + cookieLastCheckTime time.Time + cookieLastModTime time.Time + cookieLastUser string + cookieLastPass string + cookieLastErr error // Params is the string representing the network that the server // is running. If there is no parameter set in the config, then @@ -1174,12 +1177,30 @@ func (config *ConnConfig) getAuth() (username, passphrase string, err error) { return config.User, config.Pass, nil } - // Initialize the cookie retriever on first run. - if config.retrieveCookie == nil { - config.retrieveCookie = cookieRetriever(config.CookiePath) + return config.retrieveCookie() +} + +// retrieveCookie returns the cookie username and passphrase. +func (config *ConnConfig) retrieveCookie() (username, passphrase string, err error) { + if !config.cookieLastCheckTime.IsZero() && time.Now().Before(config.cookieLastCheckTime.Add(30*time.Second)) { + return config.cookieLastUser, config.cookieLastPass, config.cookieLastErr } - return config.retrieveCookie() + config.cookieLastCheckTime = time.Now() + + st, err := os.Stat(config.CookiePath) + if err != nil { + config.cookieLastErr = err + return config.cookieLastUser, config.cookieLastPass, config.cookieLastErr + } + + modTime := st.ModTime() + if !modTime.Equal(config.cookieLastModTime) { + config.cookieLastModTime = modTime + config.cookieLastUser, config.cookieLastPass, config.cookieLastErr = readCookieFile(config.CookiePath) + } + + return config.cookieLastUser, config.cookieLastPass, config.cookieLastErr } // newHTTPClient returns a new http client that is configured according to the From deb09c7435bb55697c0de74e960f9093f4ed09cf Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Wed, 9 Oct 2019 11:08:42 +0000 Subject: [PATCH 3/4] rpcclient: Try user+pass auth before cookie auth --- rpcclient/infrastructure.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index 42aeb49fbd..8609e7c5ad 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -1171,12 +1171,12 @@ type ConnConfig struct { // path is configured; if not, it will be the user-configured username and // passphrase. func (config *ConnConfig) getAuth() (username, passphrase string, err error) { - // If cookie auth isn't in use, just use the supplied - // username/passphrase. - if config.CookiePath == "" { + // Try username+passphrase auth first. + if config.Pass != "" { return config.User, config.Pass, nil } + // If no username or passphrase is set, try cookie auth. return config.retrieveCookie() } From 831de8efd925852756489a9c4755e1b0d9359236 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Thu, 10 Oct 2019 22:59:50 +0000 Subject: [PATCH 4/4] rpcclient: Read first line of cookie instead of trimming space --- rpcclient/cookiefile.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/rpcclient/cookiefile.go b/rpcclient/cookiefile.go index b29e29b759..c3f7068b30 100644 --- a/rpcclient/cookiefile.go +++ b/rpcclient/cookiefile.go @@ -6,18 +6,27 @@ package rpcclient import ( + "bufio" "fmt" - "io/ioutil" + "os" "strings" ) func readCookieFile(path string) (username, password string, err error) { - b, err := ioutil.ReadFile(path) + f, err := os.Open(path) if err != nil { return } + defer f.Close() + + scanner := bufio.NewScanner(f) + scanner.Scan() + err = scanner.Err() + if err != nil { + return + } + s := scanner.Text() - s := strings.TrimSpace(string(b)) parts := strings.SplitN(s, ":", 2) if len(parts) != 2 { err = fmt.Errorf("malformed cookie file")