pax_global_header 0000666 0000000 0000000 00000000064 14143274105 0014513 g ustar 00root root 0000000 0000000 52 comment=e2b1424d8982f08dbbaee29fc3c38dac230dd719 sockjs-go-3.0.2/ 0000775 0000000 0000000 00000000000 14143274105 0013414 5 ustar 00root root 0000000 0000000 sockjs-go-3.0.2/.gitignore 0000664 0000000 0000000 00000000442 14143274105 0015404 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000000642 14143274105 0015527 0 ustar 00root root 0000000 0000000 language: 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/LICENSE 0000664 0000000 0000000 00000002743 14143274105 0014427 0 ustar 00root root 0000000 0000000 Copyright (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.md 0000664 0000000 0000000 00000007414 14143274105 0014701 0 ustar 00root root 0000000 0000000 [](https://travis-ci.org/igm/sockjs-go) [](https://pkg.go.dev/github.com/igm/sockjs-go/v3/sockjs?tab=doc) [](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/ 0000775 0000000 0000000 00000000000 14143274105 0015232 5 ustar 00root root 0000000 0000000 sockjs-go-3.0.2/examples/webchat/ 0000775 0000000 0000000 00000000000 14143274105 0016647 5 ustar 00root root 0000000 0000000 sockjs-go-3.0.2/examples/webchat/README.md 0000664 0000000 0000000 00000000310 14143274105 0020120 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14143274105 0017424 5 ustar 00root root 0000000 0000000 sockjs-go-3.0.2/examples/webchat/web/app.js 0000664 0000000 0000000 00000001412 14143274105 0020540 0 ustar 00root root 0000000 0000000 if (!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.html 0000664 0000000 0000000 00000001242 14143274105 0021420 0 ustar 00root root 0000000 0000000
This is a SockJS hidden iframe. It's used for cross domain magic.
` sockjs-go-3.0.2/sockjs/iframe_test.go 0000664 0000000 0000000 00000002217 14143274105 0017543 0 ustar 00root root 0000000 0000000 package 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 = `This is a SockJS hidden iframe. It's used for cross domain magic.
` sockjs-go-3.0.2/sockjs/jsonp.go 0000664 0000000 0000000 00000003776 14143274105 0016405 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007055 14143274105 0017436 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001420 14143274105 0016667 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002040 14143274105 0017725 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013625 14143274105 0016741 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003522 14143274105 0017773 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006236 14143274105 0017746 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000011622 14143274105 0021000 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000012436 14143274105 0016730 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000023005 14143274105 0017761 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001115 14143274105 0016531 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001452 14143274105 0017574 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000473 14143274105 0016403 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000611 14143274105 0017434 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003652 14143274105 0016022 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010176 14143274105 0017060 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005305 14143274105 0017230 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013151 14143274105 0020265 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004766 14143274105 0016055 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014423 14143274105 0017103 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14143274105 0015622 5 ustar 00root root 0000000 0000000 sockjs-go-3.0.2/testserver/server.go 0000664 0000000 0000000 00000003401 14143274105 0017455 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14143274105 0013744 5 ustar 00root root 0000000 0000000 sockjs-go-3.0.2/v3/go.mod 0000664 0000000 0000000 00000000202 14143274105 0015044 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000002167 14143274105 0015105 0 ustar 00root root 0000000 0000000 github.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/ 0000775 0000000 0000000 00000000000 14143274105 0015240 5 ustar 00root root 0000000 0000000 sockjs-go-3.0.2/v3/sockjs/benchmarks_test.go 0000664 0000000 0000000 00000006530 14143274105 0020747 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001601 14143274105 0017036 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000131 14143274105 0016327 0 ustar 00root root 0000000 0000000 /* Package sockjs is a server side implementation of sockjs protocol. */ package sockjs sockjs-go-3.0.2/v3/sockjs/eventsource.go 0000664 0000000 0000000 00000002323 14143274105 0020131 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006637 14143274105 0024772 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001413 14143274105 0023754 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005560 14143274105 0021176 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001424 14143274105 0021757 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000320 14143274105 0016654 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000323 14143274105 0017716 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007714 14143274105 0017215 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010743 14143274105 0020250 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003772 14143274105 0017404 0 ustar 00root root 0000000 0000000 package sockjs import ( "fmt" "io" "net/http" "regexp" "strings" ) var iframeTemplate = `This is a SockJS hidden iframe. It's used for cross domain magic.
` sockjs-go-3.0.2/v3/sockjs/iframe_test.go 0000664 0000000 0000000 00000002217 14143274105 0020073 0 ustar 00root root 0000000 0000000 package 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 = `This is a SockJS hidden iframe. It's used for cross domain magic.
` sockjs-go-3.0.2/v3/sockjs/jsonp.go 0000664 0000000 0000000 00000004716 14143274105 0016730 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007322 14143274105 0017763 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001420 14143274105 0017217 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002040 14143274105 0020255 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014013 14143274105 0017261 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003601 14143274105 0020321 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006771 14143274105 0020302 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000012126 14143274105 0021330 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001574 14143274105 0017402 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014360 14143274105 0017256 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000023043 14143274105 0020313 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000017 14143274105 0017061 0 ustar 00root root 0000000 0000000 package sockjs sockjs-go-3.0.2/v3/sockjs/sockjs_test.go 0000664 0000000 0000000 00000001442 14143274105 0020123 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000473 14143274105 0016733 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000611 14143274105 0017764 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003650 14143274105 0016350 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010176 14143274105 0017410 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004675 14143274105 0017571 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013752 14143274105 0020624 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005727 14143274105 0016403 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014725 14143274105 0017440 0 ustar 00root root 0000000 0000000 package 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 }