xref: /spdk/go/rpc/client/client.go (revision b02581a89058ebaebe03bd0e16e3b58adfe406c1)
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