From 280845a8a4aebd432030201e50c74b0a29e01a94 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Mon, 25 May 2020 20:15:11 +0000 Subject: [PATCH] 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 00000000..9311fbbf --- /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 a2079886..fa9e00e2 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)