Servlet filter plugins

Plugin servlet filter hook classes operate on a search request before it begins Funnelback processing, and on the search response, before it is processed by Funnelback and also after result rendering, just before a response is returned to the user.

Search lifecycle plugins are suitable for must use cases and are much easier to work with, however in some advanced cases servlet filters are required.

One specific capability enabled by this type of hook is auditing or direct manipulation of the specific byte output returned to Funnelback users.

When developing custom servlet filter hooks it will be useful to be familiar with the Java servlet filter mechanism, on which this mechanism is based.

Plugin scopes

The plugin scope for a plugin that implements servlet filtering must include the runs on results page scope.

Interface methods

To implement a custom servlet filter hook, you will need to implement the SearchServletFilterHook interface.

SearchServletFilterHook provides three different methods:

ServletRequest preFilterRequest(SearchServletFilterHookContext context, ServletRequest request)

Allows the SearchServletFilterHook implementation to perform actions on the request before it is populated by Funnelback.

ServletResponse preFilterResponse(SearchServletFilterHookContext context, ServletRequest request, ServletResponse response)

Allows the SearchServletFilterHook implementation to perform actions on the response before it is populated by Funnelback.

void postFilterResponse(SearchServletFilterHookContext context, ServletRequest request, ServletResponse response)

Allows the SearchServletFilterHook implementation to perform actions after Funnelback has processed the request.

The methods provided by this interface, preFilterRequest, preFilterResponse and postFilterResponse, allow the hook class to intercept and alter the ServletRequest and ServletResponse objects as necessary. There is also access to the profile configuration for the request, via SearchServletFilterHookContext#getCurrentProfileConfig()

Access to the Funnelback search transaction object model is not available from within this hook class, and that all requests to Funnelback’s search interface will be processed through these methods, so the performance of their implementations must be considered during development.

Accessing plugin configuration

You can define configuration keys that can be set in the results page configuration to configure your plugin.

Configuration keys are read from the data model using the getCurrentProfileConfig() method:

...
    String modeConfig = context.getCurrentProfileConfig().get(pluginUtils.MODE_KEY.getKey());
...

Example: x-forwarded-for-edit

Manipulates the X-Forwarded-For header of HTTP Search Requests.

XForwardedForEditSearchServletFilterPlugin.java
package com.funnelback.plugin.xforwardedforedit;

import com.funnelback.plugin.servlet.filter.SearchServletFilterHook;
import com.funnelback.plugin.servlet.filter.SearchServletFilterHookContext;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.funnelback.plugin.xforwardedforedit.HttpServletRequestXForwardedForWrapper.getMode;

public class XForwardedForEditSearchServletFilterPlugin implements SearchServletFilterHook {

    final PluginUtils pluginUtils = new PluginUtils();
    private static final Logger log = LogManager.getLogger(XForwardedForEditSearchServletFilterPlugin.class);

    @Override
    public ServletRequest preFilterRequest(SearchServletFilterHookContext context, ServletRequest request) {
        if (request instanceof HttpServletRequest) {
            String modeConfig = context.getCurrentProfileConfig().get(pluginUtils.MODE_KEY.getKey());
            Optional<HttpServletRequestXForwardedForWrapper.Mode> mode = getMode(modeConfig);

            if (mode.isPresent()) {
                log.debug("wrapping HttpServletRequest");
                return new HttpServletRequestXForwardedForWrapper(mode.get(), (HttpServletRequest) request);
            }

            log.warn("Could not parse mode from config key '{}', expected one of '{}' but got '{}'",
                    pluginUtils.MODE_KEY.getKey(),
                    Arrays.stream(HttpServletRequestXForwardedForWrapper.Mode.values())
                            .map(HttpServletRequestXForwardedForWrapper.Mode::name)
                            .collect(Collectors.joining(",")),
                    modeConfig);
        }

        return request;
    }
}
HttpServletRequestXForwardedForWrapper.java
package com.funnelback.plugin.xforwardedforedit;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Class to manipulate the X-Forwarded-For header of an HTTP request
 *
 * Allows removal of the first or last value of the X-Forwarded-For
 * or removal of all values but the first
 */
public class HttpServletRequestXForwardedForWrapper extends HttpServletRequestWrapper {
    static final String XFF_HEADER = "X-Forwarded-For";
    final Mode mode; // Mode of operation

    private static final Logger log = LogManager.getLogger(HttpServletRequestXForwardedForWrapper.class);

    /**
     * Possible modes of operation
     */
    public enum Mode {
        /** Remove first value of the X-Forwarded-For */
        RemoveFirst,
        /** Remove last value of the X-Forwarded-For */
        RemoveLast,
        /** Keep only the first value of the X-Forwarded-For */
        KeepFirst,
    }

    public HttpServletRequestXForwardedForWrapper(Mode mode, HttpServletRequest request) {
        super(request);
        this.mode = mode;
    }

    public static Optional<Mode> getMode(String modeString) {
        for (Mode mode : Mode.values()) {
            if (mode.name().equalsIgnoreCase(modeString)) {
                return Optional.of(mode);
            }
        }
        return Optional.empty();
    }

    /**
     * Check if the requested header is X-Forwarded-For, and if it is
     * alter the value according to the mode
     *
     * @param name Name of the header to retrieve
     * @return Header value, possibly altered
     */
    @Override
    public String getHeader(String name) {
        if (name.equalsIgnoreCase(XFF_HEADER)) {
            String oldHeader = super.getHeader(name);
            String updatedHeader = "";

            if (oldHeader != null && oldHeader.contains(",")) {
                List <String> xffEntries = List.of(oldHeader.split(","));
                try {
                    switch (mode) {
                        case RemoveFirst:
                            updatedHeader = xffEntries.subList(1, xffEntries.size())
                                .stream().collect(Collectors.joining(","));
                            break;
                        case RemoveLast:
                            updatedHeader = xffEntries.subList(0, xffEntries.size() - 1)
                                .stream().collect(Collectors.joining(","));
                            break;
                        case KeepFirst:
                            updatedHeader = xffEntries.get(0);
                            break;
                    }
                    log.debug("Changed '{}' value from '{}' to '{}'", XFF_HEADER, oldHeader, updatedHeader);
                    return updatedHeader;
                } catch (Exception e) {
                    log.error("Error while altering ${XFF_HEADER} value '{}'", oldHeader, e);
                }
            }
        }

        return super.getHeader(name);
    }

}
XForwardedForEditSearchServletFilterPluginTest.java
package com.funnelback.plugin.xforwardedforedit;

import com.funnelback.plugin.servlet.filter.SearchServletFilterHook;
import com.funnelback.plugin.servlet.filter.SearchServletFilterHookContext;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.funnelback.plugin.xforwardedforedit.HttpServletRequestXForwardedForWrapper.getMode;

public class XForwardedForEditSearchServletFilterPlugin implements SearchServletFilterHook {

    final PluginUtils pluginUtils = new PluginUtils();
    private static final Logger log = LogManager.getLogger(XForwardedForEditSearchServletFilterPlugin.class);

    @Override
    public ServletRequest preFilterRequest(SearchServletFilterHookContext context, ServletRequest request) {
        if (request instanceof HttpServletRequest) {
            String modeConfig = context.getCurrentProfileConfig().get(pluginUtils.MODE_KEY.getKey());
            Optional<HttpServletRequestXForwardedForWrapper.Mode> mode = getMode(modeConfig);

            if (mode.isPresent()) {
                log.debug("wrapping HttpServletRequest");
                return new HttpServletRequestXForwardedForWrapper(mode.get(), (HttpServletRequest) request);
            }

            log.warn("Could not parse mode from config key '{}', expected one of '{}' but got '{}'",
                    pluginUtils.MODE_KEY.getKey(),
                    Arrays.stream(HttpServletRequestXForwardedForWrapper.Mode.values())
                            .map(HttpServletRequestXForwardedForWrapper.Mode::name)
                            .collect(Collectors.joining(",")),
                    modeConfig);
        }

        return request;
    }
}