diff --git a/.travis.yml b/.travis.yml index 3054d12b..579e611b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,13 @@ +sudo: false language: go + os: - linux - osx go: - 1.14.x + - 1.15.x + - master go_import_path: github.com/kataras/iris/v12 env: global: @@ -11,14 +15,10 @@ env: install: - go get ./... script: - - go test -count=1 -v -cover ./... + - go test -count=1 -v -cover -race ./... after_script: # examples - cd ./_examples - go get ./... - - go test -count=1 -v -cover ./... + - go test -count=1 -v -cover -race ./... - cd ../ - # make sure that the _benchmarks code is working - - cd ./_benchmarks - - go get ./... - - go test -count=1 -v -cover ./... \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 6fb58872..ada69b92 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -359,7 +359,41 @@ Response: Other Improvements: -- `Application.UseRouter(...Handler)` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming requests. Example of use-case: CORS. +- Fix `AutoTLS` when used with `iris.TLSNoRedirect` [*](https://github.com/kataras/iris/issues/1577). The `AutoTLS` runner can be customized through the new `iris.AutoTLSNoRedirect` instead, read its go documentation. Example of having both TLS and non-TLS versions of the same application without conflicts with letsencrypt `./well-known` path: + +```go +package main + +import ( + "net/http" + "time" + + "github.com/kataras/iris/v12" +) + +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + + app.Get("/", func(ctx iris.Context) { + ctx.JSON(iris.Map{ + "time": time.Now().Unix(), + "tls": ctx.Request().TLS != nil, + }) + }) + + var fallbackServer = func(acme func(http.Handler) http.Handler) *http.Server { + srv := &http.Server{Handler: acme(app)} + go srv.ListenAndServe() + return srv + } + + app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com", + iris.AutoTLSNoRedirect(fallbackServer))) +} +``` + +- `Application.UseRouter(...Handler)` - per party to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS. - `*versioning.Group` type is a full `Party` now. @@ -464,7 +498,7 @@ var dirOpts = iris.DirOptions{ - `Context.RemoveCookie` removes also the Request's specific cookie of the same request lifecycle when `iris.CookieAllowReclaim` is set to cookie options, [example](https://github.com/kataras/iris/tree/master/_examples/cookies/options). - `iris.TLS` can now accept certificates in form of raw `[]byte` contents too. -- `iris.TLS` registers a secondary http server which redirects "http://" to their "https://" equivalent requests, unless the new `iris.TLSNoRedirect` host Configurator is provided on `iris.TLS` (or `iris.AutoTLS`), e.g. `app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key", iris.TLSNoRedirect))`. +- `iris.TLS` registers a secondary http server which redirects "http://" to their "https://" equivalent requests, unless the new `iris.TLSNoRedirect` host Configurator is provided on `iris.TLS`, e.g. `app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key", iris.TLSNoRedirect))`. There is `iris.AutoTLSNoRedirect` option for `AutoTLS` too. - Fix an [issue](https://github.com/kataras/i18n/issues/1) about i18n loading from path which contains potential language code. diff --git a/_examples/http-server/listen-letsencrypt/main.go b/_examples/http-server/listen-letsencrypt/main.go index 16505d39..d49be671 100644 --- a/_examples/http-server/listen-letsencrypt/main.go +++ b/_examples/http-server/listen-letsencrypt/main.go @@ -25,8 +25,18 @@ func main() { // and a non-public e-mail instead or edit your hosts file. app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com")) - // Note: to disable automatic "http://" to "https://" redirections pass the `iris.TLSNoRedirect` - // host configurator to TLS or AutoTLS functions, e.g: - // - // app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com", iris.TLSNoRedirect)) + // Note: to disable automatic "http://" to "https://" redirections pass + // the `iris.AutoTLSNoRedirect` host configurator to AutoTLS function, example: + /* + var fallbackServer = func(acme func(http.Handler) http.Handler) *http.Server { + // Use any http.Server and Handler, as long as it's wrapped by `acme` one. + // In that case we share the application through non-tls users too: + srv := &http.Server{Handler: acme(app)} + go srv.ListenAndServe() + return srv + } + + app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com", + iris.AutoTLSNoRedirect(fallbackServer))) + */ } diff --git a/_examples/http-server/listen-tls/main.go b/_examples/http-server/listen-tls/main.go index 5dd1170e..50d56540 100644 --- a/_examples/http-server/listen-tls/main.go +++ b/_examples/http-server/listen-tls/main.go @@ -21,7 +21,7 @@ func main() { app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key")) // Note: to disable automatic "http://" to "https://" redirections pass the `iris.TLSNoRedirect` - // host configurator to TLS or AutoTLS functions, e.g: + // host configurator to TLS function, example: // // app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key", iris.TLSNoRedirect)) } diff --git a/context/context.go b/context/context.go index 32cd8edb..afd0aee3 100644 --- a/context/context.go +++ b/context/context.go @@ -740,7 +740,9 @@ func (ctx *Context) RequestPath(escape bool) string { // return false // } no, it will not work because map is a random peek data structure. -// Host returns the host part of the current URI. +// Host returns the host:port part of the request URI, calls the `Request().Host`. +// To get the subdomain part as well use the `Request().URL.Host` method instead. +// To get the subdomain only use the `Subdomain` method instead. // This method makes use of the `Configuration.HostProxyHeaders` field too. func (ctx *Context) Host() string { for header, ok := range ctx.app.ConfigurationReadOnly().GetHostProxyHeaders() { @@ -762,13 +764,14 @@ func GetHost(r *http.Request) string { return host } + // contains subdomain. return r.URL.Host } // Subdomain returns the subdomain of this request, if any. // Note that this is a fast method which does not cover all cases. func (ctx *Context) Subdomain() (subdomain string) { - host := ctx.Host() + host := ctx.request.URL.Host // ctx.Host() if index := strings.IndexByte(host, '.'); index > 0 { subdomain = host[0:index] } diff --git a/context/handler.go b/context/handler.go index 0dc23470..6e653536 100644 --- a/context/handler.go +++ b/context/handler.go @@ -116,6 +116,31 @@ func HandlerName(h interface{}) string { return trimHandlerName(name) } +// HandlersNames returns a slice of "handlers" names +// separated by commas. Can be used for debugging +// or to determinate if end-developer +// called the same exactly Use/UseRouter/Done... API methods +// so framework can give a warning. +func HandlersNames(handlers ...interface{}) string { + if len(handlers) == 1 { + if hs, ok := handlers[0].(Handlers); ok { + asInterfaces := make([]interface{}, 0, len(hs)) + for _, h := range hs { + asInterfaces = append(asInterfaces, h) + } + + return HandlersNames(asInterfaces...) + } + } + + names := make([]string, 0, len(handlers)) + for _, h := range handlers { + names = append(names, HandlerName(h)) + } + + return strings.Join(names, ",") +} + // HandlerFileLine returns the handler's file and line information. // See `context.HandlerFileLine` to get the file, line of the current running handler in the chain. func HandlerFileLine(h interface{}) (file string, line int) { @@ -304,3 +329,23 @@ func JoinHandlers(h1 Handlers, h2 Handlers) Handlers { copy(newHandlers[nowLen:], h2) return newHandlers } + +// UpsertHandlers like `JoinHandlers` but it does +// NOT copies the handlers entries and it does remove duplicates. +func UpsertHandlers(h1 Handlers, h2 Handlers) Handlers { +reg: + for _, handler := range h2 { + name := HandlerName(handler) + for i, registeredHandler := range h1 { + registeredName := HandlerName(registeredHandler) + if name == registeredName { + h1[i] = handler // replace this handler with the new one. + continue reg // break and continue to the next handler. + } + } + + h1 = append(h1, handler) // or just insert it. + } + + return h1 +} diff --git a/core/host/supervisor.go b/core/host/supervisor.go index 915ebf85..a94edeff 100644 --- a/core/host/supervisor.go +++ b/core/host/supervisor.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "errors" + "fmt" "net" "net/http" "net/url" @@ -30,7 +31,7 @@ type Configurator func(su *Supervisor) // Interfaces are separated to return relative functionality to them. type Supervisor struct { Server *http.Server - disableHTTP1ToHTTP2Redirection bool // if true then no secondary server on `ListenAndServeTLS/AutoTLS` will be registered, exposed through `NoRedirect`. + disableHTTP1ToHTTP2Redirection bool closedManually uint32 // future use, accessed atomically (non-zero means we've called the Shutdown) closedByInterruptHandler uint32 // non-zero means that the end-developer interrupted it by-purpose. manuallyTLS bool // we need that in order to determinate what to output on the console before the server begin. @@ -49,6 +50,21 @@ type Supervisor struct { IgnoredErrors []string onErr []func(error) + // Fallback should return a http.Server, which may already running + // to handle the HTTP/1.1 clients when TLS/AutoTLS. + // On manual TLS the accepted "challengeHandler" just returns the passed handler, + // otherwise it binds to the acme challenge wrapper. + // Example: + // Fallback = func(h func(fallback http.Handler) http.Handler) *http.Server { + // s := &http.Server{ + // Handler: h(myServerHandler), + // ...otherOptions + // } + // go s.ListenAndServe() + // return s + // } + Fallback func(challegeHandler func(fallback http.Handler) http.Handler) *http.Server + // See `iris.Configuration.SocketSharding`. SocketSharding bool } @@ -83,7 +99,7 @@ func (su *Supervisor) Configure(configurators ...Configurator) *Supervisor { return su } -// NoRedirect should be called before `ListenAndServeTLS/AutoTLS` when +// NoRedirect should be called before `ListenAndServeTLS` when // secondary http1 to http2 server is not required. This method will disable // the automatic registration of secondary http.Server // which would redirect "http://" requests to their "https://" equivalent. @@ -295,11 +311,8 @@ func (su *Supervisor) ListenAndServeTLS(certFileOrContents string, keyFileOrCont } } - target, _ := url.Parse("https://" + netutil.ResolveVHost(su.Server.Addr)) // e.g. https://localhost:443 - http1Handler := RedirectHandler(target, http.StatusMovedPermanently) - su.manuallyTLS = true - return su.runTLS(getCertificate, http1Handler) + return su.runTLS(getCertificate, nil) } // ListenAndServeAutoTLS acts identically to ListenAndServe, except that it @@ -346,44 +359,77 @@ func (su *Supervisor) ListenAndServeAutoTLS(domain string, email string, cacheDi Cache: cache, } - return su.runTLS(autoTLSManager.GetCertificate, autoTLSManager.HTTPHandler(nil /* nil for redirect */)) + return su.runTLS(autoTLSManager.GetCertificate, autoTLSManager.HTTPHandler) } -func (su *Supervisor) runTLS(getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error), http1Handler http.Handler) error { - if !su.disableHTTP1ToHTTP2Redirection && http1Handler != nil { - // Note: no need to use a function like ping(":http") to see - // if there is another server running, if it is - // then this server will errored and not start at all. - http1RedirectServer := &http.Server{ - ReadTimeout: 30 * time.Second, - WriteTimeout: 60 * time.Second, - Addr: ":http", - Handler: http1Handler, +func (su *Supervisor) runTLS(getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error), challengeHandler func(fallback http.Handler) http.Handler) error { + if su.manuallyTLS && !su.disableHTTP1ToHTTP2Redirection { + // If manual TLS and auto-redirection is enabled, + // then create an empty challenge handler so the :80 server starts. + challengeHandler = func(h http.Handler) http.Handler { // it is always nil on manual TLS. + target, _ := url.Parse("https://" + netutil.ResolveVHost(su.Server.Addr)) // e.g. https://localhost:443 + http1Handler := RedirectHandler(target, http.StatusMovedPermanently) + return http1Handler + } + } + + if challengeHandler != nil { + http1Server := &http.Server{ + Addr: ":http", + Handler: challengeHandler(nil), // nil for redirection. + ReadTimeout: su.Server.ReadTimeout, + ReadHeaderTimeout: su.Server.ReadHeaderTimeout, + WriteTimeout: su.Server.WriteTimeout, + IdleTimeout: su.Server.IdleTimeout, + MaxHeaderBytes: su.Server.MaxHeaderBytes, + } + + if su.Fallback == nil { + if !su.manuallyTLS && su.disableHTTP1ToHTTP2Redirection { + // automatic redirection was disabled but Fallback was not registered. + return fmt.Errorf("autotls: use iris.AutoTLSNoRedirect instead") + } + go http1Server.ListenAndServe() + } else { + // if it's manual TLS still can have its own Fallback server here, + // the handler will be the redirect one, the difference is that it can run on any port. + srv := su.Fallback(challengeHandler) + if srv == nil { + if !su.manuallyTLS { + return fmt.Errorf("autotls: relies on an HTTP/1.1 server") + } + // for any case the end-developer decided to return nil here, + // we proceed with the automatic redirection. + srv = http1Server + go srv.ListenAndServe() + } else { + if srv.Addr == "" { + srv.Addr = ":http" + } else if !su.manuallyTLS && srv.Addr != ":80" && srv.Addr != ":http" { + return fmt.Errorf("autotls: The HTTP-01 challenge relies on http://%s:80/.well-known/acme-challenge/", netutil.ResolveVHost(su.Server.Addr)) + } + + if srv.Handler == nil { + // handler was nil, caller wanted to change the server's options like read/write timeout. + srv.Handler = http1Server.Handler + go srv.ListenAndServe() // automatically start it, we assume the above ^ + } + http1Server = srv // to register the shutdown event. + } } - // register a shutdown callback to this - // supervisor in order to close the "secondary redirect server" as well. su.RegisterOnShutdown(func() { - // give it some time to close itself... timeout := 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - http1RedirectServer.Shutdown(ctx) + http1Server.Shutdown(ctx) }) - - ln, err := netutil.TCP(":http", su.SocketSharding) - if err != nil { - return err - } - - go http1RedirectServer.Serve(ln) } if su.Server.TLSConfig == nil { // If tls.Config is NOT configured manually through a host configurator, // then create it. su.Server.TLSConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, GetCertificate: getCertificate, PreferServerCipherSuites: true, diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 78f56977..a781f47c 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -153,6 +153,10 @@ func overlapRoute(r *Route, next *Route) { // APIBuilder the visible API for constructing the router // and child routers. type APIBuilder struct { + // parent is the creator of this Party. + // It is nil on Root. + parent *APIBuilder // currently it's used only on UseRouter feature. + // the per-party APIBuilder with DI. apiBuilderDI *APIContainer @@ -184,7 +188,7 @@ type APIBuilder struct { // the per-party relative path. relativePath string - // allowMethods are filled with the `AllowMethods` func. + // allowMethods are filled with the `AllowMethods` method. // They are used to create new routes // per any party's (and its children) routes registered // if the method "x" wasn't registered already via the `Handle` (and its extensions like `Get`, `Post`...). @@ -194,23 +198,64 @@ type APIBuilder struct { handlerExecutionRules ExecutionRules // the per-party (and its children) route registration rule, see `SetRegisterRule`. routeRegisterRule RouteRegisterRule + + // routerFilters field is shared across Parties. Each Party registers + // one or more middlewares to run before the router itself using the `UseRouter` method. + // Each Party calls the shared filter (`partyMatcher`) that decides if its `UseRouter` handlers + // can be executed. By default it's based on party's static path and/or subdomain, + // it can be modified through an `Application.SetPartyMatcher` call + // once before or after routerFilters filled. + // + // The Key is the Party (instance of APIBuilder), + // value wraps the partyFilter + the handlers registered through `UseRouter`. + // See `GetRouterFilters` too. + routerFilters map[Party]*Filter + // partyMatcher field is shared across all Parties, + // can be modified through the Application level only. + // + // It defaults to the internal, simple, "defaultPartyMatcher". + // It applies when "routerFilters" are used. + partyMatcher PartyMatcherFunc } -var _ Party = (*APIBuilder)(nil) -var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler) +var ( + _ Party = (*APIBuilder)(nil) + _ PartyMatcher = (*APIBuilder)(nil) + _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler) +) // NewAPIBuilder creates & returns a new builder // which is responsible to build the API and the router handler. func NewAPIBuilder() *APIBuilder { return &APIBuilder{ - macros: macro.Defaults, - errors: errgroup.New("API Builder"), - relativePath: "/", - routes: new(repository), - apiBuilderDI: &APIContainer{Container: hero.New()}, + parent: nil, + macros: macro.Defaults, + errors: errgroup.New("API Builder"), + relativePath: "/", + routes: new(repository), + apiBuilderDI: &APIContainer{Container: hero.New()}, + routerFilters: make(map[Party]*Filter), + partyMatcher: defaultPartyMatcher, } } +// IsRoot reports whether this Party is the root Application's one. +// It will return false on all children Parties, no exception. +func (api *APIBuilder) IsRoot() bool { + return api.parent == nil +} + +/* If requested: +// GetRoot returns the very first Party (the Application). +func (api *APIBuilder) GetRoot() *APIBuilder { + root := api.parent + for root != nil { + root = api.parent + } + + return root +}*/ + // ConfigureContainer accepts one or more functions that can be used // to configure dependency injection features of this Party // such as register dependency and register handlers that will automatically inject any valid dependency. @@ -565,14 +610,18 @@ func removeDuplicates(elements []string) (result []string) { return result } -// Party groups routes which may have the same prefix and share same handlers, -// returns that new rich subrouter. +// Party returns a new child Party which inherites its +// parent's options and middlewares. +// If "relativePath" matches the parent's one then it returns the current Party. +// A Party groups routes which may have the same prefix or subdomain and share same middlewares. // -// You can even declare a subdomain with relativePath as "mysub." or see `Subdomain`. +// To create a group of routes for subdomains +// use the `Subdomain` or `WildcardSubdomain` methods +// or pass a "relativePath" as "admin." or "*." respectfully. func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party { - // if app.Party("/"), root party, then just add the middlewares - // and return itself. - if api.relativePath == "/" && (relativePath == "" || relativePath == "/") { + // if app.Party("/"), root party or app.Party("/user") == app.Party("/user") + // then just add the middlewares and return itself. + if relativePath == "" || api.relativePath == relativePath { api.Use(handlers...) return api } @@ -614,7 +663,10 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P beginGlobalHandlers: api.beginGlobalHandlers, doneGlobalHandlers: api.doneGlobalHandlers, errors: api.errors, + routerFilters: api.routerFilters, // shared. + partyMatcher: api.partyMatcher, // shared. // per-party/children + parent: api, middleware: middleware, doneHandlers: api.doneHandlers[0:], relativePath: fullpath, @@ -758,6 +810,134 @@ func (api *APIBuilder) GetRouteReadOnlyByPath(tmplPath string) context.RouteRead return r.ReadOnly } +type ( + // PartyMatcherFunc used to build a filter which decides + // if the given Party is responsible to fire its `UseRouter` handlers or not. + // Can be customized through `SetPartyMatcher` method. See `Match` method too. + PartyMatcherFunc func(Party, *context.Context) bool + // PartyMatcher decides if `UseRouter` handlers should be executed or not. + // A different interface becauwe we want to separate + // the Party's public API from `UseRouter` internals. + PartyMatcher interface { + Match(ctx *context.Context) bool + } + // Filter is a wraper for a Router Filter contains information + // for its Party's fullpath, subdomain the Party's + // matcher and the associated handlers to be executed before main router's request handler. + Filter struct { + Party Party // the Party itself + Matcher PartyMatcher // it's a Party, for freedom that can be changed through a custom matcher which accepts the same filter. + Subdomain string + Path string + Handlers context.Handlers + } +) + +// SetPartyMatcher accepts a function which runs against +// a Party and should report whether its `UseRouter` handlers should be executed. +// PartyMatchers are run through parent to children. +// It modifies the default Party filter that decides +// which `UseRouter` middlewares to run before the Router, +// each one of those middlewares can skip `Context.Next` or call `Context.StopXXX` +// to stop the main router from searching for a route match. +// Can be called before or after `UseRouter`, it doesn't matter. +func (api *APIBuilder) SetPartyMatcher(matcherFunc PartyMatcherFunc) { + if matcherFunc == nil { + matcherFunc = defaultPartyMatcher + } + api.partyMatcher = matcherFunc +} + +// Match reports whether the `UseRouter` handlers should be executed. +// Calls its parent's Match if possible. +// Implements the `PartyMatcher` interface. +func (api *APIBuilder) Match(ctx *context.Context) bool { + return api.partyMatcher(api, ctx) +} + +func defaultPartyMatcher(p Party, ctx *context.Context) bool { + subdomain, path := splitSubdomainAndPath(p.GetRelPath()) + staticPath := staticPath(path) + hosts := subdomain != "" + + if p.IsRoot() { + // ALWAYS executed first when registered + // through an `Application.UseRouter` call. + return true + } + + if hosts { + // Note(@kataras): do NOT try to implement something like party matcher for each party + // separately. We will introduce a new problem with subdomain inside a subdomain: + // they are not by prefix, so parenting calls will not help + // e.g. admin. and control.admin, control.admin is a sub of the admin. + if !canHandleSubdomain(ctx, subdomain) { + return false + } + } + + // this is the longest static path. + return strings.HasPrefix(ctx.Path(), staticPath) +} + +// GetRouterFilters returns the global router filters. +// Read `UseRouter` for more. +// The map can be altered before router built. +// The router internally prioritized them by the subdomains and +// longest static path. +// Implements the `RoutesProvider` interface. +func (api *APIBuilder) GetRouterFilters() map[Party]*Filter { + return api.routerFilters +} + +// UseRouter upserts one or more handlers that will be fired +// right before the main router's request handler. +// +// Use this method to register handlers, that can ran +// independently of the incoming request's values, +// that they will be executed ALWAYS against ALL children incoming requests. +// Example of use-case: CORS. +// +// Note that because these are executed before the router itself +// the Context should not have access to the `GetCurrentRoute` +// as it is not decided yet which route is responsible to handle the incoming request. +// It's one level higher than the `WrapRouter`. +// The context SHOULD call its `Next` method in order to proceed to +// the next handler in the chain or the main request handler one. +func (api *APIBuilder) UseRouter(handlers ...context.Handler) { + if len(handlers) == 0 { + return + } + + beginHandlers := context.Handlers(handlers) + // respect any execution rules (begin). + api.handlerExecutionRules.Begin.apply(&beginHandlers) + + if f := api.routerFilters[api]; f != nil && len(f.Handlers) > 0 { // exists. + beginHandlers = context.UpsertHandlers(f.Handlers, beginHandlers) // remove dupls. + } else { + // Note(@kataras): we don't add the parent's filter handlers + // on `Party` method because we need to know if a `UseRouter` call exist + // before prepending the parent's ones and fill a new Filter on `routerFilters`, + // that key should NOT exist on a Party without `UseRouter` handlers (see router.go). + // That's the only reason we need the `parent` field. + if api.parent != nil { + // If it's not root, add the parent's handlers here. + if root, ok := api.routerFilters[api.parent]; ok { + beginHandlers = context.UpsertHandlers(root.Handlers, beginHandlers) + } + } + } + + subdomain, path := splitSubdomainAndPath(api.relativePath) + api.routerFilters[api] = &Filter{ + Matcher: api, + Subdomain: subdomain, + Path: path, + Handlers: beginHandlers, + } +} + // Use appends Handler(s) to the current Party's routes and child routes. // If the current Party is the root, then it registers the middleware to all child Parties' routes too. // @@ -774,19 +954,7 @@ func (api *APIBuilder) Use(handlers ...context.Handler) { // or on the basis of the middleware already existing, // replace that existing middleware instead. func (api *APIBuilder) UseOnce(handlers ...context.Handler) { -reg: - for _, handler := range handlers { - name := context.HandlerName(handler) - for i, registeredHandler := range api.middleware { - registeredName := context.HandlerName(registeredHandler) - if name == registeredName { - api.middleware[i] = handler // replace this handler with the new one. - continue reg // break and continue to the next handler. - } - } - - api.middleware = append(api.middleware, handler) // or just insert it. - } + api.middleware = context.UpsertHandlers(api.middleware, handlers) } // UseGlobal registers handlers that should run at the very beginning. diff --git a/core/router/handler.go b/core/router/handler.go index 12d42e35..8dd8fafc 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -118,6 +118,10 @@ func (h *routerHandler) AddRoute(r *Route) error { type RoutesProvider interface { // api builder GetRoutes() []*Route GetRoute(routeName string) *Route + // GetRouterFilters returns the app's router filters. + // Read `UseRouter` for more. + // The map can be altered before router built. + GetRouterFilters() map[Party]*Filter } func (h *routerHandler) Build(provider RoutesProvider) error { @@ -318,12 +322,12 @@ func bindMultiParamTypesHandler(r *Route) { r.topLink.beginHandlers = append(context.Handlers{decisionHandler}, r.topLink.beginHandlers...) } -func (h *routerHandler) canHandleSubdomain(ctx *context.Context, subdomain string) bool { +func canHandleSubdomain(ctx *context.Context, subdomain string) bool { if subdomain == "" { return true } - requestHost := ctx.Host() + requestHost := ctx.Request().URL.Host if netutil.IsLoopbackSubdomain(requestHost) { // this fixes a bug when listening on // 127.0.0.1:8080 for example @@ -349,7 +353,7 @@ func (h *routerHandler) canHandleSubdomain(ctx *context.Context, subdomain strin return false } // continue to that, any subdomain is valid. - } else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot. + } else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot, e.g. "admin." return false } @@ -396,7 +400,7 @@ func (h *routerHandler) HandleRequest(ctx *context.Context) { continue } - if h.hosts && !h.canHandleSubdomain(ctx, t.subdomain) { + if h.hosts && !canHandleSubdomain(ctx, t.subdomain) { continue } @@ -499,7 +503,7 @@ func (h *routerHandler) FireErrorCode(ctx *context.Context) { continue } - if h.errorHosts && !h.canHandleSubdomain(ctx, t.subdomain) { + if h.errorHosts && !canHandleSubdomain(ctx, t.subdomain) { continue } diff --git a/core/router/party.go b/core/router/party.go index d72bfbac..4051ea49 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -13,6 +13,10 @@ import ( // // Look the `APIBuilder` structure for its implementation. type Party interface { + // IsRoot reports whether this Party is the root Application's one. + // It will return false on all children Parties, no exception. + IsRoot() bool + // ConfigureContainer accepts one or more functions that can be used // to configure dependency injection features of this Party // such as register dependency and register handlers that will automatically inject any valid dependency. @@ -44,10 +48,14 @@ type Party interface { // Look `OnErrorCode` too. OnAnyErrorCode(handlers ...context.Handler) []*Route - // Party groups routes which may have the same prefix and share same handlers, - // returns that new rich subrouter. + // Party returns a new child Party which inherites its + // parent's options and middlewares. + // If "relativePath" matches the parent's one then it returns the current Party. + // A Party groups routes which may have the same prefix or subdomain and share same middlewares. // - // You can even declare a subdomain with relativePath as "mysub." or see `Subdomain`. + // To create a group of routes for subdomains + // use the `Subdomain` or `WildcardSubdomain` methods + // or pass a "relativePath" as "admin." or "*." respectfully. Party(relativePath string, middleware ...context.Handler) Party // PartyFunc same as `Party`, groups routes that share a base path or/and same handlers. // However this function accepts a function that receives this created Party instead. @@ -73,6 +81,21 @@ type Party interface { // So if app.Subdomain("admin").Subdomain("panel") then the result is: "panel.admin.". Subdomain(subdomain string, middleware ...context.Handler) Party + // UseRouter upserts one or more handlers that will be fired + // right before the main router's request handler. + // + // Use this method to register handlers, that can ran + // independently of the incoming request's values, + // that they will be executed ALWAYS against ALL children incoming requests. + // Example of use-case: CORS. + // + // Note that because these are executed before the router itself + // the Context should not have access to the `GetCurrentRoute` + // as it is not decided yet which route is responsible to handle the incoming request. + // It's one level higher than the `WrapRouter`. + // The context SHOULD call its `Next` method in order to proceed to + // the next handler in the chain or the main request handler one. + UseRouter(handlers ...context.Handler) // Use appends Handler(s) to the current Party's routes and child routes. // If the current Party is the root, then it registers the middleware to all child Parties' routes too. Use(middleware ...context.Handler) diff --git a/core/router/path.go b/core/router/path.go index 93a20e1a..5dbb5239 100644 --- a/core/router/path.go +++ b/core/router/path.go @@ -235,6 +235,14 @@ func splitSubdomainAndPath(fullUnparsedPath string) (subdomain string, path stri subdomain = s[0:slashIdx] } + if slashIdx == -1 { + // this will only happen when this function + // is called to Party's relative path (e.g. control.admin.), + // and not a route's one (the route's one always contains a slash). + // return all as subdomain and "/" as path. + return s, "/" + } + path = s[slashIdx:] if !strings.Contains(path, "{") { path = strings.ReplaceAll(path, "//", "/") diff --git a/core/router/router.go b/core/router/router.go index 015f40bd..2fd9071a 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -3,6 +3,8 @@ package router import ( "errors" "net/http" + "sort" + "strings" "sync" "github.com/kataras/iris/v12/context" @@ -19,7 +21,6 @@ import ( type Router struct { mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter, - preHandlers context.Handlers // run before requestHandler, as middleware, same way context's handlers run, see `UseRouter`. requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too. mainHandler http.HandlerFunc // init-accessible wrapperFunc WrapperFunc @@ -87,23 +88,78 @@ func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []st return list } -// UseRouter registers one or more handlers that are fired -// before the main router's request handler. -// -// Use this method to register handlers, that can ran -// independently of the incoming request's method and path values, -// that they will be executed ALWAYS against ALL incoming requests. -// Example of use-case: CORS. -// -// Note that because these are executed before the router itself -// the Context should not have access to the `GetCurrentRoute` -// as it is not decided yet which route is responsible to handle the incoming request. -// It's one level higher than the `WrapRouter`. -// The context SHOULD call its `Next` method in order to proceed to -// the next handler in the chain or the main request handler one. -// ExecutionRules are NOT applied here. -func (router *Router) UseRouter(handlers ...context.Handler) { - router.preHandlers = append(router.preHandlers, handlers...) +func (router *Router) buildMainHandlerWithFilters(routerFilters map[Party]*Filter, cPool *context.Pool, requestHandler RequestHandler) { + sortedFilters := make([]*Filter, 0, len(routerFilters)) + // key was just there to enforce uniqueness on API level. + for _, f := range routerFilters { + sortedFilters = append(sortedFilters, f) + // append it as one handlers so execution rules are being respected in that step too. + f.Handlers = append(f.Handlers, func(ctx *context.Context) { + // set the handler index back to 0 so the route's handlers can be executed as expected. + ctx.HandlerIndex(0) + // execute the main request handler, this will fire the found route's handlers + // or if error the error code's associated handler. + router.requestHandler.HandleRequest(ctx) + }) + } + + sort.SliceStable(sortedFilters, func(i, j int) bool { + left, right := sortedFilters[i], sortedFilters[j] + var ( + leftSubLen = len(left.Subdomain) + rightSubLen = len(right.Subdomain) + + leftSlashLen = strings.Count(left.Path, "/") + rightSlashLen = strings.Count(right.Path, "/") + ) + + if leftSubLen == rightSubLen { + if leftSlashLen > rightSlashLen { + return true + } + } + + if leftSubLen > rightSubLen { + return true + } + + if leftSlashLen > rightSlashLen { + return true + } + + if leftSlashLen == rightSlashLen { + if len(left.Path) > len(right.Path) { + return true + } + return false + } + + return len(left.Path) > len(right.Path) + }) + + router.mainHandler = func(w http.ResponseWriter, r *http.Request) { + ctx := cPool.Acquire(w, r) + + filterExecuted := false + for _, f := range sortedFilters { + // fmt.Printf("Sorted filter execution: [%s] [%s]\n", f.Subdomain, f.Path) + if f.Matcher.Match(ctx) { + // fmt.Printf("Matched [%s] and execute [%d] handlers [%s]\n\n", ctx.Path(), len(f.Handlers), context.HandlersNames(f.Handlers)) + filterExecuted = true + // execute the final handlers chain. + ctx.Do(f.Handlers) + break + } + } + + if !filterExecuted { + // If not at least one match filter found and executed, + // then just run the router. + router.requestHandler.HandleRequest(ctx) + } + + cPool.Release(ctx) + } } // BuildRouter builds the router based on @@ -149,22 +205,9 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan } } - // the important - if len(router.preHandlers) > 0 { - handlers := append(router.preHandlers, func(ctx *context.Context) { - // set the handler index back to 0 so the route's handlers can be executed as exepcted. - ctx.HandlerIndex(0) - // execute the main request handler, this will fire the found route's handlers - // or if error the error code's associated handler. - router.requestHandler.HandleRequest(ctx) - }) - - router.mainHandler = func(w http.ResponseWriter, r *http.Request) { - ctx := cPool.Acquire(w, r) - // execute the final handlers chain. - ctx.Do(handlers) - cPool.Release(ctx) - } + // the important stuff. + if routerFilters := routesProvider.GetRouterFilters(); len(routerFilters) > 0 { + router.buildMainHandlerWithFilters(routerFilters, cPool, requestHandler) } else { router.mainHandler = func(w http.ResponseWriter, r *http.Request) { ctx := cPool.Acquire(w, r) diff --git a/core/router/router_handlers_order_test.go b/core/router/router_handlers_order_test.go index aff3f0ed..057fbdcf 100644 --- a/core/router/router_handlers_order_test.go +++ b/core/router/router_handlers_order_test.go @@ -35,7 +35,13 @@ var ( secondUseHandler = writeHandler(secondUseResponse) firstUseRouterResponse = "userouter1" - firstUseRouterHandler = writeHandler(firstUseRouterResponse) + // Use inline handler, no the `writeHandler`, + // because it will be overriden by `secondUseRouterHandler` otherwise, + // look `UseRouter:context.UpsertHandlers` for more. + firstUseRouterHandler = func(ctx iris.Context) { + ctx.WriteString(firstUseRouterResponse) + ctx.Next() + } secondUseRouterResponse = "userouter2" secondUseRouterHandler = writeHandler(secondUseRouterResponse) @@ -178,3 +184,104 @@ func TestUseRouterStopExecution(t *testing.T) { e = httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusForbidden).Body().Equal("err: custom error") } + +func TestUseRouterParentDisallow(t *testing.T) { + const expectedResponse = "no_userouter_allowed" + + app := iris.New() + app.UseRouter(func(ctx iris.Context) { + ctx.WriteString("always") + ctx.Next() + }) + app.Get("/index", func(ctx iris.Context) { + ctx.WriteString(expectedResponse) + }) + + app.SetPartyMatcher(func(p iris.Party, ctx iris.Context) bool { + // modifies the PartyMatcher to not match any UseRouter, + // tests should receive the handlers response alone. + return false + }) + + app.PartyFunc("/", func(p iris.Party) { // it's the same instance of app. + p.UseRouter(func(ctx iris.Context) { + ctx.WriteString("_2") + ctx.Next() + }) + p.Get("/", func(ctx iris.Context) { + ctx.WriteString(expectedResponse) + }) + }) + + app.PartyFunc("/user", func(p iris.Party) { + p.UseRouter(func(ctx iris.Context) { + ctx.WriteString("_3") + ctx.Next() + }) + + p.Get("/", func(ctx iris.Context) { + ctx.WriteString(expectedResponse) + }) + }) + + e := httptest.New(t, app) + e.GET("/index").Expect().Status(iris.StatusOK).Body().Equal(expectedResponse) + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedResponse) + e.GET("/user").Expect().Status(iris.StatusOK).Body().Equal(expectedResponse) +} + +func TestUseRouterSubdomains(t *testing.T) { + app := iris.New() + app.UseRouter(func(ctx iris.Context) { + if ctx.Subdomain() == "old" { + ctx.Next() // call the router, do not write. + return + } + + // if we write here, it will always give 200 OK, + // even on not registered routes, that's the point at the end, + // full control here when we need it. + ctx.WriteString("always_") + ctx.Next() + }) + + adminAPI := app.Subdomain("admin") + adminAPI.UseRouter(func(ctx iris.Context) { + ctx.WriteString("admin always_") + ctx.Next() + }) + adminAPI.Get("/", func(ctx iris.Context) { + ctx.WriteString("admin") + }) + + adminControlAPI := adminAPI.Subdomain("control") + adminControlAPI.UseRouter(func(ctx iris.Context) { + ctx.WriteString("control admin always_") + ctx.Next() + }) + adminControlAPI.Get("/", func(ctx iris.Context) { + ctx.WriteString("control admin") + }) + + oldAPI := app.Subdomain("old") + oldAPI.Get("/", func(ctx iris.Context) { + ctx.WriteString("chat") + }) + + e := httptest.New(t, app, httptest.URL("http://example.com")) + e.GET("/notfound").Expect().Status(iris.StatusOK).Body().Equal("always_") + + e.GET("/").WithURL("http://admin.example.com").Expect().Status(iris.StatusOK).Body(). + Equal("always_admin always_admin") + + e.GET("/").WithURL("http://control.admin.example.com").Expect().Status(iris.StatusOK).Body(). + Equal("always_admin always_control admin always_control admin") + + // It has a route, and use router just proceeds to the router. + e.GET("/").WithURL("http://old.example.com").Expect().Status(iris.StatusOK).Body(). + Equal("chat") + // this is not a registered path, should fire 404, the UseRouter does not write + // anything to the response writer, so the router has control over it. + e.GET("/notfound").WithURL("http://old.example.com").Expect().Status(iris.StatusNotFound).Body(). + Equal("Not Found") +} diff --git a/go.mod b/go.mod index 23b308c1..c77486e4 100644 --- a/go.mod +++ b/go.mod @@ -31,14 +31,13 @@ require ( github.com/russross/blackfriday/v2 v2.0.1 github.com/ryanuber/columnize v2.1.0+incompatible github.com/schollz/closestmatch v2.1.0+incompatible - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 github.com/yosssi/ace v0.0.5 go.etcd.io/bbolt v1.3.5 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/net v0.0.0-20200707034311-ab3426394381 - golang.org/x/sys v0.0.0-20200802091954-4b90ce9b60b3 + golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9 golang.org/x/text v0.3.3 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e google.golang.org/protobuf v1.25.0 diff --git a/iris.go b/iris.go index dd87d90b..5f84a3b4 100644 --- a/iris.go +++ b/iris.go @@ -589,10 +589,39 @@ func Addr(addr string, hostConfigs ...host.Configurator) Runner { } } -// TLSNoRedirect is a `host.Configurator` which can be passed as last argument -// to the `TLS` and `AutoTLS` functions. It disables the automatic -// registration of redirection from "http://" to "https://" requests. -var TLSNoRedirect = func(su *host.Supervisor) { su.NoRedirect() } +var ( + // TLSNoRedirect is a `host.Configurator` which can be passed as last argument + // to the `TLS` runner function. It disables the automatic + // registration of redirection from "http://" to "https://" requests. + // Applies only to the `TLS` runner. + // See `AutoTLSNoRedirect` to register a custom fallback server for `AutoTLS` runner. + TLSNoRedirect = func(su *host.Supervisor) { su.NoRedirect() } + // AutoTLSNoRedirect is a `host.Configurator`. + // It registers a fallback HTTP/1.1 server for the `AutoTLS` one. + // The function accepts the letsencrypt wrapper and it + // should return a valid instance of http.Server which its handler should be the result + // of the "acmeHandler" wrapper. + // Usage: + // getServer := func(acme func(http.Handler) http.Handler) *http.Server { + // srv := &http.Server{Handler: acme(yourCustomHandler), ...otherOptions} + // go srv.ListenAndServe() + // return srv + // } + // app.Run(iris.AutoTLS(":443", "example.com example2.com", "mail@example.com", getServer)) + // + // Note that if Server.Handler is nil then the server is automatically ran + // by the framework and the handler set to automatic redirection, it's still + // a valid option when the caller wants just to customize the server's fields (except Addr). + // With this host configurator the caller can customize the server + // that letsencrypt relies to perform the challenge. + // LetsEncrypt Certification Manager relies on http://%s:80/.well-known/acme-challenge/. + AutoTLSNoRedirect = func(getFallbackServer func(acmeHandler func(fallback http.Handler) http.Handler) *http.Server) host.Configurator { + return func(su *host.Supervisor) { + su.NoRedirect() + su.Fallback = getFallbackServer + } + } +) // TLS can be used as an argument for the `Run` method. // It will start the Application's secure server.