1/* SPDX-License-Identifier: BSD-3-Clause 2 * Copyright (C) 2023 Intel Corporation. 3 * Copyright (c) 2023 Dell Inc, or its subsidiaries. 4 * All rights reserved. 5 */ 6 7package client 8 9import ( 10 "encoding/json" 11 "fmt" 12 "net" 13 "reflect" 14 "sync/atomic" 15) 16 17const ( 18 // jsonRPCVersion specifies the version of the JSON-RPC protocol. 19 jsonRPCVersion = "2.0" 20 // Unix specifies network type for socket connection. 21 Unix = "unix" 22 // TCP specifies network type for tcp connection. 23 TCP = "tcp" 24) 25 26// Client represents JSON-RPC 2.0 client. 27type Client struct { 28 codec *jsonCodec 29 requestId atomic.Uint64 30} 31 32// Call method sends a JSON-RPC 2.0 request to a specified address (provided during client creation). 33func (c *Client) Call(method string, params any) (*Response, error) { 34 id := c.requestId.Add(1) 35 36 request, reqErr := createRequest(method, id, params) 37 if reqErr != nil { 38 return nil, fmt.Errorf("error during client call for %s method, err: %w", 39 method, reqErr) 40 } 41 42 encErr := c.codec.encoder.Encode(request) 43 if encErr != nil { 44 return nil, fmt.Errorf("error during request encode for %s method, err: %w", 45 method, encErr) 46 } 47 48 response := &Response{} 49 decErr := c.codec.decoder.Decode(response) 50 if decErr != nil { 51 return nil, fmt.Errorf("error during response decode for %s method, err: %w", 52 method, decErr) 53 } 54 55 if request.ID != uint64(response.ID) { 56 return nil, fmt.Errorf("error mismatch request and response IDs for %s method", 57 method) 58 } 59 60 if response.Error != nil { 61 return nil, fmt.Errorf("error received for %s method, err: %w", 62 method, response.Error) 63 } 64 65 return response, nil 66} 67 68// Close closes connection with underlying stream. 69func (c *Client) Close() error { 70 return c.codec.close() 71} 72 73type jsonCodec struct { 74 encoder *json.Encoder 75 decoder *json.Decoder 76 conn net.Conn 77} 78 79func (j *jsonCodec) close() error { 80 return j.conn.Close() 81} 82 83func createJsonCodec(conn net.Conn) *jsonCodec { 84 return &jsonCodec{ 85 encoder: json.NewEncoder(conn), 86 decoder: json.NewDecoder(conn), 87 conn: conn, 88 } 89} 90 91func createRequest(method string, requestId uint64, params any) (*Request, error) { 92 paramErr := verifyRequestParamsType(params) 93 if paramErr != nil { 94 return nil, fmt.Errorf("error during request creation for %s method, err: %w", 95 method, paramErr) 96 } 97 98 return &Request{ 99 Version: jsonRPCVersion, 100 Method: method, 101 Params: params, 102 ID: requestId, 103 }, nil 104} 105 106func createConnectionToSocket(socketAddress string) (net.Conn, error) { 107 address, err := net.ResolveUnixAddr(Unix, socketAddress) 108 if err != nil { 109 return nil, err 110 } 111 112 conn, err := net.DialUnix(Unix, nil, address) 113 if err != nil { 114 return nil, fmt.Errorf("could not connect to a Unix socket on address %s, err: %w", 115 address.String(), err) 116 } 117 118 return conn, nil 119} 120 121func createConnectionToTcp(tcpAddress string) (net.Conn, error) { 122 address, err := net.ResolveTCPAddr(TCP, tcpAddress) 123 if err != nil { 124 return nil, err 125 } 126 127 conn, err := net.DialTCP(TCP, nil, address) 128 if err != nil { 129 return nil, fmt.Errorf("could not connect to a TCP socket on address %s, err: %w", 130 address.String(), err) 131 } 132 133 return conn, nil 134} 135 136func verifyRequestParamsType(params any) error { 137 // Nil is allowed value for params field. 138 if params == nil { 139 return nil 140 } 141 142 paramType := reflect.TypeOf(params).Kind() 143 if paramType == reflect.Pointer { 144 paramType = reflect.TypeOf(params).Elem().Kind() 145 } 146 147 switch paramType { 148 case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: 149 return nil 150 default: 151 return fmt.Errorf("param type %s is not supported", paramType.String()) 152 } 153} 154 155// CreateClientWithJsonCodec creates a new JSON-RPC client. 156// Both Unix and TCP sockets are supported 157func CreateClientWithJsonCodec(network, address string) (*Client, error) { 158 switch network { 159 case "unix", "unixgram", "unixpacket": 160 conn, err := createConnectionToSocket(address) 161 if err != nil { 162 return nil, fmt.Errorf("error during client creation for Unix socket, " + 163 "err: %w", err) 164 } 165 166 return &Client{codec: createJsonCodec(conn), requestId: atomic.Uint64{}}, nil 167 case "tcp", "tcp4", "tcp6": 168 conn, err := createConnectionToTcp(address) 169 if err != nil { 170 return nil, fmt.Errorf("error during client creation for TCP socket, " + 171 "err: %w", err) 172 } 173 174 return &Client{codec: createJsonCodec(conn), requestId: atomic.Uint64{}}, nil 175 default: 176 return nil, fmt.Errorf("unsupported network type") 177 } 178} 179 180// Request represents JSON-RPC request. 181// For more information visit https://www.jsonrpc.org/specification#request_object 182type Request struct { 183 Version string `json:"jsonrpc"` 184 Method string `json:"method"` 185 Params any `json:"params,omitempty"` 186 ID uint64 `json:"id,omitempty"` 187} 188 189func (req *Request) ToString() (string, error) { 190 jsonReq, err := json.Marshal(req) 191 if err != nil { 192 return "", fmt.Errorf("error when creating json string representation " + 193 "of Request, err: %w", err) 194 } 195 196 return string(jsonReq), nil 197} 198 199// Response represents JSON-RPC response. 200// For more information visit http://www.jsonrpc.org/specification#response_object 201type Response struct { 202 Version string `json:"jsonrpc"` 203 Error *Error `json:"error,omitempty"` 204 Result any `json:"result,omitempty"` 205 ID int `json:"id,omitempty"` 206} 207 208func (resp *Response) ToString() (string, error) { 209 jsonResp, err := json.Marshal(resp) 210 if err != nil { 211 return "", fmt.Errorf("error when creating json string representation " + 212 "of Response, err: %w", err) 213 } 214 215 return string(jsonResp), nil 216} 217 218// Error represents JSON-RPC error. 219// For more information visit https://www.jsonrpc.org/specification#error_object 220type Error struct { 221 Code int `json:"code"` 222 Message string `json:"message"` 223 Data any `json:"data,omitempty"` 224} 225 226// Error returns formatted string of JSON-RPC error. 227func (err *Error) Error() string { 228 return fmt.Sprintf("Code=%d Msg=%s", err.Code, err.Message) 229} 230