diff --git a/HISTORY.md b/HISTORY.md index 88d5db59..f7f25ff3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -37,6 +37,7 @@ Changes: - Remove `context.RenderTemplateSource` you should make a new template file and use the `iris.Render` to specify an `io.Writer` like `bytes.Buffer` - Remove `plugins`, replaced with more pluggable echosystem that I designed from zero on this release, named `Policy` [Adaptors](https://github.com/kataras/iris/tree/master/adaptors) (all plugins have been converted, fixed and improvement, except the iriscontrol). - `context.Log(string,...interface{})` -> `context.Log(iris.LogMode, string)` +- Remove `.Config.Websocket` , replaced with the `kataras/iris/adaptors/websocket.Config` adaptor. - https://github.com/iris-contrib/plugin -> https://github.com/iris-contrib/adaptors @@ -859,18 +860,208 @@ editors worked before but I couldn't let some developers without support. ### Websockets -There are many internal improvements to the [websocket server](https://github.com/kataras/go-websocket), and it's -operating slighty faster. +There are many internal improvements to the websocket server, it +operates slighty faster to. -The kataras/go-websocket library, which `app.OnConnection` is refering to, will not be changed, its API will still remain. -I am not putting anything new there (I doubt if any bug is available to fix, it's very simple and it just works). -I started the kataras/go-websocket back then because I wanted a simple and fast websocket server for -the fasthttp iris' version and back then no one did that before. -Now(after v6) iris is compatible with any net/http websocket library that already created by third-parties. +Websocket is an Adaptor too and you can edit more configuration fields than before. +No Write and Read timeout by default, you have to set the fields if you want to enable timeout. -If the iris' websocket feature does not cover your app's needs, you can simple use any other -library for websockets, like the Golang's compatible to `socket.io`, example: +Below you'll see the before and the after, keep note that the static and templates didn't changed, so I am not putting the whole +html and javascript sources here, you can run the full examples from [here](https://github.com/kataras/iris/tree/6.2/adaptors/websocket/_examples). + +**BEFORE:*** + +```go + +package main + +import ( + "fmt" // optional + + "github.com/kataras/iris" +) + +type clientPage struct { + Title string + Host string +} + +func main() { + iris.StaticWeb("/js", "./static/js") + + iris.Get("/", func(ctx *iris.Context) { + ctx.Render("client.html", clientPage{"Client Page", ctx.Host()}) + }) + + // the path which the websocket client should listen/registed to -> + iris.Config.Websocket.Endpoint = "/my_endpoint" + // by-default all origins are accepted, you can change this behavior by setting: + // iris.Config.Websocket.CheckOrigin + + var myChatRoom = "room1" + iris.Websocket.OnConnection(func(c iris.WebsocketConnection) { + // Request returns the (upgraded) *http.Request of this connection + // avoid using it, you normally don't need it, + // websocket has everything you need to authenticate the user BUT if it's necessary + // then you use it to receive user information, for example: from headers. + + // httpRequest := c.Request() + // fmt.Printf("Headers for the connection with ID: %s\n\n", c.ID()) + // for k, v := range httpRequest.Header { + // fmt.Printf("%s = '%s'\n", k, strings.Join(v, ", ")) + // } + + // join to a room (optional) + c.Join(myChatRoom) + + c.On("chat", func(message string) { + if message == "leave" { + c.Leave(myChatRoom) + c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.") + c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.") + return + } + // to all except this connection -> + // c.To(iris.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message) + // to all connected clients: c.To(iris.All) + + // to the client itself -> + //c.Emit("chat", "Message from myself: "+message) + + //send the message to the whole room, + //all connections are inside this room will receive this message + c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message) + }) + + // or create a new leave event + // c.On("leave", func() { + // c.Leave(myChatRoom) + // }) + + c.OnDisconnect(func() { + fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID()) + + }) + }) + + iris.Listen(":8080") +} + + + +``` + + +**AFTER** +```go +package main + +import ( + "fmt" // optional + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" + "gopkg.in/kataras/iris.v6/adaptors/websocket" +) + +type clientPage struct { + Title string + Host string +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates + + ws := websocket.New(websocket.Config{ + // the path which the websocket client should listen/registed to, + Endpoint: "/my_endpoint", + // the client-side javascript static file path + // which will be served by Iris. + // default is /iris-ws.js + // if you change that you have to change the bottom of templates/client.html + // script tag: + ClientSourcePath: "/iris-ws.js", + // + // Set the timeouts, 0 means no timeout + // websocket has more configuration, go to ../../config.go for more: + // WriteTimeout: 0, + // ReadTimeout: 0, + // by-default all origins are accepted, you can change this behavior by setting: + // CheckOrigin: (r *http.Request ) bool {}, + // + // + // IDGenerator used to create (and later on, set) + // an ID for each incoming websocket connections (clients). + // The request is an argument which you can use to generate the ID (from headers for example). + // If empty then the ID is generated by DefaultIDGenerator: randomString(64): + // IDGenerator func(ctx *iris.Context) string {}, + }) + + app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint + + app.StaticWeb("/js", "./static/js") // serve our custom javascript code + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("client.html", clientPage{"Client Page", ctx.Host()}) + }) + + var myChatRoom = "room1" + + ws.OnConnection(func(c websocket.Connection) { + // Context returns the (upgraded) *iris.Context of this connection + // avoid using it, you normally don't need it, + // websocket has everything you need to authenticate the user BUT if it's necessary + // then you use it to receive user information, for example: from headers. + + // ctx := c.Context() + + // join to a room (optional) + c.Join(myChatRoom) + + c.On("chat", func(message string) { + if message == "leave" { + c.Leave(myChatRoom) + c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.") + c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.") + return + } + // to all except this connection -> + // c.To(websocket.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message) + // to all connected clients: c.To(websocket.All) + + // to the client itself -> + //c.Emit("chat", "Message from myself: "+message) + + //send the message to the whole room, + //all connections are inside this room will receive this message + c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message) + }) + + // or create a new leave event + // c.On("leave", func() { + // c.Leave(myChatRoom) + // }) + + c.OnDisconnect(func() { + fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID()) + }) + }) + + app.Listen(":8080") +} + +``` + + + + +If the iris' websocket feature does not cover your app's needs, you can simply use any other +library for websockets that you used to use, like the Golang's compatible to `socket.io`, simple example: ```go package main diff --git a/adaptors/httprouter/httprouter.go b/adaptors/httprouter/httprouter.go index 11802ee3..bb030490 100644 --- a/adaptors/httprouter/httprouter.go +++ b/adaptors/httprouter/httprouter.go @@ -68,30 +68,30 @@ type ( var ( errMuxEntryConflictsWildcard = errors.New(` - Router: '%s' in new path '%s' + httprouter: '%s' in new path '%s' conflicts with existing wildcarded route with path: '%s' in existing prefix of'%s' `) errMuxEntryMiddlewareAlreadyExists = errors.New(` - Router: Middleware were already registered for the path: '%s'`) + httprouter: Middleware were already registered for the path: '%s'`) errMuxEntryInvalidWildcard = errors.New(` - Router: More than one wildcard found in the path part: '%s' in route's path: '%s'`) + httprouter: More than one wildcard found in the path part: '%s' in route's path: '%s'`) errMuxEntryConflictsExistingWildcard = errors.New(` - Router: Wildcard for route path: '%s' conflicts with existing children in route path: '%s'`) + httprouter: Wildcard for route path: '%s' conflicts with existing children in route path: '%s'`) errMuxEntryWildcardUnnamed = errors.New(` - Router: Unnamed wildcard found in path: '%s'`) + httprouter: Unnamed wildcard found in path: '%s'`) errMuxEntryWildcardInvalidPlace = errors.New(` - Router: Wildcard is only allowed at the end of the path, in the route path: '%s'`) + httprouter: Wildcard is only allowed at the end of the path, in the route path: '%s'`) errMuxEntryWildcardConflictsMiddleware = errors.New(` - Router: Wildcard conflicts with existing middleware for the route path: '%s'`) + httprouter: Wildcard conflicts with existing middleware for the route path: '%s'`) errMuxEntryWildcardMissingSlash = errors.New(` - Router: No slash(/) were found before wildcard in the route path: '%s'`) + httprouter: No slash(/) were found before wildcard in the route path: '%s'`) ) // getParamsLen returns the parameters length from a given path @@ -575,7 +575,7 @@ func New() iris.Policies { // while ProdMode means that the iris should not continue running // by-default it panics on these errors, but to make sure let's introduce the fatalErr to stop visiting fatalErr = true - logger(iris.ProdMode, "fatal error on httprouter build adaptor: "+err.Error()) + logger(iris.ProdMode, err.Error()) return } diff --git a/adaptors/view/LICENSE b/adaptors/view/LICENSE new file mode 100644 index 00000000..2935ad5d --- /dev/null +++ b/adaptors/view/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/adaptors/websocket/LICENSE b/adaptors/websocket/LICENSE new file mode 100644 index 00000000..cbd1a2ca --- /dev/null +++ b/adaptors/websocket/LICENSE @@ -0,0 +1,44 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/adaptors/websocket/_examples/websocket/main.go b/adaptors/websocket/_examples/websocket/main.go new file mode 100644 index 00000000..be69bb08 --- /dev/null +++ b/adaptors/websocket/_examples/websocket/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "fmt" // optional + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" + "gopkg.in/kataras/iris.v6/adaptors/websocket" +) + +type clientPage struct { + Title string + Host string +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates + + ws := websocket.New(websocket.Config{ + // the path which the websocket client should listen/registed to, + Endpoint: "/my_endpoint", + // the client-side javascript static file path + // which will be served by Iris. + // default is /iris-ws.js + // if you change that you have to change the bottom of templates/client.html + // script tag: + ClientSourcePath: "/iris-ws.js", + // + // Set the timeouts, 0 means no timeout + // websocket has more configuration, go to ../../config.go for more: + // WriteTimeout: 0, + // ReadTimeout: 0, + // by-default all origins are accepted, you can change this behavior by setting: + // CheckOrigin: (r *http.Request ) bool {}, + // + // + // IDGenerator used to create (and later on, set) + // an ID for each incoming websocket connections (clients). + // The request is an argument which you can use to generate the ID (from headers for example). + // If empty then the ID is generated by DefaultIDGenerator: randomString(64): + // IDGenerator func(ctx *iris.Context) string {}, + }) + + app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint + + app.StaticWeb("/js", "./static/js") // serve our custom javascript code + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("client.html", clientPage{"Client Page", ctx.Host()}) + }) + + var myChatRoom = "room1" + + ws.OnConnection(func(c websocket.Connection) { + // Context returns the (upgraded) *iris.Context of this connection + // avoid using it, you normally don't need it, + // websocket has everything you need to authenticate the user BUT if it's necessary + // then you use it to receive user information, for example: from headers. + + // ctx := c.Context() + + // join to a room (optional) + c.Join(myChatRoom) + + c.On("chat", func(message string) { + if message == "leave" { + c.Leave(myChatRoom) + c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.") + c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.") + return + } + // to all except this connection -> + // c.To(websocket.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message) + // to all connected clients: c.To(websocket.All) + + // to the client itself -> + //c.Emit("chat", "Message from myself: "+message) + + //send the message to the whole room, + //all connections are inside this room will receive this message + c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message) + }) + + // or create a new leave event + // c.On("leave", func() { + // c.Leave(myChatRoom) + // }) + + c.OnDisconnect(func() { + fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID()) + }) + }) + + app.Listen(":8080") +} diff --git a/adaptors/websocket/_examples/websocket/static/js/chat.js b/adaptors/websocket/_examples/websocket/static/js/chat.js new file mode 100644 index 00000000..920a2050 --- /dev/null +++ b/adaptors/websocket/_examples/websocket/static/js/chat.js @@ -0,0 +1,38 @@ +var messageTxt; +var messages; + +$(function () { + + messageTxt = $("#messageTxt"); + messages = $("#messages"); + + + w = new Ws("ws://" + HOST + "/my_endpoint"); + w.OnConnect(function () { + console.log("Websocket connection established"); + }); + + w.OnDisconnect(function () { + appendMessage($("<div><center><h3>Disconnected</h3></center></div>")); + }); + + w.On("chat", function (message) { + appendMessage($("<div>" + message + "</div>")); + }); + + $("#sendBtn").click(function () { + w.Emit("chat", messageTxt.val().toString()); + messageTxt.val(""); + }); + +}) + + +function appendMessage(messageDiv) { + var theDiv = messages[0]; + var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight; + messageDiv.appendTo(messages); + if (doScroll) { + theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight; + } +} diff --git a/adaptors/websocket/_examples/websocket/templates/client.html b/adaptors/websocket/_examples/websocket/templates/client.html new file mode 100644 index 00000000..04ad2115 --- /dev/null +++ b/adaptors/websocket/_examples/websocket/templates/client.html @@ -0,0 +1,24 @@ +<html> + +<head> +<title>{{ .Title}}</title> +</head> + +<body> + <div id="messages" + style="border-width: 1px; border-style: solid; height: 400px; width: 375px;"> + + </div> + <input type="text" id="messageTxt" /> + <button type="button" id="sendBtn">Send</button> + <script type="text/javascript"> + var HOST = {{.Host}} + </script> + <script src="js/vendor/jquery-2.2.3.min.js" type="text/javascript"></script> + <!-- This is auto-serving by the Iris, you don't need to have this file in your disk--> + <script src="/iris-ws.js" type="text/javascript"></script> + <!-- --> + <script src="js/chat.js" type="text/javascript"></script> +</body> + +</html> diff --git a/adaptors/websocket/_examples/websocket_connectionlist/main.go b/adaptors/websocket/_examples/websocket_connectionlist/main.go new file mode 100644 index 00000000..1127a944 --- /dev/null +++ b/adaptors/websocket/_examples/websocket_connectionlist/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "fmt" + "sync" + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" + "gopkg.in/kataras/iris.v6/adaptors/websocket" +) + +type clientPage struct { + Title string + Host string +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates + + ws := websocket.New(websocket.Config{ + // the path which the websocket client should listen/registed to, + Endpoint: "/my_endpoint", + // the client-side javascript static file path + // which will be served by Iris. + // default is /iris-ws.js + // if you change that you have to change the bottom of templates/client.html + // script tag: + ClientSourcePath: "/iris-ws.js", + // + // Set the timeouts, 0 means no timeout + // websocket has more configuration, go to ../../config.go for more: + // WriteTimeout: 0, + // ReadTimeout: 0, + // by-default all origins are accepted, you can change this behavior by setting: + // CheckOrigin: (r *http.Request ) bool {}, + // + // + // IDGenerator used to create (and later on, set) + // an ID for each incoming websocket connections (clients). + // The request is an argument which you can use to generate the ID (from headers for example). + // If empty then the ID is generated by DefaultIDGenerator: randomString(64): + // IDGenerator func(ctx *iris.Context) string {}, + }) + + app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint + + app.StaticWeb("/js", "./static/js") // serve our custom javascript code + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("client.html", clientPage{"Client Page", ctx.ServerHost()}) + }) + + Conn := make(map[websocket.Connection]bool) + var myChatRoom = "room1" + var mutex = new(sync.Mutex) + + ws.OnConnection(func(c websocket.Connection) { + c.Join(myChatRoom) + mutex.Lock() + Conn[c] = true + mutex.Unlock() + c.On("chat", func(message string) { + if message == "leave" { + c.Leave(myChatRoom) + c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.") + c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.") + return + } + }) + c.OnDisconnect(func() { + mutex.Lock() + delete(Conn, c) + mutex.Unlock() + fmt.Printf("\nConnection with ID: %s has been disconnected!\n", c.ID()) + }) + }) + + var delay = 1 * time.Second + go func() { + i := 0 + for { + mutex.Lock() + broadcast(Conn, fmt.Sprintf("aaaa %d\n", i)) + mutex.Unlock() + time.Sleep(delay) + i++ + } + }() + + go func() { + i := 0 + for { + mutex.Lock() + broadcast(Conn, fmt.Sprintf("aaaa2 %d\n", i)) + mutex.Unlock() + time.Sleep(delay) + i++ + } + }() + + app.Listen(":8080") +} + +func broadcast(Conn map[websocket.Connection]bool, message string) { + for k := range Conn { + k.To("room1").Emit("chat", message) + } +} diff --git a/adaptors/websocket/_examples/websocket_connectionlist/static/js/chat.js b/adaptors/websocket/_examples/websocket_connectionlist/static/js/chat.js new file mode 100644 index 00000000..920a2050 --- /dev/null +++ b/adaptors/websocket/_examples/websocket_connectionlist/static/js/chat.js @@ -0,0 +1,38 @@ +var messageTxt; +var messages; + +$(function () { + + messageTxt = $("#messageTxt"); + messages = $("#messages"); + + + w = new Ws("ws://" + HOST + "/my_endpoint"); + w.OnConnect(function () { + console.log("Websocket connection established"); + }); + + w.OnDisconnect(function () { + appendMessage($("<div><center><h3>Disconnected</h3></center></div>")); + }); + + w.On("chat", function (message) { + appendMessage($("<div>" + message + "</div>")); + }); + + $("#sendBtn").click(function () { + w.Emit("chat", messageTxt.val().toString()); + messageTxt.val(""); + }); + +}) + + +function appendMessage(messageDiv) { + var theDiv = messages[0]; + var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight; + messageDiv.appendTo(messages); + if (doScroll) { + theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight; + } +} diff --git a/adaptors/websocket/_examples/websocket_connectionlist/templates/client.html b/adaptors/websocket/_examples/websocket_connectionlist/templates/client.html new file mode 100644 index 00000000..04ad2115 --- /dev/null +++ b/adaptors/websocket/_examples/websocket_connectionlist/templates/client.html @@ -0,0 +1,24 @@ +<html> + +<head> +<title>{{ .Title}}</title> +</head> + +<body> + <div id="messages" + style="border-width: 1px; border-style: solid; height: 400px; width: 375px;"> + + </div> + <input type="text" id="messageTxt" /> + <button type="button" id="sendBtn">Send</button> + <script type="text/javascript"> + var HOST = {{.Host}} + </script> + <script src="js/vendor/jquery-2.2.3.min.js" type="text/javascript"></script> + <!-- This is auto-serving by the Iris, you don't need to have this file in your disk--> + <script src="/iris-ws.js" type="text/javascript"></script> + <!-- --> + <script src="js/chat.js" type="text/javascript"></script> +</body> + +</html> diff --git a/adaptors/websocket/_examples/websocket_custom_go_client/main.go b/adaptors/websocket/_examples/websocket_custom_go_client/main.go new file mode 100644 index 00000000..dfb7b43e --- /dev/null +++ b/adaptors/websocket/_examples/websocket_custom_go_client/main.go @@ -0,0 +1,179 @@ +package main + +// Run first `go run main.go server` +// and `go run main.go client` as many times as you want. +// Originally written by: github.com/antlaw to describe an old kataras/go-websocket's issue +// which you can find here: https://github.com/kataras/go-websocket/issues/24 +import ( + "fmt" + "os" + "strings" + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/websocket" + + xwebsocket "golang.org/x/net/websocket" +) + +// WS is the current websocket connection +var WS *xwebsocket.Conn + +func main() { + if len(os.Args) == 2 && strings.ToLower(os.Args[1]) == "server" { + ServerLoop() + } else if len(os.Args) == 2 && strings.ToLower(os.Args[1]) == "client" { + ClientLoop() + } else { + fmt.Println("wsserver [server|client]") + } +} + +///////////////////////////////////////////////////////////////////////// +// client side +func sendUntilErr(sendInterval int) { + i := 1 + for { + time.Sleep(time.Duration(sendInterval) * time.Second) + err := SendMessage("2", "all", "objectupdate", "2.UsrSchedule_v1_1") + if err != nil { + fmt.Println("failed to send join message", err.Error()) + return + } + fmt.Println("objectupdate", i) + i++ + } +} + +func recvUntilErr() { + var msg = make([]byte, 2048) + var n int + var err error + i := 1 + for { + if n, err = WS.Read(msg); err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("%v Received: %s.%v\n", time.Now(), string(msg[:n]), i) + i++ + } + +} + +//ConnectWebSocket connect a websocket to host +func ConnectWebSocket() error { + var origin = "http://localhost/" + var url = "ws://localhost:9090/socket" + var err error + WS, err = xwebsocket.Dial(url, "", origin) + return err +} + +// CloseWebSocket closes the current websocket connection +func CloseWebSocket() error { + if WS != nil { + return WS.Close() + } + return nil +} + +// SendMessage broadcast a message to server +func SendMessage(serverID, to, method, message string) error { + buffer := []byte(message) + return SendtBytes(serverID, to, method, buffer) +} + +// SendtBytes broadcast a message to server +func SendtBytes(serverID, to, method string, message []byte) error { + // look https://github.com/kataras/iris/tree/master/adaptors/websocket/message.go , client.go and client.js + // to understand the buffer line: + buffer := []byte(fmt.Sprintf("iris-websocket-message:%v;0;%v;%v;", method, serverID, to)) + buffer = append(buffer, message...) + _, err := WS.Write(buffer) + if err != nil { + fmt.Println(err) + return err + } + return nil +} + +// ClientLoop connects to websocket server, the keep send and recv dataS +func ClientLoop() { + for { + time.Sleep(time.Second) + err := ConnectWebSocket() + if err != nil { + fmt.Println("failed to connect websocket", err.Error()) + continue + } + // time.Sleep(time.Second) + err = SendMessage("2", "all", "join", "dummy2") + go sendUntilErr(1) + recvUntilErr() + err = CloseWebSocket() + if err != nil { + fmt.Println("failed to close websocket", err.Error()) + } + } + +} + +///////////////////////////////////////////////////////////////////////// +// server side + +// OnConnect handles incoming websocket connection +func OnConnect(c websocket.Connection) { + fmt.Println("socket.OnConnect()") + c.On("join", func(message string) { OnJoin(message, c) }) + c.On("objectupdate", func(message string) { OnObjectUpdated(message, c) }) + // ok works too c.EmitMessage([]byte("dsadsa")) + c.OnDisconnect(func() { OnDisconnect(c) }) + +} + +// ServerLoop listen and serve websocket requests +func ServerLoop() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + + ws := websocket.New(websocket.Config{Endpoint: "/socket"}) + app.Adapt(ws) + + ws.OnConnection(OnConnect) + app.Listen("0.0.0.0:9090") + +} + +// OnJoin handles Join broadcast group request +func OnJoin(message string, c websocket.Connection) { + t := time.Now() + c.Join("server2") + fmt.Println("OnJoin() time taken:", time.Since(t)) +} + +// OnObjectUpdated broadcasts to all client an incoming message +func OnObjectUpdated(message string, c websocket.Connection) { + t := time.Now() + s := strings.Split(message, ";") + if len(s) != 3 { + fmt.Println("OnObjectUpdated() invalid message format:" + message) + return + } + serverID, _, objectID := s[0], s[1], s[2] + err := c.To("server"+serverID).Emit("objectupdate", objectID) + if err != nil { + fmt.Println(err, "failed to broacast object") + return + } + fmt.Println(fmt.Sprintf("OnObjectUpdated() message:%v, time taken: %v", message, time.Since(t))) +} + +// OnDisconnect clean up things when a client is disconnected +func OnDisconnect(c websocket.Connection) { + c.Leave("server2") + fmt.Println("OnDisconnect(): client disconnected!") + +} diff --git a/adaptors/websocket/_examples/websocket_native_messages/main.go b/adaptors/websocket/_examples/websocket_native_messages/main.go new file mode 100644 index 00000000..4634e196 --- /dev/null +++ b/adaptors/websocket/_examples/websocket_native_messages/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" + "gopkg.in/kataras/iris.v6/adaptors/websocket" +) + +/* Native messages no need to import the iris-ws.js to the ./templates.client.html +Use of: OnMessage and EmitMessage. + + +NOTICE: IF YOU HAVE RAN THE PREVIOUS EXAMPLES YOU HAVE TO CLEAR YOUR BROWSER's CACHE +BECAUSE chat.js is different than the CACHED. OTHERWISE YOU WILL GET Ws is undefined from the browser's console, becuase it will use the cached. +*/ + +type clientPage struct { + Title string + Host string +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates + + ws := websocket.New(websocket.Config{ + // the path which the websocket client should listen/registed to, + Endpoint: "/my_endpoint", + // to enable binary messages (useful for protobuf): + // BinaryMessages: true, + }) + + app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint + + app.StaticWeb("/js", "./static/js") // serve our custom javascript code + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("client.html", clientPage{"Client Page", ctx.Host()}) + }) + + ws.OnConnection(func(c websocket.Connection) { + + c.OnMessage(func(data []byte) { + message := string(data) + c.To(websocket.Broadcast).EmitMessage([]byte("Message from: " + c.ID() + "-> " + message)) // broadcast to all clients except this + c.EmitMessage([]byte("Me: " + message)) // writes to itself + }) + + c.OnDisconnect(func() { + fmt.Printf("\nConnection with ID: %s has been disconnected!", c.ID()) + }) + + }) + + app.Listen(":8080") + +} diff --git a/adaptors/websocket/_examples/websocket_native_messages/static/js/chat.js b/adaptors/websocket/_examples/websocket_native_messages/static/js/chat.js new file mode 100644 index 00000000..344ea3b7 --- /dev/null +++ b/adaptors/websocket/_examples/websocket_native_messages/static/js/chat.js @@ -0,0 +1,38 @@ +var messageTxt; +var messages; + +$(function () { + + messageTxt = $("#messageTxt"); + messages = $("#messages"); + + + w = new WebSocket("ws://" + HOST + "/my_endpoint"); + w.onopen = function () { + console.log("Websocket connection enstablished"); + }; + + w.onclose = function () { + appendMessage($("<div><center><h3>Disconnected</h3></center></div>")); + }; + w.onmessage = function(message){ + appendMessage($("<div>" + message.data + "</div>")); + }; + + + $("#sendBtn").click(function () { + w.send(messageTxt.val().toString()); + messageTxt.val(""); + }); + +}) + + +function appendMessage(messageDiv) { + var theDiv = messages[0]; + var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight; + messageDiv.appendTo(messages); + if (doScroll) { + theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight; + } +} diff --git a/adaptors/websocket/_examples/websocket_native_messages/templates/client.html b/adaptors/websocket/_examples/websocket_native_messages/templates/client.html new file mode 100644 index 00000000..312a4143 --- /dev/null +++ b/adaptors/websocket/_examples/websocket_native_messages/templates/client.html @@ -0,0 +1,21 @@ +<html> + +<head> +<title>{{ .Title}}</title> +</head> + +<body> + <div id="messages" + style="border-width: 1px; border-style: solid; height: 400px; width: 375px;"> + + </div> + <input type="text" id="messageTxt" /> + <button type="button" id="sendBtn">Send</button> + <script type="text/javascript"> + var HOST = {{.Host}} + </script> + <script src="js/vendor/jquery-2.2.3.min.js" type="text/javascript"></script> + <script src="js/chat.js" type="text/javascript"></script> +</body> + +</html> diff --git a/adaptors/websocket/client.go b/adaptors/websocket/client.go new file mode 100644 index 00000000..08bf8f2b --- /dev/null +++ b/adaptors/websocket/client.go @@ -0,0 +1,219 @@ +package websocket + +// ------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------ +// ----------------Client side websocket javascript source which is typescript compiled +// ------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------ + +// ClientSource the client-side javascript raw source code +var ClientSource = []byte(`var websocketStringMessageType = 0; +var websocketIntMessageType = 1; +var websocketBoolMessageType = 2; +// bytes is missing here for reasons I will explain somewhen +var websocketJSONMessageType = 4; +var websocketMessagePrefix = "iris-websocket-message:"; +var websocketMessageSeparator = ";"; +var websocketMessagePrefixLen = websocketMessagePrefix.length; +var websocketMessageSeparatorLen = websocketMessageSeparator.length; +var websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1; +var websocketMessagePrefixIdx = websocketMessagePrefixLen - 1; +var websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1; +var Ws = (function () { + // + function Ws(endpoint, protocols) { + var _this = this; + // events listeners + this.connectListeners = []; + this.disconnectListeners = []; + this.nativeMessageListeners = []; + this.messageListeners = {}; + if (!window["WebSocket"]) { + return; + } + if (endpoint.indexOf("ws") == -1) { + endpoint = "ws://" + endpoint; + } + if (protocols != null && protocols.length > 0) { + this.conn = new WebSocket(endpoint, protocols); + } + else { + this.conn = new WebSocket(endpoint); + } + this.conn.onopen = (function (evt) { + _this.fireConnect(); + _this.isReady = true; + return null; + }); + this.conn.onclose = (function (evt) { + _this.fireDisconnect(); + return null; + }); + this.conn.onmessage = (function (evt) { + _this.messageReceivedFromConn(evt); + }); + } + //utils + Ws.prototype.isNumber = function (obj) { + return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false; + }; + Ws.prototype.isString = function (obj) { + return Object.prototype.toString.call(obj) == "[object String]"; + }; + Ws.prototype.isBoolean = function (obj) { + return typeof obj === 'boolean' || + (typeof obj === 'object' && typeof obj.valueOf() === 'boolean'); + }; + Ws.prototype.isJSON = function (obj) { + return typeof obj === 'object'; + }; + // + // messages + Ws.prototype._msg = function (event, websocketMessageType, dataMessage) { + return websocketMessagePrefix + event + websocketMessageSeparator + String(websocketMessageType) + websocketMessageSeparator + dataMessage; + }; + Ws.prototype.encodeMessage = function (event, data) { + var m = ""; + var t = 0; + if (this.isNumber(data)) { + t = websocketIntMessageType; + m = data.toString(); + } + else if (this.isBoolean(data)) { + t = websocketBoolMessageType; + m = data.toString(); + } + else if (this.isString(data)) { + t = websocketStringMessageType; + m = data.toString(); + } + else if (this.isJSON(data)) { + //propably json-object + t = websocketJSONMessageType; + m = JSON.stringify(data); + } + else { + console.log("Invalid"); + } + return this._msg(event, t, m); + }; + Ws.prototype.decodeMessage = function (event, websocketMessage) { + //q-websocket-message;user;4;themarshaledstringfromajsonstruct + var skipLen = websocketMessagePrefixLen + websocketMessageSeparatorLen + event.length + 2; + if (websocketMessage.length < skipLen + 1) { + return null; + } + var websocketMessageType = parseInt(websocketMessage.charAt(skipLen - 2)); + var theMessage = websocketMessage.substring(skipLen, websocketMessage.length); + if (websocketMessageType == websocketIntMessageType) { + return parseInt(theMessage); + } + else if (websocketMessageType == websocketBoolMessageType) { + return Boolean(theMessage); + } + else if (websocketMessageType == websocketStringMessageType) { + return theMessage; + } + else if (websocketMessageType == websocketJSONMessageType) { + return JSON.parse(theMessage); + } + else { + return null; // invalid + } + }; + Ws.prototype.getWebsocketCustomEvent = function (websocketMessage) { + if (websocketMessage.length < websocketMessagePrefixAndSepIdx) { + return ""; + } + var s = websocketMessage.substring(websocketMessagePrefixAndSepIdx, websocketMessage.length); + var evt = s.substring(0, s.indexOf(websocketMessageSeparator)); + return evt; + }; + Ws.prototype.getCustomMessage = function (event, websocketMessage) { + var eventIdx = websocketMessage.indexOf(event + websocketMessageSeparator); + var s = websocketMessage.substring(eventIdx + event.length + websocketMessageSeparator.length + 2, websocketMessage.length); + return s; + }; + // + // Ws Events + // messageReceivedFromConn this is the func which decides + // if it's a native websocket message or a custom qws message + // if native message then calls the fireNativeMessage + // else calls the fireMessage + // + // remember q gives you the freedom of native websocket messages if you don't want to use this client side at all. + Ws.prototype.messageReceivedFromConn = function (evt) { + //check if qws message + var message = evt.data; + if (message.indexOf(websocketMessagePrefix) != -1) { + var event_1 = this.getWebsocketCustomEvent(message); + if (event_1 != "") { + // it's a custom message + this.fireMessage(event_1, this.getCustomMessage(event_1, message)); + return; + } + } + // it's a native websocket message + this.fireNativeMessage(message); + }; + Ws.prototype.OnConnect = function (fn) { + if (this.isReady) { + fn(); + } + this.connectListeners.push(fn); + }; + Ws.prototype.fireConnect = function () { + for (var i = 0; i < this.connectListeners.length; i++) { + this.connectListeners[i](); + } + }; + Ws.prototype.OnDisconnect = function (fn) { + this.disconnectListeners.push(fn); + }; + Ws.prototype.fireDisconnect = function () { + for (var i = 0; i < this.disconnectListeners.length; i++) { + this.disconnectListeners[i](); + } + }; + Ws.prototype.OnMessage = function (cb) { + this.nativeMessageListeners.push(cb); + }; + Ws.prototype.fireNativeMessage = function (websocketMessage) { + for (var i = 0; i < this.nativeMessageListeners.length; i++) { + this.nativeMessageListeners[i](websocketMessage); + } + }; + Ws.prototype.On = function (event, cb) { + if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) { + this.messageListeners[event] = []; + } + this.messageListeners[event].push(cb); + }; + Ws.prototype.fireMessage = function (event, message) { + for (var key in this.messageListeners) { + if (this.messageListeners.hasOwnProperty(key)) { + if (key == event) { + for (var i = 0; i < this.messageListeners[key].length; i++) { + this.messageListeners[key][i](message); + } + } + } + } + }; + // + // Ws Actions + Ws.prototype.Disconnect = function () { + this.conn.close(); + }; + // EmitMessage sends a native websocket message + Ws.prototype.EmitMessage = function (websocketMessage) { + this.conn.send(websocketMessage); + }; + // Emit sends an q-custom websocket message + Ws.prototype.Emit = function (event, data) { + var messageStr = this.encodeMessage(event, data); + this.EmitMessage(messageStr); + }; + return Ws; +}()); +`) diff --git a/adaptors/websocket/client.ts b/adaptors/websocket/client.ts new file mode 100644 index 00000000..865808af --- /dev/null +++ b/adaptors/websocket/client.ts @@ -0,0 +1,261 @@ +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// ----------------Client side websocket commented typescript source code -------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +// export to client.go:clientSource []byte + +const websocketStringMessageType = 0; +const websocketIntMessageType = 1; +const websocketBoolMessageType = 2; +// bytes is missing here for reasons I will explain somewhen +const websocketJSONMessageType = 4; + +const websocketMessagePrefix = "iris-websocket-message:"; +const websocketMessageSeparator = ";"; + +const websocketMessagePrefixLen = websocketMessagePrefix.length; +var websocketMessageSeparatorLen = websocketMessageSeparator.length; +var websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1; +var websocketMessagePrefixIdx = websocketMessagePrefixLen - 1; +var websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1; + +type onConnectFunc = () => void; +type onWebsocketDisconnectFunc = () => void; +type onWebsocketNativeMessageFunc = (websocketMessage: string) => void; +type onMessageFunc = (message: any) => void; + +class Ws { + private conn: WebSocket; + private isReady: boolean; + + // events listeners + + private connectListeners: onConnectFunc[] = []; + private disconnectListeners: onWebsocketDisconnectFunc[] = []; + private nativeMessageListeners: onWebsocketNativeMessageFunc[] = []; + private messageListeners: { [event: string]: onMessageFunc[] } = {}; + + // + + constructor(endpoint: string, protocols?: string[]) { + if (!window["WebSocket"]) { + return; + } + + if (endpoint.indexOf("ws") == -1) { + endpoint = "ws://" + endpoint; + } + if (protocols != null && protocols.length > 0) { + this.conn = new WebSocket(endpoint, protocols); + } else { + this.conn = new WebSocket(endpoint); + } + + this.conn.onopen = ((evt: Event): any => { + this.fireConnect(); + this.isReady = true; + return null; + }); + + this.conn.onclose = ((evt: Event): any => { + this.fireDisconnect(); + return null; + }); + + this.conn.onmessage = ((evt: MessageEvent) => { + this.messageReceivedFromConn(evt); + }); + } + + //utils + + private isNumber(obj: any): boolean { + return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false; + } + + private isString(obj: any): boolean { + return Object.prototype.toString.call(obj) == "[object String]"; + } + + private isBoolean(obj: any): boolean { + return typeof obj === 'boolean' || + (typeof obj === 'object' && typeof obj.valueOf() === 'boolean'); + } + + private isJSON(obj: any): boolean { + return typeof obj === 'object'; + } + + // + + // messages + private _msg(event: string, websocketMessageType: number, dataMessage: string): string { + + return websocketMessagePrefix + event + websocketMessageSeparator + String(websocketMessageType) + websocketMessageSeparator + dataMessage; + } + + private encodeMessage(event: string, data: any): string { + let m = ""; + let t = 0; + if (this.isNumber(data)) { + t = websocketIntMessageType; + m = data.toString(); + } else if (this.isBoolean(data)) { + t = websocketBoolMessageType; + m = data.toString(); + } else if (this.isString(data)) { + t = websocketStringMessageType; + m = data.toString(); + } else if (this.isJSON(data)) { + //propably json-object + t = websocketJSONMessageType; + m = JSON.stringify(data); + } else { + console.log("Invalid"); + } + + return this._msg(event, t, m); + } + + private decodeMessage<T>(event: string, websocketMessage: string): T | any { + //q-websocket-message;user;4;themarshaledstringfromajsonstruct + let skipLen = websocketMessagePrefixLen + websocketMessageSeparatorLen + event.length + 2; + if (websocketMessage.length < skipLen + 1) { + return null; + } + let websocketMessageType = parseInt(websocketMessage.charAt(skipLen - 2)); + let theMessage = websocketMessage.substring(skipLen, websocketMessage.length); + if (websocketMessageType == websocketIntMessageType) { + return parseInt(theMessage); + } else if (websocketMessageType == websocketBoolMessageType) { + return Boolean(theMessage); + } else if (websocketMessageType == websocketStringMessageType) { + return theMessage; + } else if (websocketMessageType == websocketJSONMessageType) { + return JSON.parse(theMessage); + } else { + return null; // invalid + } + } + + private getWebsocketCustomEvent(websocketMessage: string): string { + if (websocketMessage.length < websocketMessagePrefixAndSepIdx) { + return ""; + } + let s = websocketMessage.substring(websocketMessagePrefixAndSepIdx, websocketMessage.length); + let evt = s.substring(0, s.indexOf(websocketMessageSeparator)); + + return evt; + } + + private getCustomMessage(event: string, websocketMessage: string): string { + let eventIdx = websocketMessage.indexOf(event + websocketMessageSeparator); + let s = websocketMessage.substring(eventIdx + event.length + websocketMessageSeparator.length + 2, websocketMessage.length); + return s; + } + + // + + // Ws Events + + // messageReceivedFromConn this is the func which decides + // if it's a native websocket message or a custom qws message + // if native message then calls the fireNativeMessage + // else calls the fireMessage + // + // remember q gives you the freedom of native websocket messages if you don't want to use this client side at all. + private messageReceivedFromConn(evt: MessageEvent): void { + //check if qws message + let message = <string>evt.data; + if (message.indexOf(websocketMessagePrefix) != -1) { + let event = this.getWebsocketCustomEvent(message); + if (event != "") { + // it's a custom message + this.fireMessage(event, this.getCustomMessage(event, message)); + return; + } + } + + // it's a native websocket message + this.fireNativeMessage(message); + } + + OnConnect(fn: onConnectFunc): void { + if (this.isReady) { + fn(); + } + this.connectListeners.push(fn); + } + + fireConnect(): void { + for (let i = 0; i < this.connectListeners.length; i++) { + this.connectListeners[i](); + } + } + + OnDisconnect(fn: onWebsocketDisconnectFunc): void { + this.disconnectListeners.push(fn); + } + + fireDisconnect(): void { + for (let i = 0; i < this.disconnectListeners.length; i++) { + this.disconnectListeners[i](); + } + } + + OnMessage(cb: onWebsocketNativeMessageFunc): void { + this.nativeMessageListeners.push(cb); + } + + fireNativeMessage(websocketMessage: string): void { + for (let i = 0; i < this.nativeMessageListeners.length; i++) { + this.nativeMessageListeners[i](websocketMessage); + } + } + + On(event: string, cb: onMessageFunc): void { + if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) { + this.messageListeners[event] = []; + } + this.messageListeners[event].push(cb); + } + + fireMessage(event: string, message: any): void { + for (let key in this.messageListeners) { + if (this.messageListeners.hasOwnProperty(key)) { + if (key == event) { + for (let i = 0; i < this.messageListeners[key].length; i++) { + this.messageListeners[key][i](message); + } + } + } + } + } + + + // + + // Ws Actions + + Disconnect(): void { + this.conn.close(); + } + + // EmitMessage sends a native websocket message + EmitMessage(websocketMessage: string): void { + this.conn.send(websocketMessage); + } + + // Emit sends an q-custom websocket message + Emit(event: string, data: any): void { + let messageStr = this.encodeMessage(event, data); + this.EmitMessage(messageStr); + } + + // + +} + +// node-modules export {Ws}; diff --git a/adaptors/websocket/config.go b/adaptors/websocket/config.go new file mode 100644 index 00000000..2d942ecb --- /dev/null +++ b/adaptors/websocket/config.go @@ -0,0 +1,136 @@ +package websocket + +import ( + "net/http" + "time" + + "gopkg.in/kataras/iris.v6" +) + +const ( + // DefaultWebsocketWriteTimeout 0, no timeout + DefaultWebsocketWriteTimeout = 0 + // DefaultWebsocketReadTimeout 0, no timeout + DefaultWebsocketReadTimeout = 0 + // DefaultWebsocketPongTimeout 60 * time.Second + DefaultWebsocketPongTimeout = 60 * time.Second + // DefaultWebsocketPingPeriod (DefaultPongTimeout * 9) / 10 + DefaultWebsocketPingPeriod = (DefaultWebsocketPongTimeout * 9) / 10 + // DefaultWebsocketMaxMessageSize 1024 + DefaultWebsocketMaxMessageSize = 1024 + // DefaultWebsocketReadBufferSize 4096 + DefaultWebsocketReadBufferSize = 4096 + // DefaultWebsocketWriterBufferSize 4096 + DefaultWebsocketWriterBufferSize = 4096 + // DefaultClientSourcePath "/iris-ws.js" + DefaultClientSourcePath = "/iris-ws.js" +) + +var ( + // DefaultIDGenerator returns the result of 64 + // random combined characters as the id of a new connection. + // Used when config.IDGenerator is nil + DefaultIDGenerator = func(*iris.Context) string { return randomString(64) } +) + +// Config the websocket server configuration +// all of these are optional. +type Config struct { + // Endpoint is the path which the websocket server will listen for clients/connections + // Default value is empty string, if you don't set it the Websocket server is disabled. + Endpoint string + // ClientSourcePath is is the path which the client-side + // will be auto-served when the server adapted to an Iris station. + // Default value is "/iris-ws.js" + ClientSourcePath string + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + CheckOrigin func(r *http.Request) bool + // WriteTimeout time allowed to write a message to the connection. + // 0 means no timeout. + // Default value is 0 + WriteTimeout time.Duration + // ReadTimeout time allowed to read a message from the connection. + // 0 means no timeout. + // Default value is 0 + ReadTimeout time.Duration + // PongTimeout allowed to read the next pong message from the connection. + // Default value is 60 * time.Second + PongTimeout time.Duration + // PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout. + // Default value is 60 *time.Second + PingPeriod time.Duration + // MaxMessageSize max message size allowed from connection. + // Default value is 1024 + MaxMessageSize int64 + // BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text + // compatible if you wanna use the Connection's EmitMessage to send a custom binary data to the client, like a native server-client communication. + // defaults to false + BinaryMessages bool + // ReadBufferSize is the buffer size for the underline reader + // Default value is 4096 + ReadBufferSize int + // WriteBufferSize is the buffer size for the underline writer + // Default value is 4096 + WriteBufferSize int + // IDGenerator used to create (and later on, set) + // an ID for each incoming websocket connections (clients). + // The request is an argument which you can use to generate the ID (from headers for example). + // If empty then the ID is generated by DefaultIDGenerator: randomString(64) + IDGenerator func(ctx *iris.Context) string +} + +// Validate validates the configuration +func (c Config) Validate() Config { + + if c.ClientSourcePath == "" { + c.ClientSourcePath = DefaultClientSourcePath + } + + // 0 means no timeout. + if c.WriteTimeout < 0 { + c.WriteTimeout = DefaultWebsocketWriteTimeout + } + + if c.ReadTimeout < 0 { + c.ReadTimeout = DefaultWebsocketReadTimeout + } + + if c.PongTimeout < 0 { + c.PongTimeout = DefaultWebsocketPongTimeout + } + + if c.PingPeriod <= 0 { + c.PingPeriod = DefaultWebsocketPingPeriod + } + + if c.MaxMessageSize <= 0 { + c.MaxMessageSize = DefaultWebsocketMaxMessageSize + } + + if c.ReadBufferSize <= 0 { + c.ReadBufferSize = DefaultWebsocketReadBufferSize + } + + if c.WriteBufferSize <= 0 { + c.WriteBufferSize = DefaultWebsocketWriterBufferSize + } + + if c.Error == nil { + c.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + //empty + } + } + + if c.CheckOrigin == nil { + c.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + } + + if c.IDGenerator == nil { + c.IDGenerator = DefaultIDGenerator + } + + return c +} diff --git a/adaptors/websocket/connection.go b/adaptors/websocket/connection.go new file mode 100644 index 00000000..1434211f --- /dev/null +++ b/adaptors/websocket/connection.go @@ -0,0 +1,382 @@ +package websocket + +import ( + "bytes" + "io" + "net" + "strconv" + "sync" + "time" + + "github.com/gorilla/websocket" + "gopkg.in/kataras/iris.v6" +) + +// UnderlineConnection is used for compatible with fasthttp and net/http underline websocket libraries +// we only need ~8 funcs from websocket.Conn so: +type UnderlineConnection interface { + // SetWriteDeadline sets the write deadline on the underlying network + // connection. After a write has timed out, the websocket state is corrupt and + // all future writes will return an error. A zero value for t means writes will + // not time out. + SetWriteDeadline(t time.Time) error + // SetReadDeadline sets the read deadline on the underlying network connection. + // After a read has timed out, the websocket connection state is corrupt and + // all future reads will return an error. A zero value for t means reads will + // not time out. + SetReadDeadline(t time.Time) error + // SetReadLimit sets the maximum size for a message read from the peer. If a + // message exceeds the limit, the connection sends a close frame to the peer + // and returns ErrReadLimit to the application. + SetReadLimit(limit int64) + // SetPongHandler sets the handler for pong messages received from the peer. + // The appData argument to h is the PONG frame application data. The default + // pong handler does nothing. + SetPongHandler(h func(appData string) error) + // SetPingHandler sets the handler for ping messages received from the peer. + // The appData argument to h is the PING frame application data. The default + // ping handler sends a pong to the peer. + SetPingHandler(h func(appData string) error) + // WriteControl writes a control message with the given deadline. The allowed + // message types are CloseMessage, PingMessage and PongMessage. + WriteControl(messageType int, data []byte, deadline time.Time) error + // WriteMessage is a helper method for getting a writer using NextWriter, + // writing the message and closing the writer. + WriteMessage(messageType int, data []byte) error + // ReadMessage is a helper method for getting a reader using NextReader and + // reading from that reader to a buffer. + ReadMessage() (messageType int, p []byte, err error) + // NextWriter returns a writer for the next message to send. The writer's Close + // method flushes the complete message to the network. + // + // There can be at most one open writer on a connection. NextWriter closes the + // previous writer if the application has not already done so. + NextWriter(messageType int) (io.WriteCloser, error) + // Close closes the underlying network connection without sending or waiting for a close frame. + Close() error +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// -------------------------------Connection implementation----------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +type ( + // DisconnectFunc is the callback which fires when a client/connection closed + DisconnectFunc func() + // ErrorFunc is the callback which fires when an error happens + ErrorFunc (func(string)) + // NativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message + NativeMessageFunc func([]byte) + // MessageFunc is the second argument to the Emitter's Emit functions. + // A callback which should receives one parameter of type string, int, bool or any valid JSON/Go struct + MessageFunc interface{} + // Connection is the front-end API that you will use to communicate with the client side + Connection interface { + // Emitter implements EmitMessage & Emit + Emitter + // ID returns the connection's identifier + ID() string + + // Context returns the (upgraded) *iris.Context of this connection + // avoid using it, you normally don't need it, + // websocket has everything you need to authenticate the user BUT if it's necessary + // then you use it to receive user information, for example: from headers + Context() *iris.Context + + // OnDisconnect registers a callback which fires when this connection is closed by an error or manual + OnDisconnect(DisconnectFunc) + // OnError registers a callback which fires when this connection occurs an error + OnError(ErrorFunc) + // EmitError can be used to send a custom error message to the connection + // + // It does nothing more than firing the OnError listeners. It doesn't sends anything to the client. + EmitError(errorMessage string) + // To defines where server should send a message + // returns an emitter to send messages + To(string) Emitter + // OnMessage registers a callback which fires when native websocket message received + OnMessage(NativeMessageFunc) + // On registers a callback to a particular event which fires when a message to this event received + On(string, MessageFunc) + // Join join a connection to a room, it doesn't check if connection is already there, so care + Join(string) + // Leave removes a connection from a room + Leave(string) + // Disconnect disconnects the client, close the underline websocket conn and removes it from the conn list + // returns the error, if any, from the underline connection + Disconnect() error + } + + connection struct { + underline UnderlineConnection + id string + messageType int + pinger *time.Ticker + disconnected bool + onDisconnectListeners []DisconnectFunc + onErrorListeners []ErrorFunc + onNativeMessageListeners []NativeMessageFunc + onEventListeners map[string][]MessageFunc + // these were maden for performance only + self Emitter // pre-defined emitter than sends message to its self client + broadcast Emitter // pre-defined emitter that sends message to all except this + all Emitter // pre-defined emitter which sends message to all clients + + // access to the Context, use with causion, you can't use response writer as you imagine. + ctx *iris.Context + server *server + // #119 , websocket writers are not protected by locks inside the gorilla's websocket code + // so we must protect them otherwise we're getting concurrent connection error on multi writers in the same time. + writerMu sync.Mutex + // same exists for reader look here: https://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages + // but we only use one reader in one goroutine, so we are safe. + // readerMu sync.Mutex + } +) + +var _ Connection = &connection{} + +func newConnection(s *server, ctx *iris.Context, underlineConn UnderlineConnection, id string) *connection { + c := &connection{ + underline: underlineConn, + id: id, + messageType: websocket.TextMessage, + onDisconnectListeners: make([]DisconnectFunc, 0), + onErrorListeners: make([]ErrorFunc, 0), + onNativeMessageListeners: make([]NativeMessageFunc, 0), + onEventListeners: make(map[string][]MessageFunc, 0), + ctx: ctx, + server: s, + } + + if s.config.BinaryMessages { + c.messageType = websocket.BinaryMessage + } + + c.self = newEmitter(c, c.id) + c.broadcast = newEmitter(c, Broadcast) + c.all = newEmitter(c, All) + + return c +} + +// write writes a raw websocket message with a specific type to the client +// used by ping messages and any CloseMessage types. +func (c *connection) write(websocketMessageType int, data []byte) { + // for any-case the app tries to write from different goroutines, + // we must protect them because they're reporting that as bug... + c.writerMu.Lock() + if writeTimeout := c.server.config.WriteTimeout; writeTimeout > 0 { + // set the write deadline based on the configuration + c.underline.SetWriteDeadline(time.Now().Add(writeTimeout)) + } + + // .WriteMessage same as NextWriter and close (flush) + err := c.underline.WriteMessage(websocketMessageType, data) + c.writerMu.Unlock() + if err != nil { + // if failed then the connection is off, fire the disconnect + c.Disconnect() + } +} + +// writeDefault is the same as write but the message type is the configured by c.messageType +// if BinaryMessages is enabled then it's raw []byte as you expected to work with protobufs +func (c *connection) writeDefault(data []byte) { + c.write(c.messageType, data) +} + +const ( + // WriteWait is 1 second at the internal implementation, + // same as here but this can be changed at the future* + WriteWait = 1 * time.Second +) + +func (c *connection) startPinger() { + + // this is the default internal handler, we just change the writeWait because of the actions we must do before + // the server sends the ping-pong. + + pingHandler := func(message string) error { + err := c.underline.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(WriteWait)) + if err == websocket.ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err + } + + c.underline.SetPingHandler(pingHandler) + + // start a new timer ticker based on the configuration + c.pinger = time.NewTicker(c.server.config.PingPeriod) + + go func() { + for { + // wait for each tick + <-c.pinger.C + // try to ping the client, if failed then it disconnects + c.write(websocket.PingMessage, []byte{}) + } + }() +} + +func (c *connection) startReader() { + conn := c.underline + hasReadTimeout := c.server.config.ReadTimeout > 0 + + conn.SetReadLimit(c.server.config.MaxMessageSize) + conn.SetPongHandler(func(s string) error { + if hasReadTimeout { + conn.SetReadDeadline(time.Now().Add(c.server.config.ReadTimeout)) + } + + return nil + }) + + defer func() { + c.Disconnect() + }() + + for { + if hasReadTimeout { + // set the read deadline based on the configuration + conn.SetReadDeadline(time.Now().Add(c.server.config.ReadTimeout)) + } + + _, data, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { + c.EmitError(err.Error()) + } + break + } else { + c.messageReceived(data) + } + + } + +} + +// messageReceived checks the incoming message and fire the nativeMessage listeners or the event listeners (ws custom message) +func (c *connection) messageReceived(data []byte) { + + if bytes.HasPrefix(data, websocketMessagePrefixBytes) { + customData := string(data) + //it's a custom ws message + receivedEvt := getWebsocketCustomEvent(customData) + listeners := c.onEventListeners[receivedEvt] + if listeners == nil { // if not listeners for this event exit from here + return + } + customMessage, err := websocketMessageDeserialize(receivedEvt, customData) + if customMessage == nil || err != nil { + return + } + + for i := range listeners { + if fn, ok := listeners[i].(func()); ok { // its a simple func(){} callback + fn() + } else if fnString, ok := listeners[i].(func(string)); ok { + + if msgString, is := customMessage.(string); is { + fnString(msgString) + } else if msgInt, is := customMessage.(int); is { + // here if server side waiting for string but client side sent an int, just convert this int to a string + fnString(strconv.Itoa(msgInt)) + } + + } else if fnInt, ok := listeners[i].(func(int)); ok { + fnInt(customMessage.(int)) + } else if fnBool, ok := listeners[i].(func(bool)); ok { + fnBool(customMessage.(bool)) + } else if fnBytes, ok := listeners[i].(func([]byte)); ok { + fnBytes(customMessage.([]byte)) + } else { + listeners[i].(func(interface{}))(customMessage) + } + + } + } else { + // it's native websocket message + for i := range c.onNativeMessageListeners { + c.onNativeMessageListeners[i](data) + } + } + +} + +func (c *connection) ID() string { + return c.id +} + +func (c *connection) Context() *iris.Context { + return c.ctx +} + +func (c *connection) fireDisconnect() { + for i := range c.onDisconnectListeners { + c.onDisconnectListeners[i]() + } +} + +func (c *connection) OnDisconnect(cb DisconnectFunc) { + c.onDisconnectListeners = append(c.onDisconnectListeners, cb) +} + +func (c *connection) OnError(cb ErrorFunc) { + c.onErrorListeners = append(c.onErrorListeners, cb) +} + +func (c *connection) EmitError(errorMessage string) { + for _, cb := range c.onErrorListeners { + cb(errorMessage) + } +} + +func (c *connection) To(to string) Emitter { + if to == Broadcast { // if send to all except me, then return the pre-defined emitter, and so on + return c.broadcast + } else if to == All { + return c.all + } else if to == c.id { + return c.self + } + // is an emitter to another client/connection + return newEmitter(c, to) +} + +func (c *connection) EmitMessage(nativeMessage []byte) error { + return c.self.EmitMessage(nativeMessage) +} + +func (c *connection) Emit(event string, message interface{}) error { + return c.self.Emit(event, message) +} + +func (c *connection) OnMessage(cb NativeMessageFunc) { + c.onNativeMessageListeners = append(c.onNativeMessageListeners, cb) +} + +func (c *connection) On(event string, cb MessageFunc) { + if c.onEventListeners[event] == nil { + c.onEventListeners[event] = make([]MessageFunc, 0) + } + + c.onEventListeners[event] = append(c.onEventListeners[event], cb) +} + +func (c *connection) Join(roomName string) { + c.server.Join(roomName, c.id) +} + +func (c *connection) Leave(roomName string) { + c.server.Leave(roomName, c.id) +} + +func (c *connection) Disconnect() error { + return c.server.Disconnect(c.ID()) +} diff --git a/adaptors/websocket/emitter.go b/adaptors/websocket/emitter.go new file mode 100644 index 00000000..6a68de6f --- /dev/null +++ b/adaptors/websocket/emitter.go @@ -0,0 +1,49 @@ +package websocket + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------Emitter implementation------------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +const ( + // All is the string which the Emitter use to send a message to all + All = "" + // Broadcast is the string which the Emitter use to send a message to all except this connection + Broadcast = ";gowebsocket;to;all;except;me;" +) + +type ( + // Emitter is the message/or/event manager + Emitter interface { + // EmitMessage sends a native websocket message + EmitMessage([]byte) error + // Emit sends a message on a particular event + Emit(string, interface{}) error + } + + emitter struct { + conn *connection + to string + } +) + +var _ Emitter = &emitter{} + +func newEmitter(c *connection, to string) *emitter { + return &emitter{conn: c, to: to} +} + +func (e *emitter) EmitMessage(nativeMessage []byte) error { + e.conn.server.emitMessage(e.conn.id, e.to, nativeMessage) + return nil +} + +func (e *emitter) Emit(event string, data interface{}) error { + message, err := websocketMessageSerialize(event, data) + if err != nil { + return err + } + e.EmitMessage([]byte(message)) + return nil +} diff --git a/adaptors/websocket/message.go b/adaptors/websocket/message.go new file mode 100644 index 00000000..f7ac748e --- /dev/null +++ b/adaptors/websocket/message.go @@ -0,0 +1,188 @@ +package websocket + +import ( + "encoding/json" + "github.com/kataras/go-errors" + "github.com/valyala/bytebufferpool" + "math/rand" + "strconv" + "strings" + "time" +) + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// -----------------websocket messages and de/serialization implementation-------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +/* +serializer, [de]websocketMessageSerialize the messages from the client to the websocketServer and from the websocketServer to the client +*/ + +// The same values are exists on client side also +const ( + websocketStringMessageType websocketMessageType = iota + websocketIntMessageType + websocketBoolMessageType + websocketBytesMessageType + websocketJSONMessageType +) + +const ( + websocketMessagePrefix = "iris-websocket-message:" + websocketMessageSeparator = ";" + websocketMessagePrefixLen = len(websocketMessagePrefix) + websocketMessageSeparatorLen = len(websocketMessageSeparator) + websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1 + websocketMessagePrefixIdx = websocketMessagePrefixLen - 1 + websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1 +) + +var ( + websocketMessageSeparatorByte = websocketMessageSeparator[0] + websocketMessageBuffer = bytebufferpool.Pool{} + websocketMessagePrefixBytes = []byte(websocketMessagePrefix) +) + +type ( + websocketMessageType uint8 +) + +func (m websocketMessageType) String() string { + return strconv.Itoa(int(m)) +} + +func (m websocketMessageType) Name() string { + if m == websocketStringMessageType { + return "string" + } else if m == websocketIntMessageType { + return "int" + } else if m == websocketBoolMessageType { + return "bool" + } else if m == websocketBytesMessageType { + return "[]byte" + } else if m == websocketJSONMessageType { + return "json" + } + + return "Invalid(" + m.String() + ")" + +} + +// websocketMessageSerialize serializes a custom websocket message from websocketServer to be delivered to the client +// returns the string form of the message +// Supported data types are: string, int, bool, bytes and JSON. +func websocketMessageSerialize(event string, data interface{}) (string, error) { + var msgType websocketMessageType + var dataMessage string + + if s, ok := data.(string); ok { + msgType = websocketStringMessageType + dataMessage = s + } else if i, ok := data.(int); ok { + msgType = websocketIntMessageType + dataMessage = strconv.Itoa(i) + } else if b, ok := data.(bool); ok { + msgType = websocketBoolMessageType + dataMessage = strconv.FormatBool(b) + } else if by, ok := data.([]byte); ok { + msgType = websocketBytesMessageType + dataMessage = string(by) + } else { + //we suppose is json + res, err := json.Marshal(data) + if err != nil { + return "", err + } + msgType = websocketJSONMessageType + dataMessage = string(res) + } + + b := websocketMessageBuffer.Get() + b.WriteString(websocketMessagePrefix) + b.WriteString(event) + b.WriteString(websocketMessageSeparator) + b.WriteString(msgType.String()) + b.WriteString(websocketMessageSeparator) + b.WriteString(dataMessage) + dataMessage = b.String() + websocketMessageBuffer.Put(b) + + return dataMessage, nil + +} + +var errInvalidTypeMessage = errors.New("Type %s is invalid for message: %s") + +// websocketMessageDeserialize deserializes a custom websocket message from the client +// ex: iris-websocket-message;chat;4;themarshaledstringfromajsonstruct will return 'hello' as string +// Supported data types are: string, int, bool, bytes and JSON. +func websocketMessageDeserialize(event string, websocketMessage string) (message interface{}, err error) { + t, formaterr := strconv.Atoi(websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+1 : websocketMessagePrefixAndSepIdx+len(event)+2]) // in order to iris-websocket-message;user;-> 4 + if formaterr != nil { + return nil, formaterr + } + _type := websocketMessageType(t) + _message := websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+3:] // in order to iris-websocket-message;user;4; -> themarshaledstringfromajsonstruct + + if _type == websocketStringMessageType { + message = string(_message) + } else if _type == websocketIntMessageType { + message, err = strconv.Atoi(_message) + } else if _type == websocketBoolMessageType { + message, err = strconv.ParseBool(_message) + } else if _type == websocketBytesMessageType { + message = []byte(_message) + } else if _type == websocketJSONMessageType { + err = json.Unmarshal([]byte(_message), &message) + } else { + return nil, errInvalidTypeMessage.Format(_type.Name(), websocketMessage) + } + + return +} + +// getWebsocketCustomEvent return empty string when the websocketMessage is native message +func getWebsocketCustomEvent(websocketMessage string) string { + if len(websocketMessage) < websocketMessagePrefixAndSepIdx { + return "" + } + s := websocketMessage[websocketMessagePrefixAndSepIdx:] + evt := s[:strings.IndexByte(s, websocketMessageSeparatorByte)] + return evt +} + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits + letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits +) + +var src = rand.NewSource(time.Now().UnixNano()) + +// random takes a parameter (int) and returns random slice of byte +// ex: var randomstrbytes []byte; randomstrbytes = utils.Random(32) +func random(n int) []byte { + b := make([]byte, n) + // A src.Int63() generates 63 random bits, enough for letterIdxMax characters! + for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return b +} + +// randomString accepts a number(10 for example) and returns a random string using simple but fairly safe random algorithm +func randomString(n int) string { + return string(random(n)) +} diff --git a/adaptors/websocket/server.go b/adaptors/websocket/server.go new file mode 100644 index 00000000..300c1a11 --- /dev/null +++ b/adaptors/websocket/server.go @@ -0,0 +1,388 @@ +package websocket + +import ( + "sync" + + "github.com/gorilla/websocket" + "gopkg.in/kataras/iris.v6" +) + +// Server is the websocket server, +// listens on the config's port, the critical part is the event OnConnection +type Server interface { + // Adapt implements the iris' adaptor, it adapts the websocket server to an Iris station. + // see websocket.go + Adapt(frame *iris.Policies) + + // Handler returns the iris.HandlerFunc + // which is setted to the 'Websocket Endpoint path', + // the client should target to this handler's developer's custom path + // ex: iris.Default.Any("/myendpoint", mywebsocket.Handler()) + Handler() iris.HandlerFunc + + // OnConnection this is the main event you, as developer, will work with each of the websocket connections + OnConnection(cb ConnectionFunc) + + /* + connection actions, same as the connection's method, + but these methods accept the connection ID, + which is useful when the developer maps + this id with a database field (using config.IDGenerator). + */ + + // IsConnected returns true if the connection with that ID is connected to the server + // useful when you have defined a custom connection id generator (based on a database) + // and you want to check if that connection is already connected (on multiple tabs) + IsConnected(connID string) bool + + // Join joins a websocket client to a room, + // first parameter is the room name and the second the connection.ID() + // + // You can use connection.Join("room name") instead. + Join(roomName string, connID string) + + // LeaveAll kicks out a connection from ALL of its joined rooms + LeaveAll(connID string) + + // Leave leaves a websocket client from a room, + // first parameter is the room name and the second the connection.ID() + // + // You can use connection.Leave("room name") instead. + Leave(roomName string, connID string) + + // Disconnect force-disconnects a websocket connection + // based on its connection.ID() + // What it does? + // 1. remove the connection from the list + // 2. leave from all joined rooms + // 3. fire the disconnect callbacks, if any + // 4. close the underline connection and return its error, if any. + // + // You can use the connection.Disconnect() instead. + Disconnect(connID string) error +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------Connection key-based list---------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +type connectionKV struct { + key string // the connection ID + value *connection +} + +type connections []connectionKV + +func (cs *connections) add(key string, value *connection) { + args := *cs + n := len(args) + // check if already id/key exist, if yes replace the conn + for i := 0; i < n; i++ { + kv := &args[i] + if kv.key == key { + kv.value = value + return + } + } + + c := cap(args) + // make the connections slice bigger and put the conn + if c > n { + args = args[:n+1] + kv := &args[n] + kv.key = key + kv.value = value + *cs = args + return + } + // append to the connections slice and put the conn + kv := connectionKV{} + kv.key = key + kv.value = value + *cs = append(args, kv) +} + +func (cs *connections) get(key string) *connection { + args := *cs + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if kv.key == key { + return kv.value + } + } + return nil +} + +// returns the connection which removed and a bool value of found or not +// the connection is useful to fire the disconnect events, we use that form in order to +// make work things faster without the need of get-remove, just -remove should do the job. +func (cs *connections) remove(key string) (*connection, bool) { + args := *cs + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if kv.key == key { + conn := kv.value + // we found the index, + // let's remove the item by appending to the temp and + // after set the pointer of the slice to this temp args + args = append(args[:i], args[i+1:]...) + *cs = args + return conn, true + } + } + return nil, false +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------Server implementation-------------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +type ( + // ConnectionFunc is the callback which fires when a client/connection is connected to the server. + // Receives one parameter which is the Connection + ConnectionFunc func(Connection) + + // websocketRoomPayload is used as payload from the connection to the server + websocketRoomPayload struct { + roomName string + connectionID string + } + + // payloads, connection -> server + websocketMessagePayload struct { + from string + to string + data []byte + } + + server struct { + config Config + connections connections + rooms map[string][]string // by default a connection is joined to a room which has the connection id as its name + mu sync.Mutex // for rooms + onConnectionListeners []ConnectionFunc + //connectionPool *sync.Pool // sadly I can't make this because the websocket connection is live until is closed. + } +) + +var _ Server = &server{} + +// server implementation + +func (s *server) Handler() iris.HandlerFunc { + // build the upgrader once + c := s.config + + upgrader := websocket.Upgrader{ReadBufferSize: c.ReadBufferSize, WriteBufferSize: c.WriteBufferSize, Error: c.Error, CheckOrigin: c.CheckOrigin} + return func(ctx *iris.Context) { + // Upgrade upgrades the HTTP server connection to the WebSocket protocol. + // + // The responseHeader is included in the response to the client's upgrade + // request. Use the responseHeader to specify cookies (Set-Cookie) and the + // application negotiated subprotocol (Sec--Protocol). + // + // If the upgrade fails, then Upgrade replies to the client with an HTTP error + // response. + conn, err := upgrader.Upgrade(ctx.ResponseWriter, ctx.Request, ctx.ResponseWriter.Header()) + if err != nil { + ctx.Log(iris.DevMode, "websocket error: "+err.Error()) + ctx.EmitError(iris.StatusServiceUnavailable) + return + } + s.handleConnection(ctx, conn) + } +} + +// handleConnection creates & starts to listening to a new connection +func (s *server) handleConnection(ctx *iris.Context, websocketConn UnderlineConnection) { + // use the config's id generator (or the default) to create a websocket client/connection id + cid := s.config.IDGenerator(ctx) + // create the new connection + c := newConnection(s, ctx, websocketConn, cid) + // add the connection to the server's list + s.connections.add(cid, c) + + // join to itself + s.Join(c.ID(), c.ID()) + + // NOTE TO ME: fire these first BEFORE startReader and startPinger + // in order to set the events and any messages to send + // the startPinger will send the OK to the client and only + // then the client is able to send and receive from server + // when all things are ready and only then. DO NOT change this order. + + // fire the on connection event callbacks, if any + for i := range s.onConnectionListeners { + s.onConnectionListeners[i](c) + } + + // start the ping + c.startPinger() + + // start the messages reader + c.startReader() +} + +/* Notes: + We use the id as the signature of the connection because with the custom IDGenerator + the developer can share this ID with a database field, so we want to give the oportunnity to handle + his/her websocket connections without even use the connection itself. + + Another question may be: + Q: Why you use server as the main actioner for all of the connection actions? + For example the server.Disconnect(connID) manages the connection internal fields, is this code-style correct? + A: It's the correct code-style for these type of applications and libraries, server manages all, the connnection's functions + should just do some internal checks (if needed) and push the action to its parent, which is the server, the server is able to + remove a connection, the rooms of its connected and all these things, so in order to not split the logic, we have the main logic + here, in the server, and let the connection with some exported functions whose exists for the per-connection action user's code-style. + + Ok my english are s** I can feel it, but these comments are mostly for me. +*/ + +// OnConnection this is the main event you, as developer, will work with each of the websocket connections +func (s *server) OnConnection(cb ConnectionFunc) { + s.onConnectionListeners = append(s.onConnectionListeners, cb) +} + +// IsConnected returns true if the connection with that ID is connected to the server +// useful when you have defined a custom connection id generator (based on a database) +// and you want to check if that connection is already connected (on multiple tabs) +func (s *server) IsConnected(connID string) bool { + c := s.connections.get(connID) + return c != nil +} + +// Join joins a websocket client to a room, +// first parameter is the room name and the second the connection.ID() +// +// You can use connection.Join("room name") instead. +func (s *server) Join(roomName string, connID string) { + s.mu.Lock() + s.join(roomName, connID) + s.mu.Unlock() +} + +// join used internally, no locks used. +func (s *server) join(roomName string, connID string) { + if s.rooms[roomName] == nil { + s.rooms[roomName] = make([]string, 0) + } + s.rooms[roomName] = append(s.rooms[roomName], connID) +} + +// LeaveAll kicks out a connection from ALL of its joined rooms +func (s *server) LeaveAll(connID string) { + s.mu.Lock() + for name, connectionIDs := range s.rooms { + for i := range connectionIDs { + if connectionIDs[i] == connID { + // the connection is inside this room, lets remove it + s.rooms[name][i] = s.rooms[name][len(s.rooms[name])-1] + s.rooms[name] = s.rooms[name][:len(s.rooms[name])-1] + } + } + } + s.mu.Unlock() +} + +// Leave leaves a websocket client from a room, +// first parameter is the room name and the second the connection.ID() +// +// You can use connection.Leave("room name") instead. +func (s *server) Leave(roomName string, connID string) { + s.mu.Lock() + s.leave(roomName, connID) + s.mu.Unlock() +} + +// leave used internally, no locks used. +func (s *server) leave(roomName string, connID string) { + ///THINK: we could add locks to its room but we still use the lock for the whole rooms or we can just do what we do with connections + // I will think about it on the next revision, so far we use the locks only for rooms so we are ok... + if s.rooms[roomName] != nil { + for i := range s.rooms[roomName] { + if s.rooms[roomName][i] == connID { + s.rooms[roomName][i] = s.rooms[roomName][len(s.rooms[roomName])-1] + s.rooms[roomName] = s.rooms[roomName][:len(s.rooms[roomName])-1] + break + } + } + if len(s.rooms[roomName]) == 0 { // if room is empty then delete it + delete(s.rooms, roomName) + } + } +} + +// emitMessage is the main 'router' of the messages coming from the connection +// this is the main function which writes the RAW websocket messages to the client. +// It sends them(messages) to the correct room (self, broadcast or to specific client) +// +// You don't have to use this generic method, exists only for extreme +// apps which you have an external goroutine with a list of custom connection list. +// +// You SHOULD use connection.EmitMessage/Emit/To().Emit/EmitMessage instead. +// let's keep it unexported for the best. +func (s *server) emitMessage(from, to string, data []byte) { + if to != All && to != Broadcast && s.rooms[to] != nil { + // it suppose to send the message to a specific room/or a user inside its own room + for _, connectionIDInsideRoom := range s.rooms[to] { + if c := s.connections.get(connectionIDInsideRoom); c != nil { + c.writeDefault(data) //send the message to the client(s) + } else { + // the connection is not connected but it's inside the room, we remove it on disconnect but for ANY CASE: + cid := connectionIDInsideRoom + if c != nil { + cid = c.id + } + s.Leave(cid, to) + } + } + } else { + // it suppose to send the message to all opened connections or to all except the sender + for _, cKV := range s.connections { + connID := cKV.key + if to != All && to != connID { // if it's not suppose to send to all connections (including itself) + if to == Broadcast && from == connID { // if broadcast to other connections except this + continue //here we do the opossite of previous block, + // just skip this connection when it's suppose to send the message to all connections except the sender + } + + } + // send to the client(s) when the top validators passed + cKV.value.writeDefault(data) + } + } +} + +// Disconnect force-disconnects a websocket connection based on its connection.ID() +// What it does? +// 1. remove the connection from the list +// 2. leave from all joined rooms +// 3. fire the disconnect callbacks, if any +// 4. close the underline connection and return its error, if any. +// +// You can use the connection.Disconnect() instead. +func (s *server) Disconnect(connID string) (err error) { + // remove the connection from the list + if c, ok := s.connections.remove(connID); ok { + if !c.disconnected { + c.disconnected = true + // stop the ping timer + c.pinger.Stop() + // leave from all joined rooms + s.LeaveAll(connID) + // fire the disconnect callbacks, if any + c.fireDisconnect() + // close the underline connection and return its error, if any. + err = c.underline.Close() + } + } + + return +} diff --git a/adaptors/websocket/websocket.go b/adaptors/websocket/websocket.go new file mode 100644 index 00000000..9e92ef54 --- /dev/null +++ b/adaptors/websocket/websocket.go @@ -0,0 +1,62 @@ +// Package websocket provides an easy way to setup server and client side rich websocket experience for Iris +package websocket + +import ( + "strings" + + "gopkg.in/kataras/iris.v6" +) + +// New returns a new websocket server policy adaptor. +func New(cfg Config) Server { + return &server{ + config: cfg.Validate(), + rooms: make(map[string][]string, 0), + onConnectionListeners: make([]ConnectionFunc, 0), + } +} + +func fixPath(s string) string { + if s == "" { + return "" + } + + if s[0] != '/' { + s = "/" + s + } + + s = strings.Replace(s, "//", "/", -1) + return s +} + +// Adapt implements the iris' adaptor, it adapts the websocket server to an Iris station. +func (s *server) Adapt(frame *iris.Policies) { + // bind the server's Handler to Iris at Boot state + evt := iris.EventPolicy{ + Boot: func(f *iris.Framework) { + wsPath := fixPath(s.config.Endpoint) + if wsPath == "" { + f.Log(iris.DevMode, "websocket's configuration field 'Endpoint' cannot be empty, websocket server stops") + return + } + + wsClientSidePath := fixPath(s.config.ClientSourcePath) + if wsClientSidePath == "" { + f.Log(iris.DevMode, "websocket's configuration field 'ClientSourcePath' cannot be empty, websocket server stops") + return + } + + // set the routing for client-side source (javascript) (optional) + clientSideLookupName := "iris-websocket-client-side" + wsHandler := s.Handler() + f.Get(wsPath, wsHandler) + // check if client side doesn't already exists + if f.Routes().Lookup(clientSideLookupName) == nil { + // serve the client side on domain:port/iris-ws.js + f.StaticContent(wsClientSidePath, "application/javascript", ClientSource).ChangeName(clientSideLookupName) + } + }, + } + + evt.Adapt(frame) +} diff --git a/configuration.go b/configuration.go index 1858104d..a7005d97 100644 --- a/configuration.go +++ b/configuration.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "net" "net/http" - "net/url" "strconv" "time" @@ -17,11 +16,12 @@ type ( // OptionSetter sets a configuration field to the main configuration // used to help developers to write less and configure only what they really want and nothing else // example: - // iris.New(iris.Configuration{Sessions:iris.SessionConfiguration{Cookie:"mysessionid"}, Websocket: iris.WebsocketConfiguration{Endpoint:"/my_endpoint"}}) + // iris.New(iris.Configuration{Charset: "UTF-8", Gzip:true}) // now can be done also by using iris.Option$FIELD: - // iris.New(irisOptionSessionsCookie("mycookieid"),iris.OptionWebsocketEndpoint("my_endpoint")) + // iris.New(iris.OptionCharset("UTF-8"), iris.OptionGzip(true)) // benefits: - // 1. user/dev have no worries what option to pass, he/she can just press iris.Option and all options should be shown to her/his editor's autocomplete-popup window + // 1. dev has no worries what option to pass, + // he/she can just press iris.Option and all options should be shown to her/his editor's autocomplete-popup window // 2. can be passed with any order // 3. Can override previous configuration OptionSetter interface { @@ -177,9 +177,6 @@ type Configuration struct { // Sessions contains the configs for sessions Sessions SessionsConfiguration - // Websocket contains the configs for Websocket's server integration - Websocket WebsocketConfiguration - // Other are the custom, dynamic options, can be empty // this fill used only by you to set any app's options you want // for each of an Iris instance @@ -443,7 +440,6 @@ func DefaultConfiguration() Configuration { Charset: DefaultCharset, Gzip: false, Sessions: DefaultSessionsConfiguration(), - Websocket: DefaultWebsocketConfiguration(), Other: options.Options{}, } } @@ -529,184 +525,6 @@ func DefaultSessionsConfiguration() SessionsConfiguration { } } -// WebsocketConfiguration the config contains options for the Websocket main config field -type WebsocketConfiguration struct { - // WriteTimeout time allowed to write a message to the connection. - // Default value is 15 * time.Second - WriteTimeout time.Duration - // PongTimeout allowed to read the next pong message from the connection - // Default value is 60 * time.Second - PongTimeout time.Duration - // PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout - // Default value is (PongTimeout * 9) / 10 - PingPeriod time.Duration - // MaxMessageSize max message size allowed from connection - // Default value is 1024 - MaxMessageSize int64 - // BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text - // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more - // Defaults to false - BinaryMessages bool - // Endpoint is the path which the websocket server will listen for clients/connections - // Default value is empty string, if you don't set it the Websocket server is disabled. - Endpoint string - // ReadBufferSize is the buffer size for the underline reader - ReadBufferSize int - // WriteBufferSize is the buffer size for the underline writer - WriteBufferSize int - // Error specifies the function for generating HTTP error responses. - // - // The default behavior is to store the reason in the context (ctx.Set(reason)) and fire any custom error (ctx.EmitError(status)) - Error func(ctx *Context, status int, reason error) - // CheckOrigin returns true if the request Origin header is acceptable. If - // CheckOrigin is nil, the host in the Origin header must not be set or - // must match the host of the request. - // - // The default behavior is to allow all origins - // you can change this behavior by setting the iris.Default.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin - CheckOrigin func(r *http.Request) bool - // IDGenerator used to create (and later on, set) - // an ID for each incoming websocket connections (clients). - // If empty then the ID is generated by the result of 64 - // random combined characters - IDGenerator func(r *http.Request) string -} - -var ( - // OptionWebsocketWriteTimeout time allowed to write a message to the connection. - // Default value is 15 * time.Second - OptionWebsocketWriteTimeout = func(val time.Duration) OptionSet { - return func(c *Configuration) { - c.Websocket.WriteTimeout = val - } - } - // OptionWebsocketPongTimeout allowed to read the next pong message from the connection - // Default value is 60 * time.Second - OptionWebsocketPongTimeout = func(val time.Duration) OptionSet { - return func(c *Configuration) { - c.Websocket.PongTimeout = val - } - } - // OptionWebsocketPingPeriod send ping messages to the connection with this period. Must be less than PongTimeout - // Default value is (PongTimeout * 9) / 10 - OptionWebsocketPingPeriod = func(val time.Duration) OptionSet { - return func(c *Configuration) { - c.Websocket.PingPeriod = val - } - } - // OptionWebsocketMaxMessageSize max message size allowed from connection - // Default value is 1024 - OptionWebsocketMaxMessageSize = func(val int64) OptionSet { - return func(c *Configuration) { - c.Websocket.MaxMessageSize = val - } - } - // OptionWebsocketBinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text - // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more - // Defaults to false - OptionWebsocketBinaryMessages = func(val bool) OptionSet { - return func(c *Configuration) { - c.Websocket.BinaryMessages = val - } - } - // OptionWebsocketEndpoint is the path which the websocket server will listen for clients/connections - // Default value is empty string, if you don't set it the Websocket server is disabled. - OptionWebsocketEndpoint = func(val string) OptionSet { - return func(c *Configuration) { - c.Websocket.Endpoint = val - } - } - // OptionWebsocketReadBufferSize is the buffer size for the underline reader - OptionWebsocketReadBufferSize = func(val int) OptionSet { - return func(c *Configuration) { - c.Websocket.ReadBufferSize = val - } - } - // OptionWebsocketWriteBufferSize is the buffer size for the underline writer - OptionWebsocketWriteBufferSize = func(val int) OptionSet { - return func(c *Configuration) { - c.Websocket.WriteBufferSize = val - } - } - - // OptionWebsocketError specifies the function for generating HTTP error responses. - OptionWebsocketError = func(val func(*Context, int, error)) OptionSet { - return func(c *Configuration) { - c.Websocket.Error = val - } - } - // OptionWebsocketCheckOrigin returns true if the request Origin header is acceptable. If - // CheckOrigin is nil, the host in the Origin header must not be set or - // must match the host of the request. - OptionWebsocketCheckOrigin = func(val func(*http.Request) bool) OptionSet { - return func(c *Configuration) { - c.Websocket.CheckOrigin = val - } - } - - // OptionWebsocketIDGenerator used to create (and later on, set) - // an ID for each incoming websocket connections (clients). - // If empty then the ID is generated by the result of 64 - // random combined characters - OptionWebsocketIDGenerator = func(val func(*http.Request) string) OptionSet { - return func(c *Configuration) { - c.Websocket.IDGenerator = val - } - } -) - -const ( - // DefaultWebsocketWriteTimeout 15 * time.Second - DefaultWebsocketWriteTimeout = 15 * time.Second - // DefaultWebsocketPongTimeout 60 * time.Second - DefaultWebsocketPongTimeout = 60 * time.Second - // DefaultWebsocketPingPeriod (DefaultPongTimeout * 9) / 10 - DefaultWebsocketPingPeriod = (DefaultWebsocketPongTimeout * 9) / 10 - // DefaultWebsocketMaxMessageSize 1024 - DefaultWebsocketMaxMessageSize = 1024 -) - -var ( - // DefaultWebsocketError is the default method to manage the handshake websocket errors - DefaultWebsocketError = func(ctx *Context, status int, reason error) { - ctx.Set("WsError", reason) - ctx.EmitError(status) - } - // DefaultWebsocketCheckOrigin is the default method to allow websocket clients to connect to this server - // you can change this behavior by setting the iris.Default.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin - DefaultWebsocketCheckOrigin = func(r *http.Request) bool { - return true - } - // WebsocketCheckSameOrigin returns true if the origin is not set or is equal to the request host - WebsocketCheckSameOrigin = func(r *http.Request) bool { - origin := r.Header.Get("origin") - if len(origin) == 0 { - return true - } - u, err := url.Parse(origin) - if err != nil { - return false - } - return u.Host == r.Host - } -) - -// DefaultWebsocketConfiguration returns the default config for iris-ws websocket package -func DefaultWebsocketConfiguration() WebsocketConfiguration { - return WebsocketConfiguration{ - WriteTimeout: DefaultWebsocketWriteTimeout, - PongTimeout: DefaultWebsocketPongTimeout, - PingPeriod: DefaultWebsocketPingPeriod, - MaxMessageSize: DefaultWebsocketMaxMessageSize, - BinaryMessages: false, - ReadBufferSize: 4096, - WriteBufferSize: 4096, - Endpoint: "", - // use the kataras/go-websocket default - IDGenerator: nil, - } -} - // Default values for base Server conf const ( // DefaultServerHostname returns the default hostname which is 0.0.0.0 diff --git a/doc.go b/doc.go index 08edcb91..9159e898 100644 --- a/doc.go +++ b/doc.go @@ -513,11 +513,11 @@ Example code: You should have a basic idea of the framework by now, we just scratched the surface. If you enjoy what you just saw and want to learn more, please follow the below links: - - examples: https://github.com/iris-contrib/examples - - book: https://docs.iris-go.com - - adaptors: https://github.com/kataras/iris/tree/v6/adaptors - - middleware: https://github.com/kataras/iris/tree/v6/middleware & https://github.com/iris-contrib/middleware - - godocs: https://godoc.org/github.com/kataras/iris +- examples: https://github.com/iris-contrib/examples +- book: https://docs.iris-go.com +- adaptors: https://github.com/kataras/iris/tree/v6/adaptors +- middleware: https://github.com/kataras/iris/tree/v6/middleware & https://github.com/iris-contrib/middleware +- godocs: https://godoc.org/github.com/kataras/iris */ diff --git a/iris.go b/iris.go index c0817bef..0055d552 100644 --- a/iris.go +++ b/iris.go @@ -70,10 +70,9 @@ type Framework struct { ln net.Listener closedManually bool - once sync.Once - Config *Configuration - sessions sessions.Sessions - Websocket *WebsocketServer + once sync.Once + Config *Configuration + sessions sessions.Sessions } var defaultGlobalLoggerOuput = log.New(os.Stdout, "[iris] ", log.LstdFlags) @@ -253,19 +252,6 @@ func New(setters ...OptionSetter) *Framework { }}) } - { - // +------------------------------------------------------------+ - // | Module Name: Websocket | - // | On Init: Attach a new websocket server. | - // | It starts on first callback registration | - // +------------------------------------------------------------+ - - // in order to be able to call $instance.Websocket.OnConnection. - // The whole server's configuration will be - // initialized on the first OnConnection registration (no runtime) - s.Websocket = NewWebsocketServer(s) - } - { // +------------------------------------------------------------+ // | Module Name: Router | diff --git a/middleware/basicauth/LICENSE b/middleware/basicauth/LICENSE new file mode 100644 index 00000000..2935ad5d --- /dev/null +++ b/middleware/basicauth/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/middleware/i18n/LICENSE b/middleware/i18n/LICENSE index d0978209..eb4883c1 100644 --- a/middleware/i18n/LICENSE +++ b/middleware/i18n/LICENSE @@ -1,3 +1,25 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/middleware/logger/LICENSE b/middleware/logger/LICENSE new file mode 100644 index 00000000..2935ad5d --- /dev/null +++ b/middleware/logger/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/middleware/recover/LICENSE b/middleware/recover/LICENSE new file mode 100644 index 00000000..2935ad5d --- /dev/null +++ b/middleware/recover/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/websocket.go b/websocket.go deleted file mode 100644 index cb30ca0b..00000000 --- a/websocket.go +++ /dev/null @@ -1,104 +0,0 @@ -package iris - -import ( - "net/http" - "sync" - - "github.com/kataras/go-websocket" -) - -// conversionals -const ( - // All is the string which the Emitter use to send a message to all - All = websocket.All - // NotMe is the string which the Emitter use to send a message to all except this websocket.Connection - NotMe = websocket.NotMe - // Broadcast is the string which the Emitter use to send a message to all except this websocket.Connection, same as 'NotMe' - Broadcast = websocket.Broadcast -) - -// Note I keep this code only to no change the front-end API, we could only use the go-websocket and set our custom upgrader - -type ( - // WebsocketServer is the iris websocket server, expose the websocket.Server - // the below code is a wrapper and bridge between iris-contrib/websocket and kataras/go-websocket - WebsocketServer struct { - websocket.Server - station *Framework - once sync.Once - // Config: - // if endpoint is not empty then this configuration is used instead of the station's - // useful when the user/dev wants more than one websocket server inside one iris instance. - Config WebsocketConfiguration - } -) - -// NewWebsocketServer returns a new empty unitialized websocket server -// it runs on first OnConnection -func NewWebsocketServer(station *Framework) *WebsocketServer { - return &WebsocketServer{station: station, Server: websocket.New(), Config: station.Config.Websocket} -} - -// NewWebsocketServer creates the client side source route and the route path Endpoint with the correct Handler -// receives the websocket configuration and the iris station -// and returns the websocket server which can be attached to more than one iris station (if needed) -func (ws *WebsocketServer) init() { - - if ws.Config.Endpoint == "" { - ws.Config = ws.station.Config.Websocket - } - - c := ws.Config - - if c.Endpoint == "" { - return - } - - if c.CheckOrigin == nil { - c.CheckOrigin = DefaultWebsocketCheckOrigin - } - - if c.Error == nil { - c.Error = DefaultWebsocketError - } - // set the underline websocket server's configuration - ws.Server.Set(websocket.Config{ - WriteTimeout: c.WriteTimeout, - PongTimeout: c.PongTimeout, - PingPeriod: c.PingPeriod, - MaxMessageSize: c.MaxMessageSize, - BinaryMessages: c.BinaryMessages, - ReadBufferSize: c.ReadBufferSize, - WriteBufferSize: c.WriteBufferSize, - Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { - ws.station.Context.Run(w, r, func(ctx *Context) { - c.Error(ctx, status, reason) - }) - }, - CheckOrigin: c.CheckOrigin, - IDGenerator: c.IDGenerator, - }) - - // set the routing for client-side source (javascript) (optional) - clientSideLookupName := "iris-websocket-client-side" - ws.station.Get(c.Endpoint, ToHandler(ws.Server.Handler())) - // check if client side already exists - if ws.station.Routes().Lookup(clientSideLookupName) == nil { - // serve the client side on domain:port/iris-ws.js - ws.station.StaticContent("/iris-ws.js", contentJavascript, websocket.ClientSource).ChangeName(clientSideLookupName) - } -} - -// WebsocketConnection is the front-end API that you will use to communicate with the client side -type WebsocketConnection interface { - websocket.Connection -} - -// OnConnection this is the main event you, as developer, will work with each of the websocket connections -func (ws *WebsocketServer) OnConnection(connectionListener func(WebsocketConnection)) { - ws.once.Do(ws.init) - - ws.Server.OnConnection(func(c websocket.Connection) { - connectionListener(c) - }) -}