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