pax_global_header00006660000000000000000000000064141432741050014513gustar00rootroot0000000000000052 comment=e2b1424d8982f08dbbaee29fc3c38dac230dd719 sockjs-go-3.0.2/000077500000000000000000000000001414327410500134145ustar00rootroot00000000000000sockjs-go-3.0.2/.gitignore000066400000000000000000000004421414327410500154040ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so *.swp # Folders _obj _test .idea .DS_Store # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.coverprofile sockjs-go-3.0.2/.travis.yml000066400000000000000000000006421414327410500155270ustar00rootroot00000000000000language: go go: - "1.14.x" before_install: - cd v3 - go get golang.org/x/tools/cmd/cover - go get github.com/mattn/goveralls - go get github.com/golangci/golangci-lint/cmd/golangci-lint after_success: - go test ./... -coverprofile=profile.out -covermode=count - PATH=$HOME/gopath/bin:$PATH goveralls -coverprofile=profile.out -service=travis-ci script: - golangci-lint run - go test ./... -race sockjs-go-3.0.2/LICENSE000066400000000000000000000027431414327410500144270ustar00rootroot00000000000000Copyright (c) 2012-2020, sockjs-go authors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sockjs-go-3.0.2/README.md000066400000000000000000000074141414327410500147010ustar00rootroot00000000000000[![Build Status](https://api.travis-ci.org/igm/sockjs-go.svg?branch=master)](https://travis-ci.org/igm/sockjs-go) [![GoDoc](https://godoc.org/github.com/igm/sockjs-go/v3/sockjs?status.svg)](https://pkg.go.dev/github.com/igm/sockjs-go/v3/sockjs?tab=doc) [![Coverage Status](https://coveralls.io/repos/github/igm/sockjs-go/badge.svg?branch=master)](https://coveralls.io/github/igm/sockjs-go?branch=master) What is SockJS? = SockJS is a JavaScript library (for browsers) that provides a WebSocket-like object. SockJS gives you a coherent, cross-browser, Javascript API which creates a low latency, full duplex, cross-domain communication channel between the browser and the web server, with WebSockets or without. This necessitates the use of a server, which this is one version of, for GO. SockJS-Go server library = SockJS-Go is a [SockJS](https://github.com/sockjs/sockjs-client) server library written in Go. For latest **v3** version of `sockjs-go` use: github.com/igm/sockjs-go/v3/sockjs For **v2** version of `sockjs-go` use: gopkg.in/igm/sockjs-go.v2/sockjs Using version **v1** is not recommended (DEPRECATED) gopkg.in/igm/sockjs-go.v1/sockjs Note: using `github.com/igm/sockjs-go/sockjs` is not recommended. It exists for backwards compatibility reasons and is not maintained. Versioning - SockJS-Go project adopted [gopkg.in](http://gopkg.in) approach for versioning. SockJS-Go library details can be found [here](https://gopkg.in/igm/sockjs-go.v2/sockjs) With the introduction of go modules a new version `v3` is developed and maintained in the `master` and has new import part `github.com/igm/sockjs-go/v3/sockjs`. Example - A simple echo sockjs server: ```go package main import ( "log" "net/http" "github.com/igm/sockjs-go/v3/sockjs" ) func main() { handler := sockjs.NewHandler("/echo", sockjs.DefaultOptions, echoHandler) log.Fatal(http.ListenAndServe(":8081", handler)) } func echoHandler(session sockjs.Session) { for { if msg, err := session.Recv(); err == nil { session.Send(msg) continue } break } } ``` SockJS Protocol Tests Status - SockJS defines a set of [protocol tests](https://github.com/sockjs/sockjs-protocol) to quarantee a server compatibility with sockjs client library and various browsers. SockJS-Go server library aims to provide full compatibility, however there are couple of tests that don't and probably will never pass due to reasons explained in table below: | Failing Test | Explanation | | -------------| ------------| | **XhrPolling.test_transport** | does not pass due to a feature in net/http that does not send content-type header in case of StatusNoContent response code (even if explicitly set in the code), [details](https://code.google.com/p/go/source/detail?r=902dc062bff8) | | **WebSocket.** | Sockjs Go version supports RFC 6455, draft protocols hixie-76, hybi-10 are not supported | | **JSONEncoding** | As menioned in [browser quirks](https://github.com/sockjs/sockjs-client#browser-quirks) section: "it's advisable to use only valid characters. Using invalid characters is a bit slower, and may not work with SockJS servers that have a proper Unicode support." Go lang has a proper Unicode support | | **RawWebsocket.** | The sockjs protocol tests use old WebSocket client library (hybi-10) that does not support RFC 6455 properly | WebSocket - As mentioned above sockjs-go library is compatible with RFC 6455. That means the browsers not supporting RFC 6455 are not supported properly. There are no plans to support draft versions of WebSocket protocol. The WebSocket support is based on [Gorilla web toolkit](http://www.gorillatoolkit.org/pkg/websocket) implementation of WebSocket. For detailed information about browser versions supporting RFC 6455 see this [wiki page](http://en.wikipedia.org/wiki/WebSocket#Browser_support). sockjs-go-3.0.2/examples/000077500000000000000000000000001414327410500152325ustar00rootroot00000000000000sockjs-go-3.0.2/examples/webchat/000077500000000000000000000000001414327410500166475ustar00rootroot00000000000000sockjs-go-3.0.2/examples/webchat/README.md000066400000000000000000000003101414327410500201200ustar00rootroot00000000000000# Chat Example Simple sockjs chat example. ## Run ```shell $ go run webchat.go ``` Navigate using web browser: http://127.0.0.1:8080 Open multiple windows with the same URL and see how chat works. sockjs-go-3.0.2/examples/webchat/web/000077500000000000000000000000001414327410500174245ustar00rootroot00000000000000sockjs-go-3.0.2/examples/webchat/web/app.js000066400000000000000000000014121414327410500205400ustar00rootroot00000000000000if (!window.location.origin) { // Some browsers (mainly IE) do not have this property, so we need to build it manually... window.location.origin = window.location.protocol + '//' + window.location.hostname + (window.location.port ? (':' + window.location.port) : ''); } var sock = new SockJS(window.location.origin+'/echo') sock.onopen = function() { // console.log('connection open'); document.getElementById("status").innerHTML = "connected"; document.getElementById("send").disabled=false; }; sock.onmessage = function(e) { document.getElementById("output").value += e.data +"\n"; }; sock.onclose = function() { // console.log('connection closed'); document.getElementById("status").innerHTML = "disconnected"; document.getElementById("send").disabled=true; }; sockjs-go-3.0.2/examples/webchat/web/index.html000066400000000000000000000012421414327410500214200ustar00rootroot00000000000000 Chat Web Example

Chat - Web Example

Input text:

Messages from server:

status: connecting... sockjs-go-3.0.2/examples/webchat/webchat.go000066400000000000000000000017501414327410500206160ustar00rootroot00000000000000package main import ( "log" "net/http" "github.com/igm/pubsub" "github.com/igm/sockjs-go/v3/sockjs" ) var chat pubsub.Publisher func main() { http.Handle("/echo/", sockjs.NewHandler("/echo", sockjs.DefaultOptions, echoHandler)) http.Handle("/", http.FileServer(http.Dir("web/"))) log.Println("Server started on port: 8080") log.Fatal(http.ListenAndServe(":8080", nil)) } func echoHandler(session sockjs.Session) { log.Println("new sockjs session established") var closedSession = make(chan struct{}) chat.Publish("[info] new participant joined chat") defer chat.Publish("[info] participant left chat") go func() { reader, _ := chat.SubChannel(nil) for { select { case <-closedSession: return case msg := <-reader: if err := session.Send(msg.(string)); err != nil { return } } } }() for { if msg, err := session.Recv(); err == nil { chat.Publish(msg) continue } break } close(closedSession) log.Println("sockjs session closed") } sockjs-go-3.0.2/examples/webecho/000077500000000000000000000000001414327410500166465ustar00rootroot00000000000000sockjs-go-3.0.2/examples/webecho/README.md000066400000000000000000000002071414327410500201240ustar00rootroot00000000000000# Echo Example Simple echo sockjs example. ## Run ```shell $ go run webecho.go ``` Navigate using web browser: http://127.0.0.1:8080 sockjs-go-3.0.2/examples/webecho/web/000077500000000000000000000000001414327410500174235ustar00rootroot00000000000000sockjs-go-3.0.2/examples/webecho/web/app.js000066400000000000000000000022161414327410500205420ustar00rootroot00000000000000if (!window.location.origin) { // Some browsers (mainly IE) do not have this property, so we need to build it manually... window.location.origin = window.location.protocol + '//' + window.location.hostname + (window.location.port ? (':' + window.location.port) : ''); } var origin = window.location.origin; // options usage example var options = { debug: true, devel: true, protocols_whitelist: ['websocket', 'xdr-streaming', 'xhr-streaming', 'iframe-eventsource', 'iframe-htmlfile', 'xdr-polling', 'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling'] }; var sock = new SockJS(origin+'/echo', undefined, options); sock.onopen = function() { //console.log('connection open'); document.getElementById("status").innerHTML = "connected"; document.getElementById("send").disabled=false; }; sock.onmessage = function(e) { document.getElementById("output").value += e.data +"\n"; }; sock.onclose = function() { document.getElementById("status").innerHTML = "connection closed"; //console.log('connection closed'); }; function send() { text = document.getElementById("input").value; sock.send(document.getElementById("input").value); return false; } sockjs-go-3.0.2/examples/webecho/web/index.html000066400000000000000000000012421414327410500214170ustar00rootroot00000000000000 Echo Web Example

Echo - Web Example

Input text:

Messages from server:

status: connecting... sockjs-go-3.0.2/examples/webecho/webecho.go000066400000000000000000000014301414327410500206070ustar00rootroot00000000000000package main import ( "flag" "log" "net/http" "github.com/igm/sockjs-go/v3/sockjs" ) var ( websocket = flag.Bool("websocket", true, "enable/disable websocket protocol") ) func init() { flag.Parse() } func main() { opts := sockjs.DefaultOptions opts.Websocket = *websocket handler := sockjs.NewHandler("/echo", opts, echoHandler) http.Handle("/echo/", handler) http.Handle("/", http.FileServer(http.Dir("web/"))) log.Println("Server started on port: 8080") log.Fatal(http.ListenAndServe(":8080", nil)) } func echoHandler(session sockjs.Session) { log.Println("new sockjs session established") for { if msg, err := session.Recv(); err == nil { if err := session.Send(msg); err != nil { break } continue } break } log.Println("sockjs session closed") } sockjs-go-3.0.2/sockjs/000077500000000000000000000000001414327410500147105ustar00rootroot00000000000000sockjs-go-3.0.2/sockjs/.gitignore000066400000000000000000000000411414327410500166730ustar00rootroot00000000000000*.swp profile.out *.coverprofile sockjs-go-3.0.2/sockjs/README.md000066400000000000000000000000641414327410500161670ustar00rootroot00000000000000see [README](../README.md) for proper import paths sockjs-go-3.0.2/sockjs/benchmarks_test.go000066400000000000000000000065011414327410500204150ustar00rootroot00000000000000package sockjs import ( "bufio" "flag" "fmt" "log" "net/http" "net/http/httptest" "net/url" "strings" "sync" "testing" "time" "github.com/gorilla/websocket" ) func BenchmarkSimple(b *testing.B) { var messages = make(chan string, 10) h := NewHandler("/echo", DefaultOptions, func(session Session) { for m := range messages { session.Send(m) } session.Close(1024, "Close") }) server := httptest.NewServer(h) defer server.Close() req, _ := http.NewRequest("POST", server.URL+fmt.Sprintf("/echo/server/%d/xhr_streaming", 1000), nil) resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } for n := 0; n < b.N; n++ { messages <- "some message" } fmt.Println(b.N) close(messages) resp.Body.Close() } func BenchmarkMessages(b *testing.B) { msg := strings.Repeat("m", 10) h := NewHandler("/echo", DefaultOptions, func(session Session) { for n := 0; n < b.N; n++ { session.Send(msg) } session.Close(1024, "Close") }) server := httptest.NewServer(h) var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(session int) { reqc := 0 req, _ := http.NewRequest("POST", server.URL+fmt.Sprintf("/echo/server/%d/xhr_streaming", session), nil) for { reqc++ resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } reader := bufio.NewReader(resp.Body) for { line, err := reader.ReadString('\n') if err != nil { goto AGAIN } if strings.HasPrefix(line, "data: c[1024") { resp.Body.Close() goto DONE } } AGAIN: resp.Body.Close() } DONE: wg.Done() }(i) } wg.Wait() server.Close() } var size = flag.Int("size", 4*1024, "Size of one message.") func BenchmarkMessageWebsocket(b *testing.B) { flag.Parse() msg := strings.Repeat("x", *size) wsFrame := []byte(fmt.Sprintf("[%q]", msg)) opts := Options{ Websocket: true, SockJSURL: "//cdnjs.cloudflare.com/ajax/libs/sockjs-client/0.3.4/sockjs.min.js", HeartbeatDelay: time.Hour, DisconnectDelay: time.Hour, ResponseLimit: uint32(*size), } h := NewHandler("/echo", opts, func(session Session) { for { msg, err := session.Recv() if err != nil { if session.GetSessionState() != SessionActive { break } b.Fatalf("Recv()=%s", err) } if err := session.Send(msg); err != nil { b.Fatalf("Send()=%s", err) } } }) server := httptest.NewServer(h) defer server.Close() url := "ws" + server.URL[4:] + "/echo/server/0/websocket" client, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { b.Fatalf("Dial()=%s", err) } _, p, err := client.ReadMessage() if err != nil || string(p) != "o" { b.Fatalf("failed to start new session: frame=%v, err=%v", p, err) } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if err := client.WriteMessage(websocket.TextMessage, wsFrame); err != nil { b.Fatalf("WriteMessage()=%s", err) } if _, _, err := client.ReadMessage(); err != nil { b.Fatalf("ReadMessage()=%s", err) } } if err := client.Close(); err != nil { b.Fatalf("Close()=%s", err) } } func BenchmarkHandler_ParseSessionID(b *testing.B) { h := handler{prefix: "/prefix"} url, _ := url.Parse("http://server:80/prefix/server/session/whatever") b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { h.parseSessionID(url) } } sockjs-go-3.0.2/sockjs/buffer.go000066400000000000000000000014631414327410500165140ustar00rootroot00000000000000package sockjs import "sync" // messageBuffer is an unbounded buffer that blocks on // pop if it's empty until the new element is enqueued. type messageBuffer struct { popCh chan string closeCh chan struct{} once sync.Once // for b.close() } func newMessageBuffer() *messageBuffer { return &messageBuffer{ popCh: make(chan string), closeCh: make(chan struct{}), } } func (b *messageBuffer) push(messages ...string) error { for _, message := range messages { select { case b.popCh <- message: case <-b.closeCh: return ErrSessionNotOpen } } return nil } func (b *messageBuffer) pop() (string, error) { select { case msg := <-b.popCh: return msg, nil case <-b.closeCh: return "", ErrSessionNotOpen } } func (b *messageBuffer) close() { b.once.Do(func() { close(b.closeCh) }) } sockjs-go-3.0.2/sockjs/doc.go000066400000000000000000000001311414327410500157770ustar00rootroot00000000000000/* Package sockjs is a server side implementation of sockjs protocol. */ package sockjs sockjs-go-3.0.2/sockjs/eventsource.go000066400000000000000000000013171414327410500176030ustar00rootroot00000000000000package sockjs import ( "fmt" "io" "net/http" ) func (h *handler) eventSource(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "text/event-stream; charset=UTF-8") fmt.Fprintf(rw, "\r\n") rw.(http.Flusher).Flush() recv := newHTTPReceiver(rw, h.options.ResponseLimit, new(eventSourceFrameWriter)) sess, _ := h.sessionByRequest(req) if err := sess.attachReceiver(recv); err != nil { recv.sendFrame(cFrame) recv.close() return } select { case <-recv.doneNotify(): case <-recv.interruptedNotify(): } } type eventSourceFrameWriter struct{} func (*eventSourceFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "data: %s\r\n\r\n", frame) } sockjs-go-3.0.2/sockjs/eventsource_test.go000066400000000000000000000042631414327410500206450ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "runtime" "testing" "time" ) func TestHandler_EventSource(t *testing.T) { rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/eventsource", nil) h := newTestHandler() h.options.ResponseLimit = 1024 go func() { var sess *session for exists := false; !exists; { runtime.Gosched() h.sessionsMux.Lock() sess, exists = h.sessions["session"] h.sessionsMux.Unlock() } for exists := false; !exists; { runtime.Gosched() sess.RLock() exists = sess.recv != nil sess.RUnlock() } sess.RLock() sess.recv.close() sess.RUnlock() }() h.eventSource(rw, req) contentType := rw.Header().Get("content-type") expected := "text/event-stream; charset=UTF-8" if contentType != expected { t.Errorf("Unexpected content type, got '%s', extected '%s'", contentType, expected) } if rw.Code != http.StatusOK { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusOK) } if rw.Body.String() != "\r\ndata: o\r\n\r\n" { t.Errorf("Event stream prelude, got '%s'", rw.Body) } } func TestHandler_EventSourceMultipleConnections(t *testing.T) { h := newTestHandler() h.options.ResponseLimit = 1024 rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/sess/eventsource", nil) go func() { rw := &ClosableRecorder{httptest.NewRecorder(), nil} h.eventSource(rw, req) if rw.Body.String() != "\r\ndata: c[2010,\"Another connection still open\"]\r\n\r\n" { t.Errorf("wrong, got '%v'", rw.Body) } h.sessionsMux.Lock() sess := h.sessions["sess"] sess.close() h.sessionsMux.Unlock() }() h.eventSource(rw, req) } func TestHandler_EventSourceConnectionInterrupted(t *testing.T) { h := newTestHandler() sess := newTestSession() sess.state = SessionActive h.sessions["session"] = sess req, _ := http.NewRequest("POST", "/server/session/eventsource", nil) rw := newClosableRecorder() close(rw.closeNotifCh) h.eventSource(rw, req) select { case <-sess.closeCh: case <-time.After(1 * time.Second): t.Errorf("session close channel should be closed") } sess.Lock() if sess.state != SessionClosed { t.Errorf("Session should be closed") } } sockjs-go-3.0.2/sockjs/example_handler_test.go000066400000000000000000000014111414327410500214230ustar00rootroot00000000000000package sockjs_test import ( "net/http" "github.com/igm/sockjs-go/sockjs" ) func ExampleNewHandler_simple() { handler := sockjs.NewHandler("/echo", sockjs.DefaultOptions, func(session sockjs.Session) { for { if msg, err := session.Recv(); err == nil { if session.Send(msg) != nil { break } } else { break } } }) http.ListenAndServe(":8080", handler) } func ExampleNewHandler_defaultMux() { handler := sockjs.NewHandler("/echo", sockjs.DefaultOptions, func(session sockjs.Session) { for { if msg, err := session.Recv(); err == nil { if session.Send(msg) != nil { break } } else { break } } }) // need to provide path prefix for http.Mux http.Handle("/echo/", handler) http.ListenAndServe(":8080", nil) } sockjs-go-3.0.2/sockjs/frame.go000066400000000000000000000003201414327410500163240ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" ) func closeFrame(status uint32, reason string) string { bytes, _ := json.Marshal([]interface{}{status, reason}) return fmt.Sprintf("c%s", string(bytes)) } sockjs-go-3.0.2/sockjs/frame_test.go000066400000000000000000000003231414327410500173660ustar00rootroot00000000000000package sockjs import "testing" func TestCloseFrame(t *testing.T) { cf := closeFrame(1024, "some close text") if cf != "c[1024,\"some close text\"]" { t.Errorf("Wrong close frame generated '%s'", cf) } } sockjs-go-3.0.2/sockjs/handler.go000066400000000000000000000106261414327410500166610ustar00rootroot00000000000000package sockjs import ( "errors" "net/http" "net/url" "regexp" "strings" "sync" ) var ( prefixRegexp = make(map[string]*regexp.Regexp) prefixRegexpMu sync.Mutex // protects prefixRegexp ) type handler struct { prefix string options Options handlerFunc func(Session) mappings []*mapping sessionsMux sync.Mutex sessions map[string]*session } // NewHandler creates new HTTP handler that conforms to the basic net/http.Handler interface. // It takes path prefix, options and sockjs handler function as parameters func NewHandler(prefix string, opts Options, handleFunc func(Session)) http.Handler { return newHandler(prefix, opts, handleFunc) } func newHandler(prefix string, opts Options, handlerFunc func(Session)) *handler { h := &handler{ prefix: prefix, options: opts, handlerFunc: handlerFunc, sessions: make(map[string]*session), } xhrCors := xhrCorsFactory(opts) matchPrefix := prefix if matchPrefix == "" { matchPrefix = "^" } sessionPrefix := matchPrefix + "/[^/.]+/[^/.]+" h.mappings = []*mapping{ newMapping("GET", matchPrefix+"[/]?$", welcomeHandler), newMapping("OPTIONS", matchPrefix+"/info$", opts.cookie, xhrCors, cacheFor, opts.info), newMapping("GET", matchPrefix+"/info$", xhrCors, noCache, opts.info), // XHR newMapping("POST", sessionPrefix+"/xhr_send$", opts.cookie, xhrCors, noCache, h.xhrSend), newMapping("OPTIONS", sessionPrefix+"/xhr_send$", opts.cookie, xhrCors, cacheFor, xhrOptions), newMapping("POST", sessionPrefix+"/xhr$", opts.cookie, xhrCors, noCache, h.xhrPoll), newMapping("OPTIONS", sessionPrefix+"/xhr$", opts.cookie, xhrCors, cacheFor, xhrOptions), newMapping("POST", sessionPrefix+"/xhr_streaming$", opts.cookie, xhrCors, noCache, h.xhrStreaming), newMapping("OPTIONS", sessionPrefix+"/xhr_streaming$", opts.cookie, xhrCors, cacheFor, xhrOptions), // EventStream newMapping("GET", sessionPrefix+"/eventsource$", opts.cookie, xhrCors, noCache, h.eventSource), // Htmlfile newMapping("GET", sessionPrefix+"/htmlfile$", opts.cookie, xhrCors, noCache, h.htmlFile), // JsonP newMapping("GET", sessionPrefix+"/jsonp$", opts.cookie, xhrCors, noCache, h.jsonp), newMapping("OPTIONS", sessionPrefix+"/jsonp$", opts.cookie, xhrCors, cacheFor, xhrOptions), newMapping("POST", sessionPrefix+"/jsonp_send$", opts.cookie, xhrCors, noCache, h.jsonpSend), // IFrame newMapping("GET", matchPrefix+"/iframe[0-9-.a-z_]*.html$", cacheFor, h.iframe), } if opts.Websocket { h.mappings = append(h.mappings, newMapping("GET", sessionPrefix+"/websocket$", h.sockjsWebsocket)) } if opts.RawWebsocket { h.mappings = append(h.mappings, newMapping("GET", matchPrefix+"/websocket$", h.rawWebsocket)) } return h } func (h *handler) Prefix() string { return h.prefix } func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // iterate over mappings allowedMethods := []string{} for _, mapping := range h.mappings { if match, method := mapping.matches(req); match == fullMatch { for _, hf := range mapping.chain { hf(rw, req) } return } else if match == pathMatch { allowedMethods = append(allowedMethods, method) } } if len(allowedMethods) > 0 { rw.Header().Set("allow", strings.Join(allowedMethods, ", ")) rw.Header().Set("Content-Type", "") rw.WriteHeader(http.StatusMethodNotAllowed) return } http.NotFound(rw, req) } func (h *handler) parseSessionID(url *url.URL) (string, error) { // cache compiled regexp objects for most used prefixes prefixRegexpMu.Lock() session, ok := prefixRegexp[h.prefix] if !ok { session = regexp.MustCompile(h.prefix + "/(?P[^/.]+)/(?P[^/.]+)/.*") prefixRegexp[h.prefix] = session } prefixRegexpMu.Unlock() matches := session.FindStringSubmatch(url.Path) if len(matches) == 3 { return matches[2], nil } return "", errors.New("unable to parse URL for session") } func (h *handler) sessionByRequest(req *http.Request) (*session, error) { h.sessionsMux.Lock() defer h.sessionsMux.Unlock() sessionID, err := h.parseSessionID(req.URL) if err != nil { return nil, err } sess, exists := h.sessions[sessionID] if !exists { sess = newSession(req, sessionID, h.options.DisconnectDelay, h.options.HeartbeatDelay) h.sessions[sessionID] = sess if h.handlerFunc != nil { go h.handlerFunc(sess) } go func() { <-sess.closedNotify() h.sessionsMux.Lock() delete(h.sessions, sessionID) h.sessionsMux.Unlock() }() } return sess, nil } sockjs-go-3.0.2/sockjs/handler_test.go000066400000000000000000000073531414327410500177230ustar00rootroot00000000000000package sockjs import ( "encoding/json" "io/ioutil" "net/http" "net/http/httptest" "net/url" "testing" "time" ) var testOptions = DefaultOptions func init() { testOptions.RawWebsocket = true } func TestHandler_Create(t *testing.T) { handler := newHandler("/echo", testOptions, nil) if handler.Prefix() != "/echo" { t.Errorf("Prefix not properly set, got '%s' expected '%s'", handler.Prefix(), "/echo") } if handler.sessions == nil { t.Errorf("Handler session map not made") } server := httptest.NewServer(handler) defer server.Close() resp, err := http.Get(server.URL + "/echo") if err != nil { t.Errorf("There should not be any error, got '%s'", err) t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusOK { t.Errorf("Unexpected status code receiver, got '%d' expected '%d'", resp.StatusCode, http.StatusOK) } } func TestHandler_RootPrefixInfoHandler(t *testing.T) { handler := newHandler("", testOptions, nil) if handler.Prefix() != "" { t.Errorf("Prefix not properly set, got '%s' expected '%s'", handler.Prefix(), "") } server := httptest.NewServer(handler) defer server.Close() resp, err := http.Get(server.URL + "/info") if err != nil { t.Errorf("There should not be any error, got '%s'", err) t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusOK { t.Errorf("Unexpected status code receiver, got '%d' expected '%d'", resp.StatusCode, http.StatusOK) } infoData, err := ioutil.ReadAll(resp.Body) if err != nil { t.Errorf("Error reading body: '%v'", err) } var i info err = json.Unmarshal(infoData, &i) if err != nil { t.Fatalf("Error unmarshaling info: '%v', data was: '%s'", err, string(infoData)) } if i.Websocket != true { t.Fatalf("Expected websocket to be true") } } func TestHandler_ParseSessionId(t *testing.T) { h := handler{prefix: "/prefix"} url, _ := url.Parse("http://server:80/prefix/server/session/whatever") if session, err := h.parseSessionID(url); session != "session" || err != nil { t.Errorf("Wrong session parsed, got '%s' expected '%s' with error = '%v'", session, "session", err) } url, _ = url.Parse("http://server:80/asdasd/server/session/whatever") if _, err := h.parseSessionID(url); err == nil { t.Errorf("Should return error") } } func TestHandler_SessionByRequest(t *testing.T) { h := newHandler("", testOptions, nil) h.options.DisconnectDelay = 10 * time.Millisecond var handlerFuncCalled = make(chan Session) h.handlerFunc = func(conn Session) { handlerFuncCalled <- conn } req, _ := http.NewRequest("POST", "/server/sessionid/whatever/follows", nil) sess, err := h.sessionByRequest(req) if sess == nil || err != nil { t.Errorf("Session should be returned") // test handlerFunc was called select { case conn := <-handlerFuncCalled: // ok if conn != sess { t.Errorf("Handler was not passed correct session") } case <-time.After(100 * time.Millisecond): t.Errorf("HandlerFunc was not called") } } // test session is reused for multiple requests with same sessionID req2, _ := http.NewRequest("POST", "/server/sessionid/whatever", nil) if sess2, err := h.sessionByRequest(req2); sess2 != sess || err != nil { t.Errorf("Expected error, got session: '%v'", sess) } // test session expires after timeout time.Sleep(15 * time.Millisecond) h.sessionsMux.Lock() if _, exists := h.sessions["sessionid"]; exists { t.Errorf("Session should not exist in handler after timeout") } h.sessionsMux.Unlock() // test proper behaviour in case URL is not correct req, _ = http.NewRequest("POST", "", nil) if _, err := h.sessionByRequest(req); err == nil { t.Errorf("Expected parser sessionID from URL error, got 'nil'") } } sockjs-go-3.0.2/sockjs/htmlfile.go000066400000000000000000000032131414327410500170420ustar00rootroot00000000000000package sockjs import ( "fmt" "io" "net/http" "regexp" "strings" ) var iframeTemplate = `

Don't panic!

` var invalidCallback = regexp.MustCompile("[^a-zA-Z0-9\\_\\.]") func init() { iframeTemplate += strings.Repeat(" ", 1024-len(iframeTemplate)+14) iframeTemplate += "\r\n\r\n" } func (h *handler) htmlFile(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "text/html; charset=UTF-8") req.ParseForm() callback := req.Form.Get("c") if callback == "" { http.Error(rw, `"callback" parameter required`, http.StatusInternalServerError) return } else if invalidCallback.MatchString(callback) { http.Error(rw, `invalid character in "callback" parameter`, http.StatusBadRequest) return } rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, iframeTemplate, callback) rw.(http.Flusher).Flush() sess, _ := h.sessionByRequest(req) recv := newHTTPReceiver(rw, h.options.ResponseLimit, new(htmlfileFrameWriter)) if err := sess.attachReceiver(recv); err != nil { recv.sendFrame(cFrame) recv.close() return } select { case <-recv.doneNotify(): case <-recv.interruptedNotify(): } } type htmlfileFrameWriter struct{} func (*htmlfileFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "\r\n", quote(frame)) } sockjs-go-3.0.2/sockjs/htmlfile_test.go000066400000000000000000000051301414327410500201010ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestHandler_htmlFileNoCallback(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/htmlfile", nil) h.htmlFile(rw, req) if rw.Code != http.StatusInternalServerError { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusInternalServerError) } expectedContentType := "text/plain; charset=utf-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } } func TestHandler_htmlFile(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/htmlfile?c=testCallback", nil) h.htmlFile(rw, req) if rw.Code != http.StatusOK { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusOK) } expectedContentType := "text/html; charset=UTF-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content-type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } if rw.Body.String() != expectedIFrame { t.Errorf("Unexpected response body, got '%s', expected '%s'", rw.Body, expectedIFrame) } } func TestHandler_cannotIntoXSS(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() // test simple injection req, _ := http.NewRequest("GET", "/server/session/htmlfile?c=fake%3Balert(1337)", nil) h.htmlFile(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusBadRequest) } h = newTestHandler() rw = httptest.NewRecorder() // test simple injection req, _ = http.NewRequest("GET", "/server/session/htmlfile?c=fake%2Dalert", nil) h.htmlFile(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusBadRequest) } } func init() { expectedIFrame += strings.Repeat(" ", 1024-len(expectedIFrame)+len("testCallack")+13) expectedIFrame += "\r\n\r\n" expectedIFrame += "\r\n" } var expectedIFrame = `

Don't panic!

` sockjs-go-3.0.2/sockjs/httpreceiver.go000066400000000000000000000046661414327410500177570ustar00rootroot00000000000000package sockjs import ( "fmt" "io" "net/http" "strings" "sync" ) type frameWriter interface { write(writer io.Writer, frame string) (int, error) } type httpReceiverState int const ( stateHTTPReceiverActive httpReceiverState = iota stateHTTPReceiverClosed ) type httpReceiver struct { sync.Mutex state httpReceiverState frameWriter frameWriter rw http.ResponseWriter maxResponseSize uint32 currentResponseSize uint32 doneCh chan struct{} interruptCh chan struct{} } func newHTTPReceiver(rw http.ResponseWriter, maxResponse uint32, frameWriter frameWriter) *httpReceiver { recv := &httpReceiver{ rw: rw, frameWriter: frameWriter, maxResponseSize: maxResponse, doneCh: make(chan struct{}), interruptCh: make(chan struct{}), } if closeNotifier, ok := rw.(http.CloseNotifier); ok { // if supported check for close notifications from http.RW closeNotifyCh := closeNotifier.CloseNotify() go func() { select { case <-closeNotifyCh: recv.Lock() defer recv.Unlock() if recv.state < stateHTTPReceiverClosed { recv.state = stateHTTPReceiverClosed close(recv.interruptCh) } case <-recv.doneCh: // ok, no action needed here, receiver closed in correct way // just finish the routine } }() } return recv } func (recv *httpReceiver) sendBulk(messages ...string) { if len(messages) > 0 { recv.sendFrame(fmt.Sprintf("a[%s]", strings.Join( transform(messages, quote), ",", ), )) } } func (recv *httpReceiver) sendFrame(value string) { recv.Lock() defer recv.Unlock() if recv.state == stateHTTPReceiverActive { // TODO(igm) check err, possibly act as if interrupted n, _ := recv.frameWriter.write(recv.rw, value) recv.currentResponseSize += uint32(n) if recv.currentResponseSize >= recv.maxResponseSize { recv.state = stateHTTPReceiverClosed close(recv.doneCh) } else { recv.rw.(http.Flusher).Flush() } } } func (recv *httpReceiver) doneNotify() <-chan struct{} { return recv.doneCh } func (recv *httpReceiver) interruptedNotify() <-chan struct{} { return recv.interruptCh } func (recv *httpReceiver) close() { recv.Lock() defer recv.Unlock() if recv.state < stateHTTPReceiverClosed { recv.state = stateHTTPReceiverClosed close(recv.doneCh) } } func (recv *httpReceiver) canSend() bool { recv.Lock() defer recv.Unlock() return recv.state != stateHTTPReceiverClosed } sockjs-go-3.0.2/sockjs/httpreceiver_test.go000066400000000000000000000060671414327410500210130ustar00rootroot00000000000000package sockjs import ( "io" "net/http/httptest" "testing" "time" ) type testFrameWriter struct { frames []string } func (t *testFrameWriter) write(w io.Writer, frame string) (int, error) { t.frames = append(t.frames, frame) return len(frame), nil } func TestHttpReceiver_Create(t *testing.T) { rec := httptest.NewRecorder() recv := newHTTPReceiver(rec, 1024, new(testFrameWriter)) if recv.doneCh != recv.doneNotify() { t.Errorf("Calling done() must return close channel, but it does not") } if recv.rw != rec { t.Errorf("Http.ResponseWriter not properly initialized") } if recv.maxResponseSize != 1024 { t.Errorf("MaxResponseSize not properly initialized") } } func TestHttpReceiver_SendEmptyFrames(t *testing.T) { rec := httptest.NewRecorder() recv := newHTTPReceiver(rec, 1024, new(testFrameWriter)) recv.sendBulk() if rec.Body.String() != "" { t.Errorf("Incorrect body content received from receiver '%s'", rec.Body.String()) } } func TestHttpReceiver_SendFrame(t *testing.T) { rec := httptest.NewRecorder() fw := new(testFrameWriter) recv := newHTTPReceiver(rec, 1024, fw) var frame = "some frame content" recv.sendFrame(frame) if len(fw.frames) != 1 || fw.frames[0] != frame { t.Errorf("Incorrect body content received, got '%s', expected '%s'", fw.frames, frame) } } func TestHttpReceiver_SendBulk(t *testing.T) { rec := httptest.NewRecorder() fw := new(testFrameWriter) recv := newHTTPReceiver(rec, 1024, fw) recv.sendBulk("message 1", "message 2", "message 3") expected := "a[\"message 1\",\"message 2\",\"message 3\"]" if len(fw.frames) != 1 || fw.frames[0] != expected { t.Errorf("Incorrect body content received from receiver, got '%s' expected '%s'", fw.frames, expected) } } func TestHttpReceiver_MaximumResponseSize(t *testing.T) { rec := httptest.NewRecorder() recv := newHTTPReceiver(rec, 52, new(testFrameWriter)) recv.sendBulk("message 1", "message 2") // produces 26 bytes of response in 1 frame if recv.currentResponseSize != 26 { t.Errorf("Incorrect response size calcualated, got '%d' expected '%d'", recv.currentResponseSize, 26) } select { case <-recv.doneNotify(): t.Errorf("Receiver should not be done yet") default: // ok } recv.sendBulk("message 1", "message 2") // produces another 26 bytes of response in 1 frame to go over max resposne size select { case <-recv.doneNotify(): // ok default: t.Errorf("Receiver closed channel did not close") } } func TestHttpReceiver_Close(t *testing.T) { rec := httptest.NewRecorder() recv := newHTTPReceiver(rec, 1024, nil) recv.close() if recv.state != stateHTTPReceiverClosed { t.Errorf("Unexpected state, got '%d', expected '%d'", recv.state, stateHTTPReceiverClosed) } } func TestHttpReceiver_ConnectionInterrupt(t *testing.T) { rw := newClosableRecorder() recv := newHTTPReceiver(rw, 1024, nil) rw.closeNotifCh <- true select { case <-recv.interruptCh: case <-time.After(1 * time.Second): t.Errorf("should interrupt") } if recv.state != stateHTTPReceiverClosed { t.Errorf("Unexpected state, got '%d', expected '%d'", recv.state, stateHTTPReceiverClosed) } } sockjs-go-3.0.2/sockjs/iframe.go000066400000000000000000000020031414327410500164750ustar00rootroot00000000000000package sockjs import ( "crypto/md5" "fmt" "net/http" "text/template" ) var tmpl = template.Must(template.New("iframe").Parse(iframeBody)) func (h *handler) iframe(rw http.ResponseWriter, req *http.Request) { etagReq := req.Header.Get("If-None-Match") hash := md5.New() hash.Write([]byte(iframeBody)) etag := fmt.Sprintf("%x", hash.Sum(nil)) if etag == etagReq { rw.WriteHeader(http.StatusNotModified) return } rw.Header().Set("Content-Type", "text/html; charset=UTF-8") rw.Header().Add("ETag", etag) tmpl.Execute(rw, h.options.SockJSURL) } var iframeBody = `

Don't panic!

This is a SockJS hidden iframe. It's used for cross domain magic.

` sockjs-go-3.0.2/sockjs/iframe_test.go000066400000000000000000000022171414327410500175430ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "testing" ) func TestHandler_iframe(t *testing.T) { h := newTestHandler() h.options.SockJSURL = "http://sockjs.com/sockjs.js" rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/sess/iframe", nil) h.iframe(rw, req) if rw.Body.String() != expected { t.Errorf("Unexpected html content,\ngot:\n'%s'\n\nexpected\n'%s'", rw.Body, expected) } eTag := rw.Header().Get("etag") req.Header.Set("if-none-match", eTag) rw = httptest.NewRecorder() h.iframe(rw, req) if rw.Code != http.StatusNotModified { t.Errorf("Unexpected response, got '%d', expected '%d'", rw.Code, http.StatusNotModified) } } var expected = `

Don't panic!

This is a SockJS hidden iframe. It's used for cross domain magic.

` sockjs-go-3.0.2/sockjs/jsonp.go000066400000000000000000000037761414327410500164050ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" "io" "net/http" "strings" ) func (h *handler) jsonp(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "application/javascript; charset=UTF-8") req.ParseForm() callback := req.Form.Get("c") if callback == "" { http.Error(rw, `"callback" parameter required`, http.StatusInternalServerError) return } else if invalidCallback.MatchString(callback) { http.Error(rw, `invalid character in "callback" parameter`, http.StatusBadRequest) return } rw.WriteHeader(http.StatusOK) rw.(http.Flusher).Flush() sess, _ := h.sessionByRequest(req) recv := newHTTPReceiver(rw, 1, &jsonpFrameWriter{callback}) if err := sess.attachReceiver(recv); err != nil { recv.sendFrame(cFrame) recv.close() return } select { case <-recv.doneNotify(): case <-recv.interruptedNotify(): } } func (h *handler) jsonpSend(rw http.ResponseWriter, req *http.Request) { req.ParseForm() var data io.Reader data = req.Body formReader := strings.NewReader(req.PostFormValue("d")) if formReader.Len() != 0 { data = formReader } if data == nil { http.Error(rw, "Payload expected.", http.StatusInternalServerError) return } var messages []string err := json.NewDecoder(data).Decode(&messages) if err == io.EOF { http.Error(rw, "Payload expected.", http.StatusInternalServerError) return } if err != nil { http.Error(rw, "Broken JSON encoding.", http.StatusInternalServerError) return } sessionID, _ := h.parseSessionID(req.URL) h.sessionsMux.Lock() defer h.sessionsMux.Unlock() if sess, ok := h.sessions[sessionID]; !ok { http.NotFound(rw, req) } else { _ = sess.accept(messages...) // TODO(igm) reponse with http.StatusInternalServerError in case of err? rw.Header().Set("content-type", "text/plain; charset=UTF-8") rw.Write([]byte("ok")) } } type jsonpFrameWriter struct { callback string } func (j *jsonpFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "%s(%s);\r\n", j.callback, quote(frame)) } sockjs-go-3.0.2/sockjs/jsonp_test.go000066400000000000000000000070551414327410500174360ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestHandler_jsonpNoCallback(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/jsonp", nil) h.jsonp(rw, req) if rw.Code != http.StatusInternalServerError { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusInternalServerError) } expectedContentType := "text/plain; charset=utf-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } } func TestHandler_jsonp(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/jsonp?c=testCallback", nil) h.jsonp(rw, req) expectedContentType := "application/javascript; charset=UTF-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } expectedBody := "testCallback(\"o\");\r\n" if rw.Body.String() != expectedBody { t.Errorf("Unexpected body, got '%s', expected '%s'", rw.Body, expectedBody) } } func TestHandler_jsonpSendNoPayload(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", nil) h.jsonpSend(rw, req) if rw.Code != http.StatusInternalServerError { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusInternalServerError) } } func TestHandler_jsonpSendWrongPayload(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("wrong payload")) h.jsonpSend(rw, req) if rw.Code != http.StatusInternalServerError { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusInternalServerError) } } func TestHandler_jsonpSendNoSession(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("[\"message\"]")) h.jsonpSend(rw, req) if rw.Code != http.StatusNotFound { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusNotFound) } } func TestHandler_jsonpSend(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("[\"message\"]")) sess := newSession(req, "session", time.Second, time.Second) h.sessions["session"] = sess var done = make(chan struct{}) go func() { h.jsonpSend(rw, req) close(done) }() msg, _ := sess.Recv() if msg != "message" { t.Errorf("Incorrect message in the channel, should be '%s', was '%s'", "some message", msg) } <-done if rw.Code != http.StatusOK { t.Errorf("Wrong response status received %d, should be %d", rw.Code, http.StatusOK) } if rw.Header().Get("content-type") != "text/plain; charset=UTF-8" { t.Errorf("Wrong content type received '%s'", rw.Header().Get("content-type")) } if rw.Body.String() != "ok" { t.Errorf("Unexpected body, got '%s', expected 'ok'", rw.Body) } } func TestHandler_jsonpCannotIntoXSS(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/jsonp?c=%3Chtml%3E%3Chead%3E%3Cscript%3Ealert(5520)%3C%2Fscript%3E", nil) h.jsonp(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("JsonP forwarded an exploitable response.") } } sockjs-go-3.0.2/sockjs/mapping.go000066400000000000000000000014201414327410500166670ustar00rootroot00000000000000package sockjs import ( "net/http" "regexp" ) type mapping struct { method string path *regexp.Regexp chain []http.HandlerFunc } func newMapping(method string, re string, handlers ...http.HandlerFunc) *mapping { return &mapping{method, regexp.MustCompile(re), handlers} } type matchType uint32 const ( fullMatch matchType = iota pathMatch noMatch ) // matches checks if given req.URL is a match with a mapping. Match can be either full, partial (http method mismatch) or no match. func (m *mapping) matches(req *http.Request) (match matchType, method string) { if !m.path.MatchString(req.URL.Path) { match, method = noMatch, "" } else if m.method != req.Method { match, method = pathMatch, m.method } else { match, method = fullMatch, m.method } return } sockjs-go-3.0.2/sockjs/mapping_test.go000066400000000000000000000020401414327410500177250ustar00rootroot00000000000000package sockjs import ( "net/http" "regexp" "testing" ) func TestMappingMatcher(t *testing.T) { mappingPrefix := mapping{"GET", regexp.MustCompile("prefix/$"), nil} mappingPrefixRegExp := mapping{"GET", regexp.MustCompile(".*x/$"), nil} var testRequests = []struct { mapping mapping method string url string expectedMatch matchType }{ {mappingPrefix, "GET", "http://foo/prefix/", fullMatch}, {mappingPrefix, "POST", "http://foo/prefix/", pathMatch}, {mappingPrefix, "GET", "http://foo/prefix_not_mapped", noMatch}, {mappingPrefixRegExp, "GET", "http://foo/prefix/", fullMatch}, } for _, request := range testRequests { req, _ := http.NewRequest(request.method, request.url, nil) m := request.mapping match, method := m.matches(req) if match != request.expectedMatch { t.Errorf("mapping %s should match url=%s", m.path, request.url) } if request.expectedMatch == pathMatch { if method != m.method { t.Errorf("Matcher method should be %s, but got %s", m.method, method) } } } } sockjs-go-3.0.2/sockjs/options.go000066400000000000000000000136251414327410500167410ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" "math/rand" "net/http" "sync" "time" "github.com/gorilla/websocket" ) var ( entropy *rand.Rand entropyMutex sync.Mutex ) func init() { entropy = rand.New(rand.NewSource(time.Now().UnixNano())) } // Options type is used for defining various sockjs options type Options struct { // Transports which don't support cross-domain communication natively ('eventsource' to name one) use an iframe trick. // A simple page is served from the SockJS server (using its foreign domain) and is placed in an invisible iframe. // Code run from this iframe doesn't need to worry about cross-domain issues, as it's being run from domain local to the SockJS server. // This iframe also does need to load SockJS javascript client library, and this option lets you specify its url (if you're unsure, // point it to the latest minified SockJS client release, this is the default). You must explicitly specify this url on the server // side for security reasons - we don't want the possibility of running any foreign javascript within the SockJS domain (aka cross site scripting attack). // Also, sockjs javascript library is probably already cached by the browser - it makes sense to reuse the sockjs url you're using in normally. SockJSURL string // Most streaming transports save responses on the client side and don't free memory used by delivered messages. // Such transports need to be garbage-collected once in a while. `response_limit` sets a minimum number of bytes that can be send // over a single http streaming request before it will be closed. After that client needs to open new request. // Setting this value to one effectively disables streaming and will make streaming transports to behave like polling transports. // The default value is 128K. ResponseLimit uint32 // Some load balancers don't support websockets. This option can be used to disable websockets support by the server. By default websockets are enabled. Websocket bool // This option can be used to enable raw websockets support by the server. By default raw websockets are disabled. RawWebsocket bool // Provide a custom Upgrader for Websocket connections to enable features like compression. // See https://godoc.org/github.com/gorilla/websocket#Upgrader for more details. WebsocketUpgrader *websocket.Upgrader // WebsocketWriteTimeout is a custom write timeout for Websocket underlying network connection. // A zero value means writes will not time out. WebsocketWriteTimeout time.Duration // In order to keep proxies and load balancers from closing long running http requests we need to pretend that the connection is active // and send a heartbeat packet once in a while. This setting controls how often this is done. // By default a heartbeat packet is sent every 25 seconds. HeartbeatDelay time.Duration // The server closes a session when a client receiving connection have not been seen for a while. // This delay is configured by this setting. // By default the session is closed when a receiving connection wasn't seen for 5 seconds. DisconnectDelay time.Duration // Some hosting providers enable sticky sessions only to requests that have JSessionID cookie set. // This setting controls if the server should set this cookie to a dummy value. // By default setting JSessionID cookie is disabled. More sophisticated behaviour can be achieved by supplying a function. JSessionID func(http.ResponseWriter, *http.Request) // CORS origin to be set on outgoing responses. If set to the empty string, it will default to the // incoming `Origin` header, or "*" if the Origin header isn't set. Origin string // CheckOrigin allows to dynamically decide whether server should set CORS // headers or not in case of XHR requests. When true returned CORS will be // configured with allowed origin equal to incoming `Origin` header, or "*" // if the request Origin header isn't set. When false returned CORS headers // won't be set at all. If this function is nil then Origin option above will // be taken into account. CheckOrigin func(*http.Request) bool } // DefaultOptions is a convenient set of options to be used for sockjs var DefaultOptions = Options{ Websocket: true, RawWebsocket: false, JSessionID: nil, SockJSURL: "//cdnjs.cloudflare.com/ajax/libs/sockjs-client/0.3.4/sockjs.min.js", HeartbeatDelay: 25 * time.Second, DisconnectDelay: 5 * time.Second, ResponseLimit: 128 * 1024, WebsocketUpgrader: nil, } type info struct { Websocket bool `json:"websocket"` CookieNeeded bool `json:"cookie_needed"` Origins []string `json:"origins"` Entropy int32 `json:"entropy"` } func (options *Options) info(rw http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": rw.Header().Set("Content-Type", "application/json; charset=UTF-8") json.NewEncoder(rw).Encode(info{ Websocket: options.Websocket, CookieNeeded: options.JSessionID != nil, Origins: []string{"*:*"}, Entropy: generateEntropy(), }) case "OPTIONS": rw.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") rw.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", 365*24*60*60)) rw.WriteHeader(http.StatusNoContent) // 204 default: http.NotFound(rw, req) } } // DefaultJSessionID is a default behaviour function to be used in options for JSessionID if JSESSIONID is needed func DefaultJSessionID(rw http.ResponseWriter, req *http.Request) { cookie, err := req.Cookie("JSESSIONID") if err == http.ErrNoCookie { cookie = &http.Cookie{ Name: "JSESSIONID", Value: "dummy", } } cookie.Path = "/" header := rw.Header() header.Add("Set-Cookie", cookie.String()) } func (options *Options) cookie(rw http.ResponseWriter, req *http.Request) { if options.JSessionID != nil { // cookie is needed options.JSessionID(rw, req) } } func generateEntropy() int32 { entropyMutex.Lock() entropy := entropy.Int31() entropyMutex.Unlock() return entropy } sockjs-go-3.0.2/sockjs/options_test.go000066400000000000000000000035221414327410500177730ustar00rootroot00000000000000package sockjs import ( "encoding/json" "net/http" "net/http/httptest" ) import "testing" func TestInfoGet(t *testing.T) { recorder := httptest.NewRecorder() request, _ := http.NewRequest("GET", "", nil) DefaultOptions.info(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("Wrong status code, got '%d' expected '%d'", recorder.Code, http.StatusOK) } decoder := json.NewDecoder(recorder.Body) var a info decoder.Decode(&a) if !a.Websocket { t.Errorf("Websocket field should be set true") } if a.CookieNeeded { t.Errorf("CookieNeeded should be set to false") } } func TestInfoOptions(t *testing.T) { recorder := httptest.NewRecorder() request, _ := http.NewRequest("OPTIONS", "", nil) DefaultOptions.info(recorder, request) if recorder.Code != http.StatusNoContent { t.Errorf("Incorrect status code received, got '%d' expected '%d'", recorder.Code, http.StatusNoContent) } } func TestInfoUnknown(t *testing.T) { req, _ := http.NewRequest("PUT", "", nil) rec := httptest.NewRecorder() DefaultOptions.info(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("Incorrec response status, got '%d' expected '%d'", rec.Code, http.StatusNotFound) } } func TestCookies(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", nil) optionsWithCookies := DefaultOptions optionsWithCookies.JSessionID = DefaultJSessionID optionsWithCookies.cookie(rec, req) if rec.Header().Get("set-cookie") != "JSESSIONID=dummy; Path=/" { t.Errorf("Cookie not properly set in response") } // cookie value set in request req.AddCookie(&http.Cookie{Name: "JSESSIONID", Value: "some_jsession_id", Path: "/"}) rec = httptest.NewRecorder() optionsWithCookies.cookie(rec, req) if rec.Header().Get("set-cookie") != "JSESSIONID=some_jsession_id; Path=/" { t.Errorf("Cookie not properly set in response") } } sockjs-go-3.0.2/sockjs/rawwebsocket.go000066400000000000000000000062361414327410500177460ustar00rootroot00000000000000package sockjs import ( "encoding/json" "net/http" "time" "github.com/gorilla/websocket" ) func (h *handler) rawWebsocket(rw http.ResponseWriter, req *http.Request) { var conn *websocket.Conn var err error if h.options.WebsocketUpgrader != nil { conn, err = h.options.WebsocketUpgrader.Upgrade(rw, req, nil) } else { // use default as before, so that those 2 buffer size variables are used as before conn, err = websocket.Upgrade(rw, req, nil, WebSocketReadBufSize, WebSocketWriteBufSize) } if _, ok := err.(websocket.HandshakeError); ok { http.Error(rw, `Can "Upgrade" only to "WebSocket".`, http.StatusBadRequest) return } else if err != nil { rw.WriteHeader(http.StatusInternalServerError) return } sessID := "" sess := newSession(req, sessID, h.options.DisconnectDelay, h.options.HeartbeatDelay) sess.raw = true receiver := newRawWsReceiver(conn, h.options.WebsocketWriteTimeout) sess.attachReceiver(receiver) if h.handlerFunc != nil { go h.handlerFunc(sess) } readCloseCh := make(chan struct{}) go func() { for { frameType, p, err := conn.ReadMessage() if err != nil { close(readCloseCh) return } if frameType == websocket.TextMessage || frameType == websocket.BinaryMessage { sess.accept(string(p)) } } }() select { case <-readCloseCh: case <-receiver.doneNotify(): } sess.close() conn.Close() } type rawWsReceiver struct { conn *websocket.Conn closeCh chan struct{} writeTimeout time.Duration } func newRawWsReceiver(conn *websocket.Conn, writeTimeout time.Duration) *rawWsReceiver { return &rawWsReceiver{ conn: conn, closeCh: make(chan struct{}), writeTimeout: writeTimeout, } } func (w *rawWsReceiver) sendBulk(messages ...string) { if len(messages) > 0 { for _, m := range messages { if w.writeTimeout != 0 { w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)) } err := w.conn.WriteMessage(websocket.TextMessage, []byte(m)) if err != nil { w.close() break } } } } func (w *rawWsReceiver) sendFrame(frame string) { if w.writeTimeout != 0 { w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)) } var err error if frame == "h" { err = w.conn.WriteMessage(websocket.PingMessage, []byte{}) } else if len(frame) > 0 && frame[0] == 'c' { status, reason := parseCloseFrame(frame) msg := websocket.FormatCloseMessage(int(status), reason) err = w.conn.WriteMessage(websocket.CloseMessage, msg) } else { err = w.conn.WriteMessage(websocket.TextMessage, []byte(frame)) } if err != nil { w.close() } } func parseCloseFrame(frame string) (status uint32, reason string) { var items [2]interface{} json.Unmarshal([]byte(frame)[1:], &items) statusF, _ := items[0].(float64) status = uint32(statusF) reason, _ = items[1].(string) return } func (w *rawWsReceiver) close() { select { case <-w.closeCh: // already closed default: close(w.closeCh) } } func (w *rawWsReceiver) canSend() bool { select { case <-w.closeCh: // already closed return false default: return true } } func (w *rawWsReceiver) doneNotify() <-chan struct{} { return w.closeCh } func (w *rawWsReceiver) interruptedNotify() <-chan struct{} { return nil } sockjs-go-3.0.2/sockjs/rawwebsocket_test.go000066400000000000000000000116221414327410500210000ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "testing" "time" "github.com/gorilla/websocket" ) func TestHandler_RawWebSocketHandshakeError(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.Close() req, _ := http.NewRequest("GET", server.URL, nil) req.Header.Set("origin", "https"+server.URL[4:]) resp, _ := http.DefaultClient.Do(req) if resp.StatusCode != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", resp.StatusCode, http.StatusBadRequest) } } func TestHandler_RawWebSocket(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.CloseClientConnections() url := "ws" + server.URL[4:] var connCh = make(chan Session) h.handlerFunc = func(conn Session) { connCh <- conn } conn, resp, err := websocket.DefaultDialer.Dial(url, nil) if conn == nil { t.Errorf("Connection should not be nil") } if err != nil { t.Errorf("Unexpected error '%v'", err) } if resp.StatusCode != http.StatusSwitchingProtocols { t.Errorf("Wrong response code returned, got '%d', expected '%d'", resp.StatusCode, http.StatusSwitchingProtocols) } select { case <-connCh: //ok case <-time.After(10 * time.Millisecond): t.Errorf("Sockjs Handler not invoked") } } func TestHandler_RawWebSocketTerminationByServer(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.Close() url := "ws" + server.URL[4:] h.handlerFunc = func(conn Session) { // close the session without sending any message conn.Close(3000, "some close message") conn.Close(0, "this should be ignored") } conn, _, err := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) if err != nil { t.Fatalf("websocket dial failed: %v", err) } for i := 0; i < 2; i++ { _, _, err := conn.ReadMessage() closeError, ok := err.(*websocket.CloseError) if !ok { t.Fatalf("expected close error but got: %v", err) } if closeError.Code != 3000 { t.Errorf("unexpected close status: %v", closeError.Code) } if closeError.Text != "some close message" { t.Errorf("unexpected close reason: '%v'", closeError.Text) } } } func TestHandler_RawWebSocketTerminationByClient(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.Close() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { if _, err := conn.Recv(); err != ErrSessionNotOpen { t.Errorf("Recv should fail") } close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) conn.Close() <-done } func TestHandler_RawWebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketWriteTimeout = time.Second server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) // defer server.CloseClientConnections() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { conn.Send("message 1") conn.Send("message 2") expected := "[\"message 3\"]\n" msg, err := conn.Recv() if msg != expected || err != nil { t.Errorf("Got '%s', expected '%s'", msg, expected) } conn.Close(123, "close") close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) conn.WriteJSON([]string{"message 3"}) var expected = []string{"message 1", "message 2"} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } func TestHandler_RawCustomWebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketWriteTimeout = time.Second h.options.WebsocketUpgrader = &websocket.Upgrader{ ReadBufferSize: 0, WriteBufferSize: 0, CheckOrigin: func(_ *http.Request) bool { return true }, Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {}, } server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { conn.Send("message 1") conn.Send("message 2") expected := "[\"message 3\"]\n" msg, err := conn.Recv() if msg != expected || err != nil { t.Errorf("Got '%s', expected '%s'", msg, expected) } conn.Close(123, "close") close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) conn.WriteJSON([]string{"message 3"}) var expected = []string{"message 1", "message 2"} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } sockjs-go-3.0.2/sockjs/session.go000066400000000000000000000124361414327410500167300ustar00rootroot00000000000000package sockjs import ( "errors" "net/http" "sync" "time" ) // SessionState defines the current state of the session type SessionState uint32 const ( // brand new session, need to send "h" to receiver SessionOpening SessionState = iota // active session SessionActive // session being closed, sending "closeFrame" to receivers SessionClosing // closed session, no activity at all, should be removed from handler completely and not reused SessionClosed ) var ( // ErrSessionNotOpen error is used to denote session not in open state. // Recv() and Send() operations are not suppored if session is closed. ErrSessionNotOpen = errors.New("sockjs: session not in open state") errSessionReceiverAttached = errors.New("sockjs: another receiver already attached") ) type session struct { sync.RWMutex id string req *http.Request state SessionState recv receiver // protocol dependent receiver (xhr, eventsource, ...) sendBuffer []string // messages to be sent to client recvBuffer *messageBuffer // messages received from client to be consumed by application closeFrame string // closeFrame to send after session is closed // do not use SockJS framing for raw websocket connections raw bool // internal timer used to handle session expiration if no receiver is attached, or heartbeats if recevier is attached sessionTimeoutInterval time.Duration heartbeatInterval time.Duration timer *time.Timer // once the session timeouts this channel also closes closeCh chan struct{} } type receiver interface { // sendBulk send multiple data messages in frame frame in format: a["msg 1", "msg 2", ....] sendBulk(...string) // sendFrame sends given frame over the wire (with possible chunking depending on receiver) sendFrame(string) // close closes the receiver in a "done" way (idempotent) close() canSend() bool // done notification channel gets closed whenever receiver ends doneNotify() <-chan struct{} // interrupted channel gets closed whenever receiver is interrupted (i.e. http connection drops,...) interruptedNotify() <-chan struct{} } // Session is a central component that handles receiving and sending frames. It maintains internal state func newSession(req *http.Request, sessionID string, sessionTimeoutInterval, heartbeatInterval time.Duration) *session { s := &session{ id: sessionID, req: req, sessionTimeoutInterval: sessionTimeoutInterval, heartbeatInterval: heartbeatInterval, recvBuffer: newMessageBuffer(), closeCh: make(chan struct{}), } s.Lock() // "go test -race" complains if ommited, not sure why as no race can happen here s.timer = time.AfterFunc(sessionTimeoutInterval, s.close) s.Unlock() return s } func (s *session) sendMessage(msg string) error { s.Lock() defer s.Unlock() if s.state > SessionActive { return ErrSessionNotOpen } s.sendBuffer = append(s.sendBuffer, msg) if s.recv != nil && s.recv.canSend() { s.recv.sendBulk(s.sendBuffer...) s.sendBuffer = nil } return nil } func (s *session) attachReceiver(recv receiver) error { s.Lock() defer s.Unlock() if s.recv != nil { return errSessionReceiverAttached } s.recv = recv go func(r receiver) { select { case <-r.doneNotify(): s.detachReceiver() case <-r.interruptedNotify(): s.detachReceiver() s.close() } }(recv) if s.state == SessionClosing { if !s.raw { s.recv.sendFrame(s.closeFrame) } s.recv.close() return nil } if s.state == SessionOpening { if !s.raw { s.recv.sendFrame("o") } s.state = SessionActive } s.recv.sendBulk(s.sendBuffer...) s.sendBuffer = nil s.timer.Stop() if s.heartbeatInterval > 0 { s.timer = time.AfterFunc(s.heartbeatInterval, s.heartbeat) } return nil } func (s *session) detachReceiver() { s.Lock() s.timer.Stop() s.timer = time.AfterFunc(s.sessionTimeoutInterval, s.close) s.recv = nil s.Unlock() } func (s *session) heartbeat() { s.Lock() if s.recv != nil { // timer could have fired between Lock and timer.Stop in detachReceiver s.recv.sendFrame("h") s.timer = time.AfterFunc(s.heartbeatInterval, s.heartbeat) } s.Unlock() } func (s *session) accept(messages ...string) error { return s.recvBuffer.push(messages...) } // idempotent operation func (s *session) closing() { s.Lock() defer s.Unlock() if s.state < SessionClosing { s.state = SessionClosing s.recvBuffer.close() if s.recv != nil { s.recv.sendFrame(s.closeFrame) s.recv.close() } } } // idempotent operation func (s *session) close() { s.closing() s.Lock() defer s.Unlock() if s.state < SessionClosed { s.state = SessionClosed s.timer.Stop() close(s.closeCh) } } func (s *session) closedNotify() <-chan struct{} { return s.closeCh } // Conn interface implementation func (s *session) Close(status uint32, reason string) error { s.Lock() if s.state < SessionClosing { s.closeFrame = closeFrame(status, reason) s.Unlock() s.closing() return nil } s.Unlock() return ErrSessionNotOpen } func (s *session) Recv() (string, error) { return s.recvBuffer.pop() } func (s *session) Send(msg string) error { return s.sendMessage(msg) } func (s *session) ID() string { return s.id } func (s *session) GetSessionState() SessionState { s.RLock() defer s.RUnlock() return s.state } func (s *session) Request() *http.Request { return s.req } sockjs-go-3.0.2/sockjs/session_test.go000066400000000000000000000230051414327410500177610ustar00rootroot00000000000000package sockjs import ( "net/http" "runtime" "strings" "sync" "testing" "time" ) func newTestSession() *session { // session with long expiration and heartbeats with ID return newSession(nil, "sessionId", 1000*time.Second, 1000*time.Second) } func TestSession_Create(t *testing.T) { session := newTestSession() session.sendMessage("this is a message") if len(session.sendBuffer) != 1 { t.Errorf("Session send buffer should contain 1 message") } session.sendMessage("another message") if len(session.sendBuffer) != 2 { t.Errorf("Session send buffer should contain 2 messages") } if session.GetSessionState() != SessionOpening { t.Errorf("Session in wrong state %v, should be %v", session.GetSessionState(), SessionOpening) } } func TestSession_Request(t *testing.T) { req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("[\"message\"]")) sess := newSession(req, "session", time.Second, time.Second) if sess.Request() == nil { t.Error("Session initial request should have been saved.") } if sess.Request().URL.String() != req.URL.String() { t.Errorf("Expected stored session request URL to equal %s, got %s", req.URL.String(), sess.Request().URL.String()) } } func TestSession_ConcurrentSend(t *testing.T) { session := newTestSession() done := make(chan bool) for i := 0; i < 100; i++ { go func() { session.sendMessage("message D") done <- true }() } for i := 0; i < 100; i++ { <-done } if len(session.sendBuffer) != 100 { t.Errorf("Session send buffer should contain 100 messages") } } func TestSession_AttachReceiver(t *testing.T) { session := newTestSession() recv := &testReceiver{} // recv := &mockRecv{ // _sendFrame: func(frame string) { // if frame != "o" { // t.Errorf("Incorrect open header received") // } // }, // _sendBulk: func(...string) {}, // } if err := session.attachReceiver(recv); err != nil { t.Errorf("Should not return error") } if session.GetSessionState() != SessionActive { t.Errorf("Session in wrong state after receiver attached %d, should be %d", session.GetSessionState(), SessionActive) } session.detachReceiver() // recv = &mockRecv{ // _sendFrame: func(frame string) { // t.Errorf("No frame shold be send, got '%s'", frame) // }, // _sendBulk: func(...string) {}, // } if err := session.attachReceiver(recv); err != nil { t.Errorf("Should not return error") } } func TestSession_Timeout(t *testing.T) { sess := newSession(nil, "id", 10*time.Millisecond, 10*time.Second) select { case <-sess.closeCh: case <-time.After(20 * time.Millisecond): select { case <-sess.closeCh: // still ok default: t.Errorf("sess close notification channel should close") } } if sess.GetSessionState() != SessionClosed { t.Errorf("Session did not timeout") } } func TestSession_TimeoutOfClosedSession(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("Unexcpected error '%v'", r) } }() sess := newSession(nil, "id", 1*time.Millisecond, time.Second) sess.closing() time.Sleep(1 * time.Millisecond) sess.closing() } func TestSession_AttachReceiverAndCheckHeartbeats(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("Unexcpected error '%v'", r) } }() session := newSession(nil, "id", time.Second, 10*time.Millisecond) // 10ms heartbeats recv := newTestReceiver() defer close(recv.doneCh) session.attachReceiver(recv) time.Sleep(120 * time.Millisecond) recv.Lock() if len(recv.frames) < 10 || len(recv.frames) > 13 { // should get around 10 heartbeats (120ms/10ms) t.Fatalf("Wrong number of frames received, got '%d'", len(recv.frames)) } for i := 1; i < len(recv.frames); i++ { if recv.frames[i] != "h" { t.Errorf("Heartbeat no received") } } } func TestSession_AttachReceiverAndRefuse(t *testing.T) { session := newTestSession() if err := session.attachReceiver(newTestReceiver()); err != nil { t.Errorf("Should not return error") } var a sync.WaitGroup a.Add(100) for i := 0; i < 100; i++ { go func() { defer a.Done() if err := session.attachReceiver(newTestReceiver()); err != errSessionReceiverAttached { t.Errorf("Should return error as another receiver is already attached") } }() } a.Wait() } func TestSession_DetachRecevier(t *testing.T) { session := newTestSession() session.detachReceiver() session.detachReceiver() // idempotent operation session.attachReceiver(newTestReceiver()) session.detachReceiver() } func TestSession_SendWithRecv(t *testing.T) { session := newTestSession() session.sendMessage("message A") session.sendMessage("message B") if len(session.sendBuffer) != 2 { t.Errorf("There should be 2 messages in buffer, but there are %d", len(session.sendBuffer)) } recv := newTestReceiver() defer close(recv.doneCh) session.attachReceiver(recv) if len(recv.frames[1:]) != 2 { t.Errorf("Reciver should get 2 message frames from session, got %d", len(recv.frames)) } session.sendMessage("message C") if len(recv.frames[1:]) != 3 { t.Errorf("Reciver should get 3 message frames from session, got %d", len(recv.frames)) } session.sendMessage("message D") if len(recv.frames[1:]) != 4 { t.Errorf("Reciver should get 4 frames from session, got %d", len(recv.frames)) } if len(session.sendBuffer) != 0 { t.Errorf("Send buffer should be empty now, but there are %d messaged", len(session.sendBuffer)) } } func TestSession_Recv(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("Panic should not happen") } }() var wg sync.WaitGroup wg.Add(1) session := newTestSession() go func() { defer wg.Done() session.accept("message A") session.accept("message B") if err := session.accept("message C"); err != ErrSessionNotOpen { t.Errorf("Session should not accept new messages if closed, got '%v' expected '%v'", err, ErrSessionNotOpen) } }() if msg, _ := session.Recv(); msg != "message A" { t.Errorf("Got %s, should be %s", msg, "message A") } if msg, _ := session.Recv(); msg != "message B" { t.Errorf("Got %s, should be %s", msg, "message B") } session.close() wg.Wait() } func TestSession_Closing(t *testing.T) { session := newTestSession() session.closing() if _, err := session.Recv(); err == nil { t.Errorf("Session's receive buffer channel should close") } if err := session.sendMessage("some message"); err != ErrSessionNotOpen { t.Errorf("Session should not accept new message after close") } } // Session as Session Tests func TestSession_AsSession(t *testing.T) { var _ Session = newSession(nil, "id", 0, 0) } func TestSession_SessionRecv(t *testing.T) { s := newTestSession() go func() { s.accept("message 1") }() msg, err := s.Recv() if msg != "message 1" || err != nil { t.Errorf("Should receive a message without error, got '%s' err '%v'", msg, err) } go func() { s.closing() _, err := s.Recv() if err != ErrSessionNotOpen { t.Errorf("Session not in correct state, got '%v', expected '%v'", err, ErrSessionNotOpen) } }() _, err = s.Recv() if err != ErrSessionNotOpen { t.Errorf("Session not in correct state, got '%v', expected '%v'", err, ErrSessionNotOpen) } } func TestSession_SessionSend(t *testing.T) { s := newTestSession() err := s.Send("message A") if err != nil { t.Errorf("Session should take messages by default") } if len(s.sendBuffer) != 1 || s.sendBuffer[0] != "message A" { t.Errorf("Message not properly queued in session, got '%v'", s.sendBuffer) } } func TestSession_SessionClose(t *testing.T) { s := newTestSession() s.state = SessionActive recv := newTestReceiver() s.attachReceiver(recv) err := s.Close(1, "some reason") if len(recv.frames) != 1 || recv.frames[0] != "c[1,\"some reason\"]" { t.Errorf("Expected close frame, got '%v'", recv.frames) } if err != nil { t.Errorf("Should not get any error, got '%s'", err) } if s.closeFrame != "c[1,\"some reason\"]" { t.Errorf("Incorrect closeFrame, got '%s'", s.closeFrame) } if s.GetSessionState() != SessionClosing { t.Errorf("Incorrect session state, expected 'sessionClosing', got '%v'", s.GetSessionState()) } // all the consequent receivers trying to attach shoult get the same close frame var i = 100 for i > 0 { recv := newTestReceiver() err := s.attachReceiver(recv) if err != nil { // give a chance to a receiver to detach runtime.Gosched() continue } i-- if len(recv.frames) != 1 || recv.frames[0] != "c[1,\"some reason\"]" { t.Errorf("Close frame not received by recv, frames '%v'", recv.frames) } } if err := s.Close(1, "some other reson"); err != ErrSessionNotOpen { t.Errorf("Expected error, got '%v'", err) } } func TestSession_SessionSessionId(t *testing.T) { s := newTestSession() if s.ID() != "sessionId" { t.Errorf("Unexpected session ID, got '%s', expected '%s'", s.ID(), "sessionId") } } func newTestReceiver() *testReceiver { return &testReceiver{ doneCh: make(chan struct{}), interruptCh: make(chan struct{}), } } type testReceiver struct { sync.Mutex doneCh, interruptCh chan struct{} frames []string } func (t *testReceiver) doneNotify() <-chan struct{} { return t.doneCh } func (t *testReceiver) interruptedNotify() <-chan struct{} { return t.interruptCh } func (t *testReceiver) close() { close(t.doneCh) } func (t *testReceiver) canSend() bool { select { case <-t.doneCh: return false // already closed default: return true } } func (t *testReceiver) sendBulk(messages ...string) { for _, m := range messages { t.sendFrame(m) } } func (t *testReceiver) sendFrame(frame string) { t.Lock() defer t.Unlock() t.frames = append(t.frames, frame) } sockjs-go-3.0.2/sockjs/sockjs.go000066400000000000000000000011151414327410500165310ustar00rootroot00000000000000package sockjs import "net/http" // Session represents a connection between server and client. type Session interface { // Id returns a session id ID() string // Request returns the first http request Request() *http.Request // Recv reads one text frame from session Recv() (string, error) // Send sends one text frame to session Send(string) error // Close closes the session with provided code and reason. Close(status uint32, reason string) error //Gets the state of the session. SessionOpening/SessionActive/SessionClosing/SessionClosed; GetSessionState() SessionState } sockjs-go-3.0.2/sockjs/sockjs_test.go000066400000000000000000000014521414327410500175740ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "regexp" "testing" ) func TestSockJS_ServeHTTP(t *testing.T) { m := handler{mappings: make([]*mapping, 0)} m.mappings = []*mapping{ &mapping{"POST", regexp.MustCompile("/foo/.*"), []http.HandlerFunc{func(http.ResponseWriter, *http.Request) {}}}, } req, _ := http.NewRequest("GET", "/foo/bar", nil) rec := httptest.NewRecorder() m.ServeHTTP(rec, req) if rec.Code != http.StatusMethodNotAllowed { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusMethodNotAllowed) } req, _ = http.NewRequest("GET", "/bar", nil) rec = httptest.NewRecorder() m.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusNotFound) } } sockjs-go-3.0.2/sockjs/utils.go000066400000000000000000000004731414327410500164030ustar00rootroot00000000000000package sockjs import "encoding/json" func quote(in string) string { quoted, _ := json.Marshal(in) return string(quoted) } func transform(values []string, transformFn func(string) string) []string { ret := make([]string, len(values)) for i, msg := range values { ret[i] = transformFn(msg) } return ret } sockjs-go-3.0.2/sockjs/utils_test.go000066400000000000000000000006111414327410500174340ustar00rootroot00000000000000package sockjs import "testing" func TestQuote(t *testing.T) { var quotationTests = []struct { input string output string }{ {"simple", "\"simple\""}, {"more complex \"", "\"more complex \\\"\""}, } for _, testCase := range quotationTests { if quote(testCase.input) != testCase.output { t.Errorf("Expected '%s', got '%s'", testCase.output, quote(testCase.input)) } } } sockjs-go-3.0.2/sockjs/web.go000066400000000000000000000036521414327410500160220ustar00rootroot00000000000000package sockjs import ( "fmt" "net/http" "time" ) func xhrCorsFactory(opts Options) func(rw http.ResponseWriter, req *http.Request) { return func(rw http.ResponseWriter, req *http.Request) { header := rw.Header() var corsEnabled bool var corsOrigin string if opts.CheckOrigin != nil { corsEnabled = opts.CheckOrigin(req) if corsEnabled { corsOrigin = req.Header.Get("origin") if corsOrigin == "" { corsOrigin = "*" } } } else { corsEnabled = true corsOrigin = opts.Origin if corsOrigin == "" { corsOrigin = req.Header.Get("origin") } if corsOrigin == "" || corsOrigin == "null" { corsOrigin = "*" } } if corsEnabled { header.Set("Access-Control-Allow-Origin", corsOrigin) if allowHeaders := req.Header.Get("Access-Control-Request-Headers"); allowHeaders != "" && allowHeaders != "null" { header.Add("Access-Control-Allow-Headers", allowHeaders) } header.Set("Access-Control-Allow-Credentials", "true") } } } func xhrOptions(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("Access-Control-Allow-Methods", "OPTIONS, POST") rw.WriteHeader(http.StatusNoContent) // 204 } func cacheFor(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", 365*24*60*60)) rw.Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(time.RFC1123)) rw.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", 365*24*60*60)) } func noCache(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") } func welcomeHandler(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "text/plain;charset=UTF-8") fmt.Fprintf(rw, "Welcome to SockJS!\n") } func httpError(w http.ResponseWriter, error string, code int) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(code) fmt.Fprintf(w, error) } sockjs-go-3.0.2/sockjs/web_test.go000066400000000000000000000101761414327410500170600ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestXhrCors(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{}) xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "*" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "*") } req.Header.Set("origin", "localhost") xhrCors(recorder, req) acao = recorder.Header().Get("access-control-allow-origin") if acao != "localhost" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "localhost") } req.Header.Set("access-control-request-headers", "some value") rec := httptest.NewRecorder() xhrCors(rec, req) if rec.Header().Get("access-control-allow-headers") != "some value" { t.Errorf("Incorent value for ACAH, got %s", rec.Header().Get("access-control-allow-headers")) } rec = httptest.NewRecorder() xhrCors(rec, req) if rec.Header().Get("access-control-allow-credentials") != "true" { t.Errorf("Incorent value for ACAC, got %s", rec.Header().Get("access-control-allow-credentials")) } // verify that if Access-Control-Allow-Credentials was previously set that xhrCors() does not duplicate the value rec = httptest.NewRecorder() rec.Header().Set("Access-Control-Allow-Credentials", "true") xhrCors(rec, req) acac := rec.Header()["Access-Control-Allow-Credentials"] if len(acac) != 1 || acac[0] != "true" { t.Errorf("Incorent value for ACAC, got %s", strings.Join(acac, ",")) } } func TestCheckOriginCORSAllowedNullOrigin(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{ CheckOrigin: func(req *http.Request) bool { return true }, }) req.Header.Set("origin", "null") xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "null" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "null") } } func TestCheckOriginCORSAllowedEmptyOrigin(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{ CheckOrigin: func(req *http.Request) bool { return true }, }) xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "*" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "*") } } func TestCheckOriginCORSNotAllowed(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{ CheckOrigin: func(req *http.Request) bool { return false }, }) req.Header.Set("origin", "localhost") xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "") } } func TestXhrOptions(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrOptions(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("Wrong response status code, expected %d, got %d", http.StatusNoContent, rec.Code) } } func TestCacheFor(t *testing.T) { rec := httptest.NewRecorder() cacheFor(rec, nil) cacheControl := rec.Header().Get("cache-control") if cacheControl != "public, max-age=31536000" { t.Errorf("Incorrect cache-control header value, got '%s'", cacheControl) } expires := rec.Header().Get("expires") if expires == "" { t.Errorf("Expires header should not be empty") // TODO(igm) check proper formating of string } maxAge := rec.Header().Get("access-control-max-age") if maxAge != "31536000" { t.Errorf("Incorrect value for access-control-max-age, got '%s'", maxAge) } } func TestNoCache(t *testing.T) { rec := httptest.NewRecorder() noCache(rec, nil) } func TestWelcomeHandler(t *testing.T) { rec := httptest.NewRecorder() welcomeHandler(rec, nil) if rec.Body.String() != "Welcome to SockJS!\n" { t.Errorf("Incorrect welcome message received, got '%s'", rec.Body.String()) } } sockjs-go-3.0.2/sockjs/websocket.go000066400000000000000000000053051414327410500172300ustar00rootroot00000000000000package sockjs import ( "fmt" "net/http" "strings" "time" "github.com/gorilla/websocket" ) // WebSocketReadBufSize is a parameter that is used for WebSocket Upgrader. // https://github.com/gorilla/websocket/blob/master/server.go#L230 var WebSocketReadBufSize = 4096 // WebSocketWriteBufSize is a parameter that is used for WebSocket Upgrader // https://github.com/gorilla/websocket/blob/master/server.go#L230 var WebSocketWriteBufSize = 4096 func (h *handler) sockjsWebsocket(rw http.ResponseWriter, req *http.Request) { var conn *websocket.Conn var err error if h.options.WebsocketUpgrader != nil { conn, err = h.options.WebsocketUpgrader.Upgrade(rw, req, nil) } else { // use default as before, so that those 2 buffer size variables are used as before conn, err = websocket.Upgrade(rw, req, nil, WebSocketReadBufSize, WebSocketWriteBufSize) } if _, ok := err.(websocket.HandshakeError); ok { http.Error(rw, `Can "Upgrade" only to "WebSocket".`, http.StatusBadRequest) return } else if err != nil { rw.WriteHeader(http.StatusInternalServerError) return } sessID, _ := h.parseSessionID(req.URL) sess := newSession(req, sessID, h.options.DisconnectDelay, h.options.HeartbeatDelay) receiver := newWsReceiver(conn, h.options.WebsocketWriteTimeout) sess.attachReceiver(receiver) if h.handlerFunc != nil { go h.handlerFunc(sess) } readCloseCh := make(chan struct{}) go func() { var d []string for { err := conn.ReadJSON(&d) if err != nil { close(readCloseCh) return } sess.accept(d...) } }() select { case <-readCloseCh: case <-receiver.doneNotify(): } sess.close() conn.Close() } type wsReceiver struct { conn *websocket.Conn closeCh chan struct{} writeTimeout time.Duration } func newWsReceiver(conn *websocket.Conn, writeTimeout time.Duration) *wsReceiver { return &wsReceiver{ conn: conn, closeCh: make(chan struct{}), writeTimeout: writeTimeout, } } func (w *wsReceiver) sendBulk(messages ...string) { if len(messages) > 0 { w.sendFrame(fmt.Sprintf("a[%s]", strings.Join(transform(messages, quote), ","))) } } func (w *wsReceiver) sendFrame(frame string) { if w.writeTimeout != 0 { w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)) } if err := w.conn.WriteMessage(websocket.TextMessage, []byte(frame)); err != nil { w.close() } } func (w *wsReceiver) close() { select { case <-w.closeCh: // already closed default: close(w.closeCh) } } func (w *wsReceiver) canSend() bool { select { case <-w.closeCh: // already closed return false default: return true } } func (w *wsReceiver) doneNotify() <-chan struct{} { return w.closeCh } func (w *wsReceiver) interruptedNotify() <-chan struct{} { return nil } sockjs-go-3.0.2/sockjs/websocket_test.go000066400000000000000000000131511414327410500202650ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gorilla/websocket" ) func TestHandler_WebSocketHandshakeError(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.Close() req, _ := http.NewRequest("GET", server.URL, nil) req.Header.Set("origin", "https"+server.URL[4:]) resp, err := http.DefaultClient.Do(req) if err != nil { t.Errorf("There should not be any error, got '%s'", err) t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", resp.StatusCode, http.StatusBadRequest) } } func TestHandler_WebSocket(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.CloseClientConnections() url := "ws" + server.URL[4:] var connCh = make(chan Session) h.handlerFunc = func(conn Session) { connCh <- conn } conn, resp, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { t.Errorf("Unexpected error '%v'", err) t.FailNow() } if conn == nil { t.Errorf("Connection should not be nil") t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusSwitchingProtocols { t.Errorf("Wrong response code returned, got '%d', expected '%d'", resp.StatusCode, http.StatusSwitchingProtocols) } select { case <-connCh: //ok case <-time.After(10 * time.Millisecond): t.Errorf("Sockjs Handler not invoked") } } func TestHandler_WebSocketTerminationByServer(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.Close() url := "ws" + server.URL[4:] h.handlerFunc = func(conn Session) { conn.Close(1024, "some close message") conn.Close(0, "this should be ignored") } conn, _, err := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) if err != nil { t.Fatalf("websocket dial failed: %v", err) t.FailNow() } if conn == nil { t.Errorf("Connection should not be nil") t.FailNow() } _, msg, err := conn.ReadMessage() if string(msg) != "o" || err != nil { t.Errorf("Open frame expected, got '%s' and error '%v', expected '%s' without error", msg, err, "o") } _, msg, err = conn.ReadMessage() if string(msg) != `c[1024,"some close message"]` || err != nil { t.Errorf("Close frame expected, got '%s' and error '%v', expected '%s' without error", msg, err, `c[1024,"some close message"]`) } _, msg, err = conn.ReadMessage() // gorilla websocket keeps `errUnexpectedEOF` private so we need to introspect the error message if err != nil { if !strings.Contains(err.Error(), "unexpected EOF") { t.Errorf("Expected 'unexpected EOF' error or similar, got '%v'", err) } } } func TestHandler_WebSocketTerminationByClient(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.Close() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { if _, err := conn.Recv(); err != ErrSessionNotOpen { t.Errorf("Recv should fail") } close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) if conn == nil { t.Errorf("Connection should not be nil") t.FailNow() } conn.Close() <-done } func TestHandler_WebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketWriteTimeout = time.Second server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) // defer server.CloseClientConnections() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { conn.Send("message 1") conn.Send("message 2") msg, err := conn.Recv() if msg != "message 3" || err != nil { t.Errorf("Got '%s', expected '%s'", msg, "message 3") } conn.Close(123, "close") close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) conn.WriteJSON([]string{"message 3"}) var expected = []string{"o", `a["message 1"]`, `a["message 2"]`, `c[123,"close"]`} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } func TestHandler_CustomWebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketUpgrader = &websocket.Upgrader{ ReadBufferSize: 0, WriteBufferSize: 0, CheckOrigin: func(_ *http.Request) bool { return true }, Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {}, } h.options.WebsocketWriteTimeout = time.Second server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { conn.Send("message 1") conn.Send("message 2") msg, err := conn.Recv() if msg != "message 3" || err != nil { t.Errorf("Got '%s', expected '%s'", msg, "message 3") } conn.Close(123, "close") close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) conn.WriteJSON([]string{"message 3"}) var expected = []string{"o", `a["message 1"]`, `a["message 2"]`, `c[123,"close"]`} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } sockjs-go-3.0.2/sockjs/xhr.go000066400000000000000000000047661414327410500160550ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" "io" "net/http" "strings" ) var ( cFrame = closeFrame(2010, "Another connection still open") xhrStreamingPrelude = strings.Repeat("h", 2048) ) func (h *handler) xhrSend(rw http.ResponseWriter, req *http.Request) { if req.Body == nil { httpError(rw, "Payload expected.", http.StatusInternalServerError) return } var messages []string err := json.NewDecoder(req.Body).Decode(&messages) if err == io.EOF { httpError(rw, "Payload expected.", http.StatusInternalServerError) return } if _, ok := err.(*json.SyntaxError); ok || err == io.ErrUnexpectedEOF { httpError(rw, "Broken JSON encoding.", http.StatusInternalServerError) return } sessionID, err := h.parseSessionID(req.URL) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.sessionsMux.Lock() defer h.sessionsMux.Unlock() if sess, ok := h.sessions[sessionID]; !ok { http.NotFound(rw, req) } else { _ = sess.accept(messages...) // TODO(igm) reponse with SISE in case of err? rw.Header().Set("content-type", "text/plain; charset=UTF-8") // Ignored by net/http (but protocol test complains), see https://code.google.com/p/go/source/detail?r=902dc062bff8 rw.WriteHeader(http.StatusNoContent) } } type xhrFrameWriter struct{} func (*xhrFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "%s\n", frame) } func (h *handler) xhrPoll(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "application/javascript; charset=UTF-8") sess, _ := h.sessionByRequest(req) // TODO(igm) add err handling, although err should not happen as handler should not pass req in that case receiver := newHTTPReceiver(rw, 1, new(xhrFrameWriter)) if err := sess.attachReceiver(receiver); err != nil { receiver.sendFrame(cFrame) receiver.close() return } select { case <-receiver.doneNotify(): case <-receiver.interruptedNotify(): } } func (h *handler) xhrStreaming(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "application/javascript; charset=UTF-8") fmt.Fprintf(rw, "%s\n", xhrStreamingPrelude) rw.(http.Flusher).Flush() sess, _ := h.sessionByRequest(req) receiver := newHTTPReceiver(rw, h.options.ResponseLimit, new(xhrFrameWriter)) if err := sess.attachReceiver(receiver); err != nil { receiver.sendFrame(cFrame) receiver.close() return } select { case <-receiver.doneNotify(): case <-receiver.interruptedNotify(): } } sockjs-go-3.0.2/sockjs/xhr_test.go000066400000000000000000000144231414327410500171030ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestHandler_XhrSendNilBody(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/non_existing_session/xhr_send", nil) h.xhrSend(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusInternalServerError) } if rec.Body.String() != "Payload expected." { t.Errorf("Unexcpected body received: '%s'", rec.Body.String()) } } func TestHandler_XhrSendEmptyBody(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/non_existing_session/xhr_send", strings.NewReader("")) h.xhrSend(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusInternalServerError) } if rec.Body.String() != "Payload expected." { t.Errorf("Unexcpected body received: '%s'", rec.Body.String()) } } func TestHandler_XhrSendWrongUrlPath(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "incorrect", strings.NewReader("[\"a\"]")) h.xhrSend(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("Unexcpected response status, got '%d', expected '%d'", rec.Code, http.StatusInternalServerError) } } func TestHandler_XhrSendToExistingSession(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"some message\"]")) sess := newSession(req, "session", time.Second, time.Second) h.sessions["session"] = sess req, _ = http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"some message\"]")) var done = make(chan bool) go func() { h.xhrSend(rec, req) done <- true }() msg, _ := sess.Recv() if msg != "some message" { t.Errorf("Incorrect message in the channel, should be '%s', was '%s'", "some message", msg) } <-done if rec.Code != http.StatusNoContent { t.Errorf("Wrong response status received %d, should be %d", rec.Code, http.StatusNoContent) } if rec.Header().Get("content-type") != "text/plain; charset=UTF-8" { t.Errorf("Wrong content type received '%s'", rec.Header().Get("content-type")) } } func TestHandler_XhrSendInvalidInput(t *testing.T) { h := newTestHandler() req, _ := http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("some invalid message frame")) rec := httptest.NewRecorder() h.xhrSend(rec, req) if rec.Code != http.StatusInternalServerError || rec.Body.String() != "Broken JSON encoding." { t.Errorf("Unexpected response, got '%d,%s' expected '%d,Broken JSON encoding.'", rec.Code, rec.Body.String(), http.StatusInternalServerError) } // unexpected EOF req, _ = http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"x")) rec = httptest.NewRecorder() h.xhrSend(rec, req) if rec.Code != http.StatusInternalServerError || rec.Body.String() != "Broken JSON encoding." { t.Errorf("Unexpected response, got '%d,%s' expected '%d,Broken JSON encoding.'", rec.Code, rec.Body.String(), http.StatusInternalServerError) } } func TestHandler_XhrSendSessionNotFound(t *testing.T) { h := handler{} req, _ := http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"some message\"]")) rec := httptest.NewRecorder() h.xhrSend(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusNotFound) } } func TestHandler_XhrPoll(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr", nil) h.xhrPoll(rw, req) if rw.Header().Get("content-type") != "application/javascript; charset=UTF-8" { t.Errorf("Wrong content type received, got '%s'", rw.Header().Get("content-type")) } } func TestHandler_XhrPollConnectionInterrupted(t *testing.T) { h := newTestHandler() sess := newTestSession() sess.state = SessionActive h.sessions["session"] = sess req, _ := http.NewRequest("POST", "/server/session/xhr", nil) rw := newClosableRecorder() close(rw.closeNotifCh) h.xhrPoll(rw, req) time.Sleep(1 * time.Millisecond) sess.Lock() if sess.state != SessionClosed { t.Errorf("Session should be closed") } } func TestHandler_XhrPollAnotherConnectionExists(t *testing.T) { h := newTestHandler() req, _ := http.NewRequest("POST", "/server/session/xhr", nil) // turn of timeoutes and heartbeats sess := newSession(req, "session", time.Hour, time.Hour) h.sessions["session"] = sess sess.attachReceiver(newTestReceiver()) req, _ = http.NewRequest("POST", "/server/session/xhr", nil) rw2 := httptest.NewRecorder() h.xhrPoll(rw2, req) if rw2.Body.String() != "c[2010,\"Another connection still open\"]\n" { t.Errorf("Unexpected body, got '%s'", rw2.Body) } } func TestHandler_XhrStreaming(t *testing.T) { h := newTestHandler() rw := newClosableRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr_streaming", nil) h.xhrStreaming(rw, req) expectedBody := strings.Repeat("h", 2048) + "\no\n" if rw.Body.String() != expectedBody { t.Errorf("Unexpected body, got '%s' expected '%s'", rw.Body, expectedBody) } } func TestHandler_XhrStreamingAnotherReceiver(t *testing.T) { h := newTestHandler() h.options.ResponseLimit = 4096 rw1 := newClosableRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr_streaming", nil) go func() { rec := httptest.NewRecorder() h.xhrStreaming(rec, req) expectedBody := strings.Repeat("h", 2048) + "\n" + "c[2010,\"Another connection still open\"]\n" if rec.Body.String() != expectedBody { t.Errorf("Unexpected body got '%s', expected '%s', ", rec.Body, expectedBody) } close(rw1.closeNotifCh) }() h.xhrStreaming(rw1, req) } // various test only structs func newTestHandler() *handler { h := &handler{sessions: make(map[string]*session)} h.options.HeartbeatDelay = time.Hour h.options.DisconnectDelay = time.Hour return h } type ClosableRecorder struct { *httptest.ResponseRecorder closeNotifCh chan bool } func newClosableRecorder() *ClosableRecorder { return &ClosableRecorder{httptest.NewRecorder(), make(chan bool)} } func (cr *ClosableRecorder) CloseNotify() <-chan bool { return cr.closeNotifCh } sockjs-go-3.0.2/testserver/000077500000000000000000000000001414327410500156225ustar00rootroot00000000000000sockjs-go-3.0.2/testserver/server.go000066400000000000000000000034011414327410500174550ustar00rootroot00000000000000package main import ( "log" "net/http" "strings" "github.com/igm/sockjs-go/v3/sockjs" ) type testHandler struct { prefix string handler http.Handler } func newSockjsHandler(prefix string, options sockjs.Options, fn func(sockjs.Session)) *testHandler { return &testHandler{prefix, sockjs.NewHandler(prefix, options, fn)} } type testHandlers []*testHandler func main() { // prepare various options for tests echoOptions := sockjs.DefaultOptions echoOptions.ResponseLimit = 4096 echoOptions.RawWebsocket = true disabledWebsocketOptions := sockjs.DefaultOptions disabledWebsocketOptions.Websocket = false cookieNeededOptions := sockjs.DefaultOptions cookieNeededOptions.JSessionID = sockjs.DefaultJSessionID closeOptions := sockjs.DefaultOptions closeOptions.RawWebsocket = true // register various test handlers var handlers = []*testHandler{ newSockjsHandler("/echo", echoOptions, echoHandler), newSockjsHandler("/cookie_needed_echo", cookieNeededOptions, echoHandler), newSockjsHandler("/close", closeOptions, closeHandler), newSockjsHandler("/disabled_websocket_echo", disabledWebsocketOptions, echoHandler), } log.Fatal(http.ListenAndServe("localhost:8081", testHandlers(handlers))) } func (t testHandlers) ServeHTTP(rw http.ResponseWriter, req *http.Request) { for _, handler := range t { if strings.HasPrefix(req.URL.Path, handler.prefix) { handler.handler.ServeHTTP(rw, req) return } } http.NotFound(rw, req) } func closeHandler(conn sockjs.Session) { conn.Close(3000, "Go away!") } func echoHandler(conn sockjs.Session) { log.Println("New connection created") for { if msg, err := conn.Recv(); err != nil { break } else { if err := conn.Send(msg); err != nil { break } } } log.Println("Sessionection closed") } sockjs-go-3.0.2/v3/000077500000000000000000000000001414327410500137445ustar00rootroot00000000000000sockjs-go-3.0.2/v3/go.mod000066400000000000000000000002021414327410500150440ustar00rootroot00000000000000module github.com/igm/sockjs-go/v3 go 1.14 require ( github.com/gorilla/websocket v1.4.2 github.com/stretchr/testify v1.5.1 ) sockjs-go-3.0.2/v3/go.sum000066400000000000000000000021671414327410500151050ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= sockjs-go-3.0.2/v3/sockjs/000077500000000000000000000000001414327410500152405ustar00rootroot00000000000000sockjs-go-3.0.2/v3/sockjs/benchmarks_test.go000066400000000000000000000065301414327410500207470ustar00rootroot00000000000000package sockjs import ( "bufio" "flag" "fmt" "log" "net/http" "net/http/httptest" "net/url" "strings" "sync" "testing" "time" "github.com/gorilla/websocket" ) func BenchmarkSimple(b *testing.B) { var messages = make(chan string, 10) h := NewHandler("/echo", DefaultOptions, func(session Session) { for m := range messages { _ = session.Send(m) } _ = session.Close(1024, "Close") }) server := httptest.NewServer(h) defer server.Close() req, _ := http.NewRequest("POST", server.URL+fmt.Sprintf("/echo/server/%d/xhr_streaming", 1000), nil) resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } for n := 0; n < b.N; n++ { messages <- "some message" } fmt.Println(b.N) close(messages) resp.Body.Close() } func BenchmarkMessages(b *testing.B) { msg := strings.Repeat("m", 10) h := NewHandler("/echo", DefaultOptions, func(session Session) { for n := 0; n < b.N; n++ { _ = session.Send(msg) } _ = session.Close(1024, "Close") }) server := httptest.NewServer(h) var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(session int) { reqc := 0 req, _ := http.NewRequest("POST", server.URL+fmt.Sprintf("/echo/server/%d/xhr_streaming", session), nil) for { reqc++ resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } reader := bufio.NewReader(resp.Body) for { line, err := reader.ReadString('\n') if err != nil { goto AGAIN } if strings.HasPrefix(line, "data: c[1024") { resp.Body.Close() goto DONE } } AGAIN: resp.Body.Close() } DONE: wg.Done() }(i) } wg.Wait() server.Close() } var size = flag.Int("size", 4*1024, "Size of one message.") func BenchmarkMessageWebsocket(b *testing.B) { flag.Parse() msg := strings.Repeat("x", *size) wsFrame := []byte(fmt.Sprintf("[%q]", msg)) opts := Options{ Websocket: true, SockJSURL: "//cdnjs.cloudflare.com/ajax/libs/sockjs-client/0.3.4/sockjs.min.js", HeartbeatDelay: time.Hour, DisconnectDelay: time.Hour, ResponseLimit: uint32(*size), } h := NewHandler("/echo", opts, func(session Session) { for { msg, err := session.Recv() if err != nil { if session.GetSessionState() != SessionActive { break } b.Fatalf("Recv()=%s", err) } if err := session.Send(msg); err != nil { b.Fatalf("Send()=%s", err) } } }) server := httptest.NewServer(h) defer server.Close() url := "ws" + server.URL[4:] + "/echo/server/0/websocket" client, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { b.Fatalf("Dial()=%s", err) } _, p, err := client.ReadMessage() if err != nil || string(p) != "o" { b.Fatalf("failed to start new session: frame=%v, err=%v", p, err) } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if err := client.WriteMessage(websocket.TextMessage, wsFrame); err != nil { b.Fatalf("WriteMessage()=%s", err) } if _, _, err := client.ReadMessage(); err != nil { b.Fatalf("ReadMessage()=%s", err) } } if err := client.Close(); err != nil { b.Fatalf("Close()=%s", err) } } func BenchmarkHandler_ParseSessionID(b *testing.B) { h := Handler{prefix: "/prefix"} url, _ := url.Parse("http://server:80/prefix/server/session/whatever") b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = h.parseSessionID(url) } } sockjs-go-3.0.2/v3/sockjs/buffer.go000066400000000000000000000016011414327410500170360ustar00rootroot00000000000000package sockjs import ( "context" "sync" ) // messageBuffer is an unbounded buffer that blocks on // pop if it's empty until the new element is enqueued. type messageBuffer struct { popCh chan string closeCh chan struct{} once sync.Once // for b.close() } func newMessageBuffer() *messageBuffer { return &messageBuffer{ popCh: make(chan string), closeCh: make(chan struct{}), } } func (b *messageBuffer) push(messages ...string) error { for _, message := range messages { select { case b.popCh <- message: case <-b.closeCh: return ErrSessionNotOpen } } return nil } func (b *messageBuffer) pop(ctx context.Context) (string, error) { select { case msg := <-b.popCh: return msg, nil case <-b.closeCh: return "", ErrSessionNotOpen case <-ctx.Done(): return "", ctx.Err() } } func (b *messageBuffer) close() { b.once.Do(func() { close(b.closeCh) }) } sockjs-go-3.0.2/v3/sockjs/doc.go000066400000000000000000000001311414327410500163270ustar00rootroot00000000000000/* Package sockjs is a server side implementation of sockjs protocol. */ package sockjs sockjs-go-3.0.2/v3/sockjs/eventsource.go000066400000000000000000000023231414327410500201310ustar00rootroot00000000000000package sockjs import ( "fmt" "io" "net/http" "net/url" "strings" ) func (h *Handler) eventSource(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "text/event-stream; charset=UTF-8") _, _ = fmt.Fprint(rw, "\r\n") rw.(http.Flusher).Flush() recv := newHTTPReceiver(rw, req, h.options.ResponseLimit, new(eventSourceFrameWriter), ReceiverTypeEventSource) sess, err := h.sessionByRequest(req) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if err := sess.attachReceiver(recv); err != nil { if err := recv.sendFrame(cFrame); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } recv.close() return } sess.startHandlerOnce.Do(func() { go h.handlerFunc(Session{sess}) }) select { case <-recv.doneNotify(): case <-recv.interruptedNotify(): } } type eventSourceFrameWriter struct{} var escaper *strings.Replacer = strings.NewReplacer( "%", url.QueryEscape("%"), "\n", url.QueryEscape("\n"), "\r", url.QueryEscape("\r"), "\x00", url.QueryEscape("\x00"), ) func (*eventSourceFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "data: %s\r\n\r\n", escaper.Replace(frame)) } sockjs-go-3.0.2/v3/sockjs/eventsource_integration_stage_test.go000066400000000000000000000066371414327410500247720ustar00rootroot00000000000000package sockjs_test import ( "bytes" "encoding/json" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" "github.com/igm/sockjs-go/v3/sockjs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type eventSourceStage struct { t *testing.T handler *sockjs.Handler server *httptest.Server resp *http.Response err error session sockjs.Session haveSession chan struct{} receivedMessages chan string } func newEventSourceStage(t *testing.T) (*eventSourceStage, *eventSourceStage, *eventSourceStage) { stage := &eventSourceStage{ t: t, haveSession: make(chan struct{}), receivedMessages: make(chan string, 1024), } return stage, stage, stage } func (s *eventSourceStage) a_new_sockjs_handler_is_created() *eventSourceStage { s.handler = sockjs.NewHandler("/prefix", sockjs.DefaultOptions, func(sess sockjs.Session) { s.session = sess close(s.haveSession) for { msg, err := sess.Recv() if err == sockjs.ErrSessionNotOpen { return } require.NoError(s.t, err) s.receivedMessages <- msg } }) return s } func (s *eventSourceStage) a_server_is_started() *eventSourceStage { s.server = httptest.NewServer(s.handler) return s } func (s *eventSourceStage) a_sockjs_eventsource_connection_is_received() *eventSourceStage { s.resp, s.err = http.Get(s.server.URL + "/prefix/123/456/eventsource") return s } func (s *eventSourceStage) handler_is_invoked_with_session() *eventSourceStage { select { case <-s.haveSession: case <-time.After(1 * time.Second): s.t.Fatal("no session was created") } assert.Equal(s.t, sockjs.ReceiverTypeEventSource, s.session.ReceiverType()) return s } func (s *eventSourceStage) session_is_closed() *eventSourceStage { s.session.Close(1024, "Close") assert.Error(s.t, s.session.Context().Err()) select { case <-s.session.Context().Done(): case <-time.After(1 * time.Second): s.t.Fatal("context should have been done") } return s } func (s *eventSourceStage) valid_eventsource_frames_should_be_received() *eventSourceStage { require.NoError(s.t, s.err) assert.Equal(s.t, "text/event-stream; charset=UTF-8", s.resp.Header.Get("content-type")) assert.Equal(s.t, "true", s.resp.Header.Get("access-control-allow-credentials")) assert.Equal(s.t, "*", s.resp.Header.Get("access-control-allow-origin")) all, err := ioutil.ReadAll(s.resp.Body) require.NoError(s.t, err) expectedBody := "\r\ndata: o\r\n\r\ndata: c[1024,\"Close\"]\r\n\r\n" assert.Equal(s.t, expectedBody, string(all)) return s } func (s *eventSourceStage) a_message_is_sent_from_client(msg string) *eventSourceStage { out, err := json.Marshal([]string{msg}) require.NoError(s.t, err) r, err := http.Post(s.server.URL+"/prefix/123/456/xhr_send", "application/json", bytes.NewReader(out)) require.NoError(s.t, err) require.Equal(s.t, http.StatusNoContent, r.StatusCode) return s } func (s *eventSourceStage) same_message_should_be_received_from_session(expectredMsg string) *eventSourceStage { select { case msg := <-s.receivedMessages: assert.Equal(s.t, expectredMsg, msg) case <-time.After(1 * time.Second): s.t.Fatal("no message was received") } return s } func (s *eventSourceStage) and() *eventSourceStage { return s } func (s *eventSourceStage) a_server_is_started_with_handler() *eventSourceStage { s.a_new_sockjs_handler_is_created() s.a_server_is_started() return s } sockjs-go-3.0.2/v3/sockjs/eventsource_intergration_test.go000066400000000000000000000014131414327410500237540ustar00rootroot00000000000000package sockjs_test import ( "testing" ) func TestEventSource(t *testing.T) { given, when, then := newEventSourceStage(t) given. a_new_sockjs_handler_is_created().and(). a_server_is_started() when. a_sockjs_eventsource_connection_is_received().and(). handler_is_invoked_with_session().and(). session_is_closed() then. valid_eventsource_frames_should_be_received() } func TestEventSourceMessageInteraction(t *testing.T) { given, when, then := newEventSourceStage(t) given. a_server_is_started_with_handler(). a_sockjs_eventsource_connection_is_received(). handler_is_invoked_with_session() when. a_message_is_sent_from_client("Hello World!").and(). session_is_closed() then. same_message_should_be_received_from_session("Hello World!") } sockjs-go-3.0.2/v3/sockjs/eventsource_test.go000066400000000000000000000055601414327410500211760ustar00rootroot00000000000000package sockjs import ( "bytes" "context" "net/http" "net/http/httptest" "runtime" "testing" "time" ) func TestHandler_EventSource(t *testing.T) { rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/eventsource", nil) h := newTestHandler() h.options.ResponseLimit = 1024 go func() { var sess *session for exists := false; !exists; { runtime.Gosched() h.sessionsMux.Lock() sess, exists = h.sessions["session"] h.sessionsMux.Unlock() } for exists := false; !exists; { runtime.Gosched() sess.mux.RLock() exists = sess.recv != nil sess.mux.RUnlock() } if rt := sess.ReceiverType(); rt != ReceiverTypeEventSource { t.Errorf("Unexpected recevier type, got '%v', extected '%v'", rt, ReceiverTypeEventSource) } sess.mux.RLock() sess.recv.close() sess.mux.RUnlock() }() h.eventSource(rw, req) contentType := rw.Header().Get("content-type") expected := "text/event-stream; charset=UTF-8" if contentType != expected { t.Errorf("Unexpected content type, got '%s', extected '%s'", contentType, expected) } if rw.Code != http.StatusOK { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusOK) } if rw.Body.String() != "\r\ndata: o\r\n\r\n" { t.Errorf("Event stream prelude, got '%s'", rw.Body) } } func TestHandler_EventSourceMultipleConnections(t *testing.T) { h := newTestHandler() h.options.ResponseLimit = 1024 rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/sess/eventsource", nil) go func() { rw := httptest.NewRecorder() h.eventSource(rw, req) if rw.Body.String() != "\r\ndata: c[2010,\"Another connection still open\"]\r\n\r\n" { t.Errorf("wrong, got '%v'", rw.Body) } h.sessionsMux.Lock() sess := h.sessions["sess"] sess.close() h.sessionsMux.Unlock() }() h.eventSource(rw, req) } func TestHandler_EventSourceConnectionInterrupted(t *testing.T) { h := newTestHandler() sess := newTestSession() sess.state = SessionActive h.sessions["session"] = sess req, _ := http.NewRequest("POST", "/server/session/eventsource", nil) ctx, cancel := context.WithCancel(req.Context()) req = req.WithContext(ctx) rw := httptest.NewRecorder() cancel() h.eventSource(rw, req) select { case <-sess.closeCh: case <-time.After(1 * time.Second): t.Errorf("session close channel should be closed") } sess.mux.Lock() if sess.state != SessionClosed { t.Errorf("session should be closed") } } func TestEventSourceFrameWriter(t *testing.T) { writer := new(eventSourceFrameWriter) out := new(bytes.Buffer) // Confirm that "important" characters are escaped, but others pass // through unmodified. _, err := writer.write(out, "escaped: %\r\n;unescaped: +&#") if err != nil { t.Errorf("unexpected write error: %s", err) } if out.String() != "data: escaped: %25%0D%0A;unescaped: +&#\r\n\r\n" { t.Errorf("wrong, got '%v'", out.String()) } } sockjs-go-3.0.2/v3/sockjs/example_handler_test.go000066400000000000000000000014241414327410500217570ustar00rootroot00000000000000package sockjs_test import ( "net/http" "github.com/igm/sockjs-go/v3/sockjs" ) func ExampleNewHandler_simple() { handler := sockjs.NewHandler("/echo", sockjs.DefaultOptions, func(session sockjs.Session) { for { if msg, err := session.Recv(); err == nil { if session.Send(msg) != nil { break } } else { break } } }) _ = http.ListenAndServe(":8080", handler) } func ExampleNewHandler_defaultMux() { handler := sockjs.NewHandler("/echo", sockjs.DefaultOptions, func(session sockjs.Session) { for { if msg, err := session.Recv(); err == nil { if session.Send(msg) != nil { break } } else { break } } }) // need to provide path prefix for http.Mux http.Handle("/echo/", handler) _ = http.ListenAndServe(":8080", nil) } sockjs-go-3.0.2/v3/sockjs/frame.go000066400000000000000000000003201414327410500166540ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" ) func closeFrame(status uint32, reason string) string { bytes, _ := json.Marshal([]interface{}{status, reason}) return fmt.Sprintf("c%s", string(bytes)) } sockjs-go-3.0.2/v3/sockjs/frame_test.go000066400000000000000000000003231414327410500177160ustar00rootroot00000000000000package sockjs import "testing" func TestCloseFrame(t *testing.T) { cf := closeFrame(1024, "some close text") if cf != "c[1024,\"some close text\"]" { t.Errorf("Wrong close frame generated '%s'", cf) } } sockjs-go-3.0.2/v3/sockjs/handler.go000066400000000000000000000077141414327410500172150ustar00rootroot00000000000000package sockjs import ( "net/http" "net/url" "regexp" "strings" "sync" ) type Handler struct { prefix string options Options handlerFunc func(Session) mappings []*mapping sessionsMux sync.Mutex sessions map[string]*session } const sessionPrefix = "^/([^/.]+)/([^/.]+)" var sessionRegExp = regexp.MustCompile(sessionPrefix) // NewHandler creates new HTTP handler that conforms to the basic net/http.Handler interface. // It takes path prefix, options and sockjs handler function as parameters func NewHandler(prefix string, opts Options, handlerFunc func(Session)) *Handler { if handlerFunc == nil { handlerFunc = func(s Session) {} } h := &Handler{ prefix: prefix, options: opts, handlerFunc: handlerFunc, sessions: make(map[string]*session), } xhrCors := xhrCorsFactory(opts) h.mappings = []*mapping{ newMapping("GET", "^[/]?$", welcomeHandler), newMapping("OPTIONS", "^/info$", opts.cookie, xhrCors, cacheFor, opts.info), newMapping("GET", "^/info$", opts.cookie, xhrCors, noCache, opts.info), // XHR newMapping("POST", sessionPrefix+"/xhr_send$", opts.cookie, xhrCors, noCache, h.xhrSend), newMapping("OPTIONS", sessionPrefix+"/xhr_send$", opts.cookie, xhrCors, cacheFor, xhrOptions), newMapping("POST", sessionPrefix+"/xhr$", opts.cookie, xhrCors, noCache, h.xhrPoll), newMapping("OPTIONS", sessionPrefix+"/xhr$", opts.cookie, xhrCors, cacheFor, xhrOptions), newMapping("POST", sessionPrefix+"/xhr_streaming$", opts.cookie, xhrCors, noCache, h.xhrStreaming), newMapping("OPTIONS", sessionPrefix+"/xhr_streaming$", opts.cookie, xhrCors, cacheFor, xhrOptions), // EventStream newMapping("GET", sessionPrefix+"/eventsource$", opts.cookie, xhrCors, noCache, h.eventSource), // Htmlfile newMapping("GET", sessionPrefix+"/htmlfile$", opts.cookie, xhrCors, noCache, h.htmlFile), // JsonP newMapping("GET", sessionPrefix+"/jsonp$", opts.cookie, xhrCors, noCache, h.jsonp), newMapping("OPTIONS", sessionPrefix+"/jsonp$", opts.cookie, xhrCors, cacheFor, xhrOptions), newMapping("POST", sessionPrefix+"/jsonp_send$", opts.cookie, xhrCors, noCache, h.jsonpSend), // IFrame newMapping("GET", "^/iframe[0-9-.a-z_]*.html$", cacheFor, h.iframe), } if opts.Websocket { h.mappings = append(h.mappings, newMapping("GET", sessionPrefix+"/websocket$", h.sockjsWebsocket)) } if opts.RawWebsocket { h.mappings = append(h.mappings, newMapping("GET", "^/websocket$", h.rawWebsocket)) } return h } func (h *Handler) Prefix() string { return h.prefix } func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // iterate over mappings http.StripPrefix(h.prefix, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { var allowedMethods []string for _, mapping := range h.mappings { if match, method := mapping.matches(req); match == fullMatch { for _, hf := range mapping.chain { hf(rw, req) } return } else if match == pathMatch { allowedMethods = append(allowedMethods, method) } } if len(allowedMethods) > 0 { rw.Header().Set("allow", strings.Join(allowedMethods, ", ")) rw.Header().Set("Content-Type", "") rw.WriteHeader(http.StatusMethodNotAllowed) return } http.NotFound(rw, req) })).ServeHTTP(rw, req) } func (h *Handler) parseSessionID(url *url.URL) (string, error) { matches := sessionRegExp.FindStringSubmatch(url.Path) if len(matches) == 3 { return matches[2], nil } return "", errSessionParse } func (h *Handler) sessionByRequest(req *http.Request) (*session, error) { h.sessionsMux.Lock() defer h.sessionsMux.Unlock() sessionID, err := h.parseSessionID(req.URL) if err != nil { return nil, err } sess, exists := h.sessions[sessionID] if !exists { sess = newSession(req, sessionID, h.options.DisconnectDelay, h.options.HeartbeatDelay) h.sessions[sessionID] = sess go func() { <-sess.closeCh h.sessionsMux.Lock() delete(h.sessions, sessionID) h.sessionsMux.Unlock() }() } sess.setCurrentRequest(req) return sess, nil } sockjs-go-3.0.2/v3/sockjs/handler_test.go000066400000000000000000000107431414327410500202500ustar00rootroot00000000000000package sockjs import ( "encoding/json" "io/ioutil" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var testOptions = DefaultOptions func init() { testOptions.RawWebsocket = true } func TestHandler_Create(t *testing.T) { handler := NewHandler("/echo", testOptions, nil) if handler.Prefix() != "/echo" { t.Errorf("Prefix not properly set, got '%s' expected '%s'", handler.Prefix(), "/echo") } if handler.sessions == nil { t.Errorf("Handler session map not made") } server := httptest.NewServer(handler) defer server.Close() resp, err := http.Get(server.URL + "/echo") if err != nil { t.Errorf("There should not be any error, got '%s'", err) t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusOK { t.Errorf("Unexpected status code receiver, got '%d' expected '%d'", resp.StatusCode, http.StatusOK) } } func TestHandler_RootPrefixInfoHandler(t *testing.T) { infoOptions := testOptions jSessionCalled := false infoOptions.JSessionID = func(writer http.ResponseWriter, request *http.Request) { jSessionCalled = true } handler := NewHandler("", infoOptions, nil) if handler.Prefix() != "" { t.Errorf("Prefix not properly set, got '%s' expected '%s'", handler.Prefix(), "") } server := httptest.NewServer(handler) defer server.Close() resp, err := http.Get(server.URL + "/info") if err != nil { t.Errorf("There should not be any error, got '%s'", err) t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusOK { t.Errorf("Unexpected status code receiver, got '%d' expected '%d'", resp.StatusCode, http.StatusOK) t.FailNow() } assert.True(t, jSessionCalled) infoData, err := ioutil.ReadAll(resp.Body) if err != nil { t.Errorf("Error reading body: '%v'", err) } var i info err = json.Unmarshal(infoData, &i) if err != nil { t.Fatalf("Error unmarshaling info: '%v', data was: '%s'", err, string(infoData)) } if i.Websocket != true { t.Fatalf("Expected websocket to be true") } } func TestHandler_ParseSessionId(t *testing.T) { h := Handler{prefix: "/prefix"} url, _ := url.Parse("http://server:80/server/session/whatever") if session, err := h.parseSessionID(url); session != "session" || err != nil { t.Errorf("Wrong session parsed, got '%s' expected '%s' with error = '%v'", session, "session", err) } } func TestHandler_SessionByRequest(t *testing.T) { h := NewHandler("", testOptions, nil) h.options.DisconnectDelay = 10 * time.Millisecond var handlerFuncCalled = make(chan Session) h.handlerFunc = func(s Session) { handlerFuncCalled <- s } req, _ := http.NewRequest("POST", "/server/sessionid/whatever/follows", nil) sess, err := h.sessionByRequest(req) if sess == nil || err != nil { t.Errorf("session should be returned") // test handlerFunc was called select { case s := <-handlerFuncCalled: // ok if s.session != sess { t.Errorf("Handler was not passed correct session") } case <-time.After(100 * time.Millisecond): t.Errorf("HandlerFunc was not called") } } // test session is reused for multiple requests with same sessionID req2, _ := http.NewRequest("POST", "/server/sessionid/whatever", nil) if sess2, err := h.sessionByRequest(req2); sess2 != sess || err != nil { t.Errorf("Expected error, got session: '%v'", sess) } // test session expires after timeout time.Sleep(15 * time.Millisecond) h.sessionsMux.Lock() if _, exists := h.sessions["sessionid"]; exists { t.Errorf("session should not exist in handler after timeout") } h.sessionsMux.Unlock() // test proper behaviour in case URL is not correct req, _ = http.NewRequest("POST", "", nil) if _, err := h.sessionByRequest(req); err == nil { t.Errorf("Expected parser sessionID from URL error, got 'nil'") } } func TestHandler_StrictPathMatching(t *testing.T) { handler := NewHandler("/echo", testOptions, nil) server := httptest.NewServer(handler) defer server.Close() cases := []struct { url string expectedStatus int }{ {url: "/echo", expectedStatus: http.StatusOK}, {url: "/test/echo", expectedStatus: http.StatusNotFound}, {url: "/echo/test/test/echo", expectedStatus: http.StatusNotFound}, } for _, urlCase := range cases { t.Run(urlCase.url, func(t *testing.T) { resp, err := http.Get(server.URL + urlCase.url) require.NoError(t, err) assert.Equal(t, urlCase.expectedStatus, resp.StatusCode) }) } } sockjs-go-3.0.2/v3/sockjs/htmlfile.go000066400000000000000000000037721414327410500174040ustar00rootroot00000000000000package sockjs import ( "fmt" "io" "net/http" "regexp" "strings" ) var iframeTemplate = `

Don't panic!

` var invalidCallback = regexp.MustCompile(`[^a-zA-Z0-9_.]`) func init() { iframeTemplate += strings.Repeat(" ", 1024-len(iframeTemplate)+14) iframeTemplate += "\r\n\r\n" } func (h *Handler) htmlFile(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "text/html; charset=UTF-8") if err := req.ParseForm(); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } callback := req.Form.Get("c") if callback == "" { http.Error(rw, `"callback" parameter required`, http.StatusBadRequest) return } else if invalidCallback.MatchString(callback) { http.Error(rw, `invalid character in "callback" parameter`, http.StatusBadRequest) return } rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, iframeTemplate, callback) rw.(http.Flusher).Flush() sess, err := h.sessionByRequest(req) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } recv := newHTTPReceiver(rw, req, h.options.ResponseLimit, new(htmlfileFrameWriter), ReceiverTypeHtmlFile) if err := sess.attachReceiver(recv); err != nil { if err := recv.sendFrame(cFrame); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } recv.close() return } sess.startHandlerOnce.Do(func() { go h.handlerFunc(Session{sess}) }) select { case <-recv.doneNotify(): case <-recv.interruptedNotify(): } } type htmlfileFrameWriter struct{} func (*htmlfileFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "\r\n", quote(frame)) } sockjs-go-3.0.2/v3/sockjs/htmlfile_integration_stage_test.go000066400000000000000000000076041414327410500242270ustar00rootroot00000000000000package sockjs_test import ( "bytes" "encoding/json" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" "github.com/igm/sockjs-go/v3/sockjs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type htmlFileStage struct { t *testing.T handler *sockjs.Handler server *httptest.Server resp *http.Response err error session sockjs.Session haveSession chan struct{} receivedMessages chan string } func newHtmlFileStage(t *testing.T) (*htmlFileStage, *htmlFileStage, *htmlFileStage) { stage := &htmlFileStage{ t: t, haveSession: make(chan struct{}), receivedMessages: make(chan string, 1024), } return stage, stage, stage } func (s *htmlFileStage) a_new_sockjs_handler_is_created() *htmlFileStage { s.handler = sockjs.NewHandler("/prefix", sockjs.DefaultOptions, func(sess sockjs.Session) { s.session = sess close(s.haveSession) for { msg, err := sess.Recv() if err == sockjs.ErrSessionNotOpen { return } require.NoError(s.t, err) s.receivedMessages <- msg } }) return s } func (s *htmlFileStage) a_server_is_started() *htmlFileStage { s.server = httptest.NewServer(s.handler) return s } func (s *htmlFileStage) a_sockjs_htmlfile_connection_is_received() *htmlFileStage { s.resp, s.err = http.Get(s.server.URL + "/prefix/123/123/htmlfile?c=testCallback") return s } func (s *htmlFileStage) correct_http_response_should_be_received() *htmlFileStage { require.NoError(s.t, s.err) assert.Equal(s.t, http.StatusOK, s.resp.StatusCode) assert.Equal(s.t, "text/html; charset=UTF-8", s.resp.Header.Get("content-type")) assert.Equal(s.t, "true", s.resp.Header.Get("access-control-allow-credentials")) assert.Equal(s.t, "*", s.resp.Header.Get("access-control-allow-origin")) return s } func (s *htmlFileStage) handler_should_be_started_with_session() *htmlFileStage { select { case <-s.haveSession: case <-time.After(1 * time.Second): s.t.Fatal("no session was created") } assert.Equal(s.t, sockjs.ReceiverTypeHtmlFile, s.session.ReceiverType()) return s } func (s *htmlFileStage) session_is_closed() *htmlFileStage { require.NoError(s.t, s.session.Close(1024, "Close")) assert.Error(s.t, s.session.Context().Err()) select { case <-s.session.Context().Done(): case <-time.After(1 * time.Second): s.t.Fatal("context should have been done") } return s } func (s *htmlFileStage) valid_htmlfile_response_should_be_received() *htmlFileStage { all, err := ioutil.ReadAll(s.resp.Body) require.NoError(s.t, err) assert.Contains(s.t, string(all), `p("o");`, string(all)) assert.Contains(s.t, string(all), `p("c[1024,\"Close\"]")`, string(all)) assert.Contains(s.t, string(all), `var c = parent.testCallback;`, string(all)) return s } func (s *htmlFileStage) and() *htmlFileStage { return s } func (s *htmlFileStage) a_server_is_started_with_handler() *htmlFileStage { s.a_new_sockjs_handler_is_created() s.a_server_is_started() return s } func (s *htmlFileStage) active_session_is_closed() *htmlFileStage { s.session_is_active() s.session_is_closed() return s } func (s *htmlFileStage) session_is_active() *htmlFileStage { s.a_sockjs_htmlfile_connection_is_received() s.handler_should_be_started_with_session() return s } func (s *htmlFileStage) a_message_is_sent_from_client(msg string) *htmlFileStage { out, err := json.Marshal([]string{msg}) require.NoError(s.t, err) r, err := http.Post(s.server.URL+"/prefix/123/123/xhr_send", "application/json", bytes.NewReader(out)) require.NoError(s.t, err) require.Equal(s.t, http.StatusNoContent, r.StatusCode) return s } func (s *htmlFileStage) same_message_should_be_received_from_session(expectredMsg string) *htmlFileStage { select { case msg := <-s.receivedMessages: assert.Equal(s.t, expectredMsg, msg) case <-time.After(1 * time.Second): s.t.Fatal("no message was received") } return s } sockjs-go-3.0.2/v3/sockjs/htmlfile_intergration_test.go000066400000000000000000000016361414327410500232250ustar00rootroot00000000000000package sockjs_test import ( "testing" ) func TestHtmlFile_StartHandler(t *testing.T) { given, when, then := newHtmlFileStage(t) given. a_new_sockjs_handler_is_created().and(). a_server_is_started() when. a_sockjs_htmlfile_connection_is_received() then. correct_http_response_should_be_received().and(). handler_should_be_started_with_session() } func TestHtmlFile_CloseSession(t *testing.T) { given, when, then := newHtmlFileStage(t) given. a_server_is_started_with_handler() when. active_session_is_closed() then. valid_htmlfile_response_should_be_received() } func TestHtmlFile_SendMessage(t *testing.T) { given, when, then := newHtmlFileStage(t) given. a_server_is_started_with_handler() when. session_is_active().and(). a_message_is_sent_from_client("Hello World!").and(). active_session_is_closed() then. same_message_should_be_received_from_session("Hello World!") } sockjs-go-3.0.2/v3/sockjs/htmlfile_test.go000066400000000000000000000054021414327410500204330ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestHandler_htmlFileNoCallback(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/htmlfile", nil) h.htmlFile(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusBadRequest) } expectedContentType := "text/plain; charset=utf-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } } func TestHandler_htmlFile(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/htmlfile?c=testCallback", nil) h.htmlFile(rw, req) if rw.Code != http.StatusOK { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusOK) } expectedContentType := "text/html; charset=UTF-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content-type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } if rw.Body.String() != expectedIFrame { t.Errorf("Unexpected response body, got '%s', expected '%s'", rw.Body, expectedIFrame) } sess, _ := h.sessionByRequest(req) if rt := sess.ReceiverType(); rt != ReceiverTypeHtmlFile { t.Errorf("Unexpected recevier type, got '%v', extected '%v'", rt, ReceiverTypeHtmlFile) } } func TestHandler_cannotIntoXSS(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() // test simple injection req, _ := http.NewRequest("GET", "/server/session/htmlfile?c=fake%3Balert(1337)", nil) h.htmlFile(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusBadRequest) } h = newTestHandler() rw = httptest.NewRecorder() // test simple injection req, _ = http.NewRequest("GET", "/server/session/htmlfile?c=fake%2Dalert", nil) h.htmlFile(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusBadRequest) } } func init() { expectedIFrame += strings.Repeat(" ", 1024-len(expectedIFrame)+len("testCallack")+13) expectedIFrame += "\r\n\r\n" expectedIFrame += "\r\n" } var expectedIFrame = `

Don't panic!

` sockjs-go-3.0.2/v3/sockjs/httpreceiver.go000066400000000000000000000047571414327410500203100ustar00rootroot00000000000000package sockjs import ( "fmt" "io" "net/http" "strings" "sync" ) type frameWriter interface { write(writer io.Writer, frame string) (int, error) } type httpReceiverState int const ( stateHTTPReceiverActive httpReceiverState = iota stateHTTPReceiverClosed ) type httpReceiver struct { sync.Mutex state httpReceiverState frameWriter frameWriter rw http.ResponseWriter maxResponseSize uint32 currentResponseSize uint32 doneCh chan struct{} interruptCh chan struct{} recType ReceiverType } func newHTTPReceiver(rw http.ResponseWriter, req *http.Request, maxResponse uint32, frameWriter frameWriter, receiverType ReceiverType) *httpReceiver { recv := &httpReceiver{ rw: rw, frameWriter: frameWriter, maxResponseSize: maxResponse, doneCh: make(chan struct{}), interruptCh: make(chan struct{}), recType: receiverType, } ctx := req.Context() go func() { select { case <-ctx.Done(): recv.Lock() defer recv.Unlock() if recv.state < stateHTTPReceiverClosed { recv.state = stateHTTPReceiverClosed close(recv.interruptCh) } case <-recv.doneCh: // ok, no action needed here, receiver closed in correct way // just finish the routine } }() return recv } func (recv *httpReceiver) sendBulk(messages ...string) error { if len(messages) > 0 { return recv.sendFrame(fmt.Sprintf("a[%s]", strings.Join( transform(messages, quote), ",", ), )) } return nil } func (recv *httpReceiver) sendFrame(value string) error { recv.Lock() defer recv.Unlock() if recv.state == stateHTTPReceiverActive { n, err := recv.frameWriter.write(recv.rw, value) if err != nil { return err } recv.currentResponseSize += uint32(n) if recv.currentResponseSize >= recv.maxResponseSize { recv.state = stateHTTPReceiverClosed close(recv.doneCh) } else { recv.rw.(http.Flusher).Flush() } } return nil } func (recv *httpReceiver) doneNotify() <-chan struct{} { return recv.doneCh } func (recv *httpReceiver) interruptedNotify() <-chan struct{} { return recv.interruptCh } func (recv *httpReceiver) close() { recv.Lock() defer recv.Unlock() if recv.state < stateHTTPReceiverClosed { recv.state = stateHTTPReceiverClosed close(recv.doneCh) } } func (recv *httpReceiver) canSend() bool { recv.Lock() defer recv.Unlock() return recv.state != stateHTTPReceiverClosed } func (recv *httpReceiver) receiverType() ReceiverType { return recv.recType } sockjs-go-3.0.2/v3/sockjs/httpreceiver_test.go000066400000000000000000000072301414327410500213340ustar00rootroot00000000000000package sockjs import ( "context" "io" "net/http" "net/http/httptest" "testing" "time" ) type testFrameWriter struct { frames []string } func (t *testFrameWriter) write(w io.Writer, frame string) (int, error) { t.frames = append(t.frames, frame) return len(frame), nil } func TestHttpReceiver_Create(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", nil) recv := newHTTPReceiver(rec, req, 1024, new(testFrameWriter), ReceiverTypeNone) if recv.doneCh != recv.doneNotify() { t.Errorf("Calling done() must return close channel, but it does not") } if recv.rw != rec { t.Errorf("Http.ResponseWriter not properly initialized") } if recv.maxResponseSize != 1024 { t.Errorf("MaxResponseSize not properly initialized") } } func TestHttpReceiver_SendEmptyFrames(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", nil) recv := newHTTPReceiver(rec, req, 1024, new(testFrameWriter), ReceiverTypeNone) noError(t, recv.sendBulk()) if rec.Body.String() != "" { t.Errorf("Incorrect body content received from receiver '%s'", rec.Body.String()) } } func TestHttpReceiver_SendFrame(t *testing.T) { rec := httptest.NewRecorder() fw := new(testFrameWriter) req, _ := http.NewRequest("GET", "", nil) recv := newHTTPReceiver(rec, req, 1024, fw, ReceiverTypeNone) var frame = "some frame content" noError(t, recv.sendFrame(frame)) if len(fw.frames) != 1 || fw.frames[0] != frame { t.Errorf("Incorrect body content received, got '%s', expected '%s'", fw.frames, frame) } } func TestHttpReceiver_SendBulk(t *testing.T) { rec := httptest.NewRecorder() fw := new(testFrameWriter) req, _ := http.NewRequest("GET", "", nil) recv := newHTTPReceiver(rec, req, 1024, fw, ReceiverTypeNone) noError(t, recv.sendBulk("message 1", "message 2", "message 3")) expected := "a[\"message 1\",\"message 2\",\"message 3\"]" if len(fw.frames) != 1 || fw.frames[0] != expected { t.Errorf("Incorrect body content received from receiver, got '%s' expected '%s'", fw.frames, expected) } } func TestHttpReceiver_MaximumResponseSize(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", nil) recv := newHTTPReceiver(rec, req, 52, new(testFrameWriter), ReceiverTypeNone) noError(t, recv.sendBulk("message 1", "message 2")) // produces 26 bytes of response in 1 frame if recv.currentResponseSize != 26 { t.Errorf("Incorrect response size calcualated, got '%d' expected '%d'", recv.currentResponseSize, 26) } select { case <-recv.doneNotify(): t.Errorf("Receiver should not be done yet") default: // ok } noError(t, recv.sendBulk("message 1", "message 2")) // produces another 26 bytes of response in 1 frame to go over max resposne size select { case <-recv.doneNotify(): // ok default: t.Errorf("Receiver closed channel did not close") } } func TestHttpReceiver_Close(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", nil) recv := newHTTPReceiver(rec, req, 1024, nil, ReceiverTypeNone) recv.close() if recv.state != stateHTTPReceiverClosed { t.Errorf("Unexpected state, got '%d', expected '%d'", recv.state, stateHTTPReceiverClosed) } } func TestHttpReceiver_ConnectionInterrupt(t *testing.T) { rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", nil) ctx, cancel := context.WithCancel(req.Context()) req = req.WithContext(ctx) recv := newHTTPReceiver(rw, req, 1024, nil, ReceiverTypeNone) cancel() select { case <-recv.interruptCh: case <-time.After(1 * time.Second): t.Errorf("should interrupt") } if recv.state != stateHTTPReceiverClosed { t.Errorf("Unexpected state, got '%d', expected '%d'", recv.state, stateHTTPReceiverClosed) } } sockjs-go-3.0.2/v3/sockjs/iframe.go000066400000000000000000000023541414327410500170360ustar00rootroot00000000000000package sockjs import ( "crypto/md5" "fmt" "net/http" "text/template" ) var tmpl = template.Must(template.New("iframe").Parse(iframeBody)) func (h *Handler) iframe(rw http.ResponseWriter, req *http.Request) { etagReq := req.Header.Get("If-None-Match") hash := md5.New() if _, err := hash.Write([]byte(iframeBody)); err!=nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } etag := fmt.Sprintf("%x", hash.Sum(nil)) if etag == etagReq { rw.WriteHeader(http.StatusNotModified) return } rw.Header().Set("Content-Type", "text/html; charset=UTF-8") rw.Header().Add("ETag", etag) if err := tmpl.Execute(rw, h.options.SockJSURL); err!=nil { http.Error(rw, "could not render iframe content: "+err.Error(), http.StatusInternalServerError) return } } var iframeBody = `

Don't panic!

This is a SockJS hidden iframe. It's used for cross domain magic.

` sockjs-go-3.0.2/v3/sockjs/iframe_test.go000066400000000000000000000022171414327410500200730ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "testing" ) func TestHandler_iframe(t *testing.T) { h := newTestHandler() h.options.SockJSURL = "http://sockjs.com/sockjs.js" rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/sess/iframe", nil) h.iframe(rw, req) if rw.Body.String() != expected { t.Errorf("Unexpected html content,\ngot:\n'%s'\n\nexpected\n'%s'", rw.Body, expected) } eTag := rw.Header().Get("etag") req.Header.Set("if-none-match", eTag) rw = httptest.NewRecorder() h.iframe(rw, req) if rw.Code != http.StatusNotModified { t.Errorf("Unexpected response, got '%d', expected '%d'", rw.Code, http.StatusNotModified) } } var expected = `

Don't panic!

This is a SockJS hidden iframe. It's used for cross domain magic.

` sockjs-go-3.0.2/v3/sockjs/jsonp.go000066400000000000000000000047161414327410500167300ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" "io" "net/http" "strings" ) func (h *Handler) jsonp(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "application/javascript; charset=UTF-8") if err := req.ParseForm(); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } callback := req.Form.Get("c") if callback == "" { http.Error(rw, `"callback" parameter required`, http.StatusInternalServerError) return } else if invalidCallback.MatchString(callback) { http.Error(rw, `invalid character in "callback" parameter`, http.StatusBadRequest) return } rw.WriteHeader(http.StatusOK) rw.(http.Flusher).Flush() sess, err := h.sessionByRequest(req) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } recv := newHTTPReceiver(rw, req, 1, &jsonpFrameWriter{callback}, ReceiverTypeJSONP) if err := sess.attachReceiver(recv); err != nil { if err := recv.sendFrame(cFrame); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } recv.close() return } sess.startHandlerOnce.Do(func() { go h.handlerFunc(Session{sess}) }) select { case <-recv.doneNotify(): case <-recv.interruptedNotify(): } } func (h *Handler) jsonpSend(rw http.ResponseWriter, req *http.Request) { if err := req.ParseForm(); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } var data io.Reader data = req.Body formReader := strings.NewReader(req.PostFormValue("d")) if formReader.Len() != 0 { data = formReader } if data == nil { http.Error(rw, "Payload expected.", http.StatusBadRequest) return } var messages []string err := json.NewDecoder(data).Decode(&messages) if err == io.EOF { http.Error(rw, "Payload expected.", http.StatusBadRequest) return } if err != nil { http.Error(rw, "Broken JSON encoding.", http.StatusBadRequest) return } sessionID, _ := h.parseSessionID(req.URL) h.sessionsMux.Lock() sess, ok := h.sessions[sessionID] h.sessionsMux.Unlock() if !ok { http.NotFound(rw, req) } else { if err := sess.accept(messages...); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } rw.Header().Set("content-type", "text/plain; charset=UTF-8") _, _ = rw.Write([]byte("ok")) } } type jsonpFrameWriter struct { callback string } func (j *jsonpFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "%s(%s);\r\n", j.callback, quote(frame)) } sockjs-go-3.0.2/v3/sockjs/jsonp_test.go000066400000000000000000000073221414327410500177630ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestHandler_jsonpNoCallback(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/jsonp", nil) h.jsonp(rw, req) if rw.Code != http.StatusInternalServerError { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusInternalServerError) } expectedContentType := "text/plain; charset=utf-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } } func TestHandler_jsonp(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/jsonp?c=testCallback", nil) h.jsonp(rw, req) expectedContentType := "application/javascript; charset=UTF-8" if rw.Header().Get("content-type") != expectedContentType { t.Errorf("Unexpected content type, got '%s', expected '%s'", rw.Header().Get("content-type"), expectedContentType) } expectedBody := "testCallback(\"o\");\r\n" if rw.Body.String() != expectedBody { t.Errorf("Unexpected body, got '%s', expected '%s'", rw.Body, expectedBody) } sess, _ := h.sessionByRequest(req) if rt := sess.ReceiverType(); rt != ReceiverTypeJSONP { t.Errorf("Unexpected recevier type, got '%v', extected '%v'", rt, ReceiverTypeJSONP) } } func TestHandler_jsonpSendNoPayload(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", nil) h.jsonpSend(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusInternalServerError) } } func TestHandler_jsonpSendWrongPayload(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("wrong payload")) h.jsonpSend(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusInternalServerError) } } func TestHandler_jsonpSendNoSession(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("[\"message\"]")) h.jsonpSend(rw, req) if rw.Code != http.StatusNotFound { t.Errorf("Unexpected response code, got '%d', expected '%d'", rw.Code, http.StatusNotFound) } } func TestHandler_jsonpSend(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("[\"message\"]")) sess := newSession(req, "session", time.Second, time.Second) h.sessions["session"] = sess var done = make(chan struct{}) go func() { h.jsonpSend(rw, req) close(done) }() msg, _ := sess.Recv() if msg != "message" { t.Errorf("Incorrect message in the channel, should be '%s', was '%s'", "some message", msg) } <-done if rw.Code != http.StatusOK { t.Errorf("Wrong response status received %d, should be %d", rw.Code, http.StatusOK) } if rw.Header().Get("content-type") != "text/plain; charset=UTF-8" { t.Errorf("Wrong content type received '%s'", rw.Header().Get("content-type")) } if rw.Body.String() != "ok" { t.Errorf("Unexpected body, got '%s', expected 'ok'", rw.Body) } } func TestHandler_jsonpCannotIntoXSS(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/server/session/jsonp?c=%3Chtml%3E%3Chead%3E%3Cscript%3Ealert(5520)%3C%2Fscript%3E", nil) h.jsonp(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("JsonP forwarded an exploitable response.") } } sockjs-go-3.0.2/v3/sockjs/mapping.go000066400000000000000000000014201414327410500172170ustar00rootroot00000000000000package sockjs import ( "net/http" "regexp" ) type mapping struct { method string path *regexp.Regexp chain []http.HandlerFunc } func newMapping(method string, re string, handlers ...http.HandlerFunc) *mapping { return &mapping{method, regexp.MustCompile(re), handlers} } type matchType uint32 const ( fullMatch matchType = iota pathMatch noMatch ) // matches checks if given req.URL is a match with a mapping. Match can be either full, partial (http method mismatch) or no match. func (m *mapping) matches(req *http.Request) (match matchType, method string) { if !m.path.MatchString(req.URL.Path) { match, method = noMatch, "" } else if m.method != req.Method { match, method = pathMatch, m.method } else { match, method = fullMatch, m.method } return } sockjs-go-3.0.2/v3/sockjs/mapping_test.go000066400000000000000000000020401414327410500202550ustar00rootroot00000000000000package sockjs import ( "net/http" "regexp" "testing" ) func TestMappingMatcher(t *testing.T) { mappingPrefix := mapping{"GET", regexp.MustCompile("prefix/$"), nil} mappingPrefixRegExp := mapping{"GET", regexp.MustCompile(".*x/$"), nil} var testRequests = []struct { mapping mapping method string url string expectedMatch matchType }{ {mappingPrefix, "GET", "http://foo/prefix/", fullMatch}, {mappingPrefix, "POST", "http://foo/prefix/", pathMatch}, {mappingPrefix, "GET", "http://foo/prefix_not_mapped", noMatch}, {mappingPrefixRegExp, "GET", "http://foo/prefix/", fullMatch}, } for _, request := range testRequests { req, _ := http.NewRequest(request.method, request.url, nil) m := request.mapping match, method := m.matches(req) if match != request.expectedMatch { t.Errorf("mapping %s should match url=%s", m.path, request.url) } if request.expectedMatch == pathMatch { if method != m.method { t.Errorf("Matcher method should be %s, but got %s", m.method, method) } } } } sockjs-go-3.0.2/v3/sockjs/options.go000066400000000000000000000140131414327410500172610ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" "math/rand" "net/http" "sync" "time" "github.com/gorilla/websocket" ) var ( entropy *rand.Rand entropyMutex sync.Mutex ) func init() { entropy = rand.New(rand.NewSource(time.Now().UnixNano())) } // Options type is used for defining various sockjs options type Options struct { // Transports which don't support cross-domain communication natively ('eventsource' to name one) use an iframe trick. // A simple page is served from the SockJS server (using its foreign domain) and is placed in an invisible iframe. // Code run from this iframe doesn't need to worry about cross-domain issues, as it's being run from domain local to the SockJS server. // This iframe also does need to load SockJS javascript client library, and this option lets you specify its url (if you're unsure, // point it to the latest minified SockJS client release, this is the default). You must explicitly specify this url on the server // side for security reasons - we don't want the possibility of running any foreign javascript within the SockJS domain (aka cross site scripting attack). // Also, sockjs javascript library is probably already cached by the browser - it makes sense to reuse the sockjs url you're using in normally. SockJSURL string // Most streaming transports save responses on the client side and don't free memory used by delivered messages. // Such transports need to be garbage-collected once in a while. `response_limit` sets a minimum number of bytes that can be send // over a single http streaming request before it will be closed. After that client needs to open new request. // Setting this value to one effectively disables streaming and will make streaming transports to behave like polling transports. // The default value is 128K. ResponseLimit uint32 // Some load balancers don't support websockets. This option can be used to disable websockets support by the server. By default websockets are enabled. Websocket bool // This option can be used to enable raw websockets support by the server. By default raw websockets are disabled. RawWebsocket bool // Provide a custom Upgrader for Websocket connections to enable features like compression. // See https://godoc.org/github.com/gorilla/websocket#Upgrader for more details. WebsocketUpgrader *websocket.Upgrader // WebsocketWriteTimeout is a custom write timeout for Websocket underlying network connection. // A zero value means writes will not time out. WebsocketWriteTimeout time.Duration // In order to keep proxies and load balancers from closing long running http requests we need to pretend that the connection is active // and send a heartbeat packet once in a while. This setting controls how often this is done. // By default a heartbeat packet is sent every 25 seconds. HeartbeatDelay time.Duration // The server closes a session when a client receiving connection have not been seen for a while. // This delay is configured by this setting. // By default the session is closed when a receiving connection wasn't seen for 5 seconds. DisconnectDelay time.Duration // Some hosting providers enable sticky sessions only to requests that have JSessionID cookie set. // This setting controls if the server should set this cookie to a dummy value. // By default setting JSessionID cookie is disabled. More sophisticated behaviour can be achieved by supplying a function. JSessionID func(http.ResponseWriter, *http.Request) // CORS origin to be set on outgoing responses. If set to the empty string, it will default to the // incoming `Origin` header, or "*" if the Origin header isn't set. Origin string // CheckOrigin allows to dynamically decide whether server should set CORS // headers or not in case of XHR requests. When true returned CORS will be // configured with allowed origin equal to incoming `Origin` header, or "*" // if the request Origin header isn't set. When false returned CORS headers // won't be set at all. If this function is nil then Origin option above will // be taken into account. CheckOrigin func(*http.Request) bool } // DefaultOptions is a convenient set of options to be used for sockjs var DefaultOptions = Options{ Websocket: true, RawWebsocket: false, JSessionID: nil, SockJSURL: "//cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js", HeartbeatDelay: 25 * time.Second, DisconnectDelay: 5 * time.Second, ResponseLimit: 128 * 1024, WebsocketUpgrader: &websocket.Upgrader{}, } type info struct { Websocket bool `json:"websocket"` CookieNeeded bool `json:"cookie_needed"` Origins []string `json:"origins"` Entropy int32 `json:"entropy"` } func (options *Options) info(rw http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: rw.Header().Set("Content-Type", "application/json; charset=UTF-8") if err := json.NewEncoder(rw).Encode(info{ Websocket: options.Websocket, CookieNeeded: options.JSessionID != nil, Origins: []string{"*:*"}, Entropy: generateEntropy(), }); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) } case http.MethodOptions: rw.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") rw.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", 365*24*60*60)) rw.WriteHeader(http.StatusNoContent) // 204 default: http.NotFound(rw, req) } } // DefaultJSessionID is a default behaviour function to be used in options for JSessionID if JSESSIONID is needed func DefaultJSessionID(rw http.ResponseWriter, req *http.Request) { cookie, err := req.Cookie("JSESSIONID") if err == http.ErrNoCookie { cookie = &http.Cookie{ Name: "JSESSIONID", Value: "dummy", } } cookie.Path = "/" header := rw.Header() header.Add("Set-Cookie", cookie.String()) } func (options *Options) cookie(rw http.ResponseWriter, req *http.Request) { if options.JSessionID != nil { // cookie is needed options.JSessionID(rw, req) } } func generateEntropy() int32 { entropyMutex.Lock() entropy := entropy.Int31() entropyMutex.Unlock() return entropy } sockjs-go-3.0.2/v3/sockjs/options_test.go000066400000000000000000000036011414327410500203210ustar00rootroot00000000000000package sockjs import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestInfoGet(t *testing.T) { recorder := httptest.NewRecorder() request, _ := http.NewRequest("GET", "", nil) DefaultOptions.info(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("Wrong status code, got '%d' expected '%d'", recorder.Code, http.StatusOK) } decoder := json.NewDecoder(recorder.Body) var a info if err := decoder.Decode(&a); err != nil { t.Error(err) t.Fail() } if !a.Websocket { t.Errorf("Websocket field should be set true") } if a.CookieNeeded { t.Errorf("CookieNeeded should be set to false") } } func TestInfoOptions(t *testing.T) { recorder := httptest.NewRecorder() request, _ := http.NewRequest("OPTIONS", "", nil) DefaultOptions.info(recorder, request) if recorder.Code != http.StatusNoContent { t.Errorf("Incorrect status code received, got '%d' expected '%d'", recorder.Code, http.StatusNoContent) } } func TestInfoUnknown(t *testing.T) { req, _ := http.NewRequest("PUT", "", nil) rec := httptest.NewRecorder() DefaultOptions.info(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("Incorrec response status, got '%d' expected '%d'", rec.Code, http.StatusNotFound) } } func TestCookies(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", nil) optionsWithCookies := DefaultOptions optionsWithCookies.JSessionID = DefaultJSessionID optionsWithCookies.cookie(rec, req) if rec.Header().Get("set-cookie") != "JSESSIONID=dummy; Path=/" { t.Errorf("Cookie not properly set in response") } // cookie value set in request req.AddCookie(&http.Cookie{Name: "JSESSIONID", Value: "some_jsession_id", Path: "/"}) rec = httptest.NewRecorder() optionsWithCookies.cookie(rec, req) if rec.Header().Get("set-cookie") != "JSESSIONID=some_jsession_id; Path=/" { t.Errorf("Cookie not properly set in response") } } sockjs-go-3.0.2/v3/sockjs/rawwebsocket.go000066400000000000000000000067711414327410500203020ustar00rootroot00000000000000package sockjs import ( "encoding/json" "net/http" "time" "github.com/gorilla/websocket" ) func (h *Handler) rawWebsocket(rw http.ResponseWriter, req *http.Request) { upgrader := h.options.WebsocketUpgrader if upgrader == nil { upgrader = new(websocket.Upgrader) } conn, err := upgrader.Upgrade(rw, req, nil) if err != nil { return } sessID := "" sess := newSession(req, sessID, h.options.DisconnectDelay, h.options.HeartbeatDelay) sess.raw = true receiver := newRawWsReceiver(conn, h.options.WebsocketWriteTimeout) if err := sess.attachReceiver(receiver); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if h.handlerFunc != nil { go h.handlerFunc(Session{sess}) } readCloseCh := make(chan struct{}) go func() { for { frameType, p, err := conn.ReadMessage() if err != nil { close(readCloseCh) return } if frameType == websocket.TextMessage || frameType == websocket.BinaryMessage { if err := sess.accept(string(p)); err != nil { close(readCloseCh) return } } } }() select { case <-readCloseCh: case <-receiver.doneNotify(): } sess.close() if err := conn.Close(); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } type rawWsReceiver struct { conn *websocket.Conn closeCh chan struct{} writeTimeout time.Duration } func newRawWsReceiver(conn *websocket.Conn, writeTimeout time.Duration) *rawWsReceiver { return &rawWsReceiver{ conn: conn, closeCh: make(chan struct{}), writeTimeout: writeTimeout, } } func (w *rawWsReceiver) sendBulk(messages ...string) error { if len(messages) > 0 { for _, m := range messages { if w.writeTimeout != 0 { if err := w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)); err != nil { w.close() return err } } if err := w.conn.WriteMessage(websocket.TextMessage, []byte(m)); err != nil { w.close() return err } } } return nil } func (w *rawWsReceiver) sendFrame(frame string) error { if w.writeTimeout != 0 { if err := w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)); err != nil { w.close() return err } } if frame == "h" { if err := w.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { w.close() return err } } else if len(frame) > 0 && frame[0] == 'c' { status, reason, err := parseCloseFrame(frame) if err != nil { w.close() return err } msg := websocket.FormatCloseMessage(int(status), reason) if err := w.conn.WriteMessage(websocket.CloseMessage, msg); err != nil { w.close() return err } } else { if err := w.conn.WriteMessage(websocket.TextMessage, []byte(frame)); err != nil { w.close() return err } } return nil } func (w *rawWsReceiver) receiverType() ReceiverType { return ReceiverTypeRawWebsocket } func parseCloseFrame(frame string) (status uint32, reason string, err error) { var items [2]interface{} if err := json.Unmarshal([]byte(frame)[1:], &items); err != nil { return 0, "", err } statusF, _ := items[0].(float64) status = uint32(statusF) reason, _ = items[1].(string) return } func (w *rawWsReceiver) close() { select { case <-w.closeCh: // already closed default: close(w.closeCh) } } func (w *rawWsReceiver) canSend() bool { select { case <-w.closeCh: // already closed return false default: return true } } func (w *rawWsReceiver) doneNotify() <-chan struct{} { return w.closeCh } func (w *rawWsReceiver) interruptedNotify() <-chan struct{} { return nil } sockjs-go-3.0.2/v3/sockjs/rawwebsocket_test.go000066400000000000000000000121261414327410500213300ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "testing" "time" "github.com/gorilla/websocket" ) func TestHandler_RawWebSocketHandshakeError(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.Close() req, _ := http.NewRequest("GET", server.URL, nil) req.Header.Set("origin", "https"+server.URL[4:]) resp, _ := http.DefaultClient.Do(req) if resp.StatusCode != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", resp.StatusCode, http.StatusBadRequest) } } func TestHandler_RawWebSocket(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.CloseClientConnections() url := "ws" + server.URL[4:] var connCh = make(chan Session) h.handlerFunc = func(conn Session) { connCh <- conn } conn, resp, err := websocket.DefaultDialer.Dial(url, nil) if conn == nil { t.Errorf("Connection should not be nil") } if err != nil { t.Errorf("Unexpected error '%v'", err) } if resp.StatusCode != http.StatusSwitchingProtocols { t.Errorf("Wrong response code returned, got '%d', expected '%d'", resp.StatusCode, http.StatusSwitchingProtocols) } select { case <-connCh: //ok case <-time.After(10 * time.Millisecond): t.Errorf("Sockjs Handler not invoked") } } func TestHandler_RawWebSocketTerminationByServer(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.Close() url := "ws" + server.URL[4:] h.handlerFunc = func(conn Session) { // close the session without sending any message if rt := conn.ReceiverType(); rt != ReceiverTypeRawWebsocket { t.Errorf("Unexpected recevier type, got '%v', extected '%v'", rt, ReceiverTypeRawWebsocket) } conn.Close(3000, "some close message") conn.Close(0, "this should be ignored") } conn, _, err := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) if err != nil { t.Fatalf("websocket dial failed: %v", err) } for i := 0; i < 2; i++ { _, _, err := conn.ReadMessage() closeError, ok := err.(*websocket.CloseError) if !ok { t.Fatalf("expected close error but got: %v", err) } if closeError.Code != 3000 { t.Errorf("unexpected close status: %v", closeError.Code) } if closeError.Text != "some close message" { t.Errorf("unexpected close reason: '%v'", closeError.Text) } } } func TestHandler_RawWebSocketTerminationByClient(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) defer server.Close() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { if _, err := conn.Recv(); err != ErrSessionNotOpen { t.Errorf("Recv should fail") } close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) conn.Close() <-done } func TestHandler_RawWebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketWriteTimeout = time.Second server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) // defer server.CloseClientConnections() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { _ = conn.Send("message 1") _ = conn.Send("message 2") expected := "[\"message 3\"]\n" msg, err := conn.Recv() if msg != expected || err != nil { t.Errorf("Got '%s', expected '%s'", msg, expected) } _ = conn.Close(123, "close") close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) _ = conn.WriteJSON([]string{"message 3"}) var expected = []string{"message 1", "message 2"} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } func TestHandler_RawCustomWebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketWriteTimeout = time.Second h.options.WebsocketUpgrader = &websocket.Upgrader{ ReadBufferSize: 0, WriteBufferSize: 0, CheckOrigin: func(_ *http.Request) bool { return true }, Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {}, } server := httptest.NewServer(http.HandlerFunc(h.rawWebsocket)) url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { _ = conn.Send("message 1") _ = conn.Send("message 2") expected := "[\"message 3\"]\n" msg, err := conn.Recv() if msg != expected || err != nil { t.Errorf("Got '%s', expected '%s'", msg, expected) } _ = conn.Close(123, "close") close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) _ = conn.WriteJSON([]string{"message 3"}) var expected = []string{"message 1", "message 2"} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } sockjs-go-3.0.2/v3/sockjs/receiver.go000066400000000000000000000015741414327410500174020ustar00rootroot00000000000000package sockjs type ReceiverType int const ( ReceiverTypeNone ReceiverType = iota ReceiverTypeXHR ReceiverTypeEventSource ReceiverTypeHtmlFile ReceiverTypeJSONP ReceiverTypeXHRStreaming ReceiverTypeRawWebsocket ReceiverTypeWebsocket ) type receiver interface { // sendBulk send multiple data messages in frame frame in format: a["msg 1", "msg 2", ....] sendBulk(...string) error // sendFrame sends given frame over the wire (with possible chunking depending on receiver) sendFrame(string) error // close closes the receiver in a "done" way (idempotent) close() canSend() bool // done notification channel gets closed whenever receiver ends doneNotify() <-chan struct{} // interrupted channel gets closed whenever receiver is interrupted (i.e. http connection drops,...) interruptedNotify() <-chan struct{} // returns the type of receiver receiverType() ReceiverType } sockjs-go-3.0.2/v3/sockjs/session.go000066400000000000000000000143601414327410500172560ustar00rootroot00000000000000package sockjs import ( "context" "errors" "net/http" "sync" "time" ) // SessionState defines the current state of the session type SessionState uint32 const ( // brand new session, need to send "h" to receiver SessionOpening SessionState = iota // active session SessionActive // session being closed, sending "closeFrame" to receivers SessionClosing // closed session, no activity at all, should be removed from handler completely and not reused SessionClosed ) var ( // ErrSessionNotOpen error is used to denote session not in open state. // Recv() and Send() operations are not supported if session is closed. ErrSessionNotOpen = errors.New("sockjs: session not in open state") errSessionReceiverAttached = errors.New("sockjs: another receiver already attached") errSessionParse = errors.New("sockjs: unable to parse URL for session") ) type Session struct { *session } type session struct { mux sync.RWMutex id string req *http.Request state SessionState recv receiver // protocol dependent receiver (xhr, eventsource, ...) receiverType ReceiverType sendBuffer []string // messages to be sent to client recvBuffer *messageBuffer // messages received from client to be consumed by application closeFrame string // closeFrame to send after session is closed // do not use SockJS framing for raw websocket connections raw bool // internal timer used to handle session expiration if no receiver is attached, or heartbeats if recevier is attached sessionTimeoutInterval time.Duration heartbeatInterval time.Duration timer *time.Timer // once the session timeouts this channel also closes closeCh chan struct{} startHandlerOnce sync.Once context context.Context cancelFunc func() } // session is a central component that handles receiving and sending frames. It maintains internal state func newSession(req *http.Request, sessionID string, sessionTimeoutInterval, heartbeatInterval time.Duration) *session { context, cancel := context.WithCancel(context.Background()) s := &session{ id: sessionID, req: req, heartbeatInterval: heartbeatInterval, recvBuffer: newMessageBuffer(), closeCh: make(chan struct{}), sessionTimeoutInterval: sessionTimeoutInterval, receiverType: ReceiverTypeNone, context: context, cancelFunc: cancel, } s.mux.Lock() s.timer = time.AfterFunc(sessionTimeoutInterval, s.close) s.mux.Unlock() return s } func (s *session) sendMessage(msg string) error { s.mux.Lock() defer s.mux.Unlock() if s.state > SessionActive { return ErrSessionNotOpen } s.sendBuffer = append(s.sendBuffer, msg) if s.recv != nil && s.recv.canSend() { if err := s.recv.sendBulk(s.sendBuffer...); err != nil { return err } s.sendBuffer = nil } return nil } func (s *session) attachReceiver(recv receiver) error { s.mux.Lock() defer s.mux.Unlock() if s.recv != nil { return errSessionReceiverAttached } s.recv = recv s.receiverType = recv.receiverType() go func(r receiver) { select { case <-r.doneNotify(): s.detachReceiver() case <-r.interruptedNotify(): s.detachReceiver() s.close() } }(recv) if s.state == SessionClosing { if !s.raw { if err := s.recv.sendFrame(s.closeFrame); err != nil { return err } } s.recv.close() return nil } if s.state == SessionOpening { if !s.raw { if err := s.recv.sendFrame("o"); err != nil { return err } } s.state = SessionActive } if err := s.recv.sendBulk(s.sendBuffer...); err != nil { return err } s.sendBuffer = nil s.timer.Stop() if s.heartbeatInterval > 0 { s.timer = time.AfterFunc(s.heartbeatInterval, s.heartbeat) } return nil } func (s *session) detachReceiver() { s.mux.Lock() s.timer.Stop() s.timer = time.AfterFunc(s.sessionTimeoutInterval, s.close) s.recv = nil s.mux.Unlock() } func (s *session) heartbeat() { s.mux.Lock() if s.recv != nil { // timer could have fired between Lock and timer.Stop in detachReceiver _ = s.recv.sendFrame("h") s.timer = time.AfterFunc(s.heartbeatInterval, s.heartbeat) } s.mux.Unlock() } func (s *session) accept(messages ...string) error { return s.recvBuffer.push(messages...) } // idempotent operation func (s *session) closing() { s.mux.Lock() defer s.mux.Unlock() if s.state < SessionClosing { s.state = SessionClosing s.recvBuffer.close() if s.recv != nil { _ = s.recv.sendFrame(s.closeFrame) s.recv.close() } s.cancelFunc() } } // idempotent operation func (s *session) close() { s.closing() s.mux.Lock() defer s.mux.Unlock() if s.state < SessionClosed { s.state = SessionClosed s.timer.Stop() close(s.closeCh) s.cancelFunc() } } func (s *session) setCurrentRequest(req *http.Request) { s.mux.Lock() s.req = req s.mux.Unlock() } // Close closes the session with provided code and reason. func (s *session) Close(status uint32, reason string) error { s.mux.Lock() if s.state < SessionClosing { s.closeFrame = closeFrame(status, reason) s.mux.Unlock() s.closing() return nil } s.mux.Unlock() return ErrSessionNotOpen } // ID returns a session id func (s *session) ID() string { return s.id } // Recv reads one text frame from session func (s *session) Recv() (string, error) { return s.recvBuffer.pop(context.Background()) } // RecvCtx reads one text frame from session func (s *session) RecvCtx(ctx context.Context) (string, error) { return s.recvBuffer.pop(ctx) } // Send sends one text frame to session func (s *session) Send(msg string) error { return s.sendMessage(msg) } // Request returns the first http request func (s *session) Request() *http.Request { s.mux.RLock() defer s.mux.RUnlock() s.req.Context() return s.req } //GetSessionState returns the current state of the session func (s *session) GetSessionState() SessionState { s.mux.RLock() defer s.mux.RUnlock() return s.state } //ReceiverType returns receiver used in session func (s *session) ReceiverType() ReceiverType { s.mux.RLock() defer s.mux.RUnlock() return s.receiverType } // Context returns session context, the context is cancelled // whenever the session gets into closing or closed state func (s *session) Context() context.Context { return s.context } sockjs-go-3.0.2/v3/sockjs/session_test.go000066400000000000000000000230431414327410500203130ustar00rootroot00000000000000package sockjs import ( "net/http" "runtime" "strings" "sync" "testing" "time" ) func newTestSession() *session { // session with long expiration and heartbeats with ID return newSession(nil, "sessionId", 1000*time.Second, 1000*time.Second) } func TestSession_Create(t *testing.T) { session := newTestSession() _ = session.sendMessage("this is a message") if len(session.sendBuffer) != 1 { t.Errorf("session send buffer should contain 1 message") } _ = session.sendMessage("another message") if len(session.sendBuffer) != 2 { t.Errorf("session send buffer should contain 2 messages") } if session.GetSessionState() != SessionOpening { t.Errorf("session in wrong state %v, should be %v", session.GetSessionState(), SessionOpening) } } func TestSession_Request(t *testing.T) { req, _ := http.NewRequest("POST", "/server/session/jsonp_send", strings.NewReader("[\"message\"]")) sess := newSession(req, "session", time.Second, time.Second) if sess.Request() == nil { t.Error("session initial request should have been saved.") } if sess.Request().URL.String() != req.URL.String() { t.Errorf("Expected stored session request URL to equal %s, got %s", req.URL.String(), sess.Request().URL.String()) } } func TestSession_ConcurrentSend(t *testing.T) { session := newTestSession() done := make(chan bool) for i := 0; i < 100; i++ { go func() { _ = session.sendMessage("message D") done <- true }() } for i := 0; i < 100; i++ { <-done } if len(session.sendBuffer) != 100 { t.Errorf("session send buffer should contain 100 messages") } } func TestSession_AttachReceiver(t *testing.T) { session := newTestSession() recv := &testReceiver{} if err := session.attachReceiver(recv); err != nil { t.Errorf("Should not return error") } if session.GetSessionState() != SessionActive { t.Errorf("session in wrong state after receiver attached %d, should be %d", session.GetSessionState(), SessionActive) } session.detachReceiver() if err := session.attachReceiver(recv); err != nil { t.Errorf("Should not return error") } } func TestSession_Timeout(t *testing.T) { sess := newSession(nil, "id", 10*time.Millisecond, 10*time.Second) select { case <-sess.closeCh: case <-time.After(20 * time.Millisecond): select { case <-sess.closeCh: // still ok default: t.Errorf("sess close notification channel should close") } } if sess.GetSessionState() != SessionClosed { t.Errorf("session did not timeout") } select { case <-sess.Context().Done(): case <-time.After(1 * time.Second): t.Errorf("session context should have been done") } } func TestSession_TimeoutOfClosedSession(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("Unexcpected error '%v'", r) } }() sess := newSession(nil, "id", 1*time.Millisecond, time.Second) sess.closing() time.Sleep(1 * time.Millisecond) sess.closing() } func TestSession_AttachReceiverAndCheckHeartbeats(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("Unexcpected error '%v'", r) } }() session := newSession(nil, "id", time.Second, 10*time.Millisecond) // 10ms heartbeats recv := newTestReceiver() defer close(recv.doneCh) noError(t, session.attachReceiver(recv)) time.Sleep(120 * time.Millisecond) recv.Lock() if len(recv.frames) < 10 || len(recv.frames) > 13 { // should get around 10 heartbeats (120ms/10ms) t.Fatalf("Wrong number of frames received, got '%d'", len(recv.frames)) } for i := 1; i < len(recv.frames); i++ { if recv.frames[i] != "h" { t.Errorf("Heartbeat no received") } } } func TestSession_AttachReceiverAndRefuse(t *testing.T) { session := newTestSession() if err := session.attachReceiver(newTestReceiver()); err != nil { t.Errorf("Should not return error") } var a sync.WaitGroup a.Add(100) for i := 0; i < 100; i++ { go func() { defer a.Done() if err := session.attachReceiver(newTestReceiver()); err != errSessionReceiverAttached { t.Errorf("Should return error as another receiver is already attached") } }() } a.Wait() } func TestSession_DetachReceiver(t *testing.T) { session := newTestSession() session.detachReceiver() session.detachReceiver() // idempotent operation _ = session.attachReceiver(newTestReceiver()) session.detachReceiver() } func TestSession_SendWithRecv(t *testing.T) { session := newTestSession() noError(t, session.sendMessage("message A")) _ = session.sendMessage("message B") if len(session.sendBuffer) != 2 { t.Errorf("There should be 2 messages in buffer, but there are %d", len(session.sendBuffer)) } recv := newTestReceiver() defer close(recv.doneCh) noError(t, session.attachReceiver(recv)) if len(recv.frames[1:]) != 2 { t.Errorf("Reciver should get 2 message frames from session, got %d", len(recv.frames)) } noError(t, session.sendMessage("message C")) if len(recv.frames[1:]) != 3 { t.Errorf("Reciver should get 3 message frames from session, got %d", len(recv.frames)) } noError(t, session.sendMessage("message D")) if len(recv.frames[1:]) != 4 { t.Errorf("Reciver should get 4 frames from session, got %d", len(recv.frames)) } if len(session.sendBuffer) != 0 { t.Errorf("Send buffer should be empty now, but there are %d messaged", len(session.sendBuffer)) } } func TestSession_Recv(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("Panic should not happen") } }() var wg sync.WaitGroup wg.Add(1) session := newTestSession() go func() { defer wg.Done() noError(t, session.accept("message A")) noError(t, session.accept("message B")) if err := session.accept("message C"); err != ErrSessionNotOpen { t.Errorf("session should not accept new messages if closed, got '%v' expected '%v'", err, ErrSessionNotOpen) } }() if msg, _ := session.Recv(); msg != "message A" { t.Errorf("Got %s, should be %s", msg, "message A") } if msg, _ := session.Recv(); msg != "message B" { t.Errorf("Got %s, should be %s", msg, "message B") } session.close() wg.Wait() } func TestSession_Closing(t *testing.T) { session := newTestSession() session.closing() if _, err := session.Recv(); err == nil { t.Errorf("session's receive buffer channel should close") } if err := session.sendMessage("some message"); err != ErrSessionNotOpen { t.Errorf("session should not accept new message after close") } } func TestSession_SessionRecv(t *testing.T) { s := newTestSession() go func() { noError(t, s.accept("message 1")) }() msg, err := s.Recv() if msg != "message 1" || err != nil { t.Errorf("Should receive a message without error, got '%s' err '%v'", msg, err) } go func() { s.closing() _, err := s.Recv() if err != ErrSessionNotOpen { t.Errorf("session not in correct state, got '%v', expected '%v'", err, ErrSessionNotOpen) } }() _, err = s.Recv() if err != ErrSessionNotOpen { t.Errorf("session not in correct state, got '%v', expected '%v'", err, ErrSessionNotOpen) } } func TestSession_SessionSend(t *testing.T) { s := newTestSession() err := s.Send("message A") if err != nil { t.Errorf("session should take messages by default") } if len(s.sendBuffer) != 1 || s.sendBuffer[0] != "message A" { t.Errorf("Message not properly queued in session, got '%v'", s.sendBuffer) } } func TestSession_SessionClose(t *testing.T) { s := newTestSession() s.state = SessionActive recv := newTestReceiver() noError(t, s.attachReceiver(recv)) err := s.Close(1, "some reason") if len(recv.frames) != 1 || recv.frames[0] != "c[1,\"some reason\"]" { t.Errorf("Expected close frame, got '%v'", recv.frames) } if err != nil { t.Errorf("Should not get any error, got '%s'", err) } if s.closeFrame != "c[1,\"some reason\"]" { t.Errorf("Incorrect closeFrame, got '%s'", s.closeFrame) } if s.GetSessionState() != SessionClosing { t.Errorf("Incorrect session state, expected 'sessionClosing', got '%v'", s.GetSessionState()) } // all the consequent receivers trying to attach shoult get the same close frame var i = 100 for i > 0 { recv := newTestReceiver() err := s.attachReceiver(recv) if err != nil { // give a chance to a receiver to detach runtime.Gosched() continue } i-- if len(recv.frames) != 1 || recv.frames[0] != "c[1,\"some reason\"]" { t.Errorf("Close frame not received by recv, frames '%v'", recv.frames) } } if err := s.Close(1, "some other reson"); err != ErrSessionNotOpen { t.Errorf("Expected error, got '%v'", err) } } func TestSession_SessionSessionId(t *testing.T) { s := newTestSession() if s.ID() != "sessionId" { t.Errorf("Unexpected session ID, got '%s', expected '%s'", s.ID(), "sessionId") } } func newTestReceiver() *testReceiver { return &testReceiver{ doneCh: make(chan struct{}), interruptCh: make(chan struct{}), } } type testReceiver struct { sync.Mutex doneCh, interruptCh chan struct{} frames []string } func (t *testReceiver) doneNotify() <-chan struct{} { return t.doneCh } func (t *testReceiver) interruptedNotify() <-chan struct{} { return t.interruptCh } func (t *testReceiver) close() { close(t.doneCh) } func (t *testReceiver) canSend() bool { select { case <-t.doneCh: return false // already closed default: return true } } func (t *testReceiver) sendBulk(messages ...string) error { for _, m := range messages { if err := t.sendFrame(m); err != nil { return err } } return nil } func (t *testReceiver) sendFrame(frame string) error { t.Lock() defer t.Unlock() t.frames = append(t.frames, frame) return nil } func noError(t *testing.T, err error) { if err != nil { t.Error(err) t.Fail() } } func (t *testReceiver) receiverType() ReceiverType { return ReceiverTypeNone } sockjs-go-3.0.2/v3/sockjs/sockjs.go000066400000000000000000000000171414327410500170610ustar00rootroot00000000000000package sockjs sockjs-go-3.0.2/v3/sockjs/sockjs_test.go000066400000000000000000000014421414327410500201230ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "regexp" "testing" ) func TestSockJS_ServeHTTP(t *testing.T) { m := Handler{mappings: make([]*mapping, 0)} m.mappings = []*mapping{ {"POST", regexp.MustCompile("/foo/.*"), []http.HandlerFunc{func(http.ResponseWriter, *http.Request) {}}}, } req, _ := http.NewRequest("GET", "/foo/bar", nil) rec := httptest.NewRecorder() m.ServeHTTP(rec, req) if rec.Code != http.StatusMethodNotAllowed { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusMethodNotAllowed) } req, _ = http.NewRequest("GET", "/bar", nil) rec = httptest.NewRecorder() m.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusNotFound) } } sockjs-go-3.0.2/v3/sockjs/utils.go000066400000000000000000000004731414327410500167330ustar00rootroot00000000000000package sockjs import "encoding/json" func quote(in string) string { quoted, _ := json.Marshal(in) return string(quoted) } func transform(values []string, transformFn func(string) string) []string { ret := make([]string, len(values)) for i, msg := range values { ret[i] = transformFn(msg) } return ret } sockjs-go-3.0.2/v3/sockjs/utils_test.go000066400000000000000000000006111414327410500177640ustar00rootroot00000000000000package sockjs import "testing" func TestQuote(t *testing.T) { var quotationTests = []struct { input string output string }{ {"simple", "\"simple\""}, {"more complex \"", "\"more complex \\\"\""}, } for _, testCase := range quotationTests { if quote(testCase.input) != testCase.output { t.Errorf("Expected '%s', got '%s'", testCase.output, quote(testCase.input)) } } } sockjs-go-3.0.2/v3/sockjs/web.go000066400000000000000000000036501414327410500163500ustar00rootroot00000000000000package sockjs import ( "fmt" "net/http" "time" ) func xhrCorsFactory(opts Options) func(rw http.ResponseWriter, req *http.Request) { return func(rw http.ResponseWriter, req *http.Request) { header := rw.Header() var corsEnabled bool var corsOrigin string if opts.CheckOrigin != nil { corsEnabled = opts.CheckOrigin(req) if corsEnabled { corsOrigin = req.Header.Get("origin") if corsOrigin == "" { corsOrigin = "*" } } } else { corsEnabled = true corsOrigin = opts.Origin if corsOrigin == "" { corsOrigin = req.Header.Get("origin") } if corsOrigin == "" || corsOrigin == "null" { corsOrigin = "*" } } if corsEnabled { header.Set("Access-Control-Allow-Origin", corsOrigin) if allowHeaders := req.Header.Get("Access-Control-Request-Headers"); allowHeaders != "" && allowHeaders != "null" { header.Add("Access-Control-Allow-Headers", allowHeaders) } header.Set("Access-Control-Allow-Credentials", "true") } } } func xhrOptions(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("Access-Control-Allow-Methods", "OPTIONS, POST") rw.WriteHeader(http.StatusNoContent) // 204 } func cacheFor(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", 365*24*60*60)) rw.Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(time.RFC1123)) rw.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", 365*24*60*60)) } func noCache(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") } func welcomeHandler(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "text/plain;charset=UTF-8") fmt.Fprint(rw, "Welcome to SockJS!\n") } func httpError(w http.ResponseWriter, error string, code int) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(code) fmt.Fprint(w, error) } sockjs-go-3.0.2/v3/sockjs/web_test.go000066400000000000000000000101761414327410500174100ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestXhrCors(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{}) xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "*" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "*") } req.Header.Set("origin", "localhost") xhrCors(recorder, req) acao = recorder.Header().Get("access-control-allow-origin") if acao != "localhost" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "localhost") } req.Header.Set("access-control-request-headers", "some value") rec := httptest.NewRecorder() xhrCors(rec, req) if rec.Header().Get("access-control-allow-headers") != "some value" { t.Errorf("Incorent value for ACAH, got %s", rec.Header().Get("access-control-allow-headers")) } rec = httptest.NewRecorder() xhrCors(rec, req) if rec.Header().Get("access-control-allow-credentials") != "true" { t.Errorf("Incorent value for ACAC, got %s", rec.Header().Get("access-control-allow-credentials")) } // verify that if Access-Control-Allow-Credentials was previously set that xhrCors() does not duplicate the value rec = httptest.NewRecorder() rec.Header().Set("Access-Control-Allow-Credentials", "true") xhrCors(rec, req) acac := rec.Header()["Access-Control-Allow-Credentials"] if len(acac) != 1 || acac[0] != "true" { t.Errorf("Incorent value for ACAC, got %s", strings.Join(acac, ",")) } } func TestCheckOriginCORSAllowedNullOrigin(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{ CheckOrigin: func(req *http.Request) bool { return true }, }) req.Header.Set("origin", "null") xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "null" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "null") } } func TestCheckOriginCORSAllowedEmptyOrigin(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{ CheckOrigin: func(req *http.Request) bool { return true }, }) xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "*" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "*") } } func TestCheckOriginCORSNotAllowed(t *testing.T) { recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrCors := xhrCorsFactory(Options{ CheckOrigin: func(req *http.Request) bool { return false }, }) req.Header.Set("origin", "localhost") xhrCors(recorder, req) acao := recorder.Header().Get("access-control-allow-origin") if acao != "" { t.Errorf("Incorrect value for access-control-allow-origin header, got %s, expected %s", acao, "") } } func TestXhrOptions(t *testing.T) { rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) xhrOptions(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("Wrong response status code, expected %d, got %d", http.StatusNoContent, rec.Code) } } func TestCacheFor(t *testing.T) { rec := httptest.NewRecorder() cacheFor(rec, nil) cacheControl := rec.Header().Get("cache-control") if cacheControl != "public, max-age=31536000" { t.Errorf("Incorrect cache-control header value, got '%s'", cacheControl) } expires := rec.Header().Get("expires") if expires == "" { t.Errorf("Expires header should not be empty") // TODO(igm) check proper formating of string } maxAge := rec.Header().Get("access-control-max-age") if maxAge != "31536000" { t.Errorf("Incorrect value for access-control-max-age, got '%s'", maxAge) } } func TestNoCache(t *testing.T) { rec := httptest.NewRecorder() noCache(rec, nil) } func TestWelcomeHandler(t *testing.T) { rec := httptest.NewRecorder() welcomeHandler(rec, nil) if rec.Body.String() != "Welcome to SockJS!\n" { t.Errorf("Incorrect welcome message received, got '%s'", rec.Body.String()) } } sockjs-go-3.0.2/v3/sockjs/websocket.go000066400000000000000000000046751414327410500175710ustar00rootroot00000000000000package sockjs import ( "fmt" "net/http" "strings" "time" "github.com/gorilla/websocket" ) func (h *Handler) sockjsWebsocket(rw http.ResponseWriter, req *http.Request) { upgrader := h.options.WebsocketUpgrader if upgrader == nil { upgrader = new(websocket.Upgrader) } conn, err := upgrader.Upgrade(rw, req, nil) if err != nil { return } sessID, _ := h.parseSessionID(req.URL) sess := newSession(req, sessID, h.options.DisconnectDelay, h.options.HeartbeatDelay) receiver := newWsReceiver(conn, h.options.WebsocketWriteTimeout) if err := sess.attachReceiver(receiver); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if h.handlerFunc != nil { go h.handlerFunc(Session{sess}) } readCloseCh := make(chan struct{}) go func() { var d []string for { err := conn.ReadJSON(&d) if err != nil { close(readCloseCh) return } if err := sess.accept(d...); err != nil { close(readCloseCh) return } } }() select { case <-readCloseCh: case <-receiver.doneNotify(): } sess.close() if err := conn.Close(); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } type wsReceiver struct { conn *websocket.Conn closeCh chan struct{} writeTimeout time.Duration } func newWsReceiver(conn *websocket.Conn, writeTimeout time.Duration) *wsReceiver { return &wsReceiver{ conn: conn, closeCh: make(chan struct{}), writeTimeout: writeTimeout, } } func (w *wsReceiver) sendBulk(messages ...string) error { if len(messages) > 0 { return w.sendFrame(fmt.Sprintf("a[%s]", strings.Join(transform(messages, quote), ","))) } return nil } func (w *wsReceiver) sendFrame(frame string) error { if w.writeTimeout != 0 { if err := w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)); err != nil { w.close() return err } } if err := w.conn.WriteMessage(websocket.TextMessage, []byte(frame)); err != nil { w.close() return err } return nil } func (w *wsReceiver) close() { select { case <-w.closeCh: // already closed default: close(w.closeCh) } } func (w *wsReceiver) canSend() bool { select { case <-w.closeCh: // already closed return false default: return true } } func (w *wsReceiver) doneNotify() <-chan struct{} { return w.closeCh } func (w *wsReceiver) interruptedNotify() <-chan struct{} { return nil } func (w *wsReceiver) receiverType() ReceiverType { return ReceiverTypeWebsocket } sockjs-go-3.0.2/v3/sockjs/websocket_test.go000066400000000000000000000137521414327410500206240ustar00rootroot00000000000000package sockjs import ( "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gorilla/websocket" ) func TestHandler_WebSocketHandshakeError(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.Close() req, _ := http.NewRequest("GET", server.URL, nil) req.Header.Set("origin", "https"+server.URL[4:]) resp, err := http.DefaultClient.Do(req) if err != nil { t.Errorf("There should not be any error, got '%s'", err) t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusBadRequest { t.Errorf("Unexpected response code, got '%d', expected '%d'", resp.StatusCode, http.StatusBadRequest) } } func TestHandler_WebSocket(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.CloseClientConnections() url := "ws" + server.URL[4:] var connCh = make(chan Session) h.handlerFunc = func(conn Session) { if rt := conn.ReceiverType(); rt != ReceiverTypeWebsocket { t.Errorf("Unexpected recevier type, got '%v', extected '%v'", rt, ReceiverTypeWebsocket) } connCh <- conn } conn, resp, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { t.Errorf("Unexpected error '%v'", err) t.FailNow() } if conn == nil { t.Errorf("Connection should not be nil") t.FailNow() } if resp == nil { t.Errorf("Response should not be nil") t.FailNow() } if resp.StatusCode != http.StatusSwitchingProtocols { t.Errorf("Wrong response code returned, got '%d', expected '%d'", resp.StatusCode, http.StatusSwitchingProtocols) } select { case <-connCh: //ok case <-time.After(10 * time.Millisecond): t.Errorf("Sockjs Handler not invoked") } } func TestHandler_WebSocketTerminationByServer(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.Close() url := "ws" + server.URL[4:] h.handlerFunc = func(conn Session) { conn.Close(1024, "some close message") conn.Close(0, "this should be ignored") } conn, _, err := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) if err != nil { t.Fatalf("websocket dial failed: %v", err) t.FailNow() } if conn == nil { t.Errorf("Connection should not be nil") t.FailNow() } _, msg, err := conn.ReadMessage() if string(msg) != "o" || err != nil { t.Errorf("Open frame expected, got '%s' and error '%v', expected '%s' without error", msg, err, "o") } _, msg, err = conn.ReadMessage() if string(msg) != `c[1024,"some close message"]` || err != nil { t.Errorf("Close frame expected, got '%s' and error '%v', expected '%s' without error", msg, err, `c[1024,"some close message"]`) } _, _, err = conn.ReadMessage() // gorilla websocket keeps `errUnexpectedEOF` private so we need to introspect the error message if err != nil { if !strings.Contains(err.Error(), "unexpected EOF") { t.Errorf("Expected 'unexpected EOF' error or similar, got '%v'", err) } } } func TestHandler_WebSocketTerminationByClient(t *testing.T) { h := newTestHandler() server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) defer server.Close() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { if _, err := conn.Recv(); err != ErrSessionNotOpen { t.Errorf("Recv should fail") } select { case <-conn.Context().Done(): case <-time.After(1 * time.Second): t.Errorf("context should have been done") } close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) if conn == nil { t.Errorf("Connection should not be nil") t.FailNow() } conn.Close() <-done } func TestHandler_WebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketWriteTimeout = time.Second server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) // defer server.CloseClientConnections() url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { noError(t, conn.Send("message 1")) noError(t, conn.Send("message 2")) msg, err := conn.Recv() if msg != "message 3" || err != nil { t.Errorf("Got '%s', expected '%s'", msg, "message 3") } noError(t, conn.Close(123, "close")) close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) noError(t, conn.WriteJSON([]string{"message 3"})) var expected = []string{"o", `a["message 1"]`, `a["message 2"]`, `c[123,"close"]`} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } func TestHandler_CustomWebSocketCommunication(t *testing.T) { h := newTestHandler() h.options.WebsocketUpgrader = &websocket.Upgrader{ ReadBufferSize: 0, WriteBufferSize: 0, CheckOrigin: func(_ *http.Request) bool { return true }, Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {}, } h.options.WebsocketWriteTimeout = time.Second server := httptest.NewServer(http.HandlerFunc(h.sockjsWebsocket)) url := "ws" + server.URL[4:] var done = make(chan struct{}) h.handlerFunc = func(conn Session) { noError(t, conn.Send("message 1")) noError(t, conn.Send("message 2")) msg, err := conn.Recv() if msg != "message 3" || err != nil { t.Errorf("Got '%s', expected '%s'", msg, "message 3") } noError(t, conn.Close(123, "close")) close(done) } conn, _, _ := websocket.DefaultDialer.Dial(url, map[string][]string{"Origin": []string{server.URL}}) noError(t, conn.WriteJSON([]string{"message 3"})) var expected = []string{"o", `a["message 1"]`, `a["message 2"]`, `c[123,"close"]`} for _, exp := range expected { _, msg, err := conn.ReadMessage() if string(msg) != exp || err != nil { t.Errorf("Wrong frame, got '%s' and error '%v', expected '%s' without error", msg, err, exp) } } <-done } sockjs-go-3.0.2/v3/sockjs/xhr.go000066400000000000000000000057271414327410500164030ustar00rootroot00000000000000package sockjs import ( "encoding/json" "fmt" "io" "net/http" "strings" ) var ( cFrame = closeFrame(2010, "Another connection still open") xhrStreamingPrelude = strings.Repeat("h", 2048) ) func (h *Handler) xhrSend(rw http.ResponseWriter, req *http.Request) { if req.Body == nil { httpError(rw, "Payload expected.", http.StatusBadRequest) return } var messages []string err := json.NewDecoder(req.Body).Decode(&messages) if err == io.EOF { httpError(rw, "Payload expected.", http.StatusBadRequest) return } if _, ok := err.(*json.SyntaxError); ok || err == io.ErrUnexpectedEOF { httpError(rw, "Broken JSON encoding.", http.StatusBadRequest) return } sessionID, err := h.parseSessionID(req.URL) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } h.sessionsMux.Lock() sess, ok := h.sessions[sessionID] h.sessionsMux.Unlock() if !ok { http.NotFound(rw, req) return } if err := sess.accept(messages...); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } rw.Header().Set("content-type", "text/plain; charset=UTF-8") // Ignored by net/http (but protocol test complains), see https://code.google.com/p/go/source/detail?r=902dc062bff8 rw.WriteHeader(http.StatusNoContent) } type xhrFrameWriter struct{} func (*xhrFrameWriter) write(w io.Writer, frame string) (int, error) { return fmt.Fprintf(w, "%s\n", frame) } func (h *Handler) xhrPoll(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "application/javascript; charset=UTF-8") sess, err := h.sessionByRequest(req) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } receiver := newHTTPReceiver(rw, req, 1, new(xhrFrameWriter), ReceiverTypeXHR) if err := sess.attachReceiver(receiver); err != nil { if err := receiver.sendFrame(cFrame); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } receiver.close() return } sess.startHandlerOnce.Do(func() { if h.handlerFunc != nil { go h.handlerFunc(Session{sess}) } }) select { case <-receiver.doneNotify(): case <-receiver.interruptedNotify(): } } func (h *Handler) xhrStreaming(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("content-type", "application/javascript; charset=UTF-8") fmt.Fprintf(rw, "%s\n", xhrStreamingPrelude) rw.(http.Flusher).Flush() sess, err := h.sessionByRequest(req) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } receiver := newHTTPReceiver(rw, req, h.options.ResponseLimit, new(xhrFrameWriter), ReceiverTypeXHRStreaming) if err := sess.attachReceiver(receiver); err != nil { if err := receiver.sendFrame(cFrame); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } receiver.close() return } sess.startHandlerOnce.Do(func() { go h.handlerFunc(Session{sess}) }) select { case <-receiver.doneNotify(): case <-receiver.interruptedNotify(): } } sockjs-go-3.0.2/v3/sockjs/xhr_test.go000066400000000000000000000147251414327410500174400ustar00rootroot00000000000000package sockjs import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestHandler_XhrSendNilBody(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/non_existing_session/xhr_send", nil) h.xhrSend(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusBadRequest) } if rec.Body.String() != "Payload expected." { t.Errorf("Unexcpected body received: '%s'", rec.Body.String()) } } func TestHandler_XhrSendEmptyBody(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/non_existing_session/xhr_send", strings.NewReader("")) h.xhrSend(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusBadRequest) } if rec.Body.String() != "Payload expected." { t.Errorf("Unexcpected body received: '%s'", rec.Body.String()) } } func TestHandler_XhrSendWrongUrlPath(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "incorrect", strings.NewReader("[\"a\"]")) h.xhrSend(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("Unexcpected response status, got '%d', expected '%d'", rec.Code, http.StatusBadRequest) } } func TestHandler_XhrSendToExistingSession(t *testing.T) { h := newTestHandler() rec := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"some message\"]")) sess := newSession(req, "session", time.Second, time.Second) h.sessions["session"] = sess req, _ = http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"some message\"]")) var done = make(chan bool) go func() { h.xhrSend(rec, req) done <- true }() msg, _ := sess.Recv() if msg != "some message" { t.Errorf("Incorrect message in the channel, should be '%s', was '%s'", "some message", msg) } <-done if rec.Code != http.StatusNoContent { t.Errorf("Wrong response status received %d, should be %d", rec.Code, http.StatusNoContent) } if rec.Header().Get("content-type") != "text/plain; charset=UTF-8" { t.Errorf("Wrong content type received '%s'", rec.Header().Get("content-type")) } } func TestHandler_XhrSendInvalidInput(t *testing.T) { h := newTestHandler() req, _ := http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("some invalid message frame")) rec := httptest.NewRecorder() h.xhrSend(rec, req) if rec.Code != http.StatusBadRequest || rec.Body.String() != "Broken JSON encoding." { t.Errorf("Unexpected response, got '%d,%s' expected '%d,Broken JSON encoding.'", rec.Code, rec.Body.String(), http.StatusBadRequest) } // unexpected EOF req, _ = http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"x")) rec = httptest.NewRecorder() h.xhrSend(rec, req) if rec.Code != http.StatusBadRequest || rec.Body.String() != "Broken JSON encoding." { t.Errorf("Unexpected response, got '%d,%s' expected '%d,Broken JSON encoding.'", rec.Code, rec.Body.String(), http.StatusBadRequest) } } func TestHandler_XhrSendSessionNotFound(t *testing.T) { h := Handler{} req, _ := http.NewRequest("POST", "/server/session/xhr_send", strings.NewReader("[\"some message\"]")) rec := httptest.NewRecorder() h.xhrSend(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("Unexpected response status, got '%d' expected '%d'", rec.Code, http.StatusNotFound) } } func TestHandler_XhrPoll(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr", nil) h.xhrPoll(rw, req) if rw.Header().Get("content-type") != "application/javascript; charset=UTF-8" { t.Errorf("Wrong content type received, got '%s'", rw.Header().Get("content-type")) } sess, _ := h.sessionByRequest(req) if rt := sess.ReceiverType(); rt != ReceiverTypeXHR { t.Errorf("Unexpected recevier type, got '%v', extected '%v'", rt, ReceiverTypeXHR) } } func TestHandler_XhrPollConnectionInterrupted(t *testing.T) { h := newTestHandler() sess := newTestSession() sess.state = SessionActive h.sessions["session"] = sess req, _ := http.NewRequest("POST", "/server/session/xhr", nil) ctx, cancel := context.WithCancel(req.Context()) req = req.WithContext(ctx) rw := httptest.NewRecorder() cancel() h.xhrPoll(rw, req) time.Sleep(1 * time.Millisecond) sess.mux.Lock() if sess.state != SessionClosed { t.Errorf("session should be closed") } } func TestHandler_XhrPollAnotherConnectionExists(t *testing.T) { h := newTestHandler() req, _ := http.NewRequest("POST", "/server/session/xhr", nil) // turn of timeoutes and heartbeats sess := newSession(req, "session", time.Hour, time.Hour) h.sessions["session"] = sess noError(t, sess.attachReceiver(newTestReceiver())) req, _ = http.NewRequest("POST", "/server/session/xhr", nil) rw2 := httptest.NewRecorder() h.xhrPoll(rw2, req) if rw2.Body.String() != "c[2010,\"Another connection still open\"]\n" { t.Errorf("Unexpected body, got '%s'", rw2.Body) } } func TestHandler_XhrStreaming(t *testing.T) { h := newTestHandler() rw := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr_streaming", nil) h.xhrStreaming(rw, req) expectedBody := strings.Repeat("h", 2048) + "\no\n" if rw.Body.String() != expectedBody { t.Errorf("Unexpected body, got '%s' expected '%s'", rw.Body, expectedBody) } sess, _ := h.sessionByRequest(req) if rt := sess.ReceiverType(); rt != ReceiverTypeXHRStreaming { t.Errorf("Unexpected recevier type, got '%v', extected '%v'", rt, ReceiverTypeXHRStreaming) } } func TestHandler_XhrStreamingAnotherReceiver(t *testing.T) { h := newTestHandler() h.options.ResponseLimit = 4096 rw1 := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/server/session/xhr_streaming", nil) ctx, cancel := context.WithCancel(req.Context()) req = req.WithContext(ctx) go func() { rec := httptest.NewRecorder() h.xhrStreaming(rec, req) expectedBody := strings.Repeat("h", 2048) + "\n" + "c[2010,\"Another connection still open\"]\n" if rec.Body.String() != expectedBody { t.Errorf("Unexpected body got '%s', expected '%s', ", rec.Body, expectedBody) } cancel() }() h.xhrStreaming(rw1, req) } // various test only structs func newTestHandler() *Handler { h := &Handler{sessions: make(map[string]*session)} h.options.HeartbeatDelay = time.Hour h.options.DisconnectDelay = time.Hour h.handlerFunc = func(s Session) {} return h }