From 5d01f71f6396515fb8c30f2a309cd07d988a458e Mon Sep 17 00:00:00 2001 From: "Rogit config --global credential.helper store" <104002271+robertmin1@users.noreply.github.com> Date: Wed, 3 Apr 2024 04:11:40 +0300 Subject: [PATCH] Sending RPC requests through unix sockets Sending RPC requests through unix sockets --- go.mod | 1 + go.sum | 4 + .../examples/bitcoincoreunixsocket/README.md | 41 +++++++ .../examples/bitcoincoreunixsocket/main.go | 37 ++++++ rpcclient/infrastructure.go | 107 +++++++++++++++++- 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 rpcclient/examples/bitcoincoreunixsocket/README.md create mode 100644 rpcclient/examples/bitcoincoreunixsocket/main.go diff --git a/go.mod b/go.mod index 6eea83508e..e678d13bcb 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.0 // indirect + golang.org/x/net v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ddd7e1ebd0..777cf82469 100644 --- a/go.sum +++ b/go.sum @@ -91,12 +91,15 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -108,6 +111,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= diff --git a/rpcclient/examples/bitcoincoreunixsocket/README.md b/rpcclient/examples/bitcoincoreunixsocket/README.md new file mode 100644 index 0000000000..efe5bbb2ad --- /dev/null +++ b/rpcclient/examples/bitcoincoreunixsocket/README.md @@ -0,0 +1,41 @@ +Bitcoin Core HTTP POST Over Unix Socket Example +============================== + +This example shows how to use the rpcclient package to connect to a Bitcoin +Core RPC server using HTTP POST mode over a Unix Socket with TLS disabled +and gets the current block count. + +## Running the Example + +The first step is to use `go get` to download and install the rpcclient package: + +```bash +$ go get github.com/btcsuite/btcd/rpcclient +``` + +Next, modify the `main.go` source to specify the correct RPC username and +password for the RPC server: + +```Go + User: "yourrpcuser", + Pass: "yourrpcpass", +``` + +As Bitcoin Core supports only TCP/IP, we'll redirect RPC requests from the +Unix Socket to Bitcoin Core. For this example, we'll use the `socat` command: + +``` + socat -d UNIX-LISTEN:"my-unix-socket-path",fork TCP:"host-address" + socat -d UNIX-LISTEN:/tmp/test.XXXX,fork TCP:localhost:8332 +``` + +Finally, navigate to the example's directory and run it with: + +```bash +$ cd $GOPATH/src/github.com/btcsuite/btcd/rpcclient/examples/bitcoincorehttp +$ go run *.go +``` + +## License + +This example is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/rpcclient/examples/bitcoincoreunixsocket/main.go b/rpcclient/examples/bitcoincoreunixsocket/main.go new file mode 100644 index 0000000000..a859a9e12a --- /dev/null +++ b/rpcclient/examples/bitcoincoreunixsocket/main.go @@ -0,0 +1,37 @@ +// Copyright (c) 2014-2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "log" + + "github.com/btcsuite/btcd/rpcclient" +) + +func main() { + // Connect to local bitcoin core RPC server using HTTP POST mode over a Unix Socket. + connCfg := &rpcclient.ConnConfig{ + // For unix sockets, unix:// + "your unix socket path" + Host: "unix:///tmp/test.XXXX", + User: "yourrpcuser", + Pass: "yourrpcpass", + HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode + DisableTLS: true, // Bitcoin core does not provide TLS by default + } + // Notice the notification parameter is nil since notifications are + // not supported in HTTP POST mode. + client, err := rpcclient.New(connCfg, nil) + if err != nil { + log.Fatal(err) + } + defer client.Shutdown() + + // Get the current block count. + blockCount, err := client.GetBlockCount() + if err != nil { + log.Fatal(err) + } + log.Printf("Block count: %d", blockCount) +} diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index 67f908efaa..a75da71b68 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -7,6 +7,7 @@ package rpcclient import ( "bytes" "container/list" + "context" "crypto/tls" "crypto/x509" "encoding/base64" @@ -20,6 +21,8 @@ import ( "net/http" "net/url" "os" + "strconv" + "strings" "sync" "sync/atomic" "time" @@ -756,7 +759,6 @@ func (c *Client) handleSendPostMessage(jReq *jsonRequest) { if !c.config.DisableTLS { protocol = "https" } - url := protocol + "://" + c.config.Host var ( err, lastErr error @@ -764,6 +766,24 @@ func (c *Client) handleSendPostMessage(jReq *jsonRequest) { httpResponse *http.Response ) + parsedAddr, err := ParseAddressString(c.config.Host, "") + if err != nil { + jReq.responseChan <- &Response{ + err: fmt.Errorf("failed to parse address %v", err), + } + return + } + + var url string + switch parsedAddr.Network(){ + case "unix", "unixpacket": + // Using a placeholder URL because a non-empty URL is required + // the Unix domain socket is specified in the DialContext. + url = protocol + "://" + "unix" + default: + url = protocol + "://" + c.config.Host + } + tries := 10 for i := 0; i < tries; i++ { var httpReq *http.Request @@ -1331,10 +1351,17 @@ func newHTTPClient(config *ConnConfig) (*http.Client, error) { } } + parsedAddr, err := ParseAddressString(config.Host, "") + if err != nil { + return nil, err + } client := http.Client{ Transport: &http.Transport{ Proxy: proxyFunc, TLSClientConfig: tlsConfig, + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial(parsedAddr.Network(), parsedAddr.String()) + }, }, } @@ -1698,3 +1725,81 @@ func (c *Client) Send() error { return nil } + +// ParseAddressString converts an address in string format to a net.Addr that is +// compatible with btcd. UDP is not supported because btcd needs reliable +// connections. We accept a custom function to resolve any TCP addresses so +// that caller is able control exactly how resolution is performed. +func ParseAddressString(strAddress string, defaultPort string) (net.Addr, error) { + var parsedNetwork, parsedAddr string + + // Addresses can either be in network://address:port format, + // network:address:port, address:port, or just port. We want to support + // all possible types. + if strings.Contains(strAddress, "://") { + parts := strings.Split(strAddress, "://") + parsedNetwork, parsedAddr = parts[0], parts[1] + } else if strings.Contains(strAddress, ":") { + parts := strings.Split(strAddress, ":") + parsedNetwork = parts[0] + parsedAddr = strings.Join(parts[1:], ":") + } else { + parsedAddr = strAddress + } + + // Only TCP and Unix socket addresses are valid. We can't use IP or + // UDP only connections for anything we do in lnd. + switch parsedNetwork { + case "unix", "unixpacket": + return net.ResolveUnixAddr(parsedNetwork, parsedAddr) + + case "tcp", "tcp4", "tcp6": + return net.ResolveTCPAddr(parsedNetwork, verifyPort(parsedAddr, defaultPort)) + + case "ip", "ip4", "ip6", "udp", "udp4", "udp6", "unixgram": + return nil, fmt.Errorf("only TCP or unix socket "+ + "addresses are supported: %s", parsedAddr) + + default: + // We'll now possibly apply the default port, use the local + // host short circuit, or parse out an all interfaces listen. + addrWithPort := verifyPort(strAddress, defaultPort) + + // Otherwise, we'll attempt to resolve the host. + return net.ResolveTCPAddr("tcp", addrWithPort) + } +} + +// verifyPort makes sure that an address string has both a host and a port. If +// there is no port found, the default port is appended. If the address is just +// a port, then we'll assume that the user is using the short cut to specify a +// localhost:port address. +func verifyPort(address string, defaultPort string) string { + host, port, err := net.SplitHostPort(address) + if err != nil { + // If the address itself is just an integer, then we'll assume + // that we're mapping this directly to a localhost:port pair. + // This ensures we maintain the legacy behavior. + if _, err := strconv.Atoi(address); err == nil { + return net.JoinHostPort("localhost", address) + } + + // Otherwise, we'll assume that the address just failed to + // attach its own port, so we'll use the default port. In the + // case of IPv6 addresses, if the host is already surrounded by + // brackets, then we'll avoid using the JoinHostPort function, + // since it will always add a pair of brackets. + if strings.HasPrefix(address, "[") { + return address + ":" + defaultPort + } + return net.JoinHostPort(address, defaultPort) + } + + // In the case that both the host and port are empty, we'll use the + // default port. + if host == "" && port == "" { + return ":" + defaultPort + } + + return address +}