diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f35f426..4d6bbe02 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: uses: golangci/golangci-lint-action@v3 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.51 + version: v1.54 test: name: Test and Coverage @@ -27,12 +27,12 @@ jobs: - name: Set up Go uses: actions/setup-go@v1 with: - go-version: 1.16 + go-version: 1.18 - name: Check out code uses: actions/checkout@v1 - - name: Run Unit tests. + - name: Run Unit tests run: make test-coverage - name: Upload Coverage report to CodeCov @@ -79,7 +79,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v1 with: - go-version: 1.16 + go-version: 1.18 - name: Check out code uses: actions/checkout@v1 diff --git a/.run/go test signalr_test.run.xml b/.run/go test signalr_test.run.xml index 5b51c9cb..d79f445d 100644 --- a/.run/go test signalr_test.run.xml +++ b/.run/go test signalr_test.run.xml @@ -9,12 +9,12 @@ - + - - + + diff --git a/Makefile b/Makefile index 395bd77b..13269612 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ test-coverage: ## Run tests with coverage @cat cover.out >> coverage.txt build: dep ## Build the binary file - @go build -i -o build/main $(PKG) + @go build -o build/main $(PKG) clean: ## Remove previous build @rm -f $(PROJECT_NAME)/build diff --git a/clientoptions.go b/clientoptions.go index f5465464..4aed5e53 100644 --- a/clientoptions.go +++ b/clientoptions.go @@ -1,8 +1,10 @@ package signalr import ( + "context" "errors" "fmt" + "github.com/cenkalti/backoff/v4" ) @@ -36,6 +38,58 @@ func WithConnector(connectionFactory func() (Connection, error)) func(Party) err } } +// HttpConnectionFactory is a connectionFactory for WithConnector which first tries to create a connection +// with WebSockets (if it is allowed by the HttpConnection options) and if this fails, falls back to a SSE based connection. +func HttpConnectionFactory(ctx context.Context, address string, options ...func(*httpConnection) error) (Connection, error) { + conn := &httpConnection{} + for i, option := range options { + if err := option(conn); err != nil { + return nil, err + } + if conn.transports != nil { + // Remove the WithTransports option + options = append(options[:i], options[i+1:]...) + break + } + } + // If no WithTransports was given, NewHTTPConnection fallbacks to both + if conn.transports == nil { + conn.transports = []TransportType{TransportWebSockets, TransportServerSentEvents} + } + + for _, transport := range conn.transports { + // If Websockets are allowed, we try to connect with these + if transport == TransportWebSockets { + wsOptions := append(options, WithTransports(TransportWebSockets)) + conn, err := NewHTTPConnection(ctx, address, wsOptions...) + // If this is ok, return the conn + if err == nil { + return conn, err + } + break + } + } + for _, transport := range conn.transports { + // If SSE is allowed, with fallback to try these + if transport == TransportServerSentEvents { + sseOptions := append(options, WithTransports(TransportServerSentEvents)) + return NewHTTPConnection(ctx, address, sseOptions...) + } + } + // None of the transports worked + return nil, fmt.Errorf("can not connect with supported transports: %v", conn.transports) +} + +// WithHttpConnection first tries to create a connection +// with WebSockets (if it is allowed by the HttpConnection options) and if this fails, falls back to a SSE based connection. +// This strategy is also used for auto reconnect if this option is used. +// WithHttpConnection is a shortcut for WithConnector(HttpConnectionFactory(...)) +func WithHttpConnection(ctx context.Context, address string, options ...func(*httpConnection) error) func(Party) error { + return WithConnector(func() (Connection, error) { + return HttpConnectionFactory(ctx, address, options...) + }) +} + // WithReceiver sets the object which will receive server side calls to client methods (e.g. callbacks) func WithReceiver(receiver interface{}) func(Party) error { return func(party Party) error { @@ -64,7 +118,7 @@ func WithBackoff(backoffFactory func() backoff.BackOff) func(party Party) error } // TransferFormat sets the transfer format used on the transport. Allowed values are "Text" and "Binary" -func TransferFormat(format string) func(Party) error { +func TransferFormat(format TransferFormatType) func(Party) error { return func(p Party) error { if c, ok := p.(*client); ok { switch format { diff --git a/doc.go b/doc.go index 77c519ad..95c9862a 100644 --- a/doc.go +++ b/doc.go @@ -32,7 +32,7 @@ which kind of connection (Websockets, Server-Sent Events) will be used. // Client with JSON encoding client, err := NewClient(ctx, WithConnection(conn), - TransferFormat("Text"), + TransferFormat(TransferFormatText), WithReceiver(receiver)) client.Start() diff --git a/go.mod b/go.mod index 305c7fcb..6420a099 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/philippseith/signalr -go 1.16 +go 1.18 require ( github.com/cenkalti/backoff/v4 v4.2.1 @@ -16,7 +16,17 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/klauspost/compress v1.16.6 // indirect + github.com/nxadm/tail v1.4.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb // indirect + golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.10.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index aa113a8a..00870af7 100644 --- a/go.sum +++ b/go.sum @@ -78,13 +78,9 @@ github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQ 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/teivah/onecontext v1.3.0 h1:tbikMhAlo6VhAuEGCvhc8HlTnpX4xTNPTOseWuhO1J0= @@ -97,29 +93,20 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -127,31 +114,19 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/httpconnection.go b/httpconnection.go index 5bb4d7b1..83448b85 100644 --- a/httpconnection.go +++ b/httpconnection.go @@ -18,8 +18,9 @@ type Doer interface { } type httpConnection struct { - client Doer - headers func() http.Header + client Doer + headers func() http.Header + transports []TransportType } // WithHTTPClient sets the http client used to connect to the signalR server. @@ -39,6 +40,21 @@ func WithHTTPHeaders(headers func() http.Header) func(*httpConnection) error { } } +func WithTransports(transports ...TransportType) func(*httpConnection) error { + return func(c *httpConnection) error { + for _, transport := range transports { + switch transport { + case TransportWebSockets, TransportServerSentEvents: + // Supported + default: + return fmt.Errorf("unsupported transport %s", transport) + } + } + c.transports = transports + return nil + } +} + // NewHTTPConnection creates a signalR HTTP Connection for usage with a Client. // ctx can be used to cancel the SignalR negotiation during the creation of the Connection // but not the Connection itself. @@ -56,6 +72,9 @@ func NewHTTPConnection(ctx context.Context, address string, options ...func(*htt if httpConn.client == nil { httpConn.client = http.DefaultClient } + if len(httpConn.transports) == 0 { + httpConn.transports = []TransportType{TransportWebSockets, TransportServerSentEvents} + } reqURL, err := url.Parse(address) if err != nil { @@ -88,22 +107,22 @@ func NewHTTPConnection(ctx context.Context, address string, options ...func(*htt return nil, err } - nr := negotiateResponse{} - if err := json.Unmarshal(body, &nr); err != nil { + negotiateResponse := negotiateResponse{} + if err := json.Unmarshal(body, &negotiateResponse); err != nil { return nil, err } q := reqURL.Query() - q.Set("id", nr.ConnectionID) + q.Set("id", negotiateResponse.ConnectionID) reqURL.RawQuery = q.Encode() // Select the best connection var conn Connection switch { - case nr.getTransferFormats("WebTransports") != nil: + case negotiateResponse.hasTransport("WebTransports"): // TODO - case nr.getTransferFormats("WebSockets") != nil: + case httpConn.hasTransport(TransportWebSockets) && negotiateResponse.hasTransport(TransportWebSockets): wsURL := reqURL // switch to wss for secure connection @@ -131,9 +150,9 @@ func NewHTTPConnection(ctx context.Context, address string, options ...func(*htt } // TODO think about if the API should give the possibility to cancel this connection - conn = newWebSocketConnection(context.Background(), nr.ConnectionID, ws) + conn = newWebSocketConnection(context.Background(), negotiateResponse.ConnectionID, ws) - case nr.getTransferFormats("ServerSentEvents") != nil: + case httpConn.hasTransport(TransportServerSentEvents) && negotiateResponse.hasTransport(TransportServerSentEvents): req, err := http.NewRequest("GET", reqURL.String(), nil) if err != nil { return nil, err @@ -149,7 +168,7 @@ func NewHTTPConnection(ctx context.Context, address string, options ...func(*htt return nil, err } - conn, err = newClientSSEConnection(address, nr.ConnectionID, resp.Body) + conn, err = newClientSSEConnection(address, negotiateResponse.ConnectionID, resp.Body) if err != nil { return nil, err } @@ -165,3 +184,12 @@ func closeResponseBody(body io.ReadCloser) { _, _ = io.Copy(io.Discard, body) _ = body.Close() } + +func (h *httpConnection) hasTransport(transport TransportType) bool { + for _, t := range h.transports { + if transport == t { + return true + } + } + return false +} diff --git a/httpmux.go b/httpmux.go index 167be121..0291c780 100644 --- a/httpmux.go +++ b/httpmux.go @@ -211,17 +211,17 @@ func (h *httpMux) negotiate(w http.ResponseWriter, req *http.Request) { var availableTransports []availableTransport for _, transport := range h.server.availableTransports() { switch transport { - case "ServerSentEvents": + case TransportServerSentEvents: availableTransports = append(availableTransports, availableTransport{ - Transport: "ServerSentEvents", - TransferFormats: []string{"Text"}, + Transport: string(TransportServerSentEvents), + TransferFormats: []string{string(TransferFormatText)}, }) - case "WebSockets": + case TransportWebSockets: availableTransports = append(availableTransports, availableTransport{ - Transport: "WebSockets", - TransferFormats: []string{"Text", "Binary"}, + Transport: string(TransportWebSockets), + TransferFormats: []string{string(TransferFormatText), string(TransferFormatBinary)}, }) } } diff --git a/httpserver_test.go b/httpserver_test.go index 8059ad4d..47466245 100644 --- a/httpserver_test.go +++ b/httpserver_test.go @@ -34,17 +34,25 @@ func (w *addHub) Echo(s string) string { } var _ = Describe("HTTP server", func() { - for _, transport := range [][]string{ - {"WebSockets", "Text"}, - {"WebSockets", "Binary"}, - {"ServerSentEvents", "Text"}, - } { - transport := transport - Context(fmt.Sprintf("%v %v", transport[0], transport[1]), func() { + for i := 0; i < 3; i++ { + var transport TransportType + var transferFormat TransferFormatType + switch i { + case 0: + transport = TransportWebSockets + transferFormat = TransferFormatText + case 1: + transport = TransportWebSockets + transferFormat = TransferFormatBinary + case 2: + transport = TransportServerSentEvents + transferFormat = TransferFormatText + } + Context(fmt.Sprintf("%v %v", transport, transferFormat), func() { Context("A correct negotiation request is sent", func() { It(fmt.Sprintf("should send a correct negotiation response with support for %v with text protocol", transport), func(done Done) { // Start server - server, err := NewServer(context.TODO(), SimpleHubFactory(&addHub{}), HTTPTransports(transport[0]), testLoggerOption()) + server, err := NewServer(context.TODO(), SimpleHubFactory(&addHub{}), HTTPTransports(transport), testLoggerOption()) Expect(err).NotTo(HaveOccurred()) router := http.NewServeMux() server.MapHTTP(WithHTTPServeMux(router), "/hub") @@ -59,11 +67,11 @@ var _ = Describe("HTTP server", func() { Expect(len(avt)).To(BeNumerically(">", 0)) Expect(avt[0]).To(BeAssignableToTypeOf(map[string]interface{}{})) avtVal := avt[0].(map[string]interface{}) - Expect(avtVal["transport"]).To(Equal(transport[0])) + Expect(avtVal["transport"]).To(Equal(string(transport))) Expect(avtVal["transferFormats"]).To(BeAssignableToTypeOf([]interface{}{})) tf := avtVal["transferFormats"].([]interface{}) Expect(tf).To(ContainElement("Text")) - if transport[0] == "WebSockets" { + if transport == TransportWebSockets { Expect(tf).To(ContainElement("Binary")) } testServer.Close() @@ -74,7 +82,7 @@ var _ = Describe("HTTP server", func() { Context("A invalid negotiation request is sent", func() { It(fmt.Sprintf("should send a correct negotiation response with support for %v with text protocol", transport), func(done Done) { // Start server - server, err := NewServer(context.TODO(), SimpleHubFactory(&addHub{}), HTTPTransports(transport[0]), testLoggerOption()) + server, err := NewServer(context.TODO(), SimpleHubFactory(&addHub{}), HTTPTransports(transport), testLoggerOption()) Expect(err).NotTo(HaveOccurred()) router := http.NewServeMux() server.MapHTTP(WithHTTPServeMux(router), "/hub") @@ -98,7 +106,7 @@ var _ = Describe("HTTP server", func() { // Start server ctx, cancel := context.WithCancel(context.Background()) server, err := NewServer(ctx, - SimpleHubFactory(&addHub{}), HTTPTransports(transport[0]), + SimpleHubFactory(&addHub{}), HTTPTransports(transport), MaximumReceiveMessageSize(50000), Logger(logger, true)) Expect(err).NotTo(HaveOccurred()) @@ -115,7 +123,7 @@ var _ = Describe("HTTP server", func() { WithConnection(conn), MaximumReceiveMessageSize(60000), Logger(logger, true), - TransferFormat(transport[1])) + TransferFormat(transferFormat)) Expect(err).NotTo(HaveOccurred()) Expect(client).NotTo(BeNil()) client.Start() @@ -130,7 +138,7 @@ var _ = Describe("HTTP server", func() { client2, err := NewClient(ctx, WithConnection(conn2), Logger(logger, true), - TransferFormat(transport[1])) + TransferFormat(transferFormat)) Expect(err).NotTo(HaveOccurred()) Expect(client2).NotTo(BeNil()) client2.Start() @@ -154,7 +162,7 @@ var _ = Describe("HTTP server", func() { Context("When no negotiation is send", func() { It("should serve websocket requests", func(done Done) { // Start server - server, err := NewServer(context.TODO(), SimpleHubFactory(&addHub{}), HTTPTransports("WebSockets"), testLoggerOption()) + server, err := NewServer(context.TODO(), SimpleHubFactory(&addHub{}), HTTPTransports(TransportWebSockets), testLoggerOption()) Expect(err).NotTo(HaveOccurred()) router := http.NewServeMux() server.MapHTTP(WithHTTPServeMux(router), "/hub") @@ -169,6 +177,34 @@ var _ = Describe("HTTP server", func() { }) }) +var _ = Describe("HTTP client", func() { + Context("WithHttpConnection", func() { + It("should fallback to SSE (this can only be tested when httpConnection is tampered with)", func(done Done) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + server, err := NewServer(ctx, SimpleHubFactory(&addHub{}), HTTPTransports(TransportWebSockets, TransportServerSentEvents), testLoggerOption()) + Expect(err).NotTo(HaveOccurred()) + router := http.NewServeMux() + server.MapHTTP(WithHTTPServeMux(router), "/hub") + testServer := httptest.NewServer(router) + url, _ := url.Parse(testServer.URL) + port, _ := strconv.Atoi(url.Port()) + waitForPort(port) + + client, err := NewClient(ctx, WithHttpConnection(ctx, fmt.Sprintf("http://127.0.0.1:%v/hub", port))) + Expect(err).NotTo(HaveOccurred()) + + client.Start() + Expect(<-client.WaitForState(context.Background(), ClientConnected)).NotTo(HaveOccurred()) + result := <-client.Invoke("Add2", 2) + Expect(result.Error).NotTo(HaveOccurred()) + + close(done) + }, 2.0) + }) +}) + type nonProtocolLogger struct { logger StructuredLogger } diff --git a/negotiateresponse.go b/negotiateresponse.go index b06b04e5..6b2680b7 100644 --- a/negotiateresponse.go +++ b/negotiateresponse.go @@ -1,5 +1,15 @@ package signalr +type TransportType string + +var TransportWebSockets TransportType = "WebSockets" +var TransportServerSentEvents TransportType = "ServerSentEvents" + +type TransferFormatType string + +var TransferFormatText TransferFormatType = "Text" +var TransferFormatBinary TransferFormatType = "Binary" + type availableTransport struct { Transport string `json:"transport"` TransferFormats []string `json:"transferFormats"` @@ -12,11 +22,11 @@ type negotiateResponse struct { AvailableTransports []availableTransport `json:"availableTransports"` } -func (nr *negotiateResponse) getTransferFormats(transportType string) []string { +func (nr *negotiateResponse) hasTransport(transportType TransportType) bool { for _, transport := range nr.AvailableTransports { - if transport.Transport == transportType { - return transport.TransferFormats + if transport.Transport == string(transportType) { + return true } } - return nil + return false } diff --git a/router/router_test/router_test.go b/router/router_test/router_test.go index 7eb01850..b3d45f91 100644 --- a/router/router_test/router_test.go +++ b/router/router_test/router_test.go @@ -21,6 +21,7 @@ import ( "github.com/gorilla/mux" "github.com/philippseith/signalr" + "github.com/philippseith/signalr/router" . "github.com/onsi/ginkgo" @@ -72,7 +73,7 @@ var _ = Describe("Router", func() { It("should send a correct negotiation response", func(done Done) { // Start server ctx, serverCancel := context.WithCancel(context.Background()) - server, err := signalr.NewServer(ctx, signalr.SimpleHubFactory(&addHub{}), signalr.HTTPTransports("WebSockets")) + server, err := signalr.NewServer(ctx, signalr.SimpleHubFactory(&addHub{}), signalr.HTTPTransports(signalr.TransportWebSockets)) Expect(err).NotTo(HaveOccurred()) port := freePort() initFunc(server, port) diff --git a/server.go b/server.go index e600c231..6ef8dc2b 100644 --- a/server.go +++ b/server.go @@ -16,10 +16,12 @@ import ( // Server is a SignalR server for one type of hub. // -// MapHTTP(mux *http.ServeMux, path string) +// MapHTTP(mux *http.ServeMux, path string) +// // maps the servers' hub to a path on a http.ServeMux. // -// Serve(conn Connection) +// Serve(conn Connection) +// // serves the hub of the server on one connection. // The same server might serve different connections in parallel. Serve does not return until the connection is closed // or the servers' context is canceled. @@ -32,7 +34,7 @@ type Server interface { MapHTTP(routerFactory func() MappableRouter, path string) Serve(conn Connection) error HubClients() HubClients - availableTransports() []string + availableTransports() []TransportType } type server struct { @@ -42,7 +44,7 @@ type server struct { defaultHubClients *defaultHubClients groupManager GroupManager reconnectAllowed bool - transports []string + transports []TransportType } // NewServer creates a new server for one type of hub. The hub type is set by one of the @@ -70,7 +72,7 @@ func NewServer(ctx context.Context, options ...func(Party) error) (Server, error } } if server.transports == nil { - server.transports = []string{"WebSockets", "ServerSentEvents"} + server.transports = []TransportType{TransportWebSockets, TransportServerSentEvents} } if server.newHub == nil { return server, errors.New("cannot determine hub type. Neither UseHub, HubFactory or SimpleHubFactory given as option") @@ -123,7 +125,7 @@ func (s *server) HubClients() HubClients { return s.defaultHubClients } -func (s *server) availableTransports() []string { +func (s *server) availableTransports() []TransportType { return s.transports } diff --git a/serveroptions.go b/serveroptions.go index 84931ac8..903381e9 100644 --- a/serveroptions.go +++ b/serveroptions.go @@ -42,12 +42,12 @@ func SimpleHubFactory(hubProto HubInterface) func(Party) error { // HTTPTransports sets the list of available transports for http connections. Allowed transports are // "WebSockets", "ServerSentEvents". Default is both transports are available. -func HTTPTransports(transports ...string) func(Party) error { +func HTTPTransports(transports ...TransportType) func(Party) error { return func(p Party) error { if s, ok := p.(*server); ok { for _, transport := range transports { switch transport { - case "WebSockets", "ServerSentEvents": + case TransportWebSockets, TransportServerSentEvents: s.transports = append(s.transports, transport) default: return fmt.Errorf("unsupported transport: %v", transport) diff --git a/serveroptions_test.go b/serveroptions_test.go index 8cfe0ae3..1c099d8f 100644 --- a/serveroptions_test.go +++ b/serveroptions_test.go @@ -347,22 +347,22 @@ var _ = Describe("Server options", func() { Describe("HTTPTransports option", func() { Context("When HTTPTransports is one of WebSockets, ServerSentEvents or both", func() { It("should set these transports", func(done Done) { - s, err := NewServer(context.TODO(), UseHub(&singleHub{}), HTTPTransports("WebSockets"), testLoggerOption()) + s, err := NewServer(context.TODO(), UseHub(&singleHub{}), HTTPTransports(TransportWebSockets), testLoggerOption()) Expect(err).NotTo(HaveOccurred()) - Expect(s.availableTransports()).To(ContainElement("WebSockets")) + Expect(s.availableTransports()).To(ContainElement(TransportWebSockets)) close(done) }) It("should set these transports", func(done Done) { - s, err := NewServer(context.TODO(), UseHub(&singleHub{}), HTTPTransports("ServerSentEvents"), testLoggerOption()) + s, err := NewServer(context.TODO(), UseHub(&singleHub{}), HTTPTransports(TransportServerSentEvents), testLoggerOption()) Expect(err).NotTo(HaveOccurred()) - Expect(s.availableTransports()).To(ContainElement("ServerSentEvents")) + Expect(s.availableTransports()).To(ContainElement(TransportServerSentEvents)) close(done) }) It("should set these transports", func(done Done) { - s, err := NewServer(context.TODO(), UseHub(&singleHub{}), HTTPTransports("ServerSentEvents", "WebSockets"), testLoggerOption()) + s, err := NewServer(context.TODO(), UseHub(&singleHub{}), HTTPTransports(TransportServerSentEvents, TransportWebSockets), testLoggerOption()) Expect(err).NotTo(HaveOccurred()) - Expect(s.availableTransports()).To(ContainElement("WebSockets")) - Expect(s.availableTransports()).To(ContainElement("ServerSentEvents")) + Expect(s.availableTransports()).To(ContainElement(TransportWebSockets)) + Expect(s.availableTransports()).To(ContainElement(TransportServerSentEvents)) close(done) }) }) @@ -375,7 +375,7 @@ var _ = Describe("Server options", func() { }) Context("When HTTPTransports is used on a client", func() { It("should return an error", func(done Done) { - _, err := NewClient(context.TODO(), WithConnection(newTestingConnection()), HTTPTransports("ServerSentEvents"), testLoggerOption()) + _, err := NewClient(context.TODO(), WithConnection(newTestingConnection()), HTTPTransports(TransportServerSentEvents), testLoggerOption()) Expect(err).To(HaveOccurred()) close(done) }) @@ -401,16 +401,16 @@ func newChannelWriter() *channelWriter { return &channelWriter{make(chan []byte, 100)} } -//type mapLogger struct { +// type mapLogger struct { // c chan bool // m map[string]string -//} +// } // -//func (m *mapLogger) Log(keyvals ...interface{}) error { +// func (m *mapLogger) Log(keyvals ...interface{}) error { // m.m = make(map[string]string) // for i := 0; i < len(keyvals); i += 2 { // m.m[keyvals[i].(string)] = keyvals[i+1].(string) // } // m.c <- true // return nil -//} +// } diff --git a/signalr_test/server_test.go b/signalr_test/server_test.go index 5b87dbe4..dd62e6ea 100644 --- a/signalr_test/server_test.go +++ b/signalr_test/server_test.go @@ -49,15 +49,15 @@ func TestServerSmoke(t *testing.T) { } func TestServerJsonWebSockets(t *testing.T) { - testServer(t, "^JSON", signalr.HTTPTransports("WebSockets")) + testServer(t, "^JSON", signalr.HTTPTransports(signalr.TransportWebSockets)) } func TestServerJsonSSE(t *testing.T) { - testServer(t, "^JSON", signalr.HTTPTransports("ServerSentEvents")) + testServer(t, "^JSON", signalr.HTTPTransports(signalr.TransportServerSentEvents)) } func TestServerMessagePack(t *testing.T) { - testServer(t, "^MessagePack", signalr.HTTPTransports("WebSockets")) + testServer(t, "^MessagePack", signalr.HTTPTransports(signalr.TransportWebSockets)) } func testServer(t *testing.T, testNamePattern string, transports func(signalr.Party) error) {