Skip to content

Commit ca133c8

Browse files
committed
fix: ensure websocket close messages are truncated to 123 bytes
It's possible for websocket close messages to be too long, which cause them to silently fail without a proper close message. See error below: ``` 2022-03-31 17:08:34.862 [INFO] (stdlib) <close_notjs.go:72> "2022/03/31 17:08:34 websocket: failed to marshal close frame: reason string max is 123 but got \"insert provisioner daemon:Cannot encode []database.ProvisionerType into oid 19098 - []database.ProvisionerType must implement Encoder or be converted to a string\" with length 161" ```
1 parent bb6c12d commit ca133c8

File tree

4 files changed

+73
-5
lines changed

4 files changed

+73
-5
lines changed

coderd/provisionerdaemons.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request
5656
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform},
5757
})
5858
if err != nil {
59-
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("insert provisioner daemon:% s", err))
59+
_ = conn.Close(websocket.StatusInternalError, fmtWebsocketCloseMsg("insert provisioner daemon: %s", err))
6060
return
6161
}
6262

@@ -67,7 +67,7 @@ func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request
6767
config.LogOutput = io.Discard
6868
session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config)
6969
if err != nil {
70-
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("multiplex server: %s", err))
70+
_ = conn.Close(websocket.StatusInternalError, fmtWebsocketCloseMsg("multiplex server: %s", err))
7171
return
7272
}
7373
mux := drpcmux.New()
@@ -80,13 +80,13 @@ func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request
8080
Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
8181
})
8282
if err != nil {
83-
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("drpc register provisioner daemon: %s", err))
83+
_ = conn.Close(websocket.StatusInternalError, fmtWebsocketCloseMsg("drpc register provisioner daemon: %s", err))
8484
return
8585
}
8686
server := drpcserver.New(mux)
8787
err = server.Serve(r.Context(), session)
8888
if err != nil {
89-
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("serve: %s", err))
89+
_ = conn.Close(websocket.StatusInternalError, fmtWebsocketCloseMsg("serve: %s", err))
9090
return
9191
}
9292
_ = conn.Close(websocket.StatusGoingAway, "")

coderd/workspaceresources.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (api *api) workspaceResourceDial(rw http.ResponseWriter, r *http.Request) {
108108
Pubsub: api.Pubsub,
109109
})
110110
if err != nil {
111-
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("serve: %s", err))
111+
_ = conn.Close(websocket.StatusInternalError, fmtWebsocketCloseMsg("serve: %s", err))
112112
return
113113
}
114114
}

coderd/ws.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package coderd
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
const websocketCloseMaxLen = 123
9+
10+
// fmtWebsocketCloseMsg formats a websocket close message and ensures it is
11+
// truncated to the maximum allowed length.
12+
func fmtWebsocketCloseMsg(format string, vars ...any) string {
13+
msg := fmt.Sprintf(format, vars...)
14+
15+
// Cap msg length at 123 bytes. nhooyr/websocket only allows close messages
16+
// of this length.
17+
if len(msg) > websocketCloseMaxLen {
18+
return truncateString(msg, websocketCloseMaxLen)
19+
}
20+
21+
return msg
22+
}
23+
24+
// truncateString safely truncates a string to a maximum size of byteLen. It
25+
// writes whole runes until a single rune would increase the string size above
26+
// byteLen.
27+
func truncateString(str string, byteLen int) string {
28+
sb := strings.Builder{}
29+
sb.Grow(byteLen)
30+
31+
for _, char := range str {
32+
if sb.Len()+len(string(char)) > byteLen {
33+
break
34+
}
35+
36+
sb.WriteRune(char)
37+
}
38+
39+
return sb.String()
40+
}

coderd/ws_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package coderd
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func Test_websocketCloseMsg(t *testing.T) {
11+
t.Parallel()
12+
13+
t.Run("TruncateSingleByteCharacters", func(t *testing.T) {
14+
t.Parallel()
15+
16+
msg := strings.Repeat("d", 255)
17+
trunc := fmtWebsocketCloseMsg(msg)
18+
assert.LessOrEqual(t, len(trunc), 123)
19+
})
20+
21+
t.Run("TruncateMultiByteCharacters", func(t *testing.T) {
22+
t.Parallel()
23+
24+
msg := strings.Repeat("こんにちは", 10)
25+
trunc := fmtWebsocketCloseMsg(msg)
26+
assert.LessOrEqual(t, len(trunc), 123)
27+
})
28+
}

0 commit comments

Comments
 (0)