From a01afeace4a00b64f92eb94a6d5c40d22b6386e3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi <hi@nhooyr.io> Date: Mon, 11 Nov 2019 22:55:47 -0500 Subject: [PATCH] Support x-webkit-deflate-frame extension for Safari --- handshake.go | 130 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 37 deletions(-) diff --git a/handshake.go b/handshake.go index 787fee2..0333103 100644 --- a/handshake.go +++ b/handshake.go @@ -152,7 +152,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, return nil, err } if copts != nil { - copts.setHeader(w.Header()) + copts.setHeader(w.Header(), false) } } @@ -190,7 +190,7 @@ func headerContainsToken(h http.Header, key, token string) bool { } for _, v := range h[key] { - if searchHeaderTokens(v, match) != "" { + if searchHeaderTokens(v, match) { return true } } @@ -198,36 +198,54 @@ func headerContainsToken(h http.Header, key, token string) bool { return false } -func headerTokenHasPrefix(h http.Header, key, prefix string) string { - key = textproto.CanonicalMIMEHeaderKey(key) - - prefix = strings.ToLower(prefix) +// readCompressionExtensionHeader extracts compression extension info from h. +// The standard says we should support multiple compression extension configurations +// from the client but we don't need to as there is only a single deflate extension +// and we support every configuration without error so we only need to check the first +// and thus preferred configuration. +func readCompressionExtensionHeader(h http.Header) (xWebkitDeflateFrame bool, params []string, ok bool) { match := func(t string) bool { - return strings.HasPrefix(t, prefix) + vals := strings.Split(t, ";") + for i := range vals { + vals[i] = strings.TrimSpace(vals[i]) + } + params = vals[1:] + + if vals[0] == "permessage-deflate" { + return true + } + + // See https://bugs.webkit.org/show_bug.cgi?id=115504 + if vals[0] == "x-webkit-deflate-frame" { + xWebkitDeflateFrame = true + return true + } + + return false } + key := textproto.CanonicalMIMEHeaderKey("Sec-WebSocket-Extensions") for _, v := range h[key] { - found := searchHeaderTokens(v, match) - if found != "" { - return found + if searchHeaderTokens(v, match) { + return xWebkitDeflateFrame, params, true } } - return "" + return false, nil, false } -func searchHeaderTokens(v string, match func(val string) bool) string { +func searchHeaderTokens(v string, match func(val string) bool) bool { + v = strings.ToLower(v) v = strings.TrimSpace(v) for _, v2 := range strings.Split(v, ",") { v2 = strings.TrimSpace(v2) - v2 = strings.ToLower(v2) if match(v2) { - return v2 + return true } } - return "" + return false } func selectSubprotocol(r *http.Request, subprotocols []string) string { @@ -332,6 +350,10 @@ type CompressionOptions struct { // // Defaults to 256. Threshold int + + // This is used for supporting Safari as it still uses x-webkit-deflate-frame. + // See negotiateCompression. + xWebkitDeflateFrame bool } // Dial performs a WebSocket handshake on the given url with the given options. @@ -407,7 +429,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } if opts.Compression != nil { - opts.Compression.setHeader(req.Header) + opts.Compression.setHeader(req.Header, true) } resp, err := opts.HTTPClient.Do(req) @@ -529,24 +551,30 @@ func makeSecWebSocketKey() (string, error) { } func negotiateCompression(h http.Header, copts *CompressionOptions) (*CompressionOptions, error) { - deflate := headerTokenHasPrefix(h, "Sec-WebSocket-Extensions", "permessage-deflate") - if deflate == "" { + xWebkitDeflateFrame, params, ok := readCompressionExtensionHeader(h) + if !ok { return nil, nil } // Ensures our changes do not modify the real compression options. copts = &*copts - - params := strings.Split(deflate, ";") - for i := range params { - params[i] = strings.TrimSpace(params[i]) - } - - if params[0] != "permessage-deflate" { - return nil, fmt.Errorf("unexpected header format for permessage-deflate extension: %q", deflate) + copts.xWebkitDeflateFrame = xWebkitDeflateFrame + + // We are the client if the header contains the accept header, meaning its from the server. + client := h.Get("Sec-WebSocket-Accept") == "" + + if copts.xWebkitDeflateFrame { + // The other endpoint dictates whether or not we can + // use context takeover on our side. We cannot force it. + // Likewise, we tell the other side so we can force that. + if client { + copts.ClientNoContextTakeover = false + } else { + copts.ServerNoContextTakeover = false + } } - for _, p := range params[1:] { + for _, p := range params { switch p { case "client_no_context_takeover": copts.ClientNoContextTakeover = true @@ -555,27 +583,55 @@ func negotiateCompression(h http.Header, copts *CompressionOptions) (*Compressio copts.ServerNoContextTakeover = true continue case "client_max_window_bits", "server-max-window-bits": - server := h.Get("Sec-WebSocket-Key") != "" - if server { + if !client { // If we are the server, we are allowed to ignore these parameters. // However, if we are the client, we must obey them but because of // https://github.com/golang/go/issues/3155 we cannot. continue } + case "no_context_takeover": + if copts.xWebkitDeflateFrame { + if client { + copts.ClientNoContextTakeover = true + } else { + copts.ServerNoContextTakeover = true + } + continue + } + + // We explicitly fail on x-webkit-deflate-frame's max_window_bits parameter instead + // of ignoring it as the draft spec is unclear. It says the server can ignore it + // but the server has no way of signalling to the client it was ignored as parameters + // are set one way. + // Thus us ignoring it would make the client think we understood it which would cause issues. + // See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06#section-4.1 + // + // Either way, we're only implementing this for webkit which never sends the max_window_bits + // parameter so we don't need to worry about it. } - return nil, fmt.Errorf("unsupported permessage-deflate parameter %q in header: %q", p, deflate) + + return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } return copts, nil } -func (copts *CompressionOptions) setHeader(h http.Header) { - s := "permessage-deflate" - if copts.ClientNoContextTakeover { - s += "; client_no_context_takeover" - } - if copts.ServerNoContextTakeover { - s += "; server_no_context_takeover" +func (copts *CompressionOptions) setHeader(h http.Header, client bool) { + var s string + if !copts.xWebkitDeflateFrame { + s := "permessage-deflate" + if copts.ClientNoContextTakeover { + s += "; client_no_context_takeover" + } + if copts.ServerNoContextTakeover { + s += "; server_no_context_takeover" + } + } else { + s = "x-webkit-deflate-frame" + // We can only set no context takeover for the peer. + if client && copts.ServerNoContextTakeover || !client && copts.ClientNoContextTakeover { + s += "; no_context_takeover" + } } h.Set("Sec-WebSocket-Extensions", s) } -- GitLab