diff --git a/HISTORY.md b/HISTORY.md index 608ef6e9..699bd9cb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,31 @@ **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`. +## 3.0.0-rc.4 -> 3.0.0-pre.release + +- `context.PostFormValue` -> `context.FormValueString`, old func stays until the next revision +- `context.PostFormMulti` -> `context.FormValues` , old func stays until the next revision + +- Added `context.VisitAllCookies(func(key,value string))` to visit all your cookies (because `context.Request.Header.VisitAllCookie` has a bug(I can't fix/pr it because the author is away atm)) +- Added `context.GetFlashes` to get all available flash messages for a particular request +- Fix flash message removed after the first `GetFlash` call in the same request + +**NEW FEATURE**: Built'n support for multi listening servers per iris station, secondary and virtual servers with one-line using the `iris.AddServer` & `iris.Go` to start all servers. + +- `iris.SecondaryListen` -> `iris.AddServer`, old func stays until the next revision +- Added `iris.Servers` with this field you can manage your servers very easy +- Added `iris.AddServer/iris.ListenTo/iris.Go`, but funcs like `Listen/ListenTLS/ListenUNIX` will stay forever +- Added `config.Server.Virtual(bool), config.Server.RedirectTo(string) and config.Server.MaxRequestBodySize(int64)` +- Added `iris.Available (channel bool)` +- `iris.HTTPServer` -> `iris.Servers.Main()` to get the main server, which is always the last registered server (if more than one used), old field removed +- `iris.Config.MaxRequestBodySize` -> `config.Server.MaxRequestBodySize`, old field removed + +**NEW FEATURE**: Build'n support for your API's end-to-end tests + +- Added `tester := iris.Tester(*testing.T)` , look inside: [http_test.go](https://github.com/kataras/iris/blob/master/http_test.go) & [./context_test.go](https://github.com/kataras/iris/blob/master/context_test.go) for `Tester` usage, you can also look inside the [httpexpect's repo](https://github.com/gavv/httpexpect/blob/master/example/iris_test.go) for extended examples with Iris. + + + ## 3.0.0-rc.3 -> 3.0.0-rc.4 **NEW FEATURE**: **Handlebars** template engine support with all Iris' view engine's functions/helpers support, as requested [here](https://github.com/kataras/iris/issues/239): diff --git a/README.md b/README.md index cff7b320..01ebcd90 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **v3.0.0-rc.4** +Current: **v3.0.0-pre.release** > Iris is an active project @@ -185,7 +185,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-v3.0.0--rc.4-blue.svg?style=flat-square +[Release Widget]: https://img.shields.io/badge/release-v3.0.0--pre.release-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/iris.go b/config/iris.go index c7d7310b..247356a2 100644 --- a/config/iris.go +++ b/config/iris.go @@ -1,15 +1,11 @@ package config -import ( - "github.com/imdario/mergo" - "github.com/valyala/fasthttp" -) +import "github.com/imdario/mergo" // Default values for base Iris conf const ( DefaultDisablePathCorrection = false DefaultDisablePathEscape = false - DefaultMaxRequestBodySize = fasthttp.DefaultMaxRequestBodySize ) type ( @@ -54,13 +50,6 @@ type ( // Default is false DisableBanner bool - // MaxRequestBodySize Maximum request body size. - // - // The server rejects requests with bodies exceeding this limit. - // - // By default request body size is 4MB. - MaxRequestBodySize int64 - // 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 @@ -103,13 +92,6 @@ type ( // Websocket contains the configs for Websocket's server integration Websocket *Websocket - // Server contains the configs for the http server - // Server configs are the only one which are setted inside base Iris package (from Listen, ListenTLS, ListenUNIX) NO from users - // - // this field is useful only when you need to READ which is the server's address, certfile & keyfile or unix's mode. - // - Server Server - // 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 Tester Tester } @@ -142,14 +124,12 @@ func Default() Iris { DisablePathCorrection: DefaultDisablePathCorrection, DisablePathEscape: DefaultDisablePathEscape, DisableBanner: false, - MaxRequestBodySize: DefaultMaxRequestBodySize, ProfilePath: "", Logger: DefaultLogger(), Sessions: DefaultSessions(), Render: DefaultRender(), Websocket: DefaultWebsocket(), - Server: DefaultServer(), - Tester: Tester{Debug: false}, + Tester: DefaultTester(), } } diff --git a/config/server.go b/config/server.go index b580fce5..29be03bc 100644 --- a/config/server.go +++ b/config/server.go @@ -5,13 +5,17 @@ import ( "strconv" "github.com/imdario/mergo" + "github.com/kataras/fasthttp" ) +// Default values for base Server conf const ( // DefaultServerHostname returns the default hostname which is 127.0.0.1 DefaultServerHostname = "127.0.0.1" // DefaultServerPort returns the default port which is 8080 DefaultServerPort = 8080 + // DefaultMaxRequestBodySize is 4MB + DefaultMaxRequestBodySize = fasthttp.DefaultMaxRequestBodySize ) var ( @@ -30,6 +34,12 @@ type Server struct { KeyFile string // 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 4MB. + MaxRequestBodySize int64 // 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 @@ -37,11 +47,14 @@ type Server struct { // // 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 } // DefaultServer returns the default configs for the server func DefaultServer() Server { - return Server{ListeningAddr: DefaultServerAddr} + return Server{ListeningAddr: DefaultServerAddr, + MaxRequestBodySize: DefaultMaxRequestBodySize} } // Merge merges the default with the given config and returns the result @@ -57,3 +70,12 @@ func (c Server) Merge(cfg []Server) (config Server) { 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 index 85f56f9f..c062bacf 100644 --- a/config/sessions.go +++ b/config/sessions.go @@ -34,7 +34,7 @@ type ( Redis struct { // Network "tcp" Network string - // Addr "127.0.01:6379" + // Addr "127.0.0.1:6379" Addr string // Password string .If no password then no 'AUTH'. Default "" Password string diff --git a/config/tester.go b/config/tester.go index 11488068..c933a366 100644 --- a/config/tester.go +++ b/config/tester.go @@ -2,5 +2,13 @@ package config // Tester configuration type Tester struct { - Debug bool + 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/context.go b/context.go index 201491c5..1136c1c6 100644 --- a/context.go +++ b/context.go @@ -1,6 +1,5 @@ /* -Context.go Implements: ./context/context.go , -files: context_renderer.go, context_storage.go, context_request.go, context_response.go +Context.go Implements: ./context/context.go */ package iris @@ -56,6 +55,9 @@ const ( stopExecutionPosition = 255 // used inside GetFlash to store the lifetime request flash messages flashMessagesStoreContextKey = "_iris_flash_messages_" + flashMessageCookiePrefix = "_iris_flash_message_" + cookieHeaderID = "Cookie: " + cookieHeaderIDLen = len(cookieHeaderID) ) // this pool is used everywhere needed in the iris for example inside party-> Static @@ -218,7 +220,7 @@ func (ctx *Context) HostString() string { func (ctx *Context) VirtualHostname() string { realhost := ctx.HostString() hostname := realhost - virtualhost := ctx.framework.HTTPServer.VirtualHostname() + virtualhost := ctx.framework.Servers.Main().VirtualHostname() if portIdx := strings.IndexByte(hostname, ':'); portIdx > 0 { hostname = hostname[0:portIdx] @@ -284,13 +286,13 @@ func (ctx *Context) RequestHeader(k string) string { return utils.BytesToString(ctx.RequestCtx.Request.Header.Peek(k)) } -// PostFormValue returns a single value from post request's data -func (ctx *Context) PostFormValue(name string) string { +// FormValueString returns a single value, as string, from post request's data +func (ctx *Context) FormValueString(name string) string { return string(ctx.FormValue(name)) } -// PostFormMulti returns a slice of string from post request's data -func (ctx *Context) PostFormMulti(name string) []string { +// FormValues returns a slice of string from post request's data +func (ctx *Context) FormValues(name string) []string { arrBytes := ctx.PostArgs().PeekMulti(name) arrStr := make([]string, len(arrBytes)) for i, v := range arrBytes { @@ -314,7 +316,7 @@ func (ctx *Context) Subdomain() (subdomain string) { // use it only for special cases, when the default behavior doesn't suits you. // // http://www.blooberry.com/indexdot/html/topics/urlencoding.htm -/* Credits to Manish Singh @kryptodev for URLEncode */ +/* Credits to Manish Singh @kryptodev for URLEncode by post issue share code */ func URLEncode(path string) string { if path == "" { return "" @@ -419,7 +421,7 @@ func (ctx *Context) SetHeader(k string, v string) { // first parameter is the url to redirect // second parameter is the http status should send, default is 302 (StatusFound), you can set it to 301 (Permant redirect), if that's nessecery func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) { - httpStatus := StatusFound // temporary redirect + httpStatus := StatusFound // a 'temporary-redirect-like' wich works better than for our purpose if statusHeader != nil && len(statusHeader) > 0 && statusHeader[0] > 0 { httpStatus = statusHeader[0] } @@ -480,7 +482,11 @@ func (ctx *Context) RenderWithStatus(status int, name string, binding interface{ // Render same as .RenderWithStatus but with status to iris.StatusOK (200) func (ctx *Context) Render(name string, binding interface{}, layout ...string) error { - return ctx.RenderWithStatus(StatusOK, name, binding, layout...) + errCode := ctx.RequestCtx.Response.StatusCode() + if errCode <= 0 { + errCode = StatusOK + } + return ctx.RenderWithStatus(errCode, name, binding, layout...) } // MustRender same as .Render but returns 500 internal server http status (error) if rendering fail @@ -687,6 +693,28 @@ func (ctx *Context) Set(key string, value interface{}) { ctx.RequestCtx.SetUserValue(key, value) } +// VisitAllCookies takes a visitor which loops on each (request's) cookie key and value +// +// Note: the method ctx.Request.Header.VisitAllCookie by fasthttp, has a strange bug which I cannot solve at the moment. +// This is the reason which this function exists and should be used instead of fasthttp's built'n. +func (ctx *Context) VisitAllCookies(visitor func(key string, value string)) { + // strange bug, this doesnt works also: cookieHeaderContent := ctx.Request.Header.Peek("Cookie")/User-Agent tested also + headerbody := string(ctx.Request.Header.Header()) + headerlines := strings.Split(headerbody, "\n") + for _, s := range headerlines { + if len(s) > cookieHeaderIDLen { + if s[0:cookieHeaderIDLen] == cookieHeaderID { + contents := s[cookieHeaderIDLen:] + values := strings.Split(contents, "; ") + for _, s := range values { + keyvalue := strings.SplitN(s, "=", 2) + visitor(keyvalue[0], keyvalue[1]) + } + } + } + } +} + // GetCookie returns cookie's value by it's name // returns empty string if nothing was found func (ctx *Context) GetCookie(name string) (val string) { @@ -719,19 +747,64 @@ func (ctx *Context) RemoveCookie(name string) { ctx.RequestCtx.Response.Header.DelClientCookie(name) } +// GetFlashes returns all the flash messages for available for this request +func (ctx *Context) GetFlashes() map[string]string { + // if already taken at least one time, this will be filled + if messages := ctx.Get(flashMessagesStoreContextKey); messages != nil { + if m, isMap := messages.(map[string]string); isMap { + return m + } + } else { + flashMessageFound := false + // else first time, get all flash cookie keys(the prefix will tell us which is a flash message), and after get all one-by-one using the GetFlash. + flashMessageCookiePrefixLen := len(flashMessageCookiePrefix) + ctx.VisitAllCookies(func(key string, value string) { + if len(key) > flashMessageCookiePrefixLen { + if key[0:flashMessageCookiePrefixLen] == flashMessageCookiePrefix { + unprefixedKey := key[flashMessageCookiePrefixLen:] + _, err := ctx.GetFlash(unprefixedKey) // this func will add to the list (flashMessagesStoreContextKey) also + if err == nil { + flashMessageFound = true + } + } + + } + }) + // if we found at least one flash message then re-execute this function to return the list + if flashMessageFound { + return ctx.GetFlashes() + } + } + return nil +} + +func (ctx *Context) decodeFlashCookie(key string) (string, string) { + cookieKey := flashMessageCookiePrefix + key + cookieValue := string(ctx.RequestCtx.Request.Header.Cookie(cookieKey)) + + if cookieValue != "" { + v, e := base64.URLEncoding.DecodeString(cookieValue) + if e == nil { + return cookieKey, string(v) + } + } + return "", "" +} + // GetFlash get a flash message by it's key // returns the value as string and an error // // if the cookie doesn't exists the string is empty and the error is filled // after the request's life the value is removed -func (ctx *Context) GetFlash(key string) (value string, err error) { +func (ctx *Context) GetFlash(key string) (string, error) { // first check if flash exists from this request's lifetime, if yes return that else continue to get the cookie storeExists := false + if messages := ctx.Get(flashMessagesStoreContextKey); messages != nil { m, isMap := messages.(map[string]string) if !isMap { - return "", fmt.Errorf("Messages request's store is not a map[string]string. This suppose will never happen, please report this bug.") + return "", fmt.Errorf("Flash store is not a map[string]string. This suppose will never happen, please report this bug.") } storeExists = true // in order to skip the check later for k, v := range m { @@ -741,38 +814,32 @@ func (ctx *Context) GetFlash(key string) (value string, err error) { } } - cookieValue := string(ctx.RequestCtx.Request.Header.Cookie(key)) - + cookieKey, cookieValue := ctx.decodeFlashCookie(key) if cookieValue == "" { - err = errFlashNotFound.Return() - } else { - v, e := base64.URLEncoding.DecodeString(cookieValue) - if e != nil { - return "", err - } - value = string(v) - // store this flash message to the lifetime request's local storage, - // I choose this method because no need to store it if not used at all - if storeExists { - ctx.Get(flashMessagesStoreContextKey).(map[string]string)[key] = value - } else { - flashStoreMap := make(map[string]string) - flashStoreMap[key] = value - ctx.Set(flashMessagesStoreContextKey, flashStoreMap) - } - - //remove the real cookie, no need to have that, we stored it on lifetime request - ctx.RemoveCookie(key) - //it should'b be removed until the next reload, so we don't do that: ctx.Request.Header.SetCookie(key, "") + return "", errFlashNotFound.Return() } - return + // store this flash message to the lifetime request's local storage, + // I choose this method because no need to store it if not used at all + if storeExists { + ctx.Get(flashMessagesStoreContextKey).(map[string]string)[key] = cookieValue + } else { + flashStoreMap := make(map[string]string) + flashStoreMap[key] = cookieValue + ctx.Set(flashMessagesStoreContextKey, flashStoreMap) + } + + //remove the real cookie, no need to have that, we stored it on lifetime request + ctx.RemoveCookie(cookieKey) + return cookieValue, nil + //it should'b be removed until the next reload, so we don't do that: ctx.Request.Header.SetCookie(key, "") + } // SetFlash sets a flash message, accepts 2 parameters the key(string) and the value(string) // the value will be available on the NEXT request func (ctx *Context) SetFlash(key string, value string) { c := fasthttp.AcquireCookie() - c.SetKey(key) + c.SetKey(flashMessageCookiePrefix + key) c.SetValue(base64.URLEncoding.EncodeToString([]byte(value))) c.SetPath("/") c.SetHTTPOnly(true) diff --git a/context/context.go b/context/context.go index 2116a83e..166455dc 100644 --- a/context/context.go +++ b/context/context.go @@ -14,6 +14,10 @@ type ( // IContext the interface for the iris/context // Used mostly inside packages which shouldn't be import ,directly, the kataras/iris. IContext interface { + // deprecated Start + PostFormValue(string) string + PostFormMulti(string) []string + // deprecated End Param(string) string ParamInt(string) (int, error) ParamInt64(string) (int64, error) @@ -29,8 +33,8 @@ type ( RequestIP() string RemoteAddr() string RequestHeader(k string) string - PostFormValue(string) string - PostFormMulti(string) []string + FormValueString(string) string + FormValues(string) []string SetStatusCode(int) SetContentType(string) SetHeader(string, string) @@ -66,9 +70,11 @@ type ( GetString(string) string GetInt(string) int Set(string, interface{}) + VisitAllCookies(func(string, string)) SetCookie(*fasthttp.Cookie) SetCookieKV(string, string) RemoveCookie(string) + GetFlashes() map[string]string GetFlash(string) (string, error) SetFlash(string, string) Session() store.IStore diff --git a/context_test.go b/context_test.go new file mode 100644 index 00000000..7045131a --- /dev/null +++ b/context_test.go @@ -0,0 +1,613 @@ +package iris + +/* +The most part of the context covered, +the other part contains serving static methods, +find remote ip, GetInt and the view engine rendering(templates) +I am not waiting unexpected behaviors from the rest of the funcs, +so that's all with context's tests. + +CONTRIBUTE & DISCUSSION ABOUT TESTS TO: https://github.com/iris-contrib/tests +*/ + +import ( + "encoding/xml" + "net/url" + "strconv" + "strings" + "testing" +) + +func TestContextReset(t *testing.T) { + var context Context + context.Params = PathParameters{PathParameter{Key: "testkey", Value: "testvalue"}} + context.Reset(nil) + if len(context.Params) > 0 { + t.Fatalf("Expecting to have %d params but got: %d", 0, len(context.Params)) + } +} + +func TestContextClone(t *testing.T) { + var context Context + context.Params = PathParameters{ + PathParameter{Key: "testkey", Value: "testvalue"}, + PathParameter{Key: "testkey2", Value: "testvalue2"}, + } + c := context.Clone() + if v := c.Param("testkey"); v != context.Param("testkey") { + t.Fatalf("Expecting to have parameter value: %s but got: %s", context.Param("testkey"), v) + } + if v := c.Param("testkey2"); v != context.Param("testkey2") { + t.Fatalf("Expecting to have parameter value: %s but got: %s", context.Param("testkey2"), v) + } +} + +func TestContextDoNextStop(t *testing.T) { + var context Context + ok := false + afterStop := false + context.middleware = Middleware{HandlerFunc(func(*Context) { + ok = true + }), HandlerFunc(func(*Context) { + ok = true + }), HandlerFunc(func(*Context) { + // this will never execute + afterStop = true + })} + context.Do() + if context.pos != 0 { + t.Fatalf("Expecting position 0 for context's middleware but we got: %d", context.pos) + } + if !ok { + t.Fatalf("Unexpected behavior, first context's middleware didn't executed") + } + ok = false + + context.Next() + + if int(context.pos) != 1 { + t.Fatalf("Expecting to have position %d but we got: %d", 1, context.pos) + } + if !ok { + t.Fatalf("Next context's middleware didn't executed") + } + + context.StopExecution() + if context.pos != stopExecutionPosition { + t.Fatalf("Context's StopExecution didn't worked, we expected to have position %d but we got %d", stopExecutionPosition, context.pos) + } + + if !context.IsStopped() { + t.Fatalf("Should be stopped") + } + + context.Next() + + if afterStop { + t.Fatalf("We stopped the execution but the next handler was executed") + } +} + +func TestContextParams(t *testing.T) { + var context Context + params := PathParameters{ + PathParameter{Key: "testkey", Value: "testvalue"}, + PathParameter{Key: "testkey2", Value: "testvalue2"}, + PathParameter{Key: "id", Value: "3"}, + PathParameter{Key: "bigint", Value: "548921854390354"}, + } + context.Params = params + + if v := context.Param(params[0].Key); v != params[0].Value { + t.Fatalf("Expecting parameter value to be %s but we got %s", params[0].Value, context.Param("testkey")) + } + if v := context.Param(params[1].Key); v != params[1].Value { + t.Fatalf("Expecting parameter value to be %s but we got %s", params[1].Value, context.Param("testkey2")) + } + + if len(context.Params) != len(params) { + t.Fatalf("Expecting to have %d parameters but we got %d", len(params), len(context.Params)) + } + + if vi, err := context.ParamInt(params[2].Key); err != nil { + t.Fatalf("Unexpecting error on context's ParamInt while trying to get the integer of the %s", params[2].Value) + } else if vi != 3 { + t.Fatalf("Expecting to receive %d but we got %d", 3, vi) + } + + if vi, err := context.ParamInt64(params[3].Key); err != nil { + t.Fatalf("Unexpecting error on context's ParamInt while trying to get the integer of the %s", params[2].Value) + } else if vi != 548921854390354 { + t.Fatalf("Expecting to receive %d but we got %d", 548921854390354, vi) + } + + // end-to-end test now, note that we will not test the whole mux here, this happens on http_test.go + + initDefault() + expectedParamsStr := "param1=myparam1,param2=myparam2,param3=myparam3afterstatic,anything=/andhere/anything/you/like" + Get("/path/:param1/:param2/staticpath/:param3/*anything", func(ctx *Context) { + paramsStr := ctx.Params.String() + ctx.Write(paramsStr) + }) + + Tester(t).GET("/path/myparam1/myparam2/staticpath/myparam3afterstatic/andhere/anything/you/like").Expect().Status(StatusOK).Body().Equal(expectedParamsStr) + +} + +func TestContextURLParams(t *testing.T) { + initDefault() + passedParams := map[string]string{"param1": "value1", "param2": "value2"} + Get("/", func(ctx *Context) { + params := ctx.URLParams() + ctx.JSON(StatusOK, params) + }) + e := Tester(t) + + e.GET("/").WithQueryObject(passedParams).Expect().Status(StatusOK).JSON().Equal(passedParams) +} + +// hoststring returns the full host, will return the HOST:IP +func TestContextHostString(t *testing.T) { + initDefault() + Config.Tester.ListeningAddr = "localhost:8080" + Get("/", func(ctx *Context) { + ctx.Write(ctx.HostString()) + }) + + Get("/wrong", func(ctx *Context) { + ctx.Write(ctx.HostString() + "w") + }) + + e := Tester(t) + e.GET("/").Expect().Status(StatusOK).Body().Equal(Config.Tester.ListeningAddr) + e.GET("/wrong").Expect().Body().NotEqual(Config.Tester.ListeningAddr) +} + +// VirtualHostname returns the hostname only, +// if the host starts with 127.0.0.1 or localhost it gives the registered hostname part of the listening addr +func TestContextVirtualHostName(t *testing.T) { + initDefault() + vhost := "mycustomvirtualname.com" + Config.Tester.ListeningAddr = vhost + ":8080" + Get("/", func(ctx *Context) { + ctx.Write(ctx.VirtualHostname()) + }) + + Get("/wrong", func(ctx *Context) { + ctx.Write(ctx.VirtualHostname() + "w") + }) + + e := Tester(t) + e.GET("/").Expect().Status(StatusOK).Body().Equal(vhost) + e.GET("/wrong").Expect().Body().NotEqual(vhost) +} + +func TestContextFormValueString(t *testing.T) { + initDefault() + var k, v string + k = "postkey" + v = "postvalue" + Post("/", func(ctx *Context) { + ctx.Write(k + "=" + ctx.FormValueString(k)) + }) + e := Tester(t) + + e.POST("/").WithFormField(k, v).Expect().Status(StatusOK).Body().Equal(k + "=" + v) +} + +func TestContextSubdomain(t *testing.T) { + initDefault() + Config.Tester.ListeningAddr = "mydomain.com:9999" + //Config.Tester.ExplicitURL = true + Party("mysubdomain.").Get("/mypath", func(ctx *Context) { + ctx.Write(ctx.Subdomain()) + }) + + e := Tester(t) + e.GET("/").WithURL("http://mysubdomain.mydomain.com:9999").Expect().Status(StatusNotFound) + e.GET("/mypath").WithURL("http://mysubdomain.mydomain.com:9999").Expect().Status(StatusOK).Body().Equal("mysubdomain") + + //e.GET("http://mysubdomain.mydomain.com:9999").Expect().Status(StatusNotFound) + //e.GET("http://mysubdomain.mydomain.com:9999/mypath").Expect().Status(StatusOK).Body().Equal("mysubdomain") +} + +type testBinderData struct { + Username string + Mail string + Data []string `form:"mydata" json:"mydata"` +} + +type testBinderXMLData struct { + XMLName xml.Name `xml:"info"` + FirstAttr string `xml:"first,attr"` + SecondAttr string `xml:"second,attr"` + Name string `xml:"name",json:"name"` + Birth string `xml:"birth",json:"birth"` + Stars int `xml:"stars",json:"stars"` +} + +func TestContextReadForm(t *testing.T) { + initDefault() + + Post("/form", func(ctx *Context) { + obj := testBinderData{} + err := ctx.ReadForm(&obj) + if err != nil { + t.Fatalf("Error when parsing the FORM: %s", err.Error()) + } + ctx.JSON(StatusOK, obj) + }) + + e := Tester(t) + + passed := map[string]interface{}{"Username": "myusername", "Mail": "mymail@iris-go.com", "mydata": url.Values{"[0]": []string{"mydata1"}, + "[1]": []string{"mydata2"}}} + + expectedObject := testBinderData{Username: "myusername", Mail: "mymail@iris-go.com", Data: []string{"mydata1", "mydata2"}} + + e.POST("/form").WithForm(passed).Expect().Status(StatusOK).JSON().Object().Equal(expectedObject) +} + +func TestContextReadJSON(t *testing.T) { + initDefault() + Post("/json", func(ctx *Context) { + obj := testBinderData{} + err := ctx.ReadJSON(&obj) + if err != nil { + t.Fatalf("Error when parsing the JSON body: %s", err.Error()) + } + ctx.JSON(StatusOK, obj) + }) + + e := Tester(t) + passed := map[string]interface{}{"Username": "myusername", "Mail": "mymail@iris-go.com", "mydata": []string{"mydata1", "mydata2"}} + expectedObject := testBinderData{Username: "myusername", Mail: "mymail@iris-go.com", Data: []string{"mydata1", "mydata2"}} + + e.POST("/json").WithJSON(passed).Expect().Status(StatusOK).JSON().Object().Equal(expectedObject) +} + +func TestContextReadXML(t *testing.T) { + initDefault() + + Post("/xml", func(ctx *Context) { + obj := testBinderXMLData{} + err := ctx.ReadXML(&obj) + if err != nil { + t.Fatalf("Error when parsing the XML body: %s", err.Error()) + } + ctx.XML(StatusOK, obj) + }) + + e := Tester(t) + expectedObj := testBinderXMLData{ + XMLName: xml.Name{Local: "info", Space: "info"}, + FirstAttr: "this is the first attr", + SecondAttr: "this is the second attr", + Name: "Iris web framework", + Birth: "13 March 2016", + Stars: 4064, + } + // so far no WithXML or .XML like WithJSON and .JSON on httpexpect I added a feature request as post issue and we're waiting + expectedBody := `<` + expectedObj.XMLName.Local + ` first="` + expectedObj.FirstAttr + `" second="` + expectedObj.SecondAttr + `">` + expectedObj.Name + `` + expectedObj.Birth + `` + strconv.Itoa(expectedObj.Stars) + `` + e.POST("/xml").WithText(expectedBody).Expect().Status(StatusOK).Body().Equal(expectedBody) +} + +// TestContextRedirectTo tests the named route redirect action +func TestContextRedirectTo(t *testing.T) { + initDefault() + h := func(ctx *Context) { ctx.Write(ctx.PathString()) } + Get("/mypath", h)("my-path") + Get("/mypostpath", h)("my-post-path") + Get("mypath/with/params/:param1/:param2", func(ctx *Context) { + if len(ctx.Params) != 2 { + t.Fatalf("Strange error, expecting parameters to be two but we got: %d", len(ctx.Params)) + } + ctx.Write(ctx.PathString()) + })("my-path-with-params") + + Get("/redirect/to/:routeName/*anyparams", func(ctx *Context) { + routeName := ctx.Param("routeName") + var args []interface{} + anyparams := ctx.Param("anyparams") + if anyparams != "" && anyparams != "/" { + params := strings.Split(anyparams[1:], "/") // firstparam/secondparam + for _, s := range params { + args = append(args, s) + } + } + //println("Redirecting to: " + routeName + " with path: " + Path(routeName, args...)) + ctx.RedirectTo(routeName, args...) + }) + + e := Tester(t) + + e.GET("/redirect/to/my-path/").Expect().Status(StatusOK).Body().Equal("/mypath") + e.GET("/redirect/to/my-post-path/").Expect().Status(StatusOK).Body().Equal("/mypostpath") + e.GET("/redirect/to/my-path-with-params/firstparam/secondparam").Expect().Status(StatusOK).Body().Equal("/mypath/with/params/firstparam/secondparam") +} + +func TestContextUserValues(t *testing.T) { + initDefault() + testCustomObjUserValue := struct{ Name string }{Name: "a name"} + values := map[string]interface{}{"key1": "value1", "key2": "value2", "key3": 3, "key4": testCustomObjUserValue, "key5": map[string]string{"key": "value"}} + + Get("/test", func(ctx *Context) { + + for k, v := range values { + ctx.Set(k, v) + } + + }, func(ctx *Context) { + for k, v := range values { + userValue := ctx.Get(k) + if userValue != v { + t.Fatalf("Expecting user value: %s to be equal with: %#v but got: %#v", k, v, userValue) + } + + if m, isMap := userValue.(map[string]string); isMap { + if m["key"] != v.(map[string]string)["key"] { + t.Fatalf("Expecting user value: %s to be equal with: %#v but got: %#v", k, v.(map[string]string)["key"], m["key"]) + } + } else { + if userValue != v { + t.Fatalf("Expecting user value: %s to be equal with: %#v but got: %#v", k, v, userValue) + } + } + + } + }) + + e := Tester(t) + + e.GET("/test").Expect().Status(StatusOK) + +} + +func TestContextFlashMessages(t *testing.T) { + initDefault() + firstKey := "name" + lastKey := "package" + + values := PathParameters{PathParameter{Key: firstKey, Value: "kataras"}, PathParameter{Key: lastKey, Value: "iris"}} + jsonExpected := map[string]string{firstKey: "kataras", lastKey: "iris"} + // set the flashes, the cookies are filled + Put("/set", func(ctx *Context) { + for _, v := range values { + ctx.SetFlash(v.Key, v.Value) + } + }) + + // get the first flash, the next should be avaiable to the next requess + Get("/get_first_flash", func(ctx *Context) { + for _, v := range values { + val, _ := ctx.GetFlash(v.Key) + ctx.JSON(StatusOK, map[string]string{v.Key: val}) + break + } + + }) + + // just an empty handler to test if the flashes should remeain to the next if GetFlash/GetFlashes used + Get("/get_no_getflash", func(ctx *Context) { + }) + + // get the last flash, the next should be avaiable to the next requess + Get("/get_last_flash", func(ctx *Context) { + for i, v := range values { + if i == len(values)-1 { + val, _ := ctx.GetFlash(v.Key) + ctx.JSON(StatusOK, map[string]string{v.Key: val}) + } + } + }) + + Get("/get_zero_flashes", func(ctx *Context) { + ctx.JSON(StatusOK, ctx.GetFlashes()) // should return nil + }) + + // we use the GetFlash to get the flash messages, the messages and the cookies should be empty after that + Get("/get_flash", func(ctx *Context) { + kv := make(map[string]string) + for _, v := range values { + val, err := ctx.GetFlash(v.Key) + if err == nil { + kv[v.Key] = val + } + } + ctx.JSON(StatusOK, kv) + }, func(ctx *Context) { + // at the same request, flashes should be available + if len(ctx.GetFlashes()) == 0 { + t.Fatalf("Flashes should be remeain to the whole request lifetime") + } + }) + + Get("/get_flashes", func(ctx *Context) { + // one time one handler, using GetFlashes + kv := make(map[string]string) + flashes := ctx.GetFlashes() + //second time on the same handler, using the GetFlash + for k := range flashes { + kv[k], _ = ctx.GetFlash(k) + } + if len(flashes) != len(kv) { + ctx.SetStatusCode(StatusNoContent) + return + } + ctx.Next() + + }, func(ctx *Context) { + // third time on a next handler + // test the if next handler has access to them(must) because flash are request lifetime now. + // print them to the client for test the response also + ctx.JSON(StatusOK, ctx.GetFlashes()) + }) + + e := Tester(t) + e.PUT("/set").Expect().Status(StatusOK).Cookies().NotEmpty() + e.GET("/get_first_flash").Expect().Status(StatusOK).JSON().Object().ContainsKey(firstKey).NotContainsKey(lastKey) + // just a request which does not use the flash message, so flash messages should be available on the next request + e.GET("/get_no_getflash").Expect().Status(StatusOK) + e.GET("/get_last_flash").Expect().Status(StatusOK).JSON().Object().ContainsKey(lastKey).NotContainsKey(firstKey) + g := e.GET("/get_zero_flashes").Expect().Status(StatusOK) + g.JSON().Null() + g.Cookies().Empty() + // set the magain + e.PUT("/set").Expect().Status(StatusOK).Cookies().NotEmpty() + // get them again using GetFlash + e.GET("/get_flash").Expect().Status(StatusOK).JSON().Object().Equal(jsonExpected) + // this should be empty again + g = e.GET("/get_zero_flashes").Expect().Status(StatusOK) + g.JSON().Null() + g.Cookies().Empty() + //set them again + e.PUT("/set").Expect().Status(StatusOK).Cookies().NotEmpty() + // get them again using GetFlashes + e.GET("/get_flashes").Expect().Status(StatusOK).JSON().Object().Equal(jsonExpected) + // this should be empty again + g = e.GET("/get_zero_flashes").Expect().Status(StatusOK) + g.JSON().Null() + g.Cookies().Empty() + +} + +func TestContextSessions(t *testing.T) { + t.Parallel() + values := map[string]interface{}{ + "Name": "iris", + "Months": "4", + "Secret": "dsads£2132215£%%Ssdsa", + } + + initDefault() + Config.Sessions.Cookie = "mycustomsessionid" + + writeValues := func(ctx *Context) { + sessValues := ctx.Session().GetAll() + ctx.JSON(StatusOK, sessValues) + } + + if testEnableSubdomain { + Party(testSubdomain+".").Get("/get", func(ctx *Context) { + writeValues(ctx) + }) + } + + Post("set", func(ctx *Context) { + vals := make(map[string]interface{}, 0) + if err := ctx.ReadJSON(&vals); err != nil { + t.Fatalf("Cannot readjson. Trace %s", err.Error()) + } + for k, v := range vals { + ctx.Session().Set(k, v) + } + }) + + Get("/get", func(ctx *Context) { + writeValues(ctx) + }) + + Get("/clear", func(ctx *Context) { + ctx.Session().Clear() + writeValues(ctx) + }) + + Get("/destroy", func(ctx *Context) { + ctx.SessionDestroy() + writeValues(ctx) + // the cookie and all values should be empty + }) + + e := Tester(t) + + e.POST("/set").WithJSON(values).Expect().Status(StatusOK).Cookies().NotEmpty() + e.GET("/get").Expect().Status(StatusOK).JSON().Object().Equal(values) + if testEnableSubdomain { + es := subdomainTester(e) + es.Request("GET", "/get").Expect().Status(StatusOK).JSON().Object().Equal(values) + } + + // test destory which also clears first + d := e.GET("/destroy").Expect().Status(StatusOK) + d.JSON().Object().Empty() + d.Cookies().ContainsOnly(Config.Sessions.Cookie) + // set and clear again + e.POST("/set").WithJSON(values).Expect().Status(StatusOK).Cookies().NotEmpty() + e.GET("/clear").Expect().Status(StatusOK).JSON().Object().Empty() +} + +type renderTestInformationType struct { + XMLName xml.Name `xml:"info"` + FirstAttr string `xml:"first,attr"` + SecondAttr string `xml:"second,attr"` + Name string `xml:"name",json:"name"` + Birth string `xml:"birth",json:"birth"` + Stars int `xml:"stars",json:"stars"` +} + +func TestContextRenderRest(t *testing.T) { + initDefault() + + dataContents := []byte("Some binary data here.") + textContents := "Plain text here" + JSONPContents := map[string]string{"hello": "jsonp"} + JSONPCallback := "callbackName" + JSONXMLContents := renderTestInformationType{ + XMLName: xml.Name{Local: "info", Space: "info"}, // only need to verify that later + FirstAttr: "this is the first attr", + SecondAttr: "this is the second attr", + Name: "Iris web framework", + Birth: "13 March 2016", + Stars: 4064, + } + markdownContents := "# Hello dynamic markdown from Iris" + + Get("/data", func(ctx *Context) { + ctx.Data(StatusOK, dataContents) + }) + + Get("/text", func(ctx *Context) { + ctx.Text(StatusOK, textContents) + }) + + Get("/jsonp", func(ctx *Context) { + ctx.JSONP(StatusOK, JSONPCallback, JSONPContents) + }) + + Get("/json", func(ctx *Context) { + ctx.JSON(StatusOK, JSONXMLContents) + }) + Get("/xml", func(ctx *Context) { + ctx.XML(StatusOK, JSONXMLContents) + }) + + Get("/markdown", func(ctx *Context) { + ctx.Markdown(StatusOK, markdownContents) + }) + + e := Tester(t) + dataT := e.GET("/data").Expect().Status(StatusOK) + dataT.Header("Content-Type").Equal("application/octet-stream") + dataT.Body().Equal(string(dataContents)) + + textT := e.GET("/text").Expect().Status(StatusOK) + textT.Header("Content-Type").Equal("text/plain; charset=UTF-8") + textT.Body().Equal(textContents) + + JSONPT := e.GET("/jsonp").Expect().Status(StatusOK) + JSONPT.Header("Content-Type").Equal("application/javascript; charset=UTF-8") + JSONPT.Body().Equal(JSONPCallback + `({"hello":"jsonp"});`) + + JSONT := e.GET("/json").Expect().Status(StatusOK) + JSONT.Header("Content-Type").Equal("application/json; charset=UTF-8") + JSONT.JSON().Object().Equal(JSONXMLContents) + + XMLT := e.GET("/xml").Expect().Status(StatusOK) + XMLT.Header("Content-Type").Equal("text/xml; charset=UTF-8") + XMLT.Body().Equal(`<` + JSONXMLContents.XMLName.Local + ` first="` + JSONXMLContents.FirstAttr + `" second="` + JSONXMLContents.SecondAttr + `">` + JSONXMLContents.Name + `` + JSONXMLContents.Birth + `` + strconv.Itoa(JSONXMLContents.Stars) + ``) + + markdownT := e.GET("/markdown").Expect().Status(StatusOK) + markdownT.Header("Content-Type").Equal("text/html; charset=UTF-8") + markdownT.Body().Equal("

" + markdownContents[2:] + "

\n") + +} diff --git a/deprecated.go b/deprecated.go new file mode 100644 index 00000000..4d24938e --- /dev/null +++ b/deprecated.go @@ -0,0 +1,82 @@ +package iris + +import "github.com/kataras/iris/config" + +/* Contains some different functions of context.go & iris.go which will be removed on the next revision */ + +// SecondaryListen same as .AddServer/.Servers.Add(config.Server) instead +// DEPRECATED: use .AddServer instead +// AddServers 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 +// 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 SecondaryListen(cfg config.Server) *Server { + return Default.SecondaryListen(cfg) +} + +// SecondaryListen same as .AddServer/.Servers.Add(config.Server) instead +// DEPRECATED: use .AddServer instead +// AddServers 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 +// 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) SecondaryListen(cfg config.Server) *Server { + return s.Servers.Add(cfg) +} + +// NoListen is useful only when you want to test Iris, it doesn't starts the server but it configures and returns it +// DEPRECATED: use ListenVirtual instead +// initializes the whole framework but server doesn't listens to a specific net.Listener +// it is not blocking the app +func NoListen(optionalAddr ...string) *Server { + return Default.NoListen(optionalAddr...) +} + +// NoListen is useful only when you want to test Iris, it doesn't starts the server but it configures and returns it +// DEPRECATED: use ListenVirtual instead +// initializes the whole framework but server doesn't listens to a specific net.Listener +// it is not blocking the app +func (s *Framework) NoListen(optionalAddr ...string) *Server { + return s.ListenVirtual(optionalAddr...) +} + +// CloseWithErr terminates all the registered servers and returns an error if any +// DEPRECATED: use Close instead, and if you want to panic on errors : iris.Must(iris.Close()) +// if you want to panic on this error use the iris.Must(iris.Close()) +func CloseWithErr() error { + return Default.Close() +} + +// CloseWithErr terminates all the registered servers and returns an error if any +// DEPRECATED: use Close instead, and if you want to panic on errors : iris.Must(iris.Close()) +// if you want to panic on this error use the iris.Must(iris.Close()) +func (s *Framework) CloseWithErr() error { + return s.Close() +} + +// PostFormMulti returns a slice of string from post request's data +// DEPRECATED: Plase use FormValues instead +func (ctx *Context) PostFormMulti(name string) []string { + return ctx.FormValues(name) +} + +// PostFormValue This will be deprecated +///DEPRECATED: please use FormValueString instead +// PostFormValue returns a single value from post request's data +func (ctx *Context) PostFormValue(name string) string { + return ctx.FormValueString(name) +} diff --git a/http.go b/http.go index 6d72aaa5..66011974 100644 --- a/http.go +++ b/http.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/iris-contrib/errors" "github.com/kataras/iris/config" @@ -236,35 +237,47 @@ var ( errServerChmod = errors.New("Cannot chmod %#o for %q: %s") ) -// Server the http server -type Server struct { - *fasthttp.Server - listener net.Listener - Config *config.Server - tls bool - mu sync.Mutex -} +type ( + // Server the http server + Server struct { + *fasthttp.Server + listener net.Listener + Config config.Server + tls bool + mu sync.Mutex + } + // ServerList contains the servers connected to the Iris station + ServerList struct { + mux *serveMux + servers []*Server + } +) // newServer returns a pointer to a Server object, and set it's options if any, nothing more -func newServer(c *config.Server) *Server { - s := &Server{Server: &fasthttp.Server{Name: config.ServerName}, Config: c} +func newServer(cfg config.Server) *Server { + s := &Server{Server: &fasthttp.Server{Name: config.ServerName}, Config: cfg} return s } -// SetHandler sets the handler in order to listen on client requests -func (s *Server) SetHandler(mux *serveMux) { - if s.Server != nil { - s.Server.Handler = mux.ServeRequest() - } -} - // IsListening returns true if server is listening/started, otherwise false func (s *Server) IsListening() bool { + if s == nil { + return false + } s.mu.Lock() defer s.mu.Unlock() return s.listener != nil && s.listener.Addr().String() != "" } +// IsOpened checks if handler is not nil and returns true if not, otherwise false +// this is used to see if a server has opened, use IsListening if you want to see if the server is actually ready to serve connections +func (s *Server) IsOpened() bool { + if s == nil { + return false + } + return s.Server != nil && s.Server.Handler != nil +} + // IsSecure returns true if server uses TLS, otherwise false func (s *Server) IsSecure() bool { return s.tls @@ -398,7 +411,11 @@ func (s *Server) serve(l net.Listener) error { } // Open opens/starts/runs/listens (to) the server, listen tls if Cert && Key is registed, listenUNIX if Mode is registed, otherwise listen -func (s *Server) Open() error { +func (s *Server) Open(h fasthttp.RequestHandler) error { + if h == nil { + return errServerHandlerMissing.Return() + } + if s.IsListening() { return errServerAlreadyStarted.Return() } @@ -407,10 +424,6 @@ func (s *Server) Open() error { return errServerConfigMissing.Return() } - if s.Handler == nil { - return errServerHandlerMissing.Return() - } - // check the addr if :8080 do it 0.0.0.0:8080 ,we need the hostname for many cases a := s.Config.ListeningAddr //check if contains hostname, we need the full host, :8080 should be : 127.0.0.1:8080 @@ -419,9 +432,13 @@ func (s *Server) Open() error { s.Config.ListeningAddr = config.DefaultServerHostname + a } + if s.Config.MaxRequestBodySize > config.DefaultMaxRequestBodySize { + s.Server.MaxRequestBodySize = int(s.Config.MaxRequestBodySize) + } + if s.Config.RedirectTo != "" { // override the handler and redirect all requests to this addr - s.Handler = func(reqCtx *fasthttp.RequestCtx) { + s.Server.Handler = func(reqCtx *fasthttp.RequestCtx) { path := string(reqCtx.Path()) redirectTo := s.Config.RedirectTo if path != "/" { @@ -429,12 +446,19 @@ func (s *Server) Open() error { } reqCtx.Redirect(redirectTo, StatusMovedPermanently) } + } else { + s.Server.Handler = h + } + + if s.Config.Virtual { + return nil } if s.Config.Mode > 0 { return s.listenUNIX() } return s.listen() + } // Close terminates the server @@ -447,6 +471,122 @@ func (s *Server) Close() (err error) { return } +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------ServerList implementation----------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +// 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) + s.servers = append(s.servers, srv) + return srv +} + +// Len returns the size of the server list +func (s *ServerList) Len() int { + return len(s.servers) +} + +// Main returns the main server, +// the last added server is the main server, even if's Virtual +func (s *ServerList) Main() (srv *Server) { + l := len(s.servers) - 1 + for i := range s.servers { + if i == l { + return s.servers[i] + } + } + return nil +} + +// Get returns the server by it's registered Address +func (s *ServerList) Get(addr string) (srv *Server) { + for i := range s.servers { + srv = s.servers[i] + if srv.Config.ListeningAddr == addr { + return + } + } + return +} + +// GetAll returns all registered servers +func (s *ServerList) GetAll() []*Server { + return s.servers +} + +// GetByIndex returns a server from the list by it's index +func (s *ServerList) GetByIndex(i int) *Server { + if len(s.servers) >= i+1 { + return s.servers[i] + } + return nil +} + +// Remove deletes a server by it's registered Address +// returns true if something was removed, otherwise returns false +func (s *ServerList) Remove(addr string) bool { + servers := s.servers + for i := range servers { + srv := servers[i] + if srv.Config.ListeningAddr == addr { + copy(servers[i:], servers[i+1:]) + servers[len(servers)-1] = nil + s.servers = servers[:len(servers)-1] + return true + } + } + return false +} + +// CloseAll terminates all listening servers +// returns the first error, if erro happens it continues to closes the rest of the servers +func (s *ServerList) CloseAll() (err error) { + for i := range s.servers { + if err == nil { + err = s.servers[i].Close() + } + } + return +} + +// OpenAll starts all servers +// returns the first error happens to one of these servers +// if one server gets error it closes the previous servers and exits from this process +func (s *ServerList) OpenAll() error { + l := len(s.servers) - 1 + h := s.mux.ServeRequest() + for i := range s.servers { + + if err := s.servers[i].Open(h); err != nil { + time.Sleep(2 * time.Second) + // for any case, + // we don't care about performance on initialization, + // we must make sure that the previous servers are running before closing them + s.CloseAll() + break + } + if i == l { + s.mux.setHostname(s.servers[i].VirtualHostname()) + } + + } + return nil +} + +// GetAllOpened returns all opened/started servers +func (s *ServerList) GetAllOpened() (servers []*Server) { + for i := range s.servers { + if s.servers[i].IsOpened() { + servers = append(servers, s.servers[i]) + } + } + return +} + // errHandler returns na error with message: 'Passed argument is not func(*Context) neither an object which implements the iris.Handler with Serve(ctx *Context) // It seems to be a +type Points to: +pointer.' var errHandler = errors.New("Passed argument is not func(*Context) neither an object which implements the iris.Handler with Serve(ctx *Context)\n It seems to be a %T Points to: %v.") diff --git a/http_test.go b/http_test.go new file mode 100644 index 00000000..e99f45cf --- /dev/null +++ b/http_test.go @@ -0,0 +1,529 @@ +package iris + +/* +This is the part we only care, these are end-to-end tests for the mux(router) and the server, the whole http file is made for these reasons only, so these tests are enough I think. + +CONTRIBUTE & DISCUSSION ABOUT TESTS TO: https://github.com/iris-contrib/tests +*/ + +import ( + "fmt" + "io/ioutil" + "os" + "strconv" + "testing" + "time" + + "github.com/gavv/httpexpect" + "github.com/kataras/iris/config" +) + +const ( + testTLSCert = `-----BEGIN CERTIFICATE----- + MIIDAzCCAeugAwIBAgIJAPDsxtKV4v3uMA0GCSqGSIb3DQEBBQUAMBgxFjAUBgNV + BAMMDTEyNy4wLjAuMTo0NDMwHhcNMTYwNjI5MTMxMjU4WhcNMjYwNjI3MTMxMjU4 + WjAYMRYwFAYDVQQDDA0xMjcuMC4wLjE6NDQzMIIBIjANBgkqhkiG9w0BAQEFAAOC + AQ8AMIIBCgKCAQEA0KtAOHKrcbLwWJXgRX7XSFyu4HHHpSty4bliv8ET4sLJpbZH + XeVX05Foex7PnrurDP6e+0H5TgqqcpQM17/ZlFcyKrJcHSCgV0ZDB3Sb8RLQSLns + 8a+MOSbn1WZ7TkC7d/cWlKmasQRHQ2V/cWlGooyKNEPoGaEz8MbY0wn2spyIJwsB + dciERC6317VTXbiZdoD8QbAsT+tBvEHM2m2A7B7PQmHNehtyFNbSV5uZNodvv1uv + ZTnDa6IqpjFLb1b2HNFgwmaVPmmkLuy1l9PN+o6/DUnXKKBrfPAx4JOlqTKEQpWs + pnfacTE3sWkkmOSSFltAXfkXIJFKdS/hy5J/KQIDAQABo1AwTjAdBgNVHQ4EFgQU + zr1df/c9+NyTpmyiQO8g3a8NswYwHwYDVR0jBBgwFoAUzr1df/c9+NyTpmyiQO8g + 3a8NswYwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEACG5shtMSDgCd + MNjOF+YmD+PX3Wy9J9zehgaDJ1K1oDvBbQFl7EOJl8lRMWITSws22Wxwh8UXVibL + sscKBp14dR3e7DdbwCVIX/JyrJyOaCfy2nNBdf1B06jYFsIHvP3vtBAb9bPNOTBQ + QE0Ztu9kCqgsmu0//sHuBEeA3d3E7wvDhlqRSxTLcLtgC1NSgkFvBw0JvwgpkX6s + M5WpSBZwZv8qpplxhFfqNy8Uf+xrpSW0pGfkHumehkQGC6/Ry7raganS0aHhDPK9 + Z1bEJ2com1bFFAQsm9yIXrRVMGGCtihB2Au0Q4jpEjUbzWYM+ItZyvRAGRM6Qex6 + s/jogMeRsw== + -----END CERTIFICATE----- +` + testTLSKey = `-----BEGIN RSA PRIVATE KEY----- + MIIEpQIBAAKCAQEA0KtAOHKrcbLwWJXgRX7XSFyu4HHHpSty4bliv8ET4sLJpbZH + XeVX05Foex7PnrurDP6e+0H5TgqqcpQM17/ZlFcyKrJcHSCgV0ZDB3Sb8RLQSLns + 8a+MOSbn1WZ7TkC7d/cWlKmasQRHQ2V/cWlGooyKNEPoGaEz8MbY0wn2spyIJwsB + dciERC6317VTXbiZdoD8QbAsT+tBvEHM2m2A7B7PQmHNehtyFNbSV5uZNodvv1uv + ZTnDa6IqpjFLb1b2HNFgwmaVPmmkLuy1l9PN+o6/DUnXKKBrfPAx4JOlqTKEQpWs + pnfacTE3sWkkmOSSFltAXfkXIJFKdS/hy5J/KQIDAQABAoIBAQDCd+bo9I0s8Fun + 4z3Y5oYSDTZ5O/CY0O5GyXPrSzCSM4Cj7EWEj1mTdb9Ohv9tam7WNHHLrcd+4NfK + 4ok5hLVs1vqM6h6IksB7taKATz+Jo0PzkzrsXvMqzERhEBo4aoGMIv2rXIkrEdas + S+pCsp8+nAWtAeBMCn0Slu65d16vQxwgfod6YZfvMKbvfhOIOShl9ejQ+JxVZcMw + Ti8sgvYmFUrdrEH3nCgptARwbx4QwlHGaw/cLGHdepfFsVaNQsEzc7m61fSO70m4 + NYJv48ZgjOooF5AccbEcQW9IxxikwNc+wpFYy5vDGzrBwS5zLZQFpoyMWFhtWdjx + hbmNn1jlAoGBAPs0ZjqsfDrH5ja4dQIdu5ErOccsmoHfMMBftMRqNG5neQXEmoLc + Uz8WeQ/QDf302aTua6E9iSjd7gglbFukVwMndQ1Q8Rwxz10jkXfiE32lFnqK0csx + ltruU6hOeSGSJhtGWBuNrT93G2lmy23fSG6BqOzdU4rn/2GPXy5zaxM/AoGBANSm + /E96RcBUiI6rDVqKhY+7M1yjLB41JrErL9a0Qfa6kYnaXMr84pOqVN11IjhNNTgl + g1lwxlpXZcZh7rYu9b7EEMdiWrJDQV7OxLDHopqUWkQ+3MHwqs6CxchyCq7kv9Df + IKqat7Me6Cyeo0MqcW+UMxlCRBxKQ9jqC7hDfZuXAoGBAJmyS8ImerP0TtS4M08i + JfsCOY21qqs/hbKOXCm42W+be56d1fOvHngBJf0YzRbO0sNo5Q14ew04DEWLsCq5 + +EsDv0hwd7VKfJd+BakV99ruQTyk5wutwaEeJK1bph12MD6L4aiqHJAyLeFldZ45 + +TUzu8mA+XaJz+U/NXtUPvU9AoGBALtl9M+tdy6I0Fa50ujJTe5eEGNAwK5WNKTI + 5D2XWNIvk/Yh4shXlux+nI8UnHV1RMMX++qkAYi3oE71GsKeG55jdk3fFQInVsJQ + APGw3FDRD8M4ip62ki+u+tEr/tIlcAyHtWfjNKO7RuubWVDlZFXqCiXmSdOMdsH/ + bxiREW49AoGACWev/eOzBoQJCRN6EvU2OV0s3b6f1QsPvcaH0zc6bgbBFOGmJU8v + pXhD88tsu9exptLkGVoYZjR0n0QT/2Kkyu93jVDW/80P7VCz8DKYyAJDa4CVwZxO + MlobQSunSDKx/CCJhWkbytCyh1bngAtwSAYLXavYIlJbAzx6FvtAIw4= + -----END RSA PRIVATE KEY----- +` +) + +// Contains the server test for multi running servers +func TestMultiRunningServers_v1(t *testing.T) { + host := "mydomain.com:443" // you have to add it to your hosts file( for windows, as 127.0.0.1 mydomain.com) + initDefault() + Config.DisableBanner = true + // create the key and cert files on the fly, and delete them when this test finished + certFile, ferr := ioutil.TempFile("", "cert") + + if ferr != nil { + t.Fatal(ferr.Error()) + } + + keyFile, ferr := ioutil.TempFile("", "key") + if ferr != nil { + t.Fatal(ferr.Error()) + } + + certFile.WriteString(testTLSCert) + keyFile.WriteString(testTLSKey) + + defer func() { + certFile.Close() + time.Sleep(350 * time.Millisecond) + os.Remove(certFile.Name()) + + keyFile.Close() + time.Sleep(350 * time.Millisecond) + os.Remove(keyFile.Name()) + }() + + Get("/", func(ctx *Context) { + ctx.Write("Hello from %s", ctx.HostString()) + }) + + // start the secondary server + SecondaryListen(config.Server{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}) + // prepare test framework + if ok := <-Available; !ok { + t.Fatal("Unexpected error: server cannot start, please report this as bug!!") + } + + e := Tester(t) + + e.Request("GET", "http://mydomain.com:80").Expect().Status(StatusOK).Body().Equal("Hello from " + host) + e.Request("GET", "https://"+host).Expect().Status(StatusOK).Body().Equal("Hello from " + host) + +} + +// Contains the server test for multi running servers +func TestMultiRunningServers_v2(t *testing.T) { + domain := "mydomain.com" + host := domain + ":443" + initDefault() + Config.DisableBanner = true + Config.Tester.ListeningAddr = host + // create the key and cert files on the fly, and delete them when this test finished + certFile, ferr := ioutil.TempFile("", "cert") + + if ferr != nil { + t.Fatal(ferr.Error()) + } + + keyFile, ferr := ioutil.TempFile("", "key") + if ferr != nil { + t.Fatal(ferr.Error()) + } + + certFile.WriteString(testTLSCert) + keyFile.WriteString(testTLSKey) + + defer func() { + certFile.Close() + time.Sleep(350 * time.Millisecond) + os.Remove(certFile.Name()) + + keyFile.Close() + time.Sleep(350 * time.Millisecond) + os.Remove(keyFile.Name()) + }() + + Get("/", func(ctx *Context) { + ctx.Write("Hello from %s", ctx.HostString()) + }) + + // add a secondary server + Servers.Add(config.Server{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}) + + go Go() + + // prepare test framework + if ok := <-Available; !ok { + t.Fatal("Unexpected error: server cannot start, please report this as bug!!") + } + + e := Tester(t) + + e.Request("GET", "http://"+domain+":80").Expect().Status(StatusOK).Body().Equal("Hello from " + host) + e.Request("GET", "https://"+host).Expect().Status(StatusOK).Body().Equal("Hello from " + host) + +} + +const ( + testEnableSubdomain = false + testSubdomain = "mysubdomain.com" +) + +func testSubdomainHost() string { + return testSubdomain + strconv.Itoa(Servers.Main().Port()) +} + +func testSubdomainURL() (subdomainURL string) { + subdomainHost := testSubdomainHost() + if Servers.Main().IsSecure() { + subdomainURL = "https://" + subdomainHost + } else { + subdomainURL = "http://" + subdomainHost + } + return +} + +func subdomainTester(e *httpexpect.Expect) *httpexpect.Expect { + es := e.Builder(func(req *httpexpect.Request) { + req.WithURL(testSubdomainURL()) + }) + return es +} + +type param struct { + Key string + Value string +} + +type testRoute struct { + Method string + Path string + RequestPath string + RequestQuery string + Body string + Status int + Register bool + Params []param + URLParams []param +} + +func TestMuxSimple(t *testing.T) { + testRoutes := []testRoute{ + // FOUND - registed + {"GET", "/test_get", "/test_get", "", "hello, get!", 200, true, nil, nil}, + {"POST", "/test_post", "/test_post", "", "hello, post!", 200, true, nil, nil}, + {"PUT", "/test_put", "/test_put", "", "hello, put!", 200, true, nil, nil}, + {"DELETE", "/test_delete", "/test_delete", "", "hello, delete!", 200, true, nil, nil}, + {"HEAD", "/test_head", "/test_head", "", "hello, head!", 200, true, nil, nil}, + {"OPTIONS", "/test_options", "/test_options", "", "hello, options!", 200, true, nil, nil}, + {"CONNECT", "/test_connect", "/test_connect", "", "hello, connect!", 200, true, nil, nil}, + {"PATCH", "/test_patch", "/test_patch", "", "hello, patch!", 200, true, nil, nil}, + {"TRACE", "/test_trace", "/test_trace", "", "hello, trace!", 200, true, nil, nil}, + // NOT FOUND - not registed + {"GET", "/test_get_nofound", "/test_get_nofound", "", "Not Found", 404, false, nil, nil}, + {"POST", "/test_post_nofound", "/test_post_nofound", "", "Not Found", 404, false, nil, nil}, + {"PUT", "/test_put_nofound", "/test_put_nofound", "", "Not Found", 404, false, nil, nil}, + {"DELETE", "/test_delete_nofound", "/test_delete_nofound", "", "Not Found", 404, false, nil, nil}, + {"HEAD", "/test_head_nofound", "/test_head_nofound", "", "Not Found", 404, false, nil, nil}, + {"OPTIONS", "/test_options_nofound", "/test_options_nofound", "", "Not Found", 404, false, nil, nil}, + {"CONNECT", "/test_connect_nofound", "/test_connect_nofound", "", "Not Found", 404, false, nil, nil}, + {"PATCH", "/test_patch_nofound", "/test_patch_nofound", "", "Not Found", 404, false, nil, nil}, + {"TRACE", "/test_trace_nofound", "/test_trace_nofound", "", "Not Found", 404, false, nil, nil}, + // Parameters + {"GET", "/test_get_parameter1/:name", "/test_get_parameter1/iris", "", "name=iris", 200, true, []param{{"name", "iris"}}, nil}, + {"GET", "/test_get_parameter2/:name/details/:something", "/test_get_parameter2/iris/details/anything", "", "name=iris,something=anything", 200, true, []param{{"name", "iris"}, {"something", "anything"}}, nil}, + {"GET", "/test_get_parameter2/:name/details/:something/*else", "/test_get_parameter2/iris/details/anything/elsehere", "", "name=iris,something=anything,else=/elsehere", 200, true, []param{{"name", "iris"}, {"something", "anything"}, {"else", "elsehere"}}, nil}, + // URL Parameters + {"GET", "/test_get_urlparameter1/first", "/test_get_urlparameter1/first", "name=irisurl", "name=irisurl", 200, true, nil, []param{{"name", "irisurl"}}}, + {"GET", "/test_get_urlparameter2/second", "/test_get_urlparameter2/second", "name=irisurl&something=anything", "name=irisurl,something=anything", 200, true, nil, []param{{"name", "irisurl"}, {"something", "anything"}}}, + {"GET", "/test_get_urlparameter2/first/second/third", "/test_get_urlparameter2/first/second/third", "name=irisurl&something=anything&else=elsehere", "name=irisurl,something=anything,else=elsehere", 200, true, nil, []param{{"name", "irisurl"}, {"something", "anything"}, {"else", "elsehere"}}}, + } + + initDefault() + + for idx := range testRoutes { + r := testRoutes[idx] + if r.Register { + HandleFunc(r.Method, r.Path, func(ctx *Context) { + ctx.SetStatusCode(r.Status) + if r.Params != nil && len(r.Params) > 0 { + ctx.SetBodyString(ctx.Params.String()) + } else if r.URLParams != nil && len(r.URLParams) > 0 { + if len(r.URLParams) != len(ctx.URLParams()) { + t.Fatalf("Error when comparing length of url parameters %d != %d", len(r.URLParams), len(ctx.URLParams())) + } + paramsKeyVal := "" + for idxp, p := range r.URLParams { + val := ctx.URLParam(p.Key) + paramsKeyVal += p.Key + "=" + val + "," + if idxp == len(r.URLParams)-1 { + paramsKeyVal = paramsKeyVal[0 : len(paramsKeyVal)-1] + } + } + ctx.SetBodyString(paramsKeyVal) + } else { + ctx.SetBodyString(r.Body) + } + + }) + } + } + + e := Tester(t) + + // run the tests (1) + for idx := range testRoutes { + r := testRoutes[idx] + e.Request(r.Method, r.RequestPath).WithQueryString(r.RequestQuery). + Expect(). + Status(r.Status).Body().Equal(r.Body) + } + +} + +func TestMuxSimpleParty(t *testing.T) { + + initDefault() + + h := func(c *Context) { c.WriteString(c.HostString() + c.PathString()) } + + if testEnableSubdomain { + subdomainParty := Party(testSubdomain + ".") + { + subdomainParty.Get("/", h) + subdomainParty.Get("/path1", h) + subdomainParty.Get("/path2", h) + subdomainParty.Get("/namedpath/:param1/something/:param2", h) + subdomainParty.Get("/namedpath/:param1/something/:param2/else", h) + } + } + + // simple + p := Party("/party1") + { + p.Get("/", h) + p.Get("/path1", h) + p.Get("/path2", h) + p.Get("/namedpath/:param1/something/:param2", h) + p.Get("/namedpath/:param1/something/:param2/else", h) + } + + e := Tester(t) + + request := func(reqPath string) { + e.Request("GET", reqPath). + Expect(). + Status(StatusOK).Body().Equal(Servers.Main().Host() + reqPath) + } + + // run the tests + request("/party1/") + request("/party1/path1") + request("/party1/path2") + request("/party1/namedpath/theparam1/something/theparam2") + request("/party1/namedpath/theparam1/something/theparam2/else") + + if testEnableSubdomain { + es := subdomainTester(e) + subdomainRequest := func(reqPath string) { + es.Request("GET", reqPath). + Expect(). + Status(StatusOK).Body().Equal(testSubdomainHost() + reqPath) + } + + subdomainRequest("/") + subdomainRequest("/path1") + subdomainRequest("/path2") + subdomainRequest("/namedpath/theparam1/something/theparam2") + subdomainRequest("/namedpath/theparam1/something/theparam2/else") + } +} + +func TestMuxPathEscape(t *testing.T) { + initDefault() + + Get("/details/:name", func(ctx *Context) { + name := ctx.Param("name") + highlight := ctx.URLParam("highlight") + ctx.Text(StatusOK, fmt.Sprintf("name=%s,highlight=%s", name, highlight)) + }) + + e := Tester(t) + + e.GET("/details/Sakamoto desu ga"). + WithQuery("highlight", "text"). + Expect().Status(StatusOK).Body().Equal("name=Sakamoto desu ga,highlight=text") +} + +func TestMuxCustomErrors(t *testing.T) { + var ( + notFoundMessage = "Iris custom message for 404 not found" + internalServerMessage = "Iris custom message for 500 internal server error" + testRoutesCustomErrors = []testRoute{ + // NOT FOUND CUSTOM ERRORS - not registed + {"GET", "/test_get_nofound_custom", "/test_get_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"POST", "/test_post_nofound_custom", "/test_post_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"PUT", "/test_put_nofound_custom", "/test_put_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"DELETE", "/test_delete_nofound_custom", "/test_delete_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"HEAD", "/test_head_nofound_custom", "/test_head_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"OPTIONS", "/test_options_nofound_custom", "/test_options_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"CONNECT", "/test_connect_nofound_custom", "/test_connect_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"PATCH", "/test_patch_nofound_custom", "/test_patch_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + {"TRACE", "/test_trace_nofound_custom", "/test_trace_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, + // SERVER INTERNAL ERROR 500 PANIC CUSTOM ERRORS - registed + {"GET", "/test_get_panic_custom", "/test_get_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"POST", "/test_post_panic_custom", "/test_post_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"PUT", "/test_put_panic_custom", "/test_put_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"DELETE", "/test_delete_panic_custom", "/test_delete_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"HEAD", "/test_head_panic_custom", "/test_head_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"OPTIONS", "/test_options_panic_custom", "/test_options_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"CONNECT", "/test_connect_panic_custom", "/test_connect_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"PATCH", "/test_patch_panic_custom", "/test_patch_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + {"TRACE", "/test_trace_panic_custom", "/test_trace_panic_custom", "", internalServerMessage, 500, true, nil, nil}, + } + ) + initDefault() + // first register the testRoutes needed + for _, r := range testRoutesCustomErrors { + if r.Register { + HandleFunc(r.Method, r.Path, func(ctx *Context) { + ctx.EmitError(r.Status) + }) + } + } + + // register the custom errors + OnError(404, func(ctx *Context) { + ctx.Write("%s", notFoundMessage) + }) + + OnError(500, func(ctx *Context) { + ctx.Write("%s", internalServerMessage) + }) + + // create httpexpect instance that will call fasthtpp.RequestHandler directly + e := Tester(t) + + // run the tests + for _, r := range testRoutesCustomErrors { + e.Request(r.Method, r.RequestPath). + Expect(). + Status(r.Status).Body().Equal(r.Body) + } +} + +type testUserAPI struct { + *Context +} + +// GET /users +func (u testUserAPI) Get() { + u.Write("Get Users\n") +} + +// GET /users/:param1 which its value passed to the id argument +func (u testUserAPI) GetBy(id string) { // id equals to u.Param("param1") + u.Write("Get By %s\n", id) +} + +// PUT /users +func (u testUserAPI) Put() { + u.Write("Put, name: %s\n", u.FormValue("name")) +} + +// POST /users/:param1 +func (u testUserAPI) PostBy(id string) { + u.Write("Post By %s, name: %s\n", id, u.FormValue("name")) +} + +// DELETE /users/:param1 +func (u testUserAPI) DeleteBy(id string) { + u.Write("Delete By %s\n", id) +} + +func TestMuxAPI(t *testing.T) { + initDefault() + + middlewareResponseText := "I assume that you are authenticated\n" + API("/users", testUserAPI{}, func(ctx *Context) { // optional middleware for .API + // do your work here, or render a login window if not logged in, get the user and send it to the next middleware, or do all here + ctx.Set("user", "username") + ctx.Next() + }, func(ctx *Context) { + if ctx.Get("user") == "username" { + ctx.Write(middlewareResponseText) + ctx.Next() + } else { + ctx.SetStatusCode(StatusUnauthorized) + } + }) + + e := Tester(t) + + userID := "4077" + formname := "kataras" + + e.GET("/users").Expect().Status(StatusOK).Body().Equal(middlewareResponseText + "Get Users\n") + e.GET("/users/" + userID).Expect().Status(StatusOK).Body().Equal(middlewareResponseText + "Get By " + userID + "\n") + e.PUT("/users").WithFormField("name", formname).Expect().Status(StatusOK).Body().Equal(middlewareResponseText + "Put, name: " + formname + "\n") + e.POST("/users/"+userID).WithFormField("name", formname).Expect().Status(StatusOK).Body().Equal(middlewareResponseText + "Post By " + userID + ", name: " + formname + "\n") + e.DELETE("/users/" + userID).Expect().Status(StatusOK).Body().Equal(middlewareResponseText + "Delete By " + userID + "\n") +} + +type myTestHandlerData struct { + Sysname string // this will be the same for all requests + Version int // this will be the same for all requests + DynamicPathParameter string // this will be different for each request +} + +type myTestCustomHandler struct { + data myTestHandlerData +} + +func (m *myTestCustomHandler) Serve(ctx *Context) { + data := &m.data + data.DynamicPathParameter = ctx.Param("myparam") + ctx.JSON(StatusOK, data) +} + +func TestMuxCustomHandler(t *testing.T) { + initDefault() + myData := myTestHandlerData{ + Sysname: "Redhat", + Version: 1, + } + Handle("GET", "/custom_handler_1/:myparam", &myTestCustomHandler{myData}) + Handle("GET", "/custom_handler_2/:myparam", &myTestCustomHandler{myData}) + + e := Tester(t) + // two times per testRoute + param1 := "thisimyparam1" + expectedData1 := myData + expectedData1.DynamicPathParameter = param1 + e.GET("/custom_handler_1/" + param1).Expect().Status(StatusOK).JSON().Equal(expectedData1) + + param2 := "thisimyparam2" + expectedData2 := myData + expectedData2.DynamicPathParameter = param2 + e.GET("/custom_handler_1/" + param2).Expect().Status(StatusOK).JSON().Equal(expectedData2) + + param3 := "thisimyparam3" + expectedData3 := myData + expectedData3.DynamicPathParameter = param3 + e.GET("/custom_handler_2/" + param3).Expect().Status(StatusOK).JSON().Equal(expectedData3) + + param4 := "thisimyparam4" + expectedData4 := myData + expectedData4.DynamicPathParameter = param4 + e.GET("/custom_handler_2/" + param4).Expect().Status(StatusOK).JSON().Equal(expectedData4) +} diff --git a/initiatory.go b/initiatory.go deleted file mode 100644 index 6d238e18..00000000 --- a/initiatory.go +++ /dev/null @@ -1,225 +0,0 @@ -package iris - -import ( - "fmt" - "os" - "sync" - "testing" - "time" - - "github.com/gavv/httpexpect" - "github.com/kataras/iris/config" - "github.com/kataras/iris/logger" - "github.com/kataras/iris/render/rest" - "github.com/kataras/iris/render/template" - "github.com/kataras/iris/sessions" - "github.com/kataras/iris/websocket" - ///NOTE: register the session providers, but the s.Config.Sessions.Provider will be used only, if this empty then sessions are disabled. - _ "github.com/kataras/iris/sessions/providers/memory" - _ "github.com/kataras/iris/sessions/providers/redis" -) - -// Default entry, use it with iris.$anyPublicFunc -var ( - Default *Framework - Config *config.Iris - Logger *logger.Logger - Plugins PluginContainer - Websocket websocket.Server - HTTPServer *Server - // Available is a channel type of bool, fired to true when the server is opened and all plugins ran - // never fires false, if the .Close called then the channel is re-allocating. - // the channel is always oepen until you close it when you don't need this. - // - // Note: it is a simple channel and decided to put it here and no inside HTTPServer, doesn't have statuses just true and false, simple as possible - // Where to use that? - // this is used on extreme cases when you don't know which .Listen/.NoListen will be called - // and you want to run/declare something external-not-Iris (all Iris functionality declared before .Listen/.NoListen) AFTER the server is started and plugins finished. - // see the server_test.go for an example - Available chan bool -) - -func init() { - Default = New() - Config = Default.Config - Logger = Default.Logger - Plugins = Default.Plugins - Websocket = Default.Websocket - HTTPServer = Default.HTTPServer - Available = Default.Available -} - -const ( - /* conversional */ - - // HTMLEngine conversion for config.HTMLEngine - HTMLEngine = config.HTMLEngine - // PongoEngine conversion for config.PongoEngine - PongoEngine = config.PongoEngine - // MarkdownEngine conversion for config.MarkdownEngine - MarkdownEngine = config.MarkdownEngine - // JadeEngine conversion for config.JadeEngine - JadeEngine = config.JadeEngine - // AmberEngine conversion for config.AmberEngine - AmberEngine = config.AmberEngine - // HandlebarsEngine conversion for config.HandlebarsEngine - HandlebarsEngine = config.HandlebarsEngine - // DefaultEngine conversion for config.DefaultEngine - DefaultEngine = config.DefaultEngine - // NoEngine conversion for config.NoEngine - NoEngine = config.NoEngine - // NoLayout to disable layout for a particular template file - // conversion for config.NoLayout - NoLayout = config.NoLayout - /* end conversional */ -) - -// Framework is our God |\| Google.Search('Greek mythology Iris') -// -// Implements the FrameworkAPI -type Framework struct { - *muxAPI - rest *rest.Render - templates *template.Template - sessions *sessions.Manager - // fields which are useful to the user/dev - HTTPServer *Server - Config *config.Iris - Logger *logger.Logger - Plugins PluginContainer - Websocket websocket.Server - Available chan bool - // this is setted once when .Tester(t) is called - testFramework *httpexpect.Expect -} - -// New creates and returns a new Iris station aka Framework. -// -// 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) - - // we always use 's' no 'f' because 's' is easier for me to remember because of 'station' - // some things never change :) - s := &Framework{Config: &c, Available: make(chan bool)} - { - ///NOTE: set all with s.Config pointer - // set the Logger - s.Logger = logger.New(s.Config.Logger) - // set the plugin container - s.Plugins = &pluginContainer{logger: s.Logger} - // set the websocket server - s.Websocket = websocket.NewServer(s.Config.Websocket) - // set the servemux, which will provide us the public API also, with its context pool - mux := newServeMux(sync.Pool{New: func() interface{} { return &Context{framework: s} }}, s.Logger) - // set the public router API (and party) - s.muxAPI = &muxAPI{mux: mux, relativePath: "/"} - // set the server - s.HTTPServer = newServer(&s.Config.Server) - } - - return s -} - -func (s *Framework) initialize() { - // set sessions - if s.Config.Sessions.Provider != "" { - s.sessions = sessions.New(s.Config.Sessions) - } - - // set the rest - s.rest = rest.New(s.Config.Render.Rest) - - // set templates if not already setted - s.prepareTemplates() - - // listen to websocket connections - websocket.RegisterServer(s, s.Websocket, s.Logger) - - // prepare the mux & the server - s.mux.setCorrectPath(!s.Config.DisablePathCorrection) - s.mux.setEscapePath(!s.Config.DisablePathEscape) - s.mux.setHostname(s.HTTPServer.VirtualHostname()) - // set the debug profiling handlers if ProfilePath is setted - if debugPath := s.Config.ProfilePath; debugPath != "" { - s.Handle(MethodGet, debugPath+"/*action", profileMiddleware(debugPath)...) - } - - if s.Config.MaxRequestBodySize > config.DefaultMaxRequestBodySize { - s.HTTPServer.MaxRequestBodySize = int(s.Config.MaxRequestBodySize) - } -} - -// prepareTemplates sets the templates if not nil, we make this check because of .TemplateString, which can be called before Listen -func (s *Framework) prepareTemplates() { - // prepare the templates - if s.templates == nil { - // These functions are directly contact with Iris' functionality. - funcs := map[string]interface{}{ - "url": s.URL, - "urlpath": s.Path, - } - - template.RegisterSharedFuncs(funcs) - - s.templates = template.New(s.Config.Render.Template) - } -} - -// openServer is internal method, open the server with specific options passed by the Listen and ListenTLS -// it's a blocking func -func (s *Framework) openServer() (err error) { - s.initialize() - s.Plugins.DoPreListen(s) - // set the server's handler now, in order to give the chance to the plugins to add their own middlewares and routes to this station - s.HTTPServer.SetHandler(s.mux) - if err = s.HTTPServer.Open(); err == nil { - // print the banner - if !s.Config.DisableBanner { - s.Logger.PrintBanner(banner, - fmt.Sprintf("%s: Running at %s\n", time.Now().Format(config.TimeFormat), - s.HTTPServer.Host())) - } - s.Plugins.DoPostListen(s) - s.Available <- true - ch := make(chan os.Signal) - <-ch - s.Close() - } - return -} - -// closeServer is used to close the tcp listener from the server, returns an error -func (s *Framework) closeServer() error { - s.Plugins.DoPreClose(s) - s.Available = make(chan bool) - return s.HTTPServer.Close() -} - -// justServe initializes the whole framework but server doesn't listens to a specific net.Listener -func (s *Framework) justServe(optionalAddr ...string) *Server { - s.HTTPServer.Config = &s.Config.Server - - if len(optionalAddr) > 0 { - s.HTTPServer.Config.ListeningAddr = optionalAddr[0] - } - - s.initialize() - s.Plugins.DoPreListen(s) - s.HTTPServer.SetHandler(s.mux) - s.Plugins.DoPostListen(s) - go func() { - s.Available <- true - }() - - return s.HTTPServer -} - -// tester returns the test framework -func (s *Framework) tester(t *testing.T) *httpexpect.Expect { - if s.testFramework == nil { - s.testFramework = NewTester(s, t) - } - return s.testFramework -} diff --git a/iris.go b/iris.go index 7eae811d..578fa141 100644 --- a/iris.go +++ b/iris.go @@ -61,40 +61,109 @@ import ( "testing" "time" + "sync" + "github.com/gavv/httpexpect" "github.com/iris-contrib/errors" "github.com/kataras/iris/config" "github.com/kataras/iris/context" + "github.com/kataras/iris/logger" + "github.com/kataras/iris/render/rest" + "github.com/kataras/iris/render/template" + "github.com/kataras/iris/sessions" "github.com/kataras/iris/utils" + "github.com/kataras/iris/websocket" "github.com/valyala/fasthttp" + ///NOTE: register the session providers, but the s.Config.Sessions.Provider will be used only, if this empty then sessions are disabled. + _ "github.com/kataras/iris/sessions/providers/memory" + _ "github.com/kataras/iris/sessions/providers/redis" ) const ( // Version of the iris - Version = "3.0.0-rc.4" - banner = ` _____ _ + Version = "3.0.0-pre.release" + + // HTMLEngine conversion for config.HTMLEngine + HTMLEngine = config.HTMLEngine + // PongoEngine conversion for config.PongoEngine + PongoEngine = config.PongoEngine + // MarkdownEngine conversion for config.MarkdownEngine + MarkdownEngine = config.MarkdownEngine + // JadeEngine conversion for config.JadeEngine + JadeEngine = config.JadeEngine + // AmberEngine conversion for config.AmberEngine + AmberEngine = config.AmberEngine + // HandlebarsEngine conversion for config.HandlebarsEngine + HandlebarsEngine = config.HandlebarsEngine + // DefaultEngine conversion for config.DefaultEngine + DefaultEngine = config.DefaultEngine + // NoEngine conversion for config.NoEngine + NoEngine = config.NoEngine + // NoLayout to disable layout for a particular template file + // conversion for config.NoLayout + NoLayout = config.NoLayout + + banner = ` _____ _ |_ _| (_) | | ____ _ ___ | | | __|| |/ __| _| |_| | | |\__ \ - |_____|_| |_||___/ ` + Version + ` - ` + |_____|_| |_||___/ ` + Version + ` ` ) +// Default entry, use it with iris.$anyPublicFunc +var ( + Default *Framework + Config *config.Iris + Logger *logger.Logger + Plugins PluginContainer + Websocket websocket.Server + Servers *ServerList + // Available is a channel type of bool, fired to true when the server is opened and all plugins ran + // never fires false, if the .Close called then the channel is re-allocating. + // the channel is closed only when .ListenVirtual is used, otherwise it remains open until you close it. + // + // Note: it is a simple channel and decided to put it here and no inside HTTPServer, doesn't have statuses just true and false, simple as possible + // Where to use that? + // this is used on extreme cases when you don't know which .Listen/.NoListen will be called + // and you want to run/declare something external-not-Iris (all Iris functionality declared before .Listen/.NoListen) AFTER the server is started and plugins finished. + // see the server_test.go for an example + Available chan bool +) + +func initDefault() { + Default = New() + Config = Default.Config + Logger = Default.Logger + Plugins = Default.Plugins + Websocket = Default.Websocket + Servers = Default.Servers + Available = Default.Available +} + +func init() { + initDefault() +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// --------------------------------Framework implementation----------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + type ( // FrameworkAPI contains the main Iris Public API FrameworkAPI interface { MuxAPI Must(error) - ListenWithErr(string) error + AddServer(config.Server) *Server + ListenTo(config.Server) error Listen(string) - ListenTLSWithErr(string, string, string) error ListenTLS(string, string, string) - ListenUNIXWithErr(string, os.FileMode) error ListenUNIX(string, os.FileMode) - SecondaryListen(config.Server) *Server - NoListen(...string) *Server - Close() + ListenVirtual(...string) *Server + Go() error + Close() error // global middleware prepending, registers to all subdomains, to all parties, you can call it at the last also MustUse(...Handler) MustUseFunc(...HandlerFunc) @@ -108,56 +177,138 @@ type ( Tester(t *testing.T) *httpexpect.Expect } - // RouteNameFunc the func returns from the MuxAPi's methods, optionally sets the name of the Route (*route) - RouteNameFunc func(string) - // MuxAPI the visible api for the serveMux - MuxAPI interface { - Party(string, ...HandlerFunc) MuxAPI - // middleware serial, appending - Use(...Handler) - UseFunc(...HandlerFunc) - - // main handlers - Handle(string, string, ...Handler) RouteNameFunc - HandleFunc(string, string, ...HandlerFunc) RouteNameFunc - // H_ is used to convert a context.IContext handler func to iris.HandlerFunc, is used only inside iris internal package to avoid import cycles - H_(string, string, func(context.IContext)) func(string) - API(string, HandlerAPI, ...HandlerFunc) - - // http methods - Get(string, ...HandlerFunc) RouteNameFunc - Post(string, ...HandlerFunc) RouteNameFunc - Put(string, ...HandlerFunc) RouteNameFunc - Delete(string, ...HandlerFunc) RouteNameFunc - Connect(string, ...HandlerFunc) RouteNameFunc - Head(string, ...HandlerFunc) RouteNameFunc - Options(string, ...HandlerFunc) RouteNameFunc - Patch(string, ...HandlerFunc) RouteNameFunc - Trace(string, ...HandlerFunc) RouteNameFunc - Any(string, ...HandlerFunc) - - // static content - StaticHandler(string, int, bool, bool, []string) HandlerFunc - Static(string, string, int) RouteNameFunc - StaticFS(string, string, int) RouteNameFunc - StaticWeb(string, string, int) RouteNameFunc - StaticServe(string, ...string) RouteNameFunc - StaticContent(string, string, []byte) func(string) - Favicon(string, ...string) RouteNameFunc - - // templates - Layout(string) MuxAPI // returns itself + // Framework is our God |\| Google.Search('Greek mythology Iris') + // + // Implements the FrameworkAPI + Framework struct { + *muxAPI + rest *rest.Render + templates *template.Template + sessions *sessions.Manager + // fields which are useful to the user/dev + // the last added server is the main server + Servers *ServerList + Config *config.Iris + Logger *logger.Logger + Plugins PluginContainer + Websocket websocket.Server + Available chan bool + // this is setted once when .Tester(t) is called + testFramework *httpexpect.Expect } ) -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- -// --------------------------------Framework implementation----------------------------- -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- - var _ FrameworkAPI = &Framework{} +// New creates and returns a new Iris station aka Framework. +// +// 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) + + // we always use 's' no 'f' because 's' is easier for me to remember because of 'station' + // some things never change :) + s := &Framework{Config: &c, Available: make(chan bool)} + { + ///NOTE: set all with s.Config pointer + // set the Logger + s.Logger = logger.New(s.Config.Logger) + // set the plugin container + s.Plugins = &pluginContainer{logger: s.Logger} + // set the websocket server + s.Websocket = websocket.NewServer(s.Config.Websocket) + // set the servemux, which will provide us the public API also, with its context pool + mux := newServeMux(sync.Pool{New: func() interface{} { return &Context{framework: s} }}, s.Logger) + // set the public router API (and party) + s.muxAPI = &muxAPI{mux: mux, relativePath: "/"} + + s.Servers = &ServerList{mux: mux, servers: make([]*Server, 0)} + } + + return s +} + +func (s *Framework) initialize() { + // set sessions + if s.Config.Sessions.Provider != "" { + s.sessions = sessions.New(s.Config.Sessions) + } + + // set the rest + s.rest = rest.New(s.Config.Render.Rest) + + // set templates if not already setted + s.prepareTemplates() + + // listen to websocket connections + websocket.RegisterServer(s, s.Websocket, s.Logger) + + // prepare the mux & the server + s.mux.setCorrectPath(!s.Config.DisablePathCorrection) + s.mux.setEscapePath(!s.Config.DisablePathEscape) + + // set the debug profiling handlers if ProfilePath is setted + if debugPath := s.Config.ProfilePath; debugPath != "" { + s.Handle(MethodGet, debugPath+"/*action", profileMiddleware(debugPath)...) + } +} + +// prepareTemplates sets the templates if not nil, we make this check because of .TemplateString, which can be called before Listen +func (s *Framework) prepareTemplates() { + // prepare the templates + if s.templates == nil { + // These functions are directly contact with Iris' functionality. + funcs := map[string]interface{}{ + "url": s.URL, + "urlpath": s.Path, + } + + template.RegisterSharedFuncs(funcs) + + s.templates = template.New(s.Config.Render.Template) + } +} + +// Go starts the iris station, listens to all registered servers, and prepare only if Virtual +func Go() error { + return Default.Go() +} + +// Go starts the iris station, listens to all registered servers, and prepare only if Virtual +func (s *Framework) Go() error { + s.initialize() + s.Plugins.DoPreListen(s) + + if firstErr := s.Servers.OpenAll(); firstErr != nil { + return firstErr + } + + // print the banner + if !s.Config.DisableBanner { + + openedServers := s.Servers.GetAllOpened() + l := len(openedServers) + hosts := make([]string, l, l) + for i, srv := range openedServers { + hosts[i] = srv.Host() + } + + bannerMessage := time.Now().Format(config.TimeFormat) + ": Running at " + strings.Join(hosts, ", ") + s.Logger.PrintBanner(banner, bannerMessage) + + } + + s.Plugins.DoPostListen(s) + + go func() { s.Available <- true }() + ch := make(chan os.Signal) + <-ch + s.Close() // btw, don't panic here + + return nil +} + // Must panics on error, it panics on registed iris' logger func Must(err error) { Default.Must(err) @@ -170,60 +321,74 @@ func (s *Framework) Must(err error) { } } -// ListenWithErr starts the standalone http server -// which listens to the addr parameter which as the form of -// host:port +// AddServer same as .Servers.Add(config.Server) instead // -// It returns an error you are responsible how to handle this -// if you need a func to panic on error use the Listen -// ex: log.Fatal(iris.ListenWithErr(":8080")) -func ListenWithErr(addr string) error { - return Default.ListenWithErr(addr) +// AddServers 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 +// 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) +} + +// AddServer same as .Servers.Add(config.Server) instead +// +// AddServers 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 +// 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) +} + +// ListenTo listens to a server but receives the full server's configuration +// returns an error, you're responsible to handle that +// or use the iris.Must(iris.ListenTo(config.Server{})) +// +// it's a blocking func +func ListenTo(cfg config.Server) error { + return Default.ListenTo(cfg) +} + +// ListenTo listens to a server but receives the full server's configuration +// it's a blocking func +func (s *Framework) ListenTo(cfg config.Server) (err error) { + s.Servers.Add(cfg) + return s.Go() } // Listen starts the standalone http server // which listens to the addr parameter which as the form of // host:port // -// It panics on error if you need a func to return an error use the ListenWithErr -// ex: iris.Listen(":8080") +// 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) } -// ListenWithErr starts the standalone http server -// which listens to the addr parameter which as the form of -// host:port -// -// It returns an error you are responsible how to handle this -// if you need a func to panic on error use the Listen -// ex: log.Fatal(iris.ListenWithErr(":8080")) -func (s *Framework) ListenWithErr(addr string) error { - s.Config.Server.ListeningAddr = addr - return s.openServer() -} - // Listen starts the standalone http server // which listens to the addr parameter which as the form of // host:port // -// It panics on error if you need a func to return an error use the ListenWithErr -// ex: iris.Listen(":8080") +// 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.ListenWithErr(addr)) -} - -// ListenTLSWithErr Starts a https server with certificates, -// if you use this method the requests of the form of 'http://' will fail -// only https:// connections are allowed -// which listens to the addr parameter which as the form of -// host:port -// -// It returns an error you are responsible how to handle this -// if you need a func to panic on error use the ListenTLS -// ex: log.Fatal(iris.ListenTLSWithErr(":8080","yourfile.cert","yourfile.key")) -func ListenTLSWithErr(addr string, certFile string, keyFile string) error { - return Default.ListenTLSWithErr(addr, certFile, keyFile) + s.Must(s.ListenTo(config.Server{ListeningAddr: addr})) } // ListenTLS Starts a https server with certificates, @@ -232,142 +397,86 @@ func ListenTLSWithErr(addr string, certFile string, keyFile string) error { // which listens to the addr parameter which as the form of // host:port // -// It panics on error if you need a func to return an error use the ListenTLSWithErr -// ex: iris.ListenTLS(":8080","yourfile.cert","yourfile.key") +// 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") func ListenTLS(addr string, certFile string, keyFile string) { Default.ListenTLS(addr, certFile, keyFile) } -// ListenTLSWithErr Starts a https server with certificates, -// if you use this method the requests of the form of 'http://' will fail -// only https:// connections are allowed -// which listens to the addr parameter which as the form of -// host:port -// -// It returns an error you are responsible how to handle this -// if you need a func to panic on error use the ListenTLS -// ex: log.Fatal(iris.ListenTLSWithErr(":8080","yourfile.cert","yourfile.key")) -func (s *Framework) ListenTLSWithErr(addr string, certFile string, keyFile string) error { - if certFile == "" || keyFile == "" { - return fmt.Errorf("You should provide certFile and keyFile for TLS/SSL") - } - s.Config.Server.ListeningAddr = addr - s.Config.Server.CertFile = certFile - s.Config.Server.KeyFile = keyFile - - return s.openServer() -} - // ListenTLS Starts a https server with certificates, // if you use this method the requests of the form of 'http://' will fail // only https:// connections are allowed // which listens to the addr parameter which as the form of // host:port // -// It panics on error if you need a func to return an error use the ListenTLSWithErr -// ex: iris.ListenTLS(":8080","yourfile.cert","yourfile.key") +// 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") func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { - s.Must(s.ListenTLSWithErr(addr, certFile, keyFile)) -} - -// ListenUNIXWithErr starts the process of listening to the new requests using a 'socket file', this works only on unix -// returns an error if something bad happens when trying to listen to -func ListenUNIXWithErr(addr string, mode os.FileMode) error { - return Default.ListenUNIXWithErr(addr, mode) + 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})) } // ListenUNIX starts the process of listening to the new requests using a 'socket file', this works only on unix -// panics on error +// +// It panics on error if you need a func to return an error, use the ListenTo +// ex: err := iris.ListenTo(":8080", Mode: os.FileMode) func ListenUNIX(addr string, mode os.FileMode) { Default.ListenUNIX(addr, mode) } -// ListenUNIXWithErr starts the process of listening to the new requests using a 'socket file', this works only on unix -// returns an error if something bad happens when trying to listen to -func (s *Framework) ListenUNIXWithErr(addr string, mode os.FileMode) error { - s.Config.Server.ListeningAddr = addr - s.Config.Server.Mode = mode - return s.openServer() -} - // ListenUNIX starts the process of listening to the new requests using a 'socket file', this works only on unix -// panics on error +// +// It panics on error if you need a func to return an error, use the ListenTo +// ex: err := iris.ListenTo(":8080", Mode: os.FileMode) func (s *Framework) ListenUNIX(addr string, mode os.FileMode) { - s.Must(s.ListenUNIXWithErr(addr, mode)) + s.Must(ListenTo(config.Server{ListeningAddr: addr, Mode: mode})) } -// SecondaryListen 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 only 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 -// returns the new standalone server( you can close this server by the returning reference) -// -// If you need only one server this function is not for you, instead you must use the normal .Listen/ListenTLS functions. -// -// this is a NOT A BLOCKING version, the main iris.Listen should be always executed LAST, so this function goes before the main .Listen. -func SecondaryListen(cfg config.Server) *Server { - return Default.SecondaryListen(cfg) -} - -// SecondaryListen 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 only 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 -// returns the new standalone server( you can close this server by the returning reference) -// -// If you need only one server this function is not for you, instead you must use the normal .Listen/ListenTLS functions. -// -// this is a NOT A BLOCKING version, the main iris.Listen should be always executed LAST, so this function goes before the main .Listen. -func (s *Framework) SecondaryListen(cfg config.Server) *Server { - srv := newServer(&cfg) - // add a post listen event to start this server after the previous started - s.Plugins.Add(PostListenFunc(func(*Framework) { - go func() { // goroutine in order to not block any runtime post listeners - srv.Handler = s.HTTPServer.Handler - if err := srv.Open(); err == nil { - ch := make(chan os.Signal) - <-ch - srv.Close() - } - }() - })) - - return srv -} - -// NoListen is useful only when you want to test Iris, it doesn't starts the server but it configures and returns it -func NoListen(optionalAddr ...string) *Server { - return Default.NoListen(optionalAddr...) -} - -// NoListen is useful only when you want to test Iris, it doesn't starts the server but it configures and returns it +// ListenVirtual is useful only when you want to test Iris, it doesn't starts the server but it configures and returns it // initializes the whole framework but server doesn't listens to a specific net.Listener -func (s *Framework) NoListen(optionalAddr ...string) *Server { - return s.justServe(optionalAddr...) +// it is not blocking the app +func ListenVirtual(optionalAddr ...string) *Server { + return Default.ListenVirtual(optionalAddr...) } -// CloseWithErr terminates the server and returns an error if any -func CloseWithErr() error { - return Default.CloseWithErr() +// ListenVirtual is useful only when you want to test Iris, it doesn't starts the server but it configures and returns it +// initializes the whole framework but server doesn't listens to a specific net.Listener +// it is not blocking the app +func (s *Framework) ListenVirtual(optionalAddr ...string) *Server { + s.Config.DisableBanner = true + cfg := config.DefaultServer() + + if len(optionalAddr) > 0 && optionalAddr[0] != "" { + cfg.ListeningAddr = optionalAddr[0] + } + cfg.Virtual = true + + go func() { + s.Must(s.ListenTo(cfg)) + }() + + if ok := <-s.Available; !ok { + s.Logger.Panic("Unexpected error:Virtual server cannot start, please report this as bug!!") + } + + close(s.Available) + return s.Servers.Main() } -//Close terminates the server and panic if error occurs -func Close() { - Default.Close() +// Close terminates all the registered servers and returns an error if any +// if you want to panic on this error use the iris.Must(iris.Close()) +func Close() error { + return Default.Close() } -// CloseWithErr terminates the server and returns an error if any -func (s *Framework) CloseWithErr() error { - return s.closeServer() -} - -//Close terminates the server and panic if error occurs -func (s *Framework) Close() { - s.Must(s.CloseWithErr()) +// Close terminates all the registered servers and returns an error if any +// if you want to panic on this error use the iris.Must(iris.Close()) +func (s *Framework) Close() error { + s.Plugins.DoPreClose(s) + s.Available = make(chan bool) + return s.Servers.CloseAll() } // MustUse registers Handler middleware to the beginning, prepends them instead of append @@ -541,13 +650,13 @@ func (s *Framework) URL(routeName string, args ...interface{}) (url string) { if r == nil { return } - + srv := s.Servers.Main() scheme := "http://" - if s.HTTPServer.IsSecure() { + if srv.IsSecure() { scheme = "https://" } - host := s.HTTPServer.VirtualHost() + host := srv.VirtualHost() arguments := args[0:] // join arrays as arguments @@ -608,21 +717,35 @@ func (s *Framework) TemplateString(templateFile string, pageContext interface{}, // NewTester Prepares and returns a new test framework based on the api // is useful when you need to have more than one test framework for the same iris insttance, otherwise you can use the iris.Tester(t *testing.T)/variable.Tester(t *testing.T) func NewTester(api *Framework, t *testing.T) *httpexpect.Expect { - api.Config.DisableBanner = true - if !api.HTTPServer.IsListening() { // maybe the user called this after .Listen/ListenTLS/ListenUNIX, the tester can be used as standalone (with no running iris instance) or inside a running instance/app - api.NoListen() - if ok := <-api.Available; !ok { - t.Fatal("Unexpected error: server cannot start, please report this as bug!!") - } - close(api.Available) + srv := api.Servers.Main() + if srv == nil { // maybe the user called this after .Listen/ListenTLS/ListenUNIX, the tester can be used as standalone (with no running iris instance) or inside a running instance/app + srv = api.ListenVirtual(api.Config.Tester.ListeningAddr) } - handler := api.HTTPServer.Handler + opened := api.Servers.GetAllOpened() + h := srv.Handler + baseURL := srv.FullHost() + if len(opened) > 1 { + baseURL = "" + //we have more than one server, so we will create a handler here and redirect by registered listening addresses + h = func(reqCtx *fasthttp.RequestCtx) { + for _, s := range opened { + if strings.HasPrefix(reqCtx.URI().String(), s.FullHost()) { // yes on :80 should be passed :80 also, this is inneed for multiserver testing + s.Handler(reqCtx) + break + } + } + } + } + + if api.Config.Tester.ExplicitURL { + baseURL = "" + } testConfiguration := httpexpect.Config{ - BaseURL: api.HTTPServer.FullHost(), + BaseURL: baseURL, Client: &http.Client{ - Transport: httpexpect.NewFastBinder(handler), + Transport: httpexpect.NewFastBinder(h), Jar: httpexpect.NewJar(), }, Reporter: httpexpect.NewAssertReporter(t), @@ -644,7 +767,10 @@ func Tester(t *testing.T) *httpexpect.Expect { // Tester returns the test framework for this iris insance func (s *Framework) Tester(t *testing.T) *httpexpect.Expect { - return s.tester(t) + if s.testFramework == nil { + s.testFramework = NewTester(s, t) + } + return s.testFramework } // ------------------------------------------------------------------------------------- @@ -652,12 +778,56 @@ func (s *Framework) Tester(t *testing.T) *httpexpect.Expect { // ----------------------------------MuxAPI implementation------------------------------ // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- +type ( + // RouteNameFunc the func returns from the MuxAPi's methods, optionally sets the name of the Route (*route) + RouteNameFunc func(string) + // MuxAPI the visible api for the serveMux + MuxAPI interface { + Party(string, ...HandlerFunc) MuxAPI + // middleware serial, appending + Use(...Handler) + UseFunc(...HandlerFunc) -type muxAPI struct { - mux *serveMux - relativePath string - middleware Middleware -} + // main handlers + Handle(string, string, ...Handler) RouteNameFunc + HandleFunc(string, string, ...HandlerFunc) RouteNameFunc + // H_ is used to convert a context.IContext handler func to iris.HandlerFunc, is used only inside iris internal package to avoid import cycles + H_(string, string, func(context.IContext)) func(string) + API(string, HandlerAPI, ...HandlerFunc) + + // http methods + Get(string, ...HandlerFunc) RouteNameFunc + Post(string, ...HandlerFunc) RouteNameFunc + Put(string, ...HandlerFunc) RouteNameFunc + Delete(string, ...HandlerFunc) RouteNameFunc + Connect(string, ...HandlerFunc) RouteNameFunc + Head(string, ...HandlerFunc) RouteNameFunc + Options(string, ...HandlerFunc) RouteNameFunc + Patch(string, ...HandlerFunc) RouteNameFunc + Trace(string, ...HandlerFunc) RouteNameFunc + Any(string, ...HandlerFunc) + + // static content + StaticHandler(string, int, bool, bool, []string) HandlerFunc + Static(string, string, int) RouteNameFunc + StaticFS(string, string, int) RouteNameFunc + StaticWeb(string, string, int) RouteNameFunc + StaticServe(string, ...string) RouteNameFunc + StaticContent(string, string, []byte) func(string) + Favicon(string, ...string) RouteNameFunc + + // templates + Layout(string) MuxAPI // returns itself + } + + muxAPI struct { + mux *serveMux + relativePath string + middleware Middleware + } +) + +var _ MuxAPI = &muxAPI{} var ( // errAPIContextNotFound returns an error with message: 'From .API: "Context *iris.Context could not be found..' @@ -666,8 +836,6 @@ var ( errDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s") ) -var _ MuxAPI = &muxAPI{} - // Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. // Party can also be named as 'Join' or 'Node' or 'Group' , Party chosen because it has more fun func Party(relativePath string, handlersFn ...HandlerFunc) MuxAPI { diff --git a/iris_test.go b/iris_test.go new file mode 100644 index 00000000..69504f1c --- /dev/null +++ b/iris_test.go @@ -0,0 +1,14 @@ +package iris + +/* +The most iris.go file implementation tested at other files like context_test, http_test, the untested are the Static methods, the favicon and some interfaces, which I already +tested them on production and I don't expect unexpected behavior but if you think we need more: + +CONTRIBUTE & DISCUSSION ABOUT TESTS TO: https://github.com/iris-contrib/tests +*/ + +// Notes: +// +// We use Default := New() via initDefault() and not api := New() neither just Default. because we want to cover as much code as possible +// The tests are mostly end-to-end, except some features like plugins. +// diff --git a/plugin.go b/plugin.go index d0a49e72..4f24abec 100644 --- a/plugin.go +++ b/plugin.go @@ -214,11 +214,13 @@ type pluginContainer struct { customEvents map[string][]func() downloader *pluginDownloadManager logger *logger.Logger + mu sync.Mutex } // Add activates the plugins and if succeed then adds it to the activated plugins list func (p *pluginContainer) Add(plugins ...Plugin) error { for _, plugin := range plugins { + if p.activatedPlugins == nil { p.activatedPlugins = make([]Plugin, 0) } @@ -232,10 +234,16 @@ func (p *pluginContainer) Add(plugins ...Plugin) error { } // Activate the plugin, if no error then add it to the plugins if pluginObj, ok := plugin.(pluginActivate); ok { - err := pluginObj.Activate(p) + tempPluginContainer := *p + err := pluginObj.Activate(&tempPluginContainer) if err != nil { return errPluginActivate.Format(pName, err.Error()) } + tempActivatedPluginsLen := len(tempPluginContainer.activatedPlugins) + if tempActivatedPluginsLen != len(p.activatedPlugins)+tempActivatedPluginsLen+1 { // see test: plugin_test.go TestPluginActivate && TestPluginActivationError + p.activatedPlugins = tempPluginContainer.activatedPlugins + } + } // All ok, add it to the plugins list diff --git a/plugin_test.go b/plugin_test.go new file mode 100644 index 00000000..67906e3e --- /dev/null +++ b/plugin_test.go @@ -0,0 +1,200 @@ +package iris + +/* +Contains tests for plugin, no end-to-end, just local-object tests, these are enoguh for now. + +CONTRIBUTE & DISCUSSION ABOUT TESTS TO: https://github.com/iris-contrib/tests +*/ + +import ( + "fmt" + "testing" +) + +const ( + testPluginExDescription = "Description for My test plugin" + testPluginExName = "My test plugin" +) + +type testPluginEx struct { + named, activated, descriptioned bool + prelistenran, postlistenran, precloseran bool +} + +func (t *testPluginEx) GetName() string { + fmt.Println("GetName Struct") + t.named = true + return testPluginExName +} + +func (t *testPluginEx) GetDescription() string { + fmt.Println("GetDescription Struct") + t.descriptioned = true + return testPluginExDescription +} + +func (t *testPluginEx) Activate(p PluginContainer) error { + fmt.Println("Activate Struct") + t.activated = true + return nil +} + +func (t *testPluginEx) PreListen(*Framework) { + fmt.Println("PreListen Struct") + t.prelistenran = true +} + +func (t *testPluginEx) PostListen(*Framework) { + fmt.Println("PostListen Struct") + t.postlistenran = true +} + +func (t *testPluginEx) PreClose(*Framework) { + fmt.Println("PreClose Struct") + t.precloseran = true +} + +func ExamplePlugins_Add() { + initDefault() + Plugins.Add(PreListenFunc(func(*Framework) { + fmt.Println("PreListen Func") + })) + + Plugins.Add(PostListenFunc(func(*Framework) { + fmt.Println("PostListen Func") + })) + + Plugins.Add(PreCloseFunc(func(*Framework) { + fmt.Println("PreClose Func") + })) + + myplugin := &testPluginEx{} + Plugins.Add(myplugin) + desc := Plugins.GetDescription(myplugin) + fmt.Println(desc) + + ListenVirtual() + Close() + + // Output: + // GetName Struct + // Activate Struct + // GetDescription Struct + // Description for My test plugin + // PreListen Func + // PreListen Struct + // PostListen Func + // PostListen Struct + // PreClose Func + // PreClose Struct +} + +// if a plugin has GetName, then it should be registered only one time, the name exists for that reason, it's like unique ID +func TestPluginDublicateName(t *testing.T) { + var plugins pluginContainer + firstNamedPlugin := &testPluginEx{} + sameNamedPlugin := &testPluginEx{} + // err := plugins.Add(firstNamedPlugin, sameNamedPlugin) or + err := plugins.Add(firstNamedPlugin) + if err != nil { + t.Fatalf("Unexpected error when adding a plugin with name: %s", testPluginExName) + } + err = plugins.Add(sameNamedPlugin) + if err == nil { + t.Fatalf("Expected an error because of dublicate named plugin!") + } + if len(plugins.activatedPlugins) != 1 { + t.Fatalf("Expected: %d activated plugin but we got: %d", 1, len(plugins.activatedPlugins)) + } +} + +type testPluginActivationType struct { + shouldError bool +} + +func (t testPluginActivationType) Activate(p PluginContainer) error { + p.Add(&testPluginEx{}) + if t.shouldError { + return fmt.Errorf("An error happens, this plugin and the added plugins by this plugin should not be registered") + } + return nil +} + +func TestPluginActivate(t *testing.T) { + var plugins pluginContainer + myplugin := testPluginActivationType{shouldError: false} + plugins.Add(myplugin) + + if len(plugins.activatedPlugins) != 2 { // 2 because it registeres a second plugin also + t.Fatalf("Expected activated plugins to be: %d but we got: %d", 0, len(plugins.activatedPlugins)) + } +} + +// if any error returned from the Activate plugin's method, then this plugin and the plugins it registers should not be registered at all +func TestPluginActivationError(t *testing.T) { + var plugins pluginContainer + myplugin := testPluginActivationType{shouldError: true} + plugins.Add(myplugin) + + if len(plugins.activatedPlugins) > 0 { + t.Fatalf("Expected activated plugins to be: %d but we got: %d", 0, len(plugins.activatedPlugins)) + } +} + +func TestPluginEvents(t *testing.T) { + var plugins pluginContainer + var prelistenran, postlistenran, precloseran bool + + plugins.Add(PreListenFunc(func(*Framework) { + prelistenran = true + })) + + plugins.Add(PostListenFunc(func(*Framework) { + postlistenran = true + })) + + plugins.Add(PreCloseFunc(func(*Framework) { + precloseran = true + })) + + myplugin := &testPluginEx{} + plugins.Add(myplugin) + if len(plugins.activatedPlugins) != 4 { + t.Fatalf("Expected: %d plugins to be registed but we got: %d", 4, len(plugins.activatedPlugins)) + } + desc := plugins.GetDescription(myplugin) + if desc != testPluginExDescription { + t.Fatalf("Expected: %s as Description of the plugin but got: %s", testPluginExDescription, desc) + } + + plugins.DoPreListen(nil) + plugins.DoPostListen(nil) + plugins.DoPreClose(nil) + + if !prelistenran { + t.Fatalf("Expected to run PreListen Func but it doesnt!") + } + if !postlistenran { + t.Fatalf("Expected to run PostListen Func but it doesnt!") + } + if !precloseran { + t.Fatalf("Expected to run PostListen Func but it doesnt!") + } + + if !myplugin.named { + t.Fatalf("Plugin should be named with: %s!", testPluginExName) + } + if !myplugin.activated { + t.Fatalf("Plugin should be activated but it's not!") + } + if !myplugin.prelistenran { + t.Fatalf("Expected to run PreListen Struct but it doesnt!") + } + if !myplugin.postlistenran { + t.Fatalf("Expected to run PostListen Struct but it doesnt!") + } + if !myplugin.precloseran { + t.Fatalf("Expected to run PostListen Struct but it doesnt!") + } + +} diff --git a/sessions/providers/memory/store.go b/sessions/providers/memory/store.go index b96835f4..40c8d91d 100644 --- a/sessions/providers/memory/store.go +++ b/sessions/providers/memory/store.go @@ -19,15 +19,11 @@ var _ store.IStore = &Store{} // GetAll returns all values func (s *Store) GetAll() map[string]interface{} { - s.mu.Lock() - defer s.mu.Unlock() return s.values } // VisitAll loop each one entry and calls the callback function func(key,value) func (s *Store) VisitAll(cb func(k string, v interface{})) { - s.mu.Lock() - defer s.mu.Unlock() for key := range s.values { cb(key, s.values[key]) } @@ -36,9 +32,7 @@ func (s *Store) VisitAll(cb func(k string, v interface{})) { // Get returns the value of an entry by its key func (s *Store) Get(key string) interface{} { Provider.Update(s.sid) - s.mu.Lock() if value, found := s.values[key]; found { - s.mu.Unlock() return value } s.mu.Unlock() diff --git a/sessions/providers/redis/redisstore.go b/sessions/providers/redis/redisstore.go index fa43213c..58a62834 100644 --- a/sessions/providers/redis/redisstore.go +++ b/sessions/providers/redis/redisstore.go @@ -81,15 +81,11 @@ func (s *Store) update() { // GetAll returns all values func (s *Store) GetAll() map[string]interface{} { - s.mu.Lock() - defer s.mu.Unlock() return s.values } // VisitAll loop each one entry and calls the callback function func(key,value) func (s *Store) VisitAll(cb func(k string, v interface{})) { - s.mu.Lock() - defer s.mu.Unlock() for key := range s.values { cb(key, s.values[key]) } @@ -98,12 +94,10 @@ func (s *Store) VisitAll(cb func(k string, v interface{})) { // Get returns the value of an entry by its key func (s *Store) Get(key string) interface{} { Provider.Update(s.sid) - s.mu.Lock() if value, found := s.values[key]; found { s.mu.Unlock() return value } - s.mu.Unlock() return nil }