Saturday, April 9, 2022

Adding custom headers filter in WSO2 Micro Gateway

Filters are a set of execution points in the request and response flow that intercept the request before it goes to the backend service and intercepts the response before forwarding it to the client. Filters are applied to all the APIs exposed via Microgateway. Custom filter can be engaged to Microgateway runtime using the Microgateway toolkit. If there is a common functionality required by all the APIs exposed via the Microgateway then a custom filter written in ballerina language can be used for that

Use case:

There are few mandatory headers with maximum character length that need to pass to the API. This filter will not affect if the resource has a "/callback"

Sample Headers: AppId (Length:10),  AppVersion (Length:10), RequestId (Length:36)

Save the below content as header_validation_filter.bal in location <PROJECT_HOME>/extensions folder.

import ballerina/http;

import ballerina/lang.'int;

import ballerina/log;

import ballerina/stringutils;

import ballerina/system;


public type HeaderValidationFilter object {


    public function filterRequest(http:Caller caller, http:Request req, http:FilterContext context) returns boolean {


        log:printInfo("<HeaderValidationFilter> : header validation custom filter enabled");

        // string headersArray = "AppId:10, AppVersion:10, RequestId:36";

        string headersString = system:getEnv("headersArray");

        string[] array = stringutils:split(headersString, ",");

        string resourceName = req.rawPath;

        boolean isCallback = stringutils:contains(resourceName,"callback");

        string? remoteHostName = caller.getRemoteHostName();


        string remoteHost = caller.remoteAddress.host;

        log:printInfo("<HeaderValidationFilter> Resource: " + resourceName);

        log:printInfo("<HeaderValidationFilter> isCallback: " + isCallback.toString());        

        log:printInfo("<HeaderValidationFilter> Client: " + remoteHost+" : "+remoteHostName.toString());

        string xFwrd = req.getHeader("X-FORWARDED-FOR");

        string realIp = req.getHeader("X-Real-Ip");

        log:printInfo("<HeaderValidationFilter> X-FORWARDED-FOR: " + xFwrd);

        log:printInfo("<HeaderValidationFilter> X-realIp: " + realIp);


        if(isCallback){

            return true;

        } 

        foreach string header in array {

            log:printDebug("<HeaderValidationFilter> : validating mandatory header : " + header);

            string[] tempArry = stringutils:split(header, ":");

            string headerKey = tempArry[0].trim();

            int | error headerVal = 'int:fromString(tempArry[1]);


            if (req.hasHeader(headerKey)) {

                string apiHeader = req.getHeader(headerKey);

                if (headerVal is int) {

                    if (apiHeader.length() === 0 || apiHeader.length() > headerVal) {

                        log:printInfo("<HeaderValidationFilter> : header " + headerKey + " validation failed");

                        http:Response res = new;

                        res.statusCode = 400;

                        json payload = {errorCode: "ERR_1000006", errorMessage: "Header validation failure", errorDescription: "Header validation failed for : " + headerKey};

                        res.setJsonPayload(payload);

                        var status = caller->respond(res);

                        return false;

                    }

                }

            } else {

                log:printInfo("<HeaderValidationFilter> : header " + headerKey + " not present");

                http:Response res = new;

                res.statusCode = 400;

                json payload = {errorCode: "ERR_1000001", errorMessage: "Missing mandatory parameters", errorDescription: "One or few mandatory parameters are missing in the request"};

                res.setJsonPayload(payload);

                var status = caller->respond(res);

                return false;

            }

        }

        return true;

    }

    public function filterResponse(http:Response response, http:FilterContext context) returns boolean {

        return true;

    }

};

Define the custom filter in the deployment-config.toml file as below.

[[filters]]

      name = "HeaderValidationFilter"

      position = 2

  • name: Name of the filter object defined in the .bal file. Ex: CustomFiler
  • position: The position of the filter to be engaged. By default, microgateway has the following set of filters. By defining the position it can engage the custom filter in between any default filter chain

Then build the project. Custom filter will be engaged in the runtime

Monday, January 17, 2022

Iterator and aggregator mediator example using call mediator with blocking='true'

This example showcase how to use a call mediator with blocking=true (Synchronous way - wait for the API response and call next) inside the iterator mediator and aggregate response inside the aggregator mediator.

Sample Request payload used:

<Bank>

<Account>21424124214</Account>

<Account>35353553343</Account>

<Account>74564352352</Account>

<Account>86643526546</Account>

<Account>35465626565</Account>

<Account>25454625466</Account>

<Account>23564342424</Account>

<Account>23453654665</Account>

</Bank>

Sample code:

<?xml version="1.0" encoding="UTF-8"?>

<proxy xmlns="http://ws.apache.org/ns/synapse"

       name="test"

       transports="http https"

       startOnLoad="true">

   <description/>

   <target>

      <inSequence>

         <property name="countRequests"

                   expression="count(//Account)"

                   scope="default"

                   type="STRING"

                   description="countRequests"/>

         <iterate id="IterateAccountCreate" expression="//Account" sequential="true">

            <target>

               <sequence>

                  <payloadFactory media-type="xml">

                     <format>

                        <echo:echoString xmlns:echo="http://echo.services.core.carbon.wso2.org">

                           <in xmlns="">$1</in>

                        </echo:echoString>

                     </format>

                     <args>

                        <arg evaluator="xml" expression="//Account/text()"/>

                     </args>

                  </payloadFactory>

                  <property name="TRANSPORT_HEADERS"

                            scope="axis2"

                            action="remove"

                            description="TRANSPORT_HEADERS"/>

                  <property name="OperationName"

                            value="echoString"

                            scope="default"

                            type="STRING"

                            description="OperationName"/>

                  <header name="To" scope="default" action="remove"/>

                  <header name="Action" scope="default" value="urn:echoString"/>

                  <header name="SOAPAction" scope="transport" value="urn:echoString"/>

                  <log category="DEBUG" description="***Request***">

                     <property name="request" expression="$body"/>

                  </log>

                  <call blocking="true">

                     <endpoint>

                        <http method="POST" uri-template="http://prabod:8313/services/echo"/>

                     </endpoint>

                  </call>

                  <log category="DEBUG"

                       separator="***Response***"

                       description="***Response***">

                     <property name="create_instanties_response" expression="$body"/>

                  </log>

  <loopback/>

               </sequence>

            </target>

         </iterate>

      </inSequence>

      <outSequence>

<aggregate description="Aggregate response" id="IterateAccountCreate">

<completeCondition timeout="10">

</completeCondition>

<onComplete expression="//ns:echoStringResponse"

xmlns:ns="http://echo.services.core.carbon.wso2.org">

<log description="***aggregated***" separator="***aggregated***">

<property expression="$body" name="aggregated_body" />

</log>

<respond/>

</onComplete>

</aggregate>

      </outSequence>

   </target>

</proxy>

In this example, I'm calling the mock backend to test the scenario.
http://prabod:8313/services/echo

This will iterate all the account numbers and call the backend by passing the account number inside the iterator mediator. Finally, aggregate all the responses into one response payload inside aggregator mediator. In this scenario, I need this to be synchronous. In that case, we need to use the call mediator with blocking="true" and the aggregator mediator needs to be inside out-sequence.