Custom rate-limiting policy for OSB services

A rate limiting policy is a policy which you can configure so that certain clients can only make a certain amount of calls on your service. For example you can make it SLA based. Another benefit is that you can prevent clients to flood your service which requests which can overload a backend system. This could be accidental or it might be on purpose (DDOS attack). Most API platforms like Mulesoft, Dell Boomi and Oracle API Platform Cloud Service have these out-of-the-box but the On-Prem version of SOA Suite doesn’t come out of the box with such a policy. Time to make one of our own.

LimitSign

Implementation

In a previous post, I created a policy which could send data to an Elastic stack. See here. Again we are going to create a custom policy to check if a certain type of IP isn’t doing too much calls within 1 minute.

I am going to re-use the BaseAssertionExecutor of last time and make a new implementation. I am going to make the amount of calls configurable. So every unique IP can a certain amount of calls per minute. For easy testing purposes, my default is 10. See here the code for my RateLimitingAssertionExecutor:

package nl.redrock.policy;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import oracle.wsm.common.sdk.IContext;
import oracle.wsm.common.sdk.IMessageContext;
import oracle.wsm.common.sdk.IResult;
import oracle.wsm.common.sdk.Result;
import oracle.wsm.common.sdk.WSMException;
import oracle.wsm.policy.model.IAssertion;
import oracle.wsm.policyengine.IExecutionContext;

/**
 * A Rate limiting policy which can be configured for the amount of calls per unique ip.
 * @author Hugo Hendriks
 */
public class RateLimitingAssertionExecutor extends BaseAssertionExecutor {

    private int limit;
    private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private Map<String, RateLimitSetting> rateLimitSettings = new HashMap();

    public RateLimitingAssertionExecutor() {
        super("RateLimitingExecutor");
    }

    @Override
    public void init(IAssertion iAssertion, IExecutionContext iExecutionContext, IContext iContext) {
        super.init(iAssertion, iExecutionContext, iContext);
        this.limit = Integer.parseInt(super.getPolicyBindingProperty("limit"));
    }

    @Override
    public IResult execute(IContext iContext) throws WSMException {
        IResult result = new Result();

        try {
 
            //Check for request stage
            IMessageContext.STAGE stage = ((IMessageContext) iContext).getStage();
            if (stage == IMessageContext.STAGE.request) {
                
                Map<String, Object> map = ((IMessageContext) iContext).getAllProperties();
            
                //get the ip
                String ip = (String) map.get("com.bea.contextelement.alsb.router.inbound.request.metadata.http.client-address");

                //get the settings belonging to this ip
                RateLimitSetting setting = rateLimitSettings.get(ip);
                //if no settings found....must be the first call....creat them
                if(setting == null){
                    setting = new RateLimitSetting();
                    System.out.println("Calling ip = " + ip);
                    rateLimitSettings.put(ip, setting);
                }
                
                //first time the service is called....set the time and add 1 to the amount of calls
                if (setting.getCallMoment() == null) {
                    setting.setCallMoment(Calendar.getInstance().getTime());
                    setting.setAmountOfActuallCalls(setting.getAmountOfActuallCalls() + 1);
                    System.out.println(DEBUG_START);
                    System.out.println("First time! " + format.format(setting.getCallMoment()));
                    System.out.println(DEBUG_END);
                } 
                //service has already been called 
                else {
                    Date now = Calendar.getInstance().getTime();
                    //if the time between the last call and now is larger then 60 seconds...reset the values
                    if (now.getTime() - setting.getCallMoment().getTime() > 60000) {
                        setting.setAmountOfActuallCalls(0);
                        setting.setCallMoment(now);
                        System.out.println(DEBUG_START);
                        System.out.println("Reset!" + format.format(now));
                        System.out.println(DEBUG_END);
                        setting.setAmountOfActuallCalls(setting.getAmountOfActuallCalls() + 1);
                        result.setStatus(IResult.SUCCEEDED);
                    } //if this is within the 60 seconds, add 1 to the amount of calls
                    else {
                        //if we are within the limit just continue
                        if (setting.getAmountOfActuallCalls() < limit) {
                            setting.setAmountOfActuallCalls(setting.getAmountOfActuallCalls() + 1);
                            System.out.println(DEBUG_START);
                            System.out.println(setting.getAmountOfActuallCalls());
                            System.out.println(DEBUG_END);
                            result.setStatus(IResult.SUCCEEDED);
                        } //if the amount limit is reached, throw and exception
                        else {
                            System.out.println(DEBUG_START);
                            System.out.println("Limit reached");
                            System.out.println(DEBUG_END);
                            result.setStatus(IResult.FAILED);
                            throw new WSMException("Rate limit reached in 1 minute");
                        }
                    }
                }
            }
        } catch (Exception e) {
            WSMException wsmException = new WSMException(e);
            generateFault(wsmException);
        }
        return result;
    }
}

So first I check the IP of the client…then check if this client has reached the limit of calls within 60 seconds. If so….fail and generate a fault.

Now compile the code into a jar…drop it in your OracleHome\user_projects\domains\my_domain\lib directory. Also upload the policy. Start your soa suite instance and go to the EM. Add your policy by importing it. Don’t forget to rename your jar to zip and remove all other files except your RateLimitingPOlicyFile or else it will not except the import.

ImportPolicy

Next up is to attach the policy to an OSB service and configure it. The default was 5…I made the overwrite to 10 in this case.

ConfigurePolicy

Now lets see if it works.

Call your webservice 10 times within 1 minute:

Limit

Now call it 1 more time:

LimitExceeded

Now wait again until the minute has passed and make another call:

LimitReset

As you can see, is the WSMException wrapped in a weird GenericFault: Generic error with in the caused by the message which we set. Not the most nice way but I haven’t found another way to make this nice.

For the complete code on github see: here

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.