You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

476 lines
14 KiB

// 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)
}