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