|
|
@ -7,6 +7,7 @@ import ( |
|
|
|
"io/ioutil" |
|
|
|
"net" |
|
|
|
"net/http" |
|
|
|
"net/url" |
|
|
|
"strings" |
|
|
|
"sync" |
|
|
|
|
|
|
@ -26,6 +27,57 @@ const ( |
|
|
|
|
|
|
|
//-------------------------------------------------------------
|
|
|
|
|
|
|
|
// Parsed URL structure
|
|
|
|
type parsedURL struct { |
|
|
|
url.URL |
|
|
|
} |
|
|
|
|
|
|
|
// Parse URL and set defaults
|
|
|
|
func newParsedURL(remoteAddr string) (*parsedURL, error) { |
|
|
|
u, err := url.Parse(remoteAddr) |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
|
|
|
|
// default to tcp if nothing specified
|
|
|
|
if u.Scheme == "" { |
|
|
|
u.Scheme = protoTCP |
|
|
|
} |
|
|
|
|
|
|
|
return &parsedURL{*u}, nil |
|
|
|
} |
|
|
|
|
|
|
|
// Change protocol to HTTP for unknown protocols and TCP protocol - useful for RPC connections
|
|
|
|
func (u *parsedURL) SetDefaultSchemeHTTP() { |
|
|
|
// protocol to use for http operations, to support both http and https
|
|
|
|
switch u.Scheme { |
|
|
|
case protoHTTP, protoHTTPS, protoWS, protoWSS: |
|
|
|
// known protocols not changed
|
|
|
|
default: |
|
|
|
// default to http for unknown protocols (ex. tcp)
|
|
|
|
u.Scheme = protoHTTP |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Get full address without the protocol - useful for Dialer connections
|
|
|
|
func (u parsedURL) GetHostWithPath() string { |
|
|
|
// Remove protocol, userinfo and # fragment, assume opaque is empty
|
|
|
|
return u.Host + u.EscapedPath() |
|
|
|
} |
|
|
|
|
|
|
|
// Get a trimmed address - useful for WS connections
|
|
|
|
func (u parsedURL) GetTrimmedHostWithPath() string { |
|
|
|
// replace / with . for http requests (kvstore domain)
|
|
|
|
return strings.Replace(u.GetHostWithPath(), "/", ".", -1) |
|
|
|
} |
|
|
|
|
|
|
|
// Get a trimmed address with protocol - useful as address in RPC connections
|
|
|
|
func (u parsedURL) GetTrimmedURL() string { |
|
|
|
return u.Scheme + "://" + u.GetTrimmedHostWithPath() |
|
|
|
} |
|
|
|
|
|
|
|
//-------------------------------------------------------------
|
|
|
|
|
|
|
|
// HTTPClient is a common interface for JSON-RPC HTTP clients.
|
|
|
|
type HTTPClient interface { |
|
|
|
// Call calls the given method with the params and returns a result.
|
|
|
@ -51,9 +103,12 @@ type JSONRPCCaller interface { |
|
|
|
//
|
|
|
|
// JSONRPCClient is safe for concurrent use by multiple goroutines.
|
|
|
|
type JSONRPCClient struct { |
|
|
|
address string |
|
|
|
client *http.Client |
|
|
|
cdc *amino.Codec |
|
|
|
address string |
|
|
|
username string |
|
|
|
password string |
|
|
|
|
|
|
|
client *http.Client |
|
|
|
cdc *amino.Codec |
|
|
|
|
|
|
|
mtx sync.Mutex |
|
|
|
nextReqID int |
|
|
@ -84,15 +139,23 @@ func NewJSONRPCClientWithHTTPClient(remote string, client *http.Client) (*JSONRP |
|
|
|
panic("nil http.Client provided") |
|
|
|
} |
|
|
|
|
|
|
|
clientAddress, err := toClientAddress(remote) |
|
|
|
parsedURL, err := newParsedURL(remote) |
|
|
|
if err != nil { |
|
|
|
return nil, fmt.Errorf("invalid remote %s: %s", remote, err) |
|
|
|
} |
|
|
|
|
|
|
|
parsedURL.SetDefaultSchemeHTTP() |
|
|
|
|
|
|
|
address := parsedURL.GetTrimmedURL() |
|
|
|
username := parsedURL.User.Username() |
|
|
|
password, _ := parsedURL.User.Password() |
|
|
|
|
|
|
|
rpcClient := &JSONRPCClient{ |
|
|
|
address: clientAddress, |
|
|
|
client: client, |
|
|
|
cdc: amino.NewCodec(), |
|
|
|
address: address, |
|
|
|
username: username, |
|
|
|
password: password, |
|
|
|
client: client, |
|
|
|
cdc: amino.NewCodec(), |
|
|
|
} |
|
|
|
|
|
|
|
return rpcClient, nil |
|
|
@ -114,7 +177,15 @@ func (c *JSONRPCClient) Call(method string, params map[string]interface{}, resul |
|
|
|
} |
|
|
|
|
|
|
|
requestBuf := bytes.NewBuffer(requestBytes) |
|
|
|
httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) |
|
|
|
httpRequest, err := http.NewRequest(http.MethodPost, c.address, requestBuf) |
|
|
|
if err != nil { |
|
|
|
return nil, errors.Wrap(err, "Request failed") |
|
|
|
} |
|
|
|
httpRequest.Header.Set("Content-Type", "text/json") |
|
|
|
if c.username != "" || c.password != "" { |
|
|
|
httpRequest.SetBasicAuth(c.username, c.password) |
|
|
|
} |
|
|
|
httpResponse, err := c.client.Do(httpRequest) |
|
|
|
if err != nil { |
|
|
|
return nil, errors.Wrap(err, "Post failed") |
|
|
|
} |
|
|
@ -153,7 +224,15 @@ func (c *JSONRPCClient) sendBatch(requests []*jsonRPCBufferedRequest) ([]interfa |
|
|
|
return nil, errors.Wrap(err, "failed to marshal requests") |
|
|
|
} |
|
|
|
|
|
|
|
httpResponse, err := c.client.Post(c.address, "text/json", bytes.NewBuffer(requestBytes)) |
|
|
|
httpRequest, err := http.NewRequest(http.MethodPost, c.address, bytes.NewBuffer(requestBytes)) |
|
|
|
if err != nil { |
|
|
|
return nil, errors.Wrap(err, "Request failed") |
|
|
|
} |
|
|
|
httpRequest.Header.Set("Content-Type", "text/json") |
|
|
|
if c.username != "" || c.password != "" { |
|
|
|
httpRequest.SetBasicAuth(c.username, c.password) |
|
|
|
} |
|
|
|
httpResponse, err := c.client.Do(httpRequest) |
|
|
|
if err != nil { |
|
|
|
return nil, errors.Wrap(err, "Post failed") |
|
|
|
} |
|
|
@ -256,62 +335,14 @@ func (b *JSONRPCRequestBatch) Call( |
|
|
|
|
|
|
|
//-------------------------------------------------------------
|
|
|
|
|
|
|
|
// protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp")
|
|
|
|
// trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "."
|
|
|
|
func toClientAddrAndParse(remoteAddr string) (network string, trimmedS string, err error) { |
|
|
|
protocol, address, err := parseRemoteAddr(remoteAddr) |
|
|
|
if err != nil { |
|
|
|
return "", "", err |
|
|
|
} |
|
|
|
|
|
|
|
// protocol to use for http operations, to support both http and https
|
|
|
|
var clientProtocol string |
|
|
|
// default to http for unknown protocols (ex. tcp)
|
|
|
|
switch protocol { |
|
|
|
case protoHTTP, protoHTTPS, protoWS, protoWSS: |
|
|
|
clientProtocol = protocol |
|
|
|
default: |
|
|
|
clientProtocol = protoHTTP |
|
|
|
} |
|
|
|
|
|
|
|
// replace / with . for http requests (kvstore domain)
|
|
|
|
trimmedAddress := strings.Replace(address, "/", ".", -1) |
|
|
|
return clientProtocol, trimmedAddress, nil |
|
|
|
} |
|
|
|
|
|
|
|
func toClientAddress(remoteAddr string) (string, error) { |
|
|
|
clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr) |
|
|
|
if err != nil { |
|
|
|
return "", err |
|
|
|
} |
|
|
|
return clientProtocol + "://" + trimmedAddress, nil |
|
|
|
} |
|
|
|
|
|
|
|
// network - name of the network (for example, "tcp", "unix")
|
|
|
|
// s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80")
|
|
|
|
// TODO: Deprecate support for IP:PORT or /path/to/socket
|
|
|
|
func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { |
|
|
|
parts := strings.SplitN(remoteAddr, "://", 2) |
|
|
|
var protocol, address string |
|
|
|
switch len(parts) { |
|
|
|
case 1: |
|
|
|
// default to tcp if nothing specified
|
|
|
|
protocol, address = protoTCP, remoteAddr |
|
|
|
case 2: |
|
|
|
protocol, address = parts[0], parts[1] |
|
|
|
default: |
|
|
|
return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) |
|
|
|
} |
|
|
|
|
|
|
|
return protocol, address, nil |
|
|
|
} |
|
|
|
|
|
|
|
func makeHTTPDialer(remoteAddr string) (func(string, string) (net.Conn, error), error) { |
|
|
|
protocol, address, err := parseRemoteAddr(remoteAddr) |
|
|
|
u, err := newParsedURL(remoteAddr) |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
|
|
|
|
protocol := u.Scheme |
|
|
|
|
|
|
|
// accept http(s) as an alias for tcp
|
|
|
|
switch protocol { |
|
|
|
case protoHTTP, protoHTTPS: |
|
|
@ -319,7 +350,7 @@ func makeHTTPDialer(remoteAddr string) (func(string, string) (net.Conn, error), |
|
|
|
} |
|
|
|
|
|
|
|
dialFn := func(proto, addr string) (net.Conn, error) { |
|
|
|
return net.Dial(protocol, address) |
|
|
|
return net.Dial(protocol, u.GetHostWithPath()) |
|
|
|
} |
|
|
|
|
|
|
|
return dialFn, nil |
|
|
|