Skip to content

GSIP 72

Jody Garnett edited this page Jul 12, 2017 · 1 revision

GSIP 72 - Control Flow Module Enhancements

Overview

Extend the control flow module to include ip based filtering and throttling of concurrent requests

Proposed By

Juan Marin

Assigned to Release

The release that this proposal will be implemented trunk, to be included in a future release (tentatively 2.2)

State

Choose one of: Under Discussion, In Progress, Completed, Rejected, Deferred

Motivation

Currently the control flow module allows GeoServer administrators to throttle ows services (or ows service calls, such as WMS GetMap request), as well as users. The current implementation of user based throttling relies on identifying them through cookies, which works well but for browser based clients but not for others. This proposal is meant to extend the control flow module to include filtering of ip addresses and throttling of concurrent requests coming from the same ip address.

Proposal

The use cases that motivate this proposal are as follows:

\1. Establish global limits for number of concurrent requests from a single IP address. A single IP address may only take up to n number of requests in parallel.

Users need to add the following to controlflow.properties to configure this option:

ip=

Where is the maximum number of requests a single IP address can execute in parallel.

Since this is very similar to the current UserControllerFlow implementation, this proposal includes a bit of refactoring to include common code in a parent abstract class that both UserFlowController and IpFlowController extend from:

public abstract class QueueController implements FlowController {

    static ThreadLocal<String> QUEUE_ID = new ThreadLocal<String>();

    int queueSize;

    Map<String, TimedBlockingQueue> queues = new ConcurrentHashMap<String, TimedBlockingQueue>();

    @Override
    public boolean requestIncoming(Request request, long timeout) {
        return false;
    }

    @Override
    public void requestComplete(Request request) {
        String queueId = QUEUE_ID.get();
        QUEUE_ID.remove();
        BlockingQueue<Request> queue = queues.get(queueId);
        if (queue != null)
            queue.remove(request);
    }

    @Override
    public int getPriority() {
        return queueSize;
    }

    @SuppressWarnings("serial")
    protected static class TimedBlockingQueue extends ArrayBlockingQueue<Request> {
        long lastModified;

        public TimedBlockingQueue(int capacity, boolean fair) {
            super(capacity, fair);
        }

        @Override
        public void put(Request o) throws InterruptedException {
            super.put(o);
            lastModified = System.currentTimeMillis();
        }

        @Override
        public boolean remove(Object o) {
            lastModified = System.currentTimeMillis();
            return super.remove(o);
        }

    }

}

The implementation for IpFlowController is as follows:

public class IpFlowController extends QueueController {

    /**
     * A flow controller that throttles concurrent requests made from the same ip (any ip)
     *
     * @author Juan Marin, OpenGeo
     */

    static final Logger LOGGER = Logging.getLogger(IpFlowController.class);

    public IpFlowController(int queueSize) {
        this.queueSize = queueSize;
    }

    protected List<String> ipAddresses = new ArrayList<String>();

    @Override
    public boolean requestIncoming(Request request, long timeout) {
        boolean retval = true;
        // check if this client already made other connections
        String incomingIp = "";
        String ip = request.getHttpRequest().getRemoteAddr();
        if (ipAddresses.size() > 0) {
            for (String ipAddress : ipAddresses) {
                if (ipAddress.equals(ip)) {
                    incomingIp = ipAddress;
                    break;
                }
            }
        }

        if (incomingIp.equals("")) {
            incomingIp = ip;
        }

        // see if we have that queue already
        TimedBlockingQueue queue = null;
        if (incomingIp != null && !incomingIp.equals("")) {
            queue = queues.get(incomingIp);
        }

        // generate a unique queue id for this client if none was found
        if (queue == null) {
            queue = new TimedBlockingQueue(queueSize, true);
            queues.put(incomingIp, queue);
        }
        QUEUE_ID.set(incomingIp);
        ipAddresses.add(incomingIp);

        // queue token handling
        try {
            if (timeout > 0) {
                retval = queue.offer(request, timeout, TimeUnit.MILLISECONDS);
            } else {
                queue.put(request);
            }
        } catch (InterruptedException e) {
            LOGGER.log(Level.WARNING, "Unexpected interruption while "
                    + "blocking on the request queue");
        }
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("IpFlowController(" + queueSize + "," + incomingIp + ") queue size "
                    + queue.size());
            LOGGER.fine("IpFlowController(" + queueSize + "," + incomingIp + ") total queues "
                    + queues.size());
        }
        return retval;

    }

\2. Specify limits for the number of requests that a particular IP address can take, same as above but specific to a particular IP

Users would add the following to controlflow.properties in order to enable this feature:

ip.address=,<ip_addr>

Where is the maximum number of requests the ip speficied in <ip_addr> will execute in parallel.

The class implementing this functionality extends from IpFlowController and overrides the requestIncoming method as follows:

public class SingleIpFlowController extends IpFlowController {

    public SingleIpFlowController(int queueSize) {
        super(queueSize);
    }

    public SingleIpFlowController(int queueSize, String ip) {
        super(queueSize);
        ipAddresses.add(ip);
    }

    @Override
    public boolean requestIncoming(Request request, long timeout) {
        boolean retval = true;
        String incomingIp = request.getHttpRequest().getRemoteAddr();
        if (incomingIp.equals(ipAddresses.get(0))) {
            TimedBlockingQueue queue = null;
            if (incomingIp != null && !incomingIp.equals("")) {
                queue = queues.get(incomingIp);
            }

            if (queue == null) {
                queue = new TimedBlockingQueue(queueSize, true);
                queues.put(incomingIp, queue);
            }
            QUEUE_ID.set(incomingIp);

            try {
                if (timeout > 0) {
                    retval = queue.offer(request, timeout, TimeUnit.MILLISECONDS);
                } else {
                    queue.put(request);
                }
            } catch (InterruptedException e) {
                LOGGER.log(Level.WARNING, "Unexpected interruption while "
                        + "blocking on the request queue");
            }
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("SingleIpFlowController(" + queueSize + "," + incomingIp + ") queue size "
                        + queue.size());
                LOGGER.fine("SingleIpFlowController(" + queueSize + "," + incomingIp + ") total queues "
                        + queues.size());
            }
        }
        return retval;
    }

}

\3. IP blacklist, which would reject requests coming from specific IP addresses.

To do this, the following would have to be added to controlflow.properties:

ip.blacklist=<ip_addr1>,<ip_addr2>,…

Where <ip_addr1>, <ip_addr2> etc. are the individual IP addresses that need to be blocked

The implementing class will be a filter that will act on blacklisted IP addresses:

public class IpBlacklistFilter implements GeoServerFilter {

 @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        isBlocked = false;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String incomingIp = httpRequest.getRemoteAddr();
        if (response instanceof HttpServletResponse) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            for (String ipAddress : ipAddresses) {
                if (ipAddress.equals(incomingIp)) {
                    httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN,
                            "This IP has been blocked. Please contact the server administrator");
                    isBlocked = true;
                    break;
                }
            }
            if (!isBlocked) {
                chain.doFilter(request, response);
            }
        }

    }

}

All new functionality is covered by automated unit tests and will be load tested for proper behavior where relevant (IP throttling).

Feedback

This section should contain feedback provided by PSC members who may have a problem with the proposal.

Backwards Compatibility

No backwards compatibility issues. New implementing classes will integrate with existing logic without affecting it.

Voting

Andrea Aime: Alessio Fabiani: Ben Caradoc Davies: Gabriel Roldan: Justin Deoliveira: Jody Garnett: Mark Leslie: Rob Atkinson: Simone Giannecchini:

Links

JIRA Task Email Discussion Wiki Page

Clone this wiki locally