-
Notifications
You must be signed in to change notification settings - Fork 0
/
conn_close.go
338 lines (283 loc) · 10.9 KB
/
conn_close.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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//spellchecker:words websocketx
package websocketx
//spellchecker:words errors strconv strings time github gorilla websocket
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
)
// close closes this connection with the given cause.
// It updates the connection state and sends a closing frame to the remote endpoint.
//
// The frame communicated to the remote endpoint will be the frame found in the cause,
// unless the caller provides an explicit frame instead.
//
// This function also updates the internal connection state.
// If force is true, also calls forceClose.
//
// If the connection is already closed, this function does nothing.
func (conn *Connection) close(cause CloseCause, frame *CloseFrame, force bool) {
conn.cancel(cause)
// modifying the state
conn.stateM.Lock()
defer conn.stateM.Unlock()
// if we are already closed, don't bother sending another frame back.
// because we are closed!
if conn.state == CLOSED {
return
}
// write the close frame; but ignore any failures
cf := cause.Frame
if frame != nil {
cf = *frame
}
_ = conn.conn.WriteControl(websocket.CloseMessage, cf.Body(), time.Now().Add(conn.opts.HandshakeTimeout))
// do the actual close
if force {
conn.forceClose(nil)
return
}
// now in closing state
conn.state = CLOSING
}
// forceClose kills the underlying network connection.
// and then updates the state of the connection.
//
// # If error is not nil, it also cancels the context with the given context
//
// the caller should hold stateM
func (conn *Connection) forceClose(err error) error {
if conn.stateM.TryLock() {
panic("conn.forceClose: stateM is not held")
}
// cancel the context if requested
if err != nil {
conn.cancel(CloseCause{
Frame: CloseFrame{
Code: StatusAbnormalClosure,
},
WasClean: false,
Err: err,
})
}
errs := make([]error, 3)
errs[0] = conn.conn.Close()
// terminate any ongoing reads/writes
// this may or may not have any effect
now := time.Now()
errs[1] = conn.conn.SetReadDeadline(now)
errs[2] = conn.conn.SetWriteDeadline(now)
// we're now in closed state
conn.state = CLOSED
return errors.Join(errs...)
}
var (
// ErrConnectionShutdownWith indicates the connection closed because connection.ShutdownWith was called.
ErrConnectionShutdownWith = errors.New("Connection.ShutdownWith called")
// ErrConnectionClose indicates that the connection closed because connection.Close was called.
ErrConnectionClose = errors.New("connection.Close called")
)
// ShutdownWith shuts down this connection with the given code and text for the client.
//
// ShutdownWith automatically formats a close message, sends it, and waits for the close handshake to complete or timeout.
// The timeout used is the normal ReadInterval timeout.
//
// When closeCode is 0, uses CloseNormalClosure.
func (conn *Connection) ShutdownWith(frame CloseFrame) {
if frame.Code <= 0 {
frame.Code = websocket.CloseNormalClosure
}
// write the connection close
conn.close(CloseCause{Frame: frame, WasClean: true, Err: ErrConnectionShutdownWith}, nil, false)
// wait for everything to close
conn.wg.Wait()
}
// Close closes the connection to the peer without sending a specific close message.
// See [CloseWith] for providing the client with a reason for the closure.
func (conn *Connection) Close() error {
conn.stateM.Lock()
defer conn.stateM.Unlock()
return conn.forceClose(ErrConnectionClose)
}
// CloseFrame represents a closing control frame of a websocket connection.
// It is defined RFC 6455, section 5.5.1.
type CloseFrame struct {
Code StatusCode
Reason string
}
// Message returns the message body used to send this frame to
// a remote endpoint.
func (cf CloseFrame) Body() []byte {
return websocket.FormatCloseMessage(int(cf.Code), cf.Reason)
}
// IsZero checks if this CloseFrame has the zero value
func (cf CloseFrame) IsZero() bool {
var zero CloseFrame
return zero == cf
}
// StatusCode is a status code as defined in RFC 6455, Section 7.4.
type StatusCode uint16
// String turns this status code in human-readable form for display.
func (st StatusCode) String() string {
code := strconv.FormatUint(uint64(st), 10)
name := st.Name()
if name == "" {
return code
}
return fmt.Sprintf("%s (%s)", code, name)
}
// Name returns the name of this status code, if it is known.
// If the name is not known, returns the empty string.
func (st StatusCode) Name() string {
return statusCodeNames[st]
}
// Defined Websocket status codes as per RFC 6455, Section 7.4.1.
// Primarily from [rfc], see also [iana].
//
// [rfc]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
// [iana]: https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
const (
// Indicates a normal closure, meaning that the purpose for
// which the connection was established has been fulfilled.
StatusNormalClosure StatusCode = 1000
// Indicates that an endpoint is "going away", such as a server
// going down or a browser having navigated away from a page.
StatusGoingAway StatusCode = 1001
// indicates that an endpoint is terminating the connection due
// to a protocol error.
StatusProtocolError StatusCode = 1002
// indicates that an endpoint is terminating the connection
// because it has received a type of data it cannot accept (e.g., an
// endpoint that understands only text data MAY send this if it
// receives a binary message).
StatusUnsupportedData StatusCode = 1003
// A reserved value and MUST NOT be set as a status code in a
// Close control frame by an endpoint. It is designated for use in
// applications expecting a status code to indicate that no status
// code was actually present.
StatusNoStatusReceived StatusCode = 1005
// A reserved value and MUST NOT be set as a status code in a
// Close control frame by an endpoint. It is designated for use in
// applications expecting a status code to indicate that the
// connection was closed abnormally, e.g., without sending or
// receiving a Close control frame.
StatusAbnormalClosure StatusCode = 1006
// Indicates that an endpoint is terminating the connection
// because it has received data within a message that was not
// consistent with the type of the message (e.g., non-UTF-8 [RFC3629]
// data within a text message).
StatusInvalidFramePayloadData StatusCode = 1007
// Indicates that an endpoint is terminating the connection
// because it has received a message that violates its policy. This
// is a generic status code that can be returned when there is no
// other more suitable status code (e.g., [StatusUnsupportedData] or [StatusCloseMessageTooBig]) or if there
// is a need to hide specific details about the policy.
StatusPolicyViolation StatusCode = 1008
// Indicates that an endpoint is terminating the connection
// because it has received a message that is too big for it to
// process.
StatusMessageTooBig StatusCode = 1009
// Indicates that an endpoint (client) is terminating the
// connection because it has expected the server to negotiate one or
// more extension, but the server didn't return them in the response
// message of the WebSocket handshake. The list of extensions that
// are needed SHOULD appear in the /reason/ part of the Close frame.
// Note that this status code is not used by the server, because it
// can fail the WebSocket handshake instead.
StatusMandatoryExtension StatusCode = 1010
// Indicates that a remote endpoint is terminating the connection
// because it encountered an unexpected condition that prevented it from
// fulfilling the request.
StatusInternalErr StatusCode = 1011
// Indicates that the service is restarted. A client may reconnect,
// and if it choses to do, should reconnect using a randomized delay
// of 5 - 30s.
StatusServiceRestart StatusCode = 1012
// Indicates that the service is experiencing overload. A client
// should only connect to a different IP (when there are multiple for the
// target) or reconnect to the same IP upon user action.
StatusTryAgainLater StatusCode = 1013
// Indicates that the server was acting as a gateway or proxy and
// received an invalid response from the upstream server.
StatusBadGateway StatusCode = 1014
// Additional status codes registered in the IANA registry.
StatusUnauthorized StatusCode = 3000
StatusForbidden StatusCode = 3003
StatusTimeout StatusCode = 3008
)
var statusCodeNames = map[StatusCode]string{
StatusNormalClosure: "Normal Closure",
StatusGoingAway: "Going Away",
StatusProtocolError: "Protocol Error",
StatusUnsupportedData: "Unsupported Data",
StatusNoStatusReceived: "No Status Received",
StatusAbnormalClosure: "Abnormal Closure",
StatusInvalidFramePayloadData: "Invalid Frame Payload Data",
StatusPolicyViolation: "Policy Violation",
StatusMessageTooBig: "Message too Big",
StatusMandatoryExtension: "Mandatory Extension",
StatusInternalErr: "Internal Error",
StatusServiceRestart: "Service Restart",
StatusTryAgainLater: "Try Again Later",
StatusBadGateway: "Bad Gateway",
StatusUnauthorized: "Unauthorized",
StatusForbidden: "Forbidden",
StatusTimeout: "Timeout",
}
// CloseCause is returned by calling [close.Cause] on the context of a connection.
// It indicates the reason why the server was closed.
type CloseCause struct {
// CloseFrame is the close frame that cause the closure.
// If no close frame was received, contains the [StatusAbnormalClosure] code.
Frame CloseFrame
// WasClean indicates if the connection is closed after receiving a close frame
// from the client, or after having sent a close frame.
//
// NOTE: This roughly corresponds to the websocket JavaScript API's CloseEvent's wasClean.
// However in situations where the server sends a close frame, but never receives a response
// the WasClean field may still be true.
// This detail is not considered part of the public API of this package, and may change in the future.
WasClean bool
// Err contains the error that caused the closure of this connection.
// This may be an error returned from the read or write end of the connection,
// or a server-side error.
//
// A special value contained in this field is ErrShuttingDown.
Err error
}
// CloseCause implements the error interface.
func (cc CloseCause) Error() string {
var builder strings.Builder
_, err := fmt.Fprint(&builder, cc.Frame.Code)
if err != nil {
goto error
}
if cc.Frame.Reason != "" {
_, err := fmt.Fprintf(&builder, " (reason: %q)", cc.Frame.Reason)
if err != nil {
goto error
}
}
if !cc.WasClean {
_, err := fmt.Fprint(&builder, " (unclean)")
if err != nil {
goto error
}
}
if cc.Err != nil {
_, err := fmt.Fprintf(&builder, ": %s", cc.Err)
if err != nil {
goto error
}
}
return builder.String()
error:
return "(error formatting CloseCause)"
}
// Unwrap implements the underlying Unwrap interface.
func (cc CloseCause) Unwrap() error {
return cc.Err
}