├── .gitignore ├── LICENSE ├── README.md ├── consumer.go ├── doc.go ├── eventmessage.go ├── eventmessage_test.go ├── eventsource.go ├── eventsource_test.go ├── settings.go └── settings_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Matthias Kalb, Railsmechanic. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EventSource (SSE) 2 | EventSource is a powerful package for quickly setting up an EventSource server in Golang. 3 | It can be used wherever you need to publish events/notifications. 4 | 5 | [See godoc here.](http://godoc.org/github.com/railsmechanic/eventsource) 6 | 7 | ## Features 8 | 9 | - Support for isolated channels *(channel1 will not see events of channel2)* 10 | - Support for global notifications across all channels *(every consumer receive this event)* 11 | - RESTful interface for publishing events, deleting, subscribing and getting information of/to channels 12 | - Token base authentication for publishing/deleting/getting information of channels 13 | - Support for CORS *(Allow-Origin, Allow-Method)* 14 | - Allows an individual configuration to set up EventSource for your needs 15 | - Simple and easy to use interface 16 | 17 | ## Getting Started 18 | First, get EventSource with `go get github.com/railsmechanic/eventsource` 19 | 20 | Then create a new `.go` file in your GOPATH. We'll call it `eventserver.go` and add the code below. 21 | ~~~go 22 | package main 23 | 24 | import ( 25 | "github.com/railsmechanic/eventsource" 26 | ) 27 | 28 | func main() { 29 | // EventSource with default settings 30 | es := eventsource.New(nil) 31 | es.Run() 32 | } 33 | ~~~ 34 | 35 | Then start the EventSource server with 36 | ~~~bash 37 | $ go run eventserver.go 38 | ~~~ 39 | Now, you will have an EventSource server running on `localhost:8080`, waiting for connections. 40 | 41 | 42 | #### Listen for events 43 | To test the new EventSource server, just use **curl** and subscribe to a channel called `updates`. 44 | ~~~bash 45 | $ curl http://localhost:8080/updates 46 | ~~~ 47 | You've successfully joined the channel `updates` and you're ready to receive incoming events. 48 | 49 | The channel name `updates` is not fixed, you can use your own name. 50 | ~~~bash 51 | $ curl http://localhost:8080/[mychannel] 52 | ~~~ 53 | 54 | 55 | #### Publish events 56 | To publish events, **curl** is your best friend, too. 57 | ~~~bash 58 | $ curl -H "Content-Type: application/json" -d '{"id":123, "event":"my-event", "data": "Hello World!"}' http://localhost:8080/updates 59 | $ curl -H "Content-Type: application/json" -d '{"id":456, "event":"my-event", "data": "Hello Again!"}' http://localhost:8080/updates 60 | ~~~ 61 | Yeah, you've sent two events to the `updates` channel. 62 | 63 | 64 | #### Received events 65 | Your consumer from above (and each other consumer) listening on channel `updates` has received the following events: 66 | ~~~bash 67 | $ curl http://localhost:8080/updates 68 | id: 123 69 | event: my-event 70 | data: Hello World! 71 | 72 | id: 456 73 | event: my-event 74 | data: Hello again! 75 | ~~~ 76 | 77 | ## Available Settings 78 | To setup EventSource with custom settings, just pass Settings to `New` 79 | ~~~go 80 | settings := &Settings{ 81 | Timeout: 30*time.Second, 82 | AuthToken: "secret", 83 | Host: "192.168.1.1", 84 | Port: 3000, 85 | CorsAllowOrigin: "*", 86 | CorsAllowMethod: []string{"GET", "POST"} 87 | } 88 | es := eventsource.New(settings) 89 | es.Run() 90 | ~~~ 91 | 92 | **Timeout** *(time.Duration)* - The default timeout for consumers to be disconnected. 93 | 94 | **AuthToken** *(string)* - Used to prevent unauthorized users to publish events, delete channels and get information on channels. 95 | 96 | **Host** *(string)* - The hostname/ip address on which the EventSource is bind on 97 | 98 | **Port** *(uint)* - The port on which the EventSource server will listen on 99 | 100 | **CorsAllowOrigin** *(string)* - Allow Cross Site HTTP request e.g. from "*" 101 | 102 | **CorsAllowMethod** *([]string)* - Explicit allow Cross Site Request Methods e.g. *"GET", "POST"* 103 | 104 | ## RESTful Interface or the Go Interface 105 | To communicate with EventSource *(publishing, deleting, etc.)* you can either use the RESTful or the Golang interface. 106 | 107 | #### The Go Interface 108 | ~~~go 109 | type EventSource interface { 110 | Router() *mux.Router 111 | SendMessage(io.Reader, string) 112 | ChannelExists(channel string) bool 113 | ConsumerCount(channel string) int 114 | ConsumerCountAll() int 115 | Channels() []string 116 | Close(channel string) 117 | CloseAll() 118 | Run() 119 | Stop() 120 | } 121 | ~~~ 122 | 123 | #### The RESTful interface 124 | To publish events e.g. from other applications or from another host in your network, you can use the RESTful interface. 125 | 126 | ##### Subscribe to channel/listening for events (GET Request) 127 | `GET: http://example.com/[channel] => Status: 200 OK` 128 | 129 | ~~~bash 130 | $ curl -X GET http://example.com/[channel] 131 | ~~~ 132 | 133 | 134 | ##### Publish events/messages (POST Request of Content-Type 'application/json') 135 | `POST: http://example.com/[channel] => Status: 201 Created` 136 | 137 | ~~~bash 138 | $ curl -X POST -H "Content-Type: application/json" -d '{"id":1, "event":"event", "data": "hello"}' http://example.com/[channel] 139 | ~~~ 140 | 141 | 142 | ##### Disconnect consumers and delete channel (DELETE Request) 143 | `DELETE: http://example.com/[channel] => Status: 200 OK` 144 | 145 | ~~~bash 146 | $ curl -X DELETE http://example.com/[channel] 147 | ~~~ 148 | 149 | 150 | ##### Get information of a channel (HEAD Request) 151 | `HEAD: http://example.com/[channel] => Status: 200 OK` 152 | 153 | ~~~bash 154 | $ curl -X HEAD -H "Connection: close" http://example.com/[channel] 155 | ~~~ 156 | 157 | *The requested information is returned as addional headers:* 158 | 159 | `X-Consumer-Count` Count of consumers subscribed to this channel (integer) 160 | 161 | `X-Channel-Exists` Channel exists (bool) 162 | 163 | `X-Available-Channels` List of existing channels (array) 164 | 165 | 166 | ## The ALL channel 167 | You already know how to work with individually named channels. For global tasks, EventSource offers the "special" channel name **all**. 168 | To publish events to consumers accross all channels just *POST* your event to the special endpoint `http://example.com/all`. 169 | 170 | ~~~bash 171 | $ curl -X POST -H "Content-Type: application/json" -d '{"id":1, "event":"event", "data": "hello"}' http://example.com/all 172 | ~~~ 173 | 174 | If you're interested in all the channels available, just *HEAD* to `http://example.com/all`. 175 | 176 | ~~~bash 177 | $ curl -X HEAD -H "Connection: close" http://example.com/all 178 | ~~~ 179 | 180 | 181 | ## Things you should know 182 | This EventSource service is mainly implemented to met the requirements of an internal project. 183 | Therefore it's quite possible that not all of the W3C standards are met. You have been warned! 184 | 185 | 186 | ## Special thanks to 187 | This package is based on some concepts of [antage/eventsource](https://github.com/antage/eventsource). 188 | Many thanks and thumbs up, you've done a great job. 189 | -------------------------------------------------------------------------------- /consumer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package eventsource 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | // Consumer stores information of a connected consumer. 16 | type consumer struct { 17 | connection net.Conn 18 | es *eventSource 19 | inbox chan *eventMessage 20 | channel string 21 | expired bool 22 | } 23 | 24 | // NewConsumer builds and returns a new consumer based on the given attributes. 25 | // A goroutine is started for handling incoming messages. 26 | func newConsumer(resp http.ResponseWriter, req *http.Request, es *eventSource, channel string) (*consumer, error) { 27 | connection, _, err := resp.(http.Hijacker).Hijack() 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | cr := &consumer{ 33 | connection: connection, 34 | es: es, 35 | inbox: make(chan *eventMessage), 36 | channel: channel, 37 | expired: false, 38 | } 39 | 40 | if err := cr.setupConnection(); err != nil { 41 | return nil, err 42 | } 43 | 44 | go cr.inboxDispatcher() 45 | 46 | return cr, nil 47 | } 48 | 49 | // SetupConnection is responsible to setup a usable connection to a consumer. 50 | // If an unexpected error (timeout,...) occurs, the connection gets closed. 51 | func (cr *consumer) setupConnection() error { 52 | headers := [][]byte{ 53 | []byte("HTTP/1.1 200 OK"), 54 | []byte("Content-Type: text/event-stream"), 55 | []byte("Cache-Control: no-cache"), 56 | []byte("Connection: keep-alive"), 57 | []byte(fmt.Sprintf("Access-Control-Allow-Origin: %s", cr.es.settings.GetCorsAllowOrigin())), 58 | []byte(fmt.Sprintf("Access-Control-Allow-Method: %s", cr.es.settings.GetCorsAllowMethod())), 59 | } 60 | 61 | headersData := append(bytes.Join(headers, []byte("\n")), []byte("\n\n")...) 62 | 63 | if _, err := cr.connection.Write(headersData); err != nil { 64 | cr.connection.Close() 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // InboxDispatcher processes incoming eventMessages. 72 | // It disconnects timed out consumers and initiates the removal from the consumer pool. 73 | func (cr *consumer) inboxDispatcher() { 74 | for message := range cr.inbox { 75 | cr.connection.SetWriteDeadline(time.Now().Add(cr.es.settings.GetTimeout())) 76 | if _, err := cr.connection.Write(message.Message()); err != nil { 77 | if netErr, ok := err.(net.Error); !ok || netErr.Timeout() { 78 | cr.expired = true 79 | cr.connection.Close() 80 | cr.es.expireConsumer <- cr 81 | return 82 | } 83 | } 84 | } 85 | cr.connection.Close() 86 | } 87 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package railsmechanic/eventsource implements EventSource Server-Sent Events (SSE) for Go. 7 | 8 | This implementation of an EventSource service offers isolated channels and 9 | a simple RESTful interface. It also offers a simple, token based authentication strategy. 10 | So publishing events, deleting channels or getting informations of a channel can be protected from unauthorized access. 11 | 12 | Main features are: 13 | 14 | - Support for isolated channels *(channel1 will not see events of channel2)* 15 | - Support for global notifications across all channels *(every consumer receive this event)* 16 | - RESTful interface for publishing events, deleting, subscribing and getting information of/to channels 17 | - Token base authentication for publishing/deleting/getting information of channels 18 | - Support for CORS *(Allow-Origin, Allow-Method)* 19 | - Allows an individual configuration to set up EventSource for your needs 20 | - Simple and easy to use interface 21 | 22 | Getting started: 23 | 24 | package main 25 | 26 | import ( 27 | "github.com/railsmechanic/eventsource" 28 | ) 29 | 30 | func main() { 31 | // EventSource with default settings 32 | es := eventsource.New(nil) 33 | es.Run() 34 | } 35 | 36 | Launched an EventSource service on '127.0.0.1:8080', just with few instructions. 37 | */ 38 | package eventsource 39 | -------------------------------------------------------------------------------- /eventmessage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package eventsource 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "strings" 13 | ) 14 | 15 | // EventMessage stores information of a message. 16 | type eventMessage struct { 17 | Id uint `json:"id"` 18 | Event string `json:"event"` 19 | Data string `json:"data"` 20 | Channel string `json:"-"` 21 | } 22 | 23 | // NewEventMessage builds and returns a new eventMessage based on the given JSON data stream. 24 | func newEventMessage(messageStream io.Reader, channel string) (*eventMessage, error) { 25 | var em eventMessage 26 | dec := json.NewDecoder(messageStream) 27 | for { 28 | if err := dec.Decode(&em); err == io.EOF { 29 | break 30 | } else if err != nil { 31 | return nil, err 32 | } 33 | } 34 | 35 | if channel == "" { 36 | em.Channel = "default" 37 | } else { 38 | em.Channel = channel 39 | } 40 | 41 | return &em, nil 42 | } 43 | 44 | // Message formats a []byte message which is finally sent to the consumers of a channel. 45 | // Empty fields or fields that does not match the standard are removed. 46 | func (em *eventMessage) Message() []byte { 47 | var messageData bytes.Buffer 48 | 49 | if em.Id > 0 { 50 | messageData.WriteString(fmt.Sprintf("id: %d\n", em.Id)) 51 | } 52 | 53 | if len(em.Event) > 0 { 54 | messageData.WriteString(fmt.Sprintf("event: %s\n", strings.Replace(em.Event, "\n", "", -1))) 55 | } 56 | 57 | if len(em.Data) > 0 { 58 | lines := strings.Split(em.Data, "\n") 59 | for _, line := range lines { 60 | messageData.WriteString(fmt.Sprintf("data: %s\n", line)) 61 | } 62 | } 63 | 64 | messageData.WriteString("\n") 65 | return messageData.Bytes() 66 | } 67 | -------------------------------------------------------------------------------- /eventmessage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package eventsource 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | // Helper function to build EventMessages 15 | func buildEventMessage(messageType, channel string) (*eventMessage, error) { 16 | var messageStream io.Reader 17 | switch messageType { 18 | case ModeAll: 19 | messageStream = strings.NewReader("{\"id\":1,\"event\":\"foo\",\"data\":\"bar\"}") 20 | case ModeNoid: 21 | messageStream = strings.NewReader("{\"event\":\"foo\",\"data\":\"bar\"}") 22 | case ModeNoevent: 23 | messageStream = strings.NewReader("{\"id\":1,\"data\":\"bar\"}") 24 | case ModeNodata: 25 | messageStream = strings.NewReader("{\"id\":1,\"event\":\"foo\"}") 26 | } 27 | 28 | return newEventMessage(messageStream, channel) 29 | } 30 | 31 | // Available test modes 32 | func messageModes() []string { 33 | return []string{ModeAll, ModeNoid, ModeNoevent, ModeNodata} 34 | } 35 | 36 | func TestBuildEventMessage(t *testing.T) { 37 | 38 | // Test EventMessage in different modes 39 | for _, mode := range messageModes() { 40 | if _, err := buildEventMessage(mode, "my-channel"); err != nil { 41 | t.Error("Unable build EventMessage from JSON data in mode", mode) 42 | } 43 | } 44 | 45 | // Test EventMessage to build without a channel name 46 | if _, err := buildEventMessage("all", ""); err != nil { 47 | t.Error("Unable build EventMessage from JSON data without channel name") 48 | } 49 | } 50 | 51 | func TestContentOfEventMessage(t *testing.T) { 52 | 53 | // Test EventMessage in different modes 54 | for _, mode := range messageModes() { 55 | em, _ := buildEventMessage(mode, "my-channel") 56 | switch mode { 57 | case ModeAll: 58 | if em.Id != 1 { 59 | t.Error("Expected 1 got", em.Id) 60 | } 61 | 62 | if em.Event != "foo" { 63 | t.Error("Expected 'foo' got", em.Event) 64 | } 65 | 66 | if em.Data != "bar" { 67 | t.Error("Expected 'bar' got", em.Data) 68 | } 69 | 70 | if em.Channel != "my-channel" { 71 | t.Error("Expected 'my-channel' got", em.Channel) 72 | } 73 | 74 | case ModeNoid: 75 | if em.Id != 0 { 76 | t.Error("Expected 0 got", em.Id) 77 | } 78 | 79 | if em.Event != "foo" { 80 | t.Error("Expected 'foo' got", em.Event) 81 | } 82 | 83 | if em.Data != "bar" { 84 | t.Error("Expected 'bar' got", em.Data) 85 | } 86 | 87 | if em.Channel != "my-channel" { 88 | t.Error("Expected 'my-channel' got", em.Channel) 89 | } 90 | 91 | case ModeNoevent: 92 | if em.Id != 1 { 93 | t.Error("Expected 1 got", em.Id) 94 | } 95 | 96 | if em.Event != "" { 97 | t.Error("Expected '' got", em.Event) 98 | } 99 | 100 | if em.Data != "bar" { 101 | t.Error("Expected 'bar' got", em.Data) 102 | } 103 | 104 | if em.Channel != "my-channel" { 105 | t.Error("Expected 'my-channel' got", em.Channel) 106 | } 107 | 108 | case ModeNodata: 109 | if em.Id != 1 { 110 | t.Error("Expected 1 got", em.Id) 111 | } 112 | 113 | if em.Event != "foo" { 114 | t.Error("Expected 'foo' got", em.Event) 115 | } 116 | 117 | if em.Data != "" { 118 | t.Error("Expected '' got", em.Data) 119 | } 120 | 121 | if em.Channel != "my-channel" { 122 | t.Error("Expected 'my-channel' got", em.Channel) 123 | } 124 | } 125 | } 126 | 127 | // Test EventMessage to use channel name 'default' when its omited 128 | if em, _ := buildEventMessage("all", ""); em.Channel != "default" { 129 | t.Error("Expected 'default' on empty channel argument, got", em.Channel) 130 | } 131 | } 132 | 133 | func TestByteMesssage(t *testing.T) { 134 | 135 | for _, mode := range messageModes() { 136 | em, _ := buildEventMessage(mode, "my-channel") 137 | 138 | var messageData bytes.Buffer 139 | 140 | if mode != ModeNoid { 141 | messageData.WriteString("id: 1\n") 142 | } 143 | 144 | if mode != ModeNoevent { 145 | messageData.WriteString("event: foo\n") 146 | } 147 | 148 | if mode != ModeNodata { 149 | messageData.WriteString("data: bar\n") 150 | } 151 | messageData.WriteString("\n") 152 | 153 | if !bytes.Equal(em.Message(), messageData.Bytes()) { 154 | t.Error("Byte Message is malformed in mode", mode) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /eventsource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package eventsource 6 | 7 | import ( 8 | "fmt" 9 | "github.com/gorilla/mux" 10 | "io" 11 | "log" 12 | "net/http" 13 | "runtime" 14 | "sort" 15 | "strings" 16 | ) 17 | 18 | const ( 19 | globalChannel = "all" 20 | ) 21 | 22 | // Interface of EventSource 23 | type EventSource interface { 24 | Router() *mux.Router 25 | SendMessage(io.Reader, string) 26 | ChannelExists(channel string) bool 27 | ConsumerCount(channel string) int 28 | ConsumerCountAll() int 29 | Channels() []string 30 | Close(channel string) 31 | CloseAll() 32 | Run() 33 | Stop() 34 | } 35 | 36 | // EventSource stores information required by the event source service. 37 | type eventSource struct { 38 | messageRouter chan *eventMessage 39 | expireConsumer chan *consumer 40 | addConsumer chan *consumer 41 | closeChannel chan string 42 | stopApplication chan bool 43 | settings *Settings 44 | consumers map[string][]*consumer 45 | } 46 | 47 | // New builds and returns a configured EventSource instance. 48 | // The instance is configured with default settings if no settings are given. 49 | // It starts a goroutine, which is the 'main hub' of the EventSource service. 50 | func New(settings *Settings) EventSource { 51 | if settings == nil { 52 | settings = &Settings{} 53 | } 54 | 55 | es := &eventSource{ 56 | messageRouter: make(chan *eventMessage), 57 | expireConsumer: make(chan *consumer), 58 | addConsumer: make(chan *consumer), 59 | closeChannel: make(chan string), 60 | stopApplication: make(chan bool), 61 | settings: settings, 62 | consumers: make(map[string][]*consumer), 63 | } 64 | 65 | go es.actionDispatcher() 66 | 67 | return es 68 | } 69 | 70 | // Router returns a router that can be used to integrate EventSource in already existing servers 71 | func (es *eventSource) Router() *mux.Router { 72 | router := mux.NewRouter() 73 | router.HandleFunc("/{channel:[a-z0-9-_]+}", es.subscribeHandler).Methods("GET") 74 | router.HandleFunc("/{channel:[a-z0-9-_]+}", es.publishHandler).Methods("POST") 75 | router.HandleFunc("/{channel:[a-z0-9-_]+}", es.closeHandler).Methods("DELETE") 76 | router.HandleFunc("/{channel:[a-z0-9-_]+}", es.informationHandler).Methods("HEAD") 77 | router.NotFoundHandler = http.HandlerFunc(channelNotFoundHandler) 78 | return router 79 | } 80 | 81 | // SendMessage sends a message to the consumers of a channel. 82 | // It is also used for sending messages to 'all' consumers. 83 | func (es *eventSource) SendMessage(messageStream io.Reader, channel string) { 84 | em, err := newEventMessage(messageStream, channel) 85 | if err != nil { 86 | log.Printf("[E] Unable to create event message for channel '%s'. %s", channel, err) 87 | return 88 | } 89 | es.messageRouter <- em 90 | } 91 | 92 | // ChannelExists checks whether a channel exits. 93 | func (es *eventSource) ChannelExists(channel string) bool { 94 | _, ok := es.consumers[channel] 95 | return ok 96 | } 97 | 98 | // ConsumerCount returns the amount of consumers subscribed to a channel. 99 | func (es *eventSource) ConsumerCount(channel string) int { 100 | if consumers, ok := es.consumers[channel]; ok { 101 | return len(consumers) 102 | } 103 | return 0 104 | } 105 | 106 | // ConsumerCountAll returns the overall amount of consumers. 107 | func (es *eventSource) ConsumerCountAll() int { 108 | var consumerCount int 109 | for _, consumers := range es.consumers { 110 | consumerCount += len(consumers) 111 | } 112 | return consumerCount 113 | } 114 | 115 | // Channel returns all available channels. 116 | func (es *eventSource) Channels() []string { 117 | channels := make([]string, 0) 118 | for channel, _ := range es.consumers { 119 | channels = append(channels, channel) 120 | } 121 | sort.Strings(channels) 122 | return channels 123 | } 124 | 125 | // Close closes a single, specified channel 126 | // Consumers gets disconnected. 127 | func (es *eventSource) Close(channel string) { 128 | es.closeChannel <- channel 129 | } 130 | 131 | // CloseAll closes all available channels 132 | // Consumers gets disconnected. 133 | func (es *eventSource) CloseAll() { 134 | es.closeChannel <- globalChannel 135 | } 136 | 137 | // Run starts the EventSource service 138 | func (es *eventSource) Run() { 139 | runtime.GOMAXPROCS(runtime.NumCPU()) 140 | router := es.Router() 141 | log.Printf("[I] Starting EventSource service on %s:%d\n", es.settings.GetHost(), es.settings.GetPort()) 142 | log.Fatal("[E]", http.ListenAndServe(fmt.Sprintf("%s:%d", es.settings.GetHost(), es.settings.GetPort()), router)) 143 | } 144 | 145 | // Stop stops the EventSource service 146 | func (es *eventSource) Stop() { 147 | es.stopApplication <- true 148 | } 149 | 150 | // SubscribeHandler handels new, incoming connections of consumers. 151 | // Allowed request type: [GET] 152 | // 153 | // Subscriptions to channel 'all' are rejected, because this is an reserved channel name. 154 | func (es *eventSource) subscribeHandler(rw http.ResponseWriter, req *http.Request) { 155 | params := mux.Vars(req) 156 | if channel := params["channel"]; len(channel) > 0 { 157 | if channel == globalChannel { 158 | log.Printf("[E] Subscribing consumer on %s to global notification channel 'all' rejected\n", req.RemoteAddr) 159 | http.Error(rw, "Error: Channel 'all' is reserved for global notifications. Please choose another channel name.", http.StatusBadRequest) 160 | return 161 | } 162 | 163 | cr, err := newConsumer(rw, req, es, channel) 164 | if err != nil { 165 | log.Printf("[E] Subscribing consumer on %s to channel '%s' failed\n", req.RemoteAddr, channel) 166 | http.Error(rw, fmt.Sprintf("[E] Unable to connect to channel '%s'.", channel), http.StatusInternalServerError) 167 | return 168 | } 169 | es.addConsumer <- cr 170 | } 171 | } 172 | 173 | // PublishHandler is responsible for publishing messages to channels. 174 | // Allowed request type: [POST] 175 | // 176 | // The Content-Type of this handler need to be 'application/json'. 177 | // If an Auth-Token is set up, only authenticated users can publish messages to channels. 178 | func (es *eventSource) publishHandler(rw http.ResponseWriter, req *http.Request) { 179 | if !es.Authenticated(req) { 180 | log.Printf("[E] Authentication of %s failed. Publishing to channel rejected\n", req.RemoteAddr) 181 | http.Error(rw, "Error: Authentication failed. Publishing to channel rejected.", http.StatusForbidden) 182 | return 183 | } 184 | 185 | if !validContentType(req.Header.Get("Content-Type")) { 186 | log.Printf("[E] Invalid Content-Type sent by %s. Expecting application/json\n", req.RemoteAddr) 187 | http.Error(rw, "Error: Invalid Content-Type. Expecting application/json.", http.StatusBadRequest) 188 | return 189 | } 190 | 191 | params := mux.Vars(req) 192 | if channel := params["channel"]; len(channel) > 0 { 193 | es.SendMessage(req.Body, channel) 194 | defer req.Body.Close() 195 | } 196 | rw.WriteHeader(http.StatusCreated) 197 | } 198 | 199 | // CloseHandler is responsible for the closing channels 200 | // Allowed request type: [DELETE] 201 | // 202 | // Consumers are disconnected. 203 | // If an Auth-Token is set up, only authenticated users can delete a channel. 204 | func (es *eventSource) closeHandler(rw http.ResponseWriter, req *http.Request) { 205 | if !es.Authenticated(req) { 206 | log.Printf("[E] Authentication of %s failed. Closing of channel rejected\n", req.RemoteAddr) 207 | http.Error(rw, "Error: Authentication failed. Closing of channel rejected.", http.StatusForbidden) 208 | return 209 | } 210 | 211 | params := mux.Vars(req) 212 | if channel := params["channel"]; len(channel) > 0 { 213 | es.Close(channel) 214 | } 215 | rw.WriteHeader(http.StatusOK) 216 | } 217 | 218 | // InformationHandler is responsible for the closing channels 219 | // Allowed request type: [HEAD] 220 | // 221 | // If an Auth-Token is set up, only authenticated users can view information of channels. 222 | func (es *eventSource) informationHandler(rw http.ResponseWriter, req *http.Request) { 223 | if !es.Authenticated(req) { 224 | log.Printf("[E] Authentication of %s failed. Gettings stats for channel rejected\n", req.RemoteAddr) 225 | http.Error(rw, "Error: Authentication failed. Gettings stats for channel rejected.", http.StatusForbidden) 226 | return 227 | } 228 | 229 | params := mux.Vars(req) 230 | if channel := params["channel"]; len(channel) > 0 { 231 | 232 | if channel == globalChannel { 233 | rw.Header().Add("X-Consumer-Count", fmt.Sprint(es.ConsumerCountAll())) 234 | rw.Header().Add("X-Available-Channels", fmt.Sprintf("[%s]", strings.Join(es.Channels(), ","))) 235 | } else { 236 | rw.Header().Add("X-Consumer-Count", fmt.Sprint(es.ConsumerCount(channel))) 237 | rw.Header().Add("X-Channel-Exists", fmt.Sprint(es.ChannelExists(channel))) 238 | } 239 | 240 | } 241 | rw.WriteHeader(http.StatusOK) 242 | } 243 | 244 | // ChannelNotFoundHandler is responsible for unknown channels. 245 | // When a consumer wants to connect to an unknown endpoint, an error message is returned. 246 | func channelNotFoundHandler(rw http.ResponseWriter, req *http.Request) { 247 | log.Printf("[E] Consumer %s tries to join invalid channel", req.RemoteAddr) 248 | http.Error(rw, "Error: Invalid channel name.", http.StatusNotFound) 249 | } 250 | 251 | // Authenticated validates the user submitted AUTH Token. 252 | func (es eventSource) Authenticated(req *http.Request) bool { 253 | authToken := strings.TrimSpace(req.Header.Get("Auth-Token")) 254 | if len(es.settings.GetAuthToken()) == 0 && len(authToken) == 0 { 255 | return true 256 | } 257 | return len(es.settings.GetAuthToken()) > 0 && authToken == es.settings.GetAuthToken() 258 | } 259 | 260 | // ValidContentType validates the submitted Content-Type. 261 | func validContentType(contentType string) bool { 262 | if strings.Contains(strings.ToLower(contentType), "application/json") { 263 | return true 264 | } 265 | return false 266 | } 267 | 268 | // ActionDispatcher is the central hub of the EventSource service. 269 | func (es *eventSource) actionDispatcher() { 270 | for { 271 | select { 272 | 273 | // em.messageRouter is responsible for delivering messages to consumers of channels. 274 | case em := <-es.messageRouter: 275 | switch em.Channel { 276 | default: 277 | if channelConsumers, ok := es.consumers[em.Channel]; ok { 278 | for _, channelConsumer := range channelConsumers { 279 | if cr := channelConsumer; !cr.expired { 280 | select { 281 | case cr.inbox <- em: 282 | default: 283 | } 284 | } 285 | } 286 | } 287 | case globalChannel: 288 | log.Println("[I] Sending global notification to all consumers") 289 | for _, channelConsumers := range es.consumers { 290 | for _, channelConsumer := range channelConsumers { 291 | if cr := channelConsumer; !cr.expired { 292 | select { 293 | case cr.inbox <- em: 294 | default: 295 | } 296 | } 297 | } 298 | } 299 | } 300 | 301 | // em.closeChannel is responsible for closing seleted or all channels. 302 | case channel := <-es.closeChannel: 303 | switch channel { 304 | default: 305 | if channelConsumers, ok := es.consumers[channel]; ok { 306 | log.Printf("[I] Closing channel '%s' and disconnecting consumers\n", channel) 307 | for _, channelConsumer := range channelConsumers { 308 | close(channelConsumer.inbox) 309 | } 310 | delete(es.consumers, channel) 311 | } 312 | case globalChannel: 313 | log.Println("[I] Closing all channels and disconnecting consumers") 314 | for channelName, channelConsumers := range es.consumers { 315 | for _, channelConsumer := range channelConsumers { 316 | close(channelConsumer.inbox) 317 | } 318 | delete(es.consumers, channelName) 319 | } 320 | } 321 | 322 | // em.stopApplication is responsible for shutting down the service properly. 323 | case <-es.stopApplication: 324 | log.Println("[I] Halting EventSource server") 325 | es.closeChannel <- globalChannel 326 | close(es.messageRouter) 327 | close(es.addConsumer) 328 | close(es.expireConsumer) 329 | close(es.closeChannel) 330 | close(es.stopApplication) 331 | return 332 | 333 | // em.addConsumer is responsible for adding consumers to channels. 334 | case cr := <-es.addConsumer: 335 | log.Printf("[I] Consumer %s joined channel '%s'\n", cr.connection.RemoteAddr(), cr.channel) 336 | es.consumers[cr.channel] = append(es.consumers[cr.channel], cr) 337 | 338 | // em.expireConsumer is responsible disconnecting and removing staled consumers. 339 | case expiredConsumer := <-es.expireConsumer: 340 | log.Printf("[I] Consumer %s expired and gets removed from channel '%s'\n", expiredConsumer.connection.RemoteAddr(), expiredConsumer.channel) 341 | if consumers, ok := es.consumers[expiredConsumer.channel]; ok { 342 | consumerSlice := make([]*consumer, 0) 343 | 344 | for _, cr := range consumers { 345 | if cr != expiredConsumer { 346 | consumerSlice = append(consumerSlice, cr) 347 | } 348 | } 349 | 350 | es.consumers[expiredConsumer.channel] = consumerSlice 351 | close(expiredConsumer.inbox) 352 | } 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /eventsource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package eventsource 6 | 7 | import ( 8 | "bytes" 9 | "github.com/gorilla/mux" 10 | "io" 11 | "net" 12 | "net/http" 13 | "net/http/httptest" 14 | "strconv" 15 | "strings" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | const ( 21 | ModeAll = "all" 22 | ModeNoid = "noid" 23 | ModeNoevent = "noevent" 24 | ModeNodata = "nodata" 25 | ) 26 | 27 | type testEventSource struct { 28 | eventSource EventSource 29 | testServer *httptest.Server 30 | } 31 | 32 | // Helper for building the EventSource environment 33 | func setupEventSource(t *testing.T, settings *Settings) *testEventSource { 34 | es := New(settings) 35 | if es == nil { 36 | t.Error("Unable to setup EventSource") 37 | } 38 | 39 | return &testEventSource{ 40 | eventSource: es, 41 | testServer: httptest.NewServer(es.Router()), 42 | } 43 | } 44 | 45 | // Helper to properly shutdown the EventSource environment 46 | func (es *testEventSource) closeEventSource() { 47 | es.eventSource.Stop() 48 | es.testServer.Close() 49 | } 50 | 51 | // Helper for reading EventSource responses 52 | func readResponse(t *testing.T, conn net.Conn) []byte { 53 | resp := make([]byte, 1024) 54 | if _, err := conn.Read(resp); err != nil && err != io.EOF { 55 | t.Error(err) 56 | } 57 | return resp 58 | } 59 | 60 | // Helper for joining an EventSource channel 61 | func (es *testEventSource) joinChannel(t *testing.T, channel string) (net.Conn, []byte) { 62 | conn, err := net.Dial("tcp", strings.Replace(es.testServer.URL, "http://", "", 1)) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | 67 | if _, err := conn.Write([]byte("GET /" + channel + " HTTP/1.1\n\n")); err != nil { 68 | t.Error(err) 69 | } 70 | 71 | return conn, readResponse(t, conn) 72 | } 73 | 74 | // Helper to compare EventSource responses 75 | func expectResponse(t *testing.T, conn net.Conn, expectedResponse string) { 76 | time.Sleep(100 * time.Millisecond) 77 | if resp := readResponse(t, conn); !strings.Contains(string(resp), expectedResponse) { 78 | t.Errorf("Expected response:\n%s\n and got:\n%s\n", expectedResponse, resp) 79 | } 80 | } 81 | 82 | // Helper function to build EventMessages 83 | func buildMessageData(messageType string) io.Reader { 84 | var messageStream io.Reader 85 | switch messageType { 86 | case ModeAll: 87 | messageStream = strings.NewReader("{\"id\":1,\"event\":\"foo\",\"data\":\"bar\"}") 88 | case ModeNoid: 89 | messageStream = strings.NewReader("{\"event\":\"foo\",\"data\":\"bar\"}") 90 | case ModeNoevent: 91 | messageStream = strings.NewReader("{\"id\":1,\"data\":\"bar\"}") 92 | case ModeNodata: 93 | messageStream = strings.NewReader("{\"id\":1,\"event\":\"foo\"}") 94 | } 95 | return messageStream 96 | } 97 | 98 | func TestRouter(t *testing.T) { 99 | es := New(nil) 100 | router := es.Router() 101 | var match mux.RouteMatch 102 | 103 | // Testing Router with a GET Request and a proper formated channel name 104 | req, err := http.NewRequest("GET", "http://127.0.0.1/default", nil) 105 | if err != nil { 106 | t.Error(err) 107 | } 108 | 109 | if !router.Match(req, &match) { 110 | t.Error("Method 'GET' on is not allowed for channel 'default'") 111 | } 112 | 113 | // Testing Router with a POST Request and a proper formated channel name 114 | req, err = http.NewRequest("POST", "http://127.0.0.1/default", nil) 115 | if err != nil { 116 | t.Error(err) 117 | } 118 | 119 | if !router.Match(req, &match) { 120 | t.Error("Method 'POST' is not allowed for channel name 'default'") 121 | } 122 | 123 | // Testing Router with a DELETE Request and a proper formated channel name 124 | req, err = http.NewRequest("DELETE", "http://127.0.0.1/default", nil) 125 | if err != nil { 126 | t.Error(err) 127 | } 128 | 129 | if !router.Match(req, &match) { 130 | t.Error("Method 'DELETE' is not allowed for channel name 'default'") 131 | } 132 | 133 | // Testing Router with a PUT Request and a proper formated channel name 134 | req, err = http.NewRequest("PUT", "http://127.0.0.1/default", nil) 135 | if err != nil { 136 | t.Error(err) 137 | } 138 | 139 | if router.Match(req, &match) { 140 | t.Error("Method 'PUT' is not allowed for channel name 'default'") 141 | } 142 | 143 | // Testing Router with a GET Request and a wrong formated channel name 144 | req, err = http.NewRequest("GET", "http://127.0.0.1/DEFAULT", nil) 145 | if err != nil { 146 | t.Error(err) 147 | } 148 | 149 | if router.Match(req, &match) { 150 | t.Error("Method 'GET' on is not allowed wrong formated for channel name 'DEFAULT'") 151 | } 152 | 153 | // Testing Router for POST Request for wrong formated channel names 154 | req, err = http.NewRequest("POST", "http://127.0.0.1/DEFAULT", nil) 155 | if err != nil { 156 | t.Error(err) 157 | } 158 | 159 | if router.Match(req, &match) { 160 | t.Error("Method 'POST' is not allowed for wrong formated channel ' nameDEFAULT'") 161 | } 162 | 163 | // Testing Router for DELETE Request for wrong formated channel names 164 | req, err = http.NewRequest("DELETE", "http://127.0.0.1/DEFAULT", nil) 165 | if err != nil { 166 | t.Error(err) 167 | } 168 | 169 | if router.Match(req, &match) { 170 | t.Error("Method 'DELETE' is not allowed for wrong formated channel ' nameDEFAULT'") 171 | } 172 | } 173 | 174 | func TestConnection(t *testing.T) { 175 | es := setupEventSource(t, nil) 176 | defer es.closeEventSource() 177 | 178 | conn, resp := es.joinChannel(t, "default") 179 | defer conn.Close() 180 | 181 | if !strings.Contains(string(resp), "HTTP/1.1 200 OK\n") { 182 | t.Error("Response has no HTTP status") 183 | } 184 | 185 | if !strings.Contains(string(resp), "Content-Type: text/event-stream\n") { 186 | t.Error("Response header does not contain 'Content-Type: text/event-stream'") 187 | } 188 | 189 | if !strings.Contains(string(resp), "Cache-Control: no-cache\n") { 190 | t.Error("Response header does not contain 'Cache-Control: no-cache'") 191 | } 192 | 193 | if !strings.Contains(string(resp), "Connection: keep-alive\n") { 194 | t.Error("Response header does not contain 'Connection: keep-alive'") 195 | } 196 | 197 | if !strings.Contains(string(resp), "Access-Control-Allow-Origin: 127.0.0.1\n") { 198 | t.Error("Response header does not contain 'Access-Control-Allow-Origin: 127.0.0.1'") 199 | } 200 | 201 | if !strings.Contains(string(resp), "Access-Control-Allow-Method: GET\n") { 202 | t.Error("Response header does not contain 'Access-Control-Allow-Method: GET'") 203 | } 204 | } 205 | 206 | func TestAuthToken(t *testing.T) { 207 | es := setupEventSource(t, 208 | &Settings{ 209 | AuthToken: "secrect", 210 | }) 211 | defer es.closeEventSource() 212 | 213 | conn, _ := es.joinChannel(t, "default") 214 | defer conn.Close() 215 | 216 | req, err := http.NewRequest("DELETE", es.testServer.URL+"/default", nil) 217 | if err != nil { 218 | t.Error("Creating DELETE request failed with", err) 219 | } 220 | req.Header.Add("Auth-Token", "secrect") 221 | 222 | resp, err := http.DefaultClient.Do(req) 223 | if err != nil { 224 | t.Error("Unable to send DELETE request") 225 | } 226 | 227 | if resp.StatusCode != 200 { 228 | t.Error("DELETE request of channel failed with status code", resp.StatusCode) 229 | } 230 | } 231 | 232 | func TestSendMessage(t *testing.T) { 233 | es := setupEventSource(t, nil) 234 | defer es.closeEventSource() 235 | 236 | conn, _ := es.joinChannel(t, "default") 237 | defer conn.Close() 238 | 239 | // Test EventMessage in different modes 240 | for _, mode := range messageModes() { 241 | messageStream := buildMessageData(mode) 242 | var expectedMessage bytes.Buffer 243 | 244 | if mode != ModeNoid { 245 | expectedMessage.WriteString("id: 1\n") 246 | } 247 | 248 | if mode != ModeNoevent { 249 | expectedMessage.WriteString("event: foo\n") 250 | } 251 | 252 | if mode != ModeNodata { 253 | expectedMessage.WriteString("data: bar\n") 254 | } 255 | expectedMessage.WriteString("\n") 256 | 257 | es.eventSource.SendMessage(messageStream, "default") 258 | expectResponse(t, conn, string(expectedMessage.Bytes())) 259 | } 260 | } 261 | 262 | func TestSendMessageViaHTTPPost(t *testing.T) { 263 | es := setupEventSource(t, nil) 264 | defer es.closeEventSource() 265 | 266 | conn, _ := es.joinChannel(t, "default") 267 | defer conn.Close() 268 | 269 | // Test EventMessage in different modes 270 | for _, mode := range messageModes() { 271 | messageStream := buildMessageData(mode) 272 | var expectedMessage bytes.Buffer 273 | 274 | if mode != ModeNoid { 275 | expectedMessage.WriteString("id: 1\n") 276 | } 277 | 278 | if mode != ModeNoevent { 279 | expectedMessage.WriteString("event: foo\n") 280 | } 281 | 282 | if mode != ModeNodata { 283 | expectedMessage.WriteString("data: bar\n") 284 | } 285 | expectedMessage.WriteString("\n") 286 | 287 | resp, err := http.Post(es.testServer.URL+"/default", "application/json", messageStream) 288 | if err != nil { 289 | t.Error("POST event failed with", err) 290 | } 291 | 292 | if resp.StatusCode != 201 { 293 | t.Error("POST event failed with status code", resp.StatusCode) 294 | } 295 | 296 | expectResponse(t, conn, string(expectedMessage.Bytes())) 297 | } 298 | } 299 | 300 | func TestChannelExists(t *testing.T) { 301 | es := setupEventSource(t, nil) 302 | defer es.closeEventSource() 303 | 304 | conn, _ := es.joinChannel(t, "default") 305 | defer conn.Close() 306 | 307 | if !es.eventSource.ChannelExists("default") { 308 | t.Error("Channel 'default' should exist") 309 | } 310 | 311 | if es.eventSource.ChannelExists("my-channel") { 312 | t.Error("Channel 'my-channel' should not exist") 313 | } 314 | } 315 | 316 | func TestConsumerCount(t *testing.T) { 317 | es := setupEventSource(t, nil) 318 | defer es.closeEventSource() 319 | 320 | conn, _ := es.joinChannel(t, "default") 321 | defer conn.Close() 322 | 323 | if es.eventSource.ConsumerCount("default") > 1 { 324 | t.Error("ConsumerCount for channel 'default' is invalid") 325 | } 326 | } 327 | 328 | func TestConsumerCountAll(t *testing.T) { 329 | es := setupEventSource(t, nil) 330 | defer es.closeEventSource() 331 | 332 | conn, _ := es.joinChannel(t, "default") 333 | defer conn.Close() 334 | 335 | if es.eventSource.ConsumerCountAll() > 1 { 336 | t.Error("ConsumerCountAll is invalid") 337 | } 338 | } 339 | 340 | func TestChannels(t *testing.T) { 341 | es := setupEventSource(t, nil) 342 | defer es.closeEventSource() 343 | 344 | conn, _ := es.joinChannel(t, "default") 345 | defer conn.Close() 346 | 347 | if es.eventSource.Channels()[0] != "default" { 348 | t.Error("Returned channel names are invalid") 349 | } 350 | } 351 | 352 | func TestChannelClose(t *testing.T) { 353 | es := setupEventSource(t, nil) 354 | defer es.closeEventSource() 355 | 356 | conn, _ := es.joinChannel(t, "default") 357 | defer conn.Close() 358 | 359 | if !es.eventSource.ChannelExists("default") { 360 | t.Error("Channel 'default' should exist") 361 | } 362 | 363 | es.eventSource.Close("default") 364 | time.Sleep(100 * time.Millisecond) 365 | 366 | if es.eventSource.ChannelExists("default") { 367 | t.Error("Channel 'default' should not exist") 368 | } 369 | } 370 | 371 | func TestChannelCloseViaHTTPDelete(t *testing.T) { 372 | es := setupEventSource(t, nil) 373 | defer es.closeEventSource() 374 | 375 | conn, _ := es.joinChannel(t, "default") 376 | defer conn.Close() 377 | 378 | if !es.eventSource.ChannelExists("default") { 379 | t.Error("Channel 'default' should exist") 380 | } 381 | 382 | req, err := http.NewRequest("DELETE", es.testServer.URL+"/default", nil) 383 | if err != nil { 384 | t.Error("Creating DELETE request failed with", err) 385 | } 386 | 387 | resp, err := http.DefaultClient.Do(req) 388 | if err != nil { 389 | t.Error("Unable to send DELETE request") 390 | } 391 | 392 | if resp.StatusCode != 200 { 393 | t.Error("DELETE request of channel failed with status code", resp.StatusCode) 394 | } 395 | 396 | if len(es.eventSource.Channels()) != 0 { 397 | t.Error("Channel 'default' should be closed") 398 | } 399 | } 400 | 401 | func TestChannelCloseAll(t *testing.T) { 402 | es := setupEventSource(t, nil) 403 | defer es.closeEventSource() 404 | 405 | conn, _ := es.joinChannel(t, "default") 406 | defer conn.Close() 407 | 408 | if len(es.eventSource.Channels()) == 0 { 409 | t.Error("At least one channel should exist") 410 | } 411 | 412 | es.eventSource.CloseAll() 413 | time.Sleep(100 * time.Millisecond) 414 | 415 | if len(es.eventSource.Channels()) != 0 { 416 | t.Error("All channels should be closed") 417 | } 418 | } 419 | 420 | func TestChannelCloseAllViaHTTPDelete(t *testing.T) { 421 | es := setupEventSource(t, nil) 422 | defer es.closeEventSource() 423 | 424 | conn, _ := es.joinChannel(t, "default") 425 | defer conn.Close() 426 | 427 | if !es.eventSource.ChannelExists("default") { 428 | t.Error("Channel 'default' should exist") 429 | } 430 | 431 | req, err := http.NewRequest("DELETE", es.testServer.URL+"/all", nil) 432 | if err != nil { 433 | t.Error("Creating DELETE request failed with", err) 434 | } 435 | 436 | resp, err := http.DefaultClient.Do(req) 437 | if err != nil { 438 | t.Error("Unable to send DELETE request") 439 | } 440 | 441 | if resp.StatusCode != 200 { 442 | t.Error("DELETE request of all channels failed with status code", resp.StatusCode) 443 | } 444 | 445 | if len(es.eventSource.Channels()) != 0 { 446 | t.Error("All channels should be closed") 447 | } 448 | } 449 | 450 | func TestStats(t *testing.T) { 451 | es := setupEventSource(t, nil) 452 | defer es.closeEventSource() 453 | 454 | conn, _ := es.joinChannel(t, "default") 455 | defer conn.Close() 456 | 457 | // HEAD for single channel 458 | req, err := http.NewRequest("HEAD", es.testServer.URL+"/default", nil) 459 | if err != nil { 460 | t.Error("Creating HEAD request failed with", err) 461 | } 462 | req.Header.Add("Connection", "close") 463 | 464 | resp, err := http.DefaultClient.Do(req) 465 | if err != nil { 466 | t.Error("Unable to send HEAD request") 467 | } 468 | 469 | if statusCode := resp.StatusCode; statusCode != 200 { 470 | t.Error("HEAD request for channel failed with status code", statusCode) 471 | } 472 | 473 | consumerCountHeader := resp.Header.Get("X-Consumer-Count") 474 | consumerCount, err := strconv.Atoi(consumerCountHeader) 475 | if err != nil { 476 | t.Error("Unable to convert to integer", err) 477 | } 478 | 479 | if consumerCount != 1 { 480 | t.Error("Response for X-Consumer-Count is invalid", consumerCount) 481 | } 482 | 483 | channelExistsHeader := resp.Header.Get("X-Channel-Exists") 484 | channelExists, err := strconv.ParseBool(channelExistsHeader) 485 | if err != nil { 486 | t.Error("Unable to convert to bool", err) 487 | } 488 | 489 | if channelExists != true { 490 | t.Error("Response for X-Channel-Exists is invalid", channelExists) 491 | } 492 | 493 | // HEAD for all channels 494 | req, err = http.NewRequest("HEAD", es.testServer.URL+"/all", nil) 495 | if err != nil { 496 | t.Error("Creating HEAD request failed with", err) 497 | } 498 | req.Header.Add("Connection", "close") 499 | 500 | resp, err = http.DefaultClient.Do(req) 501 | if err != nil { 502 | t.Error("Unable to send HEAD request") 503 | } 504 | 505 | if statusCode := resp.StatusCode; statusCode != 200 { 506 | t.Error("HEAD request for channel failed with status code", statusCode) 507 | } 508 | 509 | consumerCountHeader = resp.Header.Get("X-Consumer-Count") 510 | consumerCount, err = strconv.Atoi(consumerCountHeader) 511 | if err != nil { 512 | t.Error("Unable to convert to integer", err) 513 | } 514 | 515 | if consumerCount != 1 { 516 | t.Error("Response for X-Consumer-Count is invalid", consumerCount) 517 | } 518 | 519 | if availableChannels := resp.Header.Get("X-Available-Channels"); availableChannels != "[default]" { 520 | t.Error("Response for X-Available-Channels is invalid", availableChannels) 521 | } 522 | } 523 | 524 | func TestRun(t *testing.T) { 525 | es := New(nil) 526 | go es.Run() 527 | } 528 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package eventsource 6 | 7 | import ( 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Default settings. 13 | const ( 14 | defaultTimeout = 2 * time.Second 15 | defaultAuthToken = "" 16 | defaultHost = "127.0.0.1" 17 | defaultPort = 8080 18 | defaultCorsAllowOrigin = "127.0.0.1" 19 | defaultCorsAllowMethod = "GET" 20 | ) 21 | 22 | // Settings stores all essential settings. 23 | type Settings struct { 24 | Timeout time.Duration 25 | AuthToken string 26 | Host string 27 | Port uint 28 | CorsAllowOrigin string 29 | CorsAllowMethod []string 30 | } 31 | 32 | // GetTimeout returns the timeout for consumers. 33 | func (s *Settings) GetTimeout() time.Duration { 34 | if s == nil || s.Timeout <= 0*time.Second { 35 | return defaultTimeout 36 | } 37 | return s.Timeout 38 | } 39 | 40 | // GetAuthToken returns the authenticatoin token. 41 | func (s *Settings) GetAuthToken() string { 42 | if s == nil || len(s.AuthToken) <= 0 { 43 | return defaultAuthToken 44 | } 45 | return strings.TrimSpace(s.AuthToken) 46 | } 47 | 48 | // GetHost returns the hostname/ip address on which the service should listen on. 49 | func (s *Settings) GetHost() string { 50 | if s == nil || s.Host == "" { 51 | return defaultHost 52 | } 53 | return s.Host 54 | } 55 | 56 | // GetPort returns the port on which the service should listen on. 57 | func (s *Settings) GetPort() uint { 58 | if s == nil || s.Port == 0 { 59 | return defaultPort 60 | } 61 | return s.Port 62 | } 63 | 64 | // GetCorsAllowOrigin returns the Access-Control-Allow-Origin. 65 | func (s *Settings) GetCorsAllowOrigin() string { 66 | if s == nil || s.CorsAllowOrigin == "" { 67 | return defaultCorsAllowOrigin 68 | } 69 | return s.CorsAllowOrigin 70 | } 71 | 72 | // GetCorsAllowMethod returns the Access-Control-Allow-Method. 73 | func (s *Settings) GetCorsAllowMethod() string { 74 | if s == nil || len(s.CorsAllowMethod) == 0 { 75 | return defaultCorsAllowMethod 76 | } 77 | return strings.Join(s.CorsAllowMethod, ", ") 78 | } 79 | -------------------------------------------------------------------------------- /settings_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Matthias Kalb, Railsmechanic. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package eventsource 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestDefaultSettings(t *testing.T) { 13 | ds := &Settings{} 14 | 15 | if timeout := ds.GetTimeout(); timeout != 2*time.Second { 16 | t.Error("Expected 2 seconds, got", timeout) 17 | } 18 | 19 | if authToken := ds.GetAuthToken(); authToken != "" { 20 | t.Error("Expected empty AuthToken, got ", authToken) 21 | } 22 | 23 | if host := ds.GetHost(); host != "127.0.0.1" { 24 | t.Error("Expected 127.0.0.1, got", host) 25 | } 26 | 27 | if port := ds.GetPort(); port != 8080 { 28 | t.Error("Expected 8080, got", port) 29 | } 30 | 31 | if corsAllowOrigin := ds.GetCorsAllowOrigin(); corsAllowOrigin != "127.0.0.1" { 32 | t.Error("Expected 127.0.0.1, got", corsAllowOrigin) 33 | } 34 | 35 | if corsAllowMethod := ds.GetCorsAllowMethod(); corsAllowMethod != "GET" { 36 | t.Error("Expected GET, got", corsAllowMethod) 37 | } 38 | } 39 | 40 | func TestCustomSettings(t *testing.T) { 41 | cs := &Settings{ 42 | Timeout: 3 * time.Second, 43 | AuthToken: "TOKEN", 44 | Host: "192.168.1.1", 45 | Port: 3000, 46 | CorsAllowOrigin: "*", 47 | CorsAllowMethod: []string{"GET", "POST", "DELETE"}, 48 | } 49 | 50 | if timeout := cs.GetTimeout(); timeout != 3*time.Second { 51 | t.Error("Expected 3 seconds, got", timeout) 52 | } 53 | 54 | if authToken := cs.GetAuthToken(); authToken != "TOKEN" { 55 | t.Error("AuthToken should be 'TOKEN', got ", authToken) 56 | } 57 | 58 | if host := cs.GetHost(); host != "192.168.1.1" { 59 | t.Error("Expected 192.168.1.1, got", host) 60 | } 61 | 62 | if port := cs.GetPort(); port != 3000 { 63 | t.Error("Expected 3000, got", port) 64 | } 65 | 66 | if corsAllowOrigin := cs.GetCorsAllowOrigin(); corsAllowOrigin != "*" { 67 | t.Error("Expected '*', got", corsAllowOrigin) 68 | } 69 | 70 | if corsAllowMethod := cs.GetCorsAllowMethod(); corsAllowMethod != "GET, POST, DELETE" { 71 | t.Error("Expected 'GET, POST, DELETE', got", corsAllowMethod) 72 | } 73 | } 74 | --------------------------------------------------------------------------------