Skip to content

Commit c09b72d

Browse files
committed
per message compression; only no context modes
1 parent 2d1e454 commit c09b72d

File tree

4 files changed

+86
-0
lines changed

4 files changed

+86
-0
lines changed

client.go

+27
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
// invalid.
2424
var ErrBadHandshake = errors.New("websocket: bad handshake")
2525

26+
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
27+
2628
// NewClient creates a new client connection using the given net connection.
2729
// The URL u specifies the host and request URI. Use requestHeader to specify
2830
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
@@ -70,6 +72,12 @@ type Dialer struct {
7072

7173
// Subprotocols specifies the client's requested subprotocols.
7274
Subprotocols []string
75+
76+
// CompressionSupported specifies if the client should attempt to negotiate per
77+
// message compression (RFC 7692). Setting this value to true does not
78+
// guarantee that compression will be supported. Currently only "no context
79+
// takeover" modes are supported.
80+
CompressionSupported bool
7381
}
7482

7583
var errMalformedURL = errors.New("malformed ws or wss URL")
@@ -214,13 +222,18 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
214222
k == "Connection" ||
215223
k == "Sec-Websocket-Key" ||
216224
k == "Sec-Websocket-Version" ||
225+
k == "Sec-Websocket-Extensions" ||
217226
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
218227
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
219228
default:
220229
req.Header[k] = vs
221230
}
222231
}
223232

233+
if d.CompressionSupported {
234+
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
235+
}
236+
224237
hostPort, hostNoPort := hostPortNoPort(u)
225238

226239
var proxyURL *url.URL
@@ -337,6 +350,20 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
337350
return nil, resp, ErrBadHandshake
338351
}
339352

353+
for _, ext := range parseExtensions(req.Header) {
354+
if ext[""] != "permessage-deflate" {
355+
continue
356+
}
357+
_, snct := ext["server_no_context_takeover"]
358+
_, cnct := ext["client_no_context_takeover"]
359+
if !snct || !cnct {
360+
return nil, resp, errInvalidCompression
361+
}
362+
conn.newCompressionWriter = compressNoContextTakeover
363+
conn.newDecompressionReader = decompressNoContextTakeover
364+
break
365+
}
366+
340367
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
341368
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
342369

conn.go

+7
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,13 @@ func (c *Conn) UnderlyingConn() net.Conn {
985985
return c.conn
986986
}
987987

988+
// EnableWriteCompression enables and disables write compression of
989+
// subsequent text and binary messages. This function is a noop if
990+
// compression was not negotiated with the peer.
991+
func (c *Conn) EnableWriteCompression(enable bool) {
992+
c.enableWriteCompression = enable
993+
}
994+
988995
// FormatCloseMessage formats closeCode and text as a WebSocket close message.
989996
func FormatCloseMessage(closeCode int, text string) []byte {
990997
buf := make([]byte, 2+len(text))

doc.go

+21
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,25 @@
149149
// The deprecated Upgrade function does not enforce an origin policy. It's the
150150
// application's responsibility to check the Origin header before calling
151151
// Upgrade.
152+
//
153+
// Compression [Experimental]
154+
//
155+
// Per message compression extensions (RFC 7692) are experimentally supported
156+
// by this package in a limited capacity. Enabling the CompressionSupported
157+
// option in Dialer or Upgrader will attempt to negotiate per message deflate
158+
// support. If compression was successfully negotiated with the connection's
159+
// peer, any message received in compressed form will be automatically
160+
// decompressed. All Read methods will return uncompressed bytes.
161+
//
162+
// Per message compression of messages written to a connection can be enabled
163+
// or disabled by calling the corresponding Conn method:
164+
//
165+
// conn.EnableWriteCompression(true)
166+
//
167+
// Currently this package does not support compression with "context takeover".
168+
// This means that messages must be compressed and decompressed in isolation,
169+
// without retaining sliding window or dictionary state across messages. For
170+
// more details refer to RFC 7692.
171+
//
172+
// Use of compression is experimental and may result in decreased performance.
152173
package websocket

server.go

+31
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ type Upgrader struct {
4646
// CheckOrigin is nil, the host in the Origin header must not be set or
4747
// must match the host of the request.
4848
CheckOrigin func(r *http.Request) bool
49+
50+
// CompressionSupported specify if the server should attempt to negotiate per
51+
// message compression (RFC 7692). Setting this value to true does not
52+
// guarantee that compression will be supported. Currently only "no context
53+
// takeover" modes are supported.
54+
CompressionSupported bool
4955
}
5056

5157
func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
@@ -100,6 +106,11 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
100106
if r.Method != "GET" {
101107
return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: method not GET")
102108
}
109+
110+
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
111+
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific Sec-Websocket-Extensions headers are unsupported")
112+
}
113+
103114
if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
104115
return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13")
105116
}
@@ -127,6 +138,18 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
127138

128139
subprotocol := u.selectSubprotocol(r, responseHeader)
129140

141+
// Negotiate PMCE
142+
var compress bool
143+
if u.CompressionSupported {
144+
for _, ext := range parseExtensions(r.Header) {
145+
if ext[""] != "permessage-deflate" {
146+
continue
147+
}
148+
compress = true
149+
break
150+
}
151+
}
152+
130153
var (
131154
netConn net.Conn
132155
br *bufio.Reader
@@ -152,6 +175,11 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
152175
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize)
153176
c.subprotocol = subprotocol
154177

178+
if compress {
179+
c.newCompressionWriter = compressNoContextTakeover
180+
c.newDecompressionReader = decompressNoContextTakeover
181+
}
182+
155183
p := c.writeBuf[:0]
156184
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
157185
p = append(p, computeAcceptKey(challengeKey)...)
@@ -161,6 +189,9 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
161189
p = append(p, c.subprotocol...)
162190
p = append(p, "\r\n"...)
163191
}
192+
if compress {
193+
p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
194+
}
164195
for k, vs := range responseHeader {
165196
if k == "Sec-Websocket-Protocol" {
166197
continue

0 commit comments

Comments
 (0)