From 36ee69609eb7be3bb8d26e48e41dcce864f45bc0 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 13 Jul 2017 17:56:01 +0200 Subject: [PATCH 1/6] fix: double compression. --- glide.lock | 6 +- glide.yaml | 2 + middlewares/compress.go | 24 ++-- middlewares/compress_test.go | 110 +++++++++++++++--- vendor/github.com/NYTimes/gziphandler/gzip.go | 59 +++++----- .../github.com/NYTimes/gziphandler/wrapper.go | 58 +++++++++ 6 files changed, 198 insertions(+), 61 deletions(-) create mode 100644 vendor/github.com/NYTimes/gziphandler/wrapper.go diff --git a/glide.lock b/glide.lock index f8442e172..586c9b450 100644 --- a/glide.lock +++ b/glide.lock @@ -1,4 +1,4 @@ -hash: cebc972cf87c4b0a8f86801f38750c51b09c8dee3bf62bb48f8eaa6ab7946352 +hash: df3bba260c0e5c3183741ab4aca2ae551a5c6d9ba11f4e05b90554a9ffad96ad updated: 2017-06-29T16:47:14.848940186+02:00 imports: - name: cloud.google.com/go @@ -320,7 +320,9 @@ imports: - name: github.com/mvdan/xurls version: db96455566f05ffe42bd6ac671f05eeb1152b45d - name: github.com/NYTimes/gziphandler - version: 22d4470af89e09998fc16b35029df973932df4ae + version: 316adfc72ed3b0157975917adf62ba2dc31842ce + repo: https://github.com/containous/gziphandler.git + vcs: git - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc diff --git a/glide.yaml b/glide.yaml index fc582e5e3..0d8f43971 100644 --- a/glide.yaml +++ b/glide.yaml @@ -87,6 +87,8 @@ import: vcs: git - package: github.com/abbot/go-http-auth - package: github.com/NYTimes/gziphandler + repo: https://github.com/containous/gziphandler.git + vcs: git - package: github.com/docker/leadership - package: github.com/satori/go.uuid version: ^1.1.0 diff --git a/middlewares/compress.go b/middlewares/compress.go index 5df1dd9c9..455c959d6 100644 --- a/middlewares/compress.go +++ b/middlewares/compress.go @@ -1,13 +1,11 @@ package middlewares import ( + "compress/gzip" "net/http" "github.com/NYTimes/gziphandler" -) - -const ( - contentEncodingHeader = "Content-Encoding" + "github.com/containous/traefik/log" ) // Compress is a middleware that allows redirection @@ -15,17 +13,13 @@ type Compress struct{} // ServerHTTP is a function used by Negroni func (c *Compress) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - if isEncoded(r.Header) { - next.ServeHTTP(rw, r) - } else { - newGzipHandler := gziphandler.GzipHandler(next) - newGzipHandler.ServeHTTP(rw, r) - } + gzipHandler(next).ServeHTTP(rw, r) } -func isEncoded(headers http.Header) bool { - header := headers.Get(contentEncodingHeader) - // According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding, - // content is not encoded if the header 'Content-Encoding' is empty or equals to 'identity'. - return header != "" && header != "identity" +func gzipHandler(h http.Handler) http.Handler { + wrapper, err := gziphandler.NewGzipHandler(gzip.DefaultCompression, gziphandler.DefaultMinSize, &gziphandler.GzipResponseWriterWrapper{}) + if err != nil { + log.Error(err) + } + return wrapper(h) } diff --git a/middlewares/compress_test.go b/middlewares/compress_test.go index c83bd3b1d..8f0fc6333 100644 --- a/middlewares/compress_test.go +++ b/middlewares/compress_test.go @@ -1,36 +1,39 @@ package middlewares import ( + "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/NYTimes/gziphandler" + "github.com/codegangsta/negroni" "github.com/containous/traefik/testhelpers" "github.com/stretchr/testify/assert" ) const ( - acceptEncodingHeader = "Accept-Encoding" - varyHeader = "Vary" - gzip = "gzip" + acceptEncodingHeader = "Accept-Encoding" + contentEncodingHeader = "Content-Encoding" + varyHeader = "Vary" + gzipValue = "gzip" ) func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) { handler := &Compress{} req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzip) + req.Header.Add(acceptEncodingHeader, gzipValue) baseBody := generateBytes(gziphandler.DefaultMinSize) next := func(rw http.ResponseWriter, r *http.Request) { rw.Write(baseBody) } - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() handler.ServeHTTP(rw, req, next) - assert.Equal(t, gzip, rw.Header().Get(contentEncodingHeader)) + assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) if assert.ObjectsAreEqualValues(rw.Body.Bytes(), baseBody) { @@ -42,28 +45,105 @@ func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) { handler := &Compress{} req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzip) - req.Header.Add(contentEncodingHeader, gzip) - - baseBody := generateBytes(gziphandler.DefaultMinSize) + req.Header.Add(acceptEncodingHeader, gzipValue) + fakeCompressedBody := generateBytes(gziphandler.DefaultMinSize) next := func(rw http.ResponseWriter, r *http.Request) { - rw.Write(baseBody) + rw.Header().Add(contentEncodingHeader, gzipValue) + rw.Header().Add(varyHeader, acceptEncodingHeader) + rw.Write(fakeCompressedBody) } rw := httptest.NewRecorder() handler.ServeHTTP(rw, req, next) - assert.Equal(t, "", rw.Header().Get(contentEncodingHeader)) - assert.Equal(t, "", rw.Header().Get(varyHeader)) + assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader)) + assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) - assert.EqualValues(t, rw.Body.Bytes(), baseBody) + assert.EqualValues(t, rw.Body.Bytes(), fakeCompressedBody) +} + +func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) { + handler := &Compress{} + + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) + + fakeBody := generateBytes(gziphandler.DefaultMinSize) + next := func(rw http.ResponseWriter, r *http.Request) { + rw.Write(fakeBody) + } + + rw := httptest.NewRecorder() + handler.ServeHTTP(rw, req, next) + + assert.Empty(t, rw.Header().Get(contentEncodingHeader)) + assert.EqualValues(t, rw.Body.Bytes(), fakeBody) +} + +func TestIntegrationShouldNotCompressWhenContentAlreadyCompressed(t *testing.T) { + fakeCompressedBody := generateBytes(100000) + + handler := func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Add(contentEncodingHeader, gzipValue) + rw.Header().Add(varyHeader, acceptEncodingHeader) + rw.Write(fakeCompressedBody) + } + + comp := &Compress{} + + negro := negroni.New(comp) + negro.UseHandlerFunc(handler) + ts := httptest.NewServer(negro) + defer ts.Close() + + client := &http.Client{} + req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) + req.Header.Add(acceptEncodingHeader, gzipValue) + + resp, err := client.Do(req) + assert.NoError(t, err, "there should be no error") + + assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) + assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) + + body, err := ioutil.ReadAll(resp.Body) + assert.EqualValues(t, fakeCompressedBody, body) +} + +func TestIntegrationShouldCompressWhenAcceptEncodingHeaderIsPresent(t *testing.T) { + fakeBody := generateBytes(100000) + + handler := func(rw http.ResponseWriter, r *http.Request) { + rw.Write(fakeBody) + } + + comp := &Compress{} + + negro := negroni.New(comp) + negro.UseHandlerFunc(handler) + ts := httptest.NewServer(negro) + defer ts.Close() + + client := &http.Client{} + req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) + req.Header.Add(acceptEncodingHeader, gzipValue) + + resp, err := client.Do(req) + assert.NoError(t, err, "there should be no error") + + assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) + assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) + + body, err := ioutil.ReadAll(resp.Body) + if assert.ObjectsAreEqualValues(body, fakeBody) { + assert.Fail(t, "expected a compressed body", "got %v", body) + } } func generateBytes(len int) []byte { var value []byte for i := 0; i < len; i++ { - value = append(value, 0x61) + value = append(value, 0x61+byte(i)) } return value } diff --git a/vendor/github.com/NYTimes/gziphandler/gzip.go b/vendor/github.com/NYTimes/gziphandler/gzip.go index cccf79de7..e21205f75 100644 --- a/vendor/github.com/NYTimes/gziphandler/gzip.go +++ b/vendor/github.com/NYTimes/gziphandler/gzip.go @@ -3,6 +3,7 @@ package gziphandler import ( "bufio" "compress/gzip" + "errors" "fmt" "io" "net" @@ -97,6 +98,7 @@ func (w *GzipResponseWriter) Write(b []byte) (int, error) { } // Save the write into a buffer for later use in GZIP responseWriter (if content is long enough) or at close with regular responseWriter. + // On the first write, w.buf changes from nil to a valid slice w.buf = append(w.buf, b...) // If the global writes are bigger than the minSize, compression is enable. @@ -122,7 +124,9 @@ func (w *GzipResponseWriter) startGzip() error { w.Header().Del(contentLength) // Write the header to gzip response. - w.writeHeader() + if w.code != 0 { + w.ResponseWriter.WriteHeader(w.code) + } // Initialize the GZIP response. w.init() @@ -146,14 +150,6 @@ func (w *GzipResponseWriter) WriteHeader(code int) { w.code = code } -// writeHeader uses the saved code to send it to the ResponseWriter. -func (w *GzipResponseWriter) writeHeader() { - if w.code == 0 { - w.code = http.StatusOK - } - w.ResponseWriter.WriteHeader(w.code) -} - // init graps a new gzip writer from the gzipWriterPool and writes the correct // content encoding header. func (w *GzipResponseWriter) init() { @@ -166,19 +162,18 @@ func (w *GzipResponseWriter) init() { // Close will close the gzip.Writer and will put it back in the gzipWriterPool. func (w *GzipResponseWriter) Close() error { - // Buffer not nil means the regular response must be returned. - if w.buf != nil { - w.writeHeader() - // Make the write into the regular response. - _, writeErr := w.ResponseWriter.Write(w.buf) - // Returns the error if any at write. - if writeErr != nil { - return fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", writeErr.Error()) - } - } - - // If the GZIP responseWriter is not set no needs to close it. if w.gw == nil { + // Gzip not trigged yet, write out regular response. + if w.code != 0 { + w.ResponseWriter.WriteHeader(w.code) + } + if w.buf != nil { + _, writeErr := w.ResponseWriter.Write(w.buf) + // Returns the error if any at write. + if writeErr != nil { + return fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", writeErr.Error()) + } + } return nil } @@ -236,12 +231,22 @@ func NewGzipLevelHandler(level int) (func(http.Handler) http.Handler, error) { // NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller // specify the minimum size before compression. func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) { + return NewGzipHandler(level, minSize, &GzipResponseWriter{}) +} + +// NewGzipHandler behave as NewGzipLevelHandler except it let the caller +// specify the minimum size before compression and a GzipWriter. +func NewGzipHandler(level, minSize int, gw GzipWriter) (func(http.Handler) http.Handler, error) { if level != gzip.DefaultCompression && (level < gzip.BestSpeed || level > gzip.BestCompression) { return nil, fmt.Errorf("invalid compression level requested: %d", level) } if minSize < 0 { - return nil, fmt.Errorf("minimum size must be more than zero") + return nil, errors.New("minimum size must be more than zero") } + if gw == nil { + return nil, errors.New("the GzipWriter must be defined") + } + return func(h http.Handler) http.Handler { index := poolIndex(level) @@ -249,13 +254,9 @@ func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler w.Header().Add(vary, acceptEncoding) if acceptsGzip(r) { - gw := &GzipResponseWriter{ - ResponseWriter: w, - index: index, - minSize: minSize, - - buf: []byte{}, - } + gw.SetResponseWriter(w) + gw.setIndex(index) + gw.setMinSize(minSize) defer gw.Close() h.ServeHTTP(gw, r) diff --git a/vendor/github.com/NYTimes/gziphandler/wrapper.go b/vendor/github.com/NYTimes/gziphandler/wrapper.go new file mode 100644 index 000000000..51e532284 --- /dev/null +++ b/vendor/github.com/NYTimes/gziphandler/wrapper.go @@ -0,0 +1,58 @@ +package gziphandler + +import ( + "bufio" + "net" + "net/http" +) + +const ( + contentEncodingHeader = "Content-Encoding" +) + +// ---------- + +// http.ResponseWriter +// http.Hijacker +type GzipWriter interface { + Header() http.Header + Write([]byte) (int, error) + WriteHeader(int) + Hijack() (net.Conn, *bufio.ReadWriter, error) + Close() error + SetResponseWriter(http.ResponseWriter) + setIndex(int) + setMinSize(int) +} + +func (w *GzipResponseWriter) SetResponseWriter(rw http.ResponseWriter) { + w.ResponseWriter = rw +} + +func (w *GzipResponseWriter) setIndex(index int) { + w.index = index +} + +func (w *GzipResponseWriter) setMinSize(minSize int) { + w.minSize = minSize +} + +// -------- + +type GzipResponseWriterWrapper struct { + GzipResponseWriter +} + +func (g *GzipResponseWriterWrapper) Write(b []byte) (int, error) { + if g.gw == nil && isEncoded(g.Header()) { + return g.ResponseWriter.Write(b) + } + return g.GzipResponseWriter.Write(b) +} + +func isEncoded(headers http.Header) bool { + header := headers.Get(contentEncodingHeader) + // According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding, + // content is not encoded if the header 'Content-Encoding' is empty or equals to 'identity'. + return header != "" && header != "identity" +} From a09a8b1235fd7ba2533cea11dccbecbf2c902b55 Mon Sep 17 00:00:00 2001 From: dedalusj Date: Wed, 19 Jul 2017 18:27:52 +1000 Subject: [PATCH 2/6] Fix replace path rule * Fix replace path rule * test: add RequestURI tests. --- middlewares/replace_path.go | 1 + middlewares/replace_path_test.go | 29 +++++++++++++---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/middlewares/replace_path.go b/middlewares/replace_path.go index 208415a51..8d23b905c 100644 --- a/middlewares/replace_path.go +++ b/middlewares/replace_path.go @@ -16,5 +16,6 @@ const ReplacedPathHeader = "X-Replaced-Path" func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Header.Add(ReplacedPathHeader, r.URL.Path) r.URL.Path = s.Path + r.RequestURI = r.URL.RequestURI() s.Handler.ServeHTTP(w, r) } diff --git a/middlewares/replace_path_test.go b/middlewares/replace_path_test.go index 179ebdabb..838a7b633 100644 --- a/middlewares/replace_path_test.go +++ b/middlewares/replace_path_test.go @@ -1,10 +1,11 @@ -package middlewares_test +package middlewares import ( "net/http" "testing" - "github.com/containous/traefik/middlewares" + "github.com/containous/traefik/testhelpers" + "github.com/stretchr/testify/assert" ) func TestReplacePath(t *testing.T) { @@ -17,28 +18,24 @@ func TestReplacePath(t *testing.T) { for _, path := range paths { t.Run(path, func(t *testing.T) { - var newPath, oldPath string - handler := &middlewares.ReplacePath{ + + var expectedPath, actualHeader, requestURI string + handler := &ReplacePath{ Path: replacementPath, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - newPath = r.URL.Path - oldPath = r.Header.Get("X-Replaced-Path") + expectedPath = r.URL.Path + actualHeader = r.Header.Get(ReplacedPathHeader) + requestURI = r.RequestURI }), } - req, err := http.NewRequest("GET", "http://localhost"+path, nil) - if err != nil { - t.Error(err) - } + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+path, nil) handler.ServeHTTP(nil, req) - if newPath != replacementPath { - t.Fatalf("new path should be '%s'", replacementPath) - } - if oldPath != path { - t.Fatalf("old path should be '%s'", path) - } + assert.Equal(t, expectedPath, replacementPath, "Unexpected path.") + assert.Equal(t, path, actualHeader, "Unexpected '%s' header.", ReplacedPathHeader) + assert.Equal(t, expectedPath, requestURI, "Unexpected request URI.") }) } } From 888e6dcbc81bbdcd6c632fc33887b8db167621be Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Tue, 25 Jul 2017 16:56:46 +0200 Subject: [PATCH 3/6] Oxy with gorilla for websocket(+integration tests) --- glide.lock | 4 +- glide.yaml | 2 +- integration/fixtures/websocket/config.toml | 24 +++++ integration/integration_test.go | 15 ++++ integration/websocket_test.go | 81 +++++++++++++++++ vendor/github.com/vulcand/oxy/forward/fwd.go | 90 ++++++------------- .../github.com/vulcand/oxy/forward/headers.go | 49 +++++++--- 7 files changed, 184 insertions(+), 81 deletions(-) create mode 100644 integration/fixtures/websocket/config.toml create mode 100644 integration/websocket_test.go diff --git a/glide.lock b/glide.lock index 586c9b450..25b68c67e 100644 --- a/glide.lock +++ b/glide.lock @@ -1,4 +1,4 @@ -hash: df3bba260c0e5c3183741ab4aca2ae551a5c6d9ba11f4e05b90554a9ffad96ad +hash: bfc5801ed56be5f703a0924d8832dcccc42bf02f9e2b035ef77eab62c0cb4884 updated: 2017-06-29T16:47:14.848940186+02:00 imports: - name: cloud.google.com/go @@ -411,7 +411,7 @@ imports: - name: github.com/vdemeester/docker-events version: be74d4929ec1ad118df54349fda4b0cba60f849b - name: github.com/vulcand/oxy - version: 7da864c1d53bd58165435bb78bbf8c01f01c8f4a + version: 49f1894c20d972f5c73ff44b859f87deb83f0076 repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/glide.yaml b/glide.yaml index 0d8f43971..295d6ef92 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,7 +8,7 @@ import: - package: github.com/cenk/backoff - package: github.com/containous/flaeg - package: github.com/vulcand/oxy - version: 7da864c1d53bd58165435bb78bbf8c01f01c8f4a + version: 49f1894c20d972f5c73ff44b859f87deb83f0076 repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/integration/fixtures/websocket/config.toml b/integration/fixtures/websocket/config.toml new file mode 100644 index 000000000..5dcf0bacb --- /dev/null +++ b/integration/fixtures/websocket/config.toml @@ -0,0 +1,24 @@ +defaultEntryPoints = ["http"] + +logLevel = "DEBUG" + +[entryPoints] + [entryPoints.http] + address = ":8000" + + +[web] + address = ":8080" + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "{{ .WebsocketServer }}" + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Path:/ws" diff --git a/integration/integration_test.go b/integration/integration_test.go index d0d24c212..2193f5d54 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -14,6 +14,8 @@ import ( "github.com/containous/traefik/integration/utils" "github.com/go-check/check" + "bytes" + compose "github.com/libkermit/compose/check" checker "github.com/vdemeester/shakers" ) @@ -38,6 +40,7 @@ func init() { check.Suite(&EurekaSuite{}) check.Suite(&AcmeSuite{}) check.Suite(&DynamoDBSuite{}) + check.Suite(&WebsocketSuite{}) } var traefikBinary = "../dist/traefik" @@ -71,6 +74,18 @@ func (s *BaseSuite) createComposeProject(c *check.C, name string) { s.composeProject = compose.CreateProject(c, projectName, composeFile) } +func withConfigFile(file string) string { + return "--configFile=" + file +} + +func (s *BaseSuite) cmdTraefik(args ...string) (*exec.Cmd, *bytes.Buffer) { + cmd := exec.Command(traefikBinary, args...) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + return cmd, &out +} + func (s *BaseSuite) traefikCmd(c *check.C, args ...string) (*exec.Cmd, string) { cmd, out, err := utils.RunCommand(traefikBinary, args...) c.Assert(err, checker.IsNil, check.Commentf("Fail to run %s with %v", traefikBinary, args)) diff --git a/integration/websocket_test.go b/integration/websocket_test.go new file mode 100644 index 000000000..f852b6895 --- /dev/null +++ b/integration/websocket_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "time" + + "github.com/go-check/check" + + "errors" + "io/ioutil" + "os" + "strings" + + "github.com/containous/traefik/integration/utils" + "github.com/gorilla/websocket" + checker "github.com/vdemeester/shakers" +) + +// WebsocketSuite +type WebsocketSuite struct{ BaseSuite } + +func (suite *WebsocketSuite) TestBase(c *check.C) { + var upgrader = websocket.Upgrader{} // use default options + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer c.Close() + for { + mt, message, err := c.ReadMessage() + if err != nil { + break + } + err = c.WriteMessage(mt, message) + if err != nil { + break + } + } + })) + + file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct { + WebsocketServer string + }{ + WebsocketServer: srv.URL, + }) + + defer os.Remove(file) + cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug") + + err := cmd.Start() + + c.Assert(err, check.IsNil) + defer cmd.Process.Kill() + + // wait for traefik + err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error { + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + if !strings.Contains(string(body), "127.0.0.1") { + return errors.New("Incorrect traefik config") + } + return nil + }) + c.Assert(err, checker.IsNil) + + conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil) + + c.Assert(err, checker.IsNil) + conn.WriteMessage(websocket.TextMessage, []byte("OK")) + + _, msg, err := conn.ReadMessage() + c.Assert(err, checker.IsNil) + + c.Assert(string(msg), checker.Equals, "OK") + +} diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index 28cb1e1c8..c0c7da368 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -4,18 +4,16 @@ package forward import ( - "bufio" "crypto/tls" "io" - "net" "net/http" "net/url" "os" - "reflect" "strconv" "strings" "time" + "github.com/gorilla/websocket" "github.com/vulcand/oxy/utils" ) @@ -255,77 +253,47 @@ func (f *httpForwarder) copyRequest(req *http.Request, u *url.URL) *http.Request // serveHTTP forwards websocket traffic func (f *websocketForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx *handlerContext) { outReq := f.copyRequest(req, req.URL) - host := outReq.URL.Host - dial := net.Dial - // if host does not specify a port, use the default http port - if !strings.Contains(host, ":") { - if outReq.URL.Scheme == "wss" { - host = host + ":443" - } else { - host = host + ":80" - } + dialer := websocket.DefaultDialer + if outReq.URL.Scheme == "wss" && f.TLSClientConfig != nil { + dialer.TLSClientConfig = f.TLSClientConfig } - if outReq.URL.Scheme == "wss" { - if f.TLSClientConfig == nil { - f.TLSClientConfig = http.DefaultTransport.(*http.Transport).TLSClientConfig - } - dial = func(network, address string) (net.Conn, error) { - return tls.Dial("tcp", host, f.TLSClientConfig) - } - } - - targetConn, err := dial("tcp", host) + targetConn, resp, err := dialer.Dial(outReq.URL.String(), outReq.Header) if err != nil { - ctx.log.Errorf("Error dialing `%v`: %v", host, err) + ctx.log.Errorf("Error dialing `%v`: %v", outReq.Host, err) ctx.errHandler.ServeHTTP(w, req, err) return } - hijacker, ok := w.(http.Hijacker) - if !ok { - ctx.log.Errorf("Unable to hijack the connection: %v", reflect.TypeOf(w)) - ctx.errHandler.ServeHTTP(w, req, nil) - return - } - underlyingConn, _, err := hijacker.Hijack() + upgrader := websocket.Upgrader{} + utils.RemoveHeaders(resp.Header, WebsocketUpgradeHeaders...) + underlyingConn, err := upgrader.Upgrade(w, req, resp.Header) if err != nil { - ctx.log.Errorf("Unable to hijack the connection: %v %v", reflect.TypeOf(w), err) - ctx.errHandler.ServeHTTP(w, req, err) + ctx.log.Errorf("Error while upgrading connection : %v", err) return } - // it is now caller's responsibility to Close the underlying connection defer underlyingConn.Close() defer targetConn.Close() - ctx.log.Infof("Writing outgoing Websocket request to target connection: %+v", outReq) - - // write the modified incoming request to the dialed connection - if err = outReq.Write(targetConn); err != nil { - ctx.log.Errorf("Unable to copy request to target: %v", err) - ctx.errHandler.ServeHTTP(w, req, err) - return + errc := make(chan error, 2) + replicate := func(dst io.Writer, src io.Reader) { + _, err := io.Copy(dst, src) + errc <- err } - br := bufio.NewReader(targetConn) - resp, err := http.ReadResponse(br, req) - resp.Write(underlyingConn) - defer resp.Body.Close() + go replicate(targetConn.UnderlyingConn(), underlyingConn.UnderlyingConn()) - // We connect the conn only if the switching protocol has not failed - if resp.StatusCode == http.StatusSwitchingProtocols { - ctx.log.Infof("Switching protocol success") - errc := make(chan error, 2) - replicate := func(dst io.Writer, src io.Reader) { - _, err := io.Copy(dst, src) - errc <- err - } - go replicate(targetConn, underlyingConn) - go replicate(underlyingConn, targetConn) - <-errc + // Try to read the first message + t, msg, err := targetConn.ReadMessage() + if err != nil { + ctx.log.Errorf("Couldn't read first message : %v", err) } else { - ctx.log.Infof("Switching protocol failed") + underlyingConn.WriteMessage(t, msg) } + + go replicate(underlyingConn.UnderlyingConn(), targetConn.UnderlyingConn()) + <-errc + } // copyRequest makes a copy of the specified request. @@ -335,6 +303,7 @@ func (f *websocketForwarder) copyRequest(req *http.Request, u *url.URL) (outReq outReq.URL = utils.CopyURL(req.URL) outReq.URL.Scheme = u.Scheme + outReq.URL.Path = outReq.RequestURI //sometimes backends might be registered as HTTP/HTTPS servers so translate URLs to websocket URLs. switch u.Scheme { @@ -345,19 +314,12 @@ func (f *websocketForwarder) copyRequest(req *http.Request, u *url.URL) (outReq } outReq.URL.Host = u.Host - outReq.URL.Opaque = req.RequestURI // raw query is already included in RequestURI, so ignore it to avoid dupes outReq.URL.RawQuery = "" - outReq.Proto = "HTTP/1.1" - outReq.ProtoMajor = 1 - outReq.ProtoMinor = 1 - - // Overwrite close flag so we can keep persistent connection for the backend servers - outReq.Close = false - outReq.Header = make(http.Header) utils.CopyHeaders(outReq.Header, req.Header) + utils.RemoveHeaders(outReq.Header, WebsocketDialHeaders...) if f.rewriter != nil { f.rewriter.Rewrite(outReq) diff --git a/vendor/github.com/vulcand/oxy/forward/headers.go b/vendor/github.com/vulcand/oxy/forward/headers.go index 229a3eb0c..6c10497af 100644 --- a/vendor/github.com/vulcand/oxy/forward/headers.go +++ b/vendor/github.com/vulcand/oxy/forward/headers.go @@ -1,20 +1,25 @@ package forward const ( - XForwardedProto = "X-Forwarded-Proto" - XForwardedFor = "X-Forwarded-For" - XForwardedHost = "X-Forwarded-Host" - XForwardedServer = "X-Forwarded-Server" - Connection = "Connection" - KeepAlive = "Keep-Alive" - ProxyAuthenticate = "Proxy-Authenticate" - ProxyAuthorization = "Proxy-Authorization" - Te = "Te" // canonicalized version of "TE" - Trailers = "Trailers" - TransferEncoding = "Transfer-Encoding" - Upgrade = "Upgrade" - ContentLength = "Content-Length" - ContentType = "Content-Type" + XForwardedProto = "X-Forwarded-Proto" + XForwardedFor = "X-Forwarded-For" + XForwardedHost = "X-Forwarded-Host" + XForwardedServer = "X-Forwarded-Server" + Connection = "Connection" + KeepAlive = "Keep-Alive" + ProxyAuthenticate = "Proxy-Authenticate" + ProxyAuthorization = "Proxy-Authorization" + Te = "Te" // canonicalized version of "TE" + Trailers = "Trailers" + TransferEncoding = "Transfer-Encoding" + Upgrade = "Upgrade" + ContentLength = "Content-Length" + ContentType = "Content-Type" + SecWebsocketKey = "Sec-Websocket-Key" + SecWebsocketVersion = "Sec-Websocket-Version" + SecWebsocketExtensions = "Sec-Websocket-Extensions" + SecWebsocketProtocol = "Sec-Websocket-Protocol" + SecWebsocketAccept = "Sec-Websocket-Accept" ) // Hop-by-hop headers. These are removed when sent to the backend. @@ -30,3 +35,19 @@ var HopHeaders = []string{ TransferEncoding, Upgrade, } + +var WebsocketDialHeaders = []string{ + Upgrade, + Connection, + SecWebsocketKey, + SecWebsocketVersion, + SecWebsocketExtensions, + SecWebsocketProtocol, + SecWebsocketAccept, +} + +var WebsocketUpgradeHeaders = []string{ + Upgrade, + Connection, + SecWebsocketAccept, +} From 1db9482a8e7e23df1fbe2aab159ac9ed68c1f6c7 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Thu, 27 Jul 2017 15:58:31 +0200 Subject: [PATCH 4/6] Prepare release v1.3.4 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dba24292f..b5a412e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [v1.3.4](https://github.com/containous/traefik/tree/v1.3.4) (2017-07-27) +[All Commits](https://github.com/containous/traefik/compare/v1.3.3...v1.3.4) + +**Bug fixes:** +- **[middleware]** Double compression. ([#1863](https://github.com/containous/traefik/pull/1863) by [ldez](https://github.com/ldez)) +- **[middleware]** Fix replace path rule ([#1859](https://github.com/containous/traefik/pull/1859) by [dedalusj](https://github.com/dedalusj)) +- **[websocket]** New oxy with gorilla for websocket with integration tests ([#1896](https://github.com/containous/traefik/pull/1896) by [Juliens](https://github.com/Juliens)) + ## [v1.3.3](https://github.com/containous/traefik/tree/v1.3.3) (2017-07-06) [All Commits](https://github.com/containous/traefik/compare/v1.3.2...v1.3.3) From 1b4dc3783c1c22f1d686acbccbbd5ae80fe4007e Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Tue, 1 Aug 2017 15:24:08 +0200 Subject: [PATCH 5/6] Oxy with fixes on websocket + integration tests --- glide.lock | 6 +- glide.yaml | 2 +- integration/glide.lock | 3 +- integration/glide.yaml | 1 + .../golang.org/x/net/websocket/client.go | 113 ++++ .../vendor/golang.org/x/net/websocket/hybi.go | 586 ++++++++++++++++++ .../golang.org/x/net/websocket/server.go | 113 ++++ .../golang.org/x/net/websocket/websocket.go | 412 ++++++++++++ integration/websocket_test.go | 214 ++++++- vendor/github.com/gorilla/websocket/doc.go | 13 +- vendor/github.com/gorilla/websocket/json.go | 11 +- vendor/github.com/gorilla/websocket/server.go | 7 +- vendor/github.com/gorilla/websocket/util.go | 4 +- vendor/github.com/vulcand/oxy/forward/fwd.go | 9 +- 14 files changed, 1463 insertions(+), 31 deletions(-) create mode 100644 integration/vendor/golang.org/x/net/websocket/client.go create mode 100644 integration/vendor/golang.org/x/net/websocket/hybi.go create mode 100644 integration/vendor/golang.org/x/net/websocket/server.go create mode 100644 integration/vendor/golang.org/x/net/websocket/websocket.go diff --git a/glide.lock b/glide.lock index 25b68c67e..6aa7c2810 100644 --- a/glide.lock +++ b/glide.lock @@ -1,4 +1,4 @@ -hash: bfc5801ed56be5f703a0924d8832dcccc42bf02f9e2b035ef77eab62c0cb4884 +hash: eddc889b1c0fc19ad91d886268e46ebdf85cd9e376e78b96b0d6661cc5842aca updated: 2017-06-29T16:47:14.848940186+02:00 imports: - name: cloud.google.com/go @@ -246,7 +246,7 @@ imports: - name: github.com/gorilla/context version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 - name: github.com/gorilla/websocket - version: a91eba7f97777409bc2c443f5534d41dd20c5720 + version: a69d9f6de432e2c6b296a947d8a5ee88f68522cf - name: github.com/hashicorp/consul version: 3f92cc70e8163df866873c16c6d89889b5c95fc4 subpackages: @@ -411,7 +411,7 @@ imports: - name: github.com/vdemeester/docker-events version: be74d4929ec1ad118df54349fda4b0cba60f849b - name: github.com/vulcand/oxy - version: 49f1894c20d972f5c73ff44b859f87deb83f0076 + version: 05a57556d820dc5c56721ebdefbafc15f36fac98 repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/glide.yaml b/glide.yaml index 295d6ef92..4ec7ea3a4 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,7 +8,7 @@ import: - package: github.com/cenk/backoff - package: github.com/containous/flaeg - package: github.com/vulcand/oxy - version: 49f1894c20d972f5c73ff44b859f87deb83f0076 + version: 05a57556d820dc5c56721ebdefbafc15f36fac98 repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/integration/glide.lock b/integration/glide.lock index 038f39076..d9f18e967 100644 --- a/integration/glide.lock +++ b/integration/glide.lock @@ -1,4 +1,4 @@ -hash: c53f57a45247b08a91f127ece494d49f1b7fee8c5f75be87ab12e27aa92d065f +hash: afc00e3aa064550eed7ff6a98b4eced543fe41e84894f1ac0ec25adf06354ec7 updated: 2016-11-17T16:23:56.727970904Z imports: - name: github.com/cenk/backoff @@ -286,6 +286,7 @@ testImports: - context - proxy - publicsuffix + - websocket - name: golang.org/x/sys version: 9c60d1c508f5134d1ca726b4641db998f2523357 subpackages: diff --git a/integration/glide.yaml b/integration/glide.yaml index d434ac336..c5d17b67a 100644 --- a/integration/glide.yaml +++ b/integration/glide.yaml @@ -29,5 +29,6 @@ testImport: - package: golang.org/x/net subpackages: - context + - websocket - package: github.com/spf13/pflag version: 5644820622454e71517561946e3d94b9f9db6842 diff --git a/integration/vendor/golang.org/x/net/websocket/client.go b/integration/vendor/golang.org/x/net/websocket/client.go new file mode 100644 index 000000000..20d1e1e38 --- /dev/null +++ b/integration/vendor/golang.org/x/net/websocket/client.go @@ -0,0 +1,113 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "crypto/tls" + "io" + "net" + "net/http" + "net/url" +) + +// DialError is an error that occurs while dialling a websocket server. +type DialError struct { + *Config + Err error +} + +func (e *DialError) Error() string { + return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error() +} + +// NewConfig creates a new WebSocket config for client connection. +func NewConfig(server, origin string) (config *Config, err error) { + config = new(Config) + config.Version = ProtocolVersionHybi13 + config.Location, err = url.ParseRequestURI(server) + if err != nil { + return + } + config.Origin, err = url.ParseRequestURI(origin) + if err != nil { + return + } + config.Header = http.Header(make(map[string][]string)) + return +} + +// NewClient creates a new WebSocket client connection over rwc. +func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) { + br := bufio.NewReader(rwc) + bw := bufio.NewWriter(rwc) + err = hybiClientHandshake(config, br, bw) + if err != nil { + return + } + buf := bufio.NewReadWriter(br, bw) + ws = newHybiClientConn(config, buf, rwc) + return +} + +// Dial opens a new client connection to a WebSocket. +func Dial(url_, protocol, origin string) (ws *Conn, err error) { + config, err := NewConfig(url_, origin) + if err != nil { + return nil, err + } + if protocol != "" { + config.Protocol = []string{protocol} + } + return DialConfig(config) +} + +var portMap = map[string]string{ + "ws": "80", + "wss": "443", +} + +func parseAuthority(location *url.URL) string { + if _, ok := portMap[location.Scheme]; ok { + if _, _, err := net.SplitHostPort(location.Host); err != nil { + return net.JoinHostPort(location.Host, portMap[location.Scheme]) + } + } + return location.Host +} + +// DialConfig opens a new client connection to a WebSocket with a config. +func DialConfig(config *Config) (ws *Conn, err error) { + var client net.Conn + if config.Location == nil { + return nil, &DialError{config, ErrBadWebSocketLocation} + } + if config.Origin == nil { + return nil, &DialError{config, ErrBadWebSocketOrigin} + } + switch config.Location.Scheme { + case "ws": + client, err = net.Dial("tcp", parseAuthority(config.Location)) + + case "wss": + client, err = tls.Dial("tcp", parseAuthority(config.Location), config.TlsConfig) + + default: + err = ErrBadScheme + } + if err != nil { + goto Error + } + + ws, err = NewClient(config, client) + if err != nil { + client.Close() + goto Error + } + return + +Error: + return nil, &DialError{config, err} +} diff --git a/integration/vendor/golang.org/x/net/websocket/hybi.go b/integration/vendor/golang.org/x/net/websocket/hybi.go new file mode 100644 index 000000000..60bbc8418 --- /dev/null +++ b/integration/vendor/golang.org/x/net/websocket/hybi.go @@ -0,0 +1,586 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +// This file implements a protocol of hybi draft. +// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +const ( + websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + closeStatusNormal = 1000 + closeStatusGoingAway = 1001 + closeStatusProtocolError = 1002 + closeStatusUnsupportedData = 1003 + closeStatusFrameTooLarge = 1004 + closeStatusNoStatusRcvd = 1005 + closeStatusAbnormalClosure = 1006 + closeStatusBadMessageData = 1007 + closeStatusPolicyViolation = 1008 + closeStatusTooBigData = 1009 + closeStatusExtensionMismatch = 1010 + + maxControlFramePayloadLength = 125 +) + +var ( + ErrBadMaskingKey = &ProtocolError{"bad masking key"} + ErrBadPongMessage = &ProtocolError{"bad pong message"} + ErrBadClosingStatus = &ProtocolError{"bad closing status"} + ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"} + ErrNotImplemented = &ProtocolError{"not implemented"} + + handshakeHeader = map[string]bool{ + "Host": true, + "Upgrade": true, + "Connection": true, + "Sec-Websocket-Key": true, + "Sec-Websocket-Origin": true, + "Sec-Websocket-Version": true, + "Sec-Websocket-Protocol": true, + "Sec-Websocket-Accept": true, + } +) + +// A hybiFrameHeader is a frame header as defined in hybi draft. +type hybiFrameHeader struct { + Fin bool + Rsv [3]bool + OpCode byte + Length int64 + MaskingKey []byte + + data *bytes.Buffer +} + +// A hybiFrameReader is a reader for hybi frame. +type hybiFrameReader struct { + reader io.Reader + + header hybiFrameHeader + pos int64 + length int +} + +func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) { + n, err = frame.reader.Read(msg) + if err != nil { + return 0, err + } + if frame.header.MaskingKey != nil { + for i := 0; i < n; i++ { + msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4] + frame.pos++ + } + } + return n, err +} + +func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode } + +func (frame *hybiFrameReader) HeaderReader() io.Reader { + if frame.header.data == nil { + return nil + } + if frame.header.data.Len() == 0 { + return nil + } + return frame.header.data +} + +func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil } + +func (frame *hybiFrameReader) Len() (n int) { return frame.length } + +// A hybiFrameReaderFactory creates new frame reader based on its frame type. +type hybiFrameReaderFactory struct { + *bufio.Reader +} + +// NewFrameReader reads a frame header from the connection, and creates new reader for the frame. +// See Section 5.2 Base Framing protocol for detail. +// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2 +func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) { + hybiFrame := new(hybiFrameReader) + frame = hybiFrame + var header []byte + var b byte + // First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0 + for i := 0; i < 3; i++ { + j := uint(6 - i) + hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0 + } + hybiFrame.header.OpCode = header[0] & 0x0f + + // Second byte. Mask/Payload len(7bits) + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + mask := (b & 0x80) != 0 + b &= 0x7f + lengthFields := 0 + switch { + case b <= 125: // Payload length 7bits. + hybiFrame.header.Length = int64(b) + case b == 126: // Payload length 7+16bits + lengthFields = 2 + case b == 127: // Payload length 7+64bits + lengthFields = 8 + } + for i := 0; i < lengthFields; i++ { + b, err = buf.ReadByte() + if err != nil { + return + } + if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits + b &= 0x7f + } + header = append(header, b) + hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) + } + if mask { + // Masking key. 4 bytes. + for i := 0; i < 4; i++ { + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b) + } + } + hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length) + hybiFrame.header.data = bytes.NewBuffer(header) + hybiFrame.length = len(header) + int(hybiFrame.header.Length) + return +} + +// A HybiFrameWriter is a writer for hybi frame. +type hybiFrameWriter struct { + writer *bufio.Writer + + header *hybiFrameHeader +} + +func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) { + var header []byte + var b byte + if frame.header.Fin { + b |= 0x80 + } + for i := 0; i < 3; i++ { + if frame.header.Rsv[i] { + j := uint(6 - i) + b |= 1 << j + } + } + b |= frame.header.OpCode + header = append(header, b) + if frame.header.MaskingKey != nil { + b = 0x80 + } else { + b = 0 + } + lengthFields := 0 + length := len(msg) + switch { + case length <= 125: + b |= byte(length) + case length < 65536: + b |= 126 + lengthFields = 2 + default: + b |= 127 + lengthFields = 8 + } + header = append(header, b) + for i := 0; i < lengthFields; i++ { + j := uint((lengthFields - i - 1) * 8) + b = byte((length >> j) & 0xff) + header = append(header, b) + } + if frame.header.MaskingKey != nil { + if len(frame.header.MaskingKey) != 4 { + return 0, ErrBadMaskingKey + } + header = append(header, frame.header.MaskingKey...) + frame.writer.Write(header) + data := make([]byte, length) + for i := range data { + data[i] = msg[i] ^ frame.header.MaskingKey[i%4] + } + frame.writer.Write(data) + err = frame.writer.Flush() + return length, err + } + frame.writer.Write(header) + frame.writer.Write(msg) + err = frame.writer.Flush() + return length, err +} + +func (frame *hybiFrameWriter) Close() error { return nil } + +type hybiFrameWriterFactory struct { + *bufio.Writer + needMaskingKey bool +} + +func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) { + frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType} + if buf.needMaskingKey { + frameHeader.MaskingKey, err = generateMaskingKey() + if err != nil { + return nil, err + } + } + return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil +} + +type hybiFrameHandler struct { + conn *Conn + payloadType byte +} + +func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) { + if handler.conn.IsServerConn() { + // The client MUST mask all frames sent to the server. + if frame.(*hybiFrameReader).header.MaskingKey == nil { + handler.WriteClose(closeStatusProtocolError) + return nil, io.EOF + } + } else { + // The server MUST NOT mask all frames. + if frame.(*hybiFrameReader).header.MaskingKey != nil { + handler.WriteClose(closeStatusProtocolError) + return nil, io.EOF + } + } + if header := frame.HeaderReader(); header != nil { + io.Copy(ioutil.Discard, header) + } + switch frame.PayloadType() { + case ContinuationFrame: + frame.(*hybiFrameReader).header.OpCode = handler.payloadType + case TextFrame, BinaryFrame: + handler.payloadType = frame.PayloadType() + case CloseFrame: + return nil, io.EOF + case PingFrame, PongFrame: + b := make([]byte, maxControlFramePayloadLength) + n, err := io.ReadFull(frame, b) + if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + return nil, err + } + io.Copy(ioutil.Discard, frame) + if frame.PayloadType() == PingFrame { + if _, err := handler.WritePong(b[:n]); err != nil { + return nil, err + } + } + return nil, nil + } + return frame, nil +} + +func (handler *hybiFrameHandler) WriteClose(status int) (err error) { + handler.conn.wio.Lock() + defer handler.conn.wio.Unlock() + w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame) + if err != nil { + return err + } + msg := make([]byte, 2) + binary.BigEndian.PutUint16(msg, uint16(status)) + _, err = w.Write(msg) + w.Close() + return err +} + +func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) { + handler.conn.wio.Lock() + defer handler.conn.wio.Unlock() + w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame) + if err != nil { + return 0, err + } + n, err = w.Write(msg) + w.Close() + return n, err +} + +// newHybiConn creates a new WebSocket connection speaking hybi draft protocol. +func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + if buf == nil { + br := bufio.NewReader(rwc) + bw := bufio.NewWriter(rwc) + buf = bufio.NewReadWriter(br, bw) + } + ws := &Conn{config: config, request: request, buf: buf, rwc: rwc, + frameReaderFactory: hybiFrameReaderFactory{buf.Reader}, + frameWriterFactory: hybiFrameWriterFactory{ + buf.Writer, request == nil}, + PayloadType: TextFrame, + defaultCloseStatus: closeStatusNormal} + ws.frameHandler = &hybiFrameHandler{conn: ws} + return ws +} + +// generateMaskingKey generates a masking key for a frame. +func generateMaskingKey() (maskingKey []byte, err error) { + maskingKey = make([]byte, 4) + if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil { + return + } + return +} + +// generateNonce generates a nonce consisting of a randomly selected 16-byte +// value that has been base64-encoded. +func generateNonce() (nonce []byte) { + key := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + panic(err) + } + nonce = make([]byte, 24) + base64.StdEncoding.Encode(nonce, key) + return +} + +// removeZone removes IPv6 zone identifer from host. +// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080" +func removeZone(host string) string { + if !strings.HasPrefix(host, "[") { + return host + } + i := strings.LastIndex(host, "]") + if i < 0 { + return host + } + j := strings.LastIndex(host[:i], "%") + if j < 0 { + return host + } + return host[:j] + host[i:] +} + +// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of +// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. +func getNonceAccept(nonce []byte) (expected []byte, err error) { + h := sha1.New() + if _, err = h.Write(nonce); err != nil { + return + } + if _, err = h.Write([]byte(websocketGUID)); err != nil { + return + } + expected = make([]byte, 28) + base64.StdEncoding.Encode(expected, h.Sum(nil)) + return +} + +// Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 +func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { + bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") + + // According to RFC 6874, an HTTP client, proxy, or other + // intermediary must remove any IPv6 zone identifier attached + // to an outgoing URI. + bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n") + bw.WriteString("Upgrade: websocket\r\n") + bw.WriteString("Connection: Upgrade\r\n") + nonce := generateNonce() + if config.handshakeData != nil { + nonce = []byte(config.handshakeData["key"]) + } + bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") + bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") + + if config.Version != ProtocolVersionHybi13 { + return ErrBadProtocolVersion + } + + bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") + if len(config.Protocol) > 0 { + bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") + } + // TODO(ukai): send Sec-WebSocket-Extensions. + err = config.Header.WriteSubset(bw, handshakeHeader) + if err != nil { + return err + } + + bw.WriteString("\r\n") + if err = bw.Flush(); err != nil { + return err + } + + resp, err := http.ReadResponse(br, &http.Request{Method: "GET"}) + if err != nil { + return err + } + if resp.StatusCode != 101 { + return ErrBadStatus + } + if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || + strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { + return ErrBadUpgrade + } + expectedAccept, err := getNonceAccept(nonce) + if err != nil { + return err + } + if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) { + return ErrChallengeResponse + } + if resp.Header.Get("Sec-WebSocket-Extensions") != "" { + return ErrUnsupportedExtensions + } + offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol") + if offeredProtocol != "" { + protocolMatched := false + for i := 0; i < len(config.Protocol); i++ { + if config.Protocol[i] == offeredProtocol { + protocolMatched = true + break + } + } + if !protocolMatched { + return ErrBadWebSocketProtocol + } + config.Protocol = []string{offeredProtocol} + } + + return nil +} + +// newHybiClientConn creates a client WebSocket connection after handshake. +func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { + return newHybiConn(config, buf, rwc, nil) +} + +// A HybiServerHandshaker performs a server handshake using hybi draft protocol. +type hybiServerHandshaker struct { + *Config + accept []byte +} + +func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) { + c.Version = ProtocolVersionHybi13 + if req.Method != "GET" { + return http.StatusMethodNotAllowed, ErrBadRequestMethod + } + // HTTP version can be safely ignored. + + if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || + !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { + return http.StatusBadRequest, ErrNotWebSocket + } + + key := req.Header.Get("Sec-Websocket-Key") + if key == "" { + return http.StatusBadRequest, ErrChallengeResponse + } + version := req.Header.Get("Sec-Websocket-Version") + switch version { + case "13": + c.Version = ProtocolVersionHybi13 + default: + return http.StatusBadRequest, ErrBadWebSocketVersion + } + var scheme string + if req.TLS != nil { + scheme = "wss" + } else { + scheme = "ws" + } + c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI()) + if err != nil { + return http.StatusBadRequest, err + } + protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol")) + if protocol != "" { + protocols := strings.Split(protocol, ",") + for i := 0; i < len(protocols); i++ { + c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i])) + } + } + c.accept, err = getNonceAccept([]byte(key)) + if err != nil { + return http.StatusInternalServerError, err + } + return http.StatusSwitchingProtocols, nil +} + +// Origin parses the Origin header in req. +// If the Origin header is not set, it returns nil and nil. +func Origin(config *Config, req *http.Request) (*url.URL, error) { + var origin string + switch config.Version { + case ProtocolVersionHybi13: + origin = req.Header.Get("Origin") + } + if origin == "" { + return nil, nil + } + return url.ParseRequestURI(origin) +} + +func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) { + if len(c.Protocol) > 0 { + if len(c.Protocol) != 1 { + // You need choose a Protocol in Handshake func in Server. + return ErrBadWebSocketProtocol + } + } + buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n") + buf.WriteString("Upgrade: websocket\r\n") + buf.WriteString("Connection: Upgrade\r\n") + buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n") + if len(c.Protocol) > 0 { + buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n") + } + // TODO(ukai): send Sec-WebSocket-Extensions. + if c.Header != nil { + err := c.Header.WriteSubset(buf, handshakeHeader) + if err != nil { + return err + } + } + buf.WriteString("\r\n") + return buf.Flush() +} + +func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + return newHybiServerConn(c.Config, buf, rwc, request) +} + +// newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol. +func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + return newHybiConn(config, buf, rwc, request) +} diff --git a/integration/vendor/golang.org/x/net/websocket/server.go b/integration/vendor/golang.org/x/net/websocket/server.go new file mode 100644 index 000000000..0895dea19 --- /dev/null +++ b/integration/vendor/golang.org/x/net/websocket/server.go @@ -0,0 +1,113 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "fmt" + "io" + "net/http" +) + +func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { + var hs serverHandshaker = &hybiServerHandshaker{Config: config} + code, err := hs.ReadHandshake(buf.Reader, req) + if err == ErrBadWebSocketVersion { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if err != nil { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if handshake != nil { + err = handshake(config, req) + if err != nil { + code = http.StatusForbidden + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + } + err = hs.AcceptHandshake(buf.Writer) + if err != nil { + code = http.StatusBadRequest + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + conn = hs.NewServerConn(buf, rwc, req) + return +} + +// Server represents a server of a WebSocket. +type Server struct { + // Config is a WebSocket configuration for new WebSocket connection. + Config + + // Handshake is an optional function in WebSocket handshake. + // For example, you can check, or don't check Origin header. + // Another example, you can select config.Protocol. + Handshake func(*Config, *http.Request) error + + // Handler handles a WebSocket connection. + Handler +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.serveWebSocket(w, req) +} + +func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { + rwc, buf, err := w.(http.Hijacker).Hijack() + if err != nil { + panic("Hijack failed: " + err.Error()) + } + // The server should abort the WebSocket connection if it finds + // the client did not send a handshake that matches with protocol + // specification. + defer rwc.Close() + conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) + if err != nil { + return + } + if conn == nil { + panic("unexpected nil conn") + } + s.Handler(conn) +} + +// Handler is a simple interface to a WebSocket browser client. +// It checks if Origin header is valid URL by default. +// You might want to verify websocket.Conn.Config().Origin in the func. +// If you use Server instead of Handler, you could call websocket.Origin and +// check the origin in your Handshake func. So, if you want to accept +// non-browser clients, which do not send an Origin header, set a +// Server.Handshake that does not check the origin. +type Handler func(*Conn) + +func checkOrigin(config *Config, req *http.Request) (err error) { + config.Origin, err = Origin(config, req) + if err == nil && config.Origin == nil { + return fmt.Errorf("null origin") + } + return err +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s := Server{Handler: h, Handshake: checkOrigin} + s.serveWebSocket(w, req) +} diff --git a/integration/vendor/golang.org/x/net/websocket/websocket.go b/integration/vendor/golang.org/x/net/websocket/websocket.go new file mode 100644 index 000000000..606840098 --- /dev/null +++ b/integration/vendor/golang.org/x/net/websocket/websocket.go @@ -0,0 +1,412 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements a client and server for the WebSocket protocol +// as specified in RFC 6455. +package websocket // import "golang.org/x/net/websocket" + +import ( + "bufio" + "crypto/tls" + "encoding/json" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "sync" + "time" +) + +const ( + ProtocolVersionHybi13 = 13 + ProtocolVersionHybi = ProtocolVersionHybi13 + SupportedProtocolVersion = "13" + + ContinuationFrame = 0 + TextFrame = 1 + BinaryFrame = 2 + CloseFrame = 8 + PingFrame = 9 + PongFrame = 10 + UnknownFrame = 255 +) + +// ProtocolError represents WebSocket protocol errors. +type ProtocolError struct { + ErrorString string +} + +func (err *ProtocolError) Error() string { return err.ErrorString } + +var ( + ErrBadProtocolVersion = &ProtocolError{"bad protocol version"} + ErrBadScheme = &ProtocolError{"bad scheme"} + ErrBadStatus = &ProtocolError{"bad status"} + ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"} + ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"} + ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"} + ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"} + ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"} + ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"} + ErrBadFrame = &ProtocolError{"bad frame"} + ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"} + ErrNotWebSocket = &ProtocolError{"not websocket protocol"} + ErrBadRequestMethod = &ProtocolError{"bad method"} + ErrNotSupported = &ProtocolError{"not supported"} +) + +// Addr is an implementation of net.Addr for WebSocket. +type Addr struct { + *url.URL +} + +// Network returns the network type for a WebSocket, "websocket". +func (addr *Addr) Network() string { return "websocket" } + +// Config is a WebSocket configuration +type Config struct { + // A WebSocket server address. + Location *url.URL + + // A Websocket client origin. + Origin *url.URL + + // WebSocket subprotocols. + Protocol []string + + // WebSocket protocol version. + Version int + + // TLS config for secure WebSocket (wss). + TlsConfig *tls.Config + + // Additional header fields to be sent in WebSocket opening handshake. + Header http.Header + + handshakeData map[string]string +} + +// serverHandshaker is an interface to handle WebSocket server side handshake. +type serverHandshaker interface { + // ReadHandshake reads handshake request message from client. + // Returns http response code and error if any. + ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) + + // AcceptHandshake accepts the client handshake request and sends + // handshake response back to client. + AcceptHandshake(buf *bufio.Writer) (err error) + + // NewServerConn creates a new WebSocket connection. + NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn) +} + +// frameReader is an interface to read a WebSocket frame. +type frameReader interface { + // Reader is to read payload of the frame. + io.Reader + + // PayloadType returns payload type. + PayloadType() byte + + // HeaderReader returns a reader to read header of the frame. + HeaderReader() io.Reader + + // TrailerReader returns a reader to read trailer of the frame. + // If it returns nil, there is no trailer in the frame. + TrailerReader() io.Reader + + // Len returns total length of the frame, including header and trailer. + Len() int +} + +// frameReaderFactory is an interface to creates new frame reader. +type frameReaderFactory interface { + NewFrameReader() (r frameReader, err error) +} + +// frameWriter is an interface to write a WebSocket frame. +type frameWriter interface { + // Writer is to write payload of the frame. + io.WriteCloser +} + +// frameWriterFactory is an interface to create new frame writer. +type frameWriterFactory interface { + NewFrameWriter(payloadType byte) (w frameWriter, err error) +} + +type frameHandler interface { + HandleFrame(frame frameReader) (r frameReader, err error) + WriteClose(status int) (err error) +} + +// Conn represents a WebSocket connection. +type Conn struct { + config *Config + request *http.Request + + buf *bufio.ReadWriter + rwc io.ReadWriteCloser + + rio sync.Mutex + frameReaderFactory + frameReader + + wio sync.Mutex + frameWriterFactory + + frameHandler + PayloadType byte + defaultCloseStatus int +} + +// Read implements the io.Reader interface: +// it reads data of a frame from the WebSocket connection. +// if msg is not large enough for the frame data, it fills the msg and next Read +// will read the rest of the frame data. +// it reads Text frame or Binary frame. +func (ws *Conn) Read(msg []byte) (n int, err error) { + ws.rio.Lock() + defer ws.rio.Unlock() +again: + if ws.frameReader == nil { + frame, err := ws.frameReaderFactory.NewFrameReader() + if err != nil { + return 0, err + } + ws.frameReader, err = ws.frameHandler.HandleFrame(frame) + if err != nil { + return 0, err + } + if ws.frameReader == nil { + goto again + } + } + n, err = ws.frameReader.Read(msg) + if err == io.EOF { + if trailer := ws.frameReader.TrailerReader(); trailer != nil { + io.Copy(ioutil.Discard, trailer) + } + ws.frameReader = nil + goto again + } + return n, err +} + +// Write implements the io.Writer interface: +// it writes data as a frame to the WebSocket connection. +func (ws *Conn) Write(msg []byte) (n int, err error) { + ws.wio.Lock() + defer ws.wio.Unlock() + w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType) + if err != nil { + return 0, err + } + n, err = w.Write(msg) + w.Close() + if err != nil { + return n, err + } + return n, err +} + +// Close implements the io.Closer interface. +func (ws *Conn) Close() error { + err := ws.frameHandler.WriteClose(ws.defaultCloseStatus) + err1 := ws.rwc.Close() + if err != nil { + return err + } + return err1 +} + +func (ws *Conn) IsClientConn() bool { return ws.request == nil } +func (ws *Conn) IsServerConn() bool { return ws.request != nil } + +// LocalAddr returns the WebSocket Origin for the connection for client, or +// the WebSocket location for server. +func (ws *Conn) LocalAddr() net.Addr { + if ws.IsClientConn() { + return &Addr{ws.config.Origin} + } + return &Addr{ws.config.Location} +} + +// RemoteAddr returns the WebSocket location for the connection for client, or +// the Websocket Origin for server. +func (ws *Conn) RemoteAddr() net.Addr { + if ws.IsClientConn() { + return &Addr{ws.config.Location} + } + return &Addr{ws.config.Origin} +} + +var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn") + +// SetDeadline sets the connection's network read & write deadlines. +func (ws *Conn) SetDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetDeadline(t) + } + return errSetDeadline +} + +// SetReadDeadline sets the connection's network read deadline. +func (ws *Conn) SetReadDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetReadDeadline(t) + } + return errSetDeadline +} + +// SetWriteDeadline sets the connection's network write deadline. +func (ws *Conn) SetWriteDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetWriteDeadline(t) + } + return errSetDeadline +} + +// Config returns the WebSocket config. +func (ws *Conn) Config() *Config { return ws.config } + +// Request returns the http request upgraded to the WebSocket. +// It is nil for client side. +func (ws *Conn) Request() *http.Request { return ws.request } + +// Codec represents a symmetric pair of functions that implement a codec. +type Codec struct { + Marshal func(v interface{}) (data []byte, payloadType byte, err error) + Unmarshal func(data []byte, payloadType byte, v interface{}) (err error) +} + +// Send sends v marshaled by cd.Marshal as single frame to ws. +func (cd Codec) Send(ws *Conn, v interface{}) (err error) { + data, payloadType, err := cd.Marshal(v) + if err != nil { + return err + } + ws.wio.Lock() + defer ws.wio.Unlock() + w, err := ws.frameWriterFactory.NewFrameWriter(payloadType) + if err != nil { + return err + } + _, err = w.Write(data) + w.Close() + return err +} + +// Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores in v. +func (cd Codec) Receive(ws *Conn, v interface{}) (err error) { + ws.rio.Lock() + defer ws.rio.Unlock() + if ws.frameReader != nil { + _, err = io.Copy(ioutil.Discard, ws.frameReader) + if err != nil { + return err + } + ws.frameReader = nil + } +again: + frame, err := ws.frameReaderFactory.NewFrameReader() + if err != nil { + return err + } + frame, err = ws.frameHandler.HandleFrame(frame) + if err != nil { + return err + } + if frame == nil { + goto again + } + payloadType := frame.PayloadType() + data, err := ioutil.ReadAll(frame) + if err != nil { + return err + } + return cd.Unmarshal(data, payloadType, v) +} + +func marshal(v interface{}) (msg []byte, payloadType byte, err error) { + switch data := v.(type) { + case string: + return []byte(data), TextFrame, nil + case []byte: + return data, BinaryFrame, nil + } + return nil, UnknownFrame, ErrNotSupported +} + +func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) { + switch data := v.(type) { + case *string: + *data = string(msg) + return nil + case *[]byte: + *data = msg + return nil + } + return ErrNotSupported +} + +/* +Message is a codec to send/receive text/binary data in a frame on WebSocket connection. +To send/receive text frame, use string type. +To send/receive binary frame, use []byte type. + +Trivial usage: + + import "websocket" + + // receive text frame + var message string + websocket.Message.Receive(ws, &message) + + // send text frame + message = "hello" + websocket.Message.Send(ws, message) + + // receive binary frame + var data []byte + websocket.Message.Receive(ws, &data) + + // send binary frame + data = []byte{0, 1, 2} + websocket.Message.Send(ws, data) + +*/ +var Message = Codec{marshal, unmarshal} + +func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) { + msg, err = json.Marshal(v) + return msg, TextFrame, err +} + +func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) { + return json.Unmarshal(msg, v) +} + +/* +JSON is a codec to send/receive JSON data in a frame from a WebSocket connection. + +Trivial usage: + + import "websocket" + + type T struct { + Msg string + Count int + } + + // receive JSON type T + var data T + websocket.JSON.Receive(ws, &data) + + // send JSON type T + websocket.JSON.Send(ws, data) +*/ +var JSON = Codec{jsonMarshal, jsonUnmarshal} diff --git a/integration/websocket_test.go b/integration/websocket_test.go index f852b6895..a8550c28c 100644 --- a/integration/websocket_test.go +++ b/integration/websocket_test.go @@ -1,27 +1,27 @@ package main import ( - "net/http" - "net/http/httptest" - "time" - - "github.com/go-check/check" - "errors" "io/ioutil" + "net" + "net/http" + "net/http/httptest" "os" "strings" + "time" "github.com/containous/traefik/integration/utils" - "github.com/gorilla/websocket" + "github.com/go-check/check" + gorillawebsocket "github.com/gorilla/websocket" checker "github.com/vdemeester/shakers" + "golang.org/x/net/websocket" ) // WebsocketSuite type WebsocketSuite struct{ BaseSuite } func (suite *WebsocketSuite) TestBase(c *check.C) { - var upgrader = websocket.Upgrader{} // use default options + var upgrader = gorillawebsocket.Upgrader{} // use default options srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) @@ -68,10 +68,10 @@ func (suite *WebsocketSuite) TestBase(c *check.C) { }) c.Assert(err, checker.IsNil) - conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil) + conn, _, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil) c.Assert(err, checker.IsNil) - conn.WriteMessage(websocket.TextMessage, []byte("OK")) + conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK")) _, msg, err := conn.ReadMessage() c.Assert(err, checker.IsNil) @@ -79,3 +79,197 @@ func (suite *WebsocketSuite) TestBase(c *check.C) { c.Assert(string(msg), checker.Equals, "OK") } + +func (suite *WebsocketSuite) TestWrongOrigin(c *check.C) { + var upgrader = gorillawebsocket.Upgrader{} // use default options + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer c.Close() + for { + mt, message, err := c.ReadMessage() + if err != nil { + break + } + err = c.WriteMessage(mt, message) + if err != nil { + break + } + } + })) + + file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct { + WebsocketServer string + }{ + WebsocketServer: srv.URL, + }) + + defer os.Remove(file) + cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug") + + err := cmd.Start() + + c.Assert(err, check.IsNil) + defer cmd.Process.Kill() + + // wait for traefik + err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error { + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + if !strings.Contains(string(body), "127.0.0.1") { + return errors.New("Incorrect traefik config") + } + return nil + }) + c.Assert(err, checker.IsNil) + + config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:800") + c.Assert(err, check.IsNil) + + conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second) + _, err = websocket.NewClient(config, conn) + c.Assert(err, checker.NotNil) + c.Assert(err, checker.ErrorMatches, "bad status") + +} + +func (suite *WebsocketSuite) TestOrigin(c *check.C) { + var upgrader = gorillawebsocket.Upgrader{} // use default options + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer c.Close() + for { + mt, message, err := c.ReadMessage() + if err != nil { + break + } + err = c.WriteMessage(mt, message) + if err != nil { + break + } + } + })) + + file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct { + WebsocketServer string + }{ + WebsocketServer: srv.URL, + }) + + defer os.Remove(file) + cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug") + + err := cmd.Start() + + c.Assert(err, check.IsNil) + defer cmd.Process.Kill() + + // wait for traefik + err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error { + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + if !strings.Contains(string(body), "127.0.0.1") { + return errors.New("Incorrect traefik config") + } + return nil + }) + c.Assert(err, checker.IsNil) + + config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:8000") + c.Assert(err, check.IsNil) + + conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second) + client, err := websocket.NewClient(config, conn) + c.Assert(err, checker.IsNil) + + n, err := client.Write([]byte("OK")) + c.Assert(err, checker.IsNil) + c.Assert(n, checker.Equals, 2) + + msg := make([]byte, 2) + n, err = client.Read(msg) + c.Assert(err, checker.IsNil) + c.Assert(n, checker.Equals, 2) + c.Assert(string(msg), checker.Equals, "OK") + +} + +func (suite *WebsocketSuite) TestWrongOriginIgnoredByServer(c *check.C) { + var upgrader = gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool { + return true + }} + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer c.Close() + for { + mt, message, err := c.ReadMessage() + if err != nil { + break + } + err = c.WriteMessage(mt, message) + if err != nil { + break + } + } + })) + + file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct { + WebsocketServer string + }{ + WebsocketServer: srv.URL, + }) + + defer os.Remove(file) + cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug") + + err := cmd.Start() + + c.Assert(err, check.IsNil) + defer cmd.Process.Kill() + + // wait for traefik + err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error { + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + if !strings.Contains(string(body), "127.0.0.1") { + return errors.New("Incorrect traefik config") + } + return nil + }) + c.Assert(err, checker.IsNil) + + config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:80") + c.Assert(err, check.IsNil) + + conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second) + client, err := websocket.NewClient(config, conn) + c.Assert(err, checker.IsNil) + + n, err := client.Write([]byte("OK")) + c.Assert(err, checker.IsNil) + c.Assert(n, checker.Equals, 2) + + msg := make([]byte, 2) + n, err = client.Read(msg) + c.Assert(err, checker.IsNil) + c.Assert(n, checker.Equals, 2) + c.Assert(string(msg), checker.Equals, "OK") + +} diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go index e291a952c..f5ff0823d 100644 --- a/vendor/github.com/gorilla/websocket/doc.go +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -6,9 +6,8 @@ // // Overview // -// The Conn type represents a WebSocket connection. A server application uses -// the Upgrade function from an Upgrader object with a HTTP request handler -// to get a pointer to a Conn: +// The Conn type represents a WebSocket connection. A server application calls +// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn: // // var upgrader = websocket.Upgrader{ // ReadBufferSize: 1024, @@ -33,7 +32,7 @@ // if err != nil { // return // } -// if err = conn.WriteMessage(messageType, p); err != nil { +// if err := conn.WriteMessage(messageType, p); err != nil { // return err // } // } @@ -147,9 +146,9 @@ // CheckOrigin: func(r *http.Request) bool { return true }, // } // -// The deprecated Upgrade function does not enforce an origin policy. It's the -// application's responsibility to check the Origin header before calling -// Upgrade. +// The deprecated package-level Upgrade function does not perform origin +// checking. The application is responsible for checking the Origin header +// before calling the Upgrade function. // // Compression EXPERIMENTAL // diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go index 4f0e36875..dc2c1f641 100644 --- a/vendor/github.com/gorilla/websocket/json.go +++ b/vendor/github.com/gorilla/websocket/json.go @@ -9,12 +9,14 @@ import ( "io" ) -// WriteJSON is deprecated, use c.WriteJSON instead. +// WriteJSON writes the JSON encoding of v as a message. +// +// Deprecated: Use c.WriteJSON instead. func WriteJSON(c *Conn, v interface{}) error { return c.WriteJSON(v) } -// WriteJSON writes the JSON encoding of v to the connection. +// WriteJSON writes the JSON encoding of v as a message. // // See the documentation for encoding/json Marshal for details about the // conversion of Go values to JSON. @@ -31,7 +33,10 @@ func (c *Conn) WriteJSON(v interface{}) error { return err2 } -// ReadJSON is deprecated, use c.ReadJSON instead. +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// Deprecated: Use c.ReadJSON instead. func ReadJSON(c *Conn, v interface{}) error { return c.ReadJSON(v) } diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index 3495e0f1a..6ae97c54f 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -230,10 +230,11 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // -// This function is deprecated, use websocket.Upgrader instead. +// Deprecated: Use websocket.Upgrader instead. // -// The application is responsible for checking the request origin before -// calling Upgrade. An example implementation of the same origin policy is: +// Upgrade does not perform origin checking. The application is responsible for +// checking the Origin header before calling Upgrade. An example implementation +// of the same origin policy check is: // // if req.Header.Get("Origin") != "http://"+req.Host { // http.Error(w, "Origin not allowed", 403) diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go index 9a4908df2..262e647bc 100644 --- a/vendor/github.com/gorilla/websocket/util.go +++ b/vendor/github.com/gorilla/websocket/util.go @@ -111,14 +111,14 @@ func nextTokenOrQuoted(s string) (value string, rest string) { case escape: escape = false p[j] = b - j += 1 + j++ case b == '\\': escape = true case b == '"': return string(p[:j]), s[i+1:] default: p[j] = b - j += 1 + j++ } } return "", "" diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index c0c7da368..e2845afee 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -265,7 +265,12 @@ func (f *websocketForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx.errHandler.ServeHTTP(w, req, err) return } - upgrader := websocket.Upgrader{} + + //Only the targetConn choose to CheckOrigin or not + upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { + return true + }} + utils.RemoveHeaders(resp.Header, WebsocketUpgradeHeaders...) underlyingConn, err := upgrader.Upgrade(w, req, resp.Header) if err != nil { @@ -318,6 +323,8 @@ func (f *websocketForwarder) copyRequest(req *http.Request, u *url.URL) (outReq outReq.URL.RawQuery = "" outReq.Header = make(http.Header) + //gorilla websocket use this header to set the request.Host tested in checkSameOrigin + outReq.Header.Set("Host", outReq.Host) utils.CopyHeaders(outReq.Header, req.Header) utils.RemoveHeaders(outReq.Header, WebsocketDialHeaders...) From 413ed62933e495cea021888150d5c2da8a040e40 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 1 Aug 2017 17:43:37 +0200 Subject: [PATCH 6/6] Prepare release v1.3.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a412e61..57d99f6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [v1.3.5](https://github.com/containous/traefik/tree/v1.3.5) (2017-08-01) +[All Commits](https://github.com/containous/traefik/compare/v1.3.4...v1.3.5) + +**Bug fixes:** +- **[websocket]** Oxy with fixes on websocket + integration tests ([#1905](https://github.com/containous/traefik/pull/1905) by [Juliens](https://github.com/Juliens)) + ## [v1.3.4](https://github.com/containous/traefik/tree/v1.3.4) (2017-07-27) [All Commits](https://github.com/containous/traefik/compare/v1.3.3...v1.3.4)