├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── configuration.go ├── message.go ├── primus_js.go ├── signalbox.go ├── signalbox_test.go └── testdata └── test-config.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | *.DS_Store 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | 6 | install: 7 | - go get github.com/onsi/ginkgo 8 | - go get github.com/onsi/gomega 9 | - go get github.com/gorilla/websocket -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Clinton Freeman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | signalbox 2 | ========= 3 | 4 | An experimental Web-RTC signalling server written in Go. Designed to be compatible with the [signalling protocol](http://rtc.io/signalling-protocol.html#0) used with [rtc.io](http://rtc.io/). 5 | 6 | 7 | [![Build Status](http://img.shields.io/travis/cfreeman/signalbox.svg?style=flat)](https://travis-ci.org/cfreeman/signalbox)  8 | ![alpha](https://img.shields.io/badge/stability-alpha-orange.svg?style=flat "Alpha")  9 | ![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat "MIT License") 10 | 11 | ## Installation instructions (Ubuntu): 12 | 13 | * [Download and Install Go](http://golang.org/doc/install) 14 | 15 | * Get and compile Signalbox 16 | 17 | go get github.com/cfreeman/signalbox 18 | 19 | * Install on your server 20 | 21 | sudo cp bin/signalbox /usr/sbin/signalbox 22 | 23 | * Create signalbox configuration file 24 | 25 | vim /etc/signalbox.json 26 | { 27 | "ListenAddress":":3000" 28 | } 29 | 30 | * Create directory to hold signalbox logging output 31 | 32 | sudo mkdir /var/log/signalbox 33 | sudo chown /var/log/signalbox www-data 34 | sudo chgrp /var/log/signalbox www-data 35 | 36 | * Create upstart configuration file 37 | 38 | vim /etc/init/signalbox.conf 39 | description "A webRTC signalling server." 40 | 41 | start on filesystem or runlevel [2345] 42 | stop on runlevel [!2345] 43 | 44 | exec start-stop 45 | 46 | exec start-stop-daemon --start --chuid www-data --exec /usr/sbin/signalbox /etc/signalbox.json 2>> /var/log/signalbox/signalbox.log 47 | 48 | * Start signalbox 49 | 50 | sudo start signalbox 51 | 52 | * Signalbox is now running on port 3000. You can open it up, or proxy pass from apache or nginx. 53 | 54 | 55 | 56 | ## License: 57 | 58 | Copyright (c) 2014 Clinton Freeman 59 | 60 | Permission is hereby granted, free of charge, to any person obtaining a copy 61 | of this software and associated documentation files (the "Software"), to deal 62 | in the Software without restriction, including without limitation the rights 63 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 64 | copies of the Software, and to permit persons to whom the Software is 65 | furnished to do so, subject to the following conditions: 66 | 67 | The above copyright notice and this permission notice shall be included in all 68 | copies or substantial portions of the Software. 69 | 70 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 71 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 72 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 73 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 74 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 75 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 76 | SOFTWARE. 77 | 78 | -------------------------------------------------------------------------------- /configuration.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Clinton Freeman 2014 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, 6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or 11 | * substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "encoding/json" 24 | "os" 25 | "time" 26 | ) 27 | 28 | type Configuration struct { 29 | ListenAddress string 30 | SocketTimeout time.Duration 31 | } 32 | 33 | func parseConfiguration(configFile string) (configuration Configuration, err error) { 34 | config := Configuration{":3000", 300} 35 | 36 | // Open the configuration file. 37 | file, err := os.Open(configFile) 38 | if err != nil { 39 | return config, err 40 | } 41 | 42 | // Parse JSON in the configuration file. 43 | decoder := json.NewDecoder(file) 44 | err = decoder.Decode(&config) 45 | if err != nil { 46 | return config, err 47 | } 48 | 49 | return config, nil 50 | } 51 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Clinton Freeman 2014 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, 6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or 11 | * substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "encoding/json" 24 | "errors" 25 | "fmt" 26 | "github.com/gorilla/websocket" 27 | "log" 28 | "strings" 29 | "unicode/utf8" 30 | ) 31 | 32 | type messageFn func(message []string, 33 | sourceSocket *websocket.Conn, 34 | state SignalBox) (newState SignalBox, err error) 35 | 36 | func announce(message []string, 37 | sourceSocket *websocket.Conn, 38 | state SignalBox) (newState SignalBox, err error) { 39 | 40 | source, destination, err := ParsePeerAndRoom(message) 41 | if err != nil { 42 | return state, err 43 | } 44 | 45 | peer, exists := state.Peers[source.Id] 46 | if !exists { 47 | log.Printf("INFO - Adding Peer: %s\n", source.Id) 48 | state.Peers[source.Id] = new(Peer) 49 | state.Peers[source.Id].Id = source.Id 50 | state.Peers[source.Id].socket = sourceSocket // Inject a reference to the websocket within the new peer. 51 | peer = state.Peers[source.Id] 52 | } 53 | 54 | room, exists := state.Rooms[destination.Room] 55 | if !exists { 56 | log.Printf("INFO - Adding Room: %s\n", destination.Room) 57 | state.Rooms[destination.Room] = new(Room) 58 | state.Rooms[destination.Room].Room = destination.Room 59 | room = state.Rooms[destination.Room] 60 | } 61 | 62 | if state.PeerIsIn[peer.Id] == nil { 63 | state.PeerIsIn[peer.Id] = make(map[string]*Room) 64 | } 65 | state.PeerIsIn[peer.Id][room.Room] = room 66 | 67 | if state.RoomContains[room.Room] == nil { 68 | state.RoomContains[room.Room] = make(map[string]*Peer) 69 | } 70 | state.RoomContains[room.Room][peer.Id] = peer 71 | 72 | // Annouce the arrival to all the peers currently in the room. 73 | for _, p := range state.RoomContains[room.Room] { 74 | if p.Id != peer.Id && p.socket != nil { 75 | writeMessage(p.socket, message) 76 | } 77 | } 78 | 79 | // Report back to the announcer the number of peers in the room. 80 | members := fmt.Sprintf("{\"memberCount\":%d}", len(state.RoomContains[room.Room])) 81 | err = writeMessage(sourceSocket, []string{"/roominfo", members}) 82 | 83 | return state, nil 84 | } 85 | 86 | func leave(message []string, 87 | sourceSocket *websocket.Conn, 88 | state SignalBox) (newState SignalBox, err error) { 89 | 90 | source, destination, err := ParsePeerAndRoom(message) 91 | if err != nil { 92 | return state, err 93 | } 94 | 95 | peer, exists := state.Peers[source.Id] 96 | if !exists { 97 | return state, errors.New(fmt.Sprintf("Unable to leave, peer %s doesn't exist", source.Id)) 98 | } 99 | 100 | room, exists := state.Rooms[destination.Room] 101 | if !exists { 102 | return state, errors.New(fmt.Sprintf("Unable to leave, room %s doesn't exist", destination.Room)) 103 | } 104 | 105 | return removePeer(peer, room, message, state) 106 | } 107 | 108 | func closePeer(message []string, 109 | sourceSocket *websocket.Conn, 110 | state SignalBox) (newState SignalBox, err error) { 111 | 112 | source := findPeerBySocket(sourceSocket, state) 113 | if source == nil { 114 | return state, errors.New("Unable to close - no Peer matching socket.") 115 | } 116 | 117 | // Announce to everyone that the peer belonging to sourceSocket 118 | // has closed and bailed out of their rooms. 119 | for _, r := range state.PeerIsIn[source.Id] { 120 | for _, p := range state.RoomContains[r.Room] { 121 | if p.Id != source.Id && p.socket != nil { 122 | rm := fmt.Sprintf("{\"room\":\"%s\"}", r.Room) 123 | 124 | state, err = removePeer(source, r, []string{"/leave", source.Id, rm}, state) 125 | if err != nil { 126 | return state, err 127 | } 128 | } 129 | } 130 | } 131 | 132 | // Make sure the socket is closed from this end. 133 | err = sourceSocket.Close() 134 | 135 | return state, err 136 | } 137 | 138 | func removePeer(source *Peer, destination *Room, message []string, state SignalBox) (newState SignalBox, err error) { 139 | delete(state.PeerIsIn[source.Id], destination.Room) 140 | if len(state.PeerIsIn[source.Id]) == 0 { 141 | log.Printf("INFO - Removing Peer: %s\n", source.Id) 142 | delete(state.Peers, source.Id) 143 | delete(state.PeerIsIn, source.Id) 144 | } 145 | 146 | delete(state.RoomContains[destination.Room], source.Id) 147 | if len(state.RoomContains[destination.Room]) == 0 { 148 | log.Printf("INFO - Removing Room: %s\n", destination.Room) 149 | delete(state.Rooms, destination.Room) 150 | delete(state.RoomContains, destination.Room) 151 | } else { 152 | // Broadcast the departure to everyone else still in the room 153 | for _, p := range state.RoomContains[destination.Room] { 154 | if p.socket != nil && err == nil { 155 | err = writeMessage(p.socket, message) 156 | } 157 | } 158 | } 159 | 160 | return state, err 161 | } 162 | 163 | func to(message []string, 164 | sourceSocket *websocket.Conn, 165 | state SignalBox) (newState SignalBox, err error) { 166 | 167 | if len(message) < 3 { 168 | return state, errors.New("Not enouth parts for personalised 'to' message") 169 | } 170 | 171 | d, exists := state.Peers[message[1]] 172 | if !exists { 173 | return state, nil 174 | } 175 | 176 | if d.socket != nil { 177 | err = writeMessage(d.socket, message) 178 | } 179 | 180 | return state, err 181 | } 182 | 183 | func writeMessage(ws *websocket.Conn, message []string) error { 184 | b := strings.Join(message, "|") 185 | if ws != nil { 186 | log.Printf("INFO - Writing %s to %p", b, ws) 187 | return ws.WriteMessage(websocket.TextMessage, []byte(b)) 188 | } 189 | 190 | return nil 191 | } 192 | 193 | func custom(message []string, 194 | sourceSocket *websocket.Conn, 195 | state SignalBox) (newState SignalBox, err error) { 196 | 197 | if len(message) < 2 { 198 | return state, errors.New("Not enough parts to custom message") 199 | } 200 | 201 | source := Peer{message[1], nil} 202 | 203 | peer, exists := state.Peers[source.Id] 204 | if !exists { 205 | return state, nil 206 | } 207 | 208 | for _, r := range state.PeerIsIn[peer.Id] { 209 | for _, p := range state.RoomContains[r.Room] { 210 | if p.Id != peer.Id && p.socket != nil && err == nil { 211 | err = writeMessage(p.socket, message) 212 | } 213 | } 214 | } 215 | 216 | return state, err 217 | } 218 | 219 | func ignore(message []string, 220 | sourceSocket *websocket.Conn, 221 | state SignalBox) (newState SignalBox, err error) { 222 | return state, nil 223 | } 224 | 225 | func findPeerBySocket(sourceSocket *websocket.Conn, state SignalBox) *Peer { 226 | for _, p := range state.Peers { 227 | if p.socket == sourceSocket { 228 | return p 229 | } 230 | } 231 | 232 | return nil 233 | } 234 | 235 | func ParsePeerAndRoom(message []string) (source Peer, destination Room, err error) { 236 | if len(message) < 3 { 237 | return Peer{}, Room{}, errors.New("Not enough parts in the message body to parse peer and room.") 238 | } 239 | 240 | err = json.Unmarshal([]byte(message[2]), &destination) 241 | if err != nil { 242 | return Peer{}, Room{}, err 243 | } 244 | 245 | return Peer{message[1], nil}, destination, nil 246 | } 247 | 248 | func ParseMessage(message string) (action messageFn, messageBody []string, err error) { 249 | // All messages are text (utf-8 encoded at present) 250 | if !utf8.Valid([]byte(message)) { 251 | return nil, nil, errors.New("Message is not utf-8 encoded") 252 | } 253 | 254 | parts := strings.Split(message, "|") 255 | 256 | // rtc.io commands start with "/" - ignore everything else. 257 | if len(message) > 0 && message[0:1] == "/" { 258 | switch parts[0] { 259 | case "/announce": 260 | return announce, parts, nil 261 | 262 | case "/leave": 263 | return leave, parts, nil 264 | 265 | case "/to": 266 | return to, parts, nil 267 | 268 | case "/close": 269 | return closePeer, parts, nil 270 | 271 | default: 272 | return custom, parts, nil 273 | } 274 | } 275 | 276 | return ignore, parts, nil 277 | } 278 | -------------------------------------------------------------------------------- /primus_js.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Clinton Freeman 2014 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, 6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or 11 | * substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | package main 21 | 22 | const primus_content string = `(function (name, context, definition) { context[name] = definition.call(context); if (typeof module !== "undefined" && module.exports) { module.exports = context[name]; } else if (typeof define == "function" && define.amd) { define(function reference() { return context[name]; }); }})("Primus", this, function PRIMUS() {/*globals require, define */ 23 | 'use strict'; 24 | 25 | /** 26 | * Minimal EventEmitter interface that is molded against the Node.js 27 | * EventEmitter interface. 28 | * 29 | * @constructor 30 | * @api public 31 | */ 32 | function EventEmitter() { 33 | this._events = {}; 34 | } 35 | 36 | /** 37 | * Return a list of assigned event listeners. 38 | * 39 | * @param {String} event The events that should be listed. 40 | * @returns {Array} 41 | * @api public 42 | */ 43 | EventEmitter.prototype.listeners = function listeners(event) { 44 | return Array.apply(this, this._events[event] || []); 45 | }; 46 | 47 | /** 48 | * Emit an event to all registered event listeners. 49 | * 50 | * @param {String} event The name of the event. 51 | * @returns {Boolean} Indication if we've emitted an event. 52 | * @api public 53 | */ 54 | EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 55 | if (!this._events || !this._events[event]) return false; 56 | 57 | var listeners = this._events[event] 58 | , length = listeners.length 59 | , len = arguments.length 60 | , fn = listeners[0] 61 | , args 62 | , i; 63 | 64 | if (1 === length) { 65 | switch (len) { 66 | case 1: 67 | fn.call(fn.context || this); 68 | break; 69 | case 2: 70 | fn.call(fn.context || this, a1); 71 | break; 72 | case 3: 73 | fn.call(fn.context || this, a1, a2); 74 | break; 75 | case 4: 76 | fn.call(fn.context || this, a1, a2, a3); 77 | break; 78 | case 5: 79 | fn.call(fn.context || this, a1, a2, a3, a4); 80 | break; 81 | case 6: 82 | fn.call(fn.context || this, a1, a2, a3, a4, a5); 83 | break; 84 | 85 | default: 86 | for (i = 1, args = new Array(len -1); i < len; i++) { 87 | args[i - 1] = arguments[i]; 88 | } 89 | 90 | fn.apply(fn.context || this, args); 91 | } 92 | 93 | if (fn.once) this.removeListener(event, fn); 94 | } else { 95 | for (i = 1, args = new Array(len -1); i < len; i++) { 96 | args[i - 1] = arguments[i]; 97 | } 98 | 99 | for (i = 0; i < length; fn = listeners[++i]) { 100 | fn.apply(fn.context || this, args); 101 | if (fn.once) this.removeListener(event, fn); 102 | } 103 | } 104 | 105 | return true; 106 | }; 107 | 108 | /** 109 | * Register a new EventListener for the given event. 110 | * 111 | * @param {String} event Name of the event. 112 | * @param {Functon} fn Callback function. 113 | * @param {Mixed} context The context of the function. 114 | * @api public 115 | */ 116 | EventEmitter.prototype.on = function on(event, fn, context) { 117 | if (!this._events) this._events = {}; 118 | if (!this._events[event]) this._events[event] = []; 119 | 120 | fn.context = context; 121 | this._events[event].push(fn); 122 | 123 | return this; 124 | }; 125 | 126 | /** 127 | * Add an EventListener that's only called once. 128 | * 129 | * @param {String} event Name of the event. 130 | * @param {Function} fn Callback function. 131 | * @param {Mixed} context The context of the function. 132 | * @api public 133 | */ 134 | EventEmitter.prototype.once = function once(event, fn, context) { 135 | fn.once = true; 136 | return this.on(event, fn, context); 137 | }; 138 | 139 | /** 140 | * Remove event listeners. 141 | * 142 | * @param {String} event The event we want to remove. 143 | * @param {Function} fn The listener that we need to find. 144 | * @api public 145 | */ 146 | EventEmitter.prototype.removeListener = function removeListener(event, fn) { 147 | if (!this._events || !this._events[event]) return this; 148 | 149 | var listeners = this._events[event] 150 | , events = []; 151 | 152 | for (var i = 0, length = listeners.length; i < length; i++) { 153 | if (fn && listeners[i] !== fn && listeners[i].fn !== fn) { 154 | events.push(listeners[i]); 155 | } 156 | } 157 | 158 | // 159 | // Reset the array, or remove it completely if we have no more listeners. 160 | // 161 | if (events.length) this._events[event] = events; 162 | else this._events[event] = null; 163 | 164 | return this; 165 | }; 166 | 167 | /** 168 | * Remove all listeners or only the listeners for the specified event. 169 | * 170 | * @param {String} event The event want to remove all listeners for. 171 | * @api public 172 | */ 173 | EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { 174 | if (!this._events) return this; 175 | 176 | if (event) this._events[event] = null; 177 | else this._events = {}; 178 | 179 | return this; 180 | }; 181 | 182 | // 183 | // Alias methods names because people roll like that. 184 | // 185 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 186 | EventEmitter.prototype.addListener = EventEmitter.prototype.on; 187 | 188 | // 189 | // This function doesn't apply anymore. 190 | // 191 | EventEmitter.prototype.setMaxListeners = function setMaxListeners() { 192 | return this; 193 | }; 194 | 195 | /** 196 | * Context assertion, ensure that some of our public Primus methods are called 197 | * with the correct context to ensure that 198 | * 199 | * @param {Primus} self The context of the function. 200 | * @param {String} method The method name. 201 | * @api private 202 | */ 203 | function context(self, method) { 204 | if (self instanceof Primus) return; 205 | 206 | var failure = new Error('Primus#'+ method + '\'s context should called with a Primus instance'); 207 | 208 | if ('function' !== typeof self.listeners || !self.listeners('error').length) { 209 | throw failure; 210 | } 211 | 212 | self.emit('error', failure); 213 | } 214 | 215 | // 216 | // Sets the default connection URL, it uses the default origin of the browser 217 | // when supported but degrades for older browsers. In Node.js, we cannot guess 218 | // where the user wants to connect to, so we just default to localhost. 219 | // 220 | var defaultUrl; 221 | 222 | try { 223 | if (location.origin) { 224 | defaultUrl = location.origin; 225 | } else { 226 | defaultUrl = location.protocol +'//'+ location.hostname + (location.port ? ':'+ location.port : ''); 227 | } 228 | } catch (e) { 229 | defaultUrl = 'http://127.0.0.1'; 230 | } 231 | 232 | /** 233 | * Primus in a real-time library agnostic framework for establishing real-time 234 | * connections with servers. 235 | * 236 | * Options: 237 | * - reconnect, configuration for the reconnect process. 238 | * - manual, don't automatically call .open to start the connection. 239 | * - websockets, force the use of WebSockets, even when you should avoid them. 240 | * - timeout, connect timeout, server didn't respond in a timely manner. 241 | * - ping, The heartbeat interval for sending a ping packet to the server. 242 | * - pong, The heartbeat timeout for receiving a response to the ping. 243 | * - network, Use network events as leading method for network connection drops. 244 | * - strategy, Reconnection strategies. 245 | * - transport, Transport options. 246 | * - url, uri, The URL to use connect with the server. 247 | * 248 | * @constructor 249 | * @param {String} url The URL of your server. 250 | * @param {Object} options The configuration. 251 | * @api public 252 | */ 253 | function Primus(url, options) { 254 | if (!(this instanceof Primus)) return new Primus(url, options); 255 | if ('function' !== typeof this.client) { 256 | var message = 'The client library has not been compiled correctly, ' + 257 | 'see https://github.com/primus/primus#client-library for more details'; 258 | return this.critical(new Error(message)); 259 | } 260 | 261 | if ('object' === typeof url) { 262 | options = url; 263 | url = options.url || options.uri || defaultUrl; 264 | } else { 265 | options = options || {}; 266 | } 267 | 268 | var primus = this; 269 | 270 | // Connection timeout duration. 271 | options.timeout = 'timeout' in options ? options.timeout : 10e3; 272 | 273 | // Stores the back off configuration. 274 | options.reconnect = 'reconnect' in options ? options.reconnect : {}; 275 | 276 | // Heartbeat ping interval. 277 | options.ping = 'ping' in options ? options.ping : 25000; 278 | 279 | // Heartbeat pong response timeout. 280 | options.pong = 'pong' in options ? options.pong : 10e3; 281 | 282 | // Reconnect strategies. 283 | options.strategy = 'strategy' in options ? options.strategy : []; 284 | 285 | // Custom transport options. 286 | options.transport = 'transport' in options ? options.transport : {}; 287 | 288 | primus.buffer = []; // Stores premature send data. 289 | primus.writable = true; // Silly stream compatibility. 290 | primus.readable = true; // Silly stream compatibility. 291 | primus.url = primus.parse(url || defaultUrl); // Parse the URL to a readable format. 292 | primus.readyState = Primus.CLOSED; // The readyState of the connection. 293 | primus.options = options; // Reference to the supplied options. 294 | primus.timers = {}; // Contains all our timers. 295 | primus.attempt = null; // Current back off attempt. 296 | primus.socket = null; // Reference to the internal connection. 297 | primus.latency = 0; // Latency between messages. 298 | primus.transport = options.transport; // Transport options. 299 | primus.transformers = { // Message transformers. 300 | outgoing: [], 301 | incoming: [] 302 | }; 303 | 304 | // 305 | // Parse the reconnection strategy. It can have the following strategies: 306 | // 307 | // - timeout: Reconnect when we have a network timeout. 308 | // - disconnect: Reconnect when we have an unexpected disconnect. 309 | // - online: Reconnect when we're back online. 310 | // 311 | if ('string' === typeof options.strategy) { 312 | options.strategy = options.strategy.split(/\s?\,\s?/g); 313 | } 314 | 315 | if (false === options.strategy) { 316 | // 317 | // Strategies are disabled, but we still need an empty array to join it in 318 | // to nothing. 319 | // 320 | options.strategy = []; 321 | } else if (!options.strategy.length) { 322 | options.strategy.push('disconnect', 'online'); 323 | 324 | // 325 | // Timeout based reconnection should only be enabled conditionally. When 326 | // authorization is enabled it could trigger. 327 | // 328 | if (!this.authorization) options.strategy.push('timeout'); 329 | } 330 | 331 | options.strategy = options.strategy.join(',').toLowerCase(); 332 | 333 | // 334 | // Only initialise the EventEmitter interface if we're running in a plain 335 | // browser environment. The Stream interface is inherited differently when it 336 | // runs on browserify and on Node.js. 337 | // 338 | if (!Stream) EventEmitter.call(primus); 339 | 340 | // 341 | // Force the use of WebSockets, even when we've detected some potential 342 | // broken WebSocket implementation. 343 | // 344 | if ('websockets' in options) { 345 | primus.AVOID_WEBSOCKETS = !options.websockets; 346 | } 347 | 348 | // 349 | // Force or disable the use of NETWORK events as leading client side 350 | // disconnection detection. 351 | // 352 | if ('network' in options) { 353 | primus.NETWORK_EVENTS = options.network; 354 | } 355 | 356 | // 357 | // Check if the user wants to manually initialise a connection. If they don't, 358 | // we want to do it after a really small timeout so we give the users enough 359 | // time to listen for error events etc. 360 | // 361 | if (!options.manual) primus.timers.open = setTimeout(function open() { 362 | primus.clearTimeout('open').open(); 363 | }, 0); 364 | 365 | primus.initialise(options); 366 | } 367 | 368 | /** 369 | * Simple require wrapper to make browserify, node and require.js play nice. 370 | * 371 | * @param {String} name The module to require. 372 | * @api private 373 | */ 374 | Primus.require = function requires(name) { 375 | if ('function' !== typeof require) return undefined; 376 | 377 | return !('function' === typeof define && define.amd) 378 | ? require(name) 379 | : undefined; 380 | }; 381 | 382 | // 383 | // It's possible that we're running in Node.js or in a Node.js compatible 384 | // environment such as browserify. In these cases we want to use some build in 385 | // libraries to minimize our dependence on the DOM. 386 | // 387 | var Stream, parse; 388 | 389 | try { 390 | Primus.Stream = Stream = Primus.require('stream'); 391 | parse = Primus.require('url').parse; 392 | 393 | // 394 | // Normally inheritance is done in the same way as we do in our catch 395 | // statement. But due to changes to the EventEmitter interface in Node 0.10 396 | // this will trigger annoying memory leak warnings and other potential issues 397 | // outlined in the issue linked below. 398 | // 399 | // @see https://github.com/joyent/node/issues/4971 400 | // 401 | Primus.require('util').inherits(Primus, Stream); 402 | } catch (e) { 403 | Primus.Stream = EventEmitter; 404 | Primus.prototype = new EventEmitter(); 405 | 406 | // 407 | // In the browsers we can leverage the DOM to parse the URL for us. It will 408 | // automatically default to host of the current server when we supply it path 409 | // etc. 410 | // 411 | parse = function parse(url) { 412 | var a = document.createElement('a') 413 | , data = {} 414 | , key; 415 | 416 | a.href = url; 417 | 418 | // 419 | // Transform it from a readOnly object to a read/writable object so we can 420 | // change some parsed values. This is required if we ever want to override 421 | // a port number etc. (as browsers remove port 443 and 80 from the URL's). 422 | // 423 | for (key in a) { 424 | if ('string' === typeof a[key] || 'number' === typeof a[key]) { 425 | data[key] = a[key]; 426 | } 427 | } 428 | 429 | // 430 | // If we don't obtain a port number (e.g. when using zombie) then try 431 | // and guess at a value from the 'href' value 432 | // 433 | if (!data.port) { 434 | if (!data.href) data.href = ''; 435 | if ((data.href.match(/\:/g) || []).length > 1) { 436 | data.port = data.href.split(':')[2].split('/')[0]; 437 | } else { 438 | data.port = ('https' === data.href.substr(0, 5)) ? 443 : 80; 439 | } 440 | } 441 | 442 | // 443 | // Browsers do not parse authorization information, so we need to extract 444 | // that from the URL. 445 | // 446 | if (~data.href.indexOf('@') && !data.auth) { 447 | var start = data.protocol.length + 2; 448 | data.auth = data.href.slice(start, data.href.indexOf(data.pathname, start)).split('@')[0]; 449 | } 450 | 451 | return data; 452 | }; 453 | } 454 | 455 | /** 456 | * Primus readyStates, used internally to set the correct ready state. 457 | * 458 | * @type {Number} 459 | * @private 460 | */ 461 | Primus.OPENING = 1; // We're opening the connection. 462 | Primus.CLOSED = 2; // No active connection. 463 | Primus.OPEN = 3; // The connection is open. 464 | 465 | /** 466 | * Are we working with a potentially broken WebSockets implementation? This 467 | * boolean can be used by transformers to remove WebSockets from their 468 | * supported transports. 469 | * 470 | * @type {Boolean} 471 | * @api private 472 | */ 473 | Primus.prototype.AVOID_WEBSOCKETS = false; 474 | 475 | /** 476 | * Some browsers support registering emitting online and offline events when 477 | * the connection has been dropped on the client. We're going to detect it in 478 | * a simple try {} catch (e) {} statement so we don't have to do complicated 479 | * feature detection. 480 | * 481 | * @type {Boolean} 482 | * @api private 483 | */ 484 | Primus.prototype.NETWORK_EVENTS = false; 485 | Primus.prototype.online = true; 486 | 487 | try { 488 | if ( 489 | Primus.prototype.NETWORK_EVENTS = 'onLine' in navigator 490 | && (window.addEventListener || document.body.attachEvent) 491 | ) { 492 | if (!navigator.onLine) { 493 | Primus.prototype.online = false; 494 | } 495 | } 496 | } catch (e) { } 497 | 498 | /** 499 | * The Ark contains all our plugins definitions. It's namespaced by 500 | * name => plugin. 501 | * 502 | * @type {Object} 503 | * @private 504 | */ 505 | Primus.prototype.ark = {}; 506 | 507 | /** 508 | * Return the given plugin. 509 | * 510 | * @param {String} name The name of the plugin. 511 | * @returns {Mixed} 512 | * @api public 513 | */ 514 | Primus.prototype.plugin = function plugin(name) { 515 | context(this, 'plugin'); 516 | 517 | if (name) return this.ark[name]; 518 | 519 | var plugins = {}; 520 | 521 | for (name in this.ark) { 522 | plugins[name] = this.ark[name]; 523 | } 524 | 525 | return plugins; 526 | }; 527 | 528 | /** 529 | * Checks if the given event is an emitted event by Primus. 530 | * 531 | * @param {String} evt The event name. 532 | * @returns {Boolean} 533 | * @api public 534 | */ 535 | Primus.prototype.reserved = function reserved(evt) { 536 | return (/^(incoming|outgoing)::/).test(evt) 537 | || evt in this.reserved.events; 538 | }; 539 | 540 | /** 541 | * The actual events that are used by the client. 542 | * 543 | * @type {Object} 544 | * @api public 545 | */ 546 | Primus.prototype.reserved.events = { 547 | readyStateChange: 1, 548 | reconnecting: 1, 549 | reconnect: 1, 550 | offline: 1, 551 | timeout: 1, 552 | online: 1, 553 | error: 1, 554 | close: 1, 555 | open: 1, 556 | data: 1, 557 | end: 1 558 | }; 559 | 560 | /** 561 | * Initialise the Primus and setup all parsers and internal listeners. 562 | * 563 | * @param {Object} options The original options object. 564 | * @api private 565 | */ 566 | Primus.prototype.initialise = function initialise(options) { 567 | var primus = this 568 | , start; 569 | 570 | primus.on('outgoing::open', function opening() { 571 | var readyState = primus.readyState; 572 | 573 | primus.readyState = Primus.OPENING; 574 | if (readyState !== Primus.OPENING) { 575 | primus.emit('readyStateChange'); 576 | } 577 | 578 | start = +new Date(); 579 | }); 580 | 581 | primus.on('incoming::open', function opened() { 582 | if (primus.attempt) primus.attempt = null; 583 | 584 | var readyState = primus.readyState; 585 | 586 | primus.readyState = Primus.OPEN; 587 | if (readyState !== Primus.OPEN) { 588 | primus.emit('readyStateChange'); 589 | } 590 | 591 | primus.emit('open'); 592 | primus.clearTimeout('ping', 'pong').heartbeat(); 593 | 594 | if (primus.buffer.length) { 595 | for (var i = 0, length = primus.buffer.length; i < length; i++) { 596 | primus.write(primus.buffer[i]); 597 | } 598 | 599 | primus.buffer.length = 0; 600 | } 601 | 602 | primus.latency = +new Date() - start; 603 | }); 604 | 605 | primus.on('incoming::pong', function pong(time) { 606 | primus.online = true; 607 | primus.clearTimeout('pong').heartbeat(); 608 | 609 | primus.latency = (+new Date()) - time; 610 | }); 611 | 612 | primus.on('incoming::error', function error(e) { 613 | var connect = primus.timers.connect; 614 | 615 | // 616 | // We're still doing a reconnect attempt, it could be that we failed to 617 | // connect because the server was down. Failing connect attempts should 618 | // always emit an error event instead of a open event. 619 | // 620 | if (primus.attempt) return primus.reconnect(); 621 | if (primus.listeners('error').length) primus.emit('error', e); 622 | 623 | // 624 | // We received an error while connecting, this most likely the result of an 625 | // unauthorized access to the server. But this something that is only 626 | // triggered for Node based connections. Browsers trigger the error event. 627 | // 628 | if (connect) { 629 | if (~primus.options.strategy.indexOf('timeout')) primus.reconnect(); 630 | else primus.end(); 631 | } 632 | }); 633 | 634 | primus.on('incoming::data', function message(raw) { 635 | primus.decoder(raw, function decoding(err, data) { 636 | // 637 | // Do a "save" emit('error') when we fail to parse a message. We don't 638 | // want to throw here as listening to errors should be optional. 639 | // 640 | if (err) return primus.listeners('error').length && primus.emit('error', err); 641 | 642 | // 643 | // Handle all "primus::" prefixed protocol messages. 644 | // 645 | if (primus.protocol(data)) return; 646 | 647 | for (var i = 0, length = primus.transformers.incoming.length; i < length; i++) { 648 | var packet = { data: data }; 649 | 650 | if (false === primus.transformers.incoming[i].call(primus, packet)) { 651 | // 652 | // When false is returned by an incoming transformer it means that's 653 | // being handled by the transformer and we should not emit the data 654 | // event. 655 | // 656 | return; 657 | } 658 | 659 | data = packet.data; 660 | } 661 | 662 | // 663 | // We always emit 2 arguments for the data event, the first argument is the 664 | // parsed data and the second argument is the raw string that we received. 665 | // This allows you to do some validation on the parsed data and then save 666 | // the raw string in your database or what ever so you don't have the 667 | // stringify overhead. 668 | // 669 | primus.emit('data', data, raw); 670 | }); 671 | }); 672 | 673 | primus.on('incoming::end', function end() { 674 | var readyState = primus.readyState; 675 | 676 | // 677 | // Always set the readyState to closed, and if we're still connecting, close 678 | // the connection so we're sure that everything after this if statement block 679 | // is only executed because our readyState is set to open. 680 | // 681 | primus.readyState = Primus.CLOSED; 682 | if (readyState !== Primus.CLOSED) { 683 | primus.emit('readyStateChange'); 684 | } 685 | 686 | if (primus.timers.connect) primus.end(); 687 | if (readyState !== Primus.OPEN) return; 688 | 689 | // 690 | // Clear all timers in case we're not going to reconnect. 691 | // 692 | for (var timeout in this.timers) { 693 | this.clearTimeout(timeout); 694 | } 695 | 696 | // 697 | // Fire the close event as an indication of connection disruption. 698 | // This is also fired by primus#end so it is emitted in all cases. 699 | // 700 | primus.emit('close'); 701 | 702 | // 703 | // The disconnect was unintentional, probably because the server shut down. 704 | // So we should just start a reconnect procedure. 705 | // 706 | if (~primus.options.strategy.indexOf('disconnect')) primus.reconnect(); 707 | }); 708 | 709 | // 710 | // Setup the real-time client. 711 | // 712 | primus.client(); 713 | 714 | // 715 | // Process the potential plugins. 716 | // 717 | for (var plugin in primus.ark) { 718 | primus.ark[plugin].call(primus, primus, options); 719 | } 720 | 721 | // 722 | // NOTE: The following code is only required if we're supporting network 723 | // events as it requires access to browser globals. 724 | // 725 | if (!primus.NETWORK_EVENTS) return primus; 726 | 727 | /** 728 | * Handler for offline notifications. 729 | * 730 | * @api private 731 | */ 732 | function offline() { 733 | if (!primus.online) return; // Already or still offline, bailout. 734 | 735 | primus.online = false; 736 | primus.emit('offline'); 737 | primus.end(); 738 | } 739 | 740 | /** 741 | * Handler for online notifications. 742 | * 743 | * @api private 744 | */ 745 | function online() { 746 | if (primus.online) return; // Already or still online, bailout 747 | 748 | primus.online = true; 749 | primus.emit('online'); 750 | 751 | if (~primus.options.strategy.indexOf('online')) primus.reconnect(); 752 | } 753 | 754 | if (window.addEventListener) { 755 | window.addEventListener('offline', offline, false); 756 | window.addEventListener('online', online, false); 757 | } else if (document.body.attachEvent){ 758 | document.body.attachEvent('onoffline', offline); 759 | document.body.attachEvent('ononline', online); 760 | } 761 | 762 | return primus; 763 | }; 764 | 765 | /** 766 | * Really dead simple protocol parser. We simply assume that every message that 767 | * is prefixed with primus:: could be used as some sort of protocol definition 768 | * for Primus. 769 | * 770 | * @param {String} msg The data. 771 | * @returns {Boolean} Is a protocol message. 772 | * @api private 773 | */ 774 | Primus.prototype.protocol = function protocol(msg) { 775 | if ( 776 | 'string' !== typeof msg 777 | || msg.indexOf('primus::') !== 0 778 | ) return false; 779 | 780 | var last = msg.indexOf(':', 8) 781 | , value = msg.slice(last + 2); 782 | 783 | switch (msg.slice(8, last)) { 784 | case 'pong': 785 | this.emit('incoming::pong', value); 786 | break; 787 | 788 | case 'server': 789 | // 790 | // The server is closing the connection, forcefully disconnect so we don't 791 | // reconnect again. 792 | // 793 | if ('close' === value) this.end(); 794 | break; 795 | 796 | case 'id': 797 | this.emit('incoming::id', value); 798 | break; 799 | 800 | // 801 | // Unknown protocol, somebody is probably sending primus:: prefixed 802 | // messages. 803 | // 804 | default: 805 | return false; 806 | } 807 | 808 | return true; 809 | }; 810 | 811 | /** 812 | * Retrieve the current id from the server. 813 | * 814 | * @param {Function} fn Callback function. 815 | * @api public 816 | */ 817 | Primus.prototype.id = function id(fn) { 818 | if (this.socket && this.socket.id) return fn(this.socket.id); 819 | 820 | this.write('primus::id::'); 821 | return this.once('incoming::id', fn); 822 | }; 823 | 824 | /** 825 | * Establish a connection with the server. When this function is called we 826 | * assume that we don't have any open connections. If you do call it when you 827 | * have a connection open, it could cause duplicate connections. 828 | * 829 | * @api public 830 | */ 831 | Primus.prototype.open = function open() { 832 | context(this, 'open'); 833 | 834 | // 835 | // Only start a connection timeout procedure if we're not reconnecting as 836 | // that shouldn't count as an initial connection. This should be started 837 | // before the connection is opened to capture failing connections and kill the 838 | // timeout. 839 | // 840 | if (!this.attempt && this.options.timeout) this.timeout(); 841 | 842 | return this.emit('outgoing::open'); 843 | }; 844 | 845 | /** 846 | * Send a new message. 847 | * 848 | * @param {Mixed} data The data that needs to be written. 849 | * @returns {Boolean} Always returns true. 850 | * @api public 851 | */ 852 | Primus.prototype.write = function write(data) { 853 | var primus = this 854 | , packet; 855 | 856 | context(primus, 'write'); 857 | 858 | if (Primus.OPEN === primus.readyState) { 859 | for (var i = 0, length = primus.transformers.outgoing.length; i < length; i++) { 860 | packet = { data: data }; 861 | 862 | if (false === primus.transformers.outgoing[i].call(primus, packet)) { 863 | // 864 | // When false is returned by an incoming transformer it means that's 865 | // being handled by the transformer and we should not emit the data 866 | // event. 867 | // 868 | return; 869 | } 870 | 871 | data = packet.data; 872 | } 873 | 874 | primus.encoder(data, function encoded(err, packet) { 875 | // 876 | // Do a "save" emit('error') when we fail to parse a message. We don't 877 | // want to throw here as listening to errors should be optional. 878 | // 879 | if (err) return primus.listeners('error').length && primus.emit('error', err); 880 | primus.emit('outgoing::data', packet); 881 | }); 882 | } else { 883 | primus.buffer.push(data); 884 | } 885 | 886 | return true; 887 | }; 888 | 889 | /** 890 | * Send a new heartbeat over the connection to ensure that we're still 891 | * connected and our internet connection didn't drop. We cannot use server side 892 | * heartbeats for this unfortunately. 893 | * 894 | * @api private 895 | */ 896 | Primus.prototype.heartbeat = function heartbeat() { 897 | var primus = this; 898 | 899 | if (!primus.options.ping) return primus; 900 | 901 | /** 902 | * Exterminate the connection as we've timed out. 903 | * 904 | * @api private 905 | */ 906 | function pong() { 907 | primus.clearTimeout('pong'); 908 | 909 | // 910 | // The network events already captured the offline event. 911 | // 912 | if (!primus.online) return; 913 | 914 | primus.online = false; 915 | primus.emit('offline'); 916 | primus.emit('incoming::end'); 917 | } 918 | 919 | /** 920 | * We should send a ping message to the server. 921 | * 922 | * @api private 923 | */ 924 | function ping() { 925 | primus.clearTimeout('ping').write('primus::ping::'+ (+new Date)); 926 | primus.emit('outgoing::ping'); 927 | primus.timers.pong = setTimeout(pong, primus.options.pong); 928 | } 929 | 930 | primus.timers.ping = setTimeout(ping, primus.options.ping); 931 | }; 932 | 933 | /** 934 | * Start a connection timeout. 935 | * 936 | * @api private 937 | */ 938 | Primus.prototype.timeout = function timeout() { 939 | var primus = this; 940 | 941 | /** 942 | * Remove all references to the timeout listener as we've received an event 943 | * that can be used to determine state. 944 | * 945 | * @api private 946 | */ 947 | function remove() { 948 | primus.removeListener('error', remove) 949 | .removeListener('open', remove) 950 | .removeListener('end', remove) 951 | .clearTimeout('connect'); 952 | } 953 | 954 | primus.timers.connect = setTimeout(function setTimeout() { 955 | remove(); // Clean up old references. 956 | 957 | if (Primus.readyState === Primus.OPEN || primus.attempt) return; 958 | 959 | primus.emit('timeout'); 960 | 961 | // 962 | // We failed to connect to the server. 963 | // 964 | if (~primus.options.strategy.indexOf('timeout')) primus.reconnect(); 965 | else primus.end(); 966 | }, primus.options.timeout); 967 | 968 | return primus.on('error', remove) 969 | .on('open', remove) 970 | .on('end', remove); 971 | }; 972 | 973 | /** 974 | * Properly clean up all setTimeout references. 975 | * 976 | * @param {String} ..args.. The names of the timeout's we need clear. 977 | * @api private 978 | */ 979 | Primus.prototype.clearTimeout = function clear() { 980 | for (var args = arguments, i = 0, l = args.length; i < l; i++) { 981 | if (this.timers[args[i]]) clearTimeout(this.timers[args[i]]); 982 | delete this.timers[args[i]]; 983 | } 984 | 985 | return this; 986 | }; 987 | 988 | /** 989 | * Exponential back off algorithm for retry operations. It uses an randomized 990 | * retry so we don't DDOS our server when it goes down under pressure. 991 | * 992 | * @param {Function} callback Callback to be called after the timeout. 993 | * @param {Object} opts Options for configuring the timeout. 994 | * @api private 995 | */ 996 | Primus.prototype.backoff = function backoff(callback, opts) { 997 | opts = opts || {}; 998 | 999 | var primus = this; 1000 | 1001 | // 1002 | // Bailout when we already have a backoff process running. We shouldn't call 1003 | // the callback then as it might cause an unexpected end event as another 1004 | // reconnect process is already running. 1005 | // 1006 | if (opts.backoff) return primus; 1007 | 1008 | opts.maxDelay = 'maxDelay' in opts ? opts.maxDelay : Infinity; // Maximum delay. 1009 | opts.minDelay = 'minDelay' in opts ? opts.minDelay : 500; // Minimum delay. 1010 | opts.retries = 'retries' in opts ? opts.retries : 10; // Allowed retries. 1011 | opts.attempt = (+opts.attempt || 0) + 1; // Current attempt. 1012 | opts.factor = 'factor' in opts ? opts.factor : 2; // Back off factor. 1013 | 1014 | // 1015 | // Bailout if we are about to make to much attempts. Please note that we use 1016 | // > because we already incremented the value above. 1017 | // 1018 | if (opts.attempt > opts.retries) { 1019 | callback(new Error('Unable to retry'), opts); 1020 | return primus; 1021 | } 1022 | 1023 | // 1024 | // Prevent duplicate back off attempts using the same options object. 1025 | // 1026 | opts.backoff = true; 1027 | 1028 | // 1029 | // Calculate the timeout, but make it randomly so we don't retry connections 1030 | // at the same interval and defeat the purpose. This exponential back off is 1031 | // based on the work of: 1032 | // 1033 | // http://dthain.blogspot.nl/2009/02/exponential-backoff-in-distributed.html 1034 | // 1035 | opts.timeout = opts.attempt !== 1 1036 | ? Math.min(Math.round( 1037 | (Math.random() + 1) * opts.minDelay * Math.pow(opts.factor, opts.attempt) 1038 | ), opts.maxDelay) 1039 | : opts.minDelay; 1040 | 1041 | // 1042 | // Emit a reconnecting event with current reconnect options. This allows 1043 | // them to update the UI and provide their users with feedback. 1044 | // 1045 | primus.emit('reconnecting', opts); 1046 | 1047 | primus.timers.reconnect = setTimeout(function delay() { 1048 | opts.backoff = false; 1049 | primus.clearTimeout('reconnect'); 1050 | 1051 | callback(undefined, opts); 1052 | }, opts.timeout); 1053 | 1054 | return primus; 1055 | }; 1056 | 1057 | /** 1058 | * Start a new reconnect procedure. 1059 | * 1060 | * @api private 1061 | */ 1062 | Primus.prototype.reconnect = function reconnect() { 1063 | var primus = this; 1064 | 1065 | // 1066 | // Try to re-use the existing attempt. 1067 | // 1068 | primus.attempt = primus.attempt || primus.clone(primus.options.reconnect); 1069 | 1070 | primus.backoff(function attempt(fail, backoff) { 1071 | if (fail) { 1072 | primus.attempt = null; 1073 | return primus.emit('end'); 1074 | } 1075 | 1076 | // 1077 | // Try to re-open the connection again. 1078 | // 1079 | primus.emit('reconnect', backoff); 1080 | primus.emit('outgoing::reconnect'); 1081 | }, primus.attempt); 1082 | 1083 | return primus; 1084 | }; 1085 | 1086 | /** 1087 | * Close the connection. 1088 | * 1089 | * @param {Mixed} data last packet of data. 1090 | * @api public 1091 | */ 1092 | Primus.prototype.end = function end(data) { 1093 | context(this, 'end'); 1094 | 1095 | if (this.readyState === Primus.CLOSED && !this.timers.connect) return this; 1096 | if (data) this.write(data); 1097 | 1098 | this.writable = false; 1099 | 1100 | var readyState = this.readyState; 1101 | this.readyState = Primus.CLOSED; 1102 | if (readyState !== Primus.CLOSED) { 1103 | this.emit('readyStateChange'); 1104 | } 1105 | 1106 | for (var timeout in this.timers) { 1107 | this.clearTimeout(timeout); 1108 | } 1109 | 1110 | this.emit('outgoing::end'); 1111 | this.emit('close'); 1112 | this.emit('end'); 1113 | 1114 | return this; 1115 | }; 1116 | 1117 | /** 1118 | * Create a shallow clone of a given object. 1119 | * 1120 | * @param {Object} obj The object that needs to be cloned. 1121 | * @returns {Object} Copy. 1122 | * @api private 1123 | */ 1124 | Primus.prototype.clone = function clone(obj) { 1125 | return this.merge({}, obj); 1126 | }; 1127 | 1128 | /** 1129 | * Merge different objects in to one target object. 1130 | * 1131 | * @param {Object} target The object where everything should be merged in. 1132 | * @returns {Object} Original target with all merged objects. 1133 | * @api private 1134 | */ 1135 | Primus.prototype.merge = function merge(target) { 1136 | var args = Array.prototype.slice.call(arguments, 1); 1137 | 1138 | for (var i = 0, l = args.length, key, obj; i < l; i++) { 1139 | obj = args[i]; 1140 | 1141 | for (key in obj) { 1142 | if (obj.hasOwnProperty(key)) target[key] = obj[key]; 1143 | } 1144 | } 1145 | 1146 | return target; 1147 | }; 1148 | 1149 | /** 1150 | * Parse the connection string. 1151 | * 1152 | * @param {String} url Connection URL. 1153 | * @returns {Object} Parsed connection. 1154 | * @api private 1155 | */ 1156 | Primus.prototype.parse = parse; 1157 | 1158 | /** 1159 | * Parse a query string. 1160 | * 1161 | * @param {String} query The query string that needs to be parsed. 1162 | * @returns {Object} Parsed query string. 1163 | * @api private 1164 | */ 1165 | Primus.prototype.querystring = function querystring(query) { 1166 | var parser = /([^=?&]+)=([^&]*)/g 1167 | , result = {} 1168 | , part; 1169 | 1170 | // 1171 | // Little nifty parsing hack, leverage the fact that RegExp.exec increments 1172 | // the lastIndex property so we can continue executing this loop until we've 1173 | // parsed all results. 1174 | // 1175 | for (; part = parser.exec(query); result[part[1]] = part[2]); 1176 | 1177 | return result; 1178 | }; 1179 | 1180 | /** 1181 | * Generates a connection URI. 1182 | * 1183 | * @param {String} protocol The protocol that should used to crate the URI. 1184 | * @param {Boolean} querystring Do we need to include a query string. 1185 | * @returns {String|options} The URL. 1186 | * @api private 1187 | */ 1188 | Primus.prototype.uri = function uri(options, querystring) { 1189 | var url = this.url 1190 | , server = []; 1191 | 1192 | // 1193 | // Backwards compatible with Primus 1.4.0 1194 | // @TODO Remove me for Primus 2.0 1195 | // 1196 | if ('string' === typeof options) { 1197 | options = { protocol: options }; 1198 | if (querystring) options.query = querystring; 1199 | } 1200 | 1201 | options = options || {}; 1202 | options.protocol = 'protocol' in options ? options.protocol : 'http'; 1203 | options.query = url.search && 'query' in options ? (url.search.charAt(0) === '?' ? url.search.slice(1) : url.search) : false; 1204 | options.secure = 'secure' in options ? options.secure : url.protocol === 'https:'; 1205 | options.auth = 'auth' in options ? options.auth : url.auth; 1206 | options.pathname = 'pathname' in options ? options.pathname : this.pathname.slice(1); 1207 | options.port = 'port' in options ? options.port : url.port || (options.secure ? 443 : 80); 1208 | options.host = 'host' in options ? options.host : url.hostname || url.host.replace(':'+ url.port, ''); 1209 | 1210 | // 1211 | // Automatically suffix the protocol so we can supply ws and http and it gets 1212 | // transformed correctly. 1213 | // 1214 | server.push(options.secure ? options.protocol +'s:' : options.protocol +':', ''); 1215 | 1216 | if (options.auth) server.push(options.auth +'@'+ url.host); 1217 | else server.push(url.host); 1218 | 1219 | // 1220 | // Pathnames are optional as some Transformers would just use the pathname 1221 | // directly. 1222 | // 1223 | if (options.pathname) server.push(options.pathname); 1224 | 1225 | // 1226 | // Optionally add a search query, again, not supported by all Transformers. 1227 | // SockJS is known to throw errors when a query string is included. 1228 | // 1229 | if (options.query) server.push('?'+ options.query); 1230 | 1231 | if (options.object) return options; 1232 | return server.join('/'); 1233 | }; 1234 | 1235 | /** 1236 | * Simple emit wrapper that returns a function that emits an event once it's 1237 | * called. This makes it easier for transports to emit specific events. The 1238 | * scope of this function is limited as it will only emit one single argument. 1239 | * 1240 | * @param {String} event Name of the event that we should emit. 1241 | * @param {Function} parser Argument parser. 1242 | * @api public 1243 | */ 1244 | Primus.prototype.emits = function emits(event, parser) { 1245 | var primus = this; 1246 | 1247 | return function emit(arg) { 1248 | var data = parser ? parser.apply(primus, arguments) : arg; 1249 | 1250 | // 1251 | // Timeout is required to prevent crashes on WebSockets connections on 1252 | // mobile devices. We need to handle these edge cases in our own library 1253 | // as we cannot be certain that all frameworks fix these issues. 1254 | // 1255 | setTimeout(function timeout() { 1256 | primus.emit('incoming::'+ event, data); 1257 | }, 0); 1258 | }; 1259 | }; 1260 | 1261 | /** 1262 | * Register a new message transformer. This allows you to easily manipulate incoming 1263 | * and outgoing data which is particularity handy for plugins that want to send 1264 | * meta data together with the messages. 1265 | * 1266 | * @param {String} type Incoming or outgoing 1267 | * @param {Function} fn A new message transformer. 1268 | * @api public 1269 | */ 1270 | Primus.prototype.transform = function transform(type, fn) { 1271 | context(this, 'transform'); 1272 | 1273 | if (!(type in this.transformers)) { 1274 | return this.critical(new Error('Invalid transformer type')); 1275 | } 1276 | 1277 | this.transformers[type].push(fn); 1278 | return this; 1279 | }; 1280 | 1281 | /** 1282 | * A critical error has occurred, if we have an error listener, emit it there. 1283 | * If not, throw it, so we get a stack trace + proper error message. 1284 | * 1285 | * @param {Error} err The critical error. 1286 | * @api private 1287 | */ 1288 | Primus.prototype.critical = function critical(err) { 1289 | if (this.listeners('error').length) { 1290 | this.emit('error', err); 1291 | return this; 1292 | } 1293 | 1294 | throw err; 1295 | }; 1296 | 1297 | /** 1298 | * Syntax sugar, adopt a Socket.IO like API. 1299 | * 1300 | * @param {String} url The URL we want to connect to. 1301 | * @param {Object} options Connection options. 1302 | * @returns {Primus} 1303 | * @api public 1304 | */ 1305 | Primus.connect = function connect(url, options) { 1306 | return new Primus(url, options); 1307 | }; 1308 | 1309 | // 1310 | // Expose the EventEmitter so it can be re-used by wrapping libraries we're also 1311 | // exposing the Stream interface. 1312 | // 1313 | Primus.EventEmitter = EventEmitter; 1314 | 1315 | // 1316 | // These libraries are automatically are automatically inserted at the 1317 | // server-side using the Primus#library method. 1318 | // 1319 | Primus.prototype.client = function client() { 1320 | var primus = this 1321 | , socket; 1322 | 1323 | // 1324 | // Selects an available WebSocket constructor. 1325 | // 1326 | var Factory = (function factory() { 1327 | if ('undefined' !== typeof WebSocket) return WebSocket; 1328 | if ('undefined' !== typeof MozWebSocket) return MozWebSocket; 1329 | 1330 | try { return Primus.require('ws'); } 1331 | catch (e) {} 1332 | 1333 | return undefined; 1334 | })(); 1335 | 1336 | if (!Factory) return primus.critical(new Error('Missing required ws module. Please run npm install --save ws')); 1337 | 1338 | 1339 | // 1340 | // Connect to the given URL. 1341 | // 1342 | primus.on('outgoing::open', function opening() { 1343 | if (socket) socket.close(); 1344 | 1345 | // 1346 | // FireFox will throw an error when we try to establish a connection from 1347 | // a secure page to an unsecured WebSocket connection. This is inconsistent 1348 | // behaviour between different browsers. This should ideally be solved in 1349 | // Primus when we connect. 1350 | // 1351 | try { 1352 | // 1353 | // Only allow primus.transport object in Node.js, it will throw in 1354 | // browsers with a TypeError if we supply to much arguments. 1355 | // 1356 | if (Factory.length === 3) { 1357 | primus.socket = socket = new Factory( 1358 | primus.uri({ protocol: 'ws', query: true }), // URL 1359 | [], // Sub protocols 1360 | primus.transport // options. 1361 | ); 1362 | } else { 1363 | primus.socket = socket = new Factory(primus.uri({ protocol: 'ws', query: true })); 1364 | } 1365 | } catch (e) { return primus.emit('error', e); } 1366 | 1367 | // 1368 | // Setup the Event handlers. 1369 | // 1370 | socket.binaryType = 'arraybuffer'; 1371 | socket.onopen = primus.emits('open'); 1372 | socket.onerror = primus.emits('error'); 1373 | socket.onclose = primus.emits('end'); 1374 | socket.onmessage = primus.emits('data', function parse(evt) { 1375 | return evt.data; 1376 | }); 1377 | }); 1378 | 1379 | // 1380 | // We need to write a new message to the socket. 1381 | // 1382 | primus.on('outgoing::data', function write(message) { 1383 | if (!socket || socket.readyState !== Factory.OPEN) return; 1384 | 1385 | try { socket.send(message); } 1386 | catch (e) { primus.emit('incoming::error', e); } 1387 | }); 1388 | 1389 | // 1390 | // Attempt to reconnect the socket. It assumes that the outgoing::end event is 1391 | // called if it failed to disconnect. 1392 | // 1393 | primus.on('outgoing::reconnect', function reconnect() { 1394 | if (socket) primus.emit('outgoing::end'); 1395 | primus.emit('outgoing::open'); 1396 | }); 1397 | 1398 | // 1399 | // We need to close the socket. 1400 | // 1401 | primus.on('outgoing::end', function close() { 1402 | if (socket) { 1403 | socket.close(); 1404 | socket = null; 1405 | } 1406 | }); 1407 | }; 1408 | Primus.prototype.authorization = false; 1409 | Primus.prototype.pathname = "/primus"; 1410 | Primus.prototype.encoder = function encoder(data, fn) { 1411 | var err; 1412 | 1413 | try { data = JSON.stringify(data); } 1414 | catch (e) { err = e; } 1415 | 1416 | fn(err, data); 1417 | }; 1418 | Primus.prototype.decoder = function decoder(data, fn) { 1419 | var err; 1420 | 1421 | try { data = JSON.parse(data); } 1422 | catch (e) { err = e; } 1423 | 1424 | fn(err, data); 1425 | }; 1426 | Primus.prototype.version = "2.0.4"; 1427 | 1428 | // 1429 | // Hack 1: \u2028 and \u2029 are allowed inside string in JSON. But JavaScript 1430 | // defines them as newline separators. Because no literal newlines are allowed 1431 | // in a string this causes a ParseError. We work around this issue by replacing 1432 | // these characters with a properly escaped version for those chars. This can 1433 | // cause errors with JSONP requests or if the string is just evaluated. 1434 | // 1435 | // This could have been solved by replacing the data during the "outgoing::data" 1436 | // event. But as it affects the JSON encoding in general I've opted for a global 1437 | // patch instead so all JSON.stringify operations are save. 1438 | // 1439 | if ( 1440 | 'object' === typeof JSON 1441 | && 'function' === typeof JSON.stringify 1442 | && JSON.stringify(['\u2028\u2029']) === '["\u2028\u2029"]' 1443 | ) { 1444 | JSON.stringify = function replace(stringify) { 1445 | var u2028 = /\u2028/g 1446 | , u2029 = /\u2029/g; 1447 | 1448 | return function patched(value, replacer, spaces) { 1449 | var result = stringify.call(this, value, replacer, spaces); 1450 | 1451 | // 1452 | // Replace the bad chars. 1453 | // 1454 | if (result) { 1455 | if (~result.indexOf('\u2028')) result = result.replace(u2028, '\\u2028'); 1456 | if (~result.indexOf('\u2029')) result = result.replace(u2029, '\\u2029'); 1457 | } 1458 | 1459 | return result; 1460 | }; 1461 | }(JSON.stringify); 1462 | } 1463 | 1464 | if ( 1465 | 'undefined' !== typeof document 1466 | && 'undefined' !== typeof navigator 1467 | ) { 1468 | // 1469 | // Hack 2: If you press ESC in FireFox it will close all active connections. 1470 | // Normally this makes sense, when your page is still loading. But versions 1471 | // before FireFox 22 will close all connections including WebSocket connections 1472 | // after page load. One way to prevent this is to do a preventDefault() and 1473 | // cancel the operation before it bubbles up to the browsers default handler. 1474 | // It needs to be added as keydown event, if it's added keyup it will not be 1475 | // able to prevent the connection from being closed. 1476 | // 1477 | if (document.addEventListener) { 1478 | document.addEventListener('keydown', function keydown(e) { 1479 | if (e.keyCode !== 27 || !e.preventDefault) return; 1480 | 1481 | e.preventDefault(); 1482 | }, false); 1483 | } 1484 | 1485 | // 1486 | // Hack 3: This is a Mac/Apple bug only, when you're behind a reverse proxy or 1487 | // have you network settings set to automatic proxy discovery the safari 1488 | // browser will crash when the WebSocket constructor is initialised. There is 1489 | // no way to detect the usage of these proxies available in JavaScript so we 1490 | // need to do some nasty browser sniffing. This only affects Safari versions 1491 | // lower then 5.1.4 1492 | // 1493 | var ua = (navigator.userAgent || '').toLowerCase() 1494 | , parsed = ua.match(/.+(?:rv|it|ra|ie)[\/: ](\d+)\.(\d+)(?:\.(\d+))?/) || [] 1495 | , version = +[parsed[1], parsed[2]].join('.'); 1496 | 1497 | if ( 1498 | !~ua.indexOf('chrome') 1499 | && ~ua.indexOf('safari') 1500 | && version < 534.54 1501 | ) { 1502 | Primus.prototype.AVOID_WEBSOCKETS = true; 1503 | } 1504 | } 1505 | return Primus; });` 1506 | -------------------------------------------------------------------------------- /signalbox.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Clinton Freeman 2014 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, 6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or 11 | * substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "encoding/json" 24 | "fmt" 25 | "github.com/gorilla/websocket" 26 | "io" 27 | "log" 28 | "net/http" 29 | "os" 30 | "strings" 31 | "time" 32 | ) 33 | 34 | const bufferSize int = 2048 35 | const maxMessageSize int = 20480 // Ensure that inbound messages don't cause the signalbox to run out of memory. 36 | 37 | type Peer struct { 38 | Id string // The unique identifier of the peer. 39 | socket *websocket.Conn // The socket for writing to the peer. 40 | } 41 | 42 | type Room struct { 43 | Room string // The unique name of the room (id). 44 | } 45 | 46 | type SignalBox struct { 47 | Peers map[string]*Peer // All the peers currently inside this signalbox. 48 | Rooms map[string]*Room // All the rooms currently inside this signalbox. 49 | RoomContains map[string]map[string]*Peer // All the peers currently inside a room. 50 | PeerIsIn map[string]map[string]*Room // All the rooms a peer is currently inside. 51 | } 52 | 53 | type Message struct { 54 | msgSocket *websocket.Conn // The socket that the message was broadcast across. 55 | msgBody string // The body of the broadcasted message. 56 | } 57 | 58 | func messagePump(config Configuration, msg chan Message, ws *websocket.Conn) { 59 | ws.SetReadDeadline(time.Now().Add(config.SocketTimeout * time.Second)) 60 | ws.SetWriteDeadline(time.Now().Add(config.SocketTimeout * time.Second)) 61 | 62 | for { 63 | _, reader, err := ws.NextReader() 64 | 65 | if err != nil { 66 | // Unable to get reader from socket - probably closed, tell the signalbox. 67 | log.Printf("ERROR - messagePump: Can't read from %p, closing", ws) 68 | log.Print(err) 69 | msg <- Message{ws, "/close"} 70 | 71 | return 72 | } 73 | 74 | buffer := make([]byte, bufferSize) 75 | n, err := reader.Read(buffer) 76 | socketContents := string(buffer[0:n]) 77 | 78 | for err != io.EOF && (len(socketContents)-bufferSize) < maxMessageSize { 79 | // filled the buffer - we might have more stuff in the message. 80 | n, err = reader.Read(buffer) 81 | socketContents = socketContents + string(buffer[0:n]) 82 | } 83 | 84 | if err != io.EOF { 85 | log.Printf("ERROR - messagePump: Unable to read from websocket.") 86 | log.Print(err) 87 | continue 88 | } 89 | 90 | // Recieved content from socket - extend read deadline. 91 | ws.SetReadDeadline(time.Now().Add(config.SocketTimeout * time.Second)) 92 | 93 | // Pump the new message into the signalbox. 94 | log.Printf("Recieved %s from %p", socketContents, ws) 95 | msg <- Message{ws, socketContents} 96 | } 97 | } 98 | 99 | func signalbox(config Configuration, msg chan Message) { 100 | s := SignalBox{make(map[string]*Peer), 101 | make(map[string]*Room), 102 | make(map[string]map[string]*Peer), 103 | make(map[string]map[string]*Room)} 104 | 105 | for { 106 | m := <-msg 107 | 108 | // Message matches a primus heartbeat message. Lightly massage the connection 109 | // with pong brand baby oil to keep everything running smoothly. 110 | if strings.HasPrefix(m.msgBody, "primus::ping::") { 111 | pong := fmt.Sprintf("primus::pong::%s", strings.Split(m.msgBody, "primus::ping::")[1]) 112 | b, _ := json.Marshal(pong) 113 | 114 | m.msgSocket.WriteMessage(websocket.TextMessage, b) 115 | m.msgSocket.SetWriteDeadline(time.Now().Add(config.SocketTimeout * time.Second)) 116 | continue 117 | } 118 | 119 | action, messageBody, err := ParseMessage(m.msgBody) 120 | if err != nil { 121 | log.Printf("ERROR - signalbox: Unable to parse message.") 122 | log.Print(err) 123 | continue 124 | } 125 | 126 | s, err = action(messageBody, m.msgSocket, s) 127 | if err != nil { 128 | log.Printf("ERROR - signalbox: Unable to update state.") 129 | log.Print(err) 130 | } 131 | } 132 | } 133 | 134 | func main() { 135 | log.Printf("INFO - Started SignalBox\n") 136 | 137 | configFile := "signalbox.json" 138 | if len(os.Args) > 1 { 139 | configFile = os.Args[1] 140 | } 141 | 142 | config, err := parseConfiguration(configFile) 143 | if err != nil { 144 | log.Printf("ERROR - main: Unable to parse config %s - using defaults.", err) 145 | } 146 | 147 | msg := make(chan Message) 148 | go signalbox(config, msg) 149 | 150 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 151 | if r.Method != "GET" { 152 | http.Error(w, "Method not allowed", 405) 153 | return 154 | } 155 | 156 | // Upgrade the HTTP server connection to the WebSocket protocol. 157 | ws, err := websocket.Upgrade(w, r, nil, 1024, 1024) 158 | if err != nil { 159 | log.Printf("ERROR - http.HandleFunc: %s", err) 160 | return 161 | } 162 | 163 | // Start pumping messages from this websocket into the signal box. 164 | go messagePump(config, msg, ws) 165 | }) 166 | 167 | http.HandleFunc("/rtc.io/primus.js", func(w http.ResponseWriter, r *http.Request) { 168 | log.Printf("INFO - Serving primus.js file.") // Hope to deprecate this with the latest version rtc.io signalling protocol changes. 169 | w.Header().Set("Content-Type", "text/javascript") 170 | fmt.Fprintf(w, primus_content) 171 | }) 172 | 173 | err = http.ListenAndServe(config.ListenAddress, nil) 174 | if err != nil { 175 | panic("ListenAndServe: " + err.Error()) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /signalbox_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Clinton Freeman 2014 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, 6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or 11 | * substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "github.com/gorilla/websocket" 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | "reflect" 28 | "runtime" 29 | "testing" 30 | "time" 31 | ) 32 | 33 | func TestMessage(t *testing.T) { 34 | RegisterFailHandler(Fail) 35 | RunSpecs(t, "Message Suite") 36 | } 37 | 38 | var _ = Describe("Message", func() { 39 | Context("Utf8 encoding", func() { 40 | It("should return an error for non-utf8 encoded messages", func() { 41 | _, _, err := ParseMessage(string([]byte{0xff, 0xfe, 0xfd})) 42 | Ω(err).ShouldNot(BeNil()) 43 | }) 44 | 45 | It("should should not return an error for utf8 encoded messages", func() { 46 | _, _, err := ParseMessage("/announce|dc6ac0ae-6e15-409b-b211-228a8f4a43b9|{\"browser\":\"node\",\"browserVersion\":\"?\",\"id\":\"dc6ac0ae-6e15-409b-b211-228a8f4a43b9\",\"agent\":\"signaller@0.18.3\",\"room\":\"test-room\"}") 47 | Ω(err).Should(BeNil()) 48 | }) 49 | }) 50 | 51 | Context("Action parsing", func() { 52 | It("should be able to handle zero-length messages", func() { 53 | action, message, err := ParseMessage("") 54 | Ω(err).Should(BeNil()) 55 | Ω(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()).Should(Equal("github.com/cfreeman/signalbox.ignore")) 56 | Ω(len(message)).Should(Equal(1)) 57 | }) 58 | 59 | It("should be able to parse an announce message", func() { 60 | action, message, err := ParseMessage("/announce") 61 | Ω(err).Should(BeNil()) 62 | Ω(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()).Should(Equal("github.com/cfreeman/signalbox.announce")) 63 | Ω(len(message)).Should(Equal(1)) 64 | }) 65 | 66 | It("should be able to parse a leave message", func() { 67 | action, message, err := ParseMessage("/leave") 68 | Ω(err).Should(BeNil()) 69 | Ω(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()).Should(Equal("github.com/cfreeman/signalbox.leave")) 70 | Ω(len(message)).Should(Equal(1)) 71 | }) 72 | 73 | It("should be able to parse a close message", func() { 74 | action, message, err := ParseMessage("/close") 75 | Ω(err).Should(BeNil()) 76 | Ω(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()).Should(Equal("github.com/cfreeman/signalbox.closePeer")) 77 | Ω(len(message)).Should(Equal(1)) 78 | }) 79 | 80 | It("should be able to parse a to message", func() { 81 | action, message, err := ParseMessage("/to") 82 | Ω(err).Should(BeNil()) 83 | Ω(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()).Should(Equal("github.com/cfreeman/signalbox.to")) 84 | Ω(len(message)).Should(Equal(1)) 85 | }) 86 | 87 | It("should be able to parse a custom message", func() { 88 | action, message, err := ParseMessage("/custom|part1|part2") 89 | Ω(err).Should(BeNil()) 90 | Ω(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()).Should(Equal("github.com/cfreeman/signalbox.custom")) 91 | Ω(len(message)).Should(Equal(3)) 92 | Ω(message[0]).Should(Equal("/custom")) 93 | Ω(message[1]).Should(Equal("part1")) 94 | Ω(message[2]).Should(Equal("part2")) 95 | }) 96 | 97 | It("should ignore malformed messages", func() { 98 | action, message, err := ParseMessage(":lkajsd??asdj/foo") 99 | Ω(err).Should(BeNil()) 100 | Ω(len(message)).Should(Equal(1)) 101 | Ω(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()).Should(Equal("github.com/cfreeman/signalbox.ignore")) 102 | }) 103 | }) 104 | 105 | Context("ParsePeerAndRoom", func() { 106 | It("should return an error when their is not enough parts to a message", func() { 107 | _, message, _ := ParseMessage("/foo") 108 | _, _, err := ParsePeerAndRoom(message) 109 | Ω(err).ShouldNot(BeNil()) 110 | }) 111 | 112 | It("should parse source id and room", func() { 113 | _, message, _ := ParseMessage("/announce|abc|{\"room\":\"test\"}") 114 | source, destination, err := ParsePeerAndRoom(message) 115 | Ω(err).Should(BeNil()) 116 | Ω(source.Id).Should(Equal("abc")) 117 | Ω(destination.Room).Should(Equal("test")) 118 | }) 119 | }) 120 | 121 | Context("Test configuration parsing", func() { 122 | It("Should throw an error for an invalid config file", func() { 123 | config, err := parseConfiguration("foo") 124 | Ω(err).ShouldNot(BeNil()) 125 | Ω(config.ListenAddress).Should(Equal(":3000")) 126 | Ω(config.SocketTimeout).Should(Equal(time.Duration(300) * time.Nanosecond)) 127 | }) 128 | 129 | It("Should be able to parse a valid config file", func() { 130 | config, err := parseConfiguration("testdata/test-config.json") 131 | Ω(err).Should(BeNil()) 132 | Ω(config.ListenAddress).Should(Equal("10.1.2.3:4000")) 133 | Ω(config.SocketTimeout).Should(Equal(time.Duration(200) * time.Nanosecond)) 134 | }) 135 | }) 136 | 137 | Context("Test SignalBox State", func() { 138 | var state SignalBox 139 | var announceAAct messageFn 140 | var announceAMsg []string 141 | 142 | var announceA2Act messageFn 143 | var announceA2Msg []string 144 | 145 | var leaveAAct messageFn 146 | var leaveAMsg []string 147 | 148 | var leaveA2Act messageFn 149 | var leaveA2Msg []string 150 | 151 | var announceBAct messageFn 152 | var announceBMsg []string 153 | 154 | var leaveBAct messageFn 155 | var leaveBMsg []string 156 | 157 | BeforeEach(func() { 158 | var err error 159 | state = SignalBox{make(map[string]*Peer), 160 | make(map[string]*Room), 161 | make(map[string]map[string]*Peer), 162 | make(map[string]map[string]*Room)} 163 | 164 | announceAAct, announceAMsg, err = ParseMessage("/announce|a|{\"room\":\"test\"}") 165 | Ω(err).Should(BeNil()) 166 | 167 | announceA2Act, announceA2Msg, err = ParseMessage("/announce|a|{\"room\":\"test2\"}") 168 | Ω(err).Should(BeNil()) 169 | 170 | leaveAAct, leaveAMsg, err = ParseMessage("/leave|a|{\"room\":\"test\"}") 171 | Ω(err).Should(BeNil()) 172 | 173 | leaveA2Act, leaveA2Msg, err = ParseMessage("/leave|a|{\"room\":\"test2\"}") 174 | Ω(err).Should(BeNil()) 175 | 176 | announceBAct, announceBMsg, err = ParseMessage("/announce|b|{\"room\":\"test\"}") 177 | Ω(err).Should(BeNil()) 178 | 179 | leaveBAct, leaveBMsg, err = ParseMessage("/leave|b|{\"room\":\"test\"}") 180 | Ω(err).Should(BeNil()) 181 | }) 182 | 183 | It("only add someone to the room once, even if they announce more than once", func() { 184 | state, err := announceAAct(announceAMsg, nil, state) 185 | Ω(err).Should(BeNil()) 186 | state, err = announceAAct(announceAMsg, nil, state) 187 | Ω(err).Should(BeNil()) 188 | 189 | Ω(len(state.Peers)).Should(Equal(1)) 190 | Ω(len(state.Rooms)).Should(Equal(1)) 191 | Ω(len(state.RoomContains)).Should(Equal(1)) 192 | Ω(len(state.PeerIsIn)).Should(Equal(1)) 193 | Ω(len(state.RoomContains["test"])).Should(Equal(1)) 194 | Ω(len(state.PeerIsIn["a"])).Should(Equal(1)) 195 | Ω(state.RoomContains["test"]["a"].Id).Should(Equal("a")) 196 | Ω(state.PeerIsIn["a"]["test"].Room).Should(Equal("test")) 197 | }) 198 | 199 | It("should be able to add multiple people to a signalbox room", func() { 200 | state, err := announceAAct(announceAMsg, nil, state) 201 | Ω(err).Should(BeNil()) 202 | state, err = announceBAct(announceBMsg, nil, state) 203 | Ω(err).Should(BeNil()) 204 | 205 | Ω(len(state.Peers)).Should(Equal(2)) 206 | Ω(len(state.Rooms)).Should(Equal(1)) 207 | Ω(len(state.PeerIsIn)).Should(Equal(2)) 208 | Ω(len(state.RoomContains)).Should(Equal(1)) 209 | Ω(len(state.PeerIsIn["a"])).Should(Equal(1)) 210 | Ω(len(state.PeerIsIn["b"])).Should(Equal(1)) 211 | Ω(len(state.RoomContains["test"])).Should(Equal(2)) 212 | }) 213 | 214 | It("Should be able to have a person leave a signalbox room", func() { 215 | state, err := announceAAct(announceAMsg, nil, state) 216 | Ω(err).Should(BeNil()) 217 | state, err = leaveAAct(leaveAMsg, nil, state) 218 | Ω(err).Should(BeNil()) 219 | 220 | Ω(len(state.Peers)).Should(Equal(0)) 221 | Ω(len(state.Rooms)).Should(Equal(0)) 222 | Ω(len(state.PeerIsIn)).Should(Equal(0)) 223 | Ω(len(state.RoomContains)).Should(Equal(0)) 224 | }) 225 | 226 | It("Should keep a room, if a person leaves but it still contains peers", func() { 227 | state, err := announceBAct(announceBMsg, nil, state) 228 | Ω(err).Should(BeNil()) 229 | state, err = announceAAct(announceAMsg, nil, state) 230 | Ω(err).Should(BeNil()) 231 | state, err = announceA2Act(announceA2Msg, nil, state) 232 | Ω(err).Should(BeNil()) 233 | state, err = leaveBAct(leaveBMsg, nil, state) 234 | Ω(err).Should(BeNil()) 235 | 236 | Ω(len(state.Peers)).Should(Equal(1)) 237 | Ω(len(state.Rooms)).Should(Equal(2)) 238 | Ω(len(state.PeerIsIn)).Should(Equal(1)) 239 | Ω(len(state.RoomContains)).Should(Equal(2)) 240 | Ω(len(state.PeerIsIn["a"])).Should(Equal(2)) 241 | Ω(len(state.RoomContains["test"])).Should(Equal(1)) 242 | Ω(len(state.RoomContains["test2"])).Should(Equal(1)) 243 | Ω(state.RoomContains["test"]["a"].Id).Should(Equal("a")) 244 | Ω(state.RoomContains["test2"]["a"].Id).Should(Equal("a")) 245 | }) 246 | }) 247 | 248 | Context("Broadcast messages", func() { 249 | // Spin up the signalbox. 250 | go main() 251 | 252 | It("Should be to send announce and leave messages to peers", func() { 253 | a, err := connectPeer("a", "test-room") 254 | Ω(err).Should(BeNil()) 255 | socketShouldContain(a, "/roominfo|{\"memberCount\":1}") 256 | 257 | b, err := connectPeer("b", "test-room") 258 | Ω(err).Should(BeNil()) 259 | socketShouldContain(b, "/roominfo|{\"memberCount\":2}") 260 | 261 | socketShouldContain(a, "/announce|b|{\"room\":\"test-room\"}") 262 | 263 | socketSend(a, "/leave|a|{\"room\":\"test-room\"}") 264 | err = a.Close() 265 | Ω(err).Should(BeNil()) 266 | 267 | socketShouldContain(b, "/leave|a|{\"room\":\"test-room\"}") 268 | err = b.Close() 269 | Ω(err).Should(BeNil()) 270 | }) 271 | 272 | It("Should be able to send messages just to specified recipients", func() { 273 | a2, err := connectPeer("a2", "to-test") 274 | Ω(err).Should(BeNil()) 275 | socketShouldContain(a2, "/roominfo|{\"memberCount\":1}") 276 | 277 | b2, err := connectPeer("b2", "to-test") 278 | Ω(err).Should(BeNil()) 279 | socketShouldContain(b2, "/roominfo|{\"memberCount\":2}") 280 | 281 | c2, err := connectPeer("c2", "to-test") 282 | Ω(err).Should(BeNil()) 283 | socketShouldContain(c2, "/roominfo|{\"memberCount\":3}") 284 | 285 | socketShouldContain(a2, "/announce|b2|{\"room\":\"to-test\"}") 286 | socketShouldContain(a2, "/announce|c2|{\"room\":\"to-test\"}") 287 | 288 | socketShouldContain(b2, "/announce|c2|{\"room\":\"to-test\"}") 289 | socketSend(a2, "/to|c2|/hello|{\"id\":\"a1\"}") 290 | 291 | socketShouldContain(c2, "/to|c2|/hello|{\"id\":\"a1\"}") 292 | 293 | _, _, err = b2.ReadMessage() 294 | Ω(err).ShouldNot(BeNil()) 295 | }) 296 | 297 | It("Should be able to send custom messages to peers", func() { 298 | a3, err := connectPeer("a3", "custom-test") 299 | Ω(err).Should(BeNil()) 300 | socketShouldContain(a3, "/roominfo|{\"memberCount\":1}") 301 | 302 | b3, err := connectPeer("b3", "custom-test") 303 | Ω(err).Should(BeNil()) 304 | socketShouldContain(b3, "/roominfo|{\"memberCount\":2}") 305 | 306 | socketShouldContain(a3, "/announce|b3|{\"room\":\"custom-test\"}") 307 | socketSend(a3, "/hello|a3") 308 | socketShouldContain(b3, "/hello|a3") 309 | }) 310 | 311 | It("Should get a leave message when a peer disconnects", func() { 312 | a4, err := connectPeer("a4", "close-test") 313 | Ω(err).Should(BeNil()) 314 | socketShouldContain(a4, "/roominfo|{\"memberCount\":1}") 315 | 316 | b4, err := connectPeer("b4", "close-test") 317 | Ω(err).Should(BeNil()) 318 | socketShouldContain(b4, "/roominfo|{\"memberCount\":2}") 319 | 320 | socketShouldContain(a4, "/announce|b4|{\"room\":\"close-test\"}") 321 | err = a4.Close() 322 | Ω(err).Should(BeNil()) 323 | socketShouldContain(b4, "/leave|a4|{\"room\":\"close-test\"}") 324 | }) 325 | 326 | It("Should be able to handle very long messages", func() { 327 | a5, err := connectPeer("a5", "long-test") 328 | Ω(err).Should(BeNil()) 329 | socketShouldContain(a5, "/roominfo|{\"memberCount\":1}") 330 | 331 | b5, err := connectPeer("b5", "long-test") 332 | socketShouldContain(b5, "/roominfo|{\"memberCount\":2}") 333 | 334 | socketShouldContain(a5, "/announce|b5|{\"room\":\"long-test\"}") 335 | 336 | socketSend(b5, "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|b5") 337 | 338 | socketShouldContain(a5, "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|b5") 339 | }) 340 | }) 341 | }) 342 | 343 | func socketSend(ws *websocket.Conn, content string) { 344 | ws.WriteMessage(websocket.TextMessage, []byte(content)) 345 | } 346 | 347 | func socketShouldContain(ws *websocket.Conn, content string) { 348 | _, message, err := ws.ReadMessage() 349 | Ω(err).Should(BeNil()) 350 | Ω(string(message)).Should(Equal(content)) 351 | } 352 | 353 | func connectPeer(id string, room string) (*websocket.Conn, error) { 354 | url := "ws://localhost:3000" 355 | res, _, err := websocket.DefaultDialer.Dial(url, nil) 356 | if err != nil || res == nil { 357 | return nil, err 358 | } 359 | 360 | connect := fmt.Sprintf("/announce|%s|{\"room\":\"%s\"}", id, room) 361 | err = res.WriteMessage(websocket.TextMessage, []byte(connect)) 362 | if err != nil { 363 | return nil, err 364 | } 365 | 366 | res.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) 367 | 368 | return res, nil 369 | } 370 | -------------------------------------------------------------------------------- /testdata/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ListenAddress":"10.1.2.3:4000", 3 | "SocketTimeout":200 4 | } --------------------------------------------------------------------------------