|
// Package jsonrpc provides an jsonrpc 2.0 client that sends jsonrpc requests and receives jsonrpc responses using http.
|
|
package jsonrpc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"sync"
|
|
)
|
|
|
|
// RPCRequest represents a jsonrpc request object.
|
|
//
|
|
// See: http://www.jsonrpc.org/specification#request_object
|
|
type RPCRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params interface{} `json:"params,omitempty"`
|
|
ID uint `json:"id"`
|
|
}
|
|
|
|
// RPCNotification represents a jsonrpc notification object.
|
|
// A notification object omits the id field since there will be no server response.
|
|
//
|
|
// See: http://www.jsonrpc.org/specification#notification
|
|
type RPCNotification struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params interface{} `json:"params,omitempty"`
|
|
}
|
|
|
|
// RPCResponse represents a jsonrpc response object.
|
|
// If no rpc specific error occurred Error field is nil.
|
|
//
|
|
// See: http://www.jsonrpc.org/specification#response_object
|
|
type RPCResponse struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Result interface{} `json:"result,omitempty"`
|
|
Error *RPCError `json:"error,omitempty"`
|
|
ID uint `json:"id"`
|
|
}
|
|
|
|
// BatchResponse a list of jsonrpc response objects as a result of a batch request
|
|
//
|
|
// if you are interested in the response of a specific request use: GetResponseOf(request)
|
|
type BatchResponse struct {
|
|
rpcResponses []RPCResponse
|
|
}
|
|
|
|
// RPCError represents a jsonrpc error object if an rpc error occurred.
|
|
//
|
|
// See: http://www.jsonrpc.org/specification#error_object
|
|
type RPCError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
|
|
func (e *RPCError) Error() string {
|
|
return strconv.Itoa(e.Code) + ": " + e.Message
|
|
}
|
|
|
|
// RPCClient sends jsonrpc requests over http to the provided rpc backend.
|
|
// RPCClient is created using the factory function NewRPCClient().
|
|
type RPCClient struct {
|
|
endpoint string
|
|
httpClient *http.Client
|
|
customHeaders map[string]string
|
|
autoIncrementID bool
|
|
nextID uint
|
|
idMutex sync.Mutex
|
|
}
|
|
|
|
// NewRPCClient returns a new RPCClient instance with default configuration (no custom headers, default http.Client, autoincrement ids).
|
|
// Endpoint is the rpc-service url to which the rpc requests are sent.
|
|
func NewRPCClient(endpoint string) *RPCClient {
|
|
return &RPCClient{
|
|
endpoint: endpoint,
|
|
httpClient: http.DefaultClient,
|
|
autoIncrementID: true,
|
|
nextID: 0,
|
|
customHeaders: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// NewRPCRequestObject creates and returns a raw RPCRequest structure.
|
|
// It is mainly used when building batch requests. For single requests use RPCClient.Call().
|
|
// RPCRequest struct can also be created directly, but this function sets the ID and the jsonrpc field to the correct values.
|
|
func (client *RPCClient) NewRPCRequestObject(method string, params ...interface{}) *RPCRequest {
|
|
client.idMutex.Lock()
|
|
rpcRequest := RPCRequest{
|
|
ID: client.nextID,
|
|
JSONRPC: "2.0",
|
|
Method: method,
|
|
Params: params,
|
|
}
|
|
if client.autoIncrementID == true {
|
|
client.nextID++
|
|
}
|
|
client.idMutex.Unlock()
|
|
|
|
if len(params) == 0 {
|
|
rpcRequest.Params = nil
|
|
}
|
|
|
|
return &rpcRequest
|
|
}
|
|
|
|
// NewRPCNotificationObject creates and returns a raw RPCNotification structure.
|
|
// It is mainly used when building batch requests. For single notifications use RPCClient.Notification().
|
|
// NewRPCNotificationObject struct can also be created directly, but this function sets the ID and the jsonrpc field to the correct values.
|
|
func (client *RPCClient) NewRPCNotificationObject(method string, params ...interface{}) *RPCNotification {
|
|
rpcNotification := RPCNotification{
|
|
JSONRPC: "2.0",
|
|
Method: method,
|
|
Params: params,
|
|
}
|
|
|
|
if len(params) == 0 {
|
|
rpcNotification.Params = nil
|
|
}
|
|
|
|
return &rpcNotification
|
|
}
|
|
|
|
// Call sends an jsonrpc request over http to the rpc-service url that was provided on client creation.
|
|
//
|
|
// If something went wrong on the network / http level or if json parsing failed it returns an error.
|
|
//
|
|
// If something went wrong on the rpc-service / protocol level the Error field of the returned RPCResponse is set
|
|
// and contains information about the error.
|
|
//
|
|
// If the request was successful the Error field is nil and the Result field of the RPCRespnse struct contains the rpc result.
|
|
func (client *RPCClient) Call(method string, params ...interface{}) (*RPCResponse, error) {
|
|
// Ensure that params are nil and will be omitted from JSON if not specified.
|
|
var p interface{}
|
|
if len(params) != 0 {
|
|
p = params
|
|
}
|
|
httpRequest, err := client.newRequest(false, method, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client.doCall(httpRequest)
|
|
}
|
|
|
|
// CallNamed sends an jsonrpc request over http to the rpc-service url that was provided on client creation.
|
|
// This differs from Call() by sending named, rather than positional, arguments.
|
|
//
|
|
// If something went wrong on the network / http level or if json parsing failed it returns an error.
|
|
//
|
|
// If something went wrong on the rpc-service / protocol level the Error field of the returned RPCResponse is set
|
|
// and contains information about the error.
|
|
//
|
|
// If the request was successful the Error field is nil and the Result field of the RPCRespnse struct contains the rpc result.
|
|
func (client *RPCClient) CallNamed(method string, params map[string]interface{}) (*RPCResponse, error) {
|
|
httpRequest, err := client.newRequest(false, method, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client.doCall(httpRequest)
|
|
}
|
|
|
|
func (client *RPCClient) doCall(req *http.Request) (*RPCResponse, error) {
|
|
httpResponse, err := client.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer httpResponse.Body.Close()
|
|
|
|
rpcResponse := RPCResponse{}
|
|
decoder := json.NewDecoder(httpResponse.Body)
|
|
decoder.UseNumber()
|
|
err = decoder.Decode(&rpcResponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &rpcResponse, nil
|
|
}
|
|
|
|
// Notification sends a jsonrpc request to the rpc-service. The difference to Call() is that this request does not expect a response.
|
|
// The ID field of the request is omitted.
|
|
func (client *RPCClient) Notification(method string, params ...interface{}) error {
|
|
if len(params) == 0 {
|
|
params = nil
|
|
}
|
|
httpRequest, err := client.newRequest(true, method, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpResponse, err := client.httpClient.Do(httpRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer httpResponse.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
// Batch sends a jsonrpc batch request to the rpc-service.
|
|
// The parameter is a list of requests the could be one of:
|
|
// RPCRequest
|
|
// RPCNotification.
|
|
//
|
|
// The batch requests returns a list of RPCResponse structs.
|
|
func (client *RPCClient) Batch(requests ...interface{}) (*BatchResponse, error) {
|
|
for _, r := range requests {
|
|
switch r := r.(type) {
|
|
default:
|
|
return nil, fmt.Errorf("Invalid parameter: %s", r)
|
|
case *RPCRequest:
|
|
case *RPCNotification:
|
|
}
|
|
}
|
|
|
|
httpRequest, err := client.newBatchRequest(requests...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
httpResponse, err := client.httpClient.Do(httpRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer httpResponse.Body.Close()
|
|
|
|
rpcResponses := []RPCResponse{}
|
|
decoder := json.NewDecoder(httpResponse.Body)
|
|
decoder.UseNumber()
|
|
err = decoder.Decode(&rpcResponses)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &BatchResponse{rpcResponses: rpcResponses}, nil
|
|
}
|
|
|
|
// SetAutoIncrementID if set to true, the id field of an rpcjson request will be incremented automatically
|
|
func (client *RPCClient) SetAutoIncrementID(flag bool) {
|
|
client.autoIncrementID = flag
|
|
}
|
|
|
|
// SetNextID can be used to manually set the next id / reset the id.
|
|
func (client *RPCClient) SetNextID(id uint) {
|
|
client.idMutex.Lock()
|
|
client.nextID = id
|
|
client.idMutex.Unlock()
|
|
}
|
|
|
|
// SetCustomHeader is used to set a custom header for each rpc request.
|
|
// You could for example set the Authorization Bearer here.
|
|
func (client *RPCClient) SetCustomHeader(key string, value string) {
|
|
client.customHeaders[key] = value
|
|
}
|
|
|
|
// UnsetCustomHeader is used to removes a custom header that was added before.
|
|
func (client *RPCClient) UnsetCustomHeader(key string) {
|
|
delete(client.customHeaders, key)
|
|
}
|
|
|
|
// SetBasicAuth is a helper function that sets the header for the given basic authentication credentials.
|
|
// To reset / disable authentication just set username or password to an empty string value.
|
|
func (client *RPCClient) SetBasicAuth(username string, password string) {
|
|
if username == "" || password == "" {
|
|
delete(client.customHeaders, "Authorization")
|
|
return
|
|
}
|
|
auth := username + ":" + password
|
|
client.customHeaders["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
|
|
}
|
|
|
|
// SetHTTPClient can be used to set a custom http.Client.
|
|
// This can be useful for example if you want to customize the http.Client behaviour (e.g. proxy settings)
|
|
func (client *RPCClient) SetHTTPClient(httpClient *http.Client) {
|
|
if httpClient == nil {
|
|
panic("httpClient cannot be nil")
|
|
}
|
|
client.httpClient = httpClient
|
|
}
|
|
|
|
func (client *RPCClient) newRequest(notification bool, method string, params interface{}) (*http.Request, error) {
|
|
// TODO: easier way to remove ID from RPCRequest without extra struct
|
|
var rpcRequest interface{}
|
|
if notification {
|
|
rpcNotification := RPCNotification{
|
|
JSONRPC: "2.0",
|
|
Method: method,
|
|
Params: params,
|
|
}
|
|
rpcRequest = rpcNotification
|
|
} else {
|
|
client.idMutex.Lock()
|
|
request := RPCRequest{
|
|
ID: client.nextID,
|
|
JSONRPC: "2.0",
|
|
Method: method,
|
|
Params: params,
|
|
}
|
|
if client.autoIncrementID == true {
|
|
client.nextID++
|
|
}
|
|
client.idMutex.Unlock()
|
|
rpcRequest = request
|
|
}
|
|
|
|
body, err := json.Marshal(rpcRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request, err := http.NewRequest("POST", client.endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for k, v := range client.customHeaders {
|
|
request.Header.Add(k, v)
|
|
}
|
|
|
|
request.Header.Add("Content-Type", "application/json")
|
|
request.Header.Add("Accept", "application/json")
|
|
|
|
return request, nil
|
|
}
|
|
|
|
func (client *RPCClient) newBatchRequest(requests ...interface{}) (*http.Request, error) {
|
|
|
|
body, err := json.Marshal(requests)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request, err := http.NewRequest("POST", client.endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for k, v := range client.customHeaders {
|
|
request.Header.Add(k, v)
|
|
}
|
|
|
|
request.Header.Add("Content-Type", "application/json")
|
|
request.Header.Add("Accept", "application/json")
|
|
|
|
return request, nil
|
|
}
|
|
|
|
// UpdateRequestID updates the ID of an RPCRequest structure.
|
|
//
|
|
// This is used if a request is sent another time and the request should get an updated id.
|
|
//
|
|
// This does only make sense when used on with Batch() since Call() and Notififcation() do update the id automatically.
|
|
func (client *RPCClient) UpdateRequestID(rpcRequest *RPCRequest) {
|
|
if rpcRequest == nil {
|
|
return
|
|
}
|
|
client.idMutex.Lock()
|
|
defer client.idMutex.Unlock()
|
|
rpcRequest.ID = client.nextID
|
|
if client.autoIncrementID == true {
|
|
client.nextID++
|
|
}
|
|
}
|
|
|
|
// GetInt converts the rpc response to an int and returns it.
|
|
//
|
|
// This is a convenient function. Int could be 32 or 64 bit, depending on the architecture the code is running on.
|
|
// For a deterministic result use GetInt64().
|
|
//
|
|
// If result was not an integer an error is returned.
|
|
func (rpcResponse *RPCResponse) GetInt() (int, error) {
|
|
i, err := rpcResponse.GetInt64()
|
|
return int(i), err
|
|
}
|
|
|
|
// GetInt64 converts the rpc response to an int64 and returns it.
|
|
//
|
|
// If result was not an integer an error is returned.
|
|
func (rpcResponse *RPCResponse) GetInt64() (int64, error) {
|
|
val, ok := rpcResponse.Result.(json.Number)
|
|
if !ok {
|
|
return 0, fmt.Errorf("could not parse int64 from %s", rpcResponse.Result)
|
|
}
|
|
|
|
i, err := val.Int64()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
// GetFloat64 converts the rpc response to an float64 and returns it.
|
|
//
|
|
// If result was not an float64 an error is returned.
|
|
func (rpcResponse *RPCResponse) GetFloat64() (float64, error) {
|
|
val, ok := rpcResponse.Result.(json.Number)
|
|
if !ok {
|
|
return 0, fmt.Errorf("could not parse float64 from %s", rpcResponse.Result)
|
|
}
|
|
|
|
f, err := val.Float64()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
// GetBool converts the rpc response to a bool and returns it.
|
|
//
|
|
// If result was not a bool an error is returned.
|
|
func (rpcResponse *RPCResponse) GetBool() (bool, error) {
|
|
val, ok := rpcResponse.Result.(bool)
|
|
if !ok {
|
|
return false, fmt.Errorf("could not parse bool from %s", rpcResponse.Result)
|
|
}
|
|
|
|
return val, nil
|
|
}
|
|
|
|
// GetString converts the rpc response to a string and returns it.
|
|
//
|
|
// If result was not a string an error is returned.
|
|
func (rpcResponse *RPCResponse) GetString() (string, error) {
|
|
val, ok := rpcResponse.Result.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("could not parse string from %s", rpcResponse.Result)
|
|
}
|
|
|
|
return val, nil
|
|
}
|
|
|
|
// GetObject converts the rpc response to an object (e.g. a struct) and returns it.
|
|
// The parameter should be a structure that can hold the data of the response object.
|
|
//
|
|
// For example if the following json return value is expected: {"name": "alex", age: 33, "country": "Germany"}
|
|
// the struct should look like
|
|
// type Person struct {
|
|
// Name string
|
|
// Age int
|
|
// Country string
|
|
// }
|
|
func (rpcResponse *RPCResponse) GetObject(toType interface{}) error {
|
|
js, err := json.Marshal(rpcResponse.Result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = json.Unmarshal(js, toType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetResponseOf returns the rpc response of the corresponding request by matching the id.
|
|
//
|
|
// For this method to work, autoincrementID should be set to true (default).
|
|
func (batchResponse *BatchResponse) GetResponseOf(request *RPCRequest) (*RPCResponse, error) {
|
|
if request == nil {
|
|
return nil, errors.New("parameter cannot be nil")
|
|
}
|
|
for _, elem := range batchResponse.rpcResponses {
|
|
if elem.ID == request.ID {
|
|
return &elem, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("element with id %d not found", request.ID)
|
|
}
|