/
signalr.go
174 lines (151 loc) · 4.74 KB
/
signalr.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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// Copyright (c) 2019-2021, The Decred developers
// Copyright (c) 2023, The Cryptopower developers
// See LICENSE for details.
// This was almost entirely written using
// https://blog.3d-logic.com/2015/03/29/signalr-on-the-wire-an-informal-description-of-the-signalr-protocol/
// and github.com/carterjones/signalr as a reference guide.
package ext
import (
"encoding/json"
"fmt"
"math"
"math/rand"
"net/http"
"net/url"
"github.com/crypto-power/cryptopower/libwallet/utils"
)
// defaultClientProtocol is the default protocol version used when connecting to
// a signalR websocket.
const defaultClientProtocol = "1.5"
// signalRClientMsg represents a message sent from or to the signalR server on a
// persistent websocket connection.
type signalRClientMsg struct {
// invocation identifier – allows to match up responses with requests
I int
// the name of the hub
H string
// the name of the method
M string
// arguments (an array, can be empty if the method does not have any
// parameters)
A []interface{}
// state – a dictionary containing additional custom data (optional)
S *json.RawMessage `json:",omitempty"`
}
// signalRMessage represents a signalR message sent from the server to the
// persistent websocket connection.
type signalRMessage struct {
// message id, present for all non-KeepAlive messages
C string
// an array containing actual data
M []signalRClientMsg
// indicates that the transport was initialized (a.k.a. init message)
S int
// groups token – an encrypted string representing group membership
G string
// other miscellaneous variables that sometimes are sent by the server
I string
E string
R json.RawMessage
H json.RawMessage // could be bool or string depending on a message type
D json.RawMessage
T json.RawMessage
}
// signalRNegotiation represents a response sent after a negotiation with a
// signalR server. A bunch of other fields have been removed because they are
// not needed.
type signalRNegotiation struct {
ConnectionToken string
}
// connectSignalRWebsocket connects to a signalR websocket in three steps
// (negotiate, connect, and start) and returns a websocketFeed which can be used
// to read websocket messages from the signalR server. There are no retires if
// connection to signalR websocket fails.
func connectSignalRWebsocket(host, endpoint string) (websocketFeed, error) {
params := map[string]string{
"clientProtocol": defaultClientProtocol,
}
// Step 1: Negotiate with the signalR server to receive a connection token.
sn := new(signalRNegotiation)
reqCfg := &utils.ReqConfig{
HTTPURL: makeSignalRURL("negotiate", host, endpoint, params),
Method: http.MethodGet,
}
_, err := utils.HTTPRequest(reqCfg, &sn)
if err != nil {
return nil, err
}
// Step 2: Connect to signalR websocket.
params["transport"] = "webSockets"
params["connectionToken"] = sn.ConnectionToken
cfg := &socketConfig{
address: makeSignalRURL("connect", host, endpoint, params),
headers: map[string][]string{
"User-Agent": {fauxBrowserUA},
},
}
var success bool
ws, err := newSocketConnection(cfg)
if err != nil {
return nil, err
}
defer func() {
if success {
return
}
// Gracefully close this websocket connection if we encounter an error
// below.
ws.Close()
}()
// Step 3: Start the connection before returning the websocket connection.
// The websocket connection can be used without this step but we'd like to
// keep this step to be sure we can successfully received websocket messages
// from the signalR server.
confirmation := &struct{ Response string }{}
reqCfg.HTTPURL = makeSignalRURL("start", host, endpoint, params)
_, err = utils.HTTPRequest(reqCfg, &confirmation)
if err != nil {
return nil, err
}
// Wait for the init message.
initMsg, err := ws.Read()
if err != nil {
return nil, err
}
// Extract the server message.
var msg signalRMessage
err = json.Unmarshal(initMsg, &msg)
if err != nil {
return nil, fmt.Errorf("json.Unmarshal error: %w", err)
}
serverInitialized := 1
if msg.S != serverInitialized {
return nil, fmt.Errorf("unexpected S value received from server: %d | message: %s", msg.S, string(initMsg))
}
success = true
return ws, nil
}
// makeSignalRURL is used to construct a signalR connection URL for the action
// specified.
func makeSignalRURL(action, host, endpoint string, params map[string]string) string {
var u url.URL
u.Scheme = "https"
u.Host = host
u.Path = endpoint
param := url.Values{}
for key, value := range params {
param.Set(key, value)
}
switch action {
case "negotiate":
u.Path += "/negotiate"
case "connect":
u.Path += "/connect"
u.Scheme = "wss"
param.Set("tid", fmt.Sprintf("%.0f", math.Floor(rand.Float64()*11)))
case "start":
u.Path += "/start"
}
u.RawQuery = param.Encode()
return u.String()
}