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.
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.
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.
Now lets see if it works.
Call your webservice 10 times within 1 minute:
Now call it 1 more time:
Now wait again until the minute has passed and make another call:
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