General Improvements (UseRouter per Party, fix AutoTLS). Read HISTORY.md

relative to: https://github.com/kataras/iris/issues/1577 and https://github.com/kataras/iris/issues/1578
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-08-12 07:20:07 +03:00
parent da029d6f37
commit 0761bc35ee
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
15 changed files with 639 additions and 120 deletions

View File

@ -1,9 +1,13 @@
sudo: false
language: go language: go
os: os:
- linux - linux
- osx - osx
go: go:
- 1.14.x - 1.14.x
- 1.15.x
- master
go_import_path: github.com/kataras/iris/v12 go_import_path: github.com/kataras/iris/v12
env: env:
global: global:
@ -11,14 +15,10 @@ env:
install: install:
- go get ./... - go get ./...
script: script:
- go test -count=1 -v -cover ./... - go test -count=1 -v -cover -race ./...
after_script: after_script:
# examples # examples
- cd ./_examples - cd ./_examples
- go get ./... - go get ./...
- go test -count=1 -v -cover ./... - go test -count=1 -v -cover -race ./...
- cd ../ - cd ../
# make sure that the _benchmarks code is working
- cd ./_benchmarks
- go get ./...
- go test -count=1 -v -cover ./...

View File

@ -359,7 +359,41 @@ Response:
Other Improvements: 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. - `*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). - `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` 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. - Fix an [issue](https://github.com/kataras/i18n/issues/1) about i18n loading from path which contains potential language code.

View File

@ -25,8 +25,18 @@ func main() {
// and a non-public e-mail instead or edit your hosts file. // and a non-public e-mail instead or edit your hosts file.
app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com")) app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com"))
// Note: to disable automatic "http://" to "https://" redirections pass the `iris.TLSNoRedirect` // Note: to disable automatic "http://" to "https://" redirections pass
// host configurator to TLS or AutoTLS functions, e.g: // the `iris.AutoTLSNoRedirect` host configurator to AutoTLS function, example:
// /*
// app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com", iris.TLSNoRedirect)) 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)))
*/
} }

View File

@ -21,7 +21,7 @@ func main() {
app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key")) 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` // 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)) // app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key", iris.TLSNoRedirect))
} }

View File

@ -740,7 +740,9 @@ func (ctx *Context) RequestPath(escape bool) string {
// return false // return false
// } no, it will not work because map is a random peek data structure. // } 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. // This method makes use of the `Configuration.HostProxyHeaders` field too.
func (ctx *Context) Host() string { func (ctx *Context) Host() string {
for header, ok := range ctx.app.ConfigurationReadOnly().GetHostProxyHeaders() { for header, ok := range ctx.app.ConfigurationReadOnly().GetHostProxyHeaders() {
@ -762,13 +764,14 @@ func GetHost(r *http.Request) string {
return host return host
} }
// contains subdomain.
return r.URL.Host return r.URL.Host
} }
// Subdomain returns the subdomain of this request, if any. // Subdomain returns the subdomain of this request, if any.
// Note that this is a fast method which does not cover all cases. // Note that this is a fast method which does not cover all cases.
func (ctx *Context) Subdomain() (subdomain string) { func (ctx *Context) Subdomain() (subdomain string) {
host := ctx.Host() host := ctx.request.URL.Host // ctx.Host()
if index := strings.IndexByte(host, '.'); index > 0 { if index := strings.IndexByte(host, '.'); index > 0 {
subdomain = host[0:index] subdomain = host[0:index]
} }

View File

@ -116,6 +116,31 @@ func HandlerName(h interface{}) string {
return trimHandlerName(name) 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. // 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. // See `context.HandlerFileLine` to get the file, line of the current running handler in the chain.
func HandlerFileLine(h interface{}) (file string, line int) { func HandlerFileLine(h interface{}) (file string, line int) {
@ -304,3 +329,23 @@ func JoinHandlers(h1 Handlers, h2 Handlers) Handlers {
copy(newHandlers[nowLen:], h2) copy(newHandlers[nowLen:], h2)
return newHandlers 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
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -30,7 +31,7 @@ type Configurator func(su *Supervisor)
// Interfaces are separated to return relative functionality to them. // Interfaces are separated to return relative functionality to them.
type Supervisor struct { type Supervisor struct {
Server *http.Server 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) 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. 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. 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 IgnoredErrors []string
onErr []func(error) 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`. // See `iris.Configuration.SocketSharding`.
SocketSharding bool SocketSharding bool
} }
@ -83,7 +99,7 @@ func (su *Supervisor) Configure(configurators ...Configurator) *Supervisor {
return su 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 // secondary http1 to http2 server is not required. This method will disable
// the automatic registration of secondary http.Server // the automatic registration of secondary http.Server
// which would redirect "http://" requests to their "https://" equivalent. // 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 su.manuallyTLS = true
return su.runTLS(getCertificate, http1Handler) return su.runTLS(getCertificate, nil)
} }
// ListenAndServeAutoTLS acts identically to ListenAndServe, except that it // ListenAndServeAutoTLS acts identically to ListenAndServe, except that it
@ -346,44 +359,77 @@ func (su *Supervisor) ListenAndServeAutoTLS(domain string, email string, cacheDi
Cache: cache, 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 { func (su *Supervisor) runTLS(getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error), challengeHandler func(fallback http.Handler) http.Handler) error {
if !su.disableHTTP1ToHTTP2Redirection && http1Handler != nil { if su.manuallyTLS && !su.disableHTTP1ToHTTP2Redirection {
// Note: no need to use a function like ping(":http") to see // If manual TLS and auto-redirection is enabled,
// if there is another server running, if it is // then create an empty challenge handler so the :80 server starts.
// then this server will errored and not start at all. challengeHandler = func(h http.Handler) http.Handler { // it is always nil on manual TLS.
http1RedirectServer := &http.Server{ target, _ := url.Parse("https://" + netutil.ResolveVHost(su.Server.Addr)) // e.g. https://localhost:443
ReadTimeout: 30 * time.Second, http1Handler := RedirectHandler(target, http.StatusMovedPermanently)
WriteTimeout: 60 * time.Second, return http1Handler
Addr: ":http", }
Handler: 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() { su.RegisterOnShutdown(func() {
// give it some time to close itself...
timeout := 10 * time.Second timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() 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 su.Server.TLSConfig == nil {
// If tls.Config is NOT configured manually through a host configurator, // If tls.Config is NOT configured manually through a host configurator,
// then create it. // then create it.
su.Server.TLSConfig = &tls.Config{ su.Server.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
GetCertificate: getCertificate, GetCertificate: getCertificate,
PreferServerCipherSuites: true, PreferServerCipherSuites: true,

View File

@ -153,6 +153,10 @@ func overlapRoute(r *Route, next *Route) {
// APIBuilder the visible API for constructing the router // APIBuilder the visible API for constructing the router
// and child routers. // and child routers.
type APIBuilder struct { 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. // the per-party APIBuilder with DI.
apiBuilderDI *APIContainer apiBuilderDI *APIContainer
@ -184,7 +188,7 @@ type APIBuilder struct {
// the per-party relative path. // the per-party relative path.
relativePath string relativePath string
// allowMethods are filled with the `AllowMethods` func. // allowMethods are filled with the `AllowMethods` method.
// They are used to create new routes // They are used to create new routes
// per any party's (and its children) routes registered // 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`...). // 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 handlerExecutionRules ExecutionRules
// the per-party (and its children) route registration rule, see `SetRegisterRule`. // the per-party (and its children) route registration rule, see `SetRegisterRule`.
routeRegisterRule RouteRegisterRule 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 (
var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler) _ Party = (*APIBuilder)(nil)
_ PartyMatcher = (*APIBuilder)(nil)
_ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handler (routerHandler)
)
// NewAPIBuilder creates & returns a new builder // NewAPIBuilder creates & returns a new builder
// which is responsible to build the API and the router handler. // which is responsible to build the API and the router handler.
func NewAPIBuilder() *APIBuilder { func NewAPIBuilder() *APIBuilder {
return &APIBuilder{ return &APIBuilder{
parent: nil,
macros: macro.Defaults, macros: macro.Defaults,
errors: errgroup.New("API Builder"), errors: errgroup.New("API Builder"),
relativePath: "/", relativePath: "/",
routes: new(repository), routes: new(repository),
apiBuilderDI: &APIContainer{Container: hero.New()}, 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 // ConfigureContainer accepts one or more functions that can be used
// to configure dependency injection features of this Party // to configure dependency injection features of this Party
// such as register dependency and register handlers that will automatically inject any valid dependency. // 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 return result
} }
// Party groups routes which may have the same prefix and share same handlers, // Party returns a new child Party which inherites its
// returns that new rich subrouter. // 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 { func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party {
// if app.Party("/"), root party, then just add the middlewares // if app.Party("/"), root party or app.Party("/user") == app.Party("/user")
// and return itself. // then just add the middlewares and return itself.
if api.relativePath == "/" && (relativePath == "" || relativePath == "/") { if relativePath == "" || api.relativePath == relativePath {
api.Use(handlers...) api.Use(handlers...)
return api return api
} }
@ -614,7 +663,10 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
beginGlobalHandlers: api.beginGlobalHandlers, beginGlobalHandlers: api.beginGlobalHandlers,
doneGlobalHandlers: api.doneGlobalHandlers, doneGlobalHandlers: api.doneGlobalHandlers,
errors: api.errors, errors: api.errors,
routerFilters: api.routerFilters, // shared.
partyMatcher: api.partyMatcher, // shared.
// per-party/children // per-party/children
parent: api,
middleware: middleware, middleware: middleware,
doneHandlers: api.doneHandlers[0:], doneHandlers: api.doneHandlers[0:],
relativePath: fullpath, relativePath: fullpath,
@ -758,6 +810,134 @@ func (api *APIBuilder) GetRouteReadOnlyByPath(tmplPath string) context.RouteRead
return r.ReadOnly 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. // 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. // 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, // or on the basis of the middleware already existing,
// replace that existing middleware instead. // replace that existing middleware instead.
func (api *APIBuilder) UseOnce(handlers ...context.Handler) { func (api *APIBuilder) UseOnce(handlers ...context.Handler) {
reg: api.middleware = context.UpsertHandlers(api.middleware, handlers)
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.
}
} }
// UseGlobal registers handlers that should run at the very beginning. // UseGlobal registers handlers that should run at the very beginning.

View File

@ -118,6 +118,10 @@ func (h *routerHandler) AddRoute(r *Route) error {
type RoutesProvider interface { // api builder type RoutesProvider interface { // api builder
GetRoutes() []*Route GetRoutes() []*Route
GetRoute(routeName string) *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 { 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...) 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 == "" { if subdomain == "" {
return true return true
} }
requestHost := ctx.Host() requestHost := ctx.Request().URL.Host
if netutil.IsLoopbackSubdomain(requestHost) { if netutil.IsLoopbackSubdomain(requestHost) {
// this fixes a bug when listening on // this fixes a bug when listening on
// 127.0.0.1:8080 for example // 127.0.0.1:8080 for example
@ -349,7 +353,7 @@ func (h *routerHandler) canHandleSubdomain(ctx *context.Context, subdomain strin
return false return false
} }
// continue to that, any subdomain is valid. // 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 return false
} }
@ -396,7 +400,7 @@ func (h *routerHandler) HandleRequest(ctx *context.Context) {
continue continue
} }
if h.hosts && !h.canHandleSubdomain(ctx, t.subdomain) { if h.hosts && !canHandleSubdomain(ctx, t.subdomain) {
continue continue
} }
@ -499,7 +503,7 @@ func (h *routerHandler) FireErrorCode(ctx *context.Context) {
continue continue
} }
if h.errorHosts && !h.canHandleSubdomain(ctx, t.subdomain) { if h.errorHosts && !canHandleSubdomain(ctx, t.subdomain) {
continue continue
} }

View File

@ -13,6 +13,10 @@ import (
// //
// Look the `APIBuilder` structure for its implementation. // Look the `APIBuilder` structure for its implementation.
type Party interface { 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 // ConfigureContainer accepts one or more functions that can be used
// to configure dependency injection features of this Party // to configure dependency injection features of this Party
// such as register dependency and register handlers that will automatically inject any valid dependency. // such as register dependency and register handlers that will automatically inject any valid dependency.
@ -44,10 +48,14 @@ type Party interface {
// Look `OnErrorCode` too. // Look `OnErrorCode` too.
OnAnyErrorCode(handlers ...context.Handler) []*Route OnAnyErrorCode(handlers ...context.Handler) []*Route
// Party groups routes which may have the same prefix and share same handlers, // Party returns a new child Party which inherites its
// returns that new rich subrouter. // 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 Party(relativePath string, middleware ...context.Handler) Party
// PartyFunc same as `Party`, groups routes that share a base path or/and same handlers. // 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. // 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.". // So if app.Subdomain("admin").Subdomain("panel") then the result is: "panel.admin.".
Subdomain(subdomain string, middleware ...context.Handler) Party 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. // 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. // If the current Party is the root, then it registers the middleware to all child Parties' routes too.
Use(middleware ...context.Handler) Use(middleware ...context.Handler)

View File

@ -235,6 +235,14 @@ func splitSubdomainAndPath(fullUnparsedPath string) (subdomain string, path stri
subdomain = s[0:slashIdx] 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:] path = s[slashIdx:]
if !strings.Contains(path, "{") { if !strings.Contains(path, "{") {
path = strings.ReplaceAll(path, "//", "/") path = strings.ReplaceAll(path, "//", "/")

View File

@ -3,6 +3,8 @@ package router
import ( import (
"errors" "errors"
"net/http" "net/http"
"sort"
"strings"
"sync" "sync"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
@ -19,7 +21,6 @@ import (
type Router struct { type Router struct {
mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter, 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. requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too.
mainHandler http.HandlerFunc // init-accessible mainHandler http.HandlerFunc // init-accessible
wrapperFunc WrapperFunc wrapperFunc WrapperFunc
@ -87,23 +88,78 @@ func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []st
return list return list
} }
// UseRouter registers one or more handlers that are fired func (router *Router) buildMainHandlerWithFilters(routerFilters map[Party]*Filter, cPool *context.Pool, requestHandler RequestHandler) {
// before the main router's request handler. sortedFilters := make([]*Filter, 0, len(routerFilters))
// // key was just there to enforce uniqueness on API level.
// Use this method to register handlers, that can ran for _, f := range routerFilters {
// independently of the incoming request's method and path values, sortedFilters = append(sortedFilters, f)
// that they will be executed ALWAYS against ALL incoming requests. // append it as one handlers so execution rules are being respected in that step too.
// Example of use-case: CORS. 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.
// Note that because these are executed before the router itself ctx.HandlerIndex(0)
// the Context should not have access to the `GetCurrentRoute` // execute the main request handler, this will fire the found route's handlers
// as it is not decided yet which route is responsible to handle the incoming request. // or if error the error code's associated handler.
// It's one level higher than the `WrapRouter`. router.requestHandler.HandleRequest(ctx)
// 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) { sort.SliceStable(sortedFilters, func(i, j int) bool {
router.preHandlers = append(router.preHandlers, handlers...) 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 // BuildRouter builds the router based on
@ -149,22 +205,9 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan
} }
} }
// the important // the important stuff.
if len(router.preHandlers) > 0 { if routerFilters := routesProvider.GetRouterFilters(); len(routerFilters) > 0 {
handlers := append(router.preHandlers, func(ctx *context.Context) { router.buildMainHandlerWithFilters(routerFilters, cPool, requestHandler)
// 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)
}
} else { } else {
router.mainHandler = func(w http.ResponseWriter, r *http.Request) { router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r) ctx := cPool.Acquire(w, r)

View File

@ -35,7 +35,13 @@ var (
secondUseHandler = writeHandler(secondUseResponse) secondUseHandler = writeHandler(secondUseResponse)
firstUseRouterResponse = "userouter1" 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" secondUseRouterResponse = "userouter2"
secondUseRouterHandler = writeHandler(secondUseRouterResponse) secondUseRouterHandler = writeHandler(secondUseRouterResponse)
@ -178,3 +184,104 @@ func TestUseRouterStopExecution(t *testing.T) {
e = httptest.New(t, app) e = httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusForbidden).Body().Equal("err: custom error") 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")
}

3
go.mod
View File

@ -31,14 +31,13 @@ require (
github.com/russross/blackfriday/v2 v2.0.1 github.com/russross/blackfriday/v2 v2.0.1
github.com/ryanuber/columnize v2.1.0+incompatible github.com/ryanuber/columnize v2.1.0+incompatible
github.com/schollz/closestmatch 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/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1
github.com/yosssi/ace v0.0.5 github.com/yosssi/ace v0.0.5
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/net v0.0.0-20200707034311-ab3426394381 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/text v0.3.3
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
google.golang.org/protobuf v1.25.0 google.golang.org/protobuf v1.25.0

37
iris.go
View File

@ -589,10 +589,39 @@ func Addr(addr string, hostConfigs ...host.Configurator) Runner {
} }
} }
// TLSNoRedirect is a `host.Configurator` which can be passed as last argument var (
// to the `TLS` and `AutoTLS` functions. It disables the automatic // TLSNoRedirect is a `host.Configurator` which can be passed as last argument
// registration of redirection from "http://" to "https://" requests. // to the `TLS` runner function. It disables the automatic
var TLSNoRedirect = func(su *host.Supervisor) { su.NoRedirect() } // 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/<TOKEN>.
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. // TLS can be used as an argument for the `Run` method.
// It will start the Application's secure server. // It will start the Application's secure server.