Repository navigation
fix: send a close frame for over-long reasons and invalid codes #583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9a289b8
e822c4c
e310232
a69351a
cdbbd1c
0687491
6edb027
66281fe
2afed39
c87b857
7739453
9c7303a
a2102e3
90f00c6
534f8a5
81fe688
1d98fe0
b29cc42
b59c359
64623f3
7cda6a9
55d51d4
278ea74
edcec45
c4bfc17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,11 +92,29 @@ func CloseStatus(err error) StatusCode { | |
| // The connection can only be closed once. Additional calls to Close | ||
| // are no-ops. | ||
| // | ||
| // The maximum length of reason must be 125 bytes. Avoid sending a dynamic reason. | ||
| // Close replaces invalid UTF-8 in reason with U+FFFD, then truncates it to fit | ||
| // 123 bytes without splitting a rune. If it truncated the reason and the close | ||
| // had no other error, Close returns an error wrapping ErrCloseReasonTruncated. | ||
| // | ||
| // Valid codes are 1000 to 1003, 1007 to 1014, and 3000 to 4999. Close sends | ||
| // any other code as StatusInternalError with no reason and returns an error. | ||
| // The exception is StatusNoStatusRcvd, which sends a close frame with no code | ||
| // and no reason. | ||
| // | ||
| // When the first call to Close returns, the connection is closed, even on error. | ||
| // | ||
| // Close will unblock all goroutines interacting with the connection once | ||
| // complete. | ||
| func (c *Conn) Close(code StatusCode, reason string) (err error) { | ||
| func (c *Conn) Close(code StatusCode, reason string) error { | ||
| ce, wireErr := wireCloseError(code, reason) | ||
| err := c.closeAndWait(ce) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return wireErr | ||
|
mafredri marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| func (c *Conn) closeAndWait(ce CloseError) (err error) { | ||
| defer errd.Wrap(&err, "failed to close WebSocket") | ||
|
|
||
| if c.casClosing() { | ||
|
|
@@ -112,7 +130,7 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) { | |
| } | ||
| }() | ||
|
|
||
| err = c.closeHandshake(code, reason) | ||
| err = c.closeHandshake(ce) | ||
|
|
||
| err2 := c.close() | ||
| if err == nil && err2 != nil { | ||
|
|
@@ -154,38 +172,24 @@ func (c *Conn) CloseNow() (err error) { | |
| return err | ||
| } | ||
|
|
||
| func (c *Conn) closeHandshake(code StatusCode, reason string) error { | ||
| err := c.writeClose(code, reason) | ||
| func (c *Conn) closeHandshake(ce CloseError) error { | ||
| err := c.writeClose(ce) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err = c.waitCloseHandshake() | ||
| if CloseStatus(err) != code { | ||
| if CloseStatus(err) != ce.Code { | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (c *Conn) writeClose(code StatusCode, reason string) error { | ||
| ce := CloseError{ | ||
| Code: code, | ||
| Reason: reason, | ||
| } | ||
|
|
||
| var p []byte | ||
| var err error | ||
| if ce.Code != StatusNoStatusRcvd { | ||
| p, err = ce.bytes() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| func (c *Conn) writeClose(ce CloseError) error { | ||
| ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) | ||
| defer cancel() | ||
|
|
||
| err = c.writeControl(ctx, opClose, p) | ||
| err := c.writeControl(ctx, opClose, ce.bytes()) | ||
| // If the connection closed as we're writing we ignore the error as we might | ||
| // have written the close frame, the peer responded and then someone else read it | ||
| // and closed the connection. | ||
|
|
@@ -262,16 +266,17 @@ func parseClosePayload(p []byte) (CloseError, error) { | |
| return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) | ||
| } | ||
|
|
||
| ce := CloseError{ | ||
| Code: StatusCode(binary.BigEndian.Uint16(p)), | ||
| Reason: string(p[2:]), | ||
| } | ||
|
|
||
| if !validWireCloseCode(ce.Code) { | ||
| return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) | ||
| code := StatusCode(binary.BigEndian.Uint16(p)) | ||
| if !validWireCloseCode(code) { | ||
| return CloseError{}, fmt.Errorf("invalid close status code %v", code) | ||
| } | ||
|
|
||
| return ce, nil | ||
| // The reason is normalized as for Close, because the reply to this frame | ||
| // sends the returned CloseError. The truncation error is dropped, because a | ||
| // received reason fits in a close frame and only replacing invalid UTF-8 | ||
| // can make it longer. | ||
| reason, _ := wireCloseReason(string(p[2:])) | ||
| return CloseError{Code: code, Reason: reason}, nil | ||
| } | ||
|
|
||
| // See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number | ||
|
|
@@ -292,33 +297,29 @@ func validWireCloseCode(code StatusCode) bool { | |
| return false | ||
| } | ||
|
|
||
| func (ce CloseError) bytes() ([]byte, error) { | ||
| p, err := ce.bytesErr() | ||
| if err != nil { | ||
| err = fmt.Errorf("failed to marshal close frame: %w", err) | ||
| ce = CloseError{ | ||
| Code: StatusInternalError, | ||
| } | ||
| p, _ = ce.bytesErr() | ||
| // wireCloseError returns the CloseError to send, also when the error is | ||
| // non-nil. The error reports a replaced code or a truncated reason. | ||
| func wireCloseError(code StatusCode, reason string) (CloseError, error) { | ||
|
mafredri marked this conversation as resolved.
Resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Or get rid of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait I am dumb, they take different input. Should |
||
| if code == StatusNoStatusRcvd { | ||
| return CloseError{Code: code}, nil | ||
| } | ||
| return p, err | ||
| } | ||
|
|
||
| const maxCloseReason = maxControlPayload - 2 | ||
|
|
||
| func (ce CloseError) bytesErr() ([]byte, error) { | ||
| if len(ce.Reason) > maxCloseReason { | ||
| return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) | ||
| if !validWireCloseCode(code) { | ||
| return CloseError{Code: StatusInternalError}, fmt.Errorf("websocket: invalid close status code %v, replaced with %v", code, StatusInternalError) | ||
| } | ||
| reason, err := wireCloseReason(reason) | ||
| return CloseError{Code: code, Reason: reason}, err | ||
| } | ||
|
|
||
| if !validWireCloseCode(ce.Code) { | ||
| return nil, fmt.Errorf("status code %v cannot be set", ce.Code) | ||
| // bytes returns the close frame payload for ce, which must come from | ||
| // wireCloseError or parseClosePayload. | ||
| func (ce CloseError) bytes() []byte { | ||
| if ce.Code == StatusNoStatusRcvd { | ||
| return nil | ||
| } | ||
|
|
||
| buf := make([]byte, 2+len(ce.Reason)) | ||
| binary.BigEndian.PutUint16(buf, uint16(ce.Code)) | ||
| copy(buf[2:], ce.Reason) | ||
| return buf, nil | ||
| return buf | ||
| } | ||
|
|
||
| func (c *Conn) casClosing() bool { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package websocket | ||
|
mafredri marked this conversation as resolved.
Resolved
|
||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
| "unicode/utf8" | ||
| ) | ||
|
|
||
| // maxControlPayload is the maximum length of a control frame payload. | ||
| // See https://tools.ietf.org/html/rfc6455#section-5.5. | ||
| const maxControlPayload = 125 | ||
|
|
||
| // maxCloseReason is maxControlPayload minus the 2-byte status code. | ||
| const maxCloseReason = maxControlPayload - 2 | ||
|
|
||
| // wireCloseReason returns the reason to send, also when the error is non-nil. | ||
| // The error reports a truncated reason. | ||
| func wireCloseReason(reason string) (string, error) { | ||
| // RFC 6455 section 5.5.1 requires a UTF-8 reason. | ||
| valid := strings.ToValidUTF8(reason, "\uFFFD") | ||
| if len(valid) <= maxCloseReason { | ||
| return valid, nil | ||
| } | ||
| n := maxCloseReason | ||
| for !utf8.RuneStart(valid[n]) { | ||
| n-- | ||
| } | ||
| var replaced string | ||
| if len(valid) != len(reason) { | ||
| replaced = " after replacing invalid UTF-8" | ||
| } | ||
| return valid[:n], fmt.Errorf("%w from %d to %d bytes%s", ErrCloseReasonTruncated, len(valid), n, replaced) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.