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