diff --git a/HISTORY.md b/HISTORY.md index 0520f585..f9b58054 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,41 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras/iris` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. +## 4.0.0-alpha.2 -> 4.0.0-alpha.3 + +**New** + +A **Response Engine** gives you the freedom to create/change the render/response writer for + +- `context.JSON` +- `context.JSONP` +- `context.XML` +- `context.Text` +- `context.Markdown` +- `context.Data` +- `context.Render("my_custom_type",mystructOrData{}, iris.RenderOptions{"gzip":false,"charset":"UTF-8"})` +- `context.MarkdownString` +- `iris.ResponseString(...)` + + +**Fix** +- https://github.com/kataras/iris/issues/294 + +**Small changes** + +- `iris.Config.Charset`, before alpha.3 was `iris.Config.Rest.Charset` & `iris.Config.Render.Template.Charset`, but you can override it at runtime by passinth a map `iris.RenderOptions` on the `context.Render` call . +- `iris.Config.IsDevelopment` , before alpha.1 was `iris.Config.Render.Template.IsDevelopment` + + +**Websockets changes** + +No need to import the `github.com/kataras/iris/websocket` to use the `Connection` iteral, the websocket moved inside `kataras/iris` , now all exported variables' names have the prefix of `Websocket`, so the old `websocket.Connection` is now `iris.WebsocketConnection`. + + +Generally, no other changes on the 'frontend API', for response engines examples and how you can register your own to add more features on existing response engines or replace them, look [here](https://github.com/iris-contrib/response). + +**BAD SIDE**: E-Book is still pointing on the v3 release, but will be updated soon. + ## 4.0.0-alpha.1 -> 4.0.0-alpha.2 **Sessions were re-written ** diff --git a/README.md b/README.md index 2c95483a..adf5fd45 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **v4.0.0-alpha.2** +Current: **v4.0.0-alpha.3** > Iris is an active project @@ -159,7 +159,7 @@ Todo - [x] Refactor & extend view engine, separate the engines from the main code base, easier for the community to create new view engines - [x] Refactor & extend sessions, split the different databases functionality to the iris-contrib - [ ] Refactor & extends the rest render engine in order to be able to developer to use their own implemention for rendering restful types, like, for example a custom JSON implementation using no-standard go package for encode/decode -- [ ] Move the iris/websocket package's source code inside iris/websocket.go one file, to be easier to use by users without import a new package +- [x] Move the iris/websocket package's source code inside iris/websocket.go one file, to be easier to use by users without import a new package - [ ] configs package should be removed after all these, we will not need big configurations because of different packages splitted & moved to the iris-contrib, we will keep interfaces and all required things inside kataras/iris.go. - [ ] Implement all [opened community's feature requests](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature+request%22) - [ ] Extend i18n middleware for easier and better internalization support @@ -192,7 +192,7 @@ License can be found [here](LICENSE). [Travis]: http://travis-ci.org/kataras/iris [License Widget]: https://img.shields.io/badge/license-MIT%20%20License%20-E91E63.svg?style=flat-square [License]: https://github.com/kataras/iris/blob/master/LICENSE -[Release Widget]: https://img.shields.io/badge/release-v4.0.0--alpha.2-blue.svg?style=flat-square +[Release Widget]: https://img.shields.io/badge/release-v4.0.0--alpha.3-blue.svg?style=flat-square [Release]: https://github.com/kataras/iris/releases [Chat Widget]: https://img.shields.io/badge/community-chat-00BCD4.svg?style=flat-square [Chat]: https://kataras.rocket.chat/channel/iris diff --git a/config/config.go b/config/config.go index 97252830..78168368 100644 --- a/config/config.go +++ b/config/config.go @@ -5,11 +5,6 @@ import ( "time" ) -var ( - // Charset character encoding for various rendering - Charset = "UTF-8" -) - var ( // TimeFormat default time format for any kind of datetime parsing TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" diff --git a/config/iris.go b/config/iris.go index 716c2b74..e30c6d5f 100644 --- a/config/iris.go +++ b/config/iris.go @@ -2,13 +2,13 @@ package config import ( "github.com/imdario/mergo" - "github.com/iris-contrib/rest" ) // Default values for base Iris conf const ( DefaultDisablePathCorrection = false DefaultDisablePathEscape = false + DefaultCharset = "UTF-8" ) type ( @@ -89,12 +89,14 @@ type ( // default is false IsDevelopment bool + // Charset character encoding for various rendering + // used for templates and the rest of the responses + // defaults to "UTF-8" + Charset string + // Sessions contains the configs for sessions Sessions Sessions - // Rest contains the configs for rest render configuration - Rest rest.Config - // Websocket contains the configs for Websocket's server integration Websocket *Websocket @@ -111,9 +113,9 @@ func Default() Iris { DisableBanner: false, DisableTemplateEngines: false, IsDevelopment: false, + Charset: DefaultCharset, ProfilePath: "", Sessions: DefaultSessions(), - Rest: rest.DefaultConfig(), Websocket: DefaultWebsocket(), Tester: DefaultTester(), } diff --git a/context.go b/context.go index 2fd812c8..86b1339e 100644 --- a/context.go +++ b/context.go @@ -39,8 +39,19 @@ const ( contentLength = "Content-Length" // ContentHTML is the string of text/html response headers contentHTML = "text/html" - // ContentBINARY is the string of application/octet-stream response headers - contentBINARY = "application/octet-stream" + // ContentBinary header value for binary data. + contentBinary = "application/octet-stream" + // ContentJSON header value for JSON data. + contentJSON = "application/json" + // ContentJSONP header value for JSONP data. + contentJSONP = "application/javascript" + // ContentText header value for Text data. + contentText = "text/plain" + // ContentXML header value for XML data. + contentXML = "text/xml" + + // contentMarkdown custom key/content type, the real is the text/html + contentMarkdown = "text/markdown" // LastModified "Last-Modified" lastModified = "Last-Modified" @@ -461,26 +472,29 @@ func (ctx *Context) Write(format string, a ...interface{}) { ctx.RequestCtx.WriteString(fmt.Sprintf(format, a...)) } -// HTML writes html string with a http status -func (ctx *Context) HTML(httpStatus int, htmlContents string) { - ctx.SetContentType(contentHTML + ctx.framework.rest.CompiledCharset) - ctx.RequestCtx.SetStatusCode(httpStatus) - ctx.RequestCtx.WriteString(htmlContents) +// Gzip accepts bytes, which are compressed to gzip format and sent to the client +func (ctx *Context) Gzip(b []byte, status int) { + ctx.RequestCtx.Response.Header.Add("Content-Encoding", "gzip") + gzipWriter := ctx.framework.gzipWriterPool.Get().(*gzip.Writer) + gzipWriter.Reset(ctx.RequestCtx.Response.BodyWriter()) + gzipWriter.Write(b) + gzipWriter.Close() + ctx.framework.gzipWriterPool.Put(gzipWriter) } -// Data writes out the raw bytes as binary data. -func (ctx *Context) Data(status int, v []byte) error { - return ctx.framework.rest.Data(ctx.RequestCtx, status, v) -} - -// RenderWithStatus builds up the response from the specified template and bindings. -// Note: parameter layout has meaning only when using the iris.HTMLTemplate +// RenderWithStatus builds up the response from the specified template or a response engine. +// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or response engine func (ctx *Context) RenderWithStatus(status int, name string, binding interface{}, options ...map[string]interface{}) error { ctx.SetStatusCode(status) - return ctx.framework.templates.GetBy(name).Execute(ctx, name, binding, options...) + if strings.IndexByte(name, '.') > 0 { //we have template + return ctx.framework.templates.getBy(name).execute(ctx, name, binding, options...) + } + return ctx.framework.responses.getBy(name).render(ctx, binding, options...) } -// Render same as .RenderWithStatus but with status to iris.StatusOK (200) +// Render same as .RenderWithStatus but with status to iris.StatusOK (200) if no previous status exists +// builds up the response from the specified template or a response engine. +// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or response engine func (ctx *Context) Render(name string, binding interface{}, options ...map[string]interface{}) error { errCode := ctx.RequestCtx.Response.StatusCode() if errCode <= 0 { @@ -490,6 +504,8 @@ func (ctx *Context) Render(name string, binding interface{}, options ...map[stri } // MustRender same as .Render but returns 500 internal server http status (error) if rendering fail +// builds up the response from the specified template or a response engine. +// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or response engine func (ctx *Context) MustRender(name string, binding interface{}, options ...map[string]interface{}) { if err := ctx.Render(name, binding, options...); err != nil { ctx.Panic() @@ -503,29 +519,43 @@ func (ctx *Context) TemplateString(name string, binding interface{}, options ... return ctx.framework.TemplateString(name, binding, options...) } +// HTML writes html string with a http status +func (ctx *Context) HTML(status int, htmlContents string) { + if err := ctx.RenderWithStatus(status, contentHTML, htmlContents); err != nil { + ctx.SetContentType(contentHTML + "; charset=" + ctx.framework.Config.Charset) + ctx.RequestCtx.SetStatusCode(status) + ctx.RequestCtx.WriteString(htmlContents) + } +} + +// Data writes out the raw bytes as binary data. +func (ctx *Context) Data(status int, v []byte) error { + return ctx.RenderWithStatus(status, contentBinary, v) +} + // JSON marshals the given interface object and writes the JSON response. func (ctx *Context) JSON(status int, v interface{}) error { - return ctx.framework.rest.JSON(ctx.RequestCtx, status, v) + return ctx.RenderWithStatus(status, contentJSON, v) } // JSONP marshals the given interface object and writes the JSON response. func (ctx *Context) JSONP(status int, callback string, v interface{}) error { - return ctx.framework.rest.JSONP(ctx.RequestCtx, status, callback, v) + return ctx.RenderWithStatus(status, contentJSONP, v, map[string]interface{}{"callback": callback}) } // Text writes out a string as plain text. func (ctx *Context) Text(status int, v string) error { - return ctx.framework.rest.Text(ctx.RequestCtx, status, v) + return ctx.RenderWithStatus(status, contentText, v) } // XML marshals the given interface object and writes the XML response. func (ctx *Context) XML(status int, v interface{}) error { - return ctx.framework.rest.XML(ctx.RequestCtx, status, v) + return ctx.RenderWithStatus(status, contentXML, v) } // MarkdownString parses the (dynamic) markdown string and returns the converted html string func (ctx *Context) MarkdownString(markdownText string) string { - return ctx.framework.rest.Markdown([]byte(markdownText)) + return ctx.framework.ResponseString(contentMarkdown, markdownText) } // Markdown parses and renders to the client a particular (dynamic) markdown string diff --git a/http.go b/http.go index ec7e932d..c742766e 100644 --- a/http.go +++ b/http.go @@ -641,7 +641,7 @@ func joinMiddleware(middleware1 Middleware, middleware2 Middleware) Middleware { func profileMiddleware(debugPath string) Middleware { htmlMiddleware := HandlerFunc(func(ctx *Context) { - ctx.SetContentType(contentHTML + "; charset=" + config.Charset) + ctx.SetContentType(contentHTML + "; charset=" + ctx.framework.Config.Charset) ctx.Next() }) indexHandler := ToHandlerFunc(pprof.Index) diff --git a/iris.go b/iris.go index 13824022..b1d20138 100644 --- a/iris.go +++ b/iris.go @@ -45,13 +45,14 @@ // // -----------------------------DOCUMENTATION---------------------------- // ----------------------------_______________--------------------------- -// For middleware, templates, sessions, websockets, mails, subdomains, +// For middleware, template engines, response engines, sessions, websockets, mails, subdomains, // dynamic subdomains, routes, party of subdomains & routes and much more // visit https://www.gitbook.com/book/kataras/iris/details package iris // import "github.com/kataras/iris" import ( "fmt" + "io" "net/http" "os" "path" @@ -61,23 +62,29 @@ import ( "testing" "time" + "github.com/klauspost/compress/gzip" + "sync" "github.com/gavv/httpexpect" "github.com/iris-contrib/errors" "github.com/iris-contrib/logger" - "github.com/iris-contrib/rest" + "github.com/iris-contrib/response/data" + "github.com/iris-contrib/response/json" + "github.com/iris-contrib/response/jsonp" + "github.com/iris-contrib/response/markdown" + "github.com/iris-contrib/response/text" + "github.com/iris-contrib/response/xml" "github.com/iris-contrib/template/html" "github.com/kataras/iris/config" "github.com/kataras/iris/context" "github.com/kataras/iris/utils" - "github.com/kataras/iris/websocket" "github.com/valyala/fasthttp" ) const ( // Version of the iris - Version = "4.0.0-alpha.2" + Version = "4.0.0-alpha.3" banner = ` _____ _ |_ _| (_) @@ -93,7 +100,7 @@ var ( Config *config.Iris Logger *logger.Logger Plugins PluginContainer - Websocket websocket.Server + Websocket WebsocketServer Servers *ServerList // Available is a channel type of bool, fired to true when the server is opened and all plugins ran // never fires false, if the .Close called then the channel is re-allocating. @@ -151,6 +158,7 @@ type ( Path(string, ...interface{}) string URL(string, ...interface{}) string TemplateString(string, interface{}, ...map[string]interface{}) string + ResponseString(string, interface{}, ...map[string]interface{}) string Tester(t *testing.T) *httpexpect.Expect } @@ -159,18 +167,18 @@ type ( // Implements the FrameworkAPI Framework struct { *muxAPI - rest *rest.Render - sessions *sessionsManager - templates *TemplateEngines - + Config *config.Iris + gzipWriterPool sync.Pool // used for several methods, usually inside context + sessions *sessionsManager + responses *responseEngines + templates *templateEngines // fields which are useful to the user/dev // the last added server is the main server Servers *ServerList - Config *config.Iris // configuration by instance.Logger.Config Logger *logger.Logger Plugins PluginContainer - Websocket websocket.Server + Websocket WebsocketServer Available chan bool // this is setted once when .Tester(t) is called testFramework *httpexpect.Expect @@ -188,7 +196,14 @@ func New(cfg ...config.Iris) *Framework { // we always use 's' no 'f' because 's' is easier for me to remember because of 'station' // some things never change :) - s := &Framework{Config: &c, Available: make(chan bool)} + s := &Framework{ + Config: &c, + gzipWriterPool: sync.Pool{New: func() interface{} { + return &gzip.Writer{} + }}, + responses: &responseEngines{}, + Available: make(chan bool), + } { ///NOTE: set all with s.Config pointer // set the Logger @@ -196,17 +211,17 @@ func New(cfg ...config.Iris) *Framework { // set the plugin container s.Plugins = &pluginContainer{logger: s.Logger} // set the templates - s.templates = &TemplateEngines{ - Helpers: map[string]interface{}{ + s.templates = &templateEngines{ + helpers: map[string]interface{}{ "url": s.URL, "urlpath": s.Path, }, - Engines: make([]*TemplateEngineWrapper, 0), + engines: make([]*templateEngineWrapper, 0), } //set the session manager s.sessions = newSessionsManager(c.Sessions) // set the websocket server - s.Websocket = websocket.NewServer(s.Config.Websocket) + s.Websocket = NewWebsocketServer(s.Config.Websocket) // set the servemux, which will provide us the public API also, with its context pool mux := newServeMux(sync.Pool{New: func() interface{} { return &Context{framework: s} }}, s.Logger) mux.onLookup = s.Plugins.DoPreLookup @@ -220,22 +235,46 @@ func New(cfg ...config.Iris) *Framework { } func (s *Framework) initialize() { - // set the rest - s.rest = rest.New(s.Config.Rest) + // prepare the response engines, if no response engines setted for the default content-types + // then add them + + for _, ctype := range defaultResponseKeys { + if rengine := s.responses.getBy(ctype); rengine == nil { + // if not exists + switch ctype { + case contentText: + s.UseResponse(text.New(), ctype) + case contentBinary: + s.UseResponse(data.New(), ctype) + case contentJSON: + s.UseResponse(json.New(), ctype) + case contentJSONP: + s.UseResponse(jsonp.New(), ctype) + case contentXML: + s.UseResponse(xml.New(), ctype) + case contentMarkdown: + s.UseResponse(markdown.New(), ctype) + } + } + } + // prepare the templates if enabled if !s.Config.DisableTemplateEngines { - if err := s.templates.LoadAll(); err != nil { - s.Logger.Panic(err) // panic on templates loading before listening if we have an error. - } + // check and prepare the templates - if len(s.templates.Engines) == 0 { // no template engine is registered, let's use the default + if len(s.templates.engines) == 0 { // no template engine is registered, let's use the default s.UseTemplate(html.New()) } - s.templates.Reload = s.Config.IsDevelopment + + if err := s.templates.loadAll(); err != nil { + s.Logger.Panic(err) // panic on templates loading before listening if we have an error. + } + + s.templates.reload = s.Config.IsDevelopment } // listen to websocket connections - websocket.RegisterServer(s, s.Websocket, s.Logger) + RegisterWebsocketServer(s, s.Websocket, s.Logger) // prepare the mux & the server s.mux.setCorrectPath(!s.Config.DisablePathCorrection) @@ -486,6 +525,40 @@ func (s *Framework) UseSessionDB(db SessionDatabase) { s.sessions.provider.registerDatabase(db) } +// UseResponse accepts a ResponseEngine and the key or content type on which the developer wants to register this response engine +// the gzip and charset are automatically supported by Iris, by passing the iris.RenderOptions{} map on the context.Render +// context.Render renders this response or a template engine if no response engine with the 'key' found +// with these engines you can inject the context.JSON,Text,Data,JSONP,XML also +// to do that just register with UseResponse(myEngine,"application/json") and so on +// look at the https://github.com/iris-contrib/response for examples +// +// if more than one respone engine with the same key/content type exists then the results will be appended to the final request's body +// this allows the developer to be able to create 'middleware' responses engines +// +// Note: if you pass an engine which contains a dot('.') as key, then the engine will not be registered. +// you don't have to import and use github.com/iris-contrib/json, jsonp, xml, data, text, markdown +// because iris uses these by default if no other response engine is registered for these content types +func UseResponse(e ResponseEngine, forContentTypesOrKeys ...string) { + Default.UseResponse(e, forContentTypesOrKeys...) +} + +// UseResponse accepts a ResponseEngine and the key or content type on which the developer wants to register this response engine +// the gzip and charset are automatically supported by Iris, by passing the iris.RenderOptions{} map on the context.Render +// context.Render renders this response or a template engine if no response engine with the 'key' found +// with these engines you can inject the context.JSON,Text,Data,JSONP,XML also +// to do that just register with UseResponse(myEngine,"application/json") and so on +// look at the https://github.com/iris-contrib/response for examples +// +// if more than one respone engine with the same key/content type exists then the results will be appended to the final request's body +// this allows the developer to be able to create 'middleware' responses engines +// +// Note: if you pass an engine which contains a dot('.') as key, then the engine will not be registered. +// you don't have to import and use github.com/iris-contrib/json, jsonp, xml, data, text, markdown +// because iris uses these by default if no other response engine is registered for these content types +func (s *Framework) UseResponse(e ResponseEngine, forContentTypesOrKeys ...string) { + s.responses.add(e, forContentTypesOrKeys...) +} + // UseTemplate adds a template engine to the iris view system // it does not build/load them yet func UseTemplate(e TemplateEngine) *TemplateEngineLocation { @@ -495,7 +568,7 @@ func UseTemplate(e TemplateEngine) *TemplateEngineLocation { // UseTemplate adds a template engine to the iris view system // it does not build/load them yet func (s *Framework) UseTemplate(e TemplateEngine) *TemplateEngineLocation { - return s.templates.Add(e) + return s.templates.add(e) } // UseGlobal registers Handler middleware to the beginning, prepends them instead of append @@ -716,6 +789,25 @@ func (s *Framework) URL(routeName string, args ...interface{}) (url string) { return } +// AcquireGzip prepares a gzip writer and returns it +// +// Note that: each iris station has its own pool +// see ReleaseGzip +func (s *Framework) AcquireGzip(w io.Writer) *gzip.Writer { + gzipWriter := s.gzipWriterPool.Get().(*gzip.Writer) + gzipWriter.Reset(w) + return gzipWriter +} + +// ReleaseGzip called when flush/close and put the gzip writer back to the pool +// +// Note that: each iris station has its own pool +// see AcquireGzip +func (s *Framework) ReleaseGzip(gzipWriter *gzip.Writer) { + gzipWriter.Close() + s.gzipWriterPool.Put(gzipWriter) +} + // TemplateString executes a template from the default template engine and returns its result as string, useful when you want it for sending rich e-mails // returns empty string on error func TemplateString(templateFile string, pageContext interface{}, options ...map[string]interface{}) string { @@ -728,7 +820,25 @@ func (s *Framework) TemplateString(templateFile string, pageContext interface{}, if s.Config.DisableTemplateEngines { return "" } - res, err := s.templates.GetBy(templateFile).ExecuteToString(templateFile, pageContext, options...) + res, err := s.templates.getBy(templateFile).executeToString(templateFile, pageContext, options...) + if err != nil { + return "" + } + return res +} + +// ResponseString returns the string of a response engine, +// does not render it to the client +// returns empty string on error +func ResponseString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string { + return Default.ResponseString(keyOrContentType, obj, options...) +} + +// ResponseString returns the string of a response engine, +// does not render it to the client +// returns empty string on error +func (s *Framework) ResponseString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string { + res, err := s.responses.getBy(keyOrContentType).toString(obj, options...) if err != nil { return "" } diff --git a/response.go b/response.go new file mode 100644 index 00000000..fa0907b5 --- /dev/null +++ b/response.go @@ -0,0 +1,220 @@ +package iris + +import ( + "strings" + + "github.com/iris-contrib/errors" +) + +type ( + // notes for me: + // edw an kai kalh idea alla den 9a borw na exw ta defaults mesa sto iris + // kai 9a prepei na to metaferw auto sto context i sto utils kai pragmatika den leei + // na kanoun import auto gia na kanoun to response engine, ara na prospa9isw kapws aliws mesw context.IContext mono + // ektos an metaferw ta defaults mesa sto iris + // alla an to kanw auto 9a prepei na vrw tropo na kanoun configuration ta defaults + // kai to idio prepei na kanw kai sto template engine html tote... + // diladi na kanw prwta to render tou real engine kai meta na parw + // ta contents tou body kai na ta kanw gzip ? na kanw resetbody kai na ta ksanasteilw? + // alla auto einai argh methodos kai gia ton poutso dn m aresei + // kai ola auta gia na mh valw ena property parapanw? 9a valw ... + // 9a einai writer,headers,object,options... anagastika. + + /* notes for me: + english 'final' thoughs' results now: + + the response engine will be registered with its content type + for example: iris.UseResponse("application/json", the engine or func here, optionalOptions...) + if more than one response engines registered for the same content type + then all the content results will be sent to the client + there will be available a system something like middleware but for the response engines + this will be useful when one response engine's job is only to add more content to the existing/parent response engine's results . + for example when you want to add a standard json object like a last_fetch { "year":...,"month":...,"day":...} for all json responses. + The engine will not have access to context or something like that. + The default engines will registered when no other engine with the same content type + already registered, like I did with template engines, so if someone wants to make a 'middleware' for the default response engines + must register them explicit and after register his/her response engine too, for that reason + the default engines will not be located inside iris but inside iris-contrib (like the default,and others, template engine), + the reason is to be easier to the user/dev to remember what import path should use when he/she wants to edit something, + for templates it's 'iris-contrib/template', for response engines will be 'iris-contrib/response'. + The body content will return as []byte, al/mong with an error if something bad happened. + Now you may ask why not set the header inside from response engine? because to do that we could have one of these four downsides: + 1.to have access to context.IContext or *Context(if *Context then default engines should live here in iris repo) + and if we have context.IContext or *Context we will not be able to set a fast built'n gzip option, + because we would copy the contents to the gzip writer, and after copy these contents back to the response body's writer + but with an io.Writer as parameter we can simple change this writer to gzip writer and continue to the response engine after. + 2. we could make something like ResponseWriter struct { io.Writer,Header *fasthttp.ResponseHeader} + inside iris repo(then the default response engines should exists in the iris repo and configuration will depends on the iris' configs ) + or inside context/ folder inside iris repo, then the user/dev should import this path to + do his/her response engine, and I want simple things as usual, also we would make a pool for this response writer and create new if not available exist, + and this is performarnce downs witch I dissalow on Iris whne no need. + 3. to have 4 parameters, the writer, the headers(again the user should import the fasthttp to do his/her response engine and I want simple things, as I told before), + the object and the optional parameters + 4. one more function to implement like 'ContentType() string', but if we select this we lose the functionality for ResponseEngine created as simple function, + and the biggest issue will be that one response engine must explicit exists for one content type, the user/dev will not be available (to easly) + to set the content type for the engine. + these were the reasons I decide to set the content type by the frontend iris API itself and not taken by the response engine. + + The Response will have two parameters (one required only) interface{], ...options}, and two return values([]byte,error) + The (first) parameter will be an interface{}, for json a json struct, for xml an xml struct, for binary data .([]byte) and so on + There will be available a second optional parameter, map of options, the "gzip" option will be built'n implemented by iris + so the response engines no need to manually add gzip support(same with template engines). + The Charset will be added to the headers automatically, for the previous example of json and the default charset which is UTF-8 + the end "Content-Type" header content will be: "application/json; charset=UTF-8" + if the registered content type is not a $content/type then the text/plain will be sent to the client. + + OR WAIT, some engines maybe want to set the content type or other headers dynamically or render a response depends on cookies or some other existence headers + on that situtions it will be impossible with this implementation I explained before, so... + access to context.IContext and return the []byte, in order to be able to add the built'n gzip support + the dev/user will have to make this import no no no we stick to the previous though, because + if the user wants to check all that he/she can just use a middleware with .Use/.UseFunc + this is not a middleware implementation, this is a custom content rendering, let's stick to that. + + Ok I did that and I realized that template and response engines, final method structure (string,interface{},options...) is the same + so I make the ctx.Render/RenderWithStatus to work with both engines, so the developer can use any type of response engine and render it with ease. + Maybe at the future I could have one file 'render.go' which will contain the template engines and response engines, we will see, these all are unique so excuse me if something goes wrong xD + + That's all. Hope some one (other than me) will understand the english here... + */ + + // ResponseEngine is the interface which all response engines should implement to send responses + // ResponseEngine(s) can be registered with,for example: iris.UseResponse(json.New(), "application/json") + ResponseEngine interface { + Response(interface{}, ...map[string]interface{}) ([]byte, error) + } + // ResponseEngineFunc is the alternative way to implement a ResponseEngine using a simple function + ResponseEngineFunc func(interface{}, ...map[string]interface{}) ([]byte, error) + + // responseEngineMap is a wrapper with key (content type or name) values(engines) for the registered response engine + // it contains all response engines for a specific contentType and two functions, render and toString + // these will be used by the iris' context and iris' ResponseString, yes like TemplateToString + // it's an internal struct, no need to be exported and return that on registration, + // because the two top funcs will be easier to use by the user/dev for multiple engines + responseEngineMap struct { + values []ResponseEngine + // this is used in order to the wrapper to be gettable by the responseEngines iteral, + // if key is not a $content/type then the text/plain will be sent to the client + key string + contentType string // it's not the full content type with charset + } +) + +var ( + // markdown is custom type, used inside iris to initialize the defaults response engines if no other engine registered with these keys + defaultResponseKeys = [...]string{contentText, contentXML, contentBinary, contentJSON, contentJSONP, contentMarkdown} +) + +var errNoResponseEngineFound = errors.New("No response engine found") + +// Response returns a response to the client(request's body content) +func (r ResponseEngineFunc) Response(obj interface{}, options ...map[string]interface{}) ([]byte, error) { + return r(obj, options...) +} + +// on context: Send(contentType string, obj interface{}, ...options) + +func (r *responseEngineMap) add(engine ResponseEngine) { + r.values = append(r.values, engine) +} + +// the gzip and charset options are built'n with iris +func (r *responseEngineMap) render(ctx *Context, obj interface{}, options ...map[string]interface{}) error { + + if r == nil { + //render, but no response engine registered, this caused by context.RenderWithStatus, and responseEngines. getBy + return errNoResponseEngineFound.Return() + } + + var finalResult []byte + + for i, n := 0, len(r.values); i < n; i++ { + result, err := r.values[i].Response(obj, options...) + if err != nil { // fail on first the first error + return err + } + finalResult = append(finalResult, result...) + } + + gzipEnabled := false + charset := ctx.framework.Config.Charset + if len(options) > 0 { + gzipEnabled = getGzipOption(options[0]) // located to the template.go below the RenderOptions + if chs := getCharsetOption(options[0]); chs != "" { + charset = chs + } + } + + ctx.SetContentType(r.contentType + "; charset=" + charset) + + if gzipEnabled { + ctx.Response.Header.Add("Content-Encoding", "gzip") + gzipWriter := ctx.framework.AcquireGzip(ctx.Response.BodyWriter()) + defer ctx.framework.ReleaseGzip(gzipWriter) + _, err := gzipWriter.Write(finalResult) + if err != nil { + return err + } + } else { + ctx.Response.SetBody(finalResult) + } + + return nil +} + +func (r *responseEngineMap) toString(obj interface{}, options ...map[string]interface{}) (string, error) { + if r == nil { + //render, but no response engine registered, this caused by context.RenderWithStatus, and responseEngines. getBy + return "", errNoResponseEngineFound.Return() + } + var finalResult []byte + for i, n := 0, len(r.values); i < n; i++ { + result, err := r.values[i].Response(obj, options...) + if err != nil { + return "", err + } + finalResult = append(finalResult, result...) + } + return string(finalResult), nil +} + +type responseEngines struct { + engines []*responseEngineMap +} + +// add accepts a simple response engine with its content type or key, key should not contains a dot('.'). +func (r *responseEngines) add(engine ResponseEngine, forContentTypesOrKeys ...string) { + if r.engines == nil { + r.engines = make([]*responseEngineMap, 0) + } + for _, key := range forContentTypesOrKeys { + if strings.IndexByte(key, '.') != -1 { // the dot is not allowed as key + continue // skip this engine + } + + defaultCtypeAndKey := contentText + if len(key) == 0 { + //if empty key, then set it to text/plain + key = defaultCtypeAndKey + } + + engineMap := r.getBy(key) + if engineMap == nil { + + ctype := defaultCtypeAndKey + if strings.IndexByte(key, slashByte) != -1 { // pure check, but developer should know the content types at least. + // we have 'valid' content type + ctype = key + } + engineMap = &responseEngineMap{values: make([]ResponseEngine, 0), key: key, contentType: ctype} + r.engines = append(r.engines, engineMap) + } + engineMap.add(engine) + } +} + +func (r *responseEngines) getBy(key string) *responseEngineMap { + for i, n := 0, len(r.engines); i < n; i++ { + return r.engines[i] + } + return nil +} diff --git a/template.go b/template.go index 2bd13e09..fc2f818a 100644 --- a/template.go +++ b/template.go @@ -1,11 +1,9 @@ package iris import ( - "compress/gzip" "io" "path/filepath" - "sync" "github.com/iris-contrib/errors" "github.com/kataras/iris/utils" @@ -17,14 +15,6 @@ var ( // DefaultTemplateDirectory the default directory if empty setted DefaultTemplateDirectory = "." + utils.PathSeparator + "templates" ) -var ( - // ContentTypeHTML the content type header for rendering - // this can be changed - ContentTypeHTML = "text/html" - // Charset the charset header for rendering - // this can be changed - Charset = "UTF-8" -) const ( @@ -81,6 +71,22 @@ func (t TemplateFuncs) IsFree(key string) bool { return true } +func getGzipOption(options map[string]interface{}) bool { + gzipOpt := options["gzip"] // we only need that, so don't create new map to keep the options. + if b, isBool := gzipOpt.(bool); isBool { + return b + } + return false +} + +func getCharsetOption(options map[string]interface{}) string { + charsetOpt := options["charset"] + if s, isString := charsetOpt.(string); isString { + return s + } + return "" // we return empty in order to set the default charset if not founded. +} + type ( // TemplateEngineLocation contains the funcs to set the location for the templates by directory or by binary TemplateEngineLocation struct { @@ -119,14 +125,12 @@ func (t *TemplateEngineLocation) isBinary() bool { return t.assetFn != nil && t.namesFn != nil } -// TemplateEngineWrapper is the wrapper of a template engine -type TemplateEngineWrapper struct { +// templateEngineWrapper is the wrapper of a template engine +type templateEngineWrapper struct { TemplateEngine - location *TemplateEngineLocation - buffer *utils.BufferPool - gzipWriterPool sync.Pool - reload bool - combiledContentType string + location *TemplateEngineLocation + buffer *utils.BufferPool + reload bool } var ( @@ -134,7 +138,7 @@ var ( errNoTemplateEngineForExt = errors.New("No template engine found to manage '%s' extensions") ) -func (t *TemplateEngineWrapper) load() error { +func (t *templateEngineWrapper) load() error { if t.location.isBinary() { t.LoadAssets(t.location.directory, t.location.extension, t.location.assetFn, t.location.namesFn) } else if t.location.directory != "" { @@ -145,13 +149,14 @@ func (t *TemplateEngineWrapper) load() error { return nil } -// Execute execute a template and write its result to the context's body +// execute execute a template and write its result to the context's body // options are the optional runtime options can be passed by user and catched by the template engine when render // an example of this is the "layout" // note that gzip option is an iris dynamic option which exists for all template engines -func (t *TemplateEngineWrapper) Execute(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) (err error) { +// the gzip and charset options are built'n with iris +func (t *templateEngineWrapper) execute(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) (err error) { if t == nil { - //file extension, but no template engine registered, this caused by context, and TemplateEngines. GetBy + //file extension, but no template engine registered, this caused by context, and templateEngines. getBy return errNoTemplateEngineForExt.Format(filepath.Ext(filename)) } if t.reload { @@ -162,10 +167,11 @@ func (t *TemplateEngineWrapper) Execute(ctx *Context, filename string, binding i // we do all these because we don't want to initialize a new map for each execution... gzipEnabled := false + charset := ctx.framework.Config.Charset if len(options) > 0 { - gzipOpt := options[0]["gzip"] // we only need that, so don't create new map to keep the options. - if b, isBool := gzipOpt.(bool); isBool { - gzipEnabled = b + gzipEnabled = getGzipOption(options[0]) + if chs := getCharsetOption(options[0]); chs != "" { + charset = chs } } @@ -178,26 +184,25 @@ func (t *TemplateEngineWrapper) Execute(ctx *Context, filename string, binding i } } + ctx.SetContentType(contentHTML + "; charset=" + charset) + var out io.Writer if gzipEnabled { ctx.Response.Header.Add("Content-Encoding", "gzip") - gzipWriter := t.gzipWriterPool.Get().(*gzip.Writer) - gzipWriter.Reset(ctx.Response.BodyWriter()) - defer gzipWriter.Close() - defer t.gzipWriterPool.Put(gzipWriter) + gzipWriter := ctx.framework.AcquireGzip(ctx.Response.BodyWriter()) + defer ctx.framework.ReleaseGzip(gzipWriter) out = gzipWriter } else { out = ctx.Response.BodyWriter() } - ctx.SetHeader("Content-Type", t.combiledContentType) return t.ExecuteWriter(out, filename, binding, options...) } -// ExecuteToString executes a template from a specific template engine and returns its contents result as string, it doesn't renders -func (t *TemplateEngineWrapper) ExecuteToString(filename string, binding interface{}, opt ...map[string]interface{}) (result string, err error) { +// executeToString executes a template from a specific template engine and returns its contents result as string, it doesn't renders +func (t *templateEngineWrapper) executeToString(filename string, binding interface{}, opt ...map[string]interface{}) (result string, err error) { if t == nil { - //file extension, but no template engine registered, this caused by context, and TemplateEngines. GetBy + //file extension, but no template engine registered, this caused by context, and templateEngines. getBy return "", errNoTemplateEngineForExt.Format(filepath.Ext(filename)) } if t.reload { @@ -215,18 +220,18 @@ func (t *TemplateEngineWrapper) ExecuteToString(filename string, binding interfa return } -// TemplateEngines is the container and manager of the template engines -type TemplateEngines struct { - Helpers map[string]interface{} - Engines []*TemplateEngineWrapper - Reload bool +// templateEngines is the container and manager of the template engines +type templateEngines struct { + helpers map[string]interface{} + engines []*templateEngineWrapper + reload bool } -// GetBy receives a filename, gets its extension and returns the template engine responsible for that file extension -func (t *TemplateEngines) GetBy(filename string) *TemplateEngineWrapper { +// getBy receives a filename, gets its extension and returns the template engine responsible for that file extension +func (t *templateEngines) getBy(filename string) *templateEngineWrapper { extension := filepath.Ext(filename) - for i, n := 0, len(t.Engines); i < n; i++ { - e := t.Engines[i] + for i, n := 0, len(t.engines); i < n; i++ { + e := t.engines[i] if e.location.extension == extension { return e @@ -235,38 +240,34 @@ func (t *TemplateEngines) GetBy(filename string) *TemplateEngineWrapper { return nil } -// Add adds but not loads a template engine -func (t *TemplateEngines) Add(e TemplateEngine) *TemplateEngineLocation { +// add adds but not loads a template engine +func (t *templateEngines) add(e TemplateEngine) *TemplateEngineLocation { location := &TemplateEngineLocation{} // add the iris helper funcs if funcer, ok := e.(TemplateEngineFuncs); ok { if funcer.Funcs() != nil { - for k, v := range t.Helpers { + for k, v := range t.helpers { funcer.Funcs()[k] = v } } } - tmplEngine := &TemplateEngineWrapper{ + tmplEngine := &templateEngineWrapper{ TemplateEngine: e, location: location, - buffer: utils.NewBufferPool(20), - gzipWriterPool: sync.Pool{New: func() interface{} { - return &gzip.Writer{} - }}, - reload: t.Reload, - combiledContentType: ContentTypeHTML + "; " + Charset, + buffer: utils.NewBufferPool(8), + reload: t.reload, } - t.Engines = append(t.Engines, tmplEngine) + t.engines = append(t.engines, tmplEngine) return location } -// LoadAll loads all templates using all template engines, returns the first error +// loadAll loads all templates using all template engines, returns the first error // called on iris' initialize -func (t *TemplateEngines) LoadAll() error { - for i, n := 0, len(t.Engines); i < n; i++ { - e := t.Engines[i] +func (t *templateEngines) loadAll() error { + for i, n := 0, len(t.engines); i < n; i++ { + e := t.engines[i] if e.location.directory == "" { e.location.directory = DefaultTemplateDirectory // the defualt dir ./templates } diff --git a/websocket.go b/websocket.go new file mode 100644 index 00000000..49f30dfb --- /dev/null +++ b/websocket.go @@ -0,0 +1,1207 @@ +package iris + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/iris-contrib/logger" + "github.com/iris-contrib/websocket" + "github.com/kataras/iris/config" + "github.com/kataras/iris/utils" +) + +// --------------------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------------------- +// --------------------------------Websocket implementation------------------------------------------------- +// Global functions in order to be able to use unlimitted number of websocket servers on each iris station-- +// --------------------------------------------------------------------------------------------------------- + +// NewWebsocketServer creates a websocket server and returns it +func NewWebsocketServer(c *config.Websocket) WebsocketServer { + return newWebsocketServer(c) +} + +// RegisterWebsocketServer registers the handlers for the websocket server +// it's a bridge between station and websocket server +func RegisterWebsocketServer(station FrameworkAPI, server WebsocketServer, logger *logger.Logger) { + c := server.Config() + if c.Endpoint == "" { + return + } + + websocketHandler := func(ctx *Context) { + if err := server.Upgrade(ctx); err != nil { + logger.Panic(err) + } + } + + if c.Headers != nil && len(c.Headers) > 0 { // only for performance matter just re-create the websocketHandler if we have headers to set + websocketHandler = func(ctx *Context) { + for k, v := range c.Headers { + ctx.SetHeader(k, v) + } + + if err := server.Upgrade(ctx); err != nil { + logger.Panic(err) + } + } + } + + station.Get(c.Endpoint, websocketHandler) + // serve the client side on domain:port/iris-ws.js + station.StaticContent("/iris-ws.js", "application/json", websocketClientSource) + +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------WebsocketServer implementation----------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +type ( + // WebsocketConnectionFunc is the callback which fires when a client/websocketConnection is connected to the websocketServer. + // Receives one parameter which is the WebsocketConnection + WebsocketConnectionFunc func(WebsocketConnection) + // WebsocketRooms is just a map with key a string and value slice of string + WebsocketRooms map[string][]string + + // websocketRoomPayload is used as payload from the websocketConnection to the websocketServer + websocketRoomPayload struct { + roomName string + websocketConnectionID string + } + + // payloads, websocketConnection -> websocketServer + websocketMessagePayload struct { + from string + to string + data []byte + } + + // WebsocketServer is the websocket server, listens on the config's port, the critical part is the event OnConnection + WebsocketServer interface { + Config() *config.Websocket + Upgrade(ctx *Context) error + OnConnection(cb WebsocketConnectionFunc) + } + + websocketServer struct { + config *config.Websocket + upgrader websocket.Upgrader + put chan *websocketConnection + free chan *websocketConnection + websocketConnections map[string]*websocketConnection + join chan websocketRoomPayload + leave chan websocketRoomPayload + rooms WebsocketRooms // by default a websocketConnection is joined to a room which has the websocketConnection id as its name + mu sync.Mutex // for rooms + messages chan websocketMessagePayload + onConnectionListeners []WebsocketConnectionFunc + //websocketConnectionPool *sync.Pool // sadly I can't make this because the websocket websocketConnection is live until is closed. + } +) + +var _ WebsocketServer = &websocketServer{} + +// websocketServer implementation + +// newWebsocketServer creates a websocket websocketServer and returns it +func newWebsocketServer(c *config.Websocket) *websocketServer { + s := &websocketServer{ + config: c, + put: make(chan *websocketConnection), + free: make(chan *websocketConnection), + websocketConnections: make(map[string]*websocketConnection), + join: make(chan websocketRoomPayload, 1), // buffered because join can be called immediately on websocketConnection connected + leave: make(chan websocketRoomPayload), + rooms: make(WebsocketRooms), + messages: make(chan websocketMessagePayload, 1), // buffered because messages can be sent/received immediately on websocketConnection connected + onConnectionListeners: make([]WebsocketConnectionFunc, 0), + } + + s.upgrader = websocket.Custom(s.handleWebsocketConnection, s.config.ReadBufferSize, s.config.WriteBufferSize, false) + go s.serve() // start the websocketServer automatically + return s +} + +func (s *websocketServer) Config() *config.Websocket { + return s.config +} + +func (s *websocketServer) Upgrade(ctx *Context) error { + return s.upgrader.Upgrade(ctx) +} + +func (s *websocketServer) handleWebsocketConnection(websocketConn *websocket.Conn) { + c := newWebsocketConnection(websocketConn, s) + s.put <- c + go c.writer() + c.reader() +} + +func (s *websocketServer) OnConnection(cb WebsocketConnectionFunc) { + s.onConnectionListeners = append(s.onConnectionListeners, cb) +} + +func (s *websocketServer) joinRoom(roomName string, connID string) { + s.mu.Lock() + if s.rooms[roomName] == nil { + s.rooms[roomName] = make([]string, 0) + } + s.rooms[roomName] = append(s.rooms[roomName], connID) + s.mu.Unlock() +} + +func (s *websocketServer) leaveRoom(roomName string, connID string) { + s.mu.Lock() + 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) + } + } + + s.mu.Unlock() +} + +func (s *websocketServer) serve() { + for { + select { + case c := <-s.put: // websocketConnection connected + s.websocketConnections[c.id] = c + // make and join a room with the websocketConnection's id + s.rooms[c.id] = make([]string, 0) + s.rooms[c.id] = []string{c.id} + for i := range s.onConnectionListeners { + s.onConnectionListeners[i](c) + } + case c := <-s.free: // websocketConnection closed + if _, found := s.websocketConnections[c.id]; found { + // leave from all rooms + for roomName := range s.rooms { + s.leaveRoom(roomName, c.id) + } + delete(s.websocketConnections, c.id) + close(c.send) + c.fireDisconnect() + + } + case join := <-s.join: + s.joinRoom(join.roomName, join.websocketConnectionID) + case leave := <-s.leave: + s.leaveRoom(leave.roomName, leave.websocketConnectionID) + case msg := <-s.messages: // message received from the websocketConnection + if msg.to != All && msg.to != NotMe && s.rooms[msg.to] != nil { + // it suppose to send the message to a room + for _, websocketConnectionIDInsideRoom := range s.rooms[msg.to] { + if c, connected := s.websocketConnections[websocketConnectionIDInsideRoom]; connected { + c.send <- msg.data //here we send it without need to continue below + } else { + // the websocketConnection is not connected but it's inside the room, we remove it on disconnect but for ANY CASE: + s.leaveRoom(c.id, msg.to) + } + } + + } else { // it suppose to send the message to all opened websocketConnections or to all except the sender + for connID, c := range s.websocketConnections { + if msg.to != All { // if it's not suppose to send to all websocketConnections (including itself) + if msg.to == NotMe && msg.from == connID { // if broadcast to other websocketConnections except this + continue //here we do the opossite of previous block, just skip this websocketConnection when it's suppose to send the message to all websocketConnections except the sender + } + } + select { + case s.websocketConnections[connID].send <- msg.data: //send the message back to the websocketConnection in order to send it to the client + default: + close(c.send) + delete(s.websocketConnections, connID) + c.fireDisconnect() + + } + + } + } + + } + + } +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------WebsocketEmmiter implementation---------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +const ( + // All is the string which the WebsocketEmmiter use to send a message to all + All = "" + // NotMe is the string which the WebsocketEmmiter use to send a message to all except this websocketConnection + NotMe = ";iris;to;all;except;me;" + // Broadcast is the string which the WebsocketEmmiter use to send a message to all except this websocketConnection, same as 'NotMe' + Broadcast = NotMe +) + +type ( + // WebsocketEmmiter is the message/or/event manager + WebsocketEmmiter interface { + // EmitMessage sends a native websocket message + EmitMessage([]byte) error + // Emit sends a message on a particular event + Emit(string, interface{}) error + } + + websocketEmmiter struct { + conn *websocketConnection + to string + } +) + +var _ WebsocketEmmiter = &websocketEmmiter{} + +func newWebsocketEmmiter(c *websocketConnection, to string) *websocketEmmiter { + return &websocketEmmiter{conn: c, to: to} +} + +func (e *websocketEmmiter) EmitMessage(nativeMessage []byte) error { + mp := websocketMessagePayload{e.conn.id, e.to, nativeMessage} + e.conn.websocketServer.messages <- mp + return nil +} + +func (e *websocketEmmiter) Emit(event string, data interface{}) error { + message, err := websocketMessageSerialize(event, data) + if err != nil { + return err + } + e.EmitMessage([]byte(message)) + return nil +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------WebsocketWebsocketConnection implementation------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +type ( + // WebsocketDisconnectFunc is the callback which fires when a client/websocketConnection closed + WebsocketDisconnectFunc func() + // WebsocketErrorFunc is the callback which fires when an error happens + WebsocketErrorFunc (func(string)) + // WebsocketNativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message + WebsocketNativeMessageFunc func([]byte) + // WebsocketMessageFunc is the second argument to the WebsocketEmmiter's Emit functions. + // A callback which should receives one parameter of type string, int, bool or any valid JSON/Go struct + WebsocketMessageFunc interface{} + // WebsocketConnection is the client + WebsocketConnection interface { + // WebsocketEmmiter implements EmitMessage & Emit + WebsocketEmmiter + // ID returns the websocketConnection's identifier + ID() string + // OnDisconnect registers a callback which fires when this websocketConnection is closed by an error or manual + OnDisconnect(WebsocketDisconnectFunc) + // OnError registers a callback which fires when this websocketConnection occurs an error + OnError(WebsocketErrorFunc) + // EmitError can be used to send a custom error message to the websocketConnection + // + // It does nothing more than firing the OnError listeners. It doesn't sends anything to the client. + EmitError(errorMessage string) + // To defines where websocketServer should send a message + // returns an emmiter to send messages + To(string) WebsocketEmmiter + // OnMessage registers a callback which fires when native websocket message received + OnMessage(WebsocketNativeMessageFunc) + // On registers a callback to a particular event which fires when a message to this event received + On(string, WebsocketMessageFunc) + // Join join a websocketConnection to a room, it doesn't check if websocketConnection is already there, so care + Join(string) + // Leave removes a websocketConnection from a room + Leave(string) + } + + websocketConnection struct { + underline *websocket.Conn + id string + send chan []byte + onDisconnectListeners []WebsocketDisconnectFunc + onErrorListeners []WebsocketErrorFunc + onNativeMessageListeners []WebsocketNativeMessageFunc + onEventListeners map[string][]WebsocketMessageFunc + // these were maden for performance only + self WebsocketEmmiter // pre-defined emmiter than sends message to its self client + broadcast WebsocketEmmiter // pre-defined emmiter that sends message to all except this + all WebsocketEmmiter // pre-defined emmiter which sends message to all clients + + websocketServer *websocketServer + } +) + +var _ WebsocketConnection = &websocketConnection{} + +func newWebsocketConnection(websocketConn *websocket.Conn, s *websocketServer) *websocketConnection { + c := &websocketConnection{ + id: utils.RandomString(64), + underline: websocketConn, + send: make(chan []byte, 256), + onDisconnectListeners: make([]WebsocketDisconnectFunc, 0), + onErrorListeners: make([]WebsocketErrorFunc, 0), + onNativeMessageListeners: make([]WebsocketNativeMessageFunc, 0), + onEventListeners: make(map[string][]WebsocketMessageFunc, 0), + websocketServer: s, + } + + c.self = newWebsocketEmmiter(c, c.id) + c.broadcast = newWebsocketEmmiter(c, NotMe) + c.all = newWebsocketEmmiter(c, All) + + return c +} + +func (c *websocketConnection) write(websocketMessageType int, data []byte) error { + c.underline.SetWriteDeadline(time.Now().Add(c.websocketServer.config.WriteTimeout)) + return c.underline.WriteMessage(websocketMessageType, data) +} + +func (c *websocketConnection) writer() { + ticker := time.NewTicker(c.websocketServer.config.PingPeriod) + defer func() { + ticker.Stop() + c.underline.Close() + }() + + for { + select { + case msg, ok := <-c.send: + if !ok { + defer func() { + + // FIX FOR: https://github.com/kataras/iris/issues/175 + // AS I TESTED ON TRIDENT ENGINE (INTERNET EXPLORER/SAFARI): + // NAVIGATE TO SITE, CLOSE THE TAB, NOTHING HAPPENS + // CLOSE THE WHOLE BROWSER, THEN THE c.conn is NOT NILL BUT ALL ITS FUNCTIONS PANICS, MEANS THAT IS THE STRUCT IS NOT NIL BUT THE WRITER/READER ARE NIL + // THE ONLY SOLUTION IS TO RECOVER HERE AT ANY PANIC + // THE FRAMETYPE = 8, c.closeSend = true + // NOTE THAT THE CLIENT IS NOT DISCONNECTED UNTIL THE WHOLE WINDOW BROWSER CLOSED, this is engine's bug. + // + if err := recover(); err != nil { + ticker.Stop() + c.websocketServer.free <- c + c.underline.Close() + } + }() + c.write(websocket.CloseMessage, []byte{}) + return + } + + c.underline.SetWriteDeadline(time.Now().Add(c.websocketServer.config.WriteTimeout)) + res, err := c.underline.NextWriter(websocket.TextMessage) + if err != nil { + return + } + res.Write(msg) + + n := len(c.send) + for i := 0; i < n; i++ { + res.Write(<-c.send) + } + + if err := res.Close(); err != nil { + return + } + + // if err := c.write(websocket.TextMessage, msg); err != nil { + // return + // } + + case <-ticker.C: + if err := c.write(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +func (c *websocketConnection) reader() { + defer func() { + c.websocketServer.free <- c + c.underline.Close() + }() + conn := c.underline + + conn.SetReadLimit(c.websocketServer.config.MaxMessageSize) + conn.SetReadDeadline(time.Now().Add(c.websocketServer.config.PongTimeout)) + conn.SetPongHandler(func(s string) error { + conn.SetReadDeadline(time.Now().Add(c.websocketServer.config.PongTimeout)) + return nil + }) + + for { + if _, data, err := conn.ReadMessage(); 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 (iris-ws custom message) +func (c *websocketConnection) messageReceived(data []byte) { + + if bytes.HasPrefix(data, websocketMessagePrefixBytes) { + customData := string(data) + //it's a custom iris-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 websocketServer 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 *websocketConnection) ID() string { + return c.id +} + +func (c *websocketConnection) fireDisconnect() { + for i := range c.onDisconnectListeners { + c.onDisconnectListeners[i]() + } +} + +func (c *websocketConnection) OnDisconnect(cb WebsocketDisconnectFunc) { + c.onDisconnectListeners = append(c.onDisconnectListeners, cb) +} + +func (c *websocketConnection) OnError(cb WebsocketErrorFunc) { + c.onErrorListeners = append(c.onErrorListeners, cb) +} + +func (c *websocketConnection) EmitError(errorMessage string) { + for _, cb := range c.onErrorListeners { + cb(errorMessage) + } +} + +func (c *websocketConnection) To(to string) WebsocketEmmiter { + if to == NotMe { // if send to all except me, then return the pre-defined emmiter, and so on + return c.broadcast + } else if to == All { + return c.all + } else if to == c.id { + return c.self + } + // is an emmiter to another client/websocketConnection + return newWebsocketEmmiter(c, to) +} + +func (c *websocketConnection) EmitMessage(nativeMessage []byte) error { + return c.self.EmitMessage(nativeMessage) +} + +func (c *websocketConnection) Emit(event string, message interface{}) error { + return c.self.Emit(event, message) +} + +func (c *websocketConnection) OnMessage(cb WebsocketNativeMessageFunc) { + c.onNativeMessageListeners = append(c.onNativeMessageListeners, cb) +} + +func (c *websocketConnection) On(event string, cb WebsocketMessageFunc) { + if c.onEventListeners[event] == nil { + c.onEventListeners[event] = make([]WebsocketMessageFunc, 0) + } + + c.onEventListeners[event] = append(c.onEventListeners[event], cb) +} + +func (c *websocketConnection) Join(roomName string) { + payload := websocketRoomPayload{roomName, c.id} + c.websocketServer.join <- payload +} + +func (c *websocketConnection) Leave(roomName string) { + payload := websocketRoomPayload{roomName, c.id} + c.websocketServer.leave <- payload +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// -----------------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 = utils.NewBufferPool(256) + 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 + +} + +// 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, fmt.Errorf("Type %s is invalid for message: %s", _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 +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// ----------------Client side websocket javascript source code ------------------------ +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +var websocketClientSource = []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) { + try { + JSON.parse(obj); + } + catch (e) { + return false; + } + return true; + }; + // + // 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) { + //iris-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 iris-ws message + // if native message then calls the fireNativeMessage + // else calls the fireMessage + // + // remember Iris 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 iris-ws 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 iris-custom websocket message + Ws.prototype.Emit = function (event, data) { + var messageStr = this.encodeMessage(event, data); + this.EmitMessage(messageStr); + }; + return Ws; +}()); +`) + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// ----------------Client side websocket commented typescript source code -------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +/* +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 { + try { + JSON.parse(obj); + } catch (e) { + return false; + } + return true; + } + + // + + // 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(event: string, websocketMessage: string): T | any { + //iris-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 iris-ws message + // if native message then calls the fireNativeMessage + // else calls the fireMessage + // + // remember Iris 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 iris-ws message + let message = 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 iris-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/websocket/README.md b/websocket/README.md deleted file mode 100644 index 06945e08..00000000 --- a/websocket/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Package information - -This package is new and unique, if you notice a bug or issue [post it here](https://github.com/kataras/iris/issues). - -# How to use - -[E-Book section](https://kataras.gitbooks.io/iris/content/package-websocket.html) - - -## Notes - -On **OSX + Safari**, we had an issue which is **fixed** now. BUT by the browser's Engine Design the socket is not closed until the whole browser window is closed, -so the **connection.OnDisconnect** event will fire when the user closes the **window browser**, **not just the browser's tab**. - -- Relative issue: https://github.com/kataras/iris/issues/175 diff --git a/websocket/client_side/iris-ws.ts b/websocket/client_side/iris-ws.ts deleted file mode 100644 index 162aab60..00000000 --- a/websocket/client_side/iris-ws.ts +++ /dev/null @@ -1,258 +0,0 @@ -const stringMessageType = 0; -const intMessageType = 1; -const boolMessageType = 2; -// bytes is missing here for reasons I will explain somewhen -const jsonMessageType = 4; - -const prefix = "iris-websocket-message:"; -const separator = ";"; - -const prefixLen = prefix.length; -var separatorLen = separator.length; -var prefixAndSepIdx = prefixLen + separatorLen - 1; -var prefixIdx = prefixLen - 1; -var separatorIdx = separatorLen - 1; - -type onConnectFunc = () => void; -type onDisconnectFunc = () => void; -type onNativeMessageFunc = (websocketMessage: string) => void; -type onMessageFunc = (message: any) => void; - -class Ws { - private conn: WebSocket; - private isReady: boolean; - - // events listeners - - private connectListeners: onConnectFunc[] = []; - private disconnectListeners: onDisconnectFunc[] = []; - private nativeMessageListeners: onNativeMessageFunc[] = []; - 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 { - try { - JSON.parse(obj); - } catch (e) { - return false; - } - return true; - } - - // - - // messages - private _msg(event: string, messageType: number, dataMessage: string): string { - - return prefix + event + separator + String(messageType) + separator + dataMessage; - } - - private encodeMessage(event: string, data: any): string { - let m = ""; - let t = 0; - if (this.isNumber(data)) { - t = intMessageType; - m = data.toString(); - } else if (this.isBoolean(data)) { - t = boolMessageType; - m = data.toString(); - } else if (this.isString(data)) { - t = stringMessageType; - m = data.toString(); - } else if (this.isJSON(data)) { - //propably json-object - t = jsonMessageType; - m = JSON.stringify(data); - } else { - console.log("Invalid"); - } - - return this._msg(event, t, m); - } - - private decodeMessage(event: string, websocketMessage: string): T | any { - //iris-websocket-message;user;4;themarshaledstringfromajsonstruct - let skipLen = prefixLen + separatorLen + event.length + 2; - if (websocketMessage.length < skipLen + 1) { - return null; - } - let messageType = parseInt(websocketMessage.charAt(skipLen - 2)); - let theMessage = websocketMessage.substring(skipLen, websocketMessage.length); - if (messageType == intMessageType) { - return parseInt(theMessage); - } else if (messageType == boolMessageType) { - return Boolean(theMessage); - } else if (messageType == stringMessageType) { - return theMessage; - } else if (messageType == jsonMessageType) { - return JSON.parse(theMessage); - } else { - return null; // invalid - } - } - - private getCustomEvent(websocketMessage: string): string { - if (websocketMessage.length < prefixAndSepIdx) { - return ""; - } - let s = websocketMessage.substring(prefixAndSepIdx, websocketMessage.length); - let evt = s.substring(0, s.indexOf(separator)); - - return evt; - } - - private getCustomMessage(event: string, websocketMessage: string): string { - let eventIdx = websocketMessage.indexOf(event + separator); - let s = websocketMessage.substring(eventIdx + event.length + separator.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 iris-ws message - // if native message then calls the fireNativeMessage - // else calls the fireMessage - // - // remember Iris 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 iris-ws message - let message = evt.data; - if (message.indexOf(prefix) != -1) { - let event = this.getCustomEvent(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: onDisconnectFunc): void { - this.disconnectListeners.push(fn); - } - - fireDisconnect(): void { - for (let i = 0; i < this.disconnectListeners.length; i++) { - this.disconnectListeners[i](); - } - } - - OnMessage(cb: onNativeMessageFunc): 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 iris-custom websocket message - Emit(event: string, data: any): void { - let messageStr = this.encodeMessage(event, data); - this.EmitMessage(messageStr); - } - - // - -} - -// node-modules export {Ws}; \ No newline at end of file diff --git a/websocket/connection.go b/websocket/connection.go deleted file mode 100644 index 60eb41cf..00000000 --- a/websocket/connection.go +++ /dev/null @@ -1,296 +0,0 @@ -package websocket - -import ( - "time" - - "bytes" - - "strconv" - - "github.com/iris-contrib/websocket" - "github.com/kataras/iris/utils" -) - -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 client - Connection interface { - // Emitter implements EmitMessage & Emit - Emitter - // ID returns the connection's identifier - ID() string - // 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 emmiter 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) - } - - connection struct { - underline *websocket.Conn - id string - send chan []byte - onDisconnectListeners []DisconnectFunc - onErrorListeners []ErrorFunc - onNativeMessageListeners []NativeMessageFunc - onEventListeners map[string][]MessageFunc - // these were maden for performance only - self Emitter // pre-defined emmiter than sends message to its self client - broadcast Emitter // pre-defined emmiter that sends message to all except this - all Emitter // pre-defined emmiter which sends message to all clients - - server *server - } -) - -var _ Connection = &connection{} - -// connection implementation - -func newConnection(websocketConn *websocket.Conn, s *server) *connection { - c := &connection{ - id: utils.RandomString(64), - underline: websocketConn, - send: make(chan []byte, 256), - onDisconnectListeners: make([]DisconnectFunc, 0), - onErrorListeners: make([]ErrorFunc, 0), - onNativeMessageListeners: make([]NativeMessageFunc, 0), - onEventListeners: make(map[string][]MessageFunc, 0), - server: s, - } - - c.self = newEmitter(c, c.id) - c.broadcast = newEmitter(c, NotMe) - c.all = newEmitter(c, All) - - return c -} - -func (c *connection) write(messageType int, data []byte) error { - c.underline.SetWriteDeadline(time.Now().Add(c.server.config.WriteTimeout)) - return c.underline.WriteMessage(messageType, data) -} - -func (c *connection) writer() { - ticker := time.NewTicker(c.server.config.PingPeriod) - defer func() { - ticker.Stop() - c.underline.Close() - }() - - for { - select { - case msg, ok := <-c.send: - if !ok { - defer func() { - - // FIX FOR: https://github.com/kataras/iris/issues/175 - // AS I TESTED ON TRIDENT ENGINE (INTERNET EXPLORER/SAFARI): - // NAVIGATE TO SITE, CLOSE THE TAB, NOTHING HAPPENS - // CLOSE THE WHOLE BROWSER, THEN THE c.conn is NOT NILL BUT ALL ITS FUNCTIONS PANICS, MEANS THAT IS THE STRUCT IS NOT NIL BUT THE WRITER/READER ARE NIL - // THE ONLY SOLUTION IS TO RECOVER HERE AT ANY PANIC - // THE FRAMETYPE = 8, c.closeSend = true - // NOTE THAT THE CLIENT IS NOT DISCONNECTED UNTIL THE WHOLE WINDOW BROWSER CLOSED, this is engine's bug. - // - if err := recover(); err != nil { - ticker.Stop() - c.server.free <- c - c.underline.Close() - } - }() - c.write(websocket.CloseMessage, []byte{}) - return - } - - c.underline.SetWriteDeadline(time.Now().Add(c.server.config.WriteTimeout)) - res, err := c.underline.NextWriter(websocket.TextMessage) - if err != nil { - return - } - res.Write(msg) - - n := len(c.send) - for i := 0; i < n; i++ { - res.Write(<-c.send) - } - - if err := res.Close(); err != nil { - return - } - - // if err := c.write(websocket.TextMessage, msg); err != nil { - // return - // } - - case <-ticker.C: - if err := c.write(websocket.PingMessage, []byte{}); err != nil { - return - } - } - } -} - -func (c *connection) reader() { - defer func() { - c.server.free <- c - c.underline.Close() - }() - conn := c.underline - - conn.SetReadLimit(c.server.config.MaxMessageSize) - conn.SetReadDeadline(time.Now().Add(c.server.config.PongTimeout)) - conn.SetPongHandler(func(s string) error { - conn.SetReadDeadline(time.Now().Add(c.server.config.PongTimeout)) - return nil - }) - - for { - if _, data, err := conn.ReadMessage(); 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 (iris-ws custom message) -func (c *connection) messageReceived(data []byte) { - - if bytes.HasPrefix(data, prefixBytes) { - customData := string(data) - //it's a custom iris-ws message - receivedEvt := getCustomEvent(customData) - listeners := c.onEventListeners[receivedEvt] - if listeners == nil { // if not listeners for this event exit from here - return - } - customMessage, err := deserialize(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) 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 == NotMe { // if send to all except me, then return the pre-defined emmiter, and so on - return c.broadcast - } else if to == All { - return c.all - } else if to == c.id { - return c.self - } - // is an emmiter 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) { - payload := roomPayload{roomName, c.id} - c.server.join <- payload -} - -func (c *connection) Leave(roomName string) { - payload := roomPayload{roomName, c.id} - c.server.leave <- payload -} - -// diff --git a/websocket/emitter.go b/websocket/emitter.go deleted file mode 100644 index 892cc491..00000000 --- a/websocket/emitter.go +++ /dev/null @@ -1,50 +0,0 @@ -package websocket - -const ( - // All is the string which the Emitter use to send a message to all - All = "" - // NotMe is the string which the Emitter use to send a message to all except this connection - NotMe = ";iris;to;all;except;me;" - // Broadcast is the string which the Emitter use to send a message to all except this connection, same as 'NotMe' - Broadcast = NotMe -) - -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{} - -// emitter implementation - -func newEmitter(c *connection, to string) *emitter { - return &emitter{conn: c, to: to} -} - -func (e *emitter) EmitMessage(nativeMessage []byte) error { - mp := messagePayload{e.conn.id, e.to, nativeMessage} - e.conn.server.messages <- mp - return nil -} - -func (e *emitter) Emit(event string, data interface{}) error { - message, err := serialize(event, data) - if err != nil { - return err - } - e.EmitMessage([]byte(message)) - return nil -} - -// diff --git a/websocket/serializer.go b/websocket/serializer.go deleted file mode 100644 index 4d513f46..00000000 --- a/websocket/serializer.go +++ /dev/null @@ -1,145 +0,0 @@ -package websocket - -import ( - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/kataras/iris/utils" -) - -/* -serializer, [de]serialize the messages from the client to the server and from the server to the client -*/ - -// The same values are exists on client side also -const ( - stringMessageType messageType = iota - intMessageType - boolMessageType - bytesMessageType - jsonMessageType -) - -const ( - prefix = "iris-websocket-message:" - separator = ";" - prefixLen = len(prefix) - separatorLen = len(separator) - prefixAndSepIdx = prefixLen + separatorLen - 1 - prefixIdx = prefixLen - 1 - separatorIdx = separatorLen - 1 -) - -var ( - separatorByte = separator[0] - buf = utils.NewBufferPool(256) - prefixBytes = []byte(prefix) -) - -type ( - messageType uint8 -) - -func (m messageType) String() string { - return strconv.Itoa(int(m)) -} - -func (m messageType) Name() string { - if m == stringMessageType { - return "string" - } else if m == intMessageType { - return "int" - } else if m == boolMessageType { - return "bool" - } else if m == bytesMessageType { - return "[]byte" - } else if m == jsonMessageType { - return "json" - } - - return "Invalid(" + m.String() + ")" - -} - -// serialize serializes a custom websocket message from server to be delivered to the client -// returns the string form of the message -// Supported data types are: string, int, bool, bytes and JSON. -func serialize(event string, data interface{}) (string, error) { - var msgType messageType - var dataMessage string - - if s, ok := data.(string); ok { - msgType = stringMessageType - dataMessage = s - } else if i, ok := data.(int); ok { - msgType = intMessageType - dataMessage = strconv.Itoa(i) - } else if b, ok := data.(bool); ok { - msgType = boolMessageType - dataMessage = strconv.FormatBool(b) - } else if by, ok := data.([]byte); ok { - msgType = bytesMessageType - dataMessage = string(by) - } else { - //we suppose is json - res, err := json.Marshal(data) - if err != nil { - return "", err - } - msgType = jsonMessageType - dataMessage = string(res) - } - - b := buf.Get() - b.WriteString(prefix) - b.WriteString(event) - b.WriteString(separator) - b.WriteString(msgType.String()) - b.WriteString(separator) - b.WriteString(dataMessage) - dataMessage = b.String() - buf.Put(b) - - return dataMessage, nil - -} - -// deserialize 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 deserialize(event string, websocketMessage string) (message interface{}, err error) { - t, formaterr := strconv.Atoi(websocketMessage[prefixAndSepIdx+len(event)+1 : prefixAndSepIdx+len(event)+2]) // in order to iris-websocket-message;user;-> 4 - if formaterr != nil { - return nil, formaterr - } - _type := messageType(t) - _message := websocketMessage[prefixAndSepIdx+len(event)+3:] // in order to iris-websocket-message;user;4; -> themarshaledstringfromajsonstruct - - if _type == stringMessageType { - message = string(_message) - } else if _type == intMessageType { - message, err = strconv.Atoi(_message) - } else if _type == boolMessageType { - message, err = strconv.ParseBool(_message) - } else if _type == bytesMessageType { - message = []byte(_message) - } else if _type == jsonMessageType { - err = json.Unmarshal([]byte(_message), message) - } else { - return nil, fmt.Errorf("Type %s is invalid for message: %s", _type.Name(), websocketMessage) - } - - return -} - -// getCustomEvent return empty string when the websocketMessage is native message -func getCustomEvent(websocketMessage string) string { - if len(websocketMessage) < prefixAndSepIdx { - return "" - } - s := websocketMessage[prefixAndSepIdx:] - evt := s[:strings.IndexByte(s, separatorByte)] - return evt -} diff --git a/websocket/server.go b/websocket/server.go deleted file mode 100644 index 49cc936d..00000000 --- a/websocket/server.go +++ /dev/null @@ -1,189 +0,0 @@ -package websocket - -import ( - "sync" - - "github.com/iris-contrib/websocket" - "github.com/kataras/iris/config" - "github.com/kataras/iris/context" -) - -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) - // Rooms is just a map with key a string and value slice of string - Rooms map[string][]string - // Server is the websocket server - Server interface { - // Upgrade upgrades the client in order websocket works - Upgrade(context.IContext) error - // OnConnection registers a callback which fires when a connection/client is connected to the server - OnConnection(ConnectionFunc) - // Config returns a pointer to server's configs - Config() *config.Websocket - } - - // roomPayload is used as payload from the connection to the server - roomPayload struct { - roomName string - connectionID string - } - - // payloads, connection -> server - messagePayload struct { - from string - to string - data []byte - } - - // - - server struct { - config *config.Websocket - upgrader websocket.Upgrader - put chan *connection - free chan *connection - connections map[string]*connection - join chan roomPayload - leave chan roomPayload - rooms Rooms // by default a connection is joined to a room which has the connection id as its name - mu sync.Mutex // for rooms - messages chan messagePayload - 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 - -// newServer creates a websocket server and returns it -func newServer(c *config.Websocket) *server { - s := &server{ - config: c, - put: make(chan *connection), - free: make(chan *connection), - connections: make(map[string]*connection), - join: make(chan roomPayload, 1), // buffered because join can be called immediately on connection connected - leave: make(chan roomPayload), - rooms: make(Rooms), - messages: make(chan messagePayload, 1), // buffered because messages can be sent/received immediately on connection connected - onConnectionListeners: make([]ConnectionFunc, 0), - } - - s.upgrader = websocket.Custom(s.handleConnection, s.config.ReadBufferSize, s.config.WriteBufferSize, false) - go s.serve() // start the server automatically - return s -} - -func (s *server) Config() *config.Websocket { - return s.config -} - -func (s *server) Upgrade(ctx context.IContext) error { - return s.upgrader.Upgrade(ctx) -} - -func (s *server) handleConnection(websocketConn *websocket.Conn) { - c := newConnection(websocketConn, s) - s.put <- c - go c.writer() - c.reader() -} - -func (s *server) OnConnection(cb ConnectionFunc) { - s.onConnectionListeners = append(s.onConnectionListeners, cb) -} - -func (s *server) joinRoom(roomName string, connID string) { - s.mu.Lock() - if s.rooms[roomName] == nil { - s.rooms[roomName] = make([]string, 0) - } - s.rooms[roomName] = append(s.rooms[roomName], connID) - s.mu.Unlock() -} - -func (s *server) leaveRoom(roomName string, connID string) { - s.mu.Lock() - 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) - } - } - - s.mu.Unlock() -} - -func (s *server) serve() { - for { - select { - case c := <-s.put: // connection connected - s.connections[c.id] = c - // make and join a room with the connection's id - s.rooms[c.id] = make([]string, 0) - s.rooms[c.id] = []string{c.id} - for i := range s.onConnectionListeners { - s.onConnectionListeners[i](c) - } - case c := <-s.free: // connection closed - if _, found := s.connections[c.id]; found { - // leave from all rooms - for roomName := range s.rooms { - s.leaveRoom(roomName, c.id) - } - delete(s.connections, c.id) - close(c.send) - c.fireDisconnect() - - } - case join := <-s.join: - s.joinRoom(join.roomName, join.connectionID) - case leave := <-s.leave: - s.leaveRoom(leave.roomName, leave.connectionID) - case msg := <-s.messages: // message received from the connection - if msg.to != All && msg.to != NotMe && s.rooms[msg.to] != nil { - // it suppose to send the message to a room - for _, connectionIDInsideRoom := range s.rooms[msg.to] { - if c, connected := s.connections[connectionIDInsideRoom]; connected { - c.send <- msg.data //here we send it without need to continue below - } else { - // the connection is not connected but it's inside the room, we remove it on disconnect but for ANY CASE: - s.leaveRoom(c.id, msg.to) - } - } - - } else { // it suppose to send the message to all opened connections or to all except the sender - for connID, c := range s.connections { - if msg.to != All { // if it's not suppose to send to all connections (including itself) - if msg.to == NotMe && msg.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 - } - } - select { - case s.connections[connID].send <- msg.data: //send the message back to the connection in order to send it to the client - default: - close(c.send) - delete(s.connections, connID) - c.fireDisconnect() - - } - - } - } - - } - - } -} - -// diff --git a/websocket/websocket.go b/websocket/websocket.go deleted file mode 100644 index 71efaf55..00000000 --- a/websocket/websocket.go +++ /dev/null @@ -1,288 +0,0 @@ -package websocket - -import ( - "github.com/iris-contrib/logger" - "github.com/kataras/iris/config" - "github.com/kataras/iris/context" -) - -// to avoid the import cycle to /kataras/iris. The ws package is used inside iris' station configuration -// inside Iris' configuration like kataras/iris/sessions, kataras/iris/render/rest, kataras/iris/render/template, kataras/iris/server and so on. -type irisStation interface { - H_(string, string, func(context.IContext)) func(string) - StaticContent(string, string, []byte) func(string) -} - -// - -// New returns a new running websocket server, registers this to the iris station -// -// Note that: -// This is not usable for you, unless you need more than one websocket server, -// because iris' station already has one which you can configure and start -// -// This is deprecated after rc-1, now we create the server and after register it -// because I want to be able to call the Websocket via a property and no via func before iris.Listen. -func New(station irisStation, c *config.Websocket, logger *logger.Logger) Server { - if c.Endpoint == "" { - //station.Logger().Panicf("Websockets - config's Endpoint is empty, you have to set it in order to enable and start the websocket server!!. Refer to the docs if you can't figure out.") - return nil - } - server := newServer(c) - RegisterServer(station, server, logger) - return server -} - -// NewServer creates a websocket server and returns it -func NewServer(c *config.Websocket) Server { - return newServer(c) -} - -// RegisterServer registers the handlers for the websocket server -// it's a bridge between station and websocket server -func RegisterServer(station irisStation, server Server, logger *logger.Logger) { - c := server.Config() - if c.Endpoint == "" { - return - } - - websocketHandler := func(ctx context.IContext) { - if err := server.Upgrade(ctx); err != nil { - logger.Panic(err) - } - } - - if c.Headers != nil && len(c.Headers) > 0 { // only for performance matter just re-create the websocketHandler if we have headers to set - websocketHandler = func(ctx context.IContext) { - for k, v := range c.Headers { - ctx.SetHeader(k, v) - } - - if err := server.Upgrade(ctx); err != nil { - logger.Panic(err) - } - } - } - - station.H_("GET", c.Endpoint, websocketHandler) - // serve the client side on domain:port/iris-ws.js - station.StaticContent("/iris-ws.js", "application/json", clientSource) - -} - -var clientSource = []byte(`var stringMessageType = 0; -var intMessageType = 1; -var boolMessageType = 2; -// bytes is missing here for reasons I will explain somewhen -var jsonMessageType = 4; -var prefix = "iris-websocket-message:"; -var separator = ";"; -var prefixLen = prefix.length; -var separatorLen = separator.length; -var prefixAndSepIdx = prefixLen + separatorLen - 1; -var prefixIdx = prefixLen - 1; -var separatorIdx = separatorLen - 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) { - try { - JSON.parse(obj); - } - catch (e) { - return false; - } - return true; - }; - // - // messages - Ws.prototype._msg = function (event, messageType, dataMessage) { - return prefix + event + separator + String(messageType) + separator + dataMessage; - }; - Ws.prototype.encodeMessage = function (event, data) { - var m = ""; - var t = 0; - if (this.isNumber(data)) { - t = intMessageType; - m = data.toString(); - } - else if (this.isBoolean(data)) { - t = boolMessageType; - m = data.toString(); - } - else if (this.isString(data)) { - t = stringMessageType; - m = data.toString(); - } - else if (this.isJSON(data)) { - //propably json-object - t = jsonMessageType; - m = JSON.stringify(data); - } - else { - console.log("Invalid"); - } - return this._msg(event, t, m); - }; - Ws.prototype.decodeMessage = function (event, websocketMessage) { - //iris-websocket-message;user;4;themarshaledstringfromajsonstruct - var skipLen = prefixLen + separatorLen + event.length + 2; - if (websocketMessage.length < skipLen + 1) { - return null; - } - var messageType = parseInt(websocketMessage.charAt(skipLen - 2)); - var theMessage = websocketMessage.substring(skipLen, websocketMessage.length); - if (messageType == intMessageType) { - return parseInt(theMessage); - } - else if (messageType == boolMessageType) { - return Boolean(theMessage); - } - else if (messageType == stringMessageType) { - return theMessage; - } - else if (messageType == jsonMessageType) { - return JSON.parse(theMessage); - } - else { - return null; // invalid - } - }; - Ws.prototype.getCustomEvent = function (websocketMessage) { - if (websocketMessage.length < prefixAndSepIdx) { - return ""; - } - var s = websocketMessage.substring(prefixAndSepIdx, websocketMessage.length); - var evt = s.substring(0, s.indexOf(separator)); - return evt; - }; - Ws.prototype.getCustomMessage = function (event, websocketMessage) { - var eventIdx = websocketMessage.indexOf(event + separator); - var s = websocketMessage.substring(eventIdx + event.length + separator.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 iris-ws message - // if native message then calls the fireNativeMessage - // else calls the fireMessage - // - // remember Iris 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 iris-ws message - var message = evt.data; - if (message.indexOf(prefix) != -1) { - var event_1 = this.getCustomEvent(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 iris-custom websocket message - Ws.prototype.Emit = function (event, data) { - var messageStr = this.encodeMessage(event, data); - this.EmitMessage(messageStr); - }; - return Ws; -}()); -`)