New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add applying rules on middleware #1170

Open
zheeeng opened this Issue Jan 8, 2019 · 8 comments

Comments

Projects
None yet
2 participants
@zheeeng
Copy link

zheeeng commented Jan 8, 2019

Can we have a simple way to use a middleware on a specific handler? I made a custom middleware but only want to apply it on PostUser handler. Hope there is an approach/ built-in rules configuration to simplify codes below:

func Middleware(ctx iris.Context) {
    ctx.Next()
}

func CreateMiddleware(when func(ctx iris.Context) bool) func(ctx iris.Context) {

	return func(ctx iris.Context) {
		use := when(ctx)

		if use {
			Middleware(ctx)
		} else {
			ctx.Next()
		}
	}
}
// In route party 

m := middleware.CreateMiddleware(func(ctx iris.Context) bool {
	handlerNameChunks := strings.Split(ctx.GetCurrentRoute().MainHandlerName(), ".")
	hLen := len(handlerNameChunks)

	return hLen > 0 && handlerNameChunks[len(handlerNameChunks)-1] == "PostUser"
})

app.Router.Use(m)
@kataras

This comment has been minimized.

Copy link
Owner

kataras commented Jan 8, 2019

A middleware is just a function, if you want to call it you can refer to its variable and call it as you do on the first example: myMiddleware(ctx); return <- executed, done.

You think it all wrong my friend, do not depend on the handler name to decide if you want to execute a middleware or not, design your API smarter, iris helps you on this a lot, make checks based on the request url, parameters, form data, anything.

Also I don't understand the "apply middleware" in the title, to apply/execute it you just have to call it, nothing crazy, you pass the current context, iris takes cover of everything else.

You can even add a handler for a specific request in runtime or do something like next or if not next handler exist then do that X if you want so, check the context api for handlers:

	// Do calls the SetHandlers(handlers)
	// and executes the first handler,
	// handlers should not be empty.
	//
	// It's used by the router, developers may use that
	// to replace and execute handlers immediately.
	Do(Handlers)

	// AddHandler can add handler(s)
	// to the current request in serve-time,
	// these handlers are not persistenced to the router.
	//
	// Router is calling this function to add the route's handler.
	// If AddHandler called then the handlers will be inserted
	// to the end of the already-defined route's handler.
	//
	AddHandler(...Handler)
	// SetHandlers replaces all handlers with the new.
	SetHandlers(Handlers)
	// Handlers keeps tracking of the current handlers.
	Handlers() Handlers

	// HandlerIndex sets the current index of the
	// current context's handlers chain.
	// If -1 passed then it just returns the
	// current handler index without change the current index.
	//
	// Look Handlers(), Next() and StopExecution() too.
	HandlerIndex(n int) (currentIndex int)
	// Proceed is an alternative way to check if a particular handler
	// has been executed and called the `ctx.Next` function inside it.
	// This is useful only when you run a handler inside
	// another handler. It justs checks for before index and the after index.
	//
	// A usecase example is when you want to execute a middleware
	// inside controller's `BeginRequest` that calls the `ctx.Next` inside it.
	// The Controller looks the whole flow (BeginRequest, method handler, EndRequest)
	// as one handler, so `ctx.Next` will not be reflected to the method handler
	// if called from the `BeginRequest`.
	//
	// Although `BeginRequest` should NOT be used to call other handlers,
	// the `BeginRequest` has been introduced to be able to set
	// common data to all method handlers before their execution.
	// Controllers can accept middleware(s) from the MVC's Application's Router as normally.
	//
	// That said let's see an example of `ctx.Proceed`:
	//
	// var authMiddleware = basicauth.New(basicauth.Config{
	// 	Users: map[string]string{
	// 		"admin": "password",
	// 	},
	// })
	//
	// func (c *UsersController) BeginRequest(ctx iris.Context) {
	// 	if !ctx.Proceed(authMiddleware) {
	// 		ctx.StopExecution()
	// 	}
	// }
	// This Get() will be executed in the same handler as `BeginRequest`,
	// internally controller checks for `ctx.StopExecution`.
	// So it will not be fired if BeginRequest called the `StopExecution`.
	// func(c *UsersController) Get() []models.User {
	//	  return c.Service.GetAll()
	//}
	// Alternative way is `!ctx.IsStopped()` if middleware make use of the `ctx.StopExecution()` on failure.
	Proceed(Handler) bool

When Handlers is a list of []func(iris.Context). See more at the context/context.go (which is the fullpath of the iris.Context).

You can also change how middlewares are executed, you can omit the ctx.Next at all using the Execution Rules introduced on version 10.6.5. Iris is probably the only one go web framework providing so rich support for extensibility and customization -- as you go deeper you will see it even more, you will never go back.

Show me more so I can help you, what conditions do you have for executing a middleware or not in that case?

@kataras kataras added the question label Jan 8, 2019

@zheeeng

This comment has been minimized.

Copy link

zheeeng commented Jan 8, 2019

My controller implemented GET, POST, PUT and DELETE method handlers, I want to deny some external network request to access part APIs of them, e.g. modifying operations.
I made a middleware check whether the remote address is of A class network, I want the middleware only be applied to the POST, PUT and DELETE methods on user route, only allow internal network to change the user status and leave the GET method for all requests no matter what kind of network they are.

@zheeeng

This comment has been minimized.

Copy link

zheeeng commented Jan 8, 2019

I know the current implementation of my snippet is not very smart, so I request adding some configuration on applying middlewares on conditions. Allow us to specify the method, route, header, params, query and other conditions to enable middleware.

Here is a link as for reference: https://docs.nestjs.com/middleware
At the Middleware consumer section, NestJS leaves us the rules to decide whether use middleware or not:

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'cats', method: RequestMethod.GET },
        { path: 'cats', method: RequestMethod.POST },
      )
      .forRoutes(CatsController);
  }
}

BTW, I'm using iris MVC, seems I can't add middlewares to a specific request handler once I passed the entire route party to a controller? Does I have only one way to use a middleware that partyApp.Router.Use(middleware)? I tried adding partyApp.Router.POST('user', middleware) for prepending a middleware to controler::PostUser, it doesn't work, am I wrong with this usage?

@zheeeng zheeeng changed the title Feature: Add apply rule on middleware Feature: Add applying rules on middleware Jan 8, 2019

@kataras

This comment has been minimized.

Copy link
Owner

kataras commented Jan 11, 2019

@zheeeng this doesn't work for you?:

Does I have only one way to use a middleware that partyApp.Router.Use(middleware)?

No, mvc depends on a Party, add the middleware there as you normally do, but you can also do it inside your controller, I will show you an example that is doing that thing you want to do:

app := iris.New()
// @zheeeng I want the middleware only be applied to the POST, PUT and DELETE
// methods on user route...
userRouter := app.Party("/users", func(ctx iris.Context) {
    if method:= ctx.Method(); method == iris.MethodPost || method == iris.MethodPut || method == iris.MethodDelete {
    ctx.Next()
    return
  }
}))
mvc.New(userRouter).Handle(new(UserController))

app.Run(iris.Addr(":8080"))

MVC + Middleware examples that can definitely complete all your needs: https://github.com/kataras/iris/blob/master/_examples/mvc/middleware/per-method/main.go#L5

@zheeeng

This comment has been minimized.

Copy link

zheeeng commented Jan 12, 2019

Made a high order func helper

package irisutils

import "github.com/kataras/iris"

func CreateConditionalMiddleware(
	middleware func(ctx iris.Context),
	condition func(ctx iris.Context) bool,
) func(ctx iris.Context) {

	return func(ctx iris.Context) {
		pass := condition(ctx)

		if pass {
			middleware(ctx)
		} else {
			ctx.Next()
		}
	}
}
@kataras

This comment has been minimized.

Copy link
Owner

kataras commented Jan 12, 2019

OK I will name this 'Filter': func(Context) bool, sounds good?

@zheeeng

This comment has been minimized.

Copy link

zheeeng commented Jan 12, 2019

I'm not good at naming. filter or predicate is better than the exampled. If there is a more convenient func helper for specifying the route will be appreciated.

@kataras

This comment has been minimized.

Copy link
Owner

kataras commented Jan 19, 2019

OK, this is not as simple as the example you gave because in Iris you can modify the handlers per-request and take information about the current chain state at runtime as well (see ctx.HandlerIndex,SetHandlers,Do, AddHandler,Handlers,NextOr,NextOrNotFound,), so we must provide these information on the next handler and not just fire the handlers, I made it quitely and easy. It is ready for the next, upcoming version v11.2.0. The snippet, if you don't want to wait for the next version:

// Filter is just a type of func(Handler) bool which reports whether an action must be performed
// based on the incoming request.
//
// See `NewConditionalHandler` for more.
type Filter func(iris.Context) bool

// NewConditionalHandler returns a single Handler which can be registered
// as a middleware.
// Filter is just a type of Handler which returns a boolean.
// Handlers here should act like middleware, they should contain `ctx.Next` to proceed
// to the next handler of the chain. Those "handlers" are registered to the per-request context.
//
// It checks the "filter" and if passed then
// it, correctly, executes the "handlers".
//
// If passed, this function makes sure that the Context's information
// about its per-request handler chain based on the new "handlers" is always updated.
//
// If not passed, then simply the Next handler(if any) is executed and "handlers" are ignored.
func NewConditionalHandler(filter Filter, handlers ...iris.Handler) Handler {
	return func(ctx iris.Context) {
		if filter(ctx) {
			// Note that we don't want just to fire the incoming handlers, we must make sure
			// that it won't break any further handler chain
			// information that may be required for the next handlers.
			//
			// The below code makes sure that this conditional handler does not break
			// the ability that iris provides to its end-devs
			// to check and modify the per-request handlers chain at runtime.
			currIdx := ctx.HandlerIndex(-1)
			currHandlers := ctx.Handlers()
			if currIdx == len(currHandlers)-1 {
				// if this is the last handler of the chain
				// just add to the last the new handlers and call Next to fire those.
				ctx.AddHandler(handlers...)
				ctx.Next()
				return
			}
			// otherwise insert the new handlers in the middle of the current executed chain and the next chain.
			newHandlers := append(currHandlers[:currIdx], append(handlers, currHandlers[currIdx+1:]...)...)
			ctx.SetHandlers(newHandlers)
			ctx.Next()
			return
		}
		// if not pass, then just execute the next.
		ctx.Next()
	}
}

And an example of this simple thing:

package main

import (
	"github.com/kataras/iris"
)

func main() {
	app := iris.New()
	v1 := app.Party("/api/v1")

	myFilter := func(ctx iris.Context) bool {
		// ofc... don't do that on production, use session or/and database calls and etc.
		ok, _ := ctx.URLParamBool("admin")
		return ok
	}

	onlyWhenFilter1 := func(ctx iris.Context) {
		ctx.Application().Logger().Infof("admin: %s", ctx.Params())
		ctx.Next()
	}

	onlyWhenFilter2 := func(ctx iris.Context) {
		// You can always use the per-request storage
		// to perform actions like this ofc.
		//
		// this handler: ctx.Values().Set("is_admin", true)
		// next handler: isAdmin := ctx.Values().GetBoolDefault("is_admin", false)
		//
		// but, let's simplify it:
		ctx.HTML("<h1>Hello Admin</h1><br>")
		ctx.Next()
	}

	// HERE:
	// It can be registered anywhere, as a middleware.
	// It will fire the `onlyWhenFilter1` and `onlyWhenFilter2` as middlewares (with ctx.Next())
	// if myFilter pass otherwise it will just continue the handler chain with ctx.Next() by ignoring
	// the `onlyWhenFilter1` and `onlyWhenFilter2`.
	myMiddleware := NewConditionalHandler(myFilter, onlyWhenFilter1, onlyWhenFilter2)

	v1UsersRouter := v1.Party("/users", myMiddleware)
	v1UsersRouter.Get("/", func(ctx iris.Context) {
		ctx.HTML("requested: <b>/api/v1/users</b>")
	})

	// http://localhost:8080/api/v1/users
	// http://localhost:8080/api/v1/users?admin=true
	app.Run(iris.Addr(":8080"))
}

Thanks for the feature request @zheeeng!

kataras added a commit that referenced this issue Jan 19, 2019

@kataras kataras added this to the v11.2.0 milestone Jan 19, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment