├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── backend ├── closer │ └── closer.go ├── global │ └── global.go ├── server.go ├── socket.go └── sockets │ ├── ajaxsocket │ ├── server.go │ └── socket.go │ └── websocket │ ├── server.go │ └── socket.go ├── bower.json ├── channel.go ├── client ├── dist │ └── glue.js ├── gulpfile.js ├── package.json └── src │ ├── ajaxsocket.js │ ├── channel.js │ ├── emitter.js │ ├── glue.js │ ├── utils.js │ └── websocket.js ├── handler.go ├── log └── log.go ├── options.go ├── sample ├── Channels │ ├── .gitignore │ ├── main.go │ └── public │ │ └── index.html ├── OnlyWrite │ ├── .gitignore │ ├── main.go │ └── public │ │ └── index.html ├── ReadEventWrite │ ├── .gitignore │ ├── main.go │ └── public │ │ └── index.html └── ReadWrite │ ├── .gitignore │ ├── main.go │ └── public │ └── index.html ├── server.go ├── socket.go └── utils ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | ._.DS_Store 4 | tmp 5 | bower_components 6 | client/node_modules 7 | client/bower_components -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Roland Singer -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This project follows the [Semantic Versioning](http://semver.org/). 3 | 4 | ## 1.9.1 - 2016-06-10 5 | - Small improvements and fixes to prevent race conditions on calling the onClose function callback. 6 | 7 | ## 1.9.0 - 2016-06-02 8 | - Removed the jQuery dependency. 9 | 10 | ## 1.8.2 - 2016-05-16 11 | - Fixed possible race condition in websocket backend (Synchronous websocket write message and write close message). 12 | 13 | ## 1.8.1 - 2016-03-20 14 | - OnRead callbacks are now called in a new goroutine. 15 | 16 | ## 1.8.0 - 2015-11-26 17 | - Added support for the Cross-Origin Resource Sharing (CORS) mechanism. Added new option EnableCORS. 18 | 19 | ## 1.7.0 - 2015-11-15 20 | - Added CheckOrigin option. 21 | 22 | ## 1.6.0 - 2015-11-15 23 | - Added method to obtain the socket ID of the client-side. 24 | - Websocket: don't log messages if the 1005 close code is received (CloseNoStatusReceived). 25 | 26 | ## 1.5.0 - 2015-11-04 27 | - Added GetSocket method to server. 28 | - Some minor improvements. 29 | 30 | ## 1.4.1 - 2015-10-19 31 | - websocket: removed dirty hack to fix unnecessary log messages and fixed this with the websocket close code. 32 | 33 | ## 1.4.0 - 2015-10-18 34 | - Implemented socket ClosedChan method. 35 | - Reformatted README. 36 | 37 | ## 1.3.1 - 2015-09-09 38 | - Javascript client: code cleanup and small fixes (JSHint). 39 | - Updated Version 40 | - Added semantic version check with backward compatibility check. 41 | - Implemented ajax poll timeout and close handling. 42 | - Suppress unnecessary websocket close error message. 43 | 44 | ## 1.3.0 - 2015-09-02 45 | - Restructured backend sockets. 46 | - Moved glue methods into a server struct. 47 | - New socket ID generation. 48 | - Added support to set custom HTTP base URLs. 49 | - Added server options. 50 | - HTTP server is now started by the glue server. 51 | - Added support for custom HTTP multiplexers. 52 | 53 | ## 1.2.0 - 2015-08-11 54 | - Several small fixes and improvements. 55 | 56 | ### Added 57 | - Added channel support to communicate in different channels. 58 | - Sockets are added to a map of active sockets. 59 | - A list of active sockets can be retrieved with glue.Sockets(). 60 | - Added unique ID for each socket. 61 | - Added Release function to block new incoming connections and to close all current connected sockets. 62 | - Added socket.Value interface to store custom data. 63 | - Added glue socket protocol versions check during socket connection initialization. 64 | 65 | ### Removed 66 | - Removed discard_send_buffers from the frontend library. Use the discard callback in the send function instead. 67 | 68 | ## 1.1.1 - 2015-07-21 69 | ### Added 70 | - Added socket OnRead event. Either use Read or OnRead. 71 | - Added internal read handler with locking... 72 | - Updated README. 73 | 74 | ## 1.1.0 - 2015-07-15 75 | ### Added 76 | - Updated TODO. 77 | - Added Changelog. 78 | - Added blocking socket Read method. 79 | 80 | ### Removed 81 | - Removed socket OnRead event. 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Roland Singer. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glue - Robust Go and Javascript Socket Library 2 | 3 | [![GoDoc](https://godoc.org/github.com/desertbit/glue?status.svg)](https://godoc.org/github.com/desertbit/glue) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/desertbit/glue)](https://goreportcard.com/report/github.com/desertbit/glue) 5 | [![Join the chat at https://gitter.im/desertbit/glue](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/desertbit/glue?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | Glue is a real-time bidirectional socket library. It is a **clean**, **robust** and **efficient** alternative to [socket.io](http://socket.io/). This library is designed to connect webbrowsers with a go-backend in a simple way. It automatically detects supported socket layers and chooses the most suitable one. This library handles automatic reconnections on disconnections and handles caching to bridge those disconnections. The server implementation is **thread-safe** and **stable**. The API is **fixed** and there won't be any breaking API changes. 8 | 9 | ## Socket layers 10 | Currently two socket layers are supported: 11 | - **WebSockets** - This is the primary option. They are used if the webbrowser supports WebSockets defined by [RFC 6455](https://tools.ietf.org/html/rfc6455). 12 | - **AjaxSockets** - This socket layer is used as a fallback mode. 13 | 14 | ## Support 15 | Feel free to contribute to this project. Please check the [TODO](TODO.md) file for more information. 16 | 17 | ## Install 18 | ### Client 19 | The client javascript Glue library is located in **[client/dist/glue.js](client/dist/glue.js)**. 20 | 21 | You can use bower to install the client library: 22 | 23 | `bower install --save glue-socket` 24 | 25 | ### Server 26 | Get the source and start hacking. 27 | 28 | `go get github.com/desertbit/glue` 29 | 30 | Import it with: 31 | 32 | ```go 33 | import "github.com/desertbit/glue" 34 | ``` 35 | 36 | ## Documentation 37 | ### Client - Javascript Library 38 | A simple call to glue() without any options will establish a socket connection to the same host. A glue socket object is returned. 39 | 40 | ```js 41 | // Create and connect to the server. 42 | // Optional pass a host string and options. 43 | var socket = glue(); 44 | ``` 45 | 46 | Optional Javascript options which can be passed to Glue: 47 | 48 | ```js 49 | var host = "https://foo.bar"; 50 | 51 | var opts = { 52 | // The base URL is appended to the host string. This value has to match with the server value. 53 | baseURL: "/glue/", 54 | 55 | // Force a socket type. 56 | // Values: false, "WebSocket", "AjaxSocket" 57 | forceSocketType: false, 58 | 59 | // Kill the connect attempt after the timeout. 60 | connectTimeout: 10000, 61 | 62 | // If the connection is idle, ping the server to check if the connection is stil alive. 63 | pingInterval: 35000, 64 | // Reconnect if the server did not response with a pong within the timeout. 65 | pingReconnectTimeout: 5000, 66 | 67 | // Whenever to automatically reconnect if the connection was lost. 68 | reconnect: true, 69 | reconnectDelay: 1000, 70 | reconnectDelayMax: 5000, 71 | // To disable set to 0 (endless). 72 | reconnectAttempts: 10, 73 | 74 | // Reset the send buffer after the timeout. 75 | resetSendBufferTimeout: 10000 76 | }; 77 | 78 | // Create and connect to the server. 79 | // Optional pass a host string and options. 80 | var socket = glue(host, opts); 81 | ``` 82 | 83 | The glue socket object has following public methods: 84 | 85 | ```js 86 | // version returns the glue socket protocol version. 87 | socket.version(); 88 | 89 | // type returns the current used socket type as string. 90 | // Either "WebSocket" or "AjaxSocket". 91 | socket.type(); 92 | 93 | // state returns the current socket state as string. 94 | // Following states are available: 95 | // - "disconnected" 96 | // - "connecting" 97 | // - "reconnecting" 98 | // - "connected" 99 | socket.state(); 100 | 101 | // socketID returns the socket's ID. 102 | // This is a cryptographically secure pseudorandom number. 103 | socket.socketID(); 104 | 105 | // send a data string to the server. 106 | // One optional discard callback can be passed. 107 | // It is called if the data could not be send to the server. 108 | // The data is passed as first argument to the discard callback. 109 | // returns: 110 | // 1 if immediately send, 111 | // 0 if added to the send queue and 112 | // -1 if discarded. 113 | socket.send(data, discardCallback); 114 | 115 | // onMessage sets the function which is triggered as soon as a message is received. 116 | socket.onMessage(f); 117 | 118 | // on binds event functions to events. 119 | // This function is equivalent to jQuery's on method syntax. 120 | // Following events are available: 121 | // - "connected" 122 | // - "connecting" 123 | // - "disconnected" 124 | // - "reconnecting" 125 | // - "error" 126 | // - "connect_timeout" 127 | // - "timeout" 128 | // - "discard_send_buffer" 129 | socket.on(); 130 | 131 | // Reconnect to the server. 132 | // This is ignored if the socket is not disconnected. 133 | // It will reconnect automatically if required. 134 | socket.reconnect(); 135 | 136 | // close the socket connection. 137 | socket.close(); 138 | 139 | // channel returns the given channel object specified by name 140 | // to communicate in a separate channel than the default one. 141 | socket.channel(name); 142 | ``` 143 | 144 | A channel object has following public methods: 145 | 146 | ```js 147 | // onMessage sets the function which is triggered as soon as a message is received. 148 | c.onMessage(f); 149 | 150 | // send a data string to the channel. 151 | // One optional discard callback can be passed. 152 | // It is called if the data could not be send to the server. 153 | // The data is passed as first argument to the discard callback. 154 | // returns: 155 | // 1 if immediately send, 156 | // 0 if added to the send queue and 157 | // -1 if discarded. 158 | c.send(data, discardCallback); 159 | ``` 160 | 161 | ### Server - Go Library 162 | Check the Documentation at [GoDoc.org](https://godoc.org/github.com/desertbit/glue). 163 | 164 | #### Use a custom HTTP multiplexer 165 | If you choose to use a custom HTTP multiplexer, then it is possible to deactivate the automatic HTTP handler registration of glue. 166 | 167 | ```go 168 | // Create a new glue server without configuring and starting the HTTP server. 169 | server := glue.NewServer(glue.Options{ 170 | HTTPSocketType: HTTPSocketTypeNone, 171 | }) 172 | 173 | //... 174 | ``` 175 | 176 | The glue server implements the ServeHTTP method of the HTTP Handler interface of the http package. Use this to register the glue HTTP handler with a custom multiplexer. Be aware, that the URL of the custom HTTP handler has to match with the glue HTTPHandleURL options string. 177 | 178 | #### Reading data 179 | Data has to be read from the socket and each channel. If you don't require to read data from the socket or a channel, then discard received data with the DiscardRead() method. If received data is not discarded, then the read buffer will block as soon as it is full, which will also block the keep-alive mechanism of the socket. The result would be a closed socket... 180 | 181 | ```go 182 | // ... 183 | 184 | // Discard received data from the main socket channel. 185 | // Hint: Channels have to be discarded separately. 186 | s.DiscardRead() 187 | 188 | // ... 189 | 190 | // Create a channel. 191 | c := s.Channel("golang") 192 | 193 | // Discard received data from a channel. 194 | c.DiscardRead() 195 | ``` 196 | 197 | #### Bind custom values to a socket 198 | The socket.Value interface is a placeholder for custom data. 199 | 200 | ```go 201 | type CustomValues struct { 202 | Foo string 203 | Bar int 204 | } 205 | 206 | // ... 207 | 208 | s.Value = &CustomValues{ 209 | Foo: "Hello World", 210 | Bar: 900, 211 | } 212 | 213 | // ... 214 | 215 | v, ok := s.Value.(*CustomValues) 216 | if !ok { 217 | // Handle error 218 | return 219 | } 220 | ``` 221 | 222 | ### Channels 223 | Channels are separate communication channels from the client to the server of a single socket connections. Multiple separate communication channels can be created: 224 | 225 | Server: 226 | 227 | ```go 228 | // ... 229 | 230 | // Create a channel. 231 | c := s.Channel("golang") 232 | 233 | // Set the channel on read event function. 234 | c.OnRead(func(data string) { 235 | // ... 236 | }) 237 | 238 | // Write to the channel. 239 | c.Write("Hello Gophers!") 240 | ``` 241 | 242 | Client: 243 | 244 | ```js 245 | var c = socket.channel("golang"); 246 | 247 | c.onMessage(function(data) { 248 | console.log(data); 249 | }); 250 | 251 | c.send("Hello World"); 252 | ``` 253 | 254 | ### Broadcasting Messages 255 | 256 | With Glue it is easy to broadcast messages to multiple clients. The Glue Server keeps track of all active connected client sessions. 257 | You can make use of the server **Sockets**, **GetSocket** or **OnNewSocket** methods to implement broadcasting. 258 | 259 | 260 | ## Example 261 | This socket library is very straightforward to use. Check the [sample directory](sample) for more examples. 262 | 263 | ### Client 264 | 265 | ```html 266 | 311 | ``` 312 | 313 | ### Server 314 | Read data from the socket with a read event function. Check the sample directory for other ways of reading data from the socket. 315 | 316 | ```go 317 | import ( 318 | "log" 319 | "net/http" 320 | 321 | "github.com/desertbit/glue" 322 | ) 323 | 324 | func main() { 325 | // Create a new glue server. 326 | server := glue.NewServer(glue.Options{ 327 | HTTPListenAddress: ":8080", 328 | }) 329 | 330 | // Release the glue server on defer. 331 | // This will block new incoming connections 332 | // and close all current active sockets. 333 | defer server.Release() 334 | 335 | // Set the glue event function to handle new incoming socket connections. 336 | server.OnNewSocket(onNewSocket) 337 | 338 | // Run the glue server. 339 | err := server.Run() 340 | if err != nil { 341 | log.Fatalf("Glue Run: %v", err) 342 | } 343 | } 344 | 345 | func onNewSocket(s *glue.Socket) { 346 | // Set a function which is triggered as soon as the socket is closed. 347 | s.OnClose(func() { 348 | log.Printf("socket closed with remote address: %s", s.RemoteAddr()) 349 | }) 350 | 351 | // Set a function which is triggered during each received message. 352 | s.OnRead(func(data string) { 353 | // Echo the received data back to the client. 354 | s.Write(data) 355 | }) 356 | 357 | // Send a welcome string to the client. 358 | s.Write("Hello Client") 359 | } 360 | ``` 361 | 362 | ## Similar Go Projects 363 | - [go-socket.io](https://github.com/googollee/go-socket.io) - socket.io library for golang, a realtime application framework. 364 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - Implement a go glue client library. 4 | - Add some testing. 5 | - Extend the sample (Chat example?). 6 | - Improve the documentation. 7 | - Implement temporary compression for websockets in javascript -> https://github.com/nodeca/pako 8 | - Implement websocket compression as soon as gorilla websocket supports it. 9 | -------------------------------------------------------------------------------- /backend/closer/closer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Emit a close function only once, also if called multiple times. 20 | // This implementation is thread-safe. 21 | package closer 22 | 23 | import ( 24 | "sync" 25 | ) 26 | 27 | type Closer struct { 28 | // Channel which is closed if the closer is closed. 29 | IsClosedChan chan struct{} 30 | 31 | f func() 32 | mutex sync.Mutex 33 | } 34 | 35 | // New creates a new closer. 36 | // The passed function is emitted only once, as soon close is called. 37 | func New(f func()) *Closer { 38 | return &Closer{ 39 | IsClosedChan: make(chan struct{}), 40 | f: f, 41 | } 42 | } 43 | 44 | // Close calls the function and sets the IsClosed boolean. 45 | func (c *Closer) Close() { 46 | // Lock the mutex 47 | c.mutex.Lock() 48 | defer c.mutex.Unlock() 49 | 50 | // Just return if already closed. 51 | if c.IsClosed() { 52 | return 53 | } 54 | 55 | // Close the channel. 56 | close(c.IsClosedChan) 57 | 58 | // Emit the function. 59 | c.f() 60 | } 61 | 62 | // IsClosed returns a boolean whenever this closer is already closed. 63 | func (c *Closer) IsClosed() bool { 64 | select { 65 | case <-c.IsClosedChan: 66 | return true 67 | default: 68 | return false 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /backend/global/global.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Package global provides global types and constants for the backend packages. 20 | package global 21 | 22 | const ( 23 | // Channel buffer sizes: 24 | ReadChanSize = 5 25 | WriteChanSize = 10 26 | ) 27 | 28 | //############################// 29 | //### Backend Socket Types ###// 30 | //############################// 31 | 32 | type SocketType int 33 | 34 | const ( 35 | // The available socket types. 36 | TypeAjaxSocket SocketType = 1 << iota 37 | TypeWebSocket SocketType = 1 << iota 38 | ) 39 | -------------------------------------------------------------------------------- /backend/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Package backend provides the server backend with various socket implementations. 20 | package backend 21 | 22 | import ( 23 | "fmt" 24 | "net/http" 25 | 26 | "github.com/sirupsen/logrus" 27 | "github.com/desertbit/glue/backend/sockets/ajaxsocket" 28 | "github.com/desertbit/glue/backend/sockets/websocket" 29 | "github.com/desertbit/glue/log" 30 | "github.com/desertbit/glue/utils" 31 | ) 32 | 33 | //#################// 34 | //### Constants ###// 35 | //#################// 36 | 37 | const ( 38 | httpURLAjaxSocketSuffix = "ajax" 39 | httpURLWebSocketSuffix = "ws" 40 | ) 41 | 42 | //######################// 43 | //### Backend Server ###// 44 | //######################// 45 | 46 | type Server struct { 47 | onNewSocketConnection func(BackendSocket) 48 | 49 | // An Integer holding the length of characters which should be stripped 50 | // from the ServerHTTP URL path. 51 | httpURLStripLength int 52 | 53 | // checkOriginFunc returns true if the request Origin header is acceptable. 54 | checkOriginFunc func(r *http.Request) bool 55 | 56 | // Enables the Cross-Origin Resource Sharing (CORS) mechanism. 57 | enableCORS bool 58 | 59 | // Socket Servers 60 | webSocketServer *websocket.Server 61 | ajaxSocketServer *ajaxsocket.Server 62 | } 63 | 64 | func NewServer(httpURLStripLength int, enableCORS bool, checkOrigin func(r *http.Request) bool) *Server { 65 | // Create a new backend server. 66 | s := &Server{ 67 | // Set a dummy function. 68 | // This prevents panics, if new sockets are created, 69 | // but no function was set. 70 | onNewSocketConnection: func(BackendSocket) {}, 71 | 72 | httpURLStripLength: httpURLStripLength, 73 | enableCORS: enableCORS, 74 | checkOriginFunc: checkOrigin, 75 | } 76 | 77 | // Create the websocket server and pass the function which handles new incoming socket connections. 78 | s.webSocketServer = websocket.NewServer(func(ws *websocket.Socket) { 79 | s.triggerOnNewSocketConnection(ws) 80 | }) 81 | 82 | // Create the ajax server and pass the function which handles new incoming socket connections. 83 | s.ajaxSocketServer = ajaxsocket.NewServer(func(as *ajaxsocket.Socket) { 84 | s.triggerOnNewSocketConnection(as) 85 | }) 86 | 87 | return s 88 | } 89 | 90 | // OnNewSocketConnection sets the event function which is 91 | // triggered if a new socket connection was made. 92 | func (s *Server) OnNewSocketConnection(f func(BackendSocket)) { 93 | s.onNewSocketConnection = f 94 | } 95 | 96 | // ServeHTTP implements the HTTP Handler interface of the http package. 97 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 98 | // Get the URL path. 99 | path := r.URL.Path 100 | 101 | // Call this in an inline function to handle errors. 102 | statusCode, err := func() (int, error) { 103 | // Check the origin. 104 | if !s.checkOriginFunc(r) { 105 | return http.StatusForbidden, fmt.Errorf("origin not allowed") 106 | } 107 | 108 | // Set the required HTTP headers for cross origin requests if enabled. 109 | if s.enableCORS { 110 | // Parse the origin url. 111 | origin := r.Header["Origin"] 112 | if len(origin) == 0 || len(origin[0]) == 0 { 113 | return 400, fmt.Errorf("failed to set header: Access-Control-Allow-Origin: HTTP request origin header is empty") 114 | } 115 | 116 | w.Header().Set("Access-Control-Allow-Origin", origin[0]) // Set allowed origin. 117 | w.Header().Set("Access-Control-Allow-Methods", "POST,GET") // Only allow POST and GET requests. 118 | } 119 | 120 | // Strip the base URL. 121 | if len(path) < s.httpURLStripLength { 122 | return http.StatusBadRequest, fmt.Errorf("invalid request") 123 | } 124 | path = path[s.httpURLStripLength:] 125 | 126 | // Route the HTTP request in a very simple way by comparing the strings. 127 | if path == httpURLWebSocketSuffix { 128 | // Handle the websocket request. 129 | s.webSocketServer.HandleRequest(w, r) 130 | } else if path == httpURLAjaxSocketSuffix { 131 | // Handle the ajax request. 132 | s.ajaxSocketServer.HandleRequest(w, r) 133 | } else { 134 | return http.StatusBadRequest, fmt.Errorf("invalid request") 135 | } 136 | 137 | return http.StatusAccepted, nil 138 | }() 139 | 140 | // Handle the error. 141 | if err != nil { 142 | // Set the HTTP status code. 143 | w.WriteHeader(statusCode) 144 | 145 | // Get the remote address and user agent. 146 | remoteAddr, _ := utils.RemoteAddress(r) 147 | userAgent := r.Header.Get("User-Agent") 148 | 149 | // Log the invalid request. 150 | log.L.WithFields(logrus.Fields{ 151 | "remoteAddress": remoteAddr, 152 | "userAgent": userAgent, 153 | "url": r.URL.Path, 154 | }).Warningf("handle HTTP request: %v", err) 155 | } 156 | } 157 | 158 | //################################// 159 | //### Backend Server - Private ###// 160 | //################################// 161 | 162 | func (s *Server) triggerOnNewSocketConnection(bs BackendSocket) { 163 | // Trigger the on new socket connection event in a new goroutine 164 | // to not block any socket functions. Otherwise this might block HTTP handlers. 165 | go s.onNewSocketConnection(bs) 166 | } 167 | -------------------------------------------------------------------------------- /backend/socket.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package backend 20 | 21 | import "github.com/desertbit/glue/backend/global" 22 | 23 | //################################// 24 | //### Backend Socket Interface ###// 25 | //################################// 26 | 27 | type BackendSocket interface { 28 | Type() global.SocketType 29 | RemoteAddr() string 30 | UserAgent() string 31 | 32 | Close() 33 | IsClosed() bool 34 | ClosedChan() <-chan struct{} 35 | 36 | WriteChan() chan string 37 | ReadChan() chan string 38 | } 39 | -------------------------------------------------------------------------------- /backend/sockets/ajaxsocket/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Package ajaxsocket provides the ajax socket implementation. 20 | package ajaxsocket 21 | 22 | import ( 23 | "io" 24 | "io/ioutil" 25 | "net/http" 26 | "strings" 27 | "sync" 28 | "time" 29 | 30 | "github.com/sirupsen/logrus" 31 | "github.com/desertbit/glue/log" 32 | "github.com/desertbit/glue/utils" 33 | ) 34 | 35 | //#################// 36 | //### Constants ###// 37 | //#################// 38 | 39 | const ( 40 | ajaxPollTimeout = 35 * time.Second 41 | ajaxUIDLength = 10 42 | ajaxPollTokenLength = 7 43 | 44 | // Ajax poll data commands: 45 | ajaxPollCmdTimeout = "t" 46 | ajaxPollCmdClosed = "c" 47 | 48 | // Ajax protocol commands: 49 | ajaxSocketDataDelimiter = "&" 50 | ajaxSocketDataKeyLength = 1 51 | ajaxSocketDataKeyInit = "i" 52 | ajaxSocketDataKeyPush = "u" 53 | ajaxSocketDataKeyPoll = "o" 54 | ) 55 | 56 | //########################// 57 | //### Ajax Server type ###// 58 | //##################äääää#// 59 | 60 | type Server struct { 61 | sockets map[string]*Socket 62 | socketsMutex sync.Mutex 63 | 64 | onNewSocketConnection func(*Socket) 65 | } 66 | 67 | func NewServer(onNewSocketConnectionFunc func(*Socket)) *Server { 68 | return &Server{ 69 | sockets: make(map[string]*Socket), 70 | onNewSocketConnection: onNewSocketConnectionFunc, 71 | } 72 | } 73 | 74 | func (s *Server) HandleRequest(w http.ResponseWriter, req *http.Request) { 75 | // Get the remote address and user agent. 76 | remoteAddr, _ := utils.RemoteAddress(req) 77 | userAgent := req.Header.Get("User-Agent") 78 | 79 | // Get the request body data. 80 | body, err := ioutil.ReadAll(req.Body) 81 | if err != nil { 82 | log.L.WithFields(logrus.Fields{ 83 | "remoteAddress": remoteAddr, 84 | "userAgent": userAgent, 85 | }).Warningf("failed to read ajax request body: %v", err) 86 | 87 | http.Error(w, "Internal Server Error", 500) 88 | return 89 | } 90 | 91 | // Check for bad requests. 92 | if req.Method != "POST" { 93 | log.L.WithFields(logrus.Fields{ 94 | "remoteAddress": remoteAddr, 95 | "userAgent": userAgent, 96 | }).Warningf("client accessed the ajax interface with an invalid http method: %s", req.Method) 97 | 98 | http.Error(w, "Bad Request", 400) 99 | return 100 | } 101 | 102 | // Get the request body as string. 103 | data := string(body) 104 | 105 | // Get the head of the body data delimited by an delimiter. 106 | var head string 107 | i := strings.Index(data, ajaxSocketDataDelimiter) 108 | if i < 0 { 109 | // There is no delimiter. The complete data is the head. 110 | head = data 111 | data = "" 112 | } else { 113 | // Extract the head. 114 | head = data[:i] 115 | data = data[i+1:] 116 | } 117 | 118 | // Validate the head length. 119 | if len(head) < ajaxSocketDataKeyLength { 120 | log.L.WithFields(logrus.Fields{ 121 | "remoteAddress": remoteAddr, 122 | "userAgent": userAgent, 123 | }).Warningf("ajax: head data is too short: '%s'", head) 124 | 125 | http.Error(w, "Bad Request", 400) 126 | return 127 | } 128 | 129 | // The head is split into key and value. 130 | key := head[:ajaxSocketDataKeyLength] 131 | value := head[ajaxSocketDataKeyLength:] 132 | 133 | // Handle the specific request. 134 | switch key { 135 | case ajaxSocketDataKeyInit: 136 | s.initAjaxRequest(remoteAddr, userAgent, w) 137 | case ajaxSocketDataKeyPoll: 138 | s.pollAjaxRequest(value, remoteAddr, userAgent, data, w) 139 | case ajaxSocketDataKeyPush: 140 | s.pushAjaxRequest(value, remoteAddr, userAgent, data, w) 141 | default: 142 | log.L.WithFields(logrus.Fields{ 143 | "remoteAddress": remoteAddr, 144 | "userAgent": userAgent, 145 | "key": key, 146 | "value": value, 147 | }).Warningf("ajax: invalid request.") 148 | 149 | http.Error(w, "Bad Request", 400) 150 | return 151 | } 152 | } 153 | 154 | func (s *Server) initAjaxRequest(remoteAddr, userAgent string, w http.ResponseWriter) { 155 | var uid string 156 | 157 | // Create a new ajax socket value. 158 | a := newSocket(s) 159 | a.remoteAddr = remoteAddr 160 | a.userAgent = userAgent 161 | 162 | func() { 163 | // Lock the mutex 164 | s.socketsMutex.Lock() 165 | defer s.socketsMutex.Unlock() 166 | 167 | // Obtain a new unique ID. 168 | for { 169 | // Generate it. 170 | uid = utils.RandomString(ajaxUIDLength) 171 | 172 | // Check if the new UID is already used. 173 | // This is very unlikely, but we have to check this! 174 | _, ok := s.sockets[uid] 175 | if !ok { 176 | // Break the loop if the UID is unique. 177 | break 178 | } 179 | } 180 | 181 | // Set the UID. 182 | a.uid = uid 183 | 184 | // Add the new ajax socket to the map. 185 | s.sockets[uid] = a 186 | }() 187 | 188 | // Create a new poll token. 189 | a.pollToken = utils.RandomString(ajaxPollTokenLength) 190 | 191 | // Tell the client the UID and poll token. 192 | io.WriteString(w, uid+ajaxSocketDataDelimiter+a.pollToken) 193 | 194 | // Trigger the event that a new socket connection was made. 195 | s.onNewSocketConnection(a) 196 | } 197 | 198 | func (s *Server) pushAjaxRequest(uid, remoteAddr, userAgent, data string, w http.ResponseWriter) { 199 | // Obtain the ajax socket with the uid. 200 | a := func() *Socket { 201 | // Lock the mutex. 202 | s.socketsMutex.Lock() 203 | defer s.socketsMutex.Unlock() 204 | 205 | // Obtain the ajax socket with the uid- 206 | a, ok := s.sockets[uid] 207 | if !ok { 208 | return nil 209 | } 210 | return a 211 | }() 212 | 213 | if a == nil { 214 | log.L.WithFields(logrus.Fields{ 215 | "remoteAddress": remoteAddr, 216 | "userAgent": userAgent, 217 | "uid": uid, 218 | }).Warningf("ajax: client requested an invalid ajax socket: uid is invalid!") 219 | 220 | http.Error(w, "Bad Request", 400) 221 | return 222 | } 223 | 224 | // The user agents have to match. 225 | if a.userAgent != userAgent { 226 | log.L.WithFields(logrus.Fields{ 227 | "remoteAddress": remoteAddr, 228 | "userAgent": userAgent, 229 | "uid": uid, 230 | "clientUserAgent": userAgent, 231 | "socketUserAgent": a.userAgent, 232 | }).Warningf("ajax: client push request: user agents do not match!") 233 | 234 | http.Error(w, "Bad Request", 400) 235 | return 236 | } 237 | 238 | // Check if the push request was called with no data. 239 | if len(data) == 0 { 240 | log.L.WithFields(logrus.Fields{ 241 | "remoteAddress": remoteAddr, 242 | "userAgent": userAgent, 243 | "uid": uid, 244 | }).Warningf("ajax: client push request with no data!") 245 | 246 | http.Error(w, "Bad Request", 400) 247 | return 248 | } 249 | 250 | // Update the remote address. The client might be behind a proxy. 251 | a.remoteAddr = remoteAddr 252 | 253 | // Write the received data to the read channel. 254 | a.readChan <- data 255 | } 256 | 257 | func (s *Server) pollAjaxRequest(uid, remoteAddr, userAgent, data string, w http.ResponseWriter) { 258 | // Obtain the ajax socket with the uid. 259 | a := func() *Socket { 260 | // Lock the mutex. 261 | s.socketsMutex.Lock() 262 | defer s.socketsMutex.Unlock() 263 | 264 | // Obtain the ajax socket with the uid- 265 | a, ok := s.sockets[uid] 266 | if !ok { 267 | return nil 268 | } 269 | return a 270 | }() 271 | 272 | if a == nil { 273 | log.L.WithFields(logrus.Fields{ 274 | "remoteAddress": remoteAddr, 275 | "userAgent": userAgent, 276 | "uid": uid, 277 | }).Warningf("ajax: client requested an invalid ajax socket: uid is invalid!") 278 | 279 | http.Error(w, "Bad Request", 400) 280 | return 281 | } 282 | 283 | // The user agents have to match. 284 | if a.userAgent != userAgent { 285 | log.L.WithFields(logrus.Fields{ 286 | "remoteAddress": remoteAddr, 287 | "userAgent": userAgent, 288 | "uid": uid, 289 | "clientUserAgent": userAgent, 290 | "socketUserAgent": a.userAgent, 291 | }).Warningf("ajax: client poll request: user agents do not match!") 292 | 293 | http.Error(w, "Bad Request", 400) 294 | return 295 | } 296 | 297 | // Check if the poll tokens matches. 298 | // The poll token is the data value. 299 | if a.pollToken != data { 300 | log.L.WithFields(logrus.Fields{ 301 | "remoteAddress": remoteAddr, 302 | "userAgent": userAgent, 303 | "uid": uid, 304 | "clientPollToken": data, 305 | "socketPollToken": a.pollToken, 306 | }).Warningf("ajax: client poll request: poll tokens do not match!") 307 | 308 | http.Error(w, "Bad Request", 400) 309 | return 310 | } 311 | 312 | // Create a new poll token. 313 | a.pollToken = utils.RandomString(ajaxPollTokenLength) 314 | 315 | // Create a timeout timer for the poll. 316 | timeout := time.NewTimer(ajaxPollTimeout) 317 | 318 | defer func() { 319 | // Stop the timeout timer. 320 | timeout.Stop() 321 | }() 322 | 323 | // Send messages as soon as there are some available. 324 | select { 325 | case data := <-a.writeChan: 326 | // Send the new poll token and message data to the client. 327 | io.WriteString(w, a.pollToken+ajaxSocketDataDelimiter+data) 328 | case <-timeout.C: 329 | // Tell the client that this ajax connection has reached the timeout. 330 | io.WriteString(w, ajaxPollCmdTimeout) 331 | case <-a.closer.IsClosedChan: 332 | // Tell the client that this ajax connection is closed. 333 | io.WriteString(w, ajaxPollCmdClosed) 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /backend/sockets/ajaxsocket/socket.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package ajaxsocket 20 | 21 | import ( 22 | "github.com/desertbit/glue/backend/closer" 23 | "github.com/desertbit/glue/backend/global" 24 | ) 25 | 26 | //########################// 27 | //### Ajax Socket type ###// 28 | //########################// 29 | 30 | type Socket struct { 31 | uid string 32 | pollToken string 33 | userAgent string 34 | remoteAddr string 35 | 36 | closer *closer.Closer 37 | 38 | writeChan chan string 39 | readChan chan string 40 | } 41 | 42 | // Create a new ajax socket. 43 | func newSocket(s *Server) *Socket { 44 | a := &Socket{ 45 | writeChan: make(chan string, global.WriteChanSize), 46 | readChan: make(chan string, global.ReadChanSize), 47 | } 48 | 49 | // Set the closer function. 50 | a.closer = closer.New(func() { 51 | // Remove the ajax socket from the map. 52 | if len(a.uid) > 0 { 53 | func() { 54 | s.socketsMutex.Lock() 55 | defer s.socketsMutex.Unlock() 56 | 57 | delete(s.sockets, a.uid) 58 | }() 59 | } 60 | }) 61 | 62 | return a 63 | } 64 | 65 | //##############################################// 66 | //### Ajax Socket - Interface implementation ###// 67 | //##############################################// 68 | 69 | func (s *Socket) Type() global.SocketType { 70 | return global.TypeAjaxSocket 71 | } 72 | 73 | func (s *Socket) RemoteAddr() string { 74 | return s.remoteAddr 75 | } 76 | 77 | func (s *Socket) UserAgent() string { 78 | return s.userAgent 79 | } 80 | 81 | func (s *Socket) Close() { 82 | s.closer.Close() 83 | } 84 | 85 | func (s *Socket) IsClosed() bool { 86 | return s.closer.IsClosed() 87 | } 88 | 89 | func (s *Socket) ClosedChan() <-chan struct{} { 90 | return s.closer.IsClosedChan 91 | } 92 | 93 | func (s *Socket) WriteChan() chan string { 94 | return s.writeChan 95 | } 96 | 97 | func (s *Socket) ReadChan() chan string { 98 | return s.readChan 99 | } 100 | -------------------------------------------------------------------------------- /backend/sockets/websocket/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package websocket 20 | 21 | import ( 22 | "net/http" 23 | 24 | "github.com/sirupsen/logrus" 25 | "github.com/desertbit/glue/log" 26 | "github.com/desertbit/glue/utils" 27 | "github.com/gorilla/websocket" 28 | ) 29 | 30 | //#############################// 31 | //### WebSocket Server type ###// 32 | //##################äää#####ää#// 33 | 34 | type Server struct { 35 | // Websocket upgrader 36 | upgrader websocket.Upgrader 37 | 38 | onNewSocketConnection func(*Socket) 39 | } 40 | 41 | func NewServer(onNewSocketConnectionFunc func(*Socket)) *Server { 42 | return &Server{ 43 | upgrader: websocket.Upgrader{ 44 | ReadBufferSize: 1024, 45 | WriteBufferSize: 1024, 46 | // Don't check the origin. This is done by the backend server package. 47 | CheckOrigin: func(r *http.Request) bool { return true }, 48 | }, 49 | 50 | onNewSocketConnection: onNewSocketConnectionFunc, 51 | } 52 | } 53 | 54 | func (s *Server) HandleRequest(rw http.ResponseWriter, req *http.Request) { 55 | // Get the remote address and user agent. 56 | remoteAddr, requestRemoteAddrMethodUsed := utils.RemoteAddress(req) 57 | userAgent := req.Header.Get("User-Agent") 58 | 59 | // This has to be a GET request. 60 | if req.Method != "GET" { 61 | log.L.WithFields(logrus.Fields{ 62 | "remoteAddress": remoteAddr, 63 | "userAgent": userAgent, 64 | "method": req.Method, 65 | }).Warning("client accessed websocket handler with an invalid request method") 66 | 67 | http.Error(rw, "Method not allowed", 405) 68 | return 69 | } 70 | 71 | // Upgrade to a websocket. 72 | ws, err := s.upgrader.Upgrade(rw, req, nil) 73 | if err != nil { 74 | log.L.WithFields(logrus.Fields{ 75 | "remoteAddress": remoteAddr, 76 | "userAgent": userAgent, 77 | }).Warningf("failed to upgrade to websocket layer: %v", err) 78 | 79 | http.Error(rw, "Bad Request", 400) 80 | return 81 | } 82 | 83 | // Create a new websocket value. 84 | w := newSocket(ws) 85 | 86 | // Set the user agent. 87 | w.userAgent = userAgent 88 | 89 | // Set the remote address get function. 90 | if requestRemoteAddrMethodUsed { 91 | // Obtain the remote address from the websocket directly. 92 | w.remoteAddrFunc = func() string { 93 | return utils.RemovePortFromRemoteAddr(w.ws.RemoteAddr().String()) 94 | } 95 | } else { 96 | // Obtain the remote address from the current string. 97 | // It was obtained using the request Headers. So don't use the 98 | // websocket RemoteAddr() method, because it does not return 99 | // the clients IP address. 100 | w.remoteAddrFunc = func() string { 101 | return remoteAddr 102 | } 103 | } 104 | 105 | // Start the handlers in new goroutines. 106 | go w.writeLoop() 107 | go w.readLoop() 108 | 109 | // Trigger the event that a new socket connection was made. 110 | s.onNewSocketConnection(w) 111 | } 112 | -------------------------------------------------------------------------------- /backend/sockets/websocket/socket.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package websocket 20 | 21 | import ( 22 | "io" 23 | "sync" 24 | "time" 25 | 26 | "github.com/desertbit/glue/backend/closer" 27 | "github.com/desertbit/glue/backend/global" 28 | "github.com/desertbit/glue/log" 29 | 30 | "github.com/sirupsen/logrus" 31 | "github.com/gorilla/websocket" 32 | ) 33 | 34 | //#################// 35 | //### Constants ###// 36 | //#################// 37 | 38 | const ( 39 | // Time allowed to write a message to the peer. 40 | writeWait = 10 * time.Second 41 | 42 | // Time allowed to read the next message from the peer. 43 | readWait = 60 * time.Second 44 | 45 | // Maximum message size allowed from peer. 46 | maxMessageSize = 0 47 | ) 48 | 49 | //######################// 50 | //### WebSocket type ###// 51 | //######################// 52 | 53 | type Socket struct { 54 | ws *websocket.Conn 55 | writeMutex sync.Mutex 56 | 57 | closer *closer.Closer 58 | 59 | writeChan chan string 60 | readChan chan string 61 | 62 | userAgent string 63 | remoteAddrFunc func() string 64 | } 65 | 66 | // Create a new websocket value. 67 | func newSocket(ws *websocket.Conn) *Socket { 68 | w := &Socket{ 69 | ws: ws, 70 | writeChan: make(chan string, global.WriteChanSize), 71 | readChan: make(chan string, global.ReadChanSize), 72 | } 73 | 74 | // Set the closer function. 75 | w.closer = closer.New(func() { 76 | // Send a close message to the client. 77 | // Ignore errors. 78 | w.write(websocket.CloseMessage, []byte{}) 79 | 80 | // Close the socket. 81 | w.ws.Close() 82 | }) 83 | 84 | return w 85 | } 86 | 87 | //############################################// 88 | //### WebSocket - Interface implementation ###// 89 | //############################################// 90 | 91 | func (w *Socket) Type() global.SocketType { 92 | return global.TypeWebSocket 93 | } 94 | 95 | func (w *Socket) RemoteAddr() string { 96 | return w.remoteAddrFunc() 97 | } 98 | 99 | func (w *Socket) UserAgent() string { 100 | return w.userAgent 101 | } 102 | 103 | func (w *Socket) Close() { 104 | w.closer.Close() 105 | } 106 | 107 | func (w *Socket) IsClosed() bool { 108 | return w.closer.IsClosed() 109 | } 110 | 111 | func (w *Socket) ClosedChan() <-chan struct{} { 112 | return w.closer.IsClosedChan 113 | } 114 | 115 | func (w *Socket) WriteChan() chan string { 116 | return w.writeChan 117 | } 118 | 119 | func (w *Socket) ReadChan() chan string { 120 | return w.readChan 121 | } 122 | 123 | //###########################// 124 | //### WebSocket - Private ###// 125 | //###########################// 126 | 127 | // readLoop reads messages from the websocket 128 | func (w *Socket) readLoop() { 129 | defer func() { 130 | // Close the socket on defer. 131 | w.Close() 132 | }() 133 | 134 | // Set the limits. 135 | w.ws.SetReadLimit(maxMessageSize) 136 | 137 | // Set the pong handler. 138 | w.ws.SetPongHandler(func(string) error { 139 | // Reset the read deadline. 140 | w.ws.SetReadDeadline(time.Now().Add(readWait)) 141 | return nil 142 | }) 143 | 144 | for { 145 | // Reset the read deadline. 146 | w.ws.SetReadDeadline(time.Now().Add(readWait)) 147 | 148 | // Read from the websocket. 149 | _, data, err := w.ws.ReadMessage() 150 | if err != nil { 151 | // Websocket close code. 152 | wsCode := -1 // -1 for not set. 153 | 154 | // Try to obtain the websocket close code if present. 155 | // Assert to gorilla websocket CloseError type if possible. 156 | if closeErr, ok := err.(*websocket.CloseError); ok { 157 | wsCode = closeErr.Code 158 | } 159 | 160 | // Only log errors if this is not EOF and 161 | // if the socket was not closed already. 162 | // Also check the websocket close code. 163 | if err != io.EOF && !w.IsClosed() && 164 | wsCode != websocket.CloseNormalClosure && 165 | wsCode != websocket.CloseGoingAway && 166 | wsCode != websocket.CloseNoStatusReceived { 167 | // Log 168 | log.L.WithFields(logrus.Fields{ 169 | "remoteAddress": w.RemoteAddr(), 170 | "userAgent": w.UserAgent(), 171 | }).Warningf("failed to read data from websocket: %v", err) 172 | } 173 | 174 | // Return and release this goroutine. 175 | // This will close this socket connection. 176 | return 177 | } 178 | 179 | // Write the received data to the read channel. 180 | w.readChan <- string(data) 181 | } 182 | } 183 | 184 | // write writes a message with the given message type and payload. 185 | // This method is thread-safe. 186 | func (w *Socket) write(mt int, payload []byte) error { 187 | w.writeMutex.Lock() 188 | defer w.writeMutex.Unlock() 189 | 190 | w.ws.SetWriteDeadline(time.Now().Add(writeWait)) 191 | return w.ws.WriteMessage(mt, payload) 192 | } 193 | 194 | func (w *Socket) writeLoop() { 195 | for { 196 | select { 197 | case data := <-w.writeChan: 198 | // Write the data to the websocket. 199 | err := w.write(websocket.TextMessage, []byte(data)) 200 | if err != nil { 201 | log.L.WithFields(logrus.Fields{ 202 | "remoteAddress": w.RemoteAddr(), 203 | "userAgent": w.UserAgent(), 204 | }).Warningf("failed to write to websocket: %v", err) 205 | 206 | // Close the websocket on error. 207 | w.Close() 208 | return 209 | } 210 | 211 | case <-w.closer.IsClosedChan: 212 | // Just release this loop. 213 | return 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glue", 3 | "version": "1.9.1", 4 | "homepage": "https://github.com/desertbit/glue", 5 | "authors": [ 6 | "Roland Singer " 7 | ], 8 | "description": "Robust Go and Javascript Socket Library", 9 | "main": "[\"client\"]", 10 | "keywords": [ 11 | "[\"socket\"]" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | "**/*", 16 | "!client/*", 17 | "!client/**/*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package glue 20 | 21 | import ( 22 | "fmt" 23 | "runtime/debug" 24 | "sync" 25 | "time" 26 | 27 | "github.com/desertbit/glue/log" 28 | "github.com/desertbit/glue/utils" 29 | ) 30 | 31 | //#################// 32 | //### Constants ###// 33 | //#################// 34 | 35 | const ( 36 | // The channel buffer size for received data. 37 | readChanBuffer = 7 38 | ) 39 | 40 | //####################// 41 | //### Channel type ###// 42 | //####################// 43 | 44 | // A Channel is a separate communication channel. 45 | type Channel struct { 46 | s *Socket 47 | readHandler *handler 48 | 49 | name string 50 | readChan chan string 51 | } 52 | 53 | func newChannel(s *Socket, name string) *Channel { 54 | return &Channel{ 55 | s: s, 56 | readHandler: newHandler(), 57 | name: name, 58 | readChan: make(chan string, readChanBuffer), 59 | } 60 | } 61 | 62 | // Socket returns the channel's socket. 63 | func (c *Channel) Socket() *Socket { 64 | return c.s 65 | } 66 | 67 | // Write data to the channel. 68 | func (c *Channel) Write(data string) { 69 | // Prepend the socket command and send the channel name and data. 70 | c.s.write(cmdChannelData + utils.MarshalValues(c.name, data)) 71 | } 72 | 73 | // Read the next message from the channel. This method is blocking. 74 | // One variadic argument sets a timeout duration. 75 | // If no timeout is specified, this method will block forever. 76 | // ErrSocketClosed is returned, if the socket connection is closed. 77 | // ErrReadTimeout is returned, if the timeout is reached. 78 | func (c *Channel) Read(timeout ...time.Duration) (string, error) { 79 | timeoutChan := make(chan (struct{})) 80 | 81 | // Create a timeout timer if a timeout is specified. 82 | if len(timeout) > 0 && timeout[0] > 0 { 83 | timer := time.AfterFunc(timeout[0], func() { 84 | // Trigger the timeout by closing the channel. 85 | close(timeoutChan) 86 | }) 87 | 88 | // Always stop the timer on defer. 89 | defer timer.Stop() 90 | } 91 | 92 | select { 93 | case data := <-c.readChan: 94 | return data, nil 95 | case <-c.s.isClosedChan: 96 | // The connection was closed. 97 | // Return an error. 98 | return "", ErrSocketClosed 99 | case <-timeoutChan: 100 | // The timeout was reached. 101 | // Return an error. 102 | return "", ErrReadTimeout 103 | } 104 | } 105 | 106 | // OnRead sets the function which is triggered if new data is received on the channel. 107 | // If this event function based method of reading data from the socket is used, 108 | // then don't use the socket Read method. 109 | // Either use the OnRead or the Read approach. 110 | func (c *Channel) OnRead(f OnReadFunc) { 111 | // Create a new read handler for this channel. 112 | // Previous handlers are stopped first. 113 | handlerStopped := c.readHandler.New() 114 | 115 | // Start the handler goroutine. 116 | go func() { 117 | for { 118 | select { 119 | case data := <-c.readChan: 120 | // Call the callback in a new goroutine. 121 | go func() { 122 | // Recover panics and log the error. 123 | defer func() { 124 | if e := recover(); e != nil { 125 | log.L.Errorf("glue: panic while calling onRead function: %v\n%s", e, debug.Stack()) 126 | } 127 | }() 128 | 129 | // Trigger the on read event function. 130 | f(data) 131 | }() 132 | case <-c.s.isClosedChan: 133 | // Release this goroutine if the socket is closed. 134 | return 135 | case <-handlerStopped: 136 | // Release this goroutine. 137 | return 138 | } 139 | } 140 | }() 141 | } 142 | 143 | // DiscardRead ignores and discars the data received from this channel. 144 | // Call this method during initialization, if you don't read any data from 145 | // this channel. If received data is not discarded, then the read buffer will block as soon 146 | // as it is full, which will also block the keep-alive mechanism of the socket. The result 147 | // would be a closed socket... 148 | func (c *Channel) DiscardRead() { 149 | // Create a new read handler for this channel. 150 | // Previous handlers are stopped first. 151 | handlerStopped := c.readHandler.New() 152 | 153 | // Start the handler goroutine. 154 | go func() { 155 | for { 156 | select { 157 | case <-c.readChan: 158 | // Don't do anything. 159 | // Just discard the data. 160 | case <-c.s.isClosedChan: 161 | // Release this goroutine if the socket is closed. 162 | return 163 | case <-handlerStopped: 164 | // Release this goroutine. 165 | return 166 | } 167 | } 168 | }() 169 | } 170 | 171 | func (c *Channel) triggerRead(data string) { 172 | // Send the data to the read channel. 173 | c.readChan <- data 174 | } 175 | 176 | //#####################// 177 | //### Channels type ###// 178 | //#####################// 179 | 180 | type channels struct { 181 | m map[string]*Channel 182 | mutex sync.Mutex 183 | } 184 | 185 | func newChannels() *channels { 186 | return &channels{ 187 | m: make(map[string]*Channel), 188 | } 189 | } 190 | 191 | func (cs *channels) get(name string) *Channel { 192 | // Lock the mutex. 193 | cs.mutex.Lock() 194 | defer cs.mutex.Unlock() 195 | 196 | return cs.m[name] 197 | } 198 | 199 | func (cs *channels) triggerReadForChannel(name, data string) error { 200 | // Get the channel. 201 | c := cs.get(name) 202 | if c == nil { 203 | return fmt.Errorf("received data for channel '%s': channel does not exists", name) 204 | } 205 | 206 | // Trigger the read. 207 | c.triggerRead(data) 208 | 209 | return nil 210 | } 211 | 212 | //#################################// 213 | //### Additional Socket Methods ###// 214 | //#################################// 215 | 216 | // Channel returns the corresponding channel value specified by the name. 217 | // If no channel value exists for the given name, a new channel is created. 218 | // Multiple calls to Channel with the same name, will always return the same 219 | // channel value pointer. 220 | func (s *Socket) Channel(name string) *Channel { 221 | // Get the socket channel pointer. 222 | cs := s.channels 223 | 224 | // Lock the mutex. 225 | cs.mutex.Lock() 226 | defer cs.mutex.Unlock() 227 | 228 | // Get the channel if it exists. 229 | c, ok := cs.m[name] 230 | if ok { 231 | return c 232 | } 233 | 234 | // Create and add the new channel to the socket channels map. 235 | c = newChannel(s, name) 236 | cs.m[name] = c 237 | 238 | return c 239 | } 240 | -------------------------------------------------------------------------------- /client/dist/glue.js: -------------------------------------------------------------------------------- 1 | var glue=function(e,n){"use strict";function t(e){return e?o(e):void 0}function o(e){for(var n in t.prototype)e[n]=t.prototype[n];return e}"undefined"!=typeof module&&(module.exports=t),t.prototype.on=t.prototype.addEventListener=function(e,n){return this._callbacks=this._callbacks||{},(this._callbacks["$"+e]=this._callbacks["$"+e]||[]).push(n),this},t.prototype.once=function(e,n){function t(){this.off(e,t),n.apply(this,arguments)}return t.fn=n,this.on(e,t),this},t.prototype.off=t.prototype.removeListener=t.prototype.removeAllListeners=t.prototype.removeEventListener=function(e,n){if(this._callbacks=this._callbacks||{},0===arguments.length)return this._callbacks={},this;var t=this._callbacks["$"+e];if(!t)return this;if(1==arguments.length)return delete this._callbacks["$"+e],this;for(var o,i=0;io;++o)t[o].apply(this,n)}return this},t.prototype.listeners=function(e){return this._callbacks=this._callbacks||{},this._callbacks["$"+e]||[]},t.prototype.hasListeners=function(e){return!!this.listeners(e).length};var i,c,r,s,a,u,l,f,d=function(){var t,o={};return o.open=function(){try{var i;i=e.match("^https://")?"wss"+e.substr(5):"ws"+e.substr(4),i+=n.baseURL+"ws",t=new WebSocket(i),t.onmessage=function(e){o.onMessage(e.data.toString())},t.onerror=function(e){var n="the websocket closed the connection with ";n+=e.code?"the error code: "+e.code:"an error.",o.onError(n)},t.onclose=function(){o.onClose()},t.onopen=function(){o.onOpen()}}catch(c){o.onError()}},o.send=function(e){t.send(e)},o.reset=function(){t&&t.close(),t=void 0},o},h=function(){var t,o,i,c=e+n.baseURL+"ajax",r=8e3,s=45e3,a={Timeout:"t",Closed:"c"},u={Delimiter:"&",Init:"i",Push:"u",Poll:"o"},l={},f=!1,d=!1,h=function(){i=function(){},f&&f.abort(),d&&d.abort()},v=function(e,n,t,o,i){var c=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");return c.onload=function(){o(c.response)},c.onerror=function(){i()},c.ontimeout=function(){i("timeout")},c.open("POST",e,!0),c.responseType="text",c.timeout=n,c.send(t),c},g=function(){h(),l.onClose()},p=function(e){h(),e=e?"the ajax socket closed the connection with the error: "+e:"the ajax socket closed the connection with an error.",l.onError(e)},m=function(e,n){d=v(c,r,e,function(e){d=!1,n&&n(e)},function(e){d=!1,p(e)})};return i=function(){var e=u.Poll+t+u.Delimiter+o;f=v(c,s,e,function(e){if(f=!1,e==a.Timeout)return void i();if(e==a.Closed)return void g();var n=e.indexOf(u.Delimiter);return 0>n?void p("ajax socket: failed to split poll token from data!"):(o=e.substring(0,n),e=e.substr(n+1),i(),void l.onMessage(e))},function(e){f=!1,p(e)})},l.open=function(){m(u.Init,function(e){var n=e.indexOf(u.Delimiter);return 0>n?void p("ajax socket: failed to split uid and poll token from data!"):(t=e.substring(0,n),o=e.substr(n+1),i(),void l.onOpen())})},l.send=function(e){m(u.Push+t+u.Delimiter+e)},l.reset=function(){h()},l},v="1.9.1",g="m",p={WebSocket:"WebSocket",AjaxSocket:"AjaxSocket"},m={Len:2,Init:"in",Ping:"pi",Pong:"po",Close:"cl",Invalid:"iv",DontAutoReconnect:"dr",ChannelData:"cd"},b={Disconnected:"disconnected",Connecting:"connecting",Reconnecting:"reconnecting",Connected:"connected"},k={baseURL:"/glue/",forceSocketType:!1,connectTimeout:1e4,pingInterval:35e3,pingReconnectTimeout:5e3,reconnect:!0,reconnectDelay:1e3,reconnectDelayMax:5e3,reconnectAttempts:10,resetSendBufferTimeout:1e4},y=new t,D=!1,T=!1,w=b.Disconnected,x=0,M=!1,L=!1,S=!1,C=!1,_=[],R=!1,O=!1,I=!1,P=[],j="",A=function(){var e="&",n={};return n.extend=function(){for(var e=1;eo||o>n.length)return!1;var i=n.substr(0,o),c=n.substr(o);return{first:i,second:c}},n.marshalValues=function(n,t){return String(n.length)+e+n+t},n}(),U=function(){var e={},n={},t=function(e){var n={onMessageFunc:function(){}};return n.instance={onMessage:function(e){n.onMessageFunc=e},send:function(n,t){return n?u(m.ChannelData,A.marshalValues(e,n),t):-1}},n};return e.get=function(e){if(!e)return!1;var o=n[e];return o?o.instance:(o=t(e),n[e]=o,o.instance)},e.emitOnMessage=function(e,t){if(e&&t){var o=n[e];if(!o)return void console.log("glue: channel '"+e+"': emit onMessage event: channel does not exists");try{o.onMessageFunc(t)}catch(i){return void console.log("glue: channel '"+e+"': onMessage event call failed: "+i.message)}}},e}();a=function(e){return D?I?void D.send(e):void P.push(e):void 0};var E=function(){if(0!==P.length){for(var e=0;e1?(c=h,D=c(),void(r=p.AjaxSocket)):(!n.forceSocketType&&window.WebSocket||n.forceSocketType===p.WebSocket?(c=d,r=p.WebSocket):(c=h,r=p.AjaxSocket),void(D=c()))},B=function(e){return e=JSON.parse(e),e.socketID?(j=e.socketID,I=!0,E(),w=b.Connected,f("connected"),void setTimeout(W,0)):(s(),void console.log("glue: socket initialization failed: invalid initialization data received"))},J=function(){z(),D.onOpen=function(){$(),x=0,T=!0,H();var e={version:v};e=JSON.stringify(e),D.send(m.Init+e)},D.onClose=function(){l()},D.onError=function(e){f("error",[e]),l()},D.onMessage=function(e){if(H(),e.length0?(w=b.Reconnecting,f("reconnecting")):(w=b.Connecting,f("connecting")),V(),D.open()},0)},N=function(){$(),X(),I=!1,j="",P=[],D&&(D.onOpen=D.onClose=D.onMessage=D.onError=function(){},D.reset(),D=!1)};if(l=function(){if(N(),n.reconnectAttempts>0&&x>n.reconnectAttempts||n.reconnect===!1||M)return w=b.Disconnected,void f("disconnected");x+=1;var e=n.reconnectDelay*x;e>n.reconnectDelayMax&&(e=n.reconnectDelayMax),setTimeout(function(){J()},e)},s=function(){D&&(a(m.Close),N(),w=b.Disconnected,f("disconnected"))},i=U.get(g),e||(e=window.location.protocol+"//"+window.location.host),!e.match("^http://")&&!e.match("^https://"))return void console.log("glue: invalid host: missing 'http://' or 'https://'!");n=A.extend({},k,n),n.reconnectDelayMax", 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /client/src/ajaxsocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /* 20 | * This code lives inside the glue function. 21 | */ 22 | 23 | 24 | var newAjaxSocket = function () { 25 | /* 26 | * Constants 27 | */ 28 | 29 | var ajaxHost = host + options.baseURL + "ajax", 30 | sendTimeout = 8000, 31 | pollTimeout = 45000; 32 | 33 | var PollCommands = { 34 | Timeout: "t", 35 | Closed: "c" 36 | }; 37 | 38 | var Commands = { 39 | Delimiter: "&", 40 | Init: "i", 41 | Push: "u", 42 | Poll: "o" 43 | }; 44 | 45 | 46 | 47 | /* 48 | * Variables 49 | */ 50 | 51 | var s = {}, 52 | uid, pollToken, 53 | pollXhr = false, 54 | sendXhr = false, 55 | poll; 56 | 57 | 58 | 59 | /* 60 | * Methods 61 | */ 62 | 63 | var stopRequests = function() { 64 | // Set the poll function to a dummy function. 65 | // This will prevent further poll calls. 66 | poll = function() {}; 67 | 68 | // Kill the ajax requests. 69 | if (pollXhr) { 70 | pollXhr.abort(); 71 | } 72 | if (sendXhr) { 73 | sendXhr.abort(); 74 | } 75 | }; 76 | 77 | var postAjax = function(url, timeout, data, success, error) { 78 | var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); 79 | 80 | xhr.onload = function() { 81 | success(xhr.response); 82 | }; 83 | 84 | xhr.onerror = function() { 85 | error(); 86 | }; 87 | 88 | xhr.ontimeout = function() { 89 | error("timeout"); 90 | }; 91 | 92 | xhr.open('POST', url, true); 93 | xhr.responseType = "text"; 94 | xhr.timeout = timeout; 95 | xhr.send(data); 96 | 97 | return xhr; 98 | }; 99 | 100 | var triggerClosed = function() { 101 | // Stop the ajax requests. 102 | stopRequests(); 103 | 104 | // Trigger the event. 105 | s.onClose(); 106 | }; 107 | 108 | var triggerError = function(msg) { 109 | // Stop the ajax requests. 110 | stopRequests(); 111 | 112 | // Create the error message. 113 | if (msg) { 114 | msg = "the ajax socket closed the connection with the error: " + msg; 115 | } 116 | else { 117 | msg = "the ajax socket closed the connection with an error."; 118 | } 119 | 120 | // Trigger the event. 121 | s.onError(msg); 122 | }; 123 | 124 | var send = function (data, callback) { 125 | sendXhr = postAjax(ajaxHost, sendTimeout, data, function (data) { 126 | sendXhr = false; 127 | 128 | if (callback) { 129 | callback(data); 130 | } 131 | }, function (msg) { 132 | sendXhr = false; 133 | triggerError(msg); 134 | }); 135 | }; 136 | 137 | poll = function () { 138 | var data = Commands.Poll + uid + Commands.Delimiter + pollToken; 139 | 140 | pollXhr = postAjax(ajaxHost, pollTimeout, data, function (data) { 141 | pollXhr = false; 142 | 143 | // Check if this jax request has reached the server's timeout. 144 | if (data == PollCommands.Timeout) { 145 | // Just start the next poll request. 146 | poll(); 147 | return; 148 | } 149 | 150 | // Check if this ajax connection was closed. 151 | if (data == PollCommands.Closed) { 152 | // Trigger the closed event. 153 | triggerClosed(); 154 | return; 155 | } 156 | 157 | // Split the new token from the rest of the data. 158 | var i = data.indexOf(Commands.Delimiter); 159 | if (i < 0) { 160 | triggerError("ajax socket: failed to split poll token from data!"); 161 | return; 162 | } 163 | 164 | // Set the new token and the data variable. 165 | pollToken = data.substring(0, i); 166 | data = data.substr(i + 1); 167 | 168 | // Start the next poll request. 169 | poll(); 170 | 171 | // Call the event. 172 | s.onMessage(data); 173 | }, function (msg) { 174 | pollXhr = false; 175 | triggerError(msg); 176 | }); 177 | }; 178 | 179 | 180 | 181 | /* 182 | * Socket layer implementation. 183 | */ 184 | 185 | s.open = function () { 186 | // Initialize the ajax socket session 187 | send(Commands.Init, function (data) { 188 | // Get the uid and token string 189 | var i = data.indexOf(Commands.Delimiter); 190 | if (i < 0) { 191 | triggerError("ajax socket: failed to split uid and poll token from data!"); 192 | return; 193 | } 194 | 195 | // Set the uid and token. 196 | uid = data.substring(0, i); 197 | pollToken = data.substr(i + 1); 198 | 199 | // Start the long polling process. 200 | poll(); 201 | 202 | // Trigger the event. 203 | s.onOpen(); 204 | }); 205 | }; 206 | 207 | s.send = function (data) { 208 | // Always prepend the command with the uid to the data. 209 | send(Commands.Push + uid + Commands.Delimiter + data); 210 | }; 211 | 212 | s.reset = function() { 213 | // Stop the ajax requests. 214 | stopRequests(); 215 | }; 216 | 217 | return s; 218 | }; 219 | -------------------------------------------------------------------------------- /client/src/channel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /* 20 | * This code lives inside the glue function. 21 | */ 22 | 23 | var channel = (function() { 24 | /* 25 | * Variables 26 | */ 27 | 28 | var instance = {}, // Our public instance object returned by this function. 29 | channels = {}; // Object as key value map. 30 | 31 | 32 | 33 | /* 34 | * Private Methods 35 | */ 36 | 37 | var newChannel = function(name) { 38 | // Create the channel object. 39 | var channel = { 40 | // Set to a dummy function. 41 | onMessageFunc: function() {} 42 | }; 43 | 44 | // Set the channel public instance object. 45 | // This is the value which is returned by the public glue.channel(...) function. 46 | channel.instance = { 47 | // onMessage sets the function which is triggered as soon as a message is received. 48 | onMessage: function(f) { 49 | channel.onMessageFunc = f; 50 | }, 51 | 52 | // send a data string to the channel. 53 | // One optional discard callback can be passed. 54 | // It is called if the data could not be send to the server. 55 | // The data is passed as first argument to the discard callback. 56 | // returns: 57 | // 1 if immediately send, 58 | // 0 if added to the send queue and 59 | // -1 if discarded. 60 | send: function(data, discardCallback) { 61 | // Discard empty data. 62 | if (!data) { 63 | return -1; 64 | } 65 | 66 | // Call the helper method and send the data to the channel. 67 | return sendBuffered(Commands.ChannelData, utils.marshalValues(name, data), discardCallback); 68 | } 69 | }; 70 | 71 | // Return the channel object. 72 | return channel; 73 | }; 74 | 75 | 76 | 77 | /* 78 | * Public Methods 79 | */ 80 | 81 | // Get or create a channel if it does not exists. 82 | instance.get = function(name) { 83 | if (!name) { 84 | return false; 85 | } 86 | 87 | // Get the channel. 88 | var c = channels[name]; 89 | if (c) { 90 | return c.instance; 91 | } 92 | 93 | // Create a new one, if it does not exists and add it to the map. 94 | c = newChannel(name); 95 | channels[name] = c; 96 | 97 | return c.instance; 98 | }; 99 | 100 | instance.emitOnMessage = function(name, data) { 101 | if (!name || !data) { 102 | return; 103 | } 104 | 105 | // Get the channel. 106 | var c = channels[name]; 107 | if (!c) { 108 | console.log("glue: channel '" + name + "': emit onMessage event: channel does not exists"); 109 | return; 110 | } 111 | 112 | // Call the channel's on message event. 113 | try { 114 | c.onMessageFunc(data); 115 | } 116 | catch(err) { 117 | console.log("glue: channel '" + name + "': onMessage event call failed: " + err.message); 118 | return; 119 | } 120 | }; 121 | 122 | return instance; 123 | })(); 124 | -------------------------------------------------------------------------------- /client/src/emitter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This code lives inside the glue function. 3 | */ 4 | 5 | // Source: https://github.com/component/emitter 6 | 7 | 8 | /** 9 | * Expose `Emitter`. 10 | */ 11 | 12 | if (typeof module !== 'undefined') { 13 | module.exports = Emitter; 14 | } 15 | 16 | /** 17 | * Initialize a new `Emitter`. 18 | * 19 | * @api public 20 | */ 21 | 22 | function Emitter(obj) { 23 | if (obj) return mixin(obj); 24 | } 25 | 26 | /** 27 | * Mixin the emitter properties. 28 | * 29 | * @param {Object} obj 30 | * @return {Object} 31 | * @api private 32 | */ 33 | 34 | function mixin(obj) { 35 | for (var key in Emitter.prototype) { 36 | obj[key] = Emitter.prototype[key]; 37 | } 38 | return obj; 39 | } 40 | 41 | /** 42 | * Listen on the given `event` with `fn`. 43 | * 44 | * @param {String} event 45 | * @param {Function} fn 46 | * @return {Emitter} 47 | * @api public 48 | */ 49 | 50 | Emitter.prototype.on = 51 | Emitter.prototype.addEventListener = function(event, fn){ 52 | this._callbacks = this._callbacks || {}; 53 | (this._callbacks['$' + event] = this._callbacks['$' + event] || []) 54 | .push(fn); 55 | return this; 56 | }; 57 | 58 | /** 59 | * Adds an `event` listener that will be invoked a single 60 | * time then automatically removed. 61 | * 62 | * @param {String} event 63 | * @param {Function} fn 64 | * @return {Emitter} 65 | * @api public 66 | */ 67 | 68 | Emitter.prototype.once = function(event, fn){ 69 | function on() { 70 | this.off(event, on); 71 | fn.apply(this, arguments); 72 | } 73 | 74 | on.fn = fn; 75 | this.on(event, on); 76 | return this; 77 | }; 78 | 79 | /** 80 | * Remove the given callback for `event` or all 81 | * registered callbacks. 82 | * 83 | * @param {String} event 84 | * @param {Function} fn 85 | * @return {Emitter} 86 | * @api public 87 | */ 88 | 89 | Emitter.prototype.off = 90 | Emitter.prototype.removeListener = 91 | Emitter.prototype.removeAllListeners = 92 | Emitter.prototype.removeEventListener = function(event, fn){ 93 | this._callbacks = this._callbacks || {}; 94 | 95 | // all 96 | if (0 === arguments.length) { 97 | this._callbacks = {}; 98 | return this; 99 | } 100 | 101 | // specific event 102 | var callbacks = this._callbacks['$' + event]; 103 | if (!callbacks) return this; 104 | 105 | // remove all handlers 106 | if (1 == arguments.length) { 107 | delete this._callbacks['$' + event]; 108 | return this; 109 | } 110 | 111 | // remove specific handler 112 | var cb; 113 | for (var i = 0; i < callbacks.length; i++) { 114 | cb = callbacks[i]; 115 | if (cb === fn || cb.fn === fn) { 116 | callbacks.splice(i, 1); 117 | break; 118 | } 119 | } 120 | return this; 121 | }; 122 | 123 | /** 124 | * Emit `event` with the given args. 125 | * 126 | * @param {String} event 127 | * @param {Mixed} ... 128 | * @return {Emitter} 129 | */ 130 | 131 | Emitter.prototype.emit = function(event){ 132 | this._callbacks = this._callbacks || {}; 133 | var args = [].slice.call(arguments, 1), callbacks = this._callbacks['$' + event]; 134 | 135 | if (callbacks) { 136 | callbacks = callbacks.slice(0); 137 | for (var i = 0, len = callbacks.length; i < len; ++i) { 138 | callbacks[i].apply(this, args); 139 | } 140 | } 141 | 142 | return this; 143 | }; 144 | 145 | /** 146 | * Return array of callbacks for `event`. 147 | * 148 | * @param {String} event 149 | * @return {Array} 150 | * @api public 151 | */ 152 | 153 | Emitter.prototype.listeners = function(event){ 154 | this._callbacks = this._callbacks || {}; 155 | return this._callbacks['$' + event] || []; 156 | }; 157 | 158 | /** 159 | * Check if this emitter has `event` handlers. 160 | * 161 | * @param {String} event 162 | * @return {Boolean} 163 | * @api public 164 | */ 165 | 166 | Emitter.prototype.hasListeners = function(event){ 167 | return !! this.listeners(event).length; 168 | }; 169 | -------------------------------------------------------------------------------- /client/src/glue.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | var glue = function(host, options) { 20 | // Turn on strict mode. 21 | 'use strict'; 22 | 23 | // Include the dependencies. 24 | @@include('./emitter.js') 25 | @@include('./websocket.js') 26 | @@include('./ajaxsocket.js') 27 | 28 | 29 | 30 | /* 31 | * Constants 32 | */ 33 | 34 | var Version = "1.9.1", 35 | MainChannelName = "m"; 36 | 37 | var SocketTypes = { 38 | WebSocket: "WebSocket", 39 | AjaxSocket: "AjaxSocket" 40 | }; 41 | 42 | var Commands = { 43 | Len: 2, 44 | Init: 'in', 45 | Ping: 'pi', 46 | Pong: 'po', 47 | Close: 'cl', 48 | Invalid: 'iv', 49 | DontAutoReconnect: 'dr', 50 | ChannelData: 'cd' 51 | }; 52 | 53 | var States = { 54 | Disconnected: "disconnected", 55 | Connecting: "connecting", 56 | Reconnecting: "reconnecting", 57 | Connected: "connected" 58 | }; 59 | 60 | var DefaultOptions = { 61 | // The base URL is appended to the host string. This value has to match with the server value. 62 | baseURL: "/glue/", 63 | 64 | // Force a socket type. 65 | // Values: false, "WebSocket", "AjaxSocket" 66 | forceSocketType: false, 67 | 68 | // Kill the connect attempt after the timeout. 69 | connectTimeout: 10000, 70 | 71 | // If the connection is idle, ping the server to check if the connection is stil alive. 72 | pingInterval: 35000, 73 | // Reconnect if the server did not response with a pong within the timeout. 74 | pingReconnectTimeout: 5000, 75 | 76 | // Whenever to automatically reconnect if the connection was lost. 77 | reconnect: true, 78 | reconnectDelay: 1000, 79 | reconnectDelayMax: 5000, 80 | // To disable set to 0 (endless). 81 | reconnectAttempts: 10, 82 | 83 | // Reset the send buffer after the timeout. 84 | resetSendBufferTimeout: 10000 85 | }; 86 | 87 | 88 | 89 | /* 90 | * Variables 91 | */ 92 | 93 | var emitter = new Emitter, 94 | bs = false, 95 | mainChannel, 96 | initialConnectedOnce = false, // If at least one successful connection was made. 97 | bsNewFunc, // Function to create a new backend socket. 98 | currentSocketType, 99 | currentState = States.Disconnected, 100 | reconnectCount = 0, 101 | autoReconnectDisabled = false, 102 | connectTimeout = false, 103 | pingTimeout = false, 104 | pingReconnectTimeout = false, 105 | sendBuffer = [], 106 | resetSendBufferTimeout = false, 107 | resetSendBufferTimedOut = false, 108 | isReady = false, // If true, the socket is initialized and ready. 109 | beforeReadySendBuffer = [], // Buffer to hold requests for the server while the socket is not ready yet. 110 | socketID = ""; 111 | 112 | 113 | /* 114 | * Include the dependencies 115 | */ 116 | 117 | // Exported helper methods for the dependencies. 118 | var closeSocket, send, sendBuffered; 119 | 120 | @@include('./utils.js') 121 | @@include('./channel.js') 122 | 123 | 124 | 125 | /* 126 | * Methods 127 | */ 128 | 129 | // Function variables. 130 | var reconnect, triggerEvent; 131 | 132 | // Sends the data to the server if a socket connection exists, otherwise it is discarded. 133 | // If the socket is not ready yet, the data is buffered until the socket is ready. 134 | send = function(data) { 135 | if (!bs) { 136 | return; 137 | } 138 | 139 | // If the socket is not ready yet, buffer the data. 140 | if (!isReady) { 141 | beforeReadySendBuffer.push(data); 142 | return; 143 | } 144 | 145 | // Send the data. 146 | bs.send(data); 147 | }; 148 | 149 | // Hint: the isReady flag has to be true before calling this function! 150 | var sendBeforeReadyBufferedData = function() { 151 | // Skip if empty. 152 | if (beforeReadySendBuffer.length === 0) { 153 | return; 154 | } 155 | 156 | // Send the buffered data. 157 | for (var i = 0; i < beforeReadySendBuffer.length; i++) { 158 | send(beforeReadySendBuffer[i]); 159 | } 160 | 161 | // Clear the buffer. 162 | beforeReadySendBuffer = []; 163 | }; 164 | 165 | var stopResetSendBufferTimeout = function() { 166 | // Reset the flag. 167 | resetSendBufferTimedOut = false; 168 | 169 | // Stop the timeout timer if present. 170 | if (resetSendBufferTimeout !== false) { 171 | clearTimeout(resetSendBufferTimeout); 172 | resetSendBufferTimeout = false; 173 | } 174 | }; 175 | 176 | var startResetSendBufferTimeout = function() { 177 | // Skip if already running or if already timed out. 178 | if (resetSendBufferTimeout !== false || resetSendBufferTimedOut) { 179 | return; 180 | } 181 | 182 | // Start the timeout. 183 | resetSendBufferTimeout = setTimeout(function() { 184 | // Update the flags. 185 | resetSendBufferTimeout = false; 186 | resetSendBufferTimedOut = true; 187 | 188 | // Return if already empty. 189 | if (sendBuffer.length === 0) { 190 | return; 191 | } 192 | 193 | // Call the discard callbacks if defined. 194 | var buf; 195 | for (var i = 0; i < sendBuffer.length; i++) { 196 | buf = sendBuffer[i]; 197 | if (buf.discardCallback && utils.isFunction(buf.discardCallback)) { 198 | try { 199 | buf.discardCallback(buf.data); 200 | } 201 | catch (err) { 202 | console.log("glue: failed to call discard callback: " + err.message); 203 | } 204 | } 205 | } 206 | 207 | // Trigger the event if any buffered send data is discarded. 208 | triggerEvent("discard_send_buffer"); 209 | 210 | // Reset the buffer. 211 | sendBuffer = []; 212 | }, options.resetSendBufferTimeout); 213 | }; 214 | 215 | var sendDataFromSendBuffer = function() { 216 | // Stop the reset send buffer tiemout. 217 | stopResetSendBufferTimeout(); 218 | 219 | // Skip if empty. 220 | if (sendBuffer.length === 0) { 221 | return; 222 | } 223 | 224 | // Send data, which could not be send... 225 | var buf; 226 | for (var i = 0; i < sendBuffer.length; i++) { 227 | buf = sendBuffer[i]; 228 | send(buf.cmd + buf.data); 229 | } 230 | 231 | // Clear the buffer again. 232 | sendBuffer = []; 233 | }; 234 | 235 | // Send data to the server. 236 | // This is a helper method which handles buffering, 237 | // if the socket is currently not connected. 238 | // One optional discard callback can be passed. 239 | // It is called if the data could not be send to the server. 240 | // The data is passed as first argument to the discard callback. 241 | // returns: 242 | // 1 if immediately send, 243 | // 0 if added to the send queue and 244 | // -1 if discarded. 245 | sendBuffered = function(cmd, data, discardCallback) { 246 | // Be sure, that the data value is an empty 247 | // string if not passed to this method. 248 | if (!data) { 249 | data = ""; 250 | } 251 | 252 | // Add the data to the send buffer if disconnected. 253 | // They will be buffered for a short timeout to bridge short connection errors. 254 | if (!bs || currentState !== States.Connected) { 255 | // If already timed out, then call the discard callback and return. 256 | if (resetSendBufferTimedOut) { 257 | if (discardCallback && utils.isFunction(discardCallback)) { 258 | discardCallback(data); 259 | } 260 | 261 | return -1; 262 | } 263 | 264 | // Reset the send buffer after a specific timeout. 265 | startResetSendBufferTimeout(); 266 | 267 | // Append to the buffer. 268 | sendBuffer.push({ 269 | cmd: cmd, 270 | data: data, 271 | discardCallback: discardCallback 272 | }); 273 | 274 | return 0; 275 | } 276 | 277 | // Send the data with the command to the server. 278 | send(cmd + data); 279 | 280 | return 1; 281 | }; 282 | 283 | var stopConnectTimeout = function() { 284 | // Stop the timeout timer if present. 285 | if (connectTimeout !== false) { 286 | clearTimeout(connectTimeout); 287 | connectTimeout = false; 288 | } 289 | }; 290 | 291 | var resetConnectTimeout = function() { 292 | // Stop the timeout. 293 | stopConnectTimeout(); 294 | 295 | // Start the timeout. 296 | connectTimeout = setTimeout(function() { 297 | // Update the flag. 298 | connectTimeout = false; 299 | 300 | // Trigger the event. 301 | triggerEvent("connect_timeout"); 302 | 303 | // Reconnect to the server. 304 | reconnect(); 305 | }, options.connectTimeout); 306 | }; 307 | 308 | var stopPingTimeout = function() { 309 | // Stop the timeout timer if present. 310 | if (pingTimeout !== false) { 311 | clearTimeout(pingTimeout); 312 | pingTimeout = false; 313 | } 314 | 315 | // Stop the reconnect timeout. 316 | if (pingReconnectTimeout !== false) { 317 | clearTimeout(pingReconnectTimeout); 318 | pingReconnectTimeout = false; 319 | } 320 | }; 321 | 322 | var resetPingTimeout = function() { 323 | // Stop the timeout. 324 | stopPingTimeout(); 325 | 326 | // Start the timeout. 327 | pingTimeout = setTimeout(function() { 328 | // Update the flag. 329 | pingTimeout = false; 330 | 331 | // Request a Pong response to check if the connection is still alive. 332 | send(Commands.Ping); 333 | 334 | // Start the reconnect timeout. 335 | pingReconnectTimeout = setTimeout(function() { 336 | // Update the flag. 337 | pingReconnectTimeout = false; 338 | 339 | // Trigger the event. 340 | triggerEvent("timeout"); 341 | 342 | // Reconnect to the server. 343 | reconnect(); 344 | }, options.pingReconnectTimeout); 345 | }, options.pingInterval); 346 | }; 347 | 348 | var newBackendSocket = function() { 349 | // If at least one successfull connection was made, 350 | // then create a new socket using the last create socket function. 351 | // Otherwise determind which socket layer to use. 352 | if (initialConnectedOnce) { 353 | bs = bsNewFunc(); 354 | return; 355 | } 356 | 357 | // Fallback to the ajax socket layer if there was no successful initial 358 | // connection and more than one reconnection attempt was made. 359 | if (reconnectCount > 1) { 360 | bsNewFunc = newAjaxSocket; 361 | bs = bsNewFunc(); 362 | currentSocketType = SocketTypes.AjaxSocket; 363 | return; 364 | } 365 | 366 | // Choose the socket layer depending on the browser support. 367 | if ((!options.forceSocketType && window.WebSocket) || 368 | options.forceSocketType === SocketTypes.WebSocket) 369 | { 370 | bsNewFunc = newWebSocket; 371 | currentSocketType = SocketTypes.WebSocket; 372 | } 373 | else 374 | { 375 | bsNewFunc = newAjaxSocket; 376 | currentSocketType = SocketTypes.AjaxSocket; 377 | } 378 | 379 | // Create the new socket. 380 | bs = bsNewFunc(); 381 | }; 382 | 383 | var initSocket = function(data) { 384 | // Parse the data JSON string to an object. 385 | data = JSON.parse(data); 386 | 387 | // Validate. 388 | // Close the socket and log the error on invalid data. 389 | if (!data.socketID) { 390 | closeSocket(); 391 | console.log("glue: socket initialization failed: invalid initialization data received"); 392 | return; 393 | } 394 | 395 | // Set the socket ID. 396 | socketID = data.socketID; 397 | 398 | // The socket initialization is done. 399 | // ################################## 400 | 401 | // Set the ready flag. 402 | isReady = true; 403 | 404 | // First send all data messages which were 405 | // buffered because the socket was not ready. 406 | sendBeforeReadyBufferedData(); 407 | 408 | // Now set the state and trigger the event. 409 | currentState = States.Connected; 410 | triggerEvent("connected"); 411 | 412 | // Send the queued data from the send buffer if present. 413 | // Do this after the next tick to be sure, that 414 | // the connected event gets fired first. 415 | setTimeout(sendDataFromSendBuffer, 0); 416 | }; 417 | 418 | var connectSocket = function() { 419 | // Set a new backend socket. 420 | newBackendSocket(); 421 | 422 | // Set the backend socket events. 423 | bs.onOpen = function() { 424 | // Stop the connect timeout. 425 | stopConnectTimeout(); 426 | 427 | // Reset the reconnect count. 428 | reconnectCount = 0; 429 | 430 | // Set the flag. 431 | initialConnectedOnce = true; 432 | 433 | // Reset or start the ping timeout. 434 | resetPingTimeout(); 435 | 436 | // Prepare the init data to be send to the server. 437 | var data = { 438 | version: Version 439 | }; 440 | 441 | // Marshal the data object to a JSON string. 442 | data = JSON.stringify(data); 443 | 444 | // Send the init data to the server with the init command. 445 | // Hint: the backend socket is used directly instead of the send function, 446 | // because the socket is not ready yet and this part belongs to the 447 | // initialization process. 448 | bs.send(Commands.Init + data); 449 | }; 450 | 451 | bs.onClose = function() { 452 | // Reconnect the socket. 453 | reconnect(); 454 | }; 455 | 456 | bs.onError = function(msg) { 457 | // Trigger the error event. 458 | triggerEvent("error", [msg]); 459 | 460 | // Reconnect the socket. 461 | reconnect(); 462 | }; 463 | 464 | bs.onMessage = function(data) { 465 | // Reset the ping timeout. 466 | resetPingTimeout(); 467 | 468 | // Log if the received data is too short. 469 | if (data.length < Commands.Len) { 470 | console.log("glue: received invalid data from server: data is too short."); 471 | return; 472 | } 473 | 474 | // Extract the command from the received data string. 475 | var cmd = data.substr(0, Commands.Len); 476 | data = data.substr(Commands.Len); 477 | 478 | if (cmd === Commands.Ping) { 479 | // Response with a pong message. 480 | send(Commands.Pong); 481 | } 482 | else if (cmd === Commands.Pong) { 483 | // Don't do anything. 484 | // The ping timeout was already reset. 485 | } 486 | else if (cmd === Commands.Invalid) { 487 | // Log. 488 | console.log("glue: server replied with an invalid request notification!"); 489 | } 490 | else if (cmd === Commands.DontAutoReconnect) { 491 | // Disable auto reconnections. 492 | autoReconnectDisabled = true; 493 | 494 | // Log. 495 | console.log("glue: server replied with an don't automatically reconnect request. This might be due to an incompatible protocol version."); 496 | } 497 | else if (cmd === Commands.Init) { 498 | initSocket(data); 499 | } 500 | else if (cmd === Commands.ChannelData) { 501 | // Obtain the two values from the data string. 502 | var v = utils.unmarshalValues(data); 503 | if (!v) { 504 | console.log("glue: server requested an invalid channel data request: " + data); 505 | return; 506 | } 507 | 508 | // Trigger the event. 509 | channel.emitOnMessage(v.first, v.second); 510 | } 511 | else { 512 | console.log("glue: received invalid data from server with command '" + cmd + "' and data '" + data + "'!"); 513 | } 514 | }; 515 | 516 | // Connect during the next tick. 517 | // The user should be able to connect the event functions first. 518 | setTimeout(function() { 519 | // Set the state and trigger the event. 520 | if (reconnectCount > 0) { 521 | currentState = States.Reconnecting; 522 | triggerEvent("reconnecting"); 523 | } 524 | else { 525 | currentState = States.Connecting; 526 | triggerEvent("connecting"); 527 | } 528 | 529 | // Reset or start the connect timeout. 530 | resetConnectTimeout(); 531 | 532 | // Connect to the server 533 | bs.open(); 534 | }, 0); 535 | }; 536 | 537 | var resetSocket = function() { 538 | // Stop the timeouts. 539 | stopConnectTimeout(); 540 | stopPingTimeout(); 541 | 542 | // Reset flags and variables. 543 | isReady = false; 544 | socketID = ""; 545 | 546 | // Clear the buffer. 547 | // This buffer is attached to each single socket. 548 | beforeReadySendBuffer = []; 549 | 550 | // Reset previous backend sockets if defined. 551 | if (bs) { 552 | // Set dummy functions. 553 | // This will ensure, that previous old sockets don't 554 | // call our valid methods. This would mix things up. 555 | bs.onOpen = bs.onClose = bs.onMessage = bs.onError = function() {}; 556 | 557 | // Reset everything and close the socket. 558 | bs.reset(); 559 | bs = false; 560 | } 561 | }; 562 | 563 | reconnect = function() { 564 | // Reset the socket. 565 | resetSocket(); 566 | 567 | // If no reconnections should be made or more than max 568 | // reconnect attempts where made, trigger the disconnected event. 569 | if ((options.reconnectAttempts > 0 && reconnectCount > options.reconnectAttempts) || 570 | options.reconnect === false || autoReconnectDisabled) 571 | { 572 | // Set the state and trigger the event. 573 | currentState = States.Disconnected; 574 | triggerEvent("disconnected"); 575 | 576 | return; 577 | } 578 | 579 | // Increment the count. 580 | reconnectCount += 1; 581 | 582 | // Calculate the reconnect delay. 583 | var reconnectDelay = options.reconnectDelay * reconnectCount; 584 | if (reconnectDelay > options.reconnectDelayMax) { 585 | reconnectDelay = options.reconnectDelayMax; 586 | } 587 | 588 | // Try to reconnect. 589 | setTimeout(function() { 590 | connectSocket(); 591 | }, reconnectDelay); 592 | }; 593 | 594 | closeSocket = function() { 595 | // Check if the socket exists. 596 | if (!bs) { 597 | return; 598 | } 599 | 600 | // Notify the server. 601 | send(Commands.Close); 602 | 603 | // Reset the socket. 604 | resetSocket(); 605 | 606 | // Set the state and trigger the event. 607 | currentState = States.Disconnected; 608 | triggerEvent("disconnected"); 609 | }; 610 | 611 | 612 | 613 | /* 614 | * Initialize section 615 | */ 616 | 617 | // Create the main channel. 618 | mainChannel = channel.get(MainChannelName); 619 | 620 | // Prepare the host string. 621 | // Use the current location if the host string is not set. 622 | if (!host) { 623 | host = window.location.protocol + "//" + window.location.host; 624 | } 625 | // The host string has to start with http:// or https:// 626 | if (!host.match("^http://") && !host.match("^https://")) { 627 | console.log("glue: invalid host: missing 'http://' or 'https://'!"); 628 | return; 629 | } 630 | 631 | // Merge the options with the default options. 632 | options = utils.extend({}, DefaultOptions, options); 633 | 634 | // The max value can't be smaller than the delay. 635 | if (options.reconnectDelayMax < options.reconnectDelay) { 636 | options.reconnectDelayMax = options.reconnectDelay; 637 | } 638 | 639 | // Prepare the base URL. 640 | // The base URL has to start and end with a slash. 641 | if (options.baseURL.indexOf("/") !== 0) { 642 | options.baseURL = "/" + options.baseURL; 643 | } 644 | if (options.baseURL.slice(-1) !== "/") { 645 | options.baseURL = options.baseURL + "/"; 646 | } 647 | 648 | // Create the initial backend socket and establish a connection to the server. 649 | connectSocket(); 650 | 651 | 652 | 653 | /* 654 | * Socket object 655 | */ 656 | 657 | var socket = { 658 | // version returns the glue socket protocol version. 659 | version: function() { 660 | return Version; 661 | }, 662 | 663 | // type returns the current used socket type as string. 664 | // Either "WebSocket" or "AjaxSocket". 665 | type: function() { 666 | return currentSocketType; 667 | }, 668 | 669 | // state returns the current socket state as string. 670 | // Following states are available: 671 | // - "disconnected" 672 | // - "connecting" 673 | // - "reconnecting" 674 | // - "connected" 675 | state: function() { 676 | return currentState; 677 | }, 678 | 679 | // socketID returns the socket's ID. 680 | // This is a cryptographically secure pseudorandom number. 681 | socketID: function() { 682 | return socketID; 683 | }, 684 | 685 | // send a data string to the server. 686 | // One optional discard callback can be passed. 687 | // It is called if the data could not be send to the server. 688 | // The data is passed as first argument to the discard callback. 689 | // returns: 690 | // 1 if immediately send, 691 | // 0 if added to the send queue and 692 | // -1 if discarded. 693 | send: function(data, discardCallback) { 694 | mainChannel.send(data, discardCallback); 695 | }, 696 | 697 | // onMessage sets the function which is triggered as soon as a message is received. 698 | onMessage: function(f) { 699 | mainChannel.onMessage(f); 700 | }, 701 | 702 | // on binds event functions to events. 703 | // This function is equivalent to jQuery's on method syntax. 704 | // Following events are available: 705 | // - "connected" 706 | // - "connecting" 707 | // - "disconnected" 708 | // - "reconnecting" 709 | // - "error" 710 | // - "connect_timeout" 711 | // - "timeout" 712 | // - "discard_send_buffer" 713 | on: function() { 714 | emitter.on.apply(emitter, arguments); 715 | }, 716 | 717 | // Reconnect to the server. 718 | // This is ignored if the socket is not disconnected. 719 | // It will reconnect automatically if required. 720 | reconnect: function() { 721 | if (currentState !== States.Disconnected) { 722 | return; 723 | } 724 | 725 | // Reset the reconnect count and the auto reconnect disabled flag. 726 | reconnectCount = 0; 727 | autoReconnectDisabled = false; 728 | 729 | // Reconnect the socket. 730 | reconnect(); 731 | }, 732 | 733 | // close the socket connection. 734 | close: function() { 735 | closeSocket(); 736 | }, 737 | 738 | // channel returns the given channel object specified by name 739 | // to communicate in a separate channel than the default one. 740 | channel: function(name) { 741 | return channel.get(name); 742 | } 743 | }; 744 | 745 | // Define the function body of the triggerEvent function. 746 | triggerEvent = function() { 747 | emitter.emit.apply(emitter, arguments); 748 | }; 749 | 750 | // Return the newly created socket. 751 | return socket; 752 | }; 753 | -------------------------------------------------------------------------------- /client/src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /* 20 | * This code lives inside the glue function. 21 | */ 22 | 23 | var utils = (function() { 24 | /* 25 | * Constants 26 | */ 27 | 28 | var Delimiter = "&"; 29 | 30 | 31 | 32 | /* 33 | * Variables 34 | */ 35 | 36 | var instance = {}; // Our public instance object returned by this function. 37 | 38 | 39 | 40 | /* 41 | * Public Methods 42 | */ 43 | 44 | // Mimics jQuery's extend method. 45 | // Source: http://stackoverflow.com/questions/11197247/javascript-equivalent-of-jquerys-extend-method 46 | instance.extend = function() { 47 | for(var i=1; i data.length) { 77 | return false; 78 | } 79 | 80 | // Now split the first value from the second. 81 | var firstV = data.substr(0, len); 82 | var secondV = data.substr(len); 83 | 84 | // Return an object with both values. 85 | return { 86 | first: firstV, 87 | second: secondV 88 | }; 89 | }; 90 | 91 | // marshalValues joins two values into a single string. 92 | // They can be decoded by the unmarshalValues function. 93 | instance.marshalValues = function(first, second) { 94 | return String(first.length) + Delimiter + first + second; 95 | }; 96 | 97 | 98 | return instance; 99 | })(); 100 | -------------------------------------------------------------------------------- /client/src/websocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /* 20 | * This code lives inside the glue function. 21 | */ 22 | 23 | 24 | var newWebSocket = function () { 25 | /* 26 | * Variables 27 | */ 28 | 29 | var s = {}, 30 | ws; 31 | 32 | 33 | 34 | /* 35 | * Socket layer implementation. 36 | */ 37 | 38 | s.open = function () { 39 | try { 40 | // Generate the websocket url. 41 | var url; 42 | if (host.match("^https://")) { 43 | url = "wss" + host.substr(5); 44 | } else { 45 | url = "ws" + host.substr(4); 46 | } 47 | url += options.baseURL + "ws"; 48 | 49 | // Open the websocket connection 50 | ws = new WebSocket(url); 51 | 52 | // Set the callback handlers 53 | ws.onmessage = function(event) { 54 | s.onMessage(event.data.toString()); 55 | }; 56 | 57 | ws.onerror = function(event) { 58 | var msg = "the websocket closed the connection with "; 59 | if (event.code) { 60 | msg += "the error code: " + event.code; 61 | } 62 | else { 63 | msg += "an error."; 64 | } 65 | 66 | s.onError(msg); 67 | }; 68 | 69 | ws.onclose = function() { 70 | s.onClose(); 71 | }; 72 | 73 | ws.onopen = function() { 74 | s.onOpen(); 75 | }; 76 | } catch (e) { 77 | s.onError(); 78 | } 79 | }; 80 | 81 | s.send = function (data) { 82 | // Send the data to the server 83 | ws.send(data); 84 | }; 85 | 86 | s.reset = function() { 87 | // Close the websocket if defined. 88 | if (ws) { 89 | ws.close(); 90 | } 91 | 92 | ws = undefined; 93 | }; 94 | 95 | return s; 96 | }; 97 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package glue 20 | 21 | import "sync" 22 | 23 | //###############// 24 | //### Handler ###// 25 | //###############// 26 | 27 | type handler struct { 28 | stopChan chan struct{} 29 | stopChanClosed bool 30 | 31 | mutex sync.Mutex 32 | } 33 | 34 | func newHandler() *handler { 35 | return &handler{ 36 | stopChanClosed: true, 37 | } 38 | } 39 | 40 | // New creates a new handler and stopps the previous handler if present. 41 | // A stop channel is returned, which is closed as soon as the handler is stopped. 42 | func (h *handler) New() chan struct{} { 43 | // Lock the mutex. 44 | h.mutex.Lock() 45 | defer h.mutex.Unlock() 46 | 47 | // Signal the stop request by closing the channel if open. 48 | if !h.stopChanClosed { 49 | close(h.stopChan) 50 | } 51 | 52 | // Create a new stop channel. 53 | h.stopChan = make(chan struct{}) 54 | 55 | // Update the flag. 56 | h.stopChanClosed = false 57 | 58 | return h.stopChan 59 | } 60 | 61 | // Stop the handler if present. 62 | func (h *handler) Stop() { 63 | // Lock the mutex. 64 | h.mutex.Lock() 65 | defer h.mutex.Unlock() 66 | 67 | // Signal the stop request by closing the channel if open. 68 | if !h.stopChanClosed { 69 | close(h.stopChan) 70 | h.stopChanClosed = true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Package log holds the log backend used by the socket library. 20 | // Use the logrus L value to adapt the log formatting 21 | // or log levels if required... 22 | package log 23 | 24 | import ( 25 | "github.com/sirupsen/logrus" 26 | ) 27 | 28 | var ( 29 | // L is the public logrus value used internally by glue. 30 | L = logrus.New() 31 | ) 32 | 33 | func init() { 34 | // Set the default log options. 35 | L.Formatter = new(logrus.TextFormatter) 36 | L.Level = logrus.DebugLevel 37 | } 38 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package glue 20 | 21 | import ( 22 | "net/http" 23 | "net/url" 24 | "strings" 25 | ) 26 | 27 | //#################// 28 | //### Constants ###// 29 | //#################// 30 | 31 | // A HTTPSocketType defines which socket type to use for the HTTP glue server. 32 | type HTTPSocketType int 33 | 34 | const ( 35 | // HTTPSocketTypeNone defines to not configure and run a HTTP server. 36 | HTTPSocketTypeNone HTTPSocketType = 1 << iota 37 | 38 | // HTTPSocketTypeTCP defines to use a TCP server. 39 | HTTPSocketTypeTCP HTTPSocketType = 1 << iota 40 | 41 | // HTTPSocketTypeUnix defines to use a Unix socket server. 42 | HTTPSocketTypeUnix HTTPSocketType = 1 << iota 43 | ) 44 | 45 | //####################// 46 | //### Options type ###// 47 | //####################// 48 | 49 | // Options holds the glue server options. 50 | type Options struct { 51 | // HTTPSocketType defines which socket type to use for the HTTP glue server. 52 | // Default: HTTPSocketTypeTCP 53 | HTTPSocketType HTTPSocketType 54 | 55 | // The HTTP address to listen on. 56 | // Default: ":80" 57 | HTTPListenAddress string 58 | 59 | // HTTPHandleURL defines the base url to handle glue HTTP socket requests. 60 | // This has to be set, even if the none socket type is used. 61 | // Default: "/glue/" 62 | HTTPHandleURL string 63 | 64 | // CheckOrigin returns true if the request Origin header is acceptable. If 65 | // CheckOrigin is nil, the host in the Origin header must not be set or 66 | // must match the host of the request. 67 | // This method is used by the backend sockets before establishing connections. 68 | CheckOrigin func(r *http.Request) bool 69 | 70 | // Enables the Cross-Origin Resource Sharing (CORS) mechanism. 71 | // This will set the Access-Control-Allow-Origin HTTP headers. 72 | // A resource makes a cross-origin HTTP request when it requests a resource 73 | // from a different domain than the one which served itself. 74 | EnableCORS bool 75 | } 76 | 77 | // SetDefaults sets unset option values to its default value. 78 | func (o *Options) SetDefaults() { 79 | // Set the socket type. 80 | if o.HTTPSocketType != HTTPSocketTypeNone && 81 | o.HTTPSocketType != HTTPSocketTypeTCP && 82 | o.HTTPSocketType != HTTPSocketTypeUnix { 83 | o.HTTPSocketType = HTTPSocketTypeTCP 84 | } 85 | 86 | // Set the listen address. 87 | if len(o.HTTPListenAddress) == 0 { 88 | o.HTTPListenAddress = ":80" 89 | } 90 | 91 | // Set the handle URL. 92 | if len(o.HTTPHandleURL) == 0 { 93 | o.HTTPHandleURL = "/glue/" 94 | } 95 | 96 | // Be sure that the handle URL ends with a slash. 97 | if !strings.HasSuffix(o.HTTPHandleURL, "/") { 98 | o.HTTPHandleURL += "/" 99 | } 100 | 101 | // Set the default check origin function if not set. 102 | if o.CheckOrigin == nil { 103 | o.CheckOrigin = checkSameOrigin 104 | } 105 | } 106 | 107 | //###############// 108 | //### Private ###// 109 | //###############// 110 | 111 | // checkSameOrigin returns true if the origin is not set or is equal to the request host. 112 | // Source from gorilla websockets. 113 | func checkSameOrigin(r *http.Request) bool { 114 | origin := r.Header["Origin"] 115 | if len(origin) == 0 { 116 | return true 117 | } 118 | u, err := url.Parse(origin[0]) 119 | if err != nil { 120 | return false 121 | } 122 | return u.Host == r.Host 123 | } 124 | -------------------------------------------------------------------------------- /sample/Channels/.gitignore: -------------------------------------------------------------------------------- 1 | Channels 2 | -------------------------------------------------------------------------------- /sample/Channels/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "log" 23 | "net/http" 24 | 25 | "github.com/desertbit/glue" 26 | ) 27 | 28 | func main() { 29 | // Set the http file server. 30 | http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("public")))) 31 | http.Handle("/dist/", http.StripPrefix("/dist/", http.FileServer(http.Dir("../../client/dist")))) 32 | 33 | // Create a new glue server. 34 | server := glue.NewServer(glue.Options{ 35 | HTTPListenAddress: ":8080", 36 | }) 37 | 38 | // Release the glue server on defer. 39 | // This will block new incoming connections 40 | // and close all current active sockets. 41 | defer server.Release() 42 | 43 | // Set the glue event function to handle new incoming socket connections. 44 | server.OnNewSocket(onNewSocket) 45 | 46 | // Run the glue server. 47 | err := server.Run() 48 | if err != nil { 49 | log.Fatalf("Glue Run: %v", err) 50 | } 51 | } 52 | 53 | func onNewSocket(s *glue.Socket) { 54 | // We won't read any data from the socket itself. 55 | // Discard received data! 56 | s.DiscardRead() 57 | 58 | // Set a function which is triggered as soon as the socket is closed. 59 | s.OnClose(func() { 60 | log.Printf("socket closed with remote address: %s", s.RemoteAddr()) 61 | }) 62 | 63 | // Create a channel. 64 | c := s.Channel("golang") 65 | 66 | // Set the channel on read event function. 67 | c.OnRead(func(data string) { 68 | // Echo the received data back to the client. 69 | c.Write("channel golang: " + data) 70 | }) 71 | 72 | // Write to the channel. 73 | c.Write("Hello Gophers!") 74 | } 75 | -------------------------------------------------------------------------------- /sample/Channels/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Glue Socket Test 6 | 7 | 8 | 9 |

Glue Socket Test

10 | 11 | 12 | 13 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /sample/OnlyWrite/.gitignore: -------------------------------------------------------------------------------- 1 | OnlyWrite -------------------------------------------------------------------------------- /sample/OnlyWrite/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "log" 23 | "net/http" 24 | 25 | "github.com/desertbit/glue" 26 | ) 27 | 28 | func main() { 29 | // Set the http file server. 30 | http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("public")))) 31 | http.Handle("/dist/", http.StripPrefix("/dist/", http.FileServer(http.Dir("../../client/dist")))) 32 | 33 | // Create a new glue server. 34 | server := glue.NewServer(glue.Options{ 35 | HTTPListenAddress: ":8080", 36 | }) 37 | 38 | // Release the glue server on defer. 39 | // This will block new incoming connections 40 | // and close all current active sockets. 41 | defer server.Release() 42 | 43 | // Set the glue event function to handle new incoming socket connections. 44 | server.OnNewSocket(onNewSocket) 45 | 46 | // Run the glue server. 47 | err := server.Run() 48 | if err != nil { 49 | log.Fatalf("Glue Run: %v", err) 50 | } 51 | } 52 | func onNewSocket(s *glue.Socket) { 53 | // Set a function which is triggered as soon as the socket is closed. 54 | s.OnClose(func() { 55 | log.Printf("socket closed with remote address: %s", s.RemoteAddr()) 56 | }) 57 | 58 | // Discard all reads. 59 | // If received data is not discarded, then the read buffer will block as soon 60 | // as it is full, which will also block the keep-alive mechanism of the socket. 61 | // The result would be a closed socket... 62 | s.DiscardRead() 63 | 64 | // Send a welcome string to the client. 65 | s.Write("Hello Client") 66 | } 67 | -------------------------------------------------------------------------------- /sample/OnlyWrite/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Glue Socket Test 6 | 7 | 8 | 9 |

Glue Socket Test

10 | 11 | 12 | 13 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /sample/ReadEventWrite/.gitignore: -------------------------------------------------------------------------------- 1 | ReadEventWrite -------------------------------------------------------------------------------- /sample/ReadEventWrite/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "log" 23 | "net/http" 24 | 25 | "github.com/desertbit/glue" 26 | ) 27 | 28 | func main() { 29 | // Set the http file server. 30 | http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("public")))) 31 | http.Handle("/dist/", http.StripPrefix("/dist/", http.FileServer(http.Dir("../../client/dist")))) 32 | 33 | // Create a new glue server. 34 | server := glue.NewServer(glue.Options{ 35 | HTTPListenAddress: ":8080", 36 | }) 37 | 38 | // Release the glue server on defer. 39 | // This will block new incoming connections 40 | // and close all current active sockets. 41 | defer server.Release() 42 | 43 | // Set the glue event function to handle new incoming socket connections. 44 | server.OnNewSocket(onNewSocket) 45 | 46 | // Run the glue server. 47 | err := server.Run() 48 | if err != nil { 49 | log.Fatalf("Glue Run: %v", err) 50 | } 51 | } 52 | 53 | func onNewSocket(s *glue.Socket) { 54 | // Set a function which is triggered as soon as the socket is closed. 55 | s.OnClose(func() { 56 | log.Printf("socket closed with remote address: %s", s.RemoteAddr()) 57 | }) 58 | 59 | // Set a function which is triggered during each received message. 60 | s.OnRead(func(data string) { 61 | // Echo the received data back to the client. 62 | s.Write(data) 63 | }) 64 | 65 | // Send a welcome string to the client. 66 | s.Write("Hello Client") 67 | } 68 | -------------------------------------------------------------------------------- /sample/ReadEventWrite/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Glue Socket Test 6 | 7 | 8 | 9 |

Glue Socket Test

10 | 11 | 12 | 13 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /sample/ReadWrite/.gitignore: -------------------------------------------------------------------------------- 1 | ReadWrite -------------------------------------------------------------------------------- /sample/ReadWrite/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "log" 23 | "net/http" 24 | 25 | "github.com/desertbit/glue" 26 | ) 27 | 28 | func main() { 29 | // Set the http file server. 30 | http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("public")))) 31 | http.Handle("/dist/", http.StripPrefix("/dist/", http.FileServer(http.Dir("../../client/dist")))) 32 | 33 | // Create a new glue server. 34 | server := glue.NewServer(glue.Options{ 35 | HTTPListenAddress: ":8080", 36 | }) 37 | 38 | // Release the glue server on defer. 39 | // This will block new incoming connections 40 | // and close all current active sockets. 41 | defer server.Release() 42 | 43 | // Set the glue event function to handle new incoming socket connections. 44 | server.OnNewSocket(onNewSocket) 45 | 46 | // Run the glue server. 47 | err := server.Run() 48 | if err != nil { 49 | log.Fatalf("Glue Run: %v", err) 50 | } 51 | } 52 | 53 | func onNewSocket(s *glue.Socket) { 54 | // Set a function which is triggered as soon as the socket is closed. 55 | s.OnClose(func() { 56 | log.Printf("socket closed with remote address: %s", s.RemoteAddr()) 57 | }) 58 | 59 | // Run the read loop in a new goroutine. 60 | go readLoop(s) 61 | 62 | // Send a welcome string to the client. 63 | s.Write("Hello Client") 64 | } 65 | 66 | func readLoop(s *glue.Socket) { 67 | for { 68 | // Wait for available data. 69 | // Optional: pass a timeout duration to read. 70 | data, err := s.Read() 71 | if err != nil { 72 | // Just return and release this goroutine if the socket was closed. 73 | if err == glue.ErrSocketClosed { 74 | return 75 | } 76 | 77 | log.Printf("read error: %v", err) 78 | continue 79 | } 80 | 81 | // Echo the received data back to the client. 82 | s.Write(data) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /sample/ReadWrite/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Glue Socket Test 6 | 7 | 8 | 9 |

Glue Socket Test

10 | 11 | 12 | 13 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Package glue - Robust Go and Javascript Socket Library. 20 | // This library is thread-safe. 21 | package glue 22 | 23 | import ( 24 | "fmt" 25 | "net" 26 | "net/http" 27 | "sync" 28 | "time" 29 | 30 | "github.com/desertbit/glue/backend" 31 | ) 32 | 33 | //####################// 34 | //### Public Types ###// 35 | //####################// 36 | 37 | // OnNewSocketFunc is an event function. 38 | type OnNewSocketFunc func(s *Socket) 39 | 40 | //###################// 41 | //### Server Type ###// 42 | //###################// 43 | 44 | // A Server represents a glue server which handles incoming socket connections. 45 | type Server struct { 46 | bs *backend.Server 47 | options *Options 48 | 49 | block bool 50 | blockMutex sync.Mutex 51 | onNewSocket OnNewSocketFunc 52 | 53 | sockets map[string]*Socket // A map holding all active current sockets. 54 | socketsMutex sync.Mutex 55 | } 56 | 57 | // NewServer creates a new glue server instance. 58 | // One variadic arguments specifies the server options. 59 | func NewServer(o ...Options) *Server { 60 | // Get or create the options. 61 | var options *Options 62 | if len(o) > 0 { 63 | options = &o[0] 64 | } else { 65 | options = &Options{} 66 | } 67 | 68 | // Set the default option values for unset values. 69 | options.SetDefaults() 70 | 71 | // Create a new backend server. 72 | bs := backend.NewServer(len(options.HTTPHandleURL), options.EnableCORS, options.CheckOrigin) 73 | 74 | // Create a new server value. 75 | s := &Server{ 76 | bs: bs, 77 | options: options, 78 | onNewSocket: func(*Socket) {}, // Initialize with dummy function to remove nil check. 79 | sockets: make(map[string]*Socket), 80 | } 81 | 82 | // Set the backend server event function. 83 | bs.OnNewSocketConnection(s.handleOnNewSocketConnection) 84 | 85 | return s 86 | } 87 | 88 | // Block new incomming connections. 89 | func (s *Server) Block(b bool) { 90 | s.blockMutex.Lock() 91 | defer s.blockMutex.Unlock() 92 | 93 | s.block = b 94 | } 95 | 96 | // IsBlocked returns a boolean whenever new incoming connections should be blocked. 97 | func (s *Server) IsBlocked() bool { 98 | s.blockMutex.Lock() 99 | defer s.blockMutex.Unlock() 100 | 101 | return s.block 102 | } 103 | 104 | // OnNewSocket sets the event function which is 105 | // triggered if a new socket connection was made. 106 | // The event function must not block! As soon as the event function 107 | // returns, the socket is added to the active sockets map. 108 | func (s *Server) OnNewSocket(f OnNewSocketFunc) { 109 | s.onNewSocket = f 110 | } 111 | 112 | // GetSocket obtains a socket by its ID. 113 | // Returns nil if not found. 114 | func (s *Server) GetSocket(id string) *Socket { 115 | // Lock the mutex. 116 | s.socketsMutex.Lock() 117 | defer s.socketsMutex.Unlock() 118 | 119 | // Obtain the socket. 120 | socket, ok := s.sockets[id] 121 | if !ok { 122 | return nil 123 | } 124 | 125 | return socket 126 | } 127 | 128 | // Sockets returns a list of all current connected sockets. 129 | // Hint: Sockets are added to the active sockets list before the OnNewSocket 130 | // event function is called. 131 | // Use the IsInitialized flag to determind if a socket is not ready yet... 132 | func (s *Server) Sockets() []*Socket { 133 | // Lock the mutex. 134 | s.socketsMutex.Lock() 135 | defer s.socketsMutex.Unlock() 136 | 137 | // Create the slice. 138 | list := make([]*Socket, len(s.sockets)) 139 | 140 | // Add all sockets from the map. 141 | i := 0 142 | for _, s := range s.sockets { 143 | list[i] = s 144 | i++ 145 | } 146 | 147 | return list 148 | } 149 | 150 | // Release this package. This will block all new incomming socket connections 151 | // and close all current connected sockets. 152 | func (s *Server) Release() { 153 | // Block all new incomming socket connections. 154 | s.Block(true) 155 | 156 | // Wait for a little moment, so new incomming sockets are added 157 | // to the sockets active list. 158 | time.Sleep(200 * time.Millisecond) 159 | 160 | // Close all current connected sockets. 161 | sockets := s.Sockets() 162 | for _, s := range sockets { 163 | s.Close() 164 | } 165 | } 166 | 167 | // Run starts the server and listens for incoming socket connections. 168 | // This is a blocking method. 169 | func (s *Server) Run() error { 170 | // Skip if set to none. 171 | if s.options.HTTPSocketType != HTTPSocketTypeNone { 172 | // Set the base glue HTTP handler. 173 | http.Handle(s.options.HTTPHandleURL, s) 174 | 175 | // Start the http server. 176 | if s.options.HTTPSocketType == HTTPSocketTypeUnix { 177 | // Listen on the unix socket. 178 | l, err := net.Listen("unix", s.options.HTTPListenAddress) 179 | if err != nil { 180 | return fmt.Errorf("Listen: %v", err) 181 | } 182 | 183 | // Start the http server. 184 | err = http.Serve(l, nil) 185 | if err != nil { 186 | return fmt.Errorf("Serve: %v", err) 187 | } 188 | } else if s.options.HTTPSocketType == HTTPSocketTypeTCP { 189 | // Start the http server. 190 | err := http.ListenAndServe(s.options.HTTPListenAddress, nil) 191 | if err != nil { 192 | return fmt.Errorf("ListenAndServe: %v", err) 193 | } 194 | } else { 195 | return fmt.Errorf("invalid socket options type: %v", s.options.HTTPSocketType) 196 | } 197 | } else { 198 | // HINT: This is only a placeholder until the internal glue TCP server is implemented. 199 | w := make(chan struct{}) 200 | <-w 201 | } 202 | 203 | return nil 204 | } 205 | 206 | // ServeHTTP implements the HTTP Handler interface of the http package. 207 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 208 | s.bs.ServeHTTP(w, r) 209 | } 210 | 211 | //########################// 212 | //### Server - Private ###// 213 | //########################// 214 | 215 | func (s *Server) handleOnNewSocketConnection(bs backend.BackendSocket) { 216 | // Close the socket if incomming connections should be blocked. 217 | if s.IsBlocked() { 218 | bs.Close() 219 | return 220 | } 221 | 222 | // Create a new socket value. 223 | // The goroutines are started automatically. 224 | newSocket(s, bs) 225 | } 226 | -------------------------------------------------------------------------------- /socket.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package glue 20 | 21 | import ( 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "runtime/debug" 26 | "sync" 27 | "time" 28 | 29 | "github.com/sirupsen/logrus" 30 | "github.com/blang/semver" 31 | "github.com/desertbit/glue/backend" 32 | "github.com/desertbit/glue/log" 33 | "github.com/desertbit/glue/utils" 34 | ) 35 | 36 | //#################// 37 | //### Constants ###// 38 | //#################// 39 | 40 | // Public 41 | // ###### 42 | const ( 43 | // Version holds the Glue Socket Protocol Version as string. 44 | // This project follows the Semantic Versioning (http://semver.org/). 45 | Version = "1.9.1" 46 | ) 47 | 48 | // Private 49 | // ####### 50 | const ( 51 | // The constant length of the random socket ID. 52 | socketIDLength = 20 53 | 54 | // Send pings to the peer with this period. 55 | pingPeriod = 30 * time.Second 56 | 57 | // Kill the socket after this timeout. 58 | pingResponseTimeout = 7 * time.Second 59 | 60 | // The main channel name. 61 | mainChannelName = "m" 62 | 63 | // Socket commands. Must be two character long. 64 | // ############################################ 65 | cmdLen = 2 66 | cmdInit = "in" 67 | cmdPing = "pi" 68 | cmdPong = "po" 69 | cmdClose = "cl" 70 | cmdInvalid = "iv" 71 | cmdDontAutoReconnect = "dr" 72 | cmdChannelData = "cd" 73 | ) 74 | 75 | //#################// 76 | //### Variables ###// 77 | //#################// 78 | 79 | // Public errors: 80 | var ( 81 | ErrSocketClosed = errors.New("the socket connection is closed") 82 | ErrReadTimeout = errors.New("the read timeout was reached") 83 | ) 84 | 85 | // Private 86 | var ( 87 | serverVersion semver.Version 88 | ) 89 | 90 | //####################// 91 | //### Public Types ###// 92 | //####################// 93 | 94 | // ClosedChan is a channel which doesn't block as soon as the socket is closed. 95 | type ClosedChan <-chan struct{} 96 | 97 | // OnCloseFunc is an event function. 98 | type OnCloseFunc func() 99 | 100 | // OnReadFunc is an event function. 101 | type OnReadFunc func(data string) 102 | 103 | //#####################// 104 | //### Private Types ###// 105 | //#####################// 106 | 107 | type initData struct { 108 | SocketID string `json:"socketID"` 109 | } 110 | 111 | type clientInitData struct { 112 | Version string `json:"version"` 113 | } 114 | 115 | //###################// 116 | //### Socket Type ###// 117 | //###################// 118 | 119 | // A Socket represents a single socket connections to a client. 120 | type Socket struct { 121 | // A Value is a placeholder for custom data. 122 | // Use this to attach socket specific data. 123 | Value interface{} 124 | 125 | // Private 126 | // ####### 127 | server *Server 128 | bs backend.BackendSocket 129 | 130 | id string // Unique socket ID. 131 | isInitialized bool 132 | 133 | channels *channels 134 | mainChannel *Channel 135 | 136 | writeChan chan string 137 | readChan chan string 138 | isClosedChan ClosedChan 139 | 140 | pingTimer *time.Timer 141 | pingTimeout *time.Timer 142 | sendPingMutex sync.Mutex 143 | pingRequestActive bool 144 | } 145 | 146 | // newSocket creates a new socket and initializes it. 147 | func newSocket(server *Server, bs backend.BackendSocket) *Socket { 148 | // Create a new socket value. 149 | s := &Socket{ 150 | server: server, 151 | bs: bs, 152 | 153 | id: utils.RandomString(socketIDLength), 154 | channels: newChannels(), 155 | 156 | writeChan: bs.WriteChan(), 157 | readChan: bs.ReadChan(), 158 | isClosedChan: bs.ClosedChan(), 159 | 160 | pingTimer: time.NewTimer(pingPeriod), 161 | pingTimeout: time.NewTimer(pingResponseTimeout), 162 | } 163 | 164 | // Create the main channel. 165 | s.mainChannel = s.Channel(mainChannelName) 166 | 167 | // Call the on close method as soon as the socket closes. 168 | go func() { 169 | <-s.isClosedChan 170 | s.onClose() 171 | }() 172 | 173 | // Stop the timeout again. It will be started by the ping timer. 174 | s.pingTimeout.Stop() 175 | 176 | // Add the new socket to the active sockets map. 177 | // If the ID is already present, then generate a new one. 178 | func() { 179 | // Lock the mutex. 180 | s.server.socketsMutex.Lock() 181 | defer s.server.socketsMutex.Unlock() 182 | 183 | // Be sure that the ID is unique. 184 | for { 185 | if _, ok := s.server.sockets[s.id]; !ok { 186 | break 187 | } 188 | 189 | s.id = utils.RandomString(socketIDLength) 190 | } 191 | 192 | // Add the socket to the map. 193 | s.server.sockets[s.id] = s 194 | }() 195 | 196 | // Start the loops and handlers in new goroutines. 197 | go s.pingTimeoutHandler() 198 | go s.readLoop() 199 | go s.pingLoop() 200 | 201 | return s 202 | } 203 | 204 | // ID returns the socket's unique ID. 205 | // This is a cryptographically secure pseudorandom number. 206 | func (s *Socket) ID() string { 207 | return s.id 208 | } 209 | 210 | // IsInitialized returns a boolean indicating if a socket is initialized 211 | // and ready to be used. This flag is set to true after the OnNewSocket function 212 | // has returned for this socket. 213 | func (s *Socket) IsInitialized() bool { 214 | return s.isInitialized 215 | } 216 | 217 | // RemoteAddr returns the remote address of the client. 218 | func (s *Socket) RemoteAddr() string { 219 | return s.bs.RemoteAddr() 220 | } 221 | 222 | // UserAgent returns the user agent of the client. 223 | func (s *Socket) UserAgent() string { 224 | return s.bs.UserAgent() 225 | } 226 | 227 | // Close the socket connection. 228 | func (s *Socket) Close() { 229 | s.bs.Close() 230 | } 231 | 232 | // IsClosed returns a boolean whenever the connection is closed. 233 | func (s *Socket) IsClosed() bool { 234 | return s.bs.IsClosed() 235 | } 236 | 237 | // OnClose sets the functions which is triggered if the socket connection is closed. 238 | // This method can be called multiple times to bind multiple functions. 239 | func (s *Socket) OnClose(f OnCloseFunc) { 240 | go func() { 241 | // Recover panics and log the error. 242 | defer func() { 243 | if e := recover(); e != nil { 244 | log.L.Errorf("glue: panic while calling onClose function: %v\n%s", e, debug.Stack()) 245 | } 246 | }() 247 | 248 | <-s.isClosedChan 249 | f() 250 | }() 251 | } 252 | 253 | // ClosedChan returns a channel which is non-blocking (closed) 254 | // as soon as the socket is closed. 255 | func (s *Socket) ClosedChan() ClosedChan { 256 | return s.isClosedChan 257 | } 258 | 259 | // Write data to the client. 260 | func (s *Socket) Write(data string) { 261 | // Write to the main channel. 262 | s.mainChannel.Write(data) 263 | } 264 | 265 | // Read the next message from the socket. This method is blocking. 266 | // One variadic argument sets a timeout duration. 267 | // If no timeout is specified, this method will block forever. 268 | // ErrSocketClosed is returned, if the socket connection is closed. 269 | // ErrReadTimeout is returned, if the timeout is reached. 270 | func (s *Socket) Read(timeout ...time.Duration) (string, error) { 271 | return s.mainChannel.Read(timeout...) 272 | } 273 | 274 | // OnRead sets the function which is triggered if new data is received. 275 | // If this event function based method of reading data from the socket is used, 276 | // then don't use the socket Read method. 277 | // Either use the OnRead or the Read approach. 278 | func (s *Socket) OnRead(f OnReadFunc) { 279 | s.mainChannel.OnRead(f) 280 | } 281 | 282 | // DiscardRead ignores and discars the data received from the client. 283 | // Call this method during initialization, if you don't read any data from 284 | // the socket. If received data is not discarded, then the read buffer will block as soon 285 | // as it is full, which will also block the keep-alive mechanism of the socket. The result 286 | // would be a closed socket... 287 | func (s *Socket) DiscardRead() { 288 | s.mainChannel.DiscardRead() 289 | } 290 | 291 | //##############################// 292 | //### Private Socket methods ###// 293 | //##############################// 294 | 295 | func (s *Socket) write(rawData string) { 296 | // Write to the stream and check if the buffer is full. 297 | select { 298 | case <-s.isClosedChan: 299 | // Just return because the socket is closed. 300 | return 301 | case s.writeChan <- rawData: 302 | default: 303 | // The buffer if full. No data was send. 304 | // Send a ping. If no pong is received within 305 | // the timeout, the socket is closed. 306 | s.sendPing() 307 | 308 | // Now write the current data to the socket. 309 | // This will block if the buffer is still full. 310 | s.writeChan <- rawData 311 | } 312 | } 313 | 314 | func (s *Socket) onClose() { 315 | // Remove the socket again from the active sockets map. 316 | func() { 317 | // Lock the mutex. 318 | s.server.socketsMutex.Lock() 319 | defer s.server.socketsMutex.Unlock() 320 | 321 | delete(s.server.sockets, s.id) 322 | }() 323 | 324 | // Clear the write channel to release blocked goroutines. 325 | // The pingLoop might be blocked... 326 | for i := 0; i < len(s.writeChan); i++ { 327 | select { 328 | case <-s.writeChan: 329 | default: 330 | break 331 | } 332 | } 333 | } 334 | 335 | func (s *Socket) resetPingTimeout() { 336 | // Lock the mutex. 337 | s.sendPingMutex.Lock() 338 | defer s.sendPingMutex.Unlock() 339 | 340 | // Stop the timeout timer. 341 | s.pingTimeout.Stop() 342 | 343 | // Update the flag. 344 | s.pingRequestActive = false 345 | 346 | // Reset the ping timer again to request 347 | // a pong repsonse during the next timeout. 348 | s.pingTimer.Reset(pingPeriod) 349 | } 350 | 351 | // SendPing sends a ping to the client. If no pong response is 352 | // received within the timeout, the socket will be closed. 353 | // Multiple calls to this method won't send multiple ping requests, 354 | // if a ping request is already active. 355 | func (s *Socket) sendPing() { 356 | // Lock the mutex. 357 | s.sendPingMutex.Lock() 358 | 359 | // Check if a ping request is already active. 360 | if s.pingRequestActive { 361 | // Unlock the mutex again. 362 | s.sendPingMutex.Unlock() 363 | return 364 | } 365 | 366 | // Update the flag and unlock the mutex again. 367 | s.pingRequestActive = true 368 | s.sendPingMutex.Unlock() 369 | 370 | // Start the timeout timer. This will close 371 | // the socket if no pong response is received 372 | // within the timeout. 373 | // Do this before the write. The write channel might block 374 | // if the buffers are full. 375 | s.pingTimeout.Reset(pingResponseTimeout) 376 | 377 | // Send a ping request by writing to the stream. 378 | s.writeChan <- cmdPing 379 | } 380 | 381 | // Close the socket during a ping response timeout. 382 | func (s *Socket) pingTimeoutHandler() { 383 | defer func() { 384 | s.pingTimeout.Stop() 385 | }() 386 | 387 | select { 388 | case <-s.pingTimeout.C: 389 | // Close the socket due to the timeout. 390 | s.bs.Close() 391 | case <-s.isClosedChan: 392 | // Just release this goroutine. 393 | } 394 | } 395 | 396 | func (s *Socket) pingLoop() { 397 | defer func() { 398 | // Stop the timeout timer. 399 | s.pingTimeout.Stop() 400 | 401 | // Stop the ping timer. 402 | s.pingTimer.Stop() 403 | }() 404 | 405 | for { 406 | select { 407 | case <-s.pingTimer.C: 408 | // Send a ping. If no pong is received within 409 | // the timeout, the socket is closed. 410 | s.sendPing() 411 | 412 | case <-s.isClosedChan: 413 | // Just exit the loop. 414 | return 415 | } 416 | } 417 | } 418 | 419 | func (s *Socket) readLoop() { 420 | // Wait for data received from the read channel. 421 | for { 422 | select { 423 | case data := <-s.readChan: 424 | // Reset the ping timeout. 425 | s.resetPingTimeout() 426 | 427 | // Get the command. The command is always prepended to the data message. 428 | cmd := data[:cmdLen] 429 | data = data[cmdLen:] 430 | 431 | // Handle the received data and log error messages. 432 | if err := s.handleRead(cmd, data); err != nil { 433 | log.L.WithFields(logrus.Fields{ 434 | "remoteAddress": s.RemoteAddr(), 435 | "userAgent": s.UserAgent(), 436 | "cmd": cmd, 437 | }).Warningf("glue: handle received data: %v", err) 438 | } 439 | case <-s.isClosedChan: 440 | // Just exit the loop 441 | return 442 | } 443 | } 444 | } 445 | 446 | func (s *Socket) handleRead(cmd, data string) error { 447 | // Perform the command request. 448 | switch cmd { 449 | case cmdPing: 450 | // Send a pong reply. 451 | s.write(cmdPong) 452 | 453 | case cmdPong: 454 | // Don't do anything, The ping timer was already reset. 455 | 456 | case cmdClose: 457 | // Close the socket. 458 | s.bs.Close() 459 | 460 | case cmdInit: 461 | // Handle the initialization. 462 | initSocket(s, data) 463 | 464 | case cmdChannelData: 465 | // Unmarshal the channel name and data string. 466 | name, data, err := utils.UnmarshalValues(data) 467 | if err != nil { 468 | return err 469 | } 470 | 471 | // Push the data to the corresponding channel. 472 | if err = s.channels.triggerReadForChannel(name, data); err != nil { 473 | return err 474 | } 475 | default: 476 | // Send an invalid command response. 477 | s.write(cmdInvalid) 478 | 479 | // Return an error. 480 | return fmt.Errorf("received invalid socket command") 481 | } 482 | 483 | return nil 484 | } 485 | 486 | //###############// 487 | //### Private ###// 488 | //###############// 489 | 490 | func init() { 491 | var err error 492 | 493 | // Parses the server version string and returns a validated Version. 494 | serverVersion, err = semver.Make(Version) 495 | if err != nil { 496 | log.L.Fatalf("failed to parse glue server protocol version: %v", err) 497 | } 498 | } 499 | 500 | func initSocket(s *Socket, dataJSON string) { 501 | // Handle the socket initialization in an anonymous function 502 | // to handle the error in a clean and simple way. 503 | dontAutoReconnect, err := func() (bool, error) { 504 | // Handle received initialization data: 505 | // #################################### 506 | 507 | // Unmarshal the data JSON. 508 | var cData clientInitData 509 | err := json.Unmarshal([]byte(dataJSON), &cData) 510 | if err != nil { 511 | return false, fmt.Errorf("json unmarshal init data: %v", err) 512 | } 513 | 514 | // Parses the client version string and returns a validated Version. 515 | clientVersion, err := semver.Make(cData.Version) 516 | if err != nil { 517 | return false, fmt.Errorf("invalid client protocol version: %v", err) 518 | } 519 | 520 | // Check if the client protocol version is supported. 521 | if clientVersion.Major != serverVersion.Major || 522 | clientVersion.Minor > serverVersion.Minor || 523 | (clientVersion.Minor == serverVersion.Minor && clientVersion.Patch > serverVersion.Patch) { 524 | // The client should not automatically reconnect. Return true... 525 | return true, fmt.Errorf("client socket protocol version is not supported: %s", cData.Version) 526 | } 527 | 528 | // Send initialization data: 529 | // ######################### 530 | 531 | // Create the new initialization data value. 532 | data := initData{ 533 | SocketID: s.ID(), 534 | } 535 | 536 | // Marshal the data to a JSON string. 537 | dataJSON, err := json.Marshal(&data) 538 | if err != nil { 539 | return false, fmt.Errorf("json marshal init data: %v", err) 540 | } 541 | 542 | // Send the init data to the client. 543 | s.write(cmdInit + string(dataJSON)) 544 | 545 | return false, nil 546 | }() 547 | 548 | // Handle the error. 549 | if err != nil { 550 | if dontAutoReconnect { 551 | // Tell the client to not automatically reconnect. 552 | s.write(cmdDontAutoReconnect) 553 | 554 | // Pause to be sure that the previous socket command gets send to the client. 555 | time.Sleep(time.Second) 556 | } 557 | 558 | // Close the socket. 559 | s.Close() 560 | 561 | // Log the error. 562 | log.L.WithFields(logrus.Fields{ 563 | "remoteAddress": s.RemoteAddr(), 564 | "userAgent": s.UserAgent(), 565 | }).Warningf("glue: init socket: %v", err) 566 | 567 | return 568 | } 569 | 570 | // Trigger the on new socket event function. 571 | func() { 572 | // Recover panics and log the error. 573 | defer func() { 574 | if e := recover(); e != nil { 575 | // Close the socket and log the error message. 576 | s.Close() 577 | log.L.Errorf("glue: panic while calling on new socket function: %v\n%s", e, debug.Stack()) 578 | } 579 | }() 580 | 581 | // Trigger the event function. 582 | s.server.onNewSocket(s) 583 | }() 584 | 585 | // Update the initialized flag. 586 | s.isInitialized = true 587 | } 588 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Package utils provides utilities for the glue socket implementation. 20 | package utils 21 | 22 | import ( 23 | "crypto/rand" 24 | "fmt" 25 | "net/http" 26 | "strconv" 27 | "strings" 28 | ) 29 | 30 | //#################// 31 | //### Constants ###// 32 | //#################// 33 | 34 | const ( 35 | delimiter = "&" 36 | ) 37 | 38 | //########################// 39 | //### Public Functions ###// 40 | //########################// 41 | 42 | // RandomString generates a random string. 43 | func RandomString(n int) string { 44 | const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 45 | var bytes = make([]byte, n) 46 | rand.Read(bytes) 47 | for i, b := range bytes { 48 | bytes[i] = alphanum[b%byte(len(alphanum))] 49 | } 50 | return string(bytes) 51 | } 52 | 53 | // UnmarshalValues splits two values from a single string. 54 | // This function is chainable to extract multiple values. 55 | func UnmarshalValues(data string) (first, second string, err error) { 56 | // Find the delimiter. 57 | pos := strings.Index(data, delimiter) 58 | if pos < 0 { 59 | err = fmt.Errorf("unmarshal values: no delimiter found: '%s'", data) 60 | return 61 | } 62 | 63 | // Extract the value length integer of the first value. 64 | l, err := strconv.Atoi(data[:pos]) 65 | if err != nil { 66 | err = fmt.Errorf("invalid value length: '%s'", data[:pos]) 67 | return 68 | } 69 | 70 | // Remove the value length from the data string. 71 | data = data[pos+1:] 72 | 73 | // Validate the value length. 74 | if l < 0 || l > len(data) { 75 | err = fmt.Errorf("invalid value length: out of bounds: '%v'", l) 76 | return 77 | } 78 | 79 | // Split the first value from the second. 80 | first = data[:l] 81 | second = data[l:] 82 | 83 | return 84 | } 85 | 86 | // MarshalValues joins two values into a single string. 87 | // They can be decoded by the UnmarshalValues function. 88 | func MarshalValues(first, second string) string { 89 | return strconv.Itoa(len(first)) + delimiter + first + second 90 | } 91 | 92 | // RemoteAddress returns the IP address of the request. 93 | // If the X-Forwarded-For or X-Real-Ip http headers are set, then 94 | // they are used to obtain the remote address. 95 | // The boolean is true, if the remote address is obtained using the 96 | // request RemoteAddr() method. 97 | func RemoteAddress(r *http.Request) (string, bool) { 98 | hdr := r.Header 99 | 100 | // Try to obtain the ip from the X-Forwarded-For header 101 | ip := hdr.Get("X-Forwarded-For") 102 | if ip != "" { 103 | // X-Forwarded-For is potentially a list of addresses separated with "," 104 | parts := strings.Split(ip, ",") 105 | if len(parts) > 0 { 106 | ip = strings.TrimSpace(parts[0]) 107 | 108 | if ip != "" { 109 | return ip, false 110 | } 111 | } 112 | } 113 | 114 | // Try to obtain the ip from the X-Real-Ip header 115 | ip = strings.TrimSpace(hdr.Get("X-Real-Ip")) 116 | if ip != "" { 117 | return ip, false 118 | } 119 | 120 | // Fallback to the request remote address 121 | return RemovePortFromRemoteAddr(r.RemoteAddr), true 122 | } 123 | 124 | // RemovePortFromRemoteAddr removes the port if present from the remote address. 125 | func RemovePortFromRemoteAddr(remoteAddr string) string { 126 | pos := strings.LastIndex(remoteAddr, ":") 127 | if pos < 0 { 128 | return remoteAddr 129 | } 130 | 131 | return remoteAddr[:pos] 132 | } 133 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Glue - Robust Go and Javascript Socket Library 3 | * Copyright (C) 2015 Roland Singer 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package utils 20 | 21 | import ( 22 | "testing" 23 | ) 24 | 25 | func TestUnmarshalValues(t *testing.T) { 26 | first, second, err := UnmarshalValues(MarshalValues("1", "2")) 27 | if err != nil { 28 | t.Error(err.Error()) 29 | } else if first != "1" || second != "2" { 30 | t.Fail() 31 | } 32 | 33 | first, second, err = UnmarshalValues(MarshalValues("1s"+delimiter+"jsd", "efsf2"+delimiter+"9as")) 34 | if err != nil { 35 | t.Error(err.Error()) 36 | } else if first != "1s"+delimiter+"jsd" || second != "efsf2"+delimiter+"9as" { 37 | t.Fail() 38 | } 39 | 40 | first, second, err = UnmarshalValues("11" + delimiter + "firstsecond") 41 | if err != nil { 42 | t.Error(err.Error()) 43 | } else if first != "firstsecond" || second != "" { 44 | t.Fail() 45 | } 46 | 47 | first, second, err = UnmarshalValues("12" + delimiter + "firstsecond") 48 | if err == nil { 49 | t.Fail() 50 | } 51 | } 52 | --------------------------------------------------------------------------------