/
client.go
95 lines (81 loc) 路 2.72 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
// Package rpcclient provides client libraries for interacting with a local swapd instance using
// the JSON-RPC remote procedure call protocol and websockets.
package rpcclient
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/gorilla/rpc/v2/json2"
)
var (
contentTypeJSON = "application/json"
dialTimeout = 60 * time.Second
httpClientTimeout = 30 * time.Minute
callTimeout = 30 * time.Minute
transport = &http.Transport{
DialContext: (&net.Dialer{
Timeout: dialTimeout,
}).DialContext,
}
httpClient = &http.Client{
Transport: transport,
Timeout: httpClientTimeout,
}
)
// Client primarily exists to be a JSON-RPC client to swapd instances, but it can be used
// to POST JSON-RPC requests to any JSON-RPC server. Its current use case assumes swapd is
// running on the local host of a single use system. TLS and authentication are not
// currently supported.
type Client struct {
ctx context.Context
endpoint string
wsEndpoint string
}
// NewClient creates a new JSON-RPC client for the specified endpoint. The passed context
// is used for the full lifetime of the client.
func NewClient(ctx context.Context, port uint16) *Client {
return &Client{
ctx: ctx,
endpoint: fmt.Sprintf("http://127.0.0.1:%d", port),
wsEndpoint: fmt.Sprintf("ws://127.0.0.1:%d/ws", port),
}
}
// post makes a JSON-RPC call to the client's endpoint, serializing any passed request
// object and deserializing any passed response object from the POST response body. Nil
// can be passed as the request or response when no data needs to be serialized or
// deserialized respectively.
func (c *Client) post(method string, request any, response any) error {
data, err := json2.EncodeClientRequest(method, request)
if err != nil {
return err
}
httpReq, err := http.NewRequest(http.MethodPost, c.endpoint, bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Content-Type", contentTypeJSON)
ctx, cancel := context.WithTimeout(c.ctx, callTimeout)
defer cancel()
httpReq = httpReq.WithContext(ctx)
httpResp, err := httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to post %q request: %w", method, err)
}
defer func() { _ = httpResp.Body.Close() }()
// Even if the response is nil, we still need to parse the outer JSON-RPC
// shell to get any error that the server may have returned.
err = json2.DecodeClientResponse(httpResp.Body, response)
if response == nil && errors.Is(err, json2.ErrNullResult) {
err = nil
}
if err != nil {
return fmt.Errorf("%q failed: %w", method, err)
}
return nil
}