play framework (filter, action)

1. KeY concepts of http architect in Play framework

In core play2, the handling of HTTP is :


RequestHeader -> Array[Byte] -> Result 
RequestHeader -> Iteratee[Array[Byte],Result]

The above computation takes the request header RequestHeader, then takes the request body as Array[Byte] and produces a Result. (play doc)

2. action and filter

Most of the requests received by a Play application are handled by an Action.

A play.api.mvc.Action is basically a (play.api.mvc.Request => play.api.mvc.Result) function that handles a request and generates a result to be sent to the client.

A Controller is nothing more than a singleton object that generates Action values. (play doc)

The Filter is applying global filters to each request.

3. Using Filter and action

The filter is used for all the routes. Action can be customized for certain route (on a specific controller)

Using Filter for creating global ratelimter for the API:


public class GlobalRatelimitFilter extends Filter {

    private static final long MAX_REQUESTS = 10;
    private static final long PERIOD = 60;
    private AtomicLong remainingRequests;
    private Instant expirationTime;

    @Inject
    public GlobalRatelimitFilter(Materializer mat) {
        super(mat);
        this.remainingRequests = new AtomicLong(MAX_REQUESTS);
        this.expirationTime = Instant.now().plusSeconds(PERIOD);
    }

    @Override
    public CompletionStage apply(Function<Http.RequestHeader, CompletionStage> next, Http.RequestHeader rh) {

        if (Instant.now().isAfter(this.expirationTime)) {
            this.remainingRequests = new AtomicLong(MAX_REQUESTS);
            this.expirationTime = Instant.now().plusSeconds(PERIOD);
        }
        if (this.remainingRequests.get() > 0L) {
            this.remainingRequests.getAndDecrement();
            return next.apply(rh).thenApply(result -> result.withHeader("X-Remaining-Ratelimit", String.valueOf(this.remainingRequests.get())));
        } else {
            return CompletableFuture.completedFuture(Results.status(429, "too many requests globally"));
        }
    }
}

code github

Using action for creating specific ratelimiter for certain routes:


public class RatelimitAction extends Action {

    private static final String USER_ENDPOINT_RATELIMIT = "user-endpoint-ratelimit";

    @Inject
    @NamedCache("ratelimit-cache")
    private SyncCacheApi cache;

    @Override
    public CompletionStage call(Http.Context ctx) {
        int limit = configuration.limit();
        int period = configuration.period();
        Ratelimit ratelimit = cache.getOrElseUpdate(USER_ENDPOINT_RATELIMIT, () -> new Ratelimit(limit, period));

        if (ratelimit.expired()) {
            ratelimit = new Ratelimit(limit, period);
            cache.set(USER_ENDPOINT_RATELIMIT, ratelimit);
        }
        ratelimit.decrease();

        if (ratelimit.reached()) {
            return CompletableFuture.completedFuture(Results.status(429, "too may requests for /users endpoint"));
        }

        return delegate.call(ctx);
    }
}

Using Action to control the API, Security, verify the requests before it calling the controller.


public class IPStrictAction extends Action {

    @Override
    public CompletionStage call(Http.Context ctx) {

        String[] whiteListIPs = configuration.whiteListIPs();

        String clientHost = ctx.request().host().split(":")[0];

        if(!iPwhitelisted(whiteListIPs, clientHost)){
            CompletableFuture.completedFuture(Results.status(403, "IP not allowed"));
        }

        return delegate.call(ctx);
    }

    private boolean iPwhitelisted(String[] whiteListIPs, String clientHost) {
        return Arrays.asList(whiteListIPs).contains(clientHost);
    }
}

Code Github

Author: aerodc

Software Engineer