diff --git a/HISTORY.md b/HISTORY.md index c1d5d32a..a4b0f2fb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,286 @@ # History -**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`. +**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`. + +## 4.1.7 -> 4.2.0 + +- **ADDED**: `iris.TemplateSourceString(src string, binding interface{}) string` this will parse the src raw contents to the template engine and return the string result & `context.RenderTemplateSource(status int, src string, binding interface{}, options ...map[string]interface{}) error` this will parse the src raw contents to the template engine and render the result to the client, as requseted [here](https://github.com/kataras/iris/issues/409). + + +This version has 'breaking' changes if you were, directly, passing custom configuration to a custom iris instance before. +As the TODO2 I had to think and implement a way to make configuration even easier and more simple to use. + +With last changes in place, Iris is using new, cross-framework, and more stable packages made by me(so don't worry things are working and will as you expect) to render [templates](https://github.com/kataras/go-template), manage [sessions](https://github.com/kataras/go-sesions) and [websockets](https://github.com/kataras/go-websocket). So the `/kataras/iris/config` is no longer need to be there, we don't have core packages inside iris which need these configuration to other package-folder than the main anymore(in order to avoid the import-cycle), new file `/kataras/iris/configuration.go` is created for the configuration, which lives inside the main package, means that now: + +- **if you want to pass directly configuration to a new custom iris instance, you don't have to import the github.com/kataras/iris/config package** + +Naming changes: + +- `config.Iris` -> `iris.Configuration`, which is the parent/main configuration. Added: `TimeFormat` and `Other` (pass any dynamic custom, other options there) +- `config.Sessions` -> `iris.SessionsConfiguration` +- `config.Websocket` -> `iris.WebscoketConfiguration` +- `config.Server` -> `iris.ServerConfiguration` +- `config.Tester` -> `iris.TesterConfiguration` + +All these changes wasn't made only to remove the `./config` folder but to make easier for you to pass the exact configuration field/option you need to edit at the top of the default configuration, without need to pass the whole Configuration object. **Attention**: old way, pass `iris.Configuration` directly, is still valid object to pass to the `iris.New`, so don't be afraid for breaking change, the only thing you will need to edit is the names of the configuration you saw on the previous paragraph. + +**Configuration Declaration**: + +instead of old, but still valid to pass to the `iris.New`: +- `iris.New(iris.Configuration{Charset: "UTF-8", Sessions: iris.SessionsConfiguration{Cookie: "cookienameid"}})` +now you can just write this: +- `iris.New(iris.OptionCharset("UTF-8"), iris.OptionSessionsCookie("cookienameid"))` + +`.New` **by configuration** +```go +import "github.com/kataras/iris" +//... +myConfig := iris.Configuration{Charset: "UTF-8", IsDevelopment:true, Sessions: iris.SessionsConfiguration{Cookie:"mycookie"}, Websocket: iris.WebsocketConfiguration{Endpoint: "/my_endpoint"}} +iris.New(myConfig) +``` + +`.New` **by options** + +```go +import "github.com/kataras/iris" +//... +iris.New(iris.OptionCharset("UTF-8"), iris.OptionIsDevelopment(true), iris.OptionSessionsCookie("mycookie"), iris.OptionWebsocketEndpoint("/my_endpoint")) + +// if you want to set configuration after the .New use the .Set: +iris.Set(iris.OptionDisableBanner(true)) +``` + +**List** of all available options: +```go +// OptionDisablePathCorrection corrects and redirects the requested path to the registed path +// for example, if /home/ path is requested but no handler for this Route found, +// then the Router checks if /home handler exists, if yes, +// (permant)redirects the client to the correct path /home +// +// Default is false +OptionDisablePathCorrection(val bool) + +// OptionDisablePathEscape when is false then its escapes the path, the named parameters (if any). +OptionDisablePathEscape(val bool) + +// OptionDisableBanner outputs the iris banner at startup +// +// Default is false +OptionDisableBanner(val bool) + +// OptionLoggerOut is the destination for output +// +// Default is os.Stdout +OptionLoggerOut(val io.Writer) + +// OptionLoggerPreffix is the logger's prefix to write at beginning of each line +// +// Default is [IRIS] +OptionLoggerPreffix(val string) + +// OptionProfilePath a the route path, set it to enable http pprof tool +// Default is empty, if you set it to a $path, these routes will handled: +OptionProfilePath(val string) + +// OptionDisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine +// Default is false +OptionDisableTemplateEngines(val bool) + +// OptionIsDevelopment iris will act like a developer, for example +// If true then re-builds the templates on each request +// Default is false +OptionIsDevelopment(val bool) + +// OptionTimeFormat time format for any kind of datetime parsing +OptionTimeFormat(val string) + +// OptionCharset character encoding for various rendering +// used for templates and the rest of the responses +// Default is "UTF-8" +OptionCharset(val string) + +// OptionGzip enables gzip compression on your Render actions, this includes any type of render, templates and pure/raw content +// If you don't want to enable it globaly, you could just use the third parameter on context.Render("myfileOrResponse", structBinding{}, iris.RenderOptions{"gzip": true}) +// Default is false +OptionGzip(val bool) + +// OptionOther are the custom, dynamic options, can be empty +// this fill used only by you to set any app's options you want +// for each of an Iris instance +OptionOther(val ...options.Options) //map[string]interface{}, options is github.com/kataras/go-options + +// OptionSessionsCookie string, the session's client cookie name, for example: "qsessionid" +OptionSessionsCookie(val string) + +// OptionSessionsDecodeCookie set it to true to decode the cookie key with base64 URLEncoding +// Defaults to false +OptionSessionsDecodeCookie(val bool) + +// OptionSessionsExpires the duration of which the cookie must expires (created_time.Add(Expires)). +// If you want to delete the cookie when the browser closes, set it to -1 but in this case, the server side's session duration is up to GcDuration +// +// Default infinitive/unlimited life duration(0) +OptionSessionsExpires(val time.Duration) + +// OptionSessionsCookieLength the length of the sessionid's cookie's value, let it to 0 if you don't want to change it +// Defaults to 32 +OptionSessionsCookieLength(val int) + +// OptionSessionsGcDuration every how much duration(GcDuration) the memory should be clear for unused cookies (GcDuration) +// for example: time.Duration(2)*time.Hour. it will check every 2 hours if cookie hasn't be used for 2 hours, +// deletes it from backend memory until the user comes back, then the session continue to work as it was +// +// Default 2 hours +OptionSessionsGcDuration(val time.Duration) + +// OptionSessionsDisableSubdomainPersistence set it to true in order dissallow your q subdomains to have access to the session cookie +// defaults to false +OptionSessionsDisableSubdomainPersistence(val bool) + +// OptionWebsocketWriteTimeout time allowed to write a message to the connection. +// Default value is 15 * time.Second +OptionWebsocketWriteTimeout(val time.Duration) + +// OptionWebsocketPongTimeout allowed to read the next pong message from the connection +// Default value is 60 * time.Second +OptionWebsocketPongTimeout(val time.Duration) + +// OptionWebsocketPingPeriod send ping messages to the connection with this period. Must be less than PongTimeout +// Default value is (PongTimeout * 9) / 10 +OptionWebsocketPingPeriod(val time.Duration) + +// OptionWebsocketMaxMessageSize max message size allowed from connection +// Default value is 1024 +OptionWebsocketMaxMessageSize(val int64) + +// OptionWebsocketBinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text +// see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more +// defaults to false +OptionWebsocketBinaryMessages(val bool) + +// OptionWebsocketEndpoint is the path which the websocket server will listen for clients/connections +// Default value is empty string, if you don't set it the Websocket server is disabled. +OptionWebsocketEndpoint(val string) + +// OptionWebsocketReadBufferSize is the buffer size for the underline reader +OptionWebsocketReadBufferSize(val int) + +// OptionWebsocketWriteBufferSize is the buffer size for the underline writer +OptionWebsocketWriteBufferSize(val int) + +// OptionTesterListeningAddr is the virtual server's listening addr (host) +// Default is "iris-go.com:1993" +OptionTesterListeningAddr(val string) + +// OptionTesterExplicitURL If true then the url (should) be prepended manually, useful when want to test subdomains +// Default is false +OptionTesterExplicitURL(val bool) + +// OptionTesterDebug if true then debug messages from the httpexpect will be shown when a test runs +// Default is false +OptionTesterDebug(val bool) + + +``` + +Now, some of you maybe use more than one server inside their iris instance/app, so you used the `iris.AddServer(config.Server{})`, which now becomes `iris.AddServer(iris.ServerConfiguration{})`, ServerConfiguration has also (optional) options to pass there and to `iris.ListenTo(OptionServerListeningAddr("mydomain.com"))`: + + +```go +// examples: +iris.AddServer(iris.OptionServerCertFile("file.cert"),iris.OptionServerKeyFile("file.key")) +iris.ListenTo(iris.OptionServerReadBufferSize(42000)) + +// or, old way but still valid: +iris.AddServer(iris.ServerConfiguration{ListeningAddr: "mydomain.com", CertFile: "file.cert", KeyFile: "file.key"}) +iris.ListenTo(iris.ServerConfiguration{ReadBufferSize:42000, ListeningAddr: "mydomain.com"}) +``` + +**List** of all Server's options: + +```go +OptionServerListeningAddr(val string) + +OptionServerCertFile(val string) + +OptionServerKeyFile(val string) + +// AutoTLS enable to get certifications from the Letsencrypt +// when this configuration field is true, the CertFile & KeyFile are empty, no need to provide a key. +// +// example: https://github.com/iris-contrib/examples/blob/master/letsencyrpt/main.go +OptionServerAutoTLS(val bool) + +// Mode this is for unix only +OptionServerMode(val os.FileMode) +// OptionServerMaxRequestBodySize Maximum request body size. +// +// The server rejects requests with bodies exceeding this limit. +// +// By default request body size is 8MB. +OptionServerMaxRequestBodySize(val int) + +// Per-connection buffer size for requests' reading. +// This also limits the maximum header size. +// +// Increase this buffer if your clients send multi-KB RequestURIs +// and/or multi-KB headers (for example, BIG cookies). +// +// Default buffer size is used if not set. +OptionServerReadBufferSize(val int) + +// Per-connection buffer size for responses' writing. +// +// Default buffer size is used if not set. +OptionServerWriteBufferSize(val int) + +// Maximum duration for reading the full request (including body). +// +// This also limits the maximum duration for idle keep-alive +// connections. +// +// By default request read timeout is unlimited. +OptionServerReadTimeout(val time.Duration) + +// Maximum duration for writing the full response (including body). +// +// By default response write timeout is unlimited. +OptionServerWriteTimeout(val time.Duration) + +// RedirectTo, defaults to empty, set it in order to override the station's handler and redirect all requests to this address which is of form(HOST:PORT or :PORT) +// +// NOTE: the http status is 'StatusMovedPermanently', means one-time-redirect(the browser remembers the new addr and goes to the new address without need to request something from this server +// which means that if you want to change this address you have to clear your browser's cache in order this to be able to change to the new addr. +// +// example: https://github.com/iris-contrib/examples/tree/master/multiserver_listening2 +OptionServerRedirectTo(val string) + +// OptionServerVirtual If this server is not really listens to a real host, it mostly used in order to achieve testing without system modifications +OptionServerVirtual(val bool) + +// OptionServerVListeningAddr, can be used for both virtual = true or false, +// if it's setted to not empty, then the server's Host() will return this addr instead of the ListeningAddr. +// server's Host() is used inside global template helper funcs +// set it when you are sure you know what it does. +// +// Default is empty "" +OptionServerVListeningAddr(val string) + +// OptionServerVScheme if setted to not empty value then all template's helper funcs prepends that as the url scheme instead of the real scheme +// server's .Scheme returns VScheme if not empty && differs from real scheme +// +// Default is empty "" +OptionServerVScheme(val string) + +// OptionServerName the server's name, defaults to "iris". +// You're free to change it, but I will trust you to don't, this is the only setting whose somebody, like me, can see if iris web framework is used +OptionServerName(val string) + +``` + +View all configuration fields and options by navigating to the [kataras/iris/configuration.go source file](https://github.com/kataras/iris/blob/master/configuration.go) + +[Book](https://kataras.gitbooks.io/iris/content/configuration.html) & [Examples](https://github.com/iris-contrib/examples) are updated (website docs will be updated soon). ## 4.1.6 -> 4.1.7 diff --git a/README.md b/README.md index 9257263d..6d33e3e1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@
-Releases +Releases Examples @@ -178,7 +178,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **v4.1.7** +Current: **v4.2.0** > Iris is an active project @@ -191,7 +191,7 @@ Read more about Semantic Versioning 2.0.0 Todo ------------ - [x] Use of the standard `log.Logger` instead of the `iris-contrib/logger`(colorful logger), make these changes to all middleware, examples and plugins. -- [ ] Implement, even, a better way to manage configuration/options, devs will be able to set their own custom options inside there. ` I'm thinking of something the last days, but it will have breaking changes. ` +- [x] Implement, even, a better way to manage configuration/options, devs will be able to set their own custom options inside there. ` I'm thinking of something the last days, but it will have breaking changes. ` - [ ] Implement an internal updater, as requested [here](https://github.com/kataras/iris/issues/401). Iris is a **Community-Driven** Project, waiting for your suggestions and [feature requests](https://github.com/kataras/iris/issues?utf8=%E2%9C%93&q=label%3A%22feature%20request%22)! @@ -221,7 +221,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.1.7-blue.svg?style=flat-square +[Release Widget]: https://img.shields.io/badge/release-v4.2.0-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 deleted file mode 100644 index 78168368..00000000 --- a/config/config.go +++ /dev/null @@ -1,18 +0,0 @@ -// Package config defines the default settings and semantic variables -package config - -import ( - "time" -) - -var ( - // TimeFormat default time format for any kind of datetime parsing - TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" - // StaticCacheDuration expiration duration for INACTIVE file handlers - StaticCacheDuration = 20 * time.Second - // CompressedFileSuffix is the suffix to add to the name of - // cached compressed file when using the .StaticFS function. - // - // Defaults to iris-fasthttp.gz - CompressedFileSuffix = "iris-fasthttp.gz" -) diff --git a/config/iris.go b/config/iris.go deleted file mode 100644 index 0de92c74..00000000 --- a/config/iris.go +++ /dev/null @@ -1,163 +0,0 @@ -package config - -import ( - "io" - "os" - - "github.com/imdario/mergo" -) - -// Default values for base Iris conf -const ( - DefaultDisablePathCorrection = false - DefaultDisablePathEscape = false - DefaultCharset = "UTF-8" - DefaultLoggerPreffix = "[IRIS] " -) - -var ( - DefaultLoggerOut = os.Stdout -) - -type ( - // Iris configs for the station - Iris struct { - - // DisablePathCorrection corrects and redirects the requested path to the registed path - // for example, if /home/ path is requested but no handler for this Route found, - // then the Router checks if /home handler exists, if yes, - // (permant)redirects the client to the correct path /home - // - // Default is false - DisablePathCorrection bool - - // DisablePathEscape when is false then its escapes the path, the named parameters (if any). - // Change to true it if you want something like this https://github.com/kataras/iris/issues/135 to work - // - // When do you need to Disable(true) it: - // accepts parameters with slash '/' - // Request: http://localhost:8080/details/Project%2FDelta - // ctx.Param("project") returns the raw named parameter: Project%2FDelta - // which you can escape it manually with net/url: - // projectName, _ := url.QueryUnescape(c.Param("project"). - // Look here: https://github.com/kataras/iris/issues/135 for more - // - // Default is false - DisablePathEscape bool - - // DisableBanner outputs the iris banner at startup - // - // Default is false - DisableBanner bool - - // LoggerOut is the destination for output - // - // defaults to os.Stdout - LoggerOut io.Writer - // LoggerOut is the logger's prefix to write at beginning of each line - // - // Defaults to [IRIS] - LoggerPreffix string - - // ProfilePath a the route path, set it to enable http pprof tool - // Default is empty, if you set it to a $path, these routes will handled: - // $path/cmdline - // $path/profile - // $path/symbol - // $path/goroutine - // $path/heap - // $path/threadcreate - // $path/pprof/block - // for example if '/debug/pprof' - // http://yourdomain:PORT/debug/pprof/ - // http://yourdomain:PORT/debug/pprof/cmdline - // http://yourdomain:PORT/debug/pprof/profile - // http://yourdomain:PORT/debug/pprof/symbol - // http://yourdomain:PORT/debug/pprof/goroutine - // http://yourdomain:PORT/debug/pprof/heap - // http://yourdomain:PORT/debug/pprof/threadcreate - // http://yourdomain:PORT/debug/pprof/pprof/block - // it can be a subdomain also, for example, if 'debug.' - // http://debug.yourdomain:PORT/ - // http://debug.yourdomain:PORT/cmdline - // http://debug.yourdomain:PORT/profile - // http://debug.yourdomain:PORT/symbol - // http://debug.yourdomain:PORT/goroutine - // http://debug.yourdomain:PORT/heap - // http://debug.yourdomain:PORT/threadcreate - // http://debug.yourdomain:PORT/pprof/block - ProfilePath string - // DisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine - // default is false - DisableTemplateEngines bool - // IsDevelopment iris will act like a developer, for example - // If true then re-builds the templates on each request - // 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 - - // Gzip enables gzip compression on your Render actions, this includes any type of render, templates and pure/raw content - // If you don't want to enable it globaly, you could just use the third parameter on context.Render("myfileOrResponse", structBinding{}, iris.RenderOptions{"gzip": true}) - // defaults to false - Gzip bool - - // Sessions contains the configs for sessions - Sessions Sessions - - // Websocket contains the configs for Websocket's server integration - Websocket *Websocket - - // Tester contains the configs for the test framework, so far we have only one because all test framework's configs are setted by the iris itself - // You can find example on the https://github.com/kataras/iris/glob/master/context_test.go - Tester Tester - } -) - -// Default returns the default configuration for the Iris staton -func Default() Iris { - return Iris{ - DisablePathCorrection: DefaultDisablePathCorrection, - DisablePathEscape: DefaultDisablePathEscape, - DisableBanner: false, - LoggerOut: DefaultLoggerOut, - LoggerPreffix: DefaultLoggerPreffix, - DisableTemplateEngines: false, - IsDevelopment: false, - Charset: DefaultCharset, - Gzip: false, - ProfilePath: "", - Sessions: DefaultSessions(), - Websocket: DefaultWebsocket(), - Tester: DefaultTester(), - } -} - -// Merge merges the default with the given config and returns the result -// receives an array because the func caller is variadic -func (c Iris) Merge(cfg []Iris) (config Iris) { - // I tried to make it more generic with interfaces for all configs, inside config.go but it fails, - // so do it foreach configuration np they aint so much... - - if cfg != nil && len(cfg) > 0 { - config = cfg[0] - mergo.Merge(&config, c) - } else { - _default := c - config = _default - } - - return -} - -// MergeSingle merges the default with the given config and returns the result -func (c Iris) MergeSingle(cfg Iris) (config Iris) { - - config = cfg - mergo.Merge(&config, c) - - return -} diff --git a/config/server.go b/config/server.go deleted file mode 100644 index ba103cf6..00000000 --- a/config/server.go +++ /dev/null @@ -1,185 +0,0 @@ -package config - -import ( - "os" - "strconv" - "strings" - "time" - - "github.com/imdario/mergo" - "github.com/valyala/fasthttp" -) - -// Default values for base Server conf -const ( - // DefaultServerHostname returns the default hostname which is 0.0.0.0 - DefaultServerHostname = "0.0.0.0" - // DefaultServerPort returns the default port which is 8080 - DefaultServerPort = 8080 - // DefaultMaxRequestBodySize is 8MB - DefaultMaxRequestBodySize = 2 * fasthttp.DefaultMaxRequestBodySize - - // Per-connection buffer size for requests' reading. - // This also limits the maximum header size. - // - // Increase this buffer if your clients send multi-KB RequestURIs - // and/or multi-KB headers (for example, BIG cookies). - // - // Default buffer size is 8MB - DefaultReadBufferSize = 8096 - - // Per-connection buffer size for responses' writing. - // - // Default buffer size is 8MB - DefaultWriteBufferSize = 8096 - - // DefaultServerName the response header of the 'Server' value when writes to the client - DefaultServerName = "iris" -) - -var ( - // DefaultServerAddr the default server addr which is: 0.0.0.0:8080 - DefaultServerAddr = DefaultServerHostname + ":" + strconv.Itoa(DefaultServerPort) -) - -// Server used inside server for listening -type Server struct { - // ListenningAddr the addr that server listens to - ListeningAddr string - CertFile string - KeyFile string - // AutoTLS enable to get certifications from the Letsencrypt - // when this configuration field is true, the CertFile & KeyFile are empty, no need to provide a key. - // - // example: https://github.com/iris-contrib/examples/blob/master/letsencyrpt/main.go - AutoTLS bool - // Mode this is for unix only - Mode os.FileMode - // MaxRequestBodySize Maximum request body size. - // - // The server rejects requests with bodies exceeding this limit. - // - // By default request body size is 8MB. - MaxRequestBodySize int - - // Per-connection buffer size for requests' reading. - // This also limits the maximum header size. - // - // Increase this buffer if your clients send multi-KB RequestURIs - // and/or multi-KB headers (for example, BIG cookies). - // - // Default buffer size is used if not set. - ReadBufferSize int - - // Per-connection buffer size for responses' writing. - // - // Default buffer size is used if not set. - WriteBufferSize int - - // Maximum duration for reading the full request (including body). - // - // This also limits the maximum duration for idle keep-alive - // connections. - // - // By default request read timeout is unlimited. - ReadTimeout time.Duration - - // Maximum duration for writing the full response (including body). - // - // By default response write timeout is unlimited. - WriteTimeout time.Duration - - // RedirectTo, defaults to empty, set it in order to override the station's handler and redirect all requests to this address which is of form(HOST:PORT or :PORT) - // - // NOTE: the http status is 'StatusMovedPermanently', means one-time-redirect(the browser remembers the new addr and goes to the new address without need to request something from this server - // which means that if you want to change this address you have to clear your browser's cache in order this to be able to change to the new addr. - // - // example: https://github.com/iris-contrib/examples/tree/master/multiserver_listening2 - RedirectTo string - // Virtual If this server is not really listens to a real host, it mostly used in order to achieve testing without system modifications - Virtual bool - // VListeningAddr, can be used for both virtual = true or false, - // if it's setted to not empty, then the server's Host() will return this addr instead of the ListeningAddr. - // server's Host() is used inside global template helper funcs - // set it when you are sure you know what it does. - // - // Default is empty "" - VListeningAddr string - // VScheme if setted to not empty value then all template's helper funcs prepends that as the url scheme instead of the real scheme - // server's .Scheme returns VScheme if not empty && differs from real scheme - // - // Default is empty "" - VScheme string - // Name the server's name, defaults to "iris". - // You're free to change it, but I will trust you to don't, this is the only setting whose somebody, like me, can see if iris web framework is used - Name string -} - -// ServerParseAddr parses the listening addr and returns this -func ServerParseAddr(listeningAddr string) string { - // check if addr has :port, if not do it +:80 ,we need the hostname for many cases - a := listeningAddr - if a == "" { - // check for os environments - if oshost := os.Getenv("HOST"); oshost != "" { - a = oshost - } else if oshost := os.Getenv("ADDR"); oshost != "" { - a = oshost - } else if osport := os.Getenv("PORT"); osport != "" { - a = ":" + osport - } - - if a == "" { - a = DefaultServerAddr - } - - } - if portIdx := strings.IndexByte(a, ':'); portIdx == 0 { - // if contains only :port ,then the : is the first letter, so we dont have setted a hostname, lets set it - a = DefaultServerHostname + a - } - if portIdx := strings.IndexByte(a, ':'); portIdx < 0 { - // missing port part, add it - a = a + ":80" - } - - return a -} - -// DefaultServer returns the default configs for the server -func DefaultServer() Server { - return Server{ - ListeningAddr: DefaultServerAddr, - Name: DefaultServerName, - MaxRequestBodySize: DefaultMaxRequestBodySize, - ReadBufferSize: DefaultReadBufferSize, - WriteBufferSize: DefaultWriteBufferSize, - RedirectTo: "", - Virtual: false, - VListeningAddr: "", - VScheme: "", - } -} - -// Merge merges the default with the given config and returns the result -func (c Server) Merge(cfg []Server) (config Server) { - - if len(cfg) > 0 { - config = cfg[0] - mergo.Merge(&config, c) - } else { - _default := c - config = _default - } - - return -} - -// MergeSingle merges the default with the given config and returns the result -func (c Server) MergeSingle(cfg Server) (config Server) { - - config = cfg - mergo.Merge(&config, c) - - return -} diff --git a/config/sessions.go b/config/sessions.go deleted file mode 100644 index 75ee0792..00000000 --- a/config/sessions.go +++ /dev/null @@ -1,68 +0,0 @@ -package config - -import ( - "github.com/imdario/mergo" - "github.com/kataras/go-sessions" - "time" -) - -var ( - universe time.Time // 0001-01-01 00:00:00 +0000 UTC - // CookieExpireNever the default cookie's life for sessions, unlimited (23 years) - CookieExpireNever = time.Now().AddDate(23, 0, 0) -) - -const ( - // DefaultCookieName the secret cookie's name for sessions - DefaultCookieName = "irissessionid" - // DefaultSessionGcDuration is the default Session Manager's GCDuration , which is 2 hours - DefaultSessionGcDuration = time.Duration(2) * time.Hour - // DefaultCookieLength is the default Session Manager's CookieLength, which is 32 - DefaultCookieLength = 32 -) - -// Sessions the configuration for sessions -// has 6 fields -// first is the cookieName, the session's name (string) ["mysessionsecretcookieid"] -// second enable if you want to decode the cookie's key also -// third is the time which the client's cookie expires -// forth is the cookie length (sessionid) int, defaults to 32, do not change if you don't have any reason to do -// fifth is the gcDuration (time.Duration) when this time passes it removes the unused sessions from the memory until the user come back -// sixth is the DisableSubdomainPersistence which you can set it to true in order dissallow your q subdomains to have access to the session cook -// -type Sessions sessions.Config - -// DefaultSessions the default configs for Sessions -func DefaultSessions() Sessions { - return Sessions{ - Cookie: DefaultCookieName, - CookieLength: DefaultCookieLength, - DecodeCookie: false, - Expires: 0, - GcDuration: DefaultSessionGcDuration, - DisableSubdomainPersistence: false, - } -} - -// Merge merges the default with the given config and returns the result -func (c Sessions) Merge(cfg []Sessions) (config Sessions) { - - if len(cfg) > 0 { - config = cfg[0] - mergo.Merge(&config, c) - } else { - _default := c - config = _default - } - - return -} - -// MergeSingle merges the default with the given config and returns the result -func (c Sessions) MergeSingle(cfg Sessions) (config Sessions) { - - config = cfg - mergo.Merge(&config, c) - - return -} diff --git a/config/tester.go b/config/tester.go deleted file mode 100644 index c933a366..00000000 --- a/config/tester.go +++ /dev/null @@ -1,14 +0,0 @@ -package config - -// Tester configuration -type Tester struct { - ListeningAddr string - ExplicitURL bool - Debug bool -} - -// DefaultTester returns the default configuration for a tester -// the ListeningAddr is used as virtual only when no running server is founded -func DefaultTester() Tester { - return Tester{ListeningAddr: "iris-go.com:1993", ExplicitURL: false, Debug: false} -} diff --git a/config/websocket.go b/config/websocket.go deleted file mode 100644 index 3c4fe3fe..00000000 --- a/config/websocket.go +++ /dev/null @@ -1,88 +0,0 @@ -package config - -import ( - "time" - - "github.com/imdario/mergo" -) - -const ( - // DefaultWriteTimeout 15 * time.Second - DefaultWriteTimeout = 15 * time.Second - // DefaultPongTimeout 60 * time.Second - DefaultPongTimeout = 60 * time.Second - // DefaultPingPeriod (DefaultPongTimeout * 9) / 10 - DefaultPingPeriod = (DefaultPongTimeout * 9) / 10 - // DefaultMaxMessageSize 1024 - DefaultMaxMessageSize = 1024 -) - -// - -// Websocket the config contains options for the ../websocket.go -type Websocket struct { - // WriteTimeout time allowed to write a message to the connection. - // Default value is 15 * time.Second - WriteTimeout time.Duration - // PongTimeout allowed to read the next pong message from the connection - // Default value is 60 * time.Second - PongTimeout time.Duration - // PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout - // Default value is (PongTimeout * 9) / 10 - PingPeriod time.Duration - // MaxMessageSize max message size allowed from connection - // Default value is 1024 - MaxMessageSize int64 - // BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text - // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more - // defaults to false - BinaryMessages bool - // Endpoint is the path which the websocket server will listen for clients/connections - // Default value is empty string, if you don't set it the Websocket server is disabled. - Endpoint string - // Headers the response headers before upgrader - // Default is empty - Headers map[string]string - // ReadBufferSize is the buffer size for the underline reader - ReadBufferSize int - // WriteBufferSize is the buffer size for the underline writer - WriteBufferSize int -} - -// DefaultWebsocket returns the default config for iris-ws websocket package -func DefaultWebsocket() *Websocket { - return &Websocket{ - WriteTimeout: DefaultWriteTimeout, - PongTimeout: DefaultPongTimeout, - PingPeriod: DefaultPingPeriod, - MaxMessageSize: DefaultMaxMessageSize, - BinaryMessages: false, - ReadBufferSize: 4096, - WriteBufferSize: 4096, - Headers: make(map[string]string, 0), - Endpoint: "", - } -} - -// Merge merges the default with the given config and returns the result -func (c *Websocket) Merge(cfg []*Websocket) (config *Websocket) { - - if len(cfg) > 0 { - config = cfg[0] - mergo.Merge(config, c) - } else { - _default := c - config = _default - } - - return -} - -// MergeSingle merges the default with the given config and returns the result -func (c *Websocket) MergeSingle(cfg *Websocket) (config *Websocket) { - - config = cfg - mergo.Merge(config, c) - - return -} diff --git a/configuration.go b/configuration.go new file mode 100644 index 00000000..80556ac9 --- /dev/null +++ b/configuration.go @@ -0,0 +1,859 @@ +package iris + +import ( + "github.com/imdario/mergo" + "github.com/kataras/go-options" + "github.com/kataras/go-sessions" + "github.com/valyala/fasthttp" + "io" + "os" + "strconv" + "strings" + "time" +) + +type ( + // OptionSetter sets a configuration field to the main configuration + // used to help developers to write less and configure only what they really want and nothing else + // example: + // iris.New(iris.Configuration{Sessions:iris.SessionConfiguration{Cookie:"mysessionid"}, Websocket: iris.WebsocketConfiguration{Endpoint:"/my_endpoint"}}) + // now can be done also by using iris.Option$FIELD: + // iris.New(irisOptionSessionsCookie("mycookieid"),iris.OptionWebsocketEndpoint("my_endpoint")) + // benefits: + // 1. user/dev have no worries what option to pass, he/she can just press iris.Option and all options should be shown to her/his editor's autocomplete-popup window + // 2. can be passed with any order + // 3. Can override previous configuration + OptionSetter interface { + // Set receives a pointer to the global Configuration type and does the job of filling it + Set(c *Configuration) + } + // OptionSet implements the OptionSetter + OptionSet func(c *Configuration) +) + +// Set is the func which makes the OptionSet an OptionSetter, this is used mostly +func (o OptionSet) Set(c *Configuration) { + o(c) +} + +// Configuration the whole configuration for an iris instance ($instance.Config) or global iris instance (iris.Config) +// these can be passed via options also, look at the top of this file(configuration.go) +// +// Configuration is also implements the OptionSet so it's a valid option itself, this is briliant enough +type Configuration struct { + // DisablePathCorrection corrects and redirects the requested path to the registed path + // for example, if /home/ path is requested but no handler for this Route found, + // then the Router checks if /home handler exists, if yes, + // (permant)redirects the client to the correct path /home + // + // Default is false + DisablePathCorrection bool + + // DisablePathEscape when is false then its escapes the path, the named parameters (if any). + // Change to true it if you want something like this https://github.com/kataras/iris/issues/135 to work + // + // When do you need to Disable(true) it: + // accepts parameters with slash '/' + // Request: http://localhost:8080/details/Project%2FDelta + // ctx.Param("project") returns the raw named parameter: Project%2FDelta + // which you can escape it manually with net/url: + // projectName, _ := url.QueryUnescape(c.Param("project"). + // Look here: https://github.com/kataras/iris/issues/135 for more + // + // Default is false + DisablePathEscape bool + + // DisableBanner outputs the iris banner at startup + // + // Default is false + DisableBanner bool + + // LoggerOut is the destination for output + // + // Default is os.Stdout + LoggerOut io.Writer + // LoggerPreffix is the logger's prefix to write at beginning of each line + // + // Default is [IRIS] + LoggerPreffix string + + // ProfilePath a the route path, set it to enable http pprof tool + // Default is empty, if you set it to a $path, these routes will handled: + // $path/cmdline + // $path/profile + // $path/symbol + // $path/goroutine + // $path/heap + // $path/threadcreate + // $path/pprof/block + // for example if '/debug/pprof' + // http://yourdomain:PORT/debug/pprof/ + // http://yourdomain:PORT/debug/pprof/cmdline + // http://yourdomain:PORT/debug/pprof/profile + // http://yourdomain:PORT/debug/pprof/symbol + // http://yourdomain:PORT/debug/pprof/goroutine + // http://yourdomain:PORT/debug/pprof/heap + // http://yourdomain:PORT/debug/pprof/threadcreate + // http://yourdomain:PORT/debug/pprof/pprof/block + // it can be a subdomain also, for example, if 'debug.' + // http://debug.yourdomain:PORT/ + // http://debug.yourdomain:PORT/cmdline + // http://debug.yourdomain:PORT/profile + // http://debug.yourdomain:PORT/symbol + // http://debug.yourdomain:PORT/goroutine + // http://debug.yourdomain:PORT/heap + // http://debug.yourdomain:PORT/threadcreate + // http://debug.yourdomain:PORT/pprof/block + ProfilePath string + + // DisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine + // default is false + DisableTemplateEngines bool + + // IsDevelopment iris will act like a developer, for example + // If true then re-builds the templates on each request + // default is false + IsDevelopment bool + + // TimeFormat time format for any kind of datetime parsing + TimeFormat string + + // Charset character encoding for various rendering + // used for templates and the rest of the responses + // defaults to "UTF-8" + Charset string + + // Gzip enables gzip compression on your Render actions, this includes any type of render, templates and pure/raw content + // If you don't want to enable it globaly, you could just use the third parameter on context.Render("myfileOrResponse", structBinding{}, iris.RenderOptions{"gzip": true}) + // defaults to false + Gzip bool + + // Sessions contains the configs for sessions + Sessions SessionsConfiguration + + // Websocket contains the configs for Websocket's server integration + Websocket WebsocketConfiguration + + // Tester contains the configs for the test framework, so far we have only one because all test framework's configs are setted by the iris itself + // You can find example on the https://github.com/kataras/iris/glob/master/context_test.go + Tester TesterConfiguration + + // Other are the custom, dynamic options, can be empty + // this fill used only by you to set any app's options you want + // for each of an Iris instance + Other options.Options +} + +// Set implements the OptionSetter +func (c Configuration) Set(main *Configuration) { + mergo.MergeWithOverwrite(main, c) +} + +// All options starts with "Option" preffix in order to be easier to find what dev searching for +var ( + // OptionDisablePathCorrection corrects and redirects the requested path to the registed path + // for example, if /home/ path is requested but no handler for this Route found, + // then the Router checks if /home handler exists, if yes, + // (permant)redirects the client to the correct path /home + // + // Default is false + OptionDisablePathCorrection = func(val bool) OptionSet { + return func(c *Configuration) { + c.DisablePathCorrection = val + } + + } + // OptionDisablePathEscape when is false then its escapes the path, the named parameters (if any). + OptionDisablePathEscape = func(val bool) OptionSet { + return func(c *Configuration) { + c.DisablePathEscape = val + } + } + // OptionDisableBanner outputs the iris banner at startup + // + // Default is false + OptionDisableBanner = func(val bool) OptionSet { + return func(c *Configuration) { + c.DisableBanner = val + } + } + // OptionLoggerOut is the destination for output + // + // Default is os.Stdout + OptionLoggerOut = func(val io.Writer) OptionSet { + return func(c *Configuration) { + c.LoggerOut = val + } + } + // OptionLoggerPreffix is the logger's prefix to write at beginning of each line + // + // Default is [IRIS] + OptionLoggerPreffix = func(val string) OptionSet { + return func(c *Configuration) { + c.LoggerPreffix = val + } + } + // OptionProfilePath a the route path, set it to enable http pprof tool + // Default is empty, if you set it to a $path, these routes will handled: + OptionProfilePath = func(val string) OptionSet { + return func(c *Configuration) { + c.ProfilePath = val + } + } + // OptionDisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine + // Default is false + OptionDisableTemplateEngines = func(val bool) OptionSet { + return func(c *Configuration) { + c.DisableTemplateEngines = val + } + } + // OptionIsDevelopment iris will act like a developer, for example + // If true then re-builds the templates on each request + // Default is false + OptionIsDevelopment = func(val bool) OptionSet { + return func(c *Configuration) { + c.IsDevelopment = val + } + } + // OptionTimeFormat time format for any kind of datetime parsing + OptionTimeFormat = func(val string) OptionSet { + return func(c *Configuration) { + c.TimeFormat = val + } + } + // OptionCharset character encoding for various rendering + // used for templates and the rest of the responses + // Default is "UTF-8" + OptionCharset = func(val string) OptionSet { + return func(c *Configuration) { + c.Charset = val + } + } + // OptionGzip enables gzip compression on your Render actions, this includes any type of render, templates and pure/raw content + // If you don't want to enable it globaly, you could just use the third parameter on context.Render("myfileOrResponse", structBinding{}, iris.RenderOptions{"gzip": true}) + // Default is false + OptionGzip = func(val bool) OptionSet { + return func(c *Configuration) { + c.Gzip = val + } + } + + // OptionOther are the custom, dynamic options, can be empty + // this fill used only by you to set any app's options you want + // for each of an Iris instance + OptionOther = func(val ...options.Options) OptionSet { + opts := options.Options{} + for _, opt := range val { + for k, v := range opt { + opts[k] = v + } + } + return func(c *Configuration) { + c.Other = opts + } + } +) + +var ( + // DefaultTimeFormat default time format for any kind of datetime parsing + DefaultTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" + // StaticCacheDuration expiration duration for INACTIVE file handlers, it's a global configuration field to all iris instances + StaticCacheDuration = 20 * time.Second + // CompressedFileSuffix is the suffix to add to the name of + // cached compressed file when using the .StaticFS function. + // + // Defaults to iris-fasthttp.gz + CompressedFileSuffix = "iris-fasthttp.gz" +) + +// Default values for base Iris conf +const ( + DefaultDisablePathCorrection = false + DefaultDisablePathEscape = false + DefaultCharset = "UTF-8" + DefaultLoggerPreffix = "[IRIS] " +) + +var ( + // DefaultLoggerOut is the default logger's output + DefaultLoggerOut = os.Stdout +) + +// DefaultConfiguration returns the default configuration for an Iris station, fills the main Configuration +func DefaultConfiguration() Configuration { + return Configuration{ + DisablePathCorrection: DefaultDisablePathCorrection, + DisablePathEscape: DefaultDisablePathEscape, + DisableBanner: false, + LoggerOut: DefaultLoggerOut, + LoggerPreffix: DefaultLoggerPreffix, + DisableTemplateEngines: false, + IsDevelopment: false, + TimeFormat: DefaultTimeFormat, + Charset: DefaultCharset, + Gzip: false, + ProfilePath: "", + Sessions: DefaultSessionsConfiguration(), + Websocket: DefaultWebsocketConfiguration(), + Tester: DefaultTesterConfiguration(), + Other: options.Options{}, + } +} + +// SessionsConfiguration the configuration for sessions +// has 6 fields +// first is the cookieName, the session's name (string) ["mysessionsecretcookieid"] +// second enable if you want to decode the cookie's key also +// third is the time which the client's cookie expires +// forth is the cookie length (sessionid) int, defaults to 32, do not change if you don't have any reason to do +// fifth is the gcDuration (time.Duration) when this time passes it removes the unused sessions from the memory until the user come back +// sixth is the DisableSubdomainPersistence which you can set it to true in order dissallow your q subdomains to have access to the session cook +type SessionsConfiguration sessions.Config + +var ( + // OptionSessionsCookie string, the session's client cookie name, for example: "qsessionid" + OptionSessionsCookie = func(val string) OptionSet { + return func(c *Configuration) { + c.Sessions.Cookie = val + } + } + + // OptionSessionsDecodeCookie set it to true to decode the cookie key with base64 URLEncoding + // Defaults to false + OptionSessionsDecodeCookie = func(val bool) OptionSet { + return func(c *Configuration) { + c.Sessions.DecodeCookie = val + } + } + + // OptionSessionsExpires the duration of which the cookie must expires (created_time.Add(Expires)). + // If you want to delete the cookie when the browser closes, set it to -1 but in this case, the server side's session duration is up to GcDuration + // + // Default infinitive/unlimited life duration(0) + OptionSessionsExpires = func(val time.Duration) OptionSet { + return func(c *Configuration) { + c.Sessions.Expires = val + } + } + + // OptionSessionsCookieLength the length of the sessionid's cookie's value, let it to 0 if you don't want to change it + // Defaults to 32 + OptionSessionsCookieLength = func(val int) OptionSet { + return func(c *Configuration) { + c.Sessions.CookieLength = val + } + } + + // OptionSessionsGcDuration every how much duration(GcDuration) the memory should be clear for unused cookies (GcDuration) + // for example: time.Duration(2)*time.Hour. it will check every 2 hours if cookie hasn't be used for 2 hours, + // deletes it from backend memory until the user comes back, then the session continue to work as it was + // + // Default 2 hours + OptionSessionsGcDuration = func(val time.Duration) OptionSet { + return func(c *Configuration) { + c.Sessions.GcDuration = val + } + } + + // OptionSessionsDisableSubdomainPersistence set it to true in order dissallow your q subdomains to have access to the session cookie + // defaults to false + OptionSessionsDisableSubdomainPersistence = func(val bool) OptionSet { + return func(c *Configuration) { + c.Sessions.DisableSubdomainPersistence = val + } + } +) + +var ( + universe time.Time // 0001-01-01 00:00:00 +0000 UTC + // CookieExpireNever the default cookie's life for sessions, unlimited (23 years) + CookieExpireNever = time.Now().AddDate(23, 0, 0) +) + +const ( + // DefaultCookieName the secret cookie's name for sessions + DefaultCookieName = "irissessionid" + // DefaultSessionGcDuration is the default Session Manager's GCDuration , which is 2 hours + DefaultSessionGcDuration = time.Duration(2) * time.Hour + // DefaultCookieLength is the default Session Manager's CookieLength, which is 32 + DefaultCookieLength = 32 +) + +// DefaultSessionsConfiguration the default configs for Sessions +func DefaultSessionsConfiguration() SessionsConfiguration { + return SessionsConfiguration{ + Cookie: DefaultCookieName, + CookieLength: DefaultCookieLength, + DecodeCookie: false, + Expires: 0, + GcDuration: DefaultSessionGcDuration, + DisableSubdomainPersistence: false, + } +} + +// WebsocketConfiguration the config contains options for the Websocket main config field +type WebsocketConfiguration struct { + // WriteTimeout time allowed to write a message to the connection. + // Default value is 15 * time.Second + WriteTimeout time.Duration + // PongTimeout allowed to read the next pong message from the connection + // Default value is 60 * time.Second + PongTimeout time.Duration + // PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout + // Default value is (PongTimeout * 9) / 10 + PingPeriod time.Duration + // MaxMessageSize max message size allowed from connection + // Default value is 1024 + MaxMessageSize int64 + // BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text + // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more + // defaults to false + BinaryMessages bool + // Endpoint is the path which the websocket server will listen for clients/connections + // Default value is empty string, if you don't set it the Websocket server is disabled. + Endpoint string + // ReadBufferSize is the buffer size for the underline reader + ReadBufferSize int + // WriteBufferSize is the buffer size for the underline writer + WriteBufferSize int +} + +var ( + // OptionWebsocketWriteTimeout time allowed to write a message to the connection. + // Default value is 15 * time.Second + OptionWebsocketWriteTimeout = func(val time.Duration) OptionSet { + return func(c *Configuration) { + c.Websocket.WriteTimeout = val + } + } + // OptionWebsocketPongTimeout allowed to read the next pong message from the connection + // Default value is 60 * time.Second + OptionWebsocketPongTimeout = func(val time.Duration) OptionSet { + return func(c *Configuration) { + c.Websocket.PongTimeout = val + } + } + // OptionWebsocketPingPeriod send ping messages to the connection with this period. Must be less than PongTimeout + // Default value is (PongTimeout * 9) / 10 + OptionWebsocketPingPeriod = func(val time.Duration) OptionSet { + return func(c *Configuration) { + c.Websocket.PingPeriod = val + } + } + // OptionWebsocketMaxMessageSize max message size allowed from connection + // Default value is 1024 + OptionWebsocketMaxMessageSize = func(val int64) OptionSet { + return func(c *Configuration) { + c.Websocket.MaxMessageSize = val + } + } + // OptionWebsocketBinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text + // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more + // defaults to false + OptionWebsocketBinaryMessages = func(val bool) OptionSet { + return func(c *Configuration) { + c.Websocket.BinaryMessages = val + } + } + // OptionWebsocketEndpoint is the path which the websocket server will listen for clients/connections + // Default value is empty string, if you don't set it the Websocket server is disabled. + OptionWebsocketEndpoint = func(val string) OptionSet { + return func(c *Configuration) { + c.Websocket.Endpoint = val + } + } + // OptionWebsocketReadBufferSize is the buffer size for the underline reader + OptionWebsocketReadBufferSize = func(val int) OptionSet { + return func(c *Configuration) { + c.Websocket.ReadBufferSize = val + } + } + // OptionWebsocketWriteBufferSize is the buffer size for the underline writer + OptionWebsocketWriteBufferSize = func(val int) OptionSet { + return func(c *Configuration) { + c.Websocket.WriteBufferSize = val + } + } +) + +const ( + // DefaultWriteTimeout 15 * time.Second + DefaultWriteTimeout = 15 * time.Second + // DefaultPongTimeout 60 * time.Second + DefaultPongTimeout = 60 * time.Second + // DefaultPingPeriod (DefaultPongTimeout * 9) / 10 + DefaultPingPeriod = (DefaultPongTimeout * 9) / 10 + // DefaultMaxMessageSize 1024 + DefaultMaxMessageSize = 1024 +) + +// DefaultWebsocketConfiguration returns the default config for iris-ws websocket package +func DefaultWebsocketConfiguration() WebsocketConfiguration { + return WebsocketConfiguration{ + WriteTimeout: DefaultWriteTimeout, + PongTimeout: DefaultPongTimeout, + PingPeriod: DefaultPingPeriod, + MaxMessageSize: DefaultMaxMessageSize, + BinaryMessages: false, + ReadBufferSize: 4096, + WriteBufferSize: 4096, + Endpoint: "", + } +} + +// TesterConfiguration configuration used inside main config field 'Tester' +type TesterConfiguration struct { + // ListeningAddr is the virtual server's listening addr (host) + // Default is "iris-go.com:1993" + ListeningAddr string + // ExplicitURL If true then the url (should) be prepended manually, useful when want to test subdomains + // Default is false + ExplicitURL bool + // Debug if true then debug messages from the httpexpect will be shown when a test runs + // Default is false + Debug bool +} + +var ( + // OptionTesterListeningAddr is the virtual server's listening addr (host) + // Default is "iris-go.com:1993" + OptionTesterListeningAddr = func(val string) OptionSet { + return func(c *Configuration) { + c.Tester.ListeningAddr = val + } + } + // OptionTesterExplicitURL If true then the url (should) be prepended manually, useful when want to test subdomains + // Default is false + OptionTesterExplicitURL = func(val bool) OptionSet { + return func(c *Configuration) { + c.Tester.ExplicitURL = val + } + } + // OptionTesterDebug if true then debug messages from the httpexpect will be shown when a test runs + // Default is false + OptionTesterDebug = func(val bool) OptionSet { + return func(c *Configuration) { + c.Tester.Debug = val + } + } +) + +// DefaultTesterConfiguration returns the default configuration for a tester +// the ListeningAddr is used as virtual only when no running server is founded +func DefaultTesterConfiguration() TesterConfiguration { + return TesterConfiguration{ListeningAddr: "iris-go.com:1993", ExplicitURL: false, Debug: false} +} + +// ServerConfiguration is the configuration which is used inside iris' server(s) for listening to +type ServerConfiguration struct { + // ListenningAddr the addr that server listens to + ListeningAddr string + CertFile string + KeyFile string + // AutoTLS enable to get certifications from the Letsencrypt + // when this configuration field is true, the CertFile & KeyFile are empty, no need to provide a key. + // + // example: https://github.com/iris-contrib/examples/blob/master/letsencyrpt/main.go + AutoTLS bool + // Mode this is for unix only + Mode os.FileMode + // MaxRequestBodySize Maximum request body size. + // + // The server rejects requests with bodies exceeding this limit. + // + // By default request body size is 8MB. + MaxRequestBodySize int + + // Per-connection buffer size for requests' reading. + // This also limits the maximum header size. + // + // Increase this buffer if your clients send multi-KB RequestURIs + // and/or multi-KB headers (for example, BIG cookies). + // + // Default buffer size is used if not set. + ReadBufferSize int + + // Per-connection buffer size for responses' writing. + // + // Default buffer size is used if not set. + WriteBufferSize int + + // Maximum duration for reading the full request (including body). + // + // This also limits the maximum duration for idle keep-alive + // connections. + // + // By default request read timeout is unlimited. + ReadTimeout time.Duration + + // Maximum duration for writing the full response (including body). + // + // By default response write timeout is unlimited. + WriteTimeout time.Duration + + // RedirectTo, defaults to empty, set it in order to override the station's handler and redirect all requests to this address which is of form(HOST:PORT or :PORT) + // + // NOTE: the http status is 'StatusMovedPermanently', means one-time-redirect(the browser remembers the new addr and goes to the new address without need to request something from this server + // which means that if you want to change this address you have to clear your browser's cache in order this to be able to change to the new addr. + // + // example: https://github.com/iris-contrib/examples/tree/master/multiserver_listening2 + RedirectTo string + // Virtual If this server is not really listens to a real host, it mostly used in order to achieve testing without system modifications + Virtual bool + // VListeningAddr, can be used for both virtual = true or false, + // if it's setted to not empty, then the server's Host() will return this addr instead of the ListeningAddr. + // server's Host() is used inside global template helper funcs + // set it when you are sure you know what it does. + // + // Default is empty "" + VListeningAddr string + // VScheme if setted to not empty value then all template's helper funcs prepends that as the url scheme instead of the real scheme + // server's .Scheme returns VScheme if not empty && differs from real scheme + // + // Default is empty "" + VScheme string + // Name the server's name, defaults to "iris". + // You're free to change it, but I will trust you to don't, this is the only setting whose somebody, like me, can see if iris web framework is used + Name string +} + +// note: ServerConfiguration is the only one config which has its own option setter because +// it's independent from a specific iris instance: +// same server can run on multi iris instance +// one iris instance/station can have and listening to more than one server. + +// OptionServerSettter server configuration option setter +type OptionServerSettter interface { + Set(c *ServerConfiguration) +} + +// OptionServerSet is the func which implements the OptionServerSettter, this is used widely +type OptionServerSet func(c *ServerConfiguration) + +// Set is the func which makes OptionServerSet implements the OptionServerSettter +func (o OptionServerSet) Set(c *ServerConfiguration) { + o(c) +} + +// Set implements the OptionServerSettter to the ServerConfiguration +func (c ServerConfiguration) Set(main *ServerConfiguration) { + mergo.MergeWithOverwrite(main, c) +} + +// Options for ServerConfiguration +var ( + OptionServerListeningAddr = func(val string) OptionServerSet { + return func(c *ServerConfiguration) { + c.ListeningAddr = val + } + } + + OptionServerCertFile = func(val string) OptionServerSet { + return func(c *ServerConfiguration) { + c.CertFile = val + } + } + + OptionServerKeyFile = func(val string) OptionServerSet { + return func(c *ServerConfiguration) { + c.KeyFile = val + } + } + + // AutoTLS enable to get certifications from the Letsencrypt + // when this configuration field is true, the CertFile & KeyFile are empty, no need to provide a key. + // + // example: https://github.com/iris-contrib/examples/blob/master/letsencyrpt/main.go + OptionServerAutoTLS = func(val bool) OptionServerSet { + return func(c *ServerConfiguration) { + c.AutoTLS = val + } + } + + // Mode this is for unix only + OptionServerMode = func(val os.FileMode) OptionServerSet { + return func(c *ServerConfiguration) { + c.Mode = val + } + } + + // OptionServerMaxRequestBodySize Maximum request body size. + // + // The server rejects requests with bodies exceeding this limit. + // + // By default request body size is 8MB. + OptionServerMaxRequestBodySize = func(val int) OptionServerSet { + return func(c *ServerConfiguration) { + c.MaxRequestBodySize = val + } + } + + // Per-connection buffer size for requests' reading. + // This also limits the maximum header size. + // + // Increase this buffer if your clients send multi-KB RequestURIs + // and/or multi-KB headers (for example, BIG cookies). + // + // Default buffer size is used if not set. + OptionServerReadBufferSize = func(val int) OptionServerSet { + return func(c *ServerConfiguration) { + c.ReadBufferSize = val + } + } + + // Per-connection buffer size for responses' writing. + // + // Default buffer size is used if not set. + OptionServerWriteBufferSize = func(val int) OptionServerSet { + return func(c *ServerConfiguration) { + c.WriteBufferSize = val + } + } + + // Maximum duration for reading the full request (including body). + // + // This also limits the maximum duration for idle keep-alive + // connections. + // + // By default request read timeout is unlimited. + OptionServerReadTimeout = func(val time.Duration) OptionServerSet { + return func(c *ServerConfiguration) { + c.ReadTimeout = val + } + } + + // Maximum duration for writing the full response (including body). + // + // By default response write timeout is unlimited. + OptionServerWriteTimeout = func(val time.Duration) OptionServerSet { + return func(c *ServerConfiguration) { + c.WriteTimeout = val + } + } + + // RedirectTo, defaults to empty, set it in order to override the station's handler and redirect all requests to this address which is of form(HOST:PORT or :PORT) + // + // NOTE: the http status is 'StatusMovedPermanently', means one-time-redirect(the browser remembers the new addr and goes to the new address without need to request something from this server + // which means that if you want to change this address you have to clear your browser's cache in order this to be able to change to the new addr. + // + // example: https://github.com/iris-contrib/examples/tree/master/multiserver_listening2 + OptionServerRedirectTo = func(val string) OptionServerSet { + return func(c *ServerConfiguration) { + c.RedirectTo = val + } + } + + // OptionServerVirtual If this server is not really listens to a real host, it mostly used in order to achieve testing without system modifications + OptionServerVirtual = func(val bool) OptionServerSet { + return func(c *ServerConfiguration) { + c.Virtual = val + } + } + // OptionServerVListeningAddr, can be used for both virtual = true or false, + // if it's setted to not empty, then the server's Host() will return this addr instead of the ListeningAddr. + // server's Host() is used inside global template helper funcs + // set it when you are sure you know what it does. + // + // Default is empty "" + OptionServerVListeningAddr = func(val string) OptionServerSet { + return func(c *ServerConfiguration) { + c.VListeningAddr = val + } + } + + // OptionServerVScheme if setted to not empty value then all template's helper funcs prepends that as the url scheme instead of the real scheme + // server's .Scheme returns VScheme if not empty && differs from real scheme + // + // Default is empty "" + OptionServerVScheme = func(val string) OptionServerSet { + return func(c *ServerConfiguration) { + c.VScheme = val + } + } + + // OptionServerName the server's name, defaults to "iris". + // You're free to change it, but I will trust you to don't, this is the only setting whose somebody, like me, can see if iris web framework is used + OptionServerName = func(val string) OptionServerSet { + return func(c *ServerConfiguration) { + c.ListeningAddr = val + } + } +) + +// ServerParseAddr parses the listening addr and returns this +func ServerParseAddr(listeningAddr string) string { + // check if addr has :port, if not do it +:80 ,we need the hostname for many cases + a := listeningAddr + if a == "" { + // check for os environments + if oshost := os.Getenv("HOST"); oshost != "" { + a = oshost + } else if oshost := os.Getenv("ADDR"); oshost != "" { + a = oshost + } else if osport := os.Getenv("PORT"); osport != "" { + a = ":" + osport + } + + if a == "" { + a = DefaultServerAddr + } + + } + if portIdx := strings.IndexByte(a, ':'); portIdx == 0 { + // if contains only :port ,then the : is the first letter, so we dont have setted a hostname, lets set it + a = DefaultServerHostname + a + } + if portIdx := strings.IndexByte(a, ':'); portIdx < 0 { + // missing port part, add it + a = a + ":80" + } + + return a +} + +// Default values for base Server conf +const ( + // DefaultServerHostname returns the default hostname which is 0.0.0.0 + DefaultServerHostname = "0.0.0.0" + // DefaultServerPort returns the default port which is 8080 + DefaultServerPort = 8080 + // DefaultMaxRequestBodySize is 8MB + DefaultMaxRequestBodySize = 2 * fasthttp.DefaultMaxRequestBodySize + + // Per-connection buffer size for requests' reading. + // This also limits the maximum header size. + // + // Increase this buffer if your clients send multi-KB RequestURIs + // and/or multi-KB headers (for example, BIG cookies). + // + // Default buffer size is 8MB + DefaultReadBufferSize = 8096 + + // Per-connection buffer size for responses' writing. + // + // Default buffer size is 8MB + DefaultWriteBufferSize = 8096 + + // DefaultServerName the response header of the 'Server' value when writes to the client + DefaultServerName = "iris" +) + +var ( + // DefaultServerAddr the default server addr which is: 0.0.0.0:8080 + DefaultServerAddr = DefaultServerHostname + ":" + strconv.Itoa(DefaultServerPort) +) + +// DefaultServerConfiguration returns the default configs for the server +func DefaultServerConfiguration() ServerConfiguration { + return ServerConfiguration{ + ListeningAddr: DefaultServerAddr, + Name: DefaultServerName, + MaxRequestBodySize: DefaultMaxRequestBodySize, + ReadBufferSize: DefaultReadBufferSize, + WriteBufferSize: DefaultWriteBufferSize, + RedirectTo: "", + Virtual: false, + VListeningAddr: "", + VScheme: "", + } +} diff --git a/configuration_test.go b/configuration_test.go new file mode 100644 index 00000000..6c42ac42 --- /dev/null +++ b/configuration_test.go @@ -0,0 +1,102 @@ +package iris + +import ( + "reflect" + "testing" +) + +// go test -v -run TestConfig* + +func TestConfigStatic(t *testing.T) { + def := DefaultConfiguration() + + api := New(def) + afterNew := *api.Config + + if !reflect.DeepEqual(def, afterNew) { + t.Fatalf("Default configuration is not the same after NewFromConfig expected:\n %#v \ngot:\n %#v", def, afterNew) + } + + afterNew.Charset = "changed" + + if reflect.DeepEqual(def, afterNew) { + t.Fatalf("Configuration should be not equal, got: %#v", afterNew) + } + + api = New(Configuration{IsDevelopment: true}) + + afterNew = *api.Config + + if api.Config.IsDevelopment == false { + t.Fatalf("Passing a Configuration field as Option fails, expected IsDevelopment to be true but was false") + } + + api = New() // empty , means defaults so + if !reflect.DeepEqual(def, *api.Config) { + t.Fatalf("Default configuration is not the same after NewFromConfig expected:\n %#v \ngot:\n %#v", def, *api.Config) + } +} + +func TestConfigOptions(t *testing.T) { + charset := "MYCHARSET" + dev := true + + api := New(OptionCharset(charset), OptionIsDevelopment(dev)) + + if got := api.Config.Charset; got != charset { + t.Fatalf("Expected configuration Charset to be: %s but got: %s", charset, got) + } + + if got := api.Config.IsDevelopment; got != dev { + t.Fatalf("Expected configuration IsDevelopment to be: %#v but got: %#v", dev, got) + } + + // now check if other default values are setted (should be setted automatically) + + expected := DefaultConfiguration() + expected.Charset = charset + expected.IsDevelopment = dev + + has := *api.Config + if !reflect.DeepEqual(has, expected) { + t.Fatalf("Default configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) + } +} + +func TestConfigOptionsDeep(t *testing.T) { + cookiename := "MYSESSIONID" + charset := "MYCHARSET" + dev := true + profilePath := "/mypprof" + // first session, after charset,dev and profilepath, no canonical order. + api := New(OptionSessionsCookie(cookiename), OptionCharset(charset), OptionIsDevelopment(dev), OptionProfilePath(profilePath)) + + expected := DefaultConfiguration() + expected.Sessions.Cookie = cookiename + expected.Charset = charset + expected.IsDevelopment = dev + expected.ProfilePath = profilePath + + has := *api.Config + + if !reflect.DeepEqual(has, expected) { + t.Fatalf("DEEP configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) + } +} + +// ServerConfiguration is independent so make a small test for that +func TestConfigServerOptions(t *testing.T) { + expected := DefaultServerConfiguration() + expected.ListeningAddr = "mydomain.com:80" + expected.RedirectTo = "https://mydomain.com:443" + expected.Virtual = true + + c := ServerConfiguration{ListeningAddr: expected.ListeningAddr, RedirectTo: expected.RedirectTo, Virtual: expected.Virtual} + // static config test + s := newServer(c) + + if !reflect.DeepEqual(s.Config, expected) { + t.Fatalf("Static Server Configuration not equal after newServer, expected:\n%#v \nwhile got:\n%#v", expected, s.Config) + } + +} diff --git a/context.go b/context.go index d92d2c49..6a29e03c 100644 --- a/context.go +++ b/context.go @@ -10,6 +10,13 @@ import ( "encoding/json" "encoding/xml" "fmt" + "github.com/iris-contrib/formBinder" + "github.com/kataras/go-errors" + "github.com/kataras/go-fs" + "github.com/kataras/go-sessions" + "github.com/kataras/iris/context" + "github.com/kataras/iris/utils" + "github.com/valyala/fasthttp" "io" "net" "os" @@ -18,18 +25,7 @@ import ( "runtime" "strconv" "strings" - "sync" "time" - - "github.com/iris-contrib/formBinder" - "github.com/kataras/go-errors" - "github.com/kataras/go-fs" - "github.com/kataras/go-sessions" - "github.com/kataras/iris/config" - "github.com/kataras/iris/context" - "github.com/kataras/iris/utils" - "github.com/klauspost/compress/gzip" - "github.com/valyala/fasthttp" ) const ( @@ -80,9 +76,6 @@ const ( cookieHeaderIDLen = len(cookieHeaderID) ) -// this pool is used everywhere needed in the iris for example inside party-> Static -var gzipWriterPool = sync.Pool{New: func() interface{} { return &gzip.Writer{} }} - // errors var ( @@ -541,14 +534,29 @@ func (ctx *Context) Gzip(b []byte, status int) { } } +// RenderTemplateSource serves a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string +func (ctx *Context) RenderTemplateSource(status int, src string, binding interface{}, options ...map[string]interface{}) error { + err := ctx.framework.templates.renderSource(ctx, src, binding, options...) + if err == nil { + ctx.SetStatusCode(status) + } + + return err +} + // 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) +func (ctx *Context) RenderWithStatus(status int, name string, binding interface{}, options ...map[string]interface{}) (err error) { if strings.IndexByte(name, '.') > -1 { //we have template - return ctx.framework.templates.render(ctx, name, binding, options...) + err = ctx.framework.templates.render(ctx, name, binding, options...) } - return ctx.framework.responses.getBy(name).render(ctx, binding, options...) + err = ctx.framework.responses.getBy(name).render(ctx, binding, options...) + + if err == nil { + ctx.SetStatusCode(status) + } + + return } // Render same as .RenderWithStatus but with status to iris.StatusOK (200) if no previous status exists @@ -634,7 +642,7 @@ func (ctx *Context) Markdown(status int, markdown string) { // You can define your own "Content-Type" header also, after this function call // Doesn't implements resuming (by range), use ctx.SendFile instead func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { - if t, err := time.Parse(config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(1*time.Second)) { + if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(1*time.Second)) { ctx.RequestCtx.Response.Header.Del(contentType) ctx.RequestCtx.Response.Header.Del(contentLength) ctx.RequestCtx.SetStatusCode(StatusNotModified) @@ -642,20 +650,18 @@ func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime } ctx.RequestCtx.Response.Header.Set(contentType, fs.TypeByExtension(filename)) - ctx.RequestCtx.Response.Header.Set(lastModified, modtime.UTC().Format(config.TimeFormat)) + ctx.RequestCtx.Response.Header.Set(lastModified, modtime.UTC().Format(ctx.framework.Config.TimeFormat)) ctx.RequestCtx.SetStatusCode(StatusOK) var out io.Writer if gzipCompression && ctx.clientAllowsGzip() { ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader) ctx.SetHeader(contentEncodingHeader, "gzip") - gzipWriter := gzipWriterPool.Get().(*gzip.Writer) - gzipWriter.Reset(ctx.RequestCtx.Response.BodyWriter()) - defer gzipWriter.Close() - defer gzipWriterPool.Put(gzipWriter) + + gzipWriter := fs.AcquireGzipWriter(ctx.RequestCtx.Response.BodyWriter()) + defer fs.ReleaseGzipWriter(gzipWriter) out = gzipWriter } else { out = ctx.RequestCtx.Response.BodyWriter() - } _, err := io.Copy(out, content) return errServeContent.With(err) diff --git a/http.go b/http.go index 4e8f826e..98e39f45 100644 --- a/http.go +++ b/http.go @@ -17,7 +17,6 @@ import ( "github.com/iris-contrib/letsencrypt" "github.com/kataras/go-errors" - "github.com/kataras/iris/config" "github.com/kataras/iris/utils" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpadaptor" @@ -244,7 +243,7 @@ type ( Server struct { *fasthttp.Server listener net.Listener - Config config.Server + Config ServerConfiguration tls bool mu sync.Mutex } @@ -256,11 +255,13 @@ type ( ) // newServer returns a pointer to a Server object, and set it's options if any, nothing more -func newServer(cfg config.Server) *Server { - if cfg.Name == "" { - cfg.Name = config.DefaultServerName +func newServer(setters ...OptionServerSettter) *Server { + c := DefaultServerConfiguration() + for _, setter := range setters { + setter.Set(&c) } - s := &Server{Server: &fasthttp.Server{Name: cfg.Name}, Config: cfg} + + s := &Server{Server: &fasthttp.Server{Name: c.Name}, Config: c} return s } @@ -436,6 +437,7 @@ func (s *Server) Open(h fasthttp.RequestHandler) error { s.Server.WriteBufferSize = s.Config.WriteBufferSize s.Server.ReadTimeout = s.Config.ReadTimeout s.Server.WriteTimeout = s.Config.WriteTimeout + if s.Config.RedirectTo != "" { // override the handler and redirect all requests to this addr s.Server.Handler = func(reqCtx *fasthttp.RequestCtx) { @@ -454,7 +456,7 @@ func (s *Server) Open(h fasthttp.RequestHandler) error { return s.listenUNIX() } - s.Config.ListeningAddr = config.ServerParseAddr(s.Config.ListeningAddr) + s.Config.ListeningAddr = ServerParseAddr(s.Config.ListeningAddr) if s.Config.Virtual { return nil @@ -482,8 +484,8 @@ func (s *Server) Close() (err error) { // Add adds a server to the list by its config // returns the new server -func (s *ServerList) Add(cfg config.Server) *Server { - srv := newServer(cfg) +func (s *ServerList) Add(setters ...OptionServerSettter) *Server { + srv := newServer(setters...) s.servers = append(s.servers, srv) return srv } @@ -1356,9 +1358,9 @@ func newServeMux(logger *log.Logger) *serveMux { mux := &serveMux{ lookups: make([]*route, 0), errorHandlers: make(map[int]Handler, 0), - hostname: config.DefaultServerHostname, // these are changing when the server is up - escapePath: !config.DefaultDisablePathEscape, - correctPath: !config.DefaultDisablePathCorrection, + hostname: DefaultServerHostname, // these are changing when the server is up + escapePath: !DefaultDisablePathEscape, + correctPath: !DefaultDisablePathCorrection, logger: logger, } diff --git a/http_test.go b/http_test.go index cbfd3fc5..e2f6fbe1 100644 --- a/http_test.go +++ b/http_test.go @@ -14,7 +14,6 @@ import ( "time" "github.com/gavv/httpexpect" - "github.com/kataras/iris/config" ) const ( @@ -72,14 +71,14 @@ func TestServerHost(t *testing.T) { var server1, server2, server3 Server var expectedHost1 = "mydomain.com:1993" var expectedHost2 = "mydomain.com:80" - var expectedHost3 = config.DefaultServerHostname + ":9090" + var expectedHost3 = DefaultServerHostname + ":9090" server1.Config.ListeningAddr = expectedHost1 server2.Config.ListeningAddr = "mydomain.com" server3.Config.ListeningAddr = ":9090" - server1.Config.ListeningAddr = config.ServerParseAddr(server1.Config.ListeningAddr) - server2.Config.ListeningAddr = config.ServerParseAddr(server2.Config.ListeningAddr) - server3.Config.ListeningAddr = config.ServerParseAddr(server3.Config.ListeningAddr) + server1.Config.ListeningAddr = ServerParseAddr(server1.Config.ListeningAddr) + server2.Config.ListeningAddr = ServerParseAddr(server2.Config.ListeningAddr) + server3.Config.ListeningAddr = ServerParseAddr(server3.Config.ListeningAddr) if server1.Host() != expectedHost1 { t.Fatalf("Expecting server 1's host to be %s but we got %s", expectedHost1, server1.Host()) @@ -96,7 +95,7 @@ func TestServerHostname(t *testing.T) { var server Server var expectedHostname = "mydomain.com" server.Config.ListeningAddr = expectedHostname + ":1993" - server.Config.ListeningAddr = config.ServerParseAddr(server.Config.ListeningAddr) + server.Config.ListeningAddr = ServerParseAddr(server.Config.ListeningAddr) if server.Hostname() != expectedHostname { t.Fatalf("Expecting server's hostname to be %s but we got %s", expectedHostname, server.Hostname()) } @@ -126,8 +125,8 @@ func TestServerPort(t *testing.T) { expectedPort2 := 80 server1.Config.ListeningAddr = "mydomain.com:8080" server2.Config.ListeningAddr = "mydomain.com" - server1.Config.ListeningAddr = config.ServerParseAddr(server1.Config.ListeningAddr) - server2.Config.ListeningAddr = config.ServerParseAddr(server2.Config.ListeningAddr) + server1.Config.ListeningAddr = ServerParseAddr(server1.Config.ListeningAddr) + server2.Config.ListeningAddr = ServerParseAddr(server2.Config.ListeningAddr) if server1.Port() != expectedPort1 { t.Fatalf("Expecting server 1's port to be %d but we got %d", expectedPort1, server1.Port()) @@ -172,9 +171,9 @@ func TestMultiRunningServers_v1(t *testing.T) { }) // start the secondary server - AddServer(config.Server{ListeningAddr: "mydomain.com:80", RedirectTo: "https://" + host, Virtual: true}) + AddServer(ServerConfiguration{ListeningAddr: "mydomain.com:80", RedirectTo: "https://" + host, Virtual: true}) // start the main server - go ListenTo(config.Server{ListeningAddr: host, CertFile: certFile.Name(), KeyFile: keyFile.Name(), Virtual: true}) + go ListenTo(ServerConfiguration{ListeningAddr: host, CertFile: certFile.Name(), KeyFile: keyFile.Name(), Virtual: true}) // prepare test framework if ok := <-Available; !ok { t.Fatal("Unexpected error: server cannot start, please report this as bug!!") @@ -224,9 +223,9 @@ func TestMultiRunningServers_v2(t *testing.T) { }) // add a secondary server - Servers.Add(config.Server{ListeningAddr: domain + ":80", RedirectTo: "https://" + host, Virtual: true}) + Servers.Add(ServerConfiguration{ListeningAddr: domain + ":80", RedirectTo: "https://" + host, Virtual: true}) // add our primary/main server - Servers.Add(config.Server{ListeningAddr: host, CertFile: certFile.Name(), KeyFile: keyFile.Name(), Virtual: true}) + Servers.Add(ServerConfiguration{ListeningAddr: host, CertFile: certFile.Name(), KeyFile: keyFile.Name(), Virtual: true}) go Go() diff --git a/iris.go b/iris.go index 93dbd98f..c3d3001b 100644 --- a/iris.go +++ b/iris.go @@ -76,7 +76,6 @@ import ( "github.com/kataras/go-sessions" "github.com/kataras/go-template" "github.com/kataras/go-template/html" - "github.com/kataras/iris/config" "github.com/kataras/iris/context" "github.com/kataras/iris/utils" "github.com/valyala/fasthttp" @@ -84,7 +83,7 @@ import ( const ( // Version of the iris - Version = "4.1.7" + Version = "4.2.0" banner = ` _____ _ |_ _| (_) @@ -97,7 +96,7 @@ const ( // Default entry, use it with iris.$anyPublicFunc var ( Default *Framework - Config *config.Iris + Config *Configuration Logger *log.Logger // if you want colors in your console then you should use this https://github.com/iris-contrib/logger instead. Plugins PluginContainer Websocket *WebsocketServer @@ -143,8 +142,9 @@ type ( FrameworkAPI interface { MuxAPI Must(error) - AddServer(config.Server) *Server - ListenTo(config.Server) error + Set(...OptionSetter) + AddServer(...OptionServerSettter) *Server + ListenTo(...OptionServerSettter) error Listen(string) ListenTLS(string, string, string) ListenUNIX(string, os.FileMode) @@ -161,8 +161,9 @@ type ( Path(string, ...interface{}) string URL(string, ...interface{}) string TemplateString(string, interface{}, ...map[string]interface{}) string + TemplateSourceString(string, interface{}) string ResponseString(string, interface{}, ...map[string]interface{}) string - Tester(t *testing.T) *httpexpect.Expect + Tester(*testing.T) *httpexpect.Expect } // Framework is our God |\| Google.Search('Greek mythology Iris') @@ -171,7 +172,7 @@ type ( Framework struct { *muxAPI contextPool sync.Pool - Config *config.Iris + Config *Configuration sessions sessions.Sessions responses *responseEngines templates *templateEngines @@ -191,55 +192,76 @@ type ( var _ FrameworkAPI = &Framework{} -// New creates and returns a new Iris station aka Framework. +// New creates and returns a new Iris instance. // -// Receives an optional config.Iris as parameter -// If empty then config.Default() is used instead -func New(cfg ...config.Iris) *Framework { - c := config.Default().Merge(cfg) +// Receives (optional) multi options, use iris.Option and your editor should show you the available options to set +// all options are inside ./configuration.go +// example 1: iris.New(iris.OptionIsDevelopment(true), iris.OptionCharset("UTF-8"), irisOptionSessionsCookie("mycookieid"),iris.OptionWebsocketEndpoint("my_endpoint")) +// example 2: iris.New(iris.Configuration{IsDevelopment:true, Charset: "UTF-8", Sessions: iris.SessionsConfiguration{Cookie:"mycookieid"}, Websocket: iris.WebsocketConfiguration{Endpoint:"/my_endpoint"}}) +// both ways are totally valid and equal +func New(setters ...OptionSetter) *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, - // set the Logger - Logger: log.New(c.LoggerOut, c.LoggerPreffix, log.LstdFlags), - responses: &responseEngines{}, - Available: make(chan bool), - SSH: &SSHServer{}, - // set the sessions, configuration willbe updated on the initialization also, in order to give the user the opportunity to change its config at runtime. - sessions: sessions.New(sessions.Config(c.Sessions)), - } + s := &Framework{} + s.Set(setters...) + + // logger, plugins & ssh { - s.contextPool.New = func() interface{} { - return &Context{framework: s} - } - ///NOTE: set all with s.Config pointer - - // set the plugin container + // set the Logger, which it's configuration should be declared before .Listen because the servemux and plugins needs that + s.Logger = log.New(s.Config.LoggerOut, s.Config.LoggerPreffix, log.LstdFlags) s.Plugins = newPluginContainer(s.Logger) + s.SSH = NewSSHServer() + } + + // rendering + { + s.responses = newResponseEngines() // set the templates s.templates = newTemplateEngines(map[string]interface{}{ "url": s.URL, "urlpath": s.Path, }) - // set the websocket server - s.Websocket = NewWebsocketServer(s.Config.Websocket) + } + + // websocket + { + s.Websocket = NewWebsocketServer() // in order to be able to call $instance.Websocket.OnConnection + } + + // routing & http server + { // set the servemux, which will provide us the public API also, with its context pool mux := newServeMux(s.Logger) mux.onLookup = s.Plugins.DoPreLookup // set the public router API (and party) s.muxAPI = &muxAPI{mux: mux, relativePath: "/"} - + s.contextPool.New = func() interface{} { + return &Context{framework: s} + } s.Servers = &ServerList{mux: mux, servers: make([]*Server, 0)} + s.Available = make(chan bool) } return s } -func (s *Framework) initialize() { +// Set sets an option aka configuration field to the default iris instance +func Set(setters ...OptionSetter) { + Default.Set(setters...) +} - s.sessions.UpdateConfig(sessions.Config(s.Config.Sessions)) +// Set sets an option aka configuration field to this iris instance +func (s *Framework) Set(setters ...OptionSetter) { + if s.Config == nil { + defaultConfiguration := DefaultConfiguration() + s.Config = &defaultConfiguration + } + + for _, setter := range setters { + setter.Set(s.Config) + } +} + +func (s *Framework) initialize() { // prepare the response engines, if no response engines setted for the default content-types // then add them @@ -276,8 +298,13 @@ func (s *Framework) initialize() { s.Logger.Panic(err) // panic on templates loading before listening if we have an error. } } - // listen to websocket connections - RegisterWebsocketServer(s, s.Websocket, s.Logger) + // set the sessions + s.sessions = sessions.New(sessions.Config(s.Config.Sessions)) + + if s.Config.Websocket.Endpoint != "" { + // register the websocket server and listen to websocket connections when/if $instance.Websocket.OnConnection called by the dev + s.Websocket.RegisterTo(s, s.Config.Websocket) + } // prepare the mux & the server s.mux.setCorrectPath(!s.Config.DisablePathCorrection) @@ -328,7 +355,7 @@ func (s *Framework) Go() error { hosts[i] = srv.Host() } - bannerMessage := fmt.Sprintf("%s: Running at %s", time.Now().Format(config.TimeFormat), strings.Join(hosts, ", ")) + bannerMessage := fmt.Sprintf("%s: Running at %s", time.Now().Format(s.Config.TimeFormat), strings.Join(hosts, ", ")) // we don't print it via Logger because: // 1. The banner is only 'useful' when the developer logs to terminal and no file // 2. Prefix & LstdFlags options of the default s.Logger @@ -358,68 +385,60 @@ func (s *Framework) Must(err error) { } } -// AddServer same as .Servers.Add(config.Server) +// AddServer same as .Servers.Add(ServerConfiguration) // // AddServer starts a server which listens to this station // Note that the view engine's functions {{ url }} and {{ urlpath }} will return the first's registered server's scheme (http/https) // // this is useful mostly when you want to have two or more listening ports ( two or more servers ) for the same station // -// receives one parameter which is the config.Server for the new server +// receives one parameter which is the ServerConfiguration for the new server // returns the new standalone server( you can close this server by the returning reference) // // If you need only one server you can use the blocking-funcs: .Listen/ListenTLS/ListenUNIX/ListenTo // // this is a NOT A BLOCKING version, the main .Listen/ListenTLS/ListenUNIX/ListenTo should be always executed LAST, so this function goes before the main .Listen/ListenTLS/ListenUNIX/ListenTo -func AddServer(cfg config.Server) *Server { - return Default.AddServer(cfg) +func AddServer(setters ...OptionServerSettter) *Server { + return Default.AddServer(setters...) } -// AddServer same as .Servers.Add(config.Server) +// AddServer same as .Servers.Add(ServerConfiguration) // // AddServer starts a server which listens to this station // Note that the view engine's functions {{ url }} and {{ urlpath }} will return the last registered server's scheme (http/https) // // this is useful mostly when you want to have two or more listening ports ( two or more servers ) for the same station // -// receives one parameter which is the config.Server for the new server +// receives one parameter which is the ServerConfiguration for the new server // returns the new standalone server( you can close this server by the returning reference) // // If you need only one server you can use the blocking-funcs: .Listen/ListenTLS/ListenUNIX/ListenTo // // this is a NOT A BLOCKING version, the main .Listen/ListenTLS/ListenUNIX/ListenTo should be always executed LAST, so this function goes before the main .Listen/ListenTLS/ListenUNIX/ListenTo -func (s *Framework) AddServer(cfg config.Server) *Server { - return s.Servers.Add(cfg) +func (s *Framework) AddServer(setters ...OptionServerSettter) *Server { + return s.Servers.Add(setters...) } // ListenTo listens to a server but accepts the full server's configuration // returns an error, you're responsible to handle that -// or use the iris.Must(iris.ListenTo(config.Server{})) +// ex: ris.ListenTo(iris.ServerConfiguration{ListeningAddr:":8080"}) +// ex2: err := iris.ListenTo(iris.OptionServerListeningAddr(":8080")) +// or use the iris.Must(iris.ListenTo(iris.ServerConfiguration{ListeningAddr:":8080"})) // // it's a blocking func -func ListenTo(cfg config.Server) error { - return Default.ListenTo(cfg) +func ListenTo(setters ...OptionServerSettter) error { + return Default.ListenTo(setters...) } // ListenTo listens to a server but acceots the full server's configuration // returns an error, you're responsible to handle that -// or use the iris.Must(iris.ListenTo(config.Server{})) +// ex: ris.ListenTo(iris.ServerConfiguration{ListeningAddr:":8080"}) +// ex2: err := iris.ListenTo(iris.OptionServerListeningAddr(":8080")) +// or use the iris.Must(iris.ListenTo(iris.ServerConfiguration{ListeningAddr:":8080"})) // // it's a blocking func -func (s *Framework) ListenTo(cfg config.Server) (err error) { - if cfg.ReadBufferSize == 0 { - cfg.ReadBufferSize = config.DefaultReadBufferSize - } - if cfg.WriteBufferSize == 0 { - cfg.WriteBufferSize = config.DefaultWriteBufferSize - } - if cfg.MaxRequestBodySize == 0 { - cfg.MaxRequestBodySize = config.DefaultMaxRequestBodySize - } - if cfg.ListeningAddr == "" { - cfg.ListeningAddr = config.DefaultServerAddr - } - s.Servers.Add(cfg) +func (s *Framework) ListenTo(setters ...OptionServerSettter) (err error) { + s.Servers.Add(setters...) return s.Go() } @@ -428,7 +447,6 @@ func (s *Framework) ListenTo(cfg config.Server) (err error) { // host:port // // It panics on error if you need a func to return an error, use the ListenTo -// ex: err := iris.ListenTo(config.Server{ListeningAddr:":8080"}) func Listen(addr string) { Default.Listen(addr) } @@ -438,9 +456,8 @@ func Listen(addr string) { // host:port // // It panics on error if you need a func to return an error, use the ListenTo -// ex: err := iris.ListenTo(config.Server{ListeningAddr:":8080"}) func (s *Framework) Listen(addr string) { - s.Must(s.ListenTo(config.Server{ListeningAddr: addr})) + s.Must(s.ListenTo(ServerConfiguration{ListeningAddr: addr})) } // ListenTLS Starts a https server with certificates, @@ -450,7 +467,7 @@ func (s *Framework) Listen(addr string) { // host:port // // It panics on error if you need a func to return an error, use the ListenTo -// ex: err := iris.ListenTo(":8080","yourfile.cert","yourfile.key") +// ex: iris.ListenTLS(":8080","yourfile.cert","yourfile.key") func ListenTLS(addr string, certFile string, keyFile string) { Default.ListenTLS(addr, certFile, keyFile) } @@ -461,7 +478,7 @@ func ListenTLS(addr string, certFile string, keyFile string) { // // Notes: // if you don't want the last feature you should use this method: -// iris.ListenTo(config.Server{ListeningAddr: "mydomain.com:443", AutoTLS: true}) +// iris.ListenTo(iris.ServerConfiguration{ListeningAddr: "mydomain.com:443", AutoTLS: true}) // it's a blocking function // Limit : https://github.com/iris-contrib/letsencrypt/blob/master/lets.go#L142 // @@ -477,12 +494,12 @@ func ListenTLSAuto(addr string) { // host:port // // It panics on error if you need a func to return an error, use the ListenTo -// ex: err := iris.ListenTo(":8080","yourfile.cert","yourfile.key") +// ex: iris.ListenTLS(":8080","yourfile.cert","yourfile.key") func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { if certFile == "" || keyFile == "" { s.Logger.Panic("You should provide certFile and keyFile for TLS/SSL") } - s.Must(s.ListenTo(config.Server{ListeningAddr: addr, CertFile: certFile, KeyFile: keyFile})) + s.Must(s.ListenTo(ServerConfiguration{ListeningAddr: addr, CertFile: certFile, KeyFile: keyFile})) } // ListenTLSAuto starts a server listening at the specific nat address @@ -491,7 +508,7 @@ func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { // // Notes: // if you don't want the last feature you should use this method: -// iris.ListenTo(config.Server{ListeningAddr: "mydomain.com:443", AutoTLS: true}) +// iris.ListenTo(iris.ServerConfiguration{ListeningAddr: "mydomain.com:443", AutoTLS: true}) // it's a blocking function // Limit : https://github.com/iris-contrib/letsencrypt/blob/master/lets.go#L142 // @@ -500,18 +517,18 @@ func (s *Framework) ListenTLSAuto(addr string) { if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 { addr += ":443" } - addr = config.ServerParseAddr(addr) + addr = ServerParseAddr(addr) // start a secondary server (HTTP) on port 80, this is a non-blocking func // redirects all http to the main server which is tls/ssl on port :443 - s.AddServer(config.Server{ListeningAddr: ":80", RedirectTo: "https://" + addr}) - s.Must(s.ListenTo(config.Server{ListeningAddr: addr, AutoTLS: true})) + s.AddServer(ServerConfiguration{ListeningAddr: ":80", RedirectTo: "https://" + addr}) + s.Must(s.ListenTo(ServerConfiguration{ListeningAddr: addr, AutoTLS: true})) } // ListenUNIX starts the process of listening to the new requests using a 'socket file', this works only on unix // // It panics on error if you need a func to return an error, use the ListenTo -// ex: err := iris.ListenTo(":8080", Mode: os.FileMode) +// ex: iris.ListenUNIX(":8080", Mode: os.FileMode) func ListenUNIX(addr string, mode os.FileMode) { Default.ListenUNIX(addr, mode) } @@ -519,9 +536,9 @@ func ListenUNIX(addr string, mode os.FileMode) { // ListenUNIX starts the process of listening to the new requests using a 'socket file', this works only on unix // // It panics on error if you need a func to return an error, use the ListenTo -// ex: err := iris.ListenTo(":8080", Mode: os.FileMode) +// ex: ris.ListenUNIX(":8080", Mode: os.FileMode) func (s *Framework) ListenUNIX(addr string, mode os.FileMode) { - s.Must(ListenTo(config.Server{ListeningAddr: addr, Mode: mode})) + s.Must(ListenTo(ServerConfiguration{ListeningAddr: addr, Mode: mode})) } // ListenVirtual is useful only when you want to test Iris, it doesn't starts the server but it configures and returns it @@ -536,7 +553,7 @@ func ListenVirtual(optionalAddr ...string) *Server { // it is not blocking the app func (s *Framework) ListenVirtual(optionalAddr ...string) *Server { s.Config.DisableBanner = true - cfg := config.DefaultServer() + cfg := DefaultServerConfiguration() if len(optionalAddr) > 0 && optionalAddr[0] != "" { cfg.ListeningAddr = optionalAddr[0] @@ -901,6 +918,27 @@ func (s *Framework) TemplateString(templateFile string, pageContext interface{}, return res } +// TemplateSourceString executes a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string, +// useful when you want it for sending rich e-mails +// returns empty string on error +func TemplateSourceString(src string, pageContext interface{}) string { + return Default.TemplateSourceString(src, pageContext) +} + +// TemplateSourceString executes a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string, +// useful when you want it for sending rich e-mails +// returns empty string on error +func (s *Framework) TemplateSourceString(src string, pageContext interface{}) string { + if s.Config.DisableTemplateEngines { + return "" + } + res, err := s.templates.ExecuteRawString(src, pageContext) + if err != nil { + res = "" + } + return res +} + // ResponseString returns the string of a response engine, // does not render it to the client // returns empty string on error @@ -1527,8 +1565,8 @@ func (api *muxAPI) StaticHandler(systemPath string, stripSlashes int, compress b // Enable transparent compression to save network traffic. Compress: compress, - CacheDuration: config.StaticCacheDuration, - CompressedFileSuffix: config.CompressedFileSuffix, + CacheDuration: StaticCacheDuration, + CompressedFileSuffix: CompressedFileSuffix, } if stripSlashes > 0 { @@ -1713,9 +1751,13 @@ func StaticContent(reqPath string, contentType string, content []byte) RouteName // a good example of this is how the websocket server uses that to auto-register the /iris-ws.js func (api *muxAPI) StaticContent(reqPath string, cType string, content []byte) RouteNameFunc { // func(string) because we use that on websockets modtime := time.Now() - modtimeStr := modtime.UTC().Format(config.TimeFormat) + modtimeStr := "" h := func(ctx *Context) { - if t, err := time.Parse(config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(config.StaticCacheDuration)) { + if modtimeStr == "" { + modtimeStr = modtime.UTC().Format(ctx.framework.Config.TimeFormat) + } + + if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && modtime.Before(t.Add(StaticCacheDuration)) { ctx.Response.Header.Del(contentType) ctx.Response.Header.Del(contentLength) ctx.SetStatusCode(StatusNotModified) @@ -1772,16 +1814,19 @@ func (api *muxAPI) Favicon(favPath string, requestPath ...string) RouteNameFunc favPath = fav fi, _ = f.Stat() } - modtime := fi.ModTime().UTC().Format(config.TimeFormat) + cType := fs.TypeByExtension(favPath) // copy the bytes here in order to cache and not read the ico on each request. cacheFav := make([]byte, fi.Size()) if _, err = f.Read(cacheFav); err != nil { panic(errDirectoryFileNotFound.Format(favPath, "Couldn't read the data bytes for Favicon: "+err.Error())) } - + modtime := "" h := func(ctx *Context) { - if t, err := time.Parse(config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && fi.ModTime().Before(t.Add(config.StaticCacheDuration)) { + if modtime == "" { + modtime = fi.ModTime().UTC().Format(ctx.framework.Config.TimeFormat) + } + if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && fi.ModTime().Before(t.Add(StaticCacheDuration)) { ctx.Response.Header.Del(contentType) ctx.Response.Header.Del(contentLength) ctx.SetStatusCode(StatusNotModified) diff --git a/response.go b/response.go index 46298c12..8310c776 100644 --- a/response.go +++ b/response.go @@ -184,6 +184,10 @@ type responseEngines struct { engines []*responseEngineMap } +func newResponseEngines() *responseEngines { + return &responseEngines{} +} + // add accepts a simple response engine with its content type or key, key should not contains a dot('.'). // if key is a content type then it's the content type, but if it not, set the content type from the returned function, // if it not called/changed then the default content type text/plain will be used. diff --git a/ssh.go b/ssh.go index 9e0081da..8360de89 100644 --- a/ssh.go +++ b/ssh.go @@ -428,6 +428,11 @@ type SSHServer struct { Logger *log.Logger // log.New(...)/ $qinstance.Logger, fill it when you want to receive debug and info/warnings messages } +// NewSSHServer returns a new empty SSHServer +func NewSSHServer() *SSHServer { + return &SSHServer{} +} + // Enabled returns true if SSH can be started, if Host != "" func (s *SSHServer) Enabled() bool { if s == nil { diff --git a/template.go b/template.go index 8122ebf0..7d6cdb6e 100644 --- a/template.go +++ b/template.go @@ -73,3 +73,31 @@ func (t *templateEngines) render(ctx *Context, filename string, binding interfac err = t.ExecuteWriter(out, filename, binding, options...) return err } + +// renderSource executes a template source raw contents (string) and write its result to the context's body +// note that gzip option is an iris dynamic option which exists for all template engines +// the gzip and charset options are built'n with iris +func (t *templateEngines) renderSource(ctx *Context, src string, binding interface{}, options ...map[string]interface{}) (err error) { + // we do all these because we don't want to initialize a new map for each execution... + gzipEnabled := ctx.framework.Config.Gzip + charset := ctx.framework.Config.Charset + if len(options) > 0 { + gzipEnabled = template.GetGzipOption(gzipEnabled, options[0]) + charset = template.GetCharsetOption(charset, options[0]) + } + + ctx.SetContentType(contentHTML + "; charset=" + charset) + + var out io.Writer + if gzipEnabled && ctx.clientAllowsGzip() { + ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader) + ctx.SetHeader(contentEncodingHeader, "gzip") + + gzipWriter := fs.AcquireGzipWriter(ctx.Response.BodyWriter()) + defer fs.ReleaseGzipWriter(gzipWriter) + out = gzipWriter + } else { + out = ctx.Response.BodyWriter() + } + return t.ExecuteRaw(src, out, binding) +} diff --git a/websocket.go b/websocket.go index 57d01c9c..cfac6ff9 100644 --- a/websocket.go +++ b/websocket.go @@ -1,86 +1,10 @@ package iris import ( - "log" - irisWebsocket "github.com/iris-contrib/websocket" "github.com/kataras/go-websocket" - "github.com/kataras/iris/config" ) -// --------------------------------------------------------------------------------------------------------- -// --------------------------------------------------------------------------------------------------------- -// --------------------------------Websocket implementation------------------------------------------------- -// Global functions in order to be able to use unlimitted number of websocket servers on each iris station-- -// --------------------------------------------------------------------------------------------------------- - -// Note I keep this code only to no change the front-end API, we could only use the go-websocket and set our custom upgrader - -// NewWebsocketServer creates a websocket server and returns it -func NewWebsocketServer(c *config.Websocket) *WebsocketServer { - wsConfig := websocket.Config{ - WriteTimeout: c.WriteTimeout, - PongTimeout: c.PongTimeout, - PingPeriod: c.PingPeriod, - MaxMessageSize: c.MaxMessageSize, - BinaryMessages: c.BinaryMessages, - ReadBufferSize: c.ReadBufferSize, - WriteBufferSize: c.WriteBufferSize, - } - - wsServer := websocket.New(wsConfig) - - upgrader := irisWebsocket.Custom(wsServer.HandleConnection, c.ReadBufferSize, c.WriteBufferSize, false) - - srv := &WebsocketServer{Server: wsServer, Config: c, upgrader: upgrader} - - return srv -} - -// RegisterWebsocketServer registers the handlers for the websocket server -// it's a bridge between station and websocket server -func RegisterWebsocketServer(station FrameworkAPI, server *WebsocketServer, logger *log.Logger) { - c := server.Config - if c.Endpoint == "" { - return - } - - websocketHandler := func(ctx *Context) { - - if err := server.Upgrade(ctx); err != nil { - if ctx.framework.Config.IsDevelopment { - logger.Printf("Websocket error while trying to Upgrade the connection. Trace: %s", err.Error()) - } - ctx.EmitError(StatusBadRequest) - } - } - - 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 { - if ctx.framework.Config.IsDevelopment { - logger.Printf("Websocket error while trying to Upgrade the connection. Trace: %s", err.Error()) - } - ctx.EmitError(StatusBadRequest) - } - } - } - clientSideLookupName := "iris-websocket-client-side" - station.Get(c.Endpoint, websocketHandler) - // check if client side already exists - if station.Lookup(clientSideLookupName) == nil { - // serve the client side on domain:port/iris-ws.js - station.StaticContent("/iris-ws.js", contentJavascript, websocket.ClientSource)(clientSideLookupName) - } - - // run the ws server - server.Serve() -} - // conversionals const ( // All is the string which the Emmiter use to send a message to all @@ -91,16 +15,38 @@ const ( Broadcast = websocket.Broadcast ) +// newUnderlineWsServer returns a new go-websocket.Server from configuration, used internaly by Iris. +func newUnderlineWsServer(c WebsocketConfiguration) websocket.Server { + wsConfig := websocket.Config{ + WriteTimeout: c.WriteTimeout, + PongTimeout: c.PongTimeout, + PingPeriod: c.PingPeriod, + MaxMessageSize: c.MaxMessageSize, + BinaryMessages: c.BinaryMessages, + ReadBufferSize: c.ReadBufferSize, + WriteBufferSize: c.WriteBufferSize, + } + + return websocket.New(wsConfig) +} + +// Note I keep this code only to no change the front-end API, we could only use the go-websocket and set our custom upgrader + type ( // WebsocketServer is the iris websocket server, expose the websocket.Server // the below code is a wrapper and bridge between iris-contrib/websocket and kataras/go-websocket WebsocketServer struct { websocket.Server - Config *config.Websocket + config WebsocketConfiguration upgrader irisWebsocket.Upgrader } ) +// NewWebsocketServer returns an empty WebsocketServer, nothing special here. +func NewWebsocketServer() *WebsocketServer { + return &WebsocketServer{} +} + // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // // The responseHeader is included in the response to the client's upgrade @@ -113,6 +59,30 @@ func (s *WebsocketServer) Upgrade(ctx *Context) error { return s.upgrader.Upgrade(ctx) } +// Handler is the iris Handler to upgrade the request +// used inside RegisterRoutes +func (s *WebsocketServer) Handler(ctx *Context) { + if err := s.Upgrade(ctx); err != nil { + if ctx.framework.Config.IsDevelopment { + ctx.Log("Websocket error while trying to Upgrade the connection. Trace: %s", err.Error()) + } + ctx.EmitError(StatusBadRequest) + } +} + +// RegisterTo creates the client side source route and the route path Endpoint with the correct Handler +// receives the websocket configuration and the iris station +func (s *WebsocketServer) RegisterTo(station *Framework, c WebsocketConfiguration) { + s.config = c // save the configuration, we will need that on the .OnConnection + clientSideLookupName := "iris-websocket-client-side" + station.Get(s.config.Endpoint, s.Handler) + // check if client side already exists + if station.Lookup(clientSideLookupName) == nil { + // serve the client side on domain:port/iris-ws.js + station.StaticContent("/iris-ws.js", contentJavascript, websocket.ClientSource)(clientSideLookupName) + } +} + // WebsocketConnection is the front-end API that you will use to communicate with the client side type WebsocketConnection interface { websocket.Connection @@ -120,6 +90,17 @@ type WebsocketConnection interface { // OnConnection this is the main event you, as developer, will work with each of the websocket connections func (s *WebsocketServer) OnConnection(connectionListener func(WebsocketConnection)) { + // let's initialize here the ws server, the user/dev is free to change its config before this step. + if s.Server == nil { + s.Server = newUnderlineWsServer(s.config) + } + + if s.upgrader.Receiver == nil { + s.upgrader = irisWebsocket.Custom(s.Server.HandleConnection, s.config.ReadBufferSize, s.config.WriteBufferSize, false) + // run the ws server + s.Server.Serve() + } + s.Server.OnConnection(func(c websocket.Connection) { connectionListener(c) })