From 2cc75817b7b32a204ee04789d9b581c2d572013f Mon Sep 17 00:00:00 2001 From: Makis Maropoulos Date: Wed, 6 Jul 2016 20:24:34 +0200 Subject: [PATCH] Add support for more than one listening server to one station, virtual and no virtual --- config/iris.go | 16 +- config/server.go | 22 ++- config/tester.go | 9 +- context.go | 8 +- context_binder_test.go | 2 + context_test.go | 111 +++++++++++ http.go | 181 +++++++++++++++--- initiatory.go | 206 --------------------- iris.go | 407 ++++++++++++++++++++++++++++++----------- mux_test.go | 6 +- server_test.go | 95 ++++++---- sessions_test.go | 1 - 12 files changed, 674 insertions(+), 390 deletions(-) create mode 100644 context_test.go delete mode 100644 initiatory.go diff --git a/config/iris.go b/config/iris.go index 930b0883..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 @@ -135,13 +124,12 @@ func Default() Iris { DisablePathCorrection: DefaultDisablePathCorrection, DisablePathEscape: DefaultDisablePathEscape, DisableBanner: false, - MaxRequestBodySize: DefaultMaxRequestBodySize, ProfilePath: "", Logger: DefaultLogger(), Sessions: DefaultSessions(), Render: DefaultRender(), Websocket: DefaultWebsocket(), - Tester: Tester{Debug: false}, + Tester: DefaultTester(), } } diff --git a/config/server.go b/config/server.go index 089ced10..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 @@ -43,7 +53,8 @@ type Server struct { // 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 @@ -59,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/tester.go b/config/tester.go index 11488068..da56e2f5 100644 --- a/config/tester.go +++ b/config/tester.go @@ -2,5 +2,12 @@ package config // Tester configuration type Tester struct { - Debug bool + Debug bool + ListeningAddr string +} + +// 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{Debug: false, ListeningAddr: "iris-go.com:1993"} } diff --git a/context.go b/context.go index 201491c5..b80a9c00 100644 --- a/context.go +++ b/context.go @@ -218,7 +218,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] @@ -480,7 +480,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 diff --git a/context_binder_test.go b/context_binder_test.go index 74d1a160..99f6fad4 100644 --- a/context_binder_test.go +++ b/context_binder_test.go @@ -26,6 +26,7 @@ type testBinderXMLData struct { func TestBindForm(t *testing.T) { initDefault() + Post("/form", func(ctx *Context) { obj := testBinderData{} err := ctx.ReadForm(&obj) @@ -36,6 +37,7 @@ func TestBindForm(t *testing.T) { }) e := Tester(t) + passed := map[string]interface{}{"Username": "myusername", "Mail": "mymail@iris-go.com", "mydata": url.Values{"[0]": []string{"mydata1"}, "[1]": []string{"mydata2"}}} diff --git a/context_test.go b/context_test.go new file mode 100644 index 00000000..596ad3b0 --- /dev/null +++ b/context_test.go @@ -0,0 +1,111 @@ +package iris + +import "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 TestContextParam(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) + } +} + +func TestContextURLParam(t *testing.T) { + +} diff --git a/http.go b/http.go index c3ff9e57..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,6 +446,8 @@ func (s *Server) Open() error { } reqCtx.Redirect(redirectTo, StatusMovedPermanently) } + } else { + s.Server.Handler = h } if s.Config.Virtual { @@ -452,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/initiatory.go b/initiatory.go deleted file mode 100644 index 111861c2..00000000 --- a/initiatory.go +++ /dev/null @@ -1,206 +0,0 @@ -package iris - -import ( - "fmt" - "os" - "sync" - "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 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 init() { - initDefault() -} - -func initDefault() { - 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 with the default configuration, which is changed on Listen functions - defaultServerCfg := config.DefaultServer() - s.HTTPServer = newServer(&defaultServerCfg) - } - - 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) - - go func() { 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() -} diff --git a/iris.go b/iris.go index 42f419ab..4d77d24f 100644 --- a/iris.go +++ b/iris.go @@ -61,26 +61,96 @@ 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 = ` _____ _ + + // 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 { @@ -110,56 +180,140 @@ 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 { + panic("iris:287") + return firstErr + } + + // print the banner + if !s.Config.DisableBanner { + serversMessage := time.Now().Format(config.TimeFormat) + ": Running at " + openedServers := s.Servers.GetAllOpened() + if len(openedServers) == 1 { + // if only one server then don't need to add a new line + serversMessage += openedServers[0].Host() + } else { + for _, srv := range openedServers { + serversMessage += "\n" + srv.Host() + } + } + + s.Logger.PrintBanner(banner, serversMessage) + } + + s.Plugins.DoPostListen(s) + + go func() { s.Available <- true }() + ch := make(chan os.Signal) + <-ch + s.CloseWithErr() // btw, don't panic here + + return nil +} + // Must panics on error, it panics on registed iris' logger func Must(err error) { Default.Must(err) @@ -180,9 +334,9 @@ func ListenTo(cfg config.Server) error { // ListenTo listens to a server but receives the full server's configuration // it's a blocking func -func (s *Framework) ListenTo(cfg config.Server) error { - s.HTTPServer.Config = &cfg - return s.openServer() +func (s *Framework) ListenTo(cfg config.Server) (err error) { + s.Servers.Add(cfg) + return s.Go() } // ListenWithErr starts the standalone http server @@ -214,12 +368,7 @@ func Listen(addr string) { // 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 { - cfg := config.DefaultServer() - if len(addr) > 0 { - cfg.ListeningAddr = addr - } - - return s.ListenTo(cfg) + return s.ListenTo(config.Server{ListeningAddr: addr}) } // Listen starts the standalone http server @@ -267,14 +416,10 @@ func ListenTLS(addr string, certFile string, keyFile string) { // 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 { - cfg := config.DefaultServer() if certFile == "" || keyFile == "" { return fmt.Errorf("You should provide certFile and keyFile for TLS/SSL") } - cfg.ListeningAddr = addr - cfg.CertFile = certFile - cfg.KeyFile = keyFile - return s.ListenTo(cfg) + return s.ListenTo(config.Server{ListeningAddr: addr, CertFile: certFile, KeyFile: keyFile}) } // ListenTLS Starts a https server with certificates, @@ -304,10 +449,7 @@ func ListenUNIX(addr string, mode os.FileMode) { // 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 { - cfg := config.DefaultServer() - cfg.ListeningAddr = addr - cfg.Mode = mode - return s.ListenTo(cfg) + return s.ListenTo(config.Server{ListeningAddr: addr, Mode: mode}) } // ListenUNIX starts the process of listening to the new requests using a 'socket file', this works only on unix @@ -316,6 +458,9 @@ func (s *Framework) ListenUNIX(addr string, mode os.FileMode) { s.Must(s.ListenUNIXWithErr(addr, mode)) } +// SecondaryListen NOTE: This will be deprecated +// Use .Servers.Add(config.Server) instead +// // 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) // @@ -331,6 +476,9 @@ func SecondaryListen(cfg config.Server) *Server { return Default.SecondaryListen(cfg) } +// SecondaryListen NOTE: This will be deprecated +// Use .Servers.Add(config.Server) instead +// // 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) // @@ -343,22 +491,7 @@ func SecondaryListen(cfg config.Server) *Server { // // 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 { - if !cfg.Virtual { - ch := make(chan os.Signal) - <-ch - srv.Close() - } - } - }() - })) - - return srv + 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 @@ -391,34 +524,41 @@ func (s *Framework) ListenVirtual(optionalAddr ...string) *Server { s.Config.DisableBanner = true cfg := config.DefaultServer() - if len(optionalAddr) > 0 { + if len(optionalAddr) > 0 && optionalAddr[0] != "" { cfg.ListeningAddr = optionalAddr[0] } cfg.Virtual = true - go s.ListenTo(cfg) + + 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.HTTPServer + return s.Servers.Main() } -// CloseWithErr terminates the server and returns an error if any +// CloseWithErr terminates all the registered servers and returns an error if any func CloseWithErr() error { return Default.CloseWithErr() } -//Close terminates the server and panic if error occurs +//Close terminates all the registered servers and panic if error occurs func Close() { Default.Close() } -// CloseWithErr terminates the server and returns an error if any +// CloseWithErr terminates all the registered servers and returns an error if any func (s *Framework) CloseWithErr() error { - return s.closeServer() + s.Plugins.DoPreClose(s) + s.Available = make(chan bool) + return s.Servers.CloseAll() } -//Close terminates the server and panic if error occurs +//Close terminates all the registered servers and panic if error occurs func (s *Framework) Close() { s.Must(s.CloseWithErr()) } @@ -594,13 +734,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 @@ -661,16 +801,31 @@ 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 { - 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.ListenVirtual() + 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 + } + } + } + } 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), @@ -703,12 +858,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..' @@ -717,8 +916,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/mux_test.go b/mux_test.go index 2e3b28eb..95501fdd 100644 --- a/mux_test.go +++ b/mux_test.go @@ -16,12 +16,12 @@ const ( ) func testSubdomainHost() string { - return testSubdomain + strconv.Itoa(HTTPServer.Port()) + return testSubdomain + strconv.Itoa(Servers.Main().Port()) } func testSubdomainURL() (subdomainURL string) { subdomainHost := testSubdomainHost() - if HTTPServer.IsSecure() { + if Servers.Main().IsSecure() { subdomainURL = "https://" + subdomainHost } else { subdomainURL = "http://" + subdomainHost @@ -159,7 +159,7 @@ func TestMuxSimpleParty(t *testing.T) { request := func(reqPath string) { e.Request("GET", reqPath). Expect(). - Status(StatusOK).Body().Equal(HTTPServer.Host() + reqPath) + Status(StatusOK).Body().Equal(Servers.Main().Host() + reqPath) } // run the tests diff --git a/server_test.go b/server_test.go index 1e457862..ba403168 100644 --- a/server_test.go +++ b/server_test.go @@ -2,12 +2,10 @@ package iris import ( "io/ioutil" - "net/http" "os" "testing" "time" - "github.com/gavv/httpexpect" "github.com/kataras/iris/config" ) @@ -63,10 +61,10 @@ const ( ) // Contains the server test for multi running servers -// Note: this test runs two standalone (real) servers -func TestMultiRunningServers(t *testing.T) { +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") @@ -92,48 +90,77 @@ func TestMultiRunningServers(t *testing.T) { os.Remove(keyFile.Name()) }() - initDefault() - Config.DisableBanner = true - Get("/", func(ctx *Context) { ctx.Write("Hello from %s", ctx.HostString()) }) // start the secondary server - secondary := SecondaryListen(config.Server{ListeningAddr: ":80", RedirectTo: "https://" + host, Virtual: true}) + 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}) - - defer func() { - go secondary.Close() - go CloseWithErr() - close(Available) - }() // prepare test framework if ok := <-Available; !ok { t.Fatal("Unexpected error: server cannot start, please report this as bug!!") } - handler := HTTPServer.Handler - - testConfiguration := httpexpect.Config{ - Client: &http.Client{ - Transport: httpexpect.NewFastBinder(handler), - Jar: httpexpect.NewJar(), - }, - Reporter: httpexpect.NewAssertReporter(t), - } - - if Config.Tester.Debug { - testConfiguration.Printers = []httpexpect.Printer{ - httpexpect.NewDebugPrinter(t, true), - } - } - // - - e := httpexpect.WithConfig(testConfiguration) + 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) - e.Request("GET", "http://"+host).Expect().Status(StatusOK).Body().Equal("Hello from " + host) } diff --git a/sessions_test.go b/sessions_test.go index 88cbac36..84845de1 100644 --- a/sessions_test.go +++ b/sessions_test.go @@ -15,7 +15,6 @@ func TestSessions(t *testing.T) { } initDefault() - HTTPServer.Config.ListeningAddr = "127.0.0.1:8080" // in order to test the sessions Config.Sessions.Cookie = "mycustomsessionid" writeValues := func(ctx *Context) {